Nach der Transkription eines Dokuments werden referenzierte externe
Dokumente (AGB, Verhaltenskodex) erkannt, sicher heruntergeladen und ihr
Text dem Hauptdokument fuer die "Summen"-Bewertung angehaengt (Option D).
Die Referenzen werden zusaetzlich als eigene, herunterladbare Zeilen
gespeichert.
- DB: dc_project_documents um parent_document_id, is_reference, source_url
erweitert (DC_REFERENCES_ALTER.sql)
- ORDS: Dokumentenliste um die Felder erweitert; neue Endpunkte
POST /documents/:id/references und DELETE /projects/:id/references;
q-Quote-Parserfehler im ai-costs-Handler ('[]') behoben
- Backend: ReferenceDownloadService (SSRF-sicher via DNS-/Public-IP-
Validierung, Redirect-Pruefung, Groessen-/Timeout-Limits, jsoup fuer HTML),
EvaluationService.extractReferenceUrls (LLM-basierte URL-Erkennung),
Orchestrierung (Discovery in Phase A, Referenz-Skip + combinedText in Phase B)
- Config: dc.references.* in application.properties
Verifiziert an Projekt 144: AGB-PDF/-HTML heruntergeladen, verknuepft,
"Summen"-Bewertung zitiert AGB-Klauseln aus dem Referenzdokument.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1683 lines
60 KiB
MySQL
1683 lines
60 KiB
MySQL
-- =============================================================================
|
||
-- ORDS REST Services: Dokumenten-Check
|
||
-- Modul: frigosped.dc
|
||
-- Base-Path: /api/dc/
|
||
-- =============================================================================
|
||
-- Voraussetzungen:
|
||
-- - Oracle Database 23c mit installiertem ORDS (>= 23.x)
|
||
-- - Script wird als Schema-Owner ausgeführt (hat ORDS_METADATA-Rolle)
|
||
-- - Datenbankschema bereits angelegt via "Datenmodell DocumentCheck.sql"
|
||
--
|
||
-- Deployment:
|
||
-- sqlplus user/pass@db @"Scripts/ORDS REST Services.sql"
|
||
--
|
||
-- Das Skript ist idempotent: bestehende Modul-Definition wird gelöscht und
|
||
-- neu erstellt. Bestehende Daten bleiben unberührt.
|
||
--
|
||
-- Endpunkt-Übersicht:
|
||
-- 1. GET /api/dc/catalogs/ Alle Fragenkataloge
|
||
-- 2. GET /api/dc/catalogs/:id/questions/ Fragen eines Katalogs (flach, nach order_nr sortiert)
|
||
-- 3. GET /api/dc/projects/:id Projektdetail (inkl. Detailstatus + ETA)
|
||
-- 4. POST /api/dc/projects/:id/start → IN_PROGRESS (setzt processing_started_at)
|
||
-- 5. PUT /api/dc/projects/:id/progress Fortschritt + Phase/Doc/Question/ETA
|
||
-- 6. POST /api/dc/projects/:id/complete → COMPLETED (setzt current_phase=COMPLETED)
|
||
-- 7. GET /api/dc/projects/:id/documents/ Dokument-Liste (kein BLOB)
|
||
-- 8. GET /api/dc/documents/:id/file BLOB-Download (PLSQL-Workaround)
|
||
-- 9. GET /api/dc/documents/:id/texts OCR-Text + Übersetzung lesen (für Resume)
|
||
-- 9b. PUT /api/dc/documents/:id/texts OCR-Text + Übersetzung schreiben
|
||
-- 10. POST /api/dc/projects/:id/results Prüfergebnis einfügen
|
||
-- 11. DELETE /api/dc/projects/:id/results Alle Ergebnisse löschen
|
||
-- =============================================================================
|
||
|
||
-- =============================================================================
|
||
-- BLOCK 1: Modul, Templates und Handler
|
||
-- =============================================================================
|
||
BEGIN
|
||
|
||
-- -------------------------------------------------------------------------
|
||
-- Idempotenz: Modul löschen falls vorhanden
|
||
-- -------------------------------------------------------------------------
|
||
BEGIN
|
||
ORDS.DELETE_MODULE(p_module_name => 'frigosped.dc');
|
||
EXCEPTION
|
||
WHEN OTHERS THEN NULL;
|
||
END;
|
||
|
||
-- -------------------------------------------------------------------------
|
||
-- Modul definieren
|
||
-- -------------------------------------------------------------------------
|
||
ORDS.DEFINE_MODULE(
|
||
p_module_name => 'frigosped.dc',
|
||
p_base_path => '/api/dc/',
|
||
p_items_per_page => 0, -- Pagination deaktiviert (Quarkus steuert selbst)
|
||
p_status => 'PUBLISHED',
|
||
p_comments => 'Dokumenten-Check Backend-API fuer den Quarkus-Verarbeitungsserver'
|
||
);
|
||
|
||
|
||
-- ===========================================================================
|
||
-- TEMPLATE 1: catalogs/
|
||
-- ===========================================================================
|
||
ORDS.DEFINE_TEMPLATE(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'catalogs/',
|
||
p_priority => 0,
|
||
p_etag_type => 'HASH',
|
||
p_comments => 'Alle Fragenkataloge mit Dokumenttyp-Name'
|
||
);
|
||
|
||
-- 1. GET /api/dc/catalogs/
|
||
ORDS.DEFINE_HANDLER(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'catalogs/',
|
||
p_method => 'GET',
|
||
p_source_type => ORDS.SOURCE_TYPE_COLLECTION_FEED,
|
||
p_items_per_page => 0,
|
||
p_comments => 'Gibt alle Kataloge mit Dokumenttyp-Name zurueck',
|
||
p_source => q'[
|
||
SELECT
|
||
c.id,
|
||
c.name,
|
||
c.description,
|
||
c.document_type_id,
|
||
dt.name AS document_type_name,
|
||
c.row_version,
|
||
c.created,
|
||
c.updated
|
||
FROM dc_question_catalogs c
|
||
JOIN dc_document_types dt ON dt.id = c.document_type_id
|
||
ORDER BY c.name
|
||
]'
|
||
);
|
||
|
||
|
||
-- ===========================================================================
|
||
-- TEMPLATE 2: catalogs/:catalog_id/questions/
|
||
-- ===========================================================================
|
||
ORDS.DEFINE_TEMPLATE(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'catalogs/:catalog_id/questions/',
|
||
p_priority => 0,
|
||
p_etag_type => 'HASH',
|
||
p_comments => 'Alle Fragen eines Katalogs (flach mit Kategorie-Info)'
|
||
);
|
||
|
||
-- 2. GET /api/dc/catalogs/:catalog_id/questions/
|
||
ORDS.DEFINE_HANDLER(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'catalogs/:catalog_id/questions/',
|
||
p_method => 'GET',
|
||
p_source_type => ORDS.SOURCE_TYPE_COLLECTION_FEED,
|
||
p_items_per_page => 0,
|
||
p_comments => 'Flacher Join: Kategorie + Frage-Spalten, nach Kategorie/ID sortiert',
|
||
p_source => q'[
|
||
SELECT
|
||
q.id AS question_id,
|
||
cat.id AS category_id,
|
||
cat.name AS category_name,
|
||
cat.description AS category_description,
|
||
cat.order_nr AS category_order_nr,
|
||
q.question_text,
|
||
q.evaluation_type,
|
||
q.threshold,
|
||
q.result_handling,
|
||
q.example_0_percent,
|
||
q.example_100_percent,
|
||
q.order_nr,
|
||
q.row_version
|
||
FROM dc_questions q
|
||
JOIN dc_question_categories cat ON cat.id = q.category_id
|
||
WHERE cat.catalog_id = :catalog_id
|
||
ORDER BY cat.order_nr, q.order_nr, q.id
|
||
]'
|
||
);
|
||
|
||
|
||
-- ===========================================================================
|
||
-- TEMPLATE 3: projects/:project_id (kein Trailing-Slash = Einzel-Ressource)
|
||
-- ===========================================================================
|
||
ORDS.DEFINE_TEMPLATE(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'projects/:project_id',
|
||
p_priority => 0,
|
||
p_etag_type => 'HASH',
|
||
p_comments => 'Einzelnes Projekt'
|
||
);
|
||
|
||
-- 3. GET /api/dc/projects/:project_id
|
||
ORDS.DEFINE_HANDLER(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'projects/:project_id',
|
||
p_method => 'GET',
|
||
p_source_type => ORDS.SOURCE_TYPE_COLLECTION_ITEM,
|
||
p_items_per_page => 1,
|
||
p_comments => 'Projektdetail mit Status-Details und ETA; 404 wenn nicht gefunden',
|
||
p_source => q'[
|
||
SELECT
|
||
p.id,
|
||
p.name,
|
||
p.description,
|
||
p.catalog_id,
|
||
p.created_by_user,
|
||
p.status,
|
||
p.progress,
|
||
p.completed_at,
|
||
p.notification_email,
|
||
p.row_version,
|
||
p.created,
|
||
p.updated,
|
||
p.processing_started_at,
|
||
p.current_phase,
|
||
p.current_doc_id,
|
||
p.current_question_id,
|
||
p.estimated_completion_at,
|
||
p.notify_ok,
|
||
p.notify_hinweis,
|
||
p.notify_abweichend
|
||
FROM dc_projects p
|
||
WHERE p.id = :project_id
|
||
]'
|
||
);
|
||
|
||
|
||
-- ===========================================================================
|
||
-- TEMPLATE 4: projects/:project_id/start
|
||
-- ===========================================================================
|
||
ORDS.DEFINE_TEMPLATE(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'projects/:project_id/start',
|
||
p_priority => 0,
|
||
p_comments => 'Projekt-Status auf IN_PROGRESS setzen'
|
||
);
|
||
|
||
-- 4. POST /api/dc/projects/:project_id/start
|
||
ORDS.DEFINE_HANDLER(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'projects/:project_id/start',
|
||
p_method => 'POST',
|
||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||
p_comments => '200 OK; 409 wenn IN_PROGRESS/COMPLETED; 404 wenn Projekt fehlt',
|
||
p_source => q'[
|
||
DECLARE
|
||
v_status dc_projects.status%TYPE;
|
||
BEGIN
|
||
SELECT status
|
||
INTO v_status
|
||
FROM dc_projects
|
||
WHERE id = :project_id
|
||
FOR UPDATE NOWAIT;
|
||
|
||
IF v_status IN ('IN_PROGRESS', 'COMPLETED') THEN
|
||
:status_code := 409;
|
||
HTP.P('{"error":"Projekt ist bereits ' || v_status || '",'
|
||
|| '"current_status":"' || v_status || '"}');
|
||
RETURN;
|
||
END IF;
|
||
|
||
UPDATE dc_projects
|
||
SET status = 'IN_PROGRESS',
|
||
progress = 0,
|
||
processing_started_at = SYSTIMESTAMP,
|
||
current_phase = 'OCR',
|
||
current_doc_id = NULL,
|
||
current_question_id = NULL,
|
||
estimated_completion_at = NULL
|
||
WHERE id = :project_id;
|
||
|
||
:status_code := 200;
|
||
HTP.P('{"project_id":' || :project_id
|
||
|| ',"status":"IN_PROGRESS"}');
|
||
|
||
EXCEPTION
|
||
WHEN NO_DATA_FOUND THEN
|
||
:status_code := 404;
|
||
HTP.P('{"error":"Projekt nicht gefunden","project_id":' || :project_id || '}');
|
||
WHEN OTHERS THEN
|
||
ROLLBACK;
|
||
:status_code := 500;
|
||
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
|
||
END;
|
||
]'
|
||
);
|
||
|
||
|
||
-- ===========================================================================
|
||
-- TEMPLATE 5: projects/:project_id/progress
|
||
-- ===========================================================================
|
||
ORDS.DEFINE_TEMPLATE(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'projects/:project_id/progress',
|
||
p_priority => 0,
|
||
p_comments => 'Verarbeitungsfortschritt aktualisieren'
|
||
);
|
||
|
||
-- 5. PUT /api/dc/projects/:project_id/progress
|
||
-- Body: {"progress":45,"current_phase":"QUESTIONS","current_doc_id":2,
|
||
-- "current_question_id":15,"estimated_completion_at":"2026-04-27T16:30:00"}
|
||
ORDS.DEFINE_HANDLER(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'projects/:project_id/progress',
|
||
p_method => 'PUT',
|
||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||
p_comments => 'Setzt progress (0-100) + Phase/Dokument/Frage/ETA; 400 bei ungueltigem Wert',
|
||
p_source => q'[
|
||
DECLARE
|
||
v_body CLOB;
|
||
v_progress NUMBER;
|
||
v_phase VARCHAR2(30);
|
||
v_doc_id NUMBER;
|
||
v_question_id NUMBER;
|
||
v_eta_str VARCHAR2(30);
|
||
v_eta TIMESTAMP;
|
||
BEGIN
|
||
v_body := :body_text;
|
||
v_progress := TO_NUMBER(JSON_VALUE(v_body, '$.progress'));
|
||
v_phase := JSON_VALUE(v_body, '$.current_phase');
|
||
v_doc_id := TO_NUMBER(JSON_VALUE(v_body, '$.current_doc_id'));
|
||
v_question_id := TO_NUMBER(JSON_VALUE(v_body, '$.current_question_id'));
|
||
v_eta_str := JSON_VALUE(v_body, '$.estimated_completion_at');
|
||
|
||
IF v_progress IS NULL OR v_progress < 0 OR v_progress > 100 THEN
|
||
:status_code := 400;
|
||
HTP.P('{"error":"progress muss eine Zahl zwischen 0 und 100 sein"}');
|
||
RETURN;
|
||
END IF;
|
||
|
||
IF v_eta_str IS NOT NULL THEN
|
||
BEGIN
|
||
v_eta := TO_TIMESTAMP(v_eta_str, 'YYYY-MM-DD"T"HH24:MI:SS');
|
||
EXCEPTION WHEN OTHERS THEN
|
||
v_eta := NULL;
|
||
END;
|
||
END IF;
|
||
|
||
UPDATE dc_projects
|
||
SET progress = v_progress,
|
||
current_phase = NVL(v_phase, current_phase),
|
||
current_doc_id = v_doc_id,
|
||
current_question_id = v_question_id,
|
||
estimated_completion_at = NVL(v_eta, estimated_completion_at)
|
||
WHERE id = :project_id;
|
||
|
||
IF SQL%ROWCOUNT = 0 THEN
|
||
:status_code := 404;
|
||
HTP.P('{"error":"Projekt nicht gefunden","project_id":' || :project_id || '}');
|
||
RETURN;
|
||
END IF;
|
||
|
||
:status_code := 200;
|
||
HTP.P('{"project_id":' || :project_id
|
||
|| ',"progress":' || v_progress || '}');
|
||
|
||
EXCEPTION
|
||
WHEN VALUE_ERROR THEN
|
||
:status_code := 400;
|
||
HTP.P('{"error":"progress muss eine gueltige Zahl sein"}');
|
||
WHEN OTHERS THEN
|
||
ROLLBACK;
|
||
:status_code := 500;
|
||
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
|
||
END;
|
||
]'
|
||
);
|
||
|
||
|
||
-- ===========================================================================
|
||
-- TEMPLATE 6: projects/:project_id/complete
|
||
-- ===========================================================================
|
||
ORDS.DEFINE_TEMPLATE(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'projects/:project_id/complete',
|
||
p_priority => 0,
|
||
p_comments => 'Projekt auf COMPLETED setzen'
|
||
);
|
||
|
||
-- 6. POST /api/dc/projects/:project_id/complete
|
||
ORDS.DEFINE_HANDLER(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'projects/:project_id/complete',
|
||
p_method => 'POST',
|
||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||
p_comments => 'Setzt status=COMPLETED, speichert Java-Markdown, sendet pro Dokument ein PDF per E-Mail',
|
||
p_source => q'[
|
||
DECLARE
|
||
v_status dc_projects.status%TYPE;
|
||
v_now DATE := SYSDATE;
|
||
v_report_md CLOB;
|
||
v_doc_pdfs JSON_ARRAY_T;
|
||
|
||
-- Konvertiert den BLOB-Request-Body (UTF-8 JSON) in einen CLOB
|
||
FUNCTION body_to_clob(p_blob IN BLOB) RETURN CLOB IS
|
||
v_clob CLOB;
|
||
v_dest INTEGER := 1;
|
||
v_src INTEGER := 1;
|
||
v_lang INTEGER := DBMS_LOB.DEFAULT_LANG_CTX;
|
||
v_warning INTEGER;
|
||
BEGIN
|
||
IF p_blob IS NULL OR DBMS_LOB.GETLENGTH(p_blob) = 0 THEN RETURN NULL; END IF;
|
||
DBMS_LOB.CREATETEMPORARY(v_clob, TRUE);
|
||
DBMS_LOB.CONVERTTOCLOB(v_clob, p_blob, DBMS_LOB.LOBMAXSIZE,
|
||
v_dest, v_src, NLS_CHARSET_ID('AL32UTF8'), v_lang, v_warning);
|
||
RETURN v_clob;
|
||
END body_to_clob;
|
||
|
||
-- Dekodiert einen Base64-CLOB in einen BLOB (chunk-weise, 24000 Zeichen pro Schritt)
|
||
FUNCTION base64_to_blob(p_b64 IN CLOB) RETURN BLOB IS
|
||
v_blob BLOB;
|
||
v_raw RAW(32767);
|
||
v_len INTEGER;
|
||
v_chunk INTEGER := 24000;
|
||
v_offset INTEGER := 1;
|
||
v_sub VARCHAR2(32767);
|
||
BEGIN
|
||
DBMS_LOB.CREATETEMPORARY(v_blob, TRUE);
|
||
v_len := DBMS_LOB.GETLENGTH(p_b64);
|
||
WHILE v_offset <= v_len LOOP
|
||
v_sub := DBMS_LOB.SUBSTR(p_b64, v_chunk, v_offset);
|
||
v_raw := UTL_ENCODE.BASE64_DECODE(UTL_RAW.CAST_TO_RAW(v_sub));
|
||
DBMS_LOB.WRITEAPPEND(v_blob, UTL_RAW.LENGTH(v_raw), v_raw);
|
||
v_offset := v_offset + v_chunk;
|
||
END LOOP;
|
||
RETURN v_blob;
|
||
END base64_to_blob;
|
||
|
||
BEGIN
|
||
-- JSON-Body parsen: report_markdown + document_pdfs (Array)
|
||
DECLARE
|
||
v_body_clob CLOB;
|
||
v_json JSON_OBJECT_T;
|
||
BEGIN
|
||
v_body_clob := body_to_clob(:body);
|
||
IF v_body_clob IS NOT NULL THEN
|
||
v_json := JSON_OBJECT_T.parse(v_body_clob);
|
||
v_report_md := v_json.get_clob('report_markdown');
|
||
BEGIN
|
||
v_doc_pdfs := v_json.get_array('document_pdfs');
|
||
EXCEPTION WHEN OTHERS THEN v_doc_pdfs := NULL; END;
|
||
DBMS_LOB.FREETEMPORARY(v_body_clob);
|
||
END IF;
|
||
EXCEPTION WHEN OTHERS THEN NULL;
|
||
END;
|
||
|
||
SELECT status
|
||
INTO v_status
|
||
FROM dc_projects
|
||
WHERE id = :project_id
|
||
FOR UPDATE NOWAIT;
|
||
|
||
IF v_status = 'COMPLETED' THEN
|
||
:status_code := 409;
|
||
HTP.P('{"error":"Projekt ist bereits COMPLETED","project_id":' || :project_id || '}');
|
||
RETURN;
|
||
END IF;
|
||
|
||
UPDATE dc_projects
|
||
SET status = 'COMPLETED',
|
||
completed_at = v_now,
|
||
progress = 100,
|
||
current_phase = 'COMPLETED',
|
||
current_doc_id = NULL,
|
||
current_question_id = NULL,
|
||
estimated_completion_at = NULL
|
||
WHERE id = :project_id;
|
||
|
||
IF v_report_md IS NOT NULL AND DBMS_LOB.GETLENGTH(v_report_md) > 0 THEN
|
||
-- Kombinierten Markdown-Bericht speichern
|
||
UPDATE dc_projects SET report_markdown = v_report_md WHERE id = :project_id;
|
||
|
||
-- E-Mail mit je einem PDF-Anhang pro Dokument versenden
|
||
BEGIN
|
||
DECLARE
|
||
v_email dc_projects.notification_email%TYPE;
|
||
v_name dc_projects.name%TYPE;
|
||
v_done dc_projects.completed_at%TYPE;
|
||
v_mail_id NUMBER;
|
||
v_ok_cnt NUMBER := 0;
|
||
v_warn_cnt NUMBER := 0;
|
||
v_dev_cnt NUMBER := 0;
|
||
v_total NUMBER := 0;
|
||
C_OK CONSTANT VARCHAR2(10 CHAR) := UNISTR('\2705');
|
||
C_NOK CONSTANT VARCHAR2(10 CHAR) := UNISTR('\274C');
|
||
C_WARN CONSTANT VARCHAR2(10 CHAR) := UNISTR('\26A0\FE0F');
|
||
CRLF CONSTANT VARCHAR2(2) := CHR(13)||CHR(10);
|
||
v_body VARCHAR2(4000);
|
||
v_doc_obj JSON_OBJECT_T;
|
||
v_pdf_b64 CLOB;
|
||
v_pdf_blob BLOB;
|
||
v_fname VARCHAR2(500);
|
||
BEGIN
|
||
SELECT notification_email, name, completed_at
|
||
INTO v_email, v_name, v_done
|
||
FROM dc_projects WHERE id = :project_id;
|
||
|
||
IF v_email IS NULL OR TRIM(v_email) IS NULL THEN
|
||
NULL; -- keine Empfängeradresse → abbrechen
|
||
ELSE
|
||
SELECT COUNT(*),
|
||
SUM(CASE WHEN deviation = 1 THEN 1 ELSE 0 END),
|
||
SUM(CASE WHEN warning = 1 AND deviation = 0 THEN 1 ELSE 0 END)
|
||
INTO v_total, v_dev_cnt, v_warn_cnt
|
||
FROM dc_results WHERE project_id = :project_id;
|
||
v_ok_cnt := v_total - v_dev_cnt - v_warn_cnt;
|
||
|
||
v_body :=
|
||
'Sehr geehrte Damen und Herren,' || CRLF ||
|
||
CRLF ||
|
||
'die automatische Dokumentenprüfung für das Projekt' || CRLF ||
|
||
'„' || v_name || '"' || CRLF ||
|
||
'wurde am ' || TO_CHAR(NVL(v_done, SYSDATE), 'DD.MM.YYYY') ||
|
||
' erfolgreich abgeschlossen.' || CRLF ||
|
||
CRLF ||
|
||
'Zusammenfassung der Prüfergebnisse:' || CRLF ||
|
||
C_OK || ' OK: ' || v_ok_cnt || CRLF ||
|
||
C_WARN || ' Hinweise: ' || v_warn_cnt || CRLF ||
|
||
C_NOK || ' Abweichungen: ' || v_dev_cnt || CRLF ||
|
||
' Gesamt: ' || v_total || ' Prüfpunkte' || CRLF ||
|
||
CRLF ||
|
||
'Die Prüfberichte der einzelnen Dokumente finden Sie als PDF-Anhänge.' || CRLF ||
|
||
CRLF ||
|
||
'Mit freundlichen Grüßen' || CRLF ||
|
||
'Ihr Dokumenten-Check-System' || CRLF ||
|
||
'Frigosped Express GmbH';
|
||
|
||
v_mail_id := APEX_MAIL.SEND(
|
||
p_to => v_email,
|
||
p_from => DC_UTILS_PKG.c_mail_from,
|
||
p_subj => 'Prüfbericht: ' || v_name,
|
||
p_body => v_body
|
||
);
|
||
|
||
-- Einen PDF-Anhang pro Dokument hinzufügen
|
||
IF v_doc_pdfs IS NOT NULL AND v_doc_pdfs.get_size > 0 THEN
|
||
FOR i IN 0 .. v_doc_pdfs.get_size - 1 LOOP
|
||
BEGIN
|
||
v_doc_obj := TREAT(v_doc_pdfs.get(i) AS JSON_OBJECT_T);
|
||
v_fname := v_doc_obj.get_string('filename');
|
||
v_pdf_b64 := v_doc_obj.get_clob('pdf_base64');
|
||
IF v_pdf_b64 IS NOT NULL AND DBMS_LOB.GETLENGTH(v_pdf_b64) > 0 THEN
|
||
v_pdf_blob := base64_to_blob(v_pdf_b64);
|
||
APEX_MAIL.ADD_ATTACHMENT(
|
||
p_mail_id => v_mail_id,
|
||
p_attachment => v_pdf_blob,
|
||
p_filename => NVL(v_fname, 'Pruefbericht.pdf'),
|
||
p_mime_type => 'application/pdf'
|
||
);
|
||
DBMS_LOB.FREETEMPORARY(v_pdf_blob);
|
||
DBMS_LOB.FREETEMPORARY(v_pdf_b64);
|
||
END IF;
|
||
EXCEPTION WHEN OTHERS THEN NULL;
|
||
END;
|
||
END LOOP;
|
||
END IF;
|
||
END IF;
|
||
EXCEPTION WHEN OTHERS THEN NULL;
|
||
END;
|
||
END;
|
||
ELSE
|
||
-- Fallback: Bericht in DB generieren (z.B. bei leerem Body)
|
||
BEGIN
|
||
DC_UTILS_PKG.generate_report(:project_id);
|
||
EXCEPTION WHEN OTHERS THEN NULL;
|
||
END;
|
||
END IF;
|
||
|
||
:status_code := 200;
|
||
HTP.P('{"project_id":' || :project_id
|
||
|| ',"status":"COMPLETED"'
|
||
|| ',"completed_at":"' || TO_CHAR(v_now, 'YYYY-MM-DD"T"HH24:MI:SS') || '"'
|
||
|| '}');
|
||
|
||
EXCEPTION
|
||
WHEN NO_DATA_FOUND THEN
|
||
:status_code := 404;
|
||
HTP.P('{"error":"Projekt nicht gefunden","project_id":' || :project_id || '}');
|
||
WHEN OTHERS THEN
|
||
ROLLBACK;
|
||
:status_code := 500;
|
||
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
|
||
END;
|
||
]'
|
||
);
|
||
|
||
-- ===========================================================================
|
||
-- TEMPLATE 12: projects/:project_id/report → Bericht generieren / abrufen
|
||
-- ===========================================================================
|
||
ORDS.DEFINE_TEMPLATE(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'projects/:project_id/report',
|
||
p_priority => 0,
|
||
p_comments => 'Markdown-Bericht eines Projekts'
|
||
);
|
||
|
||
-- 12. POST /api/dc/projects/:project_id/report – (neu) generieren
|
||
ORDS.DEFINE_HANDLER(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'projects/:project_id/report',
|
||
p_method => 'POST',
|
||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||
p_comments => 'Generiert report_markdown neu (z.B. nach Konfigurationsänderung)',
|
||
p_source => q'[
|
||
BEGIN
|
||
DC_UTILS_PKG.generate_report(:project_id);
|
||
:status_code := 200;
|
||
HTP.P('{"project_id":' || :project_id || ',"report_generated":true}');
|
||
EXCEPTION
|
||
WHEN OTHERS THEN
|
||
ROLLBACK;
|
||
:status_code := 500;
|
||
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
|
||
END;
|
||
]'
|
||
);
|
||
|
||
|
||
-- ===========================================================================
|
||
-- TEMPLATE 7: projects/:project_id/documents/
|
||
-- ===========================================================================
|
||
ORDS.DEFINE_TEMPLATE(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'projects/:project_id/documents/',
|
||
p_priority => 0,
|
||
p_etag_type => 'HASH',
|
||
p_comments => 'Dokument-Liste eines Projekts (ohne BLOB-Payload)'
|
||
);
|
||
|
||
-- 7. GET /api/dc/projects/:project_id/documents/
|
||
ORDS.DEFINE_HANDLER(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'projects/:project_id/documents/',
|
||
p_method => 'GET',
|
||
p_source_type => ORDS.SOURCE_TYPE_COLLECTION_FEED,
|
||
p_items_per_page => 0,
|
||
p_comments => 'Metadaten aller Dokumente; has_original_text/has_translated_text als 0/1',
|
||
p_source => q'[
|
||
SELECT
|
||
d.id,
|
||
d.filename,
|
||
d.mime_type,
|
||
d.document_type_id,
|
||
d.catalog_id,
|
||
d.uploaded_at,
|
||
d.status,
|
||
d.progress,
|
||
d.parent_document_id,
|
||
NVL(d.is_reference, 0) AS is_reference,
|
||
d.source_url,
|
||
CASE WHEN d.original_text IS NOT NULL THEN 1 ELSE 0 END AS has_original_text,
|
||
CASE WHEN d.translated_text IS NOT NULL THEN 1 ELSE 0 END AS has_translated_text,
|
||
d.row_version,
|
||
d.created,
|
||
d.updated
|
||
FROM dc_project_documents d
|
||
WHERE d.project_id = :project_id
|
||
ORDER BY d.uploaded_at
|
||
]'
|
||
);
|
||
|
||
|
||
-- ===========================================================================
|
||
-- TEMPLATE 8: documents/:doc_id/file
|
||
-- ===========================================================================
|
||
ORDS.DEFINE_TEMPLATE(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'documents/:doc_id/file',
|
||
p_priority => 0,
|
||
p_comments => 'BLOB-Download: original_file mit korrektem Content-Type'
|
||
);
|
||
|
||
-- 8. GET /api/dc/documents/:doc_id/file
|
||
-- PLSQL-Workaround: SOURCE_TYPE_MEDIA ruft getString() auf BLOB auf → ORA-17004 in dieser ORDS-Version
|
||
ORDS.DEFINE_HANDLER(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'documents/:doc_id/file',
|
||
p_method => 'GET',
|
||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||
p_comments => 'Streamt original_file BLOB mit korrektem Content-Type (PLSQL-Workaround fuer ORDS-Bug)',
|
||
p_source => q'[
|
||
DECLARE
|
||
v_blob BLOB;
|
||
v_mime VARCHAR2(200);
|
||
v_filename VARCHAR2(500);
|
||
BEGIN
|
||
SELECT original_file, mime_type, filename
|
||
INTO v_blob, v_mime, v_filename
|
||
FROM dc_project_documents
|
||
WHERE id = :doc_id;
|
||
|
||
IF v_blob IS NULL THEN
|
||
:status_code := 404;
|
||
HTP.P('{"error":"Datei nicht vorhanden","doc_id":' || :doc_id || '}');
|
||
RETURN;
|
||
END IF;
|
||
|
||
OWA_UTIL.MIME_HEADER(NVL(v_mime, 'application/octet-stream'), FALSE);
|
||
HTP.P('Content-Disposition: attachment; filename="' || v_filename || '"');
|
||
OWA_UTIL.HTTP_HEADER_CLOSE;
|
||
WPG_DOCLOAD.DOWNLOAD_FILE(v_blob);
|
||
:status_code := 200;
|
||
EXCEPTION
|
||
WHEN NO_DATA_FOUND THEN
|
||
:status_code := 404;
|
||
HTP.P('{"error":"Dokument nicht gefunden","doc_id":' || :doc_id || '}');
|
||
WHEN OTHERS THEN
|
||
:status_code := 500;
|
||
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
|
||
END;
|
||
]'
|
||
);
|
||
|
||
|
||
-- ===========================================================================
|
||
-- TEMPLATE 9: documents/:doc_id/texts (GET + PUT)
|
||
-- ===========================================================================
|
||
ORDS.DEFINE_TEMPLATE(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'documents/:doc_id/texts',
|
||
p_priority => 0,
|
||
p_comments => 'OCR-Ergebnis und Uebersetzung lesen/schreiben'
|
||
);
|
||
|
||
-- 9a. GET /api/dc/documents/:doc_id/texts (fuer Resume nach Phase A)
|
||
ORDS.DEFINE_HANDLER(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'documents/:doc_id/texts',
|
||
p_method => 'GET',
|
||
p_source_type => ORDS.SOURCE_TYPE_COLLECTION_ITEM,
|
||
p_items_per_page => 1,
|
||
p_comments => 'Gibt original_text und translated_text zurueck',
|
||
p_source => q'[
|
||
SELECT
|
||
d.id,
|
||
d.original_text,
|
||
d.translated_text,
|
||
CASE WHEN d.original_text IS NOT NULL THEN 1 ELSE 0 END AS has_original_text,
|
||
CASE WHEN d.translated_text IS NOT NULL THEN 1 ELSE 0 END AS has_translated_text
|
||
FROM dc_project_documents d
|
||
WHERE d.id = :doc_id
|
||
]'
|
||
);
|
||
|
||
-- 9b. PUT /api/dc/documents/:doc_id/texts
|
||
-- Body: {"original_text": "## ...", "translated_text": "## ..."}
|
||
-- Beide Felder sind optional: NULL-Wert ueberschreibt nicht (nur wenn im Body vorhanden)
|
||
ORDS.DEFINE_HANDLER(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'documents/:doc_id/texts',
|
||
p_method => 'PUT',
|
||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||
p_comments => 'Schreibt original_text und/oder translated_text als CLOB',
|
||
p_source => q'[
|
||
DECLARE
|
||
v_body CLOB;
|
||
v_original_text CLOB;
|
||
v_translated CLOB;
|
||
v_rows INTEGER;
|
||
BEGIN
|
||
v_body := :body_text;
|
||
-- RETURNING CLOB erlaubt Werte groesser als 32767 Bytes (wichtig fuer OCR-Output)
|
||
v_original_text := JSON_VALUE(v_body, '$.original_text' RETURNING CLOB);
|
||
v_translated := JSON_VALUE(v_body, '$.translated_text' RETURNING CLOB);
|
||
|
||
UPDATE dc_project_documents
|
||
SET original_text = CASE
|
||
WHEN JSON_EXISTS(v_body, '$.original_text')
|
||
THEN v_original_text
|
||
ELSE original_text
|
||
END,
|
||
translated_text = CASE
|
||
WHEN JSON_EXISTS(v_body, '$.translated_text')
|
||
THEN v_translated
|
||
ELSE translated_text
|
||
END
|
||
WHERE id = :doc_id;
|
||
|
||
v_rows := SQL%ROWCOUNT;
|
||
IF v_rows = 0 THEN
|
||
:status_code := 404;
|
||
HTP.P('{"error":"Dokument nicht gefunden","doc_id":' || :doc_id || '}');
|
||
RETURN;
|
||
END IF;
|
||
|
||
:status_code := 200;
|
||
HTP.P('{"doc_id":' || :doc_id || ',"updated":true}');
|
||
|
||
EXCEPTION
|
||
WHEN OTHERS THEN
|
||
ROLLBACK;
|
||
:status_code := 500;
|
||
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
|
||
END;
|
||
]'
|
||
);
|
||
|
||
|
||
-- ===========================================================================
|
||
-- TEMPLATE 9c: documents/:doc_id/progress (PUT)
|
||
-- ===========================================================================
|
||
ORDS.DEFINE_TEMPLATE(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'documents/:doc_id/progress',
|
||
p_priority => 0,
|
||
p_comments => 'Dokument-Fortschritt und -Status schreiben'
|
||
);
|
||
|
||
-- 9c. PUT /api/dc/documents/:doc_id/progress
|
||
-- Body: {"progress":45,"status":"IN_PROGRESS"}
|
||
ORDS.DEFINE_HANDLER(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'documents/:doc_id/progress',
|
||
p_method => 'PUT',
|
||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||
p_comments => 'Setzt progress (0-100) und/oder status des Dokuments',
|
||
p_source => q'[
|
||
DECLARE
|
||
v_body CLOB;
|
||
v_progress NUMBER;
|
||
v_status VARCHAR2(20);
|
||
v_rows INTEGER;
|
||
BEGIN
|
||
v_body := :body_text;
|
||
v_progress := TO_NUMBER(JSON_VALUE(v_body, '$.progress'));
|
||
v_status := JSON_VALUE(v_body, '$.status');
|
||
|
||
IF v_progress IS NOT NULL AND (v_progress < 0 OR v_progress > 100) THEN
|
||
:status_code := 400;
|
||
HTP.P('{"error":"progress muss zwischen 0 und 100 liegen"}');
|
||
RETURN;
|
||
END IF;
|
||
|
||
UPDATE dc_project_documents
|
||
SET progress = CASE WHEN JSON_EXISTS(v_body, '$.progress') THEN v_progress ELSE progress END,
|
||
status = CASE WHEN JSON_EXISTS(v_body, '$.status') THEN v_status ELSE status END
|
||
WHERE id = :doc_id;
|
||
|
||
v_rows := SQL%ROWCOUNT;
|
||
IF v_rows = 0 THEN
|
||
:status_code := 404;
|
||
HTP.P('{"error":"Dokument nicht gefunden","doc_id":' || :doc_id || '}');
|
||
RETURN;
|
||
END IF;
|
||
|
||
:status_code := 200;
|
||
HTP.P('{"doc_id":' || :doc_id || ',"progress":' || NVL(v_progress, -1)
|
||
|| ',"status":"' || NVL(v_status, '') || '"}');
|
||
|
||
EXCEPTION
|
||
WHEN OTHERS THEN
|
||
ROLLBACK;
|
||
:status_code := 500;
|
||
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
|
||
END;
|
||
]'
|
||
);
|
||
|
||
|
||
-- ===========================================================================
|
||
-- TEMPLATE 10+11: projects/:project_id/results
|
||
-- (POST = Einfuegen, DELETE = Alle loeschen)
|
||
-- ===========================================================================
|
||
ORDS.DEFINE_TEMPLATE(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'projects/:project_id/results',
|
||
p_priority => 0,
|
||
p_comments => 'Prüfergebnisse: POST einfuegen, DELETE alle loeschen'
|
||
);
|
||
|
||
-- 10. POST /api/dc/projects/:project_id/results
|
||
-- Body: {
|
||
-- "question_id": 5,
|
||
-- "doc_id": 101,
|
||
-- "answer": "Ja, §305 BGB ist erfuellt.",
|
||
-- "score": 85,
|
||
-- "result_type": "OK",
|
||
-- "deviation": 0,
|
||
-- "warning": 0,
|
||
-- "the_comment": "Klare Einbeziehungsklausel vorhanden"
|
||
-- }
|
||
ORDS.DEFINE_HANDLER(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'projects/:project_id/results',
|
||
p_method => 'POST',
|
||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||
p_comments => '201 Created mit Location-Header; 400 bei fehlendem question_id/doc_id/result_type',
|
||
p_source => q'[
|
||
DECLARE
|
||
v_body CLOB;
|
||
v_question_id NUMBER;
|
||
v_doc_id NUMBER;
|
||
v_answer CLOB;
|
||
v_score NUMBER;
|
||
v_result_type VARCHAR2(20);
|
||
v_deviation NUMBER;
|
||
v_warning NUMBER;
|
||
v_comment CLOB;
|
||
v_new_id NUMBER;
|
||
BEGIN
|
||
v_body := :body_text;
|
||
v_question_id := TO_NUMBER(JSON_VALUE(v_body, '$.question_id'));
|
||
v_doc_id := TO_NUMBER(JSON_VALUE(v_body, '$.doc_id'));
|
||
v_answer := JSON_VALUE(v_body, '$.answer' RETURNING CLOB);
|
||
v_score := TO_NUMBER(JSON_VALUE(v_body, '$.score'));
|
||
v_result_type := JSON_VALUE(v_body, '$.result_type');
|
||
v_deviation := NVL(TO_NUMBER(JSON_VALUE(v_body, '$.deviation')), 0);
|
||
v_warning := NVL(TO_NUMBER(JSON_VALUE(v_body, '$.warning')), 0);
|
||
v_comment := JSON_VALUE(v_body, '$.the_comment' RETURNING CLOB);
|
||
|
||
-- Pflichtfelder prüfen
|
||
IF v_question_id IS NULL OR v_doc_id IS NULL THEN
|
||
:status_code := 400;
|
||
HTP.P('{"error":"question_id und doc_id sind Pflichtfelder"}');
|
||
RETURN;
|
||
END IF;
|
||
|
||
IF v_result_type NOT IN ('OK', 'UNKLAR', 'NOK') THEN
|
||
:status_code := 400;
|
||
HTP.P('{"error":"result_type muss OK, UNKLAR oder NOK sein",'
|
||
|| '"received":"' || NVL(v_result_type, 'NULL') || '"}');
|
||
RETURN;
|
||
END IF;
|
||
|
||
INSERT INTO dc_results (
|
||
project_id,
|
||
question_id,
|
||
doc_id,
|
||
answer,
|
||
score,
|
||
result_type,
|
||
deviation,
|
||
warning,
|
||
the_comment,
|
||
evaluated_at
|
||
) VALUES (
|
||
:project_id,
|
||
v_question_id,
|
||
v_doc_id,
|
||
v_answer,
|
||
v_score,
|
||
v_result_type,
|
||
v_deviation,
|
||
v_warning,
|
||
v_comment,
|
||
SYSDATE
|
||
)
|
||
RETURNING id INTO v_new_id;
|
||
|
||
:status_code := 201;
|
||
HTP.P('{"id":' || v_new_id
|
||
|| ',"project_id":' || :project_id
|
||
|| ',"question_id":' || v_question_id
|
||
|| ',"doc_id":' || v_doc_id
|
||
|| ',"result_type":"' || v_result_type || '"'
|
||
|| ',"score":' || NVL(TO_CHAR(v_score), 'null')
|
||
|| '}');
|
||
|
||
EXCEPTION
|
||
WHEN OTHERS THEN
|
||
ROLLBACK;
|
||
:status_code := 500;
|
||
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
|
||
END;
|
||
]'
|
||
);
|
||
|
||
-- 11. DELETE /api/dc/projects/:project_id/results
|
||
ORDS.DEFINE_HANDLER(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'projects/:project_id/results',
|
||
p_method => 'DELETE',
|
||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||
p_comments => 'Loescht alle Ergebnisse eines Projekts (fuer Re-Processing)',
|
||
p_source => q'[
|
||
DECLARE
|
||
v_deleted INTEGER;
|
||
BEGIN
|
||
DELETE FROM dc_results
|
||
WHERE project_id = :project_id;
|
||
|
||
v_deleted := SQL%ROWCOUNT;
|
||
|
||
:status_code := 200;
|
||
HTP.P('{"project_id":' || :project_id
|
||
|| ',"deleted_count":' || v_deleted || '}');
|
||
|
||
EXCEPTION
|
||
WHEN OTHERS THEN
|
||
ROLLBACK;
|
||
:status_code := 500;
|
||
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
|
||
END;
|
||
]'
|
||
);
|
||
|
||
|
||
-- ===========================================================================
|
||
-- TEMPLATE 13: catalogs/ – POST (Katalog anlegen)
|
||
-- Ergaenzt das bestehende catalogs/-Template um eine Schreib-Operation.
|
||
-- ===========================================================================
|
||
|
||
-- 13. POST /api/dc/catalogs/
|
||
-- Body: {"name":"ADSp 2017","description":"...","document_type_id":1}
|
||
-- Legt den Katalog mit generation_status=GENERATING an (async Flow).
|
||
ORDS.DEFINE_HANDLER(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'catalogs/',
|
||
p_method => 'POST',
|
||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||
p_comments => '201 Created mit neuer catalog_id; 400 bei fehlendem name/document_type_id',
|
||
p_source => q'[
|
||
DECLARE
|
||
v_body CLOB;
|
||
v_name VARCHAR2(100);
|
||
v_desc CLOB;
|
||
v_dt_id NUMBER;
|
||
v_new_id NUMBER;
|
||
BEGIN
|
||
v_body := :body_text;
|
||
v_name := JSON_VALUE(v_body, '$.name');
|
||
v_desc := JSON_VALUE(v_body, '$.description' RETURNING CLOB);
|
||
v_dt_id := TO_NUMBER(JSON_VALUE(v_body, '$.document_type_id'));
|
||
|
||
IF v_name IS NULL OR v_dt_id IS NULL THEN
|
||
:status_code := 400;
|
||
HTP.P('{"error":"name und document_type_id sind Pflichtfelder"}');
|
||
RETURN;
|
||
END IF;
|
||
|
||
INSERT INTO dc_question_catalogs (name, description, document_type_id, generation_status)
|
||
VALUES (v_name, v_desc, v_dt_id, 'GENERATING')
|
||
RETURNING id INTO v_new_id;
|
||
|
||
:status_code := 201;
|
||
HTP.P('{"id":' || v_new_id
|
||
|| ',"name":"' || REPLACE(v_name, '"', '''') || '"'
|
||
|| ',"document_type_id":' || v_dt_id
|
||
|| ',"generation_status":"GENERATING"}');
|
||
|
||
EXCEPTION
|
||
WHEN OTHERS THEN
|
||
ROLLBACK;
|
||
:status_code := 500;
|
||
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
|
||
END;
|
||
]'
|
||
);
|
||
|
||
|
||
-- ===========================================================================
|
||
-- TEMPLATE 16: catalogs/:catalog_id/generation-status (GET + PUT)
|
||
-- ===========================================================================
|
||
ORDS.DEFINE_TEMPLATE(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'catalogs/:catalog_id/generation-status',
|
||
p_priority => 0,
|
||
p_comments => 'Status der asynchronen Katalog-Generierung lesen/schreiben'
|
||
);
|
||
|
||
-- 16a. GET /api/dc/catalogs/:catalog_id/generation-status
|
||
ORDS.DEFINE_HANDLER(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'catalogs/:catalog_id/generation-status',
|
||
p_method => 'GET',
|
||
p_source_type => ORDS.SOURCE_TYPE_COLLECTION_ITEM,
|
||
p_items_per_page => 1,
|
||
p_comments => 'Gibt generation_status und generation_error zurueck',
|
||
p_source => q'[
|
||
SELECT
|
||
c.id AS catalog_id,
|
||
c.name,
|
||
c.description,
|
||
c.generation_status,
|
||
c.generation_error
|
||
FROM dc_question_catalogs c
|
||
WHERE c.id = :catalog_id
|
||
]'
|
||
);
|
||
|
||
-- 16b. PUT /api/dc/catalogs/:catalog_id/generation-status
|
||
-- Body: {"generation_status":"READY"} oder {"generation_status":"ERROR","generation_error":"..."}
|
||
ORDS.DEFINE_HANDLER(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'catalogs/:catalog_id/generation-status',
|
||
p_method => 'PUT',
|
||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||
p_comments => 'Setzt generation_status (GENERATING|READY|ERROR) und optionalen Fehlertext',
|
||
p_source => q'[
|
||
DECLARE
|
||
v_body CLOB;
|
||
v_status VARCHAR2(20);
|
||
v_error VARCHAR2(4000);
|
||
v_rows INTEGER;
|
||
BEGIN
|
||
v_body := :body_text;
|
||
v_status := JSON_VALUE(v_body, '$.generation_status');
|
||
v_error := JSON_VALUE(v_body, '$.generation_error');
|
||
|
||
IF v_status NOT IN ('GENERATING', 'READY', 'ERROR') THEN
|
||
:status_code := 400;
|
||
HTP.P('{"error":"generation_status muss GENERATING, READY oder ERROR sein"}');
|
||
RETURN;
|
||
END IF;
|
||
|
||
UPDATE dc_question_catalogs
|
||
SET generation_status = v_status,
|
||
generation_error = v_error
|
||
WHERE id = :catalog_id;
|
||
|
||
v_rows := SQL%ROWCOUNT;
|
||
IF v_rows = 0 THEN
|
||
:status_code := 404;
|
||
HTP.P('{"error":"Katalog nicht gefunden","catalog_id":' || :catalog_id || '}');
|
||
RETURN;
|
||
END IF;
|
||
|
||
:status_code := 200;
|
||
HTP.P('{"catalog_id":' || :catalog_id
|
||
|| ',"generation_status":"' || v_status || '"'
|
||
|| ',"updated":true}');
|
||
|
||
EXCEPTION
|
||
WHEN OTHERS THEN
|
||
ROLLBACK;
|
||
:status_code := 500;
|
||
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
|
||
END;
|
||
]'
|
||
);
|
||
|
||
|
||
-- ===========================================================================
|
||
-- TEMPLATE 14: catalogs/:catalog_id/categories/ (POST)
|
||
-- ===========================================================================
|
||
ORDS.DEFINE_TEMPLATE(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'catalogs/:catalog_id/categories/',
|
||
p_priority => 0,
|
||
p_comments => 'Fragenkategorien eines Katalogs anlegen'
|
||
);
|
||
|
||
-- 14. POST /api/dc/catalogs/:catalog_id/categories/
|
||
-- Body: {"name":"Haftung","description":"...","order_nr":1}
|
||
ORDS.DEFINE_HANDLER(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'catalogs/:catalog_id/categories/',
|
||
p_method => 'POST',
|
||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||
p_comments => '201 Created mit neuer category_id; 400 bei fehlendem name',
|
||
p_source => q'[
|
||
DECLARE
|
||
v_body CLOB;
|
||
v_name VARCHAR2(100);
|
||
v_desc CLOB;
|
||
v_order NUMBER;
|
||
v_new_id NUMBER;
|
||
BEGIN
|
||
v_body := :body_text;
|
||
v_name := JSON_VALUE(v_body, '$.name');
|
||
v_desc := JSON_VALUE(v_body, '$.description' RETURNING CLOB);
|
||
v_order := NVL(TO_NUMBER(JSON_VALUE(v_body, '$.order_nr')), 0);
|
||
|
||
IF v_name IS NULL THEN
|
||
:status_code := 400;
|
||
HTP.P('{"error":"name ist ein Pflichtfeld"}');
|
||
RETURN;
|
||
END IF;
|
||
|
||
INSERT INTO dc_question_categories (catalog_id, name, description, order_nr)
|
||
VALUES (:catalog_id, v_name, v_desc, v_order)
|
||
RETURNING id INTO v_new_id;
|
||
|
||
:status_code := 201;
|
||
HTP.P('{"id":' || v_new_id
|
||
|| ',"catalog_id":' || :catalog_id
|
||
|| ',"name":"' || REPLACE(v_name, '"', '''') || '"'
|
||
|| ',"order_nr":' || v_order || '}');
|
||
|
||
EXCEPTION
|
||
WHEN OTHERS THEN
|
||
ROLLBACK;
|
||
:status_code := 500;
|
||
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
|
||
END;
|
||
]'
|
||
);
|
||
|
||
|
||
-- ===========================================================================
|
||
-- TEMPLATE 15: categories/:category_id/questions/ (POST)
|
||
-- ===========================================================================
|
||
ORDS.DEFINE_TEMPLATE(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'categories/:category_id/questions/',
|
||
p_priority => 0,
|
||
p_comments => 'Fragen einer Kategorie anlegen'
|
||
);
|
||
|
||
-- 15. POST /api/dc/categories/:category_id/questions/
|
||
-- Body: {
|
||
-- "question_text": "...",
|
||
-- "evaluation_type": "SCORE",
|
||
-- "threshold": 70,
|
||
-- "result_handling": "ABWEICHUNG",
|
||
-- "example_0_percent": "...",
|
||
-- "example_100_percent": "...",
|
||
-- "order_nr": 1
|
||
-- }
|
||
ORDS.DEFINE_HANDLER(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'categories/:category_id/questions/',
|
||
p_method => 'POST',
|
||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||
p_comments => '201 Created mit neuer question_id; 400 bei fehlendem question_text',
|
||
p_source => q'[
|
||
DECLARE
|
||
v_body CLOB;
|
||
v_text CLOB;
|
||
v_eval_type VARCHAR2(20);
|
||
v_threshold NUMBER;
|
||
v_handling VARCHAR2(20);
|
||
v_ex0 CLOB;
|
||
v_ex100 CLOB;
|
||
v_order NUMBER;
|
||
v_new_id NUMBER;
|
||
BEGIN
|
||
v_body := :body_text;
|
||
v_text := JSON_VALUE(v_body, '$.question_text' RETURNING CLOB);
|
||
v_eval_type := NVL(JSON_VALUE(v_body, '$.evaluation_type'), 'SCORE');
|
||
v_threshold := NVL(TO_NUMBER(JSON_VALUE(v_body, '$.threshold')), 70);
|
||
v_handling := NVL(JSON_VALUE(v_body, '$.result_handling'), 'ABWEICHUNG');
|
||
v_ex0 := JSON_VALUE(v_body, '$.example_0_percent' RETURNING CLOB);
|
||
v_ex100 := JSON_VALUE(v_body, '$.example_100_percent' RETURNING CLOB);
|
||
v_order := NVL(TO_NUMBER(JSON_VALUE(v_body, '$.order_nr')), 0);
|
||
|
||
IF v_text IS NULL THEN
|
||
:status_code := 400;
|
||
HTP.P('{"error":"question_text ist ein Pflichtfeld"}');
|
||
RETURN;
|
||
END IF;
|
||
|
||
IF v_eval_type NOT IN ('SCORE', 'BINARY') THEN
|
||
:status_code := 400;
|
||
HTP.P('{"error":"evaluation_type muss SCORE oder BINARY sein"}');
|
||
RETURN;
|
||
END IF;
|
||
|
||
IF v_handling NOT IN ('ABWEICHUNG', 'HINWEIS') THEN
|
||
:status_code := 400;
|
||
HTP.P('{"error":"result_handling muss ABWEICHUNG oder HINWEIS sein"}');
|
||
RETURN;
|
||
END IF;
|
||
|
||
INSERT INTO dc_questions (
|
||
category_id,
|
||
question_text,
|
||
evaluation_type,
|
||
threshold,
|
||
result_handling,
|
||
example_0_percent,
|
||
example_100_percent,
|
||
order_nr
|
||
) VALUES (
|
||
:category_id,
|
||
v_text,
|
||
v_eval_type,
|
||
v_threshold,
|
||
v_handling,
|
||
v_ex0,
|
||
v_ex100,
|
||
v_order
|
||
)
|
||
RETURNING id INTO v_new_id;
|
||
|
||
:status_code := 201;
|
||
HTP.P('{"id":' || v_new_id
|
||
|| ',"category_id":' || :category_id
|
||
|| ',"evaluation_type":"' || v_eval_type || '"'
|
||
|| ',"threshold":' || v_threshold
|
||
|| ',"order_nr":' || v_order || '}');
|
||
|
||
EXCEPTION
|
||
WHEN OTHERS THEN
|
||
ROLLBACK;
|
||
:status_code := 500;
|
||
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
|
||
END;
|
||
]'
|
||
);
|
||
|
||
-- ===========================================================================
|
||
-- TEMPLATE 17: ai-costs (POST = Eintrag speichern, GET = Auswertung)
|
||
-- ===========================================================================
|
||
ORDS.DEFINE_TEMPLATE(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'ai-costs',
|
||
p_priority => 0,
|
||
p_comments => 'KI-Kostenprotokoll: POST speichert Aufruf, GET liefert Auswertung'
|
||
);
|
||
|
||
-- 17a. POST /api/dc/ai-costs
|
||
-- Body: {"provider":"mistral","model_name":"mistral-small-latest","operation":"TRANSLATE",
|
||
-- "project_id":42,"catalog_id":null,
|
||
-- "prompt_tokens":1234,"completion_tokens":567,"total_tokens":1801,"cost_eur":0.000157}
|
||
ORDS.DEFINE_HANDLER(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'ai-costs',
|
||
p_method => 'POST',
|
||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||
p_comments => '201 Created mit neuer cost_log id',
|
||
p_source => q'[
|
||
DECLARE
|
||
v_body CLOB;
|
||
v_new_id NUMBER;
|
||
BEGIN
|
||
v_body := :body_text;
|
||
|
||
INSERT INTO dc_ai_cost_log (
|
||
provider, model_name, operation,
|
||
project_id, catalog_id,
|
||
prompt_tokens, completion_tokens, total_tokens,
|
||
cost_eur
|
||
) VALUES (
|
||
JSON_VALUE(v_body, '$.provider'),
|
||
JSON_VALUE(v_body, '$.model_name'),
|
||
JSON_VALUE(v_body, '$.operation'),
|
||
TO_NUMBER(JSON_VALUE(v_body, '$.project_id')),
|
||
TO_NUMBER(JSON_VALUE(v_body, '$.catalog_id')),
|
||
NVL(TO_NUMBER(JSON_VALUE(v_body, '$.prompt_tokens')), 0),
|
||
NVL(TO_NUMBER(JSON_VALUE(v_body, '$.completion_tokens')), 0),
|
||
NVL(TO_NUMBER(JSON_VALUE(v_body, '$.total_tokens')), 0),
|
||
NVL(TO_NUMBER(JSON_VALUE(v_body, '$.cost_eur')), 0)
|
||
)
|
||
RETURNING id INTO v_new_id;
|
||
|
||
:status_code := 201;
|
||
HTP.P('{"id":' || v_new_id || '}');
|
||
|
||
EXCEPTION
|
||
WHEN OTHERS THEN
|
||
ROLLBACK;
|
||
:status_code := 500;
|
||
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
|
||
END;
|
||
]'
|
||
);
|
||
|
||
-- 17b. GET /api/dc/ai-costs
|
||
-- Liefert: Gesamtkosten, laufender Monat, Monats-Aufschlüsselung (12 Monate)
|
||
ORDS.DEFINE_HANDLER(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'ai-costs',
|
||
p_method => 'GET',
|
||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||
p_comments => 'Kostenauswertung: total, current_month, monatliche Aufschluesselung via DC_COSTS_PKG',
|
||
p_source => q'[
|
||
DECLARE
|
||
v_total_eur NUMBER;
|
||
v_total_tokens NUMBER;
|
||
v_total_calls NUMBER;
|
||
v_month_eur NUMBER;
|
||
v_month_tokens NUMBER;
|
||
v_month_calls NUMBER;
|
||
v_monthly_json CLOB;
|
||
BEGIN
|
||
DC_COSTS_PKG.get_summary(
|
||
v_total_eur, v_total_tokens, v_total_calls,
|
||
v_month_eur, v_month_tokens, v_month_calls
|
||
);
|
||
|
||
SELECT JSON_ARRAYAGG(
|
||
JSON_OBJECT(
|
||
'month' VALUE month,
|
||
'cost_eur' VALUE ROUND(cost_eur, 6),
|
||
'prompt_tokens' VALUE prompt_tokens,
|
||
'completion_tokens' VALUE completion_tokens,
|
||
'total_tokens' VALUE total_tokens,
|
||
'call_count' VALUE call_count
|
||
)
|
||
ORDER BY month DESC
|
||
RETURNING CLOB
|
||
)
|
||
INTO v_monthly_json
|
||
FROM (
|
||
SELECT
|
||
TO_CHAR(called_at, 'YYYY-MM') AS month,
|
||
SUM(cost_eur) AS cost_eur,
|
||
SUM(prompt_tokens) AS prompt_tokens,
|
||
SUM(completion_tokens) AS completion_tokens,
|
||
SUM(total_tokens) AS total_tokens,
|
||
COUNT(*) AS call_count
|
||
FROM dc_ai_cost_log
|
||
WHERE called_at >= ADD_MONTHS(TRUNC(SYSDATE, 'MM'), -11)
|
||
GROUP BY TO_CHAR(called_at, 'YYYY-MM')
|
||
);
|
||
|
||
:status_code := 200;
|
||
HTP.P(
|
||
'{"total":{'
|
||
|| '"cost_eur":' || ROUND(NVL(v_total_eur, 0), 6)
|
||
|| ',"total_tokens":'|| NVL(v_total_tokens, 0)
|
||
|| ',"call_count":' || NVL(v_total_calls, 0)
|
||
|| '},"current_month":{'
|
||
|| '"month":"' || TO_CHAR(SYSDATE, 'YYYY-MM') || '"'
|
||
|| ',"cost_eur":' || ROUND(NVL(v_month_eur, 0), 6)
|
||
|| ',"total_tokens":'|| NVL(v_month_tokens, 0)
|
||
|| ',"call_count":' || NVL(v_month_calls, 0)
|
||
|| '},"monthly":' || NVL(v_monthly_json, '[' || CHR(93))
|
||
|| '}'
|
||
);
|
||
|
||
EXCEPTION
|
||
WHEN OTHERS THEN
|
||
:status_code := 500;
|
||
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
|
||
END;
|
||
]'
|
||
);
|
||
|
||
|
||
-- ===========================================================================
|
||
-- TEMPLATE 18: documents/:parent_id/references (POST)
|
||
-- Legt eine heruntergeladene externe Referenz als neue dc_project_documents-
|
||
-- Zeile an (is_reference=1, verknuepft mit dem Originaldokument).
|
||
-- ===========================================================================
|
||
ORDS.DEFINE_TEMPLATE(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'documents/:parent_id/references',
|
||
p_priority => 0,
|
||
p_comments => 'Heruntergeladene externe Referenz zu einem Dokument speichern'
|
||
);
|
||
|
||
-- 18. POST /api/dc/documents/:parent_id/references
|
||
-- Body: {"source_url":"https://...","filename":"AGB.pdf",
|
||
-- "mime_type":"application/pdf","file_base64":"JVBERi0x..."}
|
||
ORDS.DEFINE_HANDLER(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'documents/:parent_id/references',
|
||
p_method => 'POST',
|
||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||
p_comments => '201 Created mit neuer doc-id; legt is_reference=1-Zeile mit BLOB an',
|
||
p_source => q'[
|
||
DECLARE
|
||
v_body_clob CLOB;
|
||
v_json JSON_OBJECT_T;
|
||
v_source_url VARCHAR2(2000);
|
||
v_filename VARCHAR2(255);
|
||
v_mime VARCHAR2(100);
|
||
v_file_b64 CLOB;
|
||
v_blob BLOB;
|
||
v_project_id NUMBER;
|
||
v_doc_type_id NUMBER;
|
||
v_new_id NUMBER;
|
||
|
||
-- Wandelt den (ggf. grossen) Request-BLOB in einen CLOB
|
||
FUNCTION body_to_clob(p_body IN BLOB) RETURN CLOB IS
|
||
v_clob CLOB;
|
||
v_dest INTEGER := 1;
|
||
v_src INTEGER := 1;
|
||
v_lang INTEGER := 0;
|
||
v_warning INTEGER := 0;
|
||
BEGIN
|
||
IF p_body IS NULL OR DBMS_LOB.GETLENGTH(p_body) = 0 THEN
|
||
RETURN NULL;
|
||
END IF;
|
||
DBMS_LOB.CREATETEMPORARY(v_clob, TRUE);
|
||
DBMS_LOB.CONVERTTOCLOB(v_clob, p_body, DBMS_LOB.LOBMAXSIZE,
|
||
v_dest, v_src, NLS_CHARSET_ID('AL32UTF8'), v_lang, v_warning);
|
||
RETURN v_clob;
|
||
END body_to_clob;
|
||
|
||
-- Dekodiert einen Base64-CLOB in einen BLOB (chunk-weise)
|
||
FUNCTION base64_to_blob(p_b64 IN CLOB) RETURN BLOB IS
|
||
v_blob BLOB;
|
||
v_raw RAW(32767);
|
||
v_chunk INTEGER := 24000;
|
||
v_offset INTEGER := 1;
|
||
v_len INTEGER;
|
||
v_sub VARCHAR2(32767);
|
||
BEGIN
|
||
DBMS_LOB.CREATETEMPORARY(v_blob, TRUE);
|
||
v_len := DBMS_LOB.GETLENGTH(p_b64);
|
||
WHILE v_offset <= v_len LOOP
|
||
v_sub := DBMS_LOB.SUBSTR(p_b64, v_chunk, v_offset);
|
||
v_raw := UTL_ENCODE.BASE64_DECODE(UTL_RAW.CAST_TO_RAW(v_sub));
|
||
DBMS_LOB.WRITEAPPEND(v_blob, UTL_RAW.LENGTH(v_raw), v_raw);
|
||
v_offset := v_offset + v_chunk;
|
||
END LOOP;
|
||
RETURN v_blob;
|
||
END base64_to_blob;
|
||
|
||
BEGIN
|
||
v_body_clob := body_to_clob(:body);
|
||
IF v_body_clob IS NULL THEN
|
||
:status_code := 400;
|
||
HTP.P('{"error":"Leerer Request-Body"}');
|
||
RETURN;
|
||
END IF;
|
||
|
||
v_json := JSON_OBJECT_T.parse(v_body_clob);
|
||
v_source_url := v_json.get_string('source_url');
|
||
v_filename := v_json.get_string('filename');
|
||
v_mime := v_json.get_string('mime_type');
|
||
BEGIN
|
||
v_file_b64 := v_json.get_clob('file_base64');
|
||
EXCEPTION WHEN OTHERS THEN v_file_b64 := NULL; END;
|
||
DBMS_LOB.FREETEMPORARY(v_body_clob);
|
||
|
||
IF v_file_b64 IS NULL OR DBMS_LOB.GETLENGTH(v_file_b64) = 0 THEN
|
||
:status_code := 400;
|
||
HTP.P('{"error":"file_base64 ist Pflichtfeld"}');
|
||
RETURN;
|
||
END IF;
|
||
|
||
-- Projekt + Dokumenttyp vom Originaldokument uebernehmen
|
||
SELECT project_id, document_type_id
|
||
INTO v_project_id, v_doc_type_id
|
||
FROM dc_project_documents
|
||
WHERE id = :parent_id;
|
||
|
||
v_blob := base64_to_blob(v_file_b64);
|
||
|
||
INSERT INTO dc_project_documents (
|
||
project_id,
|
||
document_type_id,
|
||
parent_document_id,
|
||
is_reference,
|
||
source_url,
|
||
original_file,
|
||
mime_type,
|
||
filename,
|
||
status,
|
||
progress,
|
||
uploaded_at
|
||
) VALUES (
|
||
v_project_id,
|
||
v_doc_type_id,
|
||
:parent_id,
|
||
1,
|
||
v_source_url,
|
||
v_blob,
|
||
NVL(v_mime, 'application/octet-stream'),
|
||
NVL(v_filename, 'referenz'),
|
||
'IN_PROGRESS',
|
||
0,
|
||
SYSDATE
|
||
)
|
||
RETURNING id INTO v_new_id;
|
||
|
||
:status_code := 201;
|
||
HTP.P('{"id":' || v_new_id
|
||
|| ',"parent_document_id":' || :parent_id
|
||
|| ',"project_id":' || v_project_id || '}');
|
||
|
||
EXCEPTION
|
||
WHEN NO_DATA_FOUND THEN
|
||
ROLLBACK;
|
||
:status_code := 404;
|
||
HTP.P('{"error":"Originaldokument nicht gefunden","parent_id":' || :parent_id || '}');
|
||
WHEN OTHERS THEN
|
||
ROLLBACK;
|
||
:status_code := 500;
|
||
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
|
||
END;
|
||
]'
|
||
);
|
||
|
||
|
||
-- ===========================================================================
|
||
-- TEMPLATE 19: projects/:project_id/references (DELETE)
|
||
-- Loescht alle heruntergeladenen Referenzen eines Projekts (Re-Processing).
|
||
-- ===========================================================================
|
||
ORDS.DEFINE_TEMPLATE(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'projects/:project_id/references',
|
||
p_priority => 0,
|
||
p_comments => 'Heruntergeladene Referenzen eines Projekts verwalten'
|
||
);
|
||
|
||
-- 19. DELETE /api/dc/projects/:project_id/references
|
||
ORDS.DEFINE_HANDLER(
|
||
p_module_name => 'frigosped.dc',
|
||
p_pattern => 'projects/:project_id/references',
|
||
p_method => 'DELETE',
|
||
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
|
||
p_comments => 'Loescht alle is_reference=1-Zeilen eines Projekts (fuer Re-Processing)',
|
||
p_source => q'[
|
||
DECLARE
|
||
v_deleted INTEGER;
|
||
BEGIN
|
||
DELETE FROM dc_project_documents
|
||
WHERE project_id = :project_id
|
||
AND is_reference = 1;
|
||
|
||
v_deleted := SQL%ROWCOUNT;
|
||
|
||
:status_code := 200;
|
||
HTP.P('{"project_id":' || :project_id
|
||
|| ',"deleted_count":' || v_deleted || '}');
|
||
|
||
EXCEPTION
|
||
WHEN OTHERS THEN
|
||
ROLLBACK;
|
||
:status_code := 500;
|
||
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
|
||
END;
|
||
]'
|
||
);
|
||
|
||
|
||
COMMIT;
|
||
END;
|
||
/
|
||
|
||
|
||
-- =============================================================================
|
||
-- BLOCK 2: Auth / Privilege (fuer Produktion)
|
||
-- =============================================================================
|
||
-- In der Entwicklung kann dieser Block auskommentiert bleiben – das Modul
|
||
-- ist dann ohne Auth-Pruefung erreichbar (STATUS = PUBLISHED genuegt).
|
||
--
|
||
-- Fuer Produktion einkommentieren und danach den OAuth2-Client anlegen:
|
||
--
|
||
-- SELECT client_id, client_secret
|
||
-- FROM user_ords_clients
|
||
-- WHERE name = 'quarkus_dc_backend';
|
||
--
|
||
-- Den client_id/client_secret in die Quarkus application.properties eintragen.
|
||
-- =============================================================================
|
||
/*
|
||
BEGIN
|
||
-- Rolle anlegen (falls noch nicht vorhanden)
|
||
BEGIN
|
||
ORDS.CREATE_ROLE(p_role_name => 'dc_api_user');
|
||
EXCEPTION
|
||
WHEN OTHERS THEN NULL;
|
||
END;
|
||
|
||
-- Privilege loeschen falls vorhanden
|
||
BEGIN
|
||
ORDS.DROP_PRIVILEGE(p_name => 'dc.api.privilege');
|
||
EXCEPTION
|
||
WHEN OTHERS THEN NULL;
|
||
END;
|
||
|
||
-- Privilege definieren: mappt Rolle auf alle /api/dc/-Endpunkte
|
||
ORDS.DEFINE_PRIVILEGE(
|
||
p_privilege_name => 'dc.api.privilege',
|
||
p_roles => ORDS_TYPES.T_ORDS_VARCHARS('dc_api_user'),
|
||
p_patterns => ORDS_TYPES.T_ORDS_VARCHARS('/api/dc/*'),
|
||
p_module_name => 'frigosped.dc',
|
||
p_label => 'Dokumenten-Check API',
|
||
p_description => 'Zugriff auf alle /api/dc/-Endpunkte',
|
||
p_comments => 'Wird dem OAuth2-Client quarkus_dc_backend zugewiesen'
|
||
);
|
||
|
||
COMMIT;
|
||
END;
|
||
/
|
||
|
||
-- OAuth2-Client fuer den Quarkus-Server (einmalig ausfuehren)
|
||
BEGIN
|
||
OAUTH.CREATE_CLIENT(
|
||
p_name => 'quarkus_dc_backend',
|
||
p_grant_type => 'CLIENT_CREDENTIALS',
|
||
p_owner => 'Frigosped',
|
||
p_description => 'Quarkus-Verarbeitungsserver Service-Account',
|
||
p_redirect_uri => NULL,
|
||
p_support_email => 'admin@frigosped.de',
|
||
p_privilege_names => 'dc.api.privilege'
|
||
);
|
||
COMMIT;
|
||
END;
|
||
/
|
||
*/
|
||
|
||
|
||
-- =============================================================================
|
||
-- Test-Aufrufe (curl)
|
||
-- =============================================================================
|
||
-- Ersetze <BASE> und <SCHEMA> mit deinen ORDS-Werten, z.B.:
|
||
-- BASE="https://apex.example.com/ords/FRIGOSPED_APP"
|
||
--
|
||
-- Ohne Auth (Entwicklung):
|
||
--
|
||
-- # 1. Alle Kataloge
|
||
-- curl -sS "$BASE/api/dc/catalogs/" | jq .
|
||
--
|
||
-- # 2. Fragen fuer Katalog 1
|
||
-- curl -sS "$BASE/api/dc/catalogs/1/questions/" | jq .
|
||
--
|
||
-- # 3. Projektdetail
|
||
-- curl -sS "$BASE/api/dc/projects/42" | jq .
|
||
--
|
||
-- # 4. Projekt starten
|
||
-- curl -sS -X POST "$BASE/api/dc/projects/42/start" | jq .
|
||
--
|
||
-- # 5. Fortschritt setzen
|
||
-- curl -sS -X PUT -H "Content-Type: application/json" \
|
||
-- -d '{"progress":35}' "$BASE/api/dc/projects/42/progress" | jq .
|
||
--
|
||
-- # 6. Projekt abschliessen
|
||
-- curl -sS -X POST "$BASE/api/dc/projects/42/complete" | jq .
|
||
--
|
||
-- # 7. Dokumente auflisten
|
||
-- curl -sS "$BASE/api/dc/projects/42/documents/" | jq .
|
||
--
|
||
-- # 8. Datei herunterladen
|
||
-- curl -sS -o original.pdf "$BASE/api/dc/documents/101/file"
|
||
--
|
||
-- # 9. OCR-Text + Uebersetzung zurueckschreiben
|
||
-- curl -sS -X PUT -H "Content-Type: application/json" \
|
||
-- -d '{"original_text":"## Vertrag\n\nAbsatz 1...",
|
||
-- "translated_text":"## Vertrag (DE)\n\nAbsatz 1..."}' \
|
||
-- "$BASE/api/dc/documents/101/texts" | jq .
|
||
--
|
||
-- # 10. Ergebnis einfuegen
|
||
-- curl -sS -X POST -H "Content-Type: application/json" \
|
||
-- -d '{"question_id":5,"doc_id":101,"answer":"Ja, §305 erfuellt.",
|
||
-- "score":85,"result_type":"OK","deviation":0,"warning":0,
|
||
-- "the_comment":"Klare Einbeziehungsklausel vorhanden"}' \
|
||
-- "$BASE/api/dc/projects/42/results" | jq .
|
||
--
|
||
-- # 11. Alle Ergebnisse loeschen (Re-Processing)
|
||
-- curl -sS -X DELETE "$BASE/api/dc/projects/42/results" | jq .
|
||
-- =============================================================================
|