Files
Dokumenten-Check/Scripts/DC_UTILS_PKG.sql
Philipp Ostmeyer c75f460ec5 feat: Externe Dokument-Referenzen erkennen, herunterladen & mitbewerten
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>
2026-06-22 12:45:55 +02:00

453 lines
18 KiB
SQL
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- =============================================================================
-- DC_UTILS_PKG
-- Hilfsprozeduren und -funktionen für den Dokumenten-Check
--
-- Deployment:
-- sql user/pass@db @"Scripts/DC_UTILS_PKG.sql"
-- =============================================================================
-- =============================================================================
-- Package Spec
-- =============================================================================
CREATE OR REPLACE PACKAGE DC_UTILS_PKG AS
-- Absender-Adresse für Berichts-Mails (anpassen falls nötig)
c_mail_from CONSTANT VARCHAR2(100) := 'express-o@frigosped.de';
-- Basis-URL der APEX-App für Direktlinks in Mails (Friendly URL, ohne
-- Session-ID -> APEX Deep-Linking leitet bei Bedarf über den Login).
-- Die Projekt-ID wird angehängt: ...?p7_project_id=<id>
c_app_link_base CONSTANT VARCHAR2(300) :=
'https://apex-ai.express-o.org/ords/r/ai_dev/dokumentencheck/' ||
'projekt-bearbeiten?p7_project_id=';
-- -------------------------------------------------------------------------
-- Erstellt einen Markdown-Prüfbericht für ein Projekt und speichert ihn
-- in dc_projects.report_markdown. Anschließend wird der Bericht per Mail
-- an notification_email versendet (sofern gesetzt).
--
-- Aufbau des Berichts:
-- 1. Kopf (Projektname, Abschlussdatum)
-- 2. Zusammenfassung (OK / Hinweise / Abweichungen) gefiltert nach notify_*
-- 3. Ergebnisse: je Dokument → Kategorie (order_nr) → Frage (order_nr)
-- Icons: ✅ OK | ⚠️ Hinweis | ❌ Abweichung | ❓ Unklar
--
-- Beispiel:
-- BEGIN DC_UTILS_PKG.generate_report(p_project_id => 1); COMMIT; END;
-- -------------------------------------------------------------------------
PROCEDURE generate_report(p_project_id IN NUMBER);
-- -------------------------------------------------------------------------
-- Versendet den gespeicherten Markdown-Bericht per E-Mail als PDF-Anhang.
-- Wird automatisch von generate_report aufgerufen; kann aber auch
-- manuell ausgelöst werden (z.B. für erneuten Versand).
-- Tut nichts wenn notification_email oder report_markdown leer ist.
-- p_skip_pdf = TRUE: kein HTTP-Rückruf ans Java-Backend (nur Text-Mail).
-- -------------------------------------------------------------------------
PROCEDURE send_report_email(p_project_id IN NUMBER,
p_skip_pdf IN BOOLEAN DEFAULT FALSE,
p_pdf IN BLOB DEFAULT NULL);
END DC_UTILS_PKG;
/
-- =============================================================================
-- Package Body
-- =============================================================================
CREATE OR REPLACE PACKAGE BODY DC_UTILS_PKG AS
-- -------------------------------------------------------------------------
PROCEDURE generate_report(p_project_id IN NUMBER) IS
v_md CLOB;
v_name dc_projects.name%TYPE;
v_status dc_projects.status%TYPE;
v_done dc_projects.completed_at%TYPE;
v_ok_cnt NUMBER := 0;
v_warn_cnt NUMBER := 0;
v_dev_cnt NUMBER := 0;
v_unkl_cnt NUMBER := 0;
v_total NUMBER := 0;
v_total_all NUMBER := 0;
-- Zähler pro Dokument
v_doc_ok_cnt NUMBER := 0;
v_doc_warn_cnt NUMBER := 0;
v_doc_dev_cnt NUMBER := 0;
v_doc_unkl_cnt NUMBER := 0;
v_doc_total NUMBER := 0; -- gefiltert (für ok_cnt-Berechnung)
v_doc_total_all NUMBER := 0; -- ungefiltert (für Gesamt-Zeile)
v_notify_ok dc_projects.notify_ok%TYPE;
v_notify_hinweis dc_projects.notify_hinweis%TYPE;
v_notify_abweichend dc_projects.notify_abweichend%TYPE;
-- Emoji-Konstanten via UNISTR (vermeidet Quellcode-Encoding-Probleme)
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'); -- ⚠️
C_UNKL CONSTANT VARCHAR2(10 CHAR) := UNISTR('\2753'); -- ❓
C_DOC CONSTANT VARCHAR2(10 CHAR) := UNISTR('\D83D\DCC4'); -- 📄
CURSOR c_docs IS
SELECT d.id, d.filename
FROM dc_project_documents d
WHERE d.project_id = p_project_id
ORDER BY d.uploaded_at, d.id;
CURSOR c_cats(p_doc_id NUMBER) IS
SELECT DISTINCT qc.id, qc.name, qc.order_nr
FROM dc_question_categories qc
JOIN dc_questions q ON q.category_id = qc.id
JOIN dc_results r ON r.question_id = q.id
WHERE r.project_id = p_project_id
AND r.doc_id = p_doc_id
AND ( (r.deviation = 1 AND v_notify_abweichend = 'Y')
OR (r.warning = 1 AND v_notify_hinweis = 'Y')
OR (r.deviation = 0 AND r.warning = 0 AND v_notify_ok = 'Y')
)
ORDER BY qc.order_nr, qc.name;
CURSOR c_qs(p_doc_id NUMBER, p_cat_id NUMBER) IS
SELECT q.question_text,
r.result_type,
r.score,
r.deviation,
r.warning,
r.answer,
r.the_comment
FROM dc_questions q
JOIN dc_results r ON r.question_id = q.id
WHERE r.project_id = p_project_id
AND r.doc_id = p_doc_id
AND q.category_id = p_cat_id
AND ( (r.deviation = 1 AND v_notify_abweichend = 'Y')
OR (r.warning = 1 AND v_notify_hinweis = 'Y')
OR (r.deviation = 0 AND r.warning = 0 AND v_notify_ok = 'Y')
)
ORDER BY q.order_nr, q.id;
v_icon VARCHAR2(20 CHAR);
-- Fügt eine Zeile + Newline an den CLOB an. Akzeptiert CLOB damit kein
-- implizites CLOB→VARCHAR2-Cast nötig ist (question_text etc. sind CLOBs).
PROCEDURE ln(p_text IN CLOB DEFAULT NULL) IS
BEGIN
IF p_text IS NOT NULL AND DBMS_LOB.GETLENGTH(p_text) > 0 THEN
DBMS_LOB.APPEND(v_md, p_text);
END IF;
DBMS_LOB.APPEND(v_md, TO_CLOB(CHR(13)||CHR(10)));
END ln;
BEGIN
DBMS_LOB.CREATETEMPORARY(v_md, TRUE);
SELECT name, status, completed_at,
notify_ok, notify_hinweis, notify_abweichend
INTO v_name, v_status, v_done,
v_notify_ok, v_notify_hinweis, v_notify_abweichend
FROM dc_projects
WHERE id = p_project_id;
SELECT COUNT(*)
INTO v_total_all
FROM dc_results
WHERE project_id = p_project_id;
SELECT SUM(CASE WHEN deviation = 1 THEN 1 ELSE 0 END),
SUM(CASE WHEN warning = 1 THEN 1 ELSE 0 END),
SUM(CASE WHEN result_type = 'UNKLAR'
AND deviation = 0
AND warning = 0 THEN 1 ELSE 0 END),
COUNT(*)
INTO v_dev_cnt, v_warn_cnt, v_unkl_cnt, v_total
FROM dc_results
WHERE project_id = p_project_id
AND ( (deviation = 1 AND v_notify_abweichend = 'Y')
OR (warning = 1 AND v_notify_hinweis = 'Y')
OR (deviation = 0 AND warning = 0 AND v_notify_ok = 'Y')
);
v_ok_cnt := v_total - v_dev_cnt - v_warn_cnt - v_unkl_cnt;
-- =====================================================================
-- Kopf
-- =====================================================================
ln('# Prüfbericht: ' || v_name);
ln();
IF v_done IS NOT NULL THEN
ln('**Abgeschlossen:** ' || TO_CHAR(v_done, 'DD.MM.YYYY HH24:MI'));
ELSE
ln('**Erstellt:** ' || TO_CHAR(SYSDATE, 'DD.MM.YYYY HH24:MI'));
END IF;
ln();
ln('---');
ln();
-- =====================================================================
-- Zusammenfassung (keine Tabelle Plaintext für maximale Kompatibilität)
-- =====================================================================
ln('## Zusammenfassung');
ln();
IF v_notify_ok = 'Y' THEN
ln(C_OK || ' OK: ' || v_ok_cnt);
IF v_unkl_cnt > 0 THEN
ln(C_UNKL || ' Unklar: ' || v_unkl_cnt);
END IF;
END IF;
IF v_notify_hinweis = 'Y' THEN
ln(C_WARN || ' Hinweise: ' || v_warn_cnt);
END IF;
IF v_notify_abweichend = 'Y' THEN
ln(C_NOK || ' Abweichungen: ' || v_dev_cnt);
END IF;
ln('**Gesamt: ' || v_total_all || '**');
ln();
ln('---');
ln();
-- =====================================================================
-- Ergebnisse je Dokument → Kategorie (order_nr) → Frage (order_nr)
-- =====================================================================
ln('## Ergebnisse');
ln();
FOR doc IN c_docs LOOP
ln('## ' || C_DOC || ' ' || doc.filename);
ln();
-- Zusammenfassung je Dokument (gefiltert nach notify_*, analog zur Gesamt-Zusammenfassung)
SELECT SUM(CASE WHEN deviation = 1
AND v_notify_abweichend = 'Y' THEN 1 ELSE 0 END),
SUM(CASE WHEN warning = 1 AND deviation = 0
AND v_notify_hinweis = 'Y' THEN 1 ELSE 0 END),
SUM(CASE WHEN result_type = 'UNKLAR' AND deviation = 0 AND warning = 0
AND v_notify_ok = 'Y' THEN 1 ELSE 0 END),
SUM(CASE WHEN (deviation = 1 AND v_notify_abweichend = 'Y')
OR (warning = 1 AND v_notify_hinweis = 'Y')
OR (deviation = 0 AND warning = 0
AND v_notify_ok = 'Y') THEN 1 ELSE 0 END),
COUNT(*)
INTO v_doc_dev_cnt, v_doc_warn_cnt, v_doc_unkl_cnt, v_doc_total, v_doc_total_all
FROM dc_results
WHERE project_id = p_project_id
AND doc_id = doc.id;
v_doc_ok_cnt := v_doc_total - v_doc_dev_cnt - v_doc_warn_cnt - v_doc_unkl_cnt;
IF v_notify_ok = 'Y' THEN
ln(C_OK || ' OK: ' || v_doc_ok_cnt);
IF v_doc_unkl_cnt > 0 THEN
ln(C_UNKL || ' Unklar: ' || v_doc_unkl_cnt);
END IF;
END IF;
IF v_notify_hinweis = 'Y' THEN
ln(C_WARN || ' Hinweise: ' || v_doc_warn_cnt);
END IF;
IF v_notify_abweichend = 'Y' THEN
ln(C_NOK || ' Abweichungen: ' || v_doc_dev_cnt);
END IF;
ln('**Gesamt: ' || v_doc_total_all || '**');
ln();
ln('---');
ln();
FOR cat IN c_cats(doc.id) LOOP
ln();
ln('### ' || cat.name);
ln();
FOR q IN c_qs(doc.id, cat.id) LOOP
IF q.deviation = 1 THEN v_icon := C_NOK;
ELSIF q.warning = 1 THEN v_icon := C_WARN;
ELSIF q.result_type = 'UNKLAR' THEN v_icon := C_UNKL;
ELSE v_icon := C_OK;
END IF;
ln();
ln();
ln(v_icon || ' **' || q.question_text || '**');
ln();
IF q.answer IS NOT NULL AND DBMS_LOB.GETLENGTH(q.answer) > 0 THEN
ln(q.answer);
ln();
END IF;
IF q.score IS NOT NULL THEN
ln('*Score: ' || q.score || '%*');
END IF;
IF q.the_comment IS NOT NULL
AND DBMS_LOB.GETLENGTH(q.the_comment) > 0 THEN
ln();
ln('> ' || q.the_comment);
END IF;
ln();
END LOOP;
END LOOP;
ln('---');
ln();
END LOOP;
UPDATE dc_projects
SET report_markdown = v_md
WHERE id = p_project_id;
DBMS_LOB.FREETEMPORARY(v_md);
-- Mail-Versand (Fehler blockieren nie den Report-Build)
BEGIN
send_report_email(p_project_id);
EXCEPTION WHEN OTHERS THEN NULL;
END;
EXCEPTION
WHEN OTHERS THEN
DBMS_LOB.FREETEMPORARY(v_md);
RAISE;
END generate_report;
-- -------------------------------------------------------------------------
PROCEDURE send_report_email(p_project_id IN NUMBER,
p_skip_pdf IN BOOLEAN DEFAULT FALSE,
p_pdf IN BLOB DEFAULT NULL) IS
v_email dc_projects.notification_email%TYPE;
v_name dc_projects.name%TYPE;
v_markdown dc_projects.report_markdown%TYPE;
v_done dc_projects.completed_at%TYPE;
v_mail_id NUMBER;
v_pdf BLOB;
v_body VARCHAR2(4000);
v_ok_cnt NUMBER;
v_warn_cnt NUMBER;
v_dev_cnt NUMBER;
v_total_all NUMBER;
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);
BEGIN
SELECT notification_email, name, report_markdown, completed_at
INTO v_email, v_name, v_markdown, v_done
FROM dc_projects
WHERE id = p_project_id;
IF v_email IS NULL OR TRIM(v_email) IS NULL THEN RETURN; END IF;
IF v_markdown IS NULL OR DBMS_LOB.GETLENGTH(v_markdown) = 0 THEN RETURN; END IF;
-- Zusammenfassung aus dc_results (alle Ergebnisse, ungefiltert)
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_all, v_dev_cnt, v_warn_cnt
FROM dc_results
WHERE project_id = p_project_id;
v_ok_cnt := v_total_all - v_dev_cnt - v_warn_cnt;
-- =====================================================================
-- E-Mail-Beschreibungstext
-- =====================================================================
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_all || ' Prüfpunkte' || CRLF ||
CRLF ||
'Den vollständigen Prüfbericht finden Sie als PDF im Anhang.' || CRLF ||
CRLF ||
'Direkt zur Prüfung in der Anwendung:' || CRLF ||
c_app_link_base || p_project_id || CRLF ||
CRLF ||
'Mit freundlichen Grüßen' || CRLF ||
'Ihr Dokumenten-Check-System' || CRLF ||
'Frigosped Express GmbH';
-- =====================================================================
-- PDF bestimmen: Priorität p_pdf (von Java mitgeliefert) >
-- convert_markdown HTTP-Callback > kein PDF (skip_pdf oder Fehler)
-- =====================================================================
IF p_pdf IS NOT NULL AND DBMS_LOB.GETLENGTH(p_pdf) > 0 THEN
-- Java hat das PDF schon fertig mitgeschickt direkt verwenden
v_pdf := p_pdf;
ELSIF NOT p_skip_pdf THEN
BEGIN
v_pdf := DC_BACKEND_PKG.convert_markdown(v_markdown, 'PDF');
EXCEPTION WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('PDF-Generierung fehlgeschlagen: ' || SQLERRM);
v_pdf := NULL;
END;
END IF;
IF v_pdf IS NOT NULL THEN
-- Mail mit PDF-Anhang
v_mail_id := APEX_MAIL.SEND(
p_to => v_email,
p_from => c_mail_from,
p_subj => 'Prüfbericht: ' || v_name,
p_body => v_body
);
APEX_MAIL.ADD_ATTACHMENT(
p_mail_id => v_mail_id,
p_attachment => v_pdf,
p_filename => 'Pruefbericht_' ||
REGEXP_REPLACE(v_name, '[^A-Za-z0-9_äöüÄÖÜß-]', '_') ||
'.pdf',
p_mime_type => 'application/pdf'
);
ELSE
-- Fallback: Backend nicht erreichbar → Markdown direkt im Body
APEX_MAIL.SEND(
p_to => v_email,
p_from => c_mail_from,
p_subj => 'Prüfbericht: ' || v_name,
p_body => v_body || CRLF || CRLF ||
'--- Detaillierter Bericht ---' || CRLF || CRLF ||
v_markdown
);
END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN NULL;
END send_report_email;
END DC_UTILS_PKG;
/
-- =============================================================================
-- Schnelltest (optional, auskommentiert)
-- =============================================================================
/*
BEGIN
DC_UTILS_PKG.generate_report(p_project_id => 1);
COMMIT;
DBMS_OUTPUT.PUT_LINE('Bericht generiert.');
END;
/
*/