commit 39390e94e527069d5699ee9c2a6a7ad791c21e39 Author: Simon C. Kessler Date: Wed Apr 8 08:37:58 2026 +0200 Initialen Implementierungsplan hinzugefügt diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2bbf984 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,266 @@ +# CLAUDE.md — Oracle Database + +## Projektbeschreibung + +Projekt für verschiedene Oracle-Datenbank Entwicklungen. +Enthält Tabellendefinitionen, PL/SQL-Packages, ORDS-Endpunkte, Views, Trigger etc. + +## SQL / PL/SQL Guidelines + +### Formatierung + +**Keywords immer klein:** +```sql +select, from, where, insert into, create table, begin, end, exception, ... +``` + +**Tab-Ausrichtung in mehrzeiligen Statements:** + +Schlüsselwort + Tab, dann Inhalt. Erste Spalte und alle nachfolgenden Kommas stehen auf +demselben Tab-Stop (eine Fluchtlinie). Komma direkt vor dem Spaltennamen ohne Leerzeichen. + +```sql +select order_id + ,principal_order_number + ,total_amount +from fgt_order +where status = 'COMPLETE' +``` + +```sql +insert into fgt_order ( + order_id + ,principal_order_number + ,total_amount +) values ( + fgt_order_seq.nextval + ,p_order_number + ,p_total_amount +); +``` + +**Block-Keywords in eigener Zeile (lineup bracing):** + +`loop`, `then`, `exception` und ihre schließenden `end`-Varianten stehen auf gleicher +Einrückungsebene wie der Block-Öffner: + +```sql +for rec in cur_open_orders +loop + l_count := l_count + 1; +end loop; + +if l_count > 0 +then + do_something(); +elsif l_count < 0 +then + handle_negative(); +end if; + +begin + l_result := do_something(); +exception + when e_not_found + then + l_result := null; + when others + then + raise; +end; +``` + +**`end`-Anweisungen immer mit Label:** +```sql +end my_procedure; +end my_package; +``` + +**Indentation:** 4 Spaces innerhalb von Blöcken. + +--- + +### Benennungskonventionen + +| Objekt | Konvention | Beispiel | +|---|---|---| +| Tabellen | Modulabhängiger Präfix, z.B.`adr_`, **Singular**, Dateiendung `.tab` | `sy_parameter`, `adr_adresse_zuweisung`, | +| Views | `v_` Präfix | `v_adr_adresse_zuweisung` | +| Packages | `pck_` Präfix, Separate header und body Dateien mit den Endungen `.pkh` und `.pkb` | `pck_system` | +| Prozeduren | `p_`-Präfix | `p_insert_order`, `p_delete_folder` | +| Funktionen | `f_`-Präfix | `f_get_order`, `f_build_url` | +| Lokale Variablen | `l_`-Präfix | `l_order_id`, `l_count` | +| Parameter (Input) | `i_`-Präfix | `i_json`, `i_order_id` | +| Package-globale Variablen | `g_`-Präfix | `g_default_currency` | +| Konstanten | `c_`-Präfix | `c_max_retries` | +| Cursor | `cur_`-Präfix | `cur_open_orders` | +| Exceptions | `e_`-Präfix | `e_not_found`, `e_duplicate` | +| Views | `_v`-Suffix | `fgt_order_v` | +| Trigger | `trg_`-Präfix | `trg_fgt_order_created_at` | +| Sequences | `_seq` | `fgt_order_id_seq` | +| Primary Key (Spalte) | `_id` | `order_id`, `shipment_id` | +| Foreign Key (Constraint) | `_fk` | `fgt_order_bill_addr_fk` | + +--- + +### PL/SQL-Struktur + +- `as` für Package-Spec/Body-Header, `is` für Prozedur-/Funktionskörper innerhalb eines Packages +- Implizite Cursor-FOR-Loops bevorzugen gegenüber expliziten Cursors +- `is null` / `is not null` verwenden — niemals `= null` / `!= null` +- Explizite Formatmasken in `to_date`, `to_char`, `to_timestamp` +- `commit` nur im äußersten Aufrufer — nie in Sub-Prozeduren + +```sql +create or replace package body pck_orders as + + procedure insert_order (i_json in clob, i_run_id in varchar2) + is + l_order_id number; + l_status varchar2(20); + begin + -- Implementierung + l_order_id := bet_order_id_seq.nextval; + end insert_order; + +end fgt_orders; +``` + +--- + +### Exception Handling + +- **Niemals** `when others then null` — Exceptions nie stillschweigend schlucken +- Exceptions mit `e_`-Präfix benennen und im Deklarationsteil definieren +- `sqlerrm` und `sqlcode` für Fehlerinformationen nutzen + +```sql +declare + e_invalid_status exception; + l_status varchar2(20); +begin + if l_status not in ('COMPLETE', 'INCOMPLETE') + then + raise e_invalid_status; + end if; +exception +when e_invalid_status +then + -- gezielt behandeln + log_error('Ungültiger Status: ' || l_status); +when others +then + -- sqlcode / sqlerrm immer loggen, nie schlucken + log_error(sqlerrm); + raise; +end; +``` + +--- + +### Audit-Spalten + +Jede Tabelle bekommt am Ende vier Audit-Spalten: + +```sql +create table bet_order ( + bet_order_id number generated by default as identity not null enable + , ... + ,bet_mit_id_angelegt NUMBER(10) not null, + ,bet_datum_angelegt DATE not null, + ,bet_mit_id_geaendert NUMBER(10) not null, + ,bet_datum_geaendert DATE not null, +); +``` + +Befüllt werden sie über einen `before insert or update`-Trigger pro Tabelle. +Namenskonvention: `trg__audit`. + +```sql +create or replace editionable trigger trg_fgt_order_audit +before + insert or update on fgt_order + referencing + new as new + old as old + for each row +declare + l_user varchar2(100 byte) := substr(upper(coalesce( + sys_context('apex$session', 'app_user'), + sys_context('userenv', 'os_user'), + sys_context('userenv', 'session_user'))), 1, 100); + l_now date := cast(current_timestamp at time zone 'Europe/Berlin' as date); +begin + if inserting + then + :new.created_at := l_now; + :new.created_by := l_user; + end if; + :new.changed_at := l_now; + :new.changed_by := l_user; +end trg_fgt_order_audit; +``` + +Der Benutzername wird bevorzugt aus der APEX-Session gelesen, mit Fallback auf OS- bzw. DB-Session-User. + +--- + +### Tabellenänderungen (Migrations-Skripte) + +Tabellen können von SQLcl Projects nicht gedropt und neu erstellt werden, wenn sie Daten enthalten. +Änderungen an bestehenden Tabellen werden deshalb als `alter table`-Statements **am Ende der +jeweiligen Tabellendatei** ergänzt — nach dem `create table` und dem Primary-Key-Constraint, +aber **vor** dem `-- sqlcl_snapshot`-Kommentar. + +So kann die gesamte Datei als Skript auf eine bestehende Datenbank angewendet werden: +Das `create table` schlägt fehl (Tabelle existiert schon), aber die `alter table`-Statements +laufen durch und bringen die Tabelle auf den aktuellen Stand. + +```sql +create table fgt_order ( ... ); + +alter table fgt_order + add constraint pk_fgt_order primary key (order_id) + using index enable; + +-- Migrationsscript — hinzugefügt am 2026-03-30 +alter table fgt_order add ( + status varchar2(16 char) default 'COMPLETE' not null + ,run_id varchar2(36 char) +); + +-- sqlcl_snapshot { ... } +``` + +--- + +### Planungsdokumente + +Für komplexere Features/Packages wird ein Planungsdokument als `.md`-Datei im Projektverzeichnis +abgelegt (z.B. `plan_pck_net_storage.md`). Der Plan wird während der Umsetzung aktuell gehalten — +jede Änderung an Architektur oder Schnittstelle wird sofort im Plan nachgeführt. + +--- + +### Kommentare + +- Beschreibungen auf **Deutsch**, Code (außer Fachbegriffe der Anwendungsdomäne) auf **Englisch** +- Block-Kommentar zu Beginn Prozedur/Funktion mit Zweck und Parametern +- Inline-Kommentare mit `--` nur für nicht-offensichtliche Logik + +```sql +procedure insert_order (i_json in clob, i_run_id in varchar2) + /*Kopf------------------------------------------------------------------------------------------------ + -- Beschreibung: Fügt einen verarbeiteten Auftrag in die Datenbank ein. + ------------------------------------------------------------------------------------------------------ + -- Parameter: i_json Klippa-Ergebnis als JSON-String + -- i_run_id Ausführungs-Korrelations-ID des Verarbeitungslaufs + ------------------------------------------------------------------------------------------------------ + -- Rückgabe: + ------------------------------------------------------------------------------------------------------ + -- MA Datum Änderung + -- SCK 2026-04-07 Funktion erstellt + ------------------------------------------------------------------------------------------------Kopf*/ +is +... +``` diff --git a/plan_pck_net_storage.md b/plan_pck_net_storage.md new file mode 100644 index 0000000..74882f2 --- /dev/null +++ b/plan_pck_net_storage.md @@ -0,0 +1,287 @@ +# Plan: PCK_NET_STORAGE + +**Stand:** 2026-04-07 +**Status:** Planung abgeschlossen — bereit zur Implementierung + +--- + +## Ziel + +PL/SQL Package `PCK_NET_STORAGE` kapselt alle Zugriffe auf OCI Object Storage über REST API. +Wird in Oracle APEX für einen Dateimanager genutzt (Browse, Upload, Download, Rename, Move, Delete). + +--- + +## Dateien + +``` +tables/ + lg_app_log.tab + +packages/ + pck_log.pkh + pck_log.pkb + pck_net_storage.pkh + pck_net_storage.pkb +``` + +Keine Schema-Level Types. + +--- + +## Authentifizierung + +**APEX Web Credentials** (Typ: Oracle Cloud Infrastructure) +Static ID: `OCI_OBJ_STORE_CRED` +Wird in APEX Admin UI einmalig angelegt mit: OCI User OCID, Tenancy OCID, Fingerprint, Private Key (PEM). +In PL/SQL referenziert via `p_credential_static_id => c_credential_id` in `apex_web_service.make_rest_request`. + +Grund: DBMS_CLOUD ist auf dieser DB nicht installiert. APEX übernimmt OCI HTTP Signature V1 (SHA-256) automatisch. + +--- + +## Konfigurationsparameter + +Alle zur Laufzeit via `pck_system.f_get_par_wert_by_programmid`: + +| Parameter-ID | Inhalt | +|---|---| +| `NET_STORAGE_BUCKET` | Bucket-Name | +| `NET_STORAGE_NAMESPACE` | OCI Object Storage Namespace | +| `NET_STORAGE_REGION` | z.B. `eu-frankfurt-1` | +| `NET_STORAGE_TENANT_ID` | Erlaubter Root-Prefix, z.B. `mandant_42/` | +| `NET_STORAGE_APEX_CREDENTIAL_ID` | Credential Static ID für apex_web_service calls, z.B. 'OCI_OBJ_STORE_CRED' | + +--- + +## Öffentliche Schnittstelle (Package Spec) + +### Record Type `t_object_meta` + +| Feld | Typ | +|---|---| +| `object_name` | `varchar2(1024)` | +| `object_size` | `number` | +| `last_modified` | `date` | +| `content_type` | `varchar2(256)` | +| `etag` | `varchar2(256)` | + +### Öffentliche Prozeduren und Funktionen + +Namenskonvention: Prozeduren `p_`-Präfix, Funktionen `f_`-Präfix. Parameter immer `i_`-Präfix. + +| Name | Typ | Signatur | Rückgabe | Anmerkung | +|---|---|---|---|---| +| `f_list_objects` | Funktion | `(i_prefix, i_delimiter, i_start_with, i_limit)` | `sys_refcursor` | Cursor-Spalten: `object_name, object_size, last_modified, is_folder(Y/N), etag` | +| `f_download_object` | Funktion | `(i_object_key)` | `blob` | | +| `p_upload_object` | Prozedur | `(i_object_key, i_content blob, i_content_type)` | — | | +| `p_delete_object` | Prozedur | `(i_object_key)` | — | | +| `p_delete_folder` | Prozedur | `(i_prefix)` | — | intern: f_list_objects-Loop + p_delete_object | +| `p_rename_object` | Prozedur | `(i_object_key, i_new_name)` | — | intern: OCI RenameObject API | +| `p_move_object` | Prozedur | `(i_object_key, i_target_prefix)` | — | intern: OCI RenameObject API | +| `p_create_folder` | Prozedur | `(i_prefix, i_folder_name)` | — | PUT leeres Objekt mit trailing `/` | +| `f_get_object_metadata` | Funktion | `(i_object_key)` | `t_object_meta` | HEAD Request | + +Rechteprüfungen: + +Zu beginn von delete-operationen muss folgender Aufruf durchgeführt werden, um die Rechte des nutzers zu prüfen: `pck_mitarbeiterrecht.p_hat_recht('ADMIN');` + +Bei anderen schreib-Operationen, wie rename oder move muss die Prüfung von folgendem Recht erfolgen: +`pck_mitarbeiterrecht.p_hat_recht('SCHREIBEN_ALLES');` + +Bei lese-Operationen wie `f_get_object_metadata` und `f_download_object` muss folgendes Recht geprüft werden: + +`pck_mitarbeiterrecht.p_hat_recht('LESEN_ALLES');` + +Schlägt eine Prüfung fehl, wirft die p_hat_recht Prozedur einen speziellen Application Error mit Fehlermeldung für den Benutzer. + +--- + +## Private Helpers (nur Body) + +| Name | Typ | Zweck | +|---|---|---| +| `f_get_cfg` | Funktion | Wrapper für `pck_system.f_get_par_wert_by_programmid` | +| `f_build_url` | Funktion | Baut vollständige OCI REST URL (`/n/{ns}/b/{bucket}/o/{key}`) | +| `p_assert_allowed` | Prozedur | Tenant-Prefix-Guard + Path-Traversal-Prüfung | +| `f_make_request` | Funktion | Wrapper für `apex_web_service.make_rest_request`, wertet HTTP-Status aus, gibt Response-Body zurück | + +--- + +## Pagination (`f_list_objects`) + +OCI liefert max. 1000 Objekte pro Request. `f_list_objects` ruft OCI intern in einer Schleife auf +(`nextStartWith`-Token aus JSON-Response) bis alle Seiten geladen sind. Ergebnis wird in einer +lokalen Collection gesammelt, am Ende als `sys_refcursor` geöffnet. +`i_limit = 0` → unbegrenzt, `i_limit > 0` → Abbruch wenn Limit erreicht. + +OCI-Response trennt Unterordner (`prefixes`-Array) von Dateien (`objects`-Array) — beide werden +eingesammelt, Ordner mit `is_folder = 'Y'`. + +--- + +## Fehlerbehandlung + +`f_make_request` wirft bei HTTP-Fehler: + +| HTTP-Status | Fehlercode | Meldung | +|---|---|---| +| 404 | -20001 | `Object not found: {key}` | +| 401 / 403 | -20002 | `OCI authentication failed` | +| 409 | -20007 | `Object already exists: {key}` | +| sonstige 4xx/5xx | -20006 | `OCI API error {status}: {response body}` | + +`p_assert_allowed` wirft: + +| Fall | Fehlercode | Meldung | +|---|---|---| +| `..` im Pfad | -20004 | `Path traversal attempt detected` | +| Pfad außerhalb Tenant-Prefix | -20005 | `Access denied: outside tenant scope` | + +Bucket nicht erreichbar → -20003 via `f_make_request` + +--- + +## Sicherheit / Tenant-Isolation + +`p_assert_allowed` wird am Anfang jeder öffentlichen Funktion/Prozedur aufgerufen: +1. Tenant-Prefix laden via `f_get_cfg('NET_STORAGE_TENANT_ID')` +2. `..`-Sequenzen im übergebenen Key prüfen → -20004 +3. Prüfen ob Key mit Tenant-Prefix beginnt → -20005 + +--- + +## Dateieingang-Verarbeitung + +### OCI-Struktur + +n8n legt Dateien in einem Unterordner ab, der nach der ZIP-Datei benannt ist. +Unterordner innerhalb der ZIP werden beibehalten: + +``` +eingang//datei1.csv +eingang//unterordner/datei3.csv +eingang//_READY_FOR_DB_PROCESSING_ ← Marker +``` + +### Marker-Datei + +n8n legt nach erfolgreichem Upload aller Dateien eine leere Marker-Datei ab: +`eingang//_READY_FOR_DB_PROCESSING_` + +Die DB verarbeitet einen Unterordner **ausschließlich wenn der Marker vorhanden ist.** +Der Marker wird erst gelöscht wenn **alle** Dateien des Unterordners erfolgreich +verarbeitet wurden — so werden fehlgeschlagene Dateien beim nächsten Lauf erneut versucht. + +### Prozedur `p_process_incoming_files` (in separatem Fachpackage, nicht in pck_net_storage) + +``` +1. Unterordner von eingang/ auflisten (f_list_objects mit delimiter='/') +2. Für jeden Unterordner: + a. Marker eingang//_READY_FOR_DB_PROCESSING_ vorhanden? + → nein: überspringen (unvollständiger Upload) + b. Alle Dateien in eingang// auflisten (ohne Marker) + c. Für jede Datei einzeln: + begin + Fachliche Verarbeitung (Import) — noch kein Commit + log_object_ref := 'eingang//datei.csv' ← ZIP aus Pfad ableitbar + pck_net_storage.p_move_object(datei → Zielordner//) + commit + pck_log.p_info(...) + exception when others then + rollback + pck_log.p_error(i_object_ref => 'eingang//datei.csv', ...) + end + d. Noch Dateien in eingang// (außer Marker)? + → nein: pck_net_storage.p_delete_object(Marker) — Batch abgeschlossen + → ja: Marker bleibt — nächster Lauf versucht verbleibende Dateien +``` + +Rollback nach fehlgeschlagenem `p_move_object` macht auch den Import rückgängig. +Kein Verhalten wird aus `lg_app_log` abgeleitet — es dient ausschließlich dem Audit. + +### ORDS-Endpunkt + +`POST /ords/.../net_storage/process_incoming` → ruft `p_process_incoming_files` auf. +Wird von n8n nach erfolgreichem Upload aufgerufen. +Schlägt der Aufruf fehl: APEX Automation greift beim nächsten Stundenlauf. + +--- + +## APEX REST Data Source (manuell einrichten) + +Für das APEX Interactive Report im Dateimanager-Browser: + +``` +Name: NET_STORAGE_LIST_OBJECTS +Base-URL: https://objectstorage.{region}.oraclecloud.com +Endpoint: /n/{namespace}/b/{bucket}/o +Method: GET +Web Credential: OCI_OBJ_STORE_CRED + +URL-Parameter: + prefix → aktueller Ordner-Prefix (Bind-Variable aus APEX) + delimiter → / + limit → 1000 + start → für Pagination (optional) + +Spalten-Mapping: + objects[*].name → OBJECT_NAME + objects[*].size → OBJECT_SIZE + objects[*].timeModified → LAST_MODIFIED + objects[*].etag → ETAG +``` + +Hinweis: `prefixes` (Unterordner) und `objects` (Dateien) sind zwei separate JSON-Arrays. +Für ein gemeinsames IR entweder zwei REST Sources mit UNION, oder `f_list_objects` über +eine APEX Collection als IR-Quelle nutzen. + +--- + +## Logging: lg_app_log + pck_log + +### Tabelle `lg_app_log` + +Allgemeine Anwendungs-Log-Tabelle, modulübergreifend nutzbar. Keine Indizes (Log-Volumen +ist gering, Ad-hoc-Suche reicht ohne Index). + +| Spalte | Typ | Pflicht | Beschreibung | +|---|---|---|---| +| `lg_app_log_id` | `number generated by default as identity` | ja | PK | +| `log_timestamp` | `timestamp` | ja | Zeitpunkt des Log-Eintrags | +| `log_level` | `varchar2(10 char)` | ja | `INFO`, `WARN`, `ERROR` | +| `log_module` | `varchar2(100 char)` | ja | z.B. `PCK_NET_STORAGE` | +| `log_action` | `varchar2(100 char)` | nein | z.B. `PROCESS_FILE` | +| `log_object_ref` | `varchar2(512 char)` | nein | z.B. `eingang/datei.csv` | +| `log_message` | `varchar2(4000 char)` | nein | Kurzmeldung | +| `log_detail` | `clob` | nein | Stack Trace, JSON, etc. | +| `log_user` | `varchar2(100 char)` | nein | APEX- oder DB-User | +| `log_session_id` | `number` | nein | Oracle Session-ID | + +Keine Audit-Spalten (Log-Tabelle ist self-auditing durch `log_timestamp` + `log_user`). + +### Package `pck_log` + +Öffentliche Prozeduren: + +| Name | Signatur | +|---|---| +| `p_info` | `(i_module, i_action, i_message, i_object_ref default null)` | +| `p_warn` | `(i_module, i_action, i_message, i_object_ref default null)` | +| `p_error` | `(i_module, i_action, i_message, i_detail default null, i_object_ref default null)` | + +Intern immer `PRAGMA AUTONOMOUS_TRANSACTION` + `COMMIT` — Log-Einträge sind +transaktionsunabhängig und überleben Rollbacks der Haupttransaktion. + +--- + +## OCI API Endpunkte (Referenz) + +| Operation | Method | Pfad | +|---|---|---| +| List Objects | GET | `/n/{ns}/b/{bucket}/o?prefix=...&delimiter=/` | +| Get Object | GET | `/n/{ns}/b/{bucket}/o/{objectName}` | +| Put Object | PUT | `/n/{ns}/b/{bucket}/o/{objectName}` | +| Delete Object | DELETE | `/n/{ns}/b/{bucket}/o/{objectName}` | +| Head Object | HEAD | `/n/{ns}/b/{bucket}/o/{objectName}` | +| Rename Object | POST | `/n/{ns}/b/{bucket}/actions/renameObject` | diff --git a/workflow_dateieingang.md b/workflow_dateieingang.md new file mode 100644 index 0000000..548bfa2 --- /dev/null +++ b/workflow_dateieingang.md @@ -0,0 +1,131 @@ +# Workflow: Automatischer Dateieingang via n8n → OCI Object Storage → DB + +**Stand:** 2026-04-07 + +--- + +## Beteiligte Systeme + +| System | Rolle | +|---|---| +| **SFTP-Server** | Quelle — externer Lieferant legt ZIP-Dateien ab | +| **n8n** | Middleware — holt ZIP, entpackt, lädt Dateien + Marker in OCI hoch | +| **OCI Object Storage** | Zwischenspeicher — Eingangsordner, Zielordner nach Verarbeitung | +| **Oracle DB / APEX** | Verarbeitung — liest Dateien aus OCI, importiert Daten | + +--- + +## Ablauf + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ APEX Automation (stündlich) │ +│ │ +│ 1. Unterordner in eingang/ mit Marker vorhanden? │ +│ → ja: Dateien verarbeiten (gleiche Logik wie Schritt 4-6) │ +│ │ +│ 2. n8n Webhook auslösen (fire & forget) │ +└────────────────────────────┬────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ n8n Workflow │ +│ │ +│ 3a. ZIP-Datei vom SFTP-Server herunterladen │ +│ 3b. ZIP entpacken │ +│ 3c. Alle Dateien in OCI eingang// hochladen │ +│ (Unterordner aus der ZIP werden beibehalten) │ +│ → "Continue on Error" AUS: ein Fehler stoppt alles │ +│ 3d. Marker eingang//_READY_FOR_DB_PROCESSING_ │ +│ hochladen │ +│ 3e. ZIP auf SFTP umbenennen / verschieben │ +│ → erst NACH erfolgreichem Marker-Upload │ +│ 3f. ORDS-Endpunkt aufrufen │ +└────────────────────────────┬────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Oracle DB (via ORDS-Endpunkt oder APEX Automation) │ +│ │ +│ 4. Unterordner in eingang/ auflisten │ +│ 5. Für jeden Unterordner mit Marker: │ +│ Für jede Datei (außer Marker) einzeln: │ +│ a. Daten importieren (noch kein Commit) │ +│ → log_object_ref = eingang//datei.csv │ +│ b. Datei in Zielordner verschieben │ +│ c. Commit │ +│ d. Fehler → Rollback, ERROR in lg_app_log, nächste Datei │ +│ 6. Keine Dateien mehr im Unterordner (außer Marker)? │ +│ → Marker löschen (Batch abgeschlossen) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## OCI Ordnerstruktur + +``` +bucket/ + eingang/ + export_2026-04-07/ ← Unterordner = ZIP-Name + datei1.csv + datei2.csv + unterordner/ + datei3.csv + _READY_FOR_DB_PROCESSING_ ← Marker: Batch vollständig + / + export_2026-04-07/ ← gleiche Struktur nach Verarbeitung + datei1.csv + ... +``` + +Der Marker bleibt solange erhalten bis **alle** Dateien des Unterordners +erfolgreich verarbeitet wurden. So werden fehlgeschlagene Dateien beim +nächsten Lauf erneut aufgegriffen. + +--- + +## Fehlerfall-Verhalten + +**n8n: Upload einer Datei schlägt fehl** +- Workflow stoppt sofort ("Continue on Error" ist aus) +- Kein Marker wird geschrieben, ZIP bleibt unverändert auf SFTP +- ORDS wird nicht aufgerufen +- Beim nächsten Stundenlauf lädt n8n die ZIP erneut herunter +- Bereits hochgeladene Dateien werden überschrieben (OCI PUT idempotent) + +**n8n: ORDS-Aufruf schlägt fehl** +- Marker liegt in `eingang//`, Dateien sind vollständig hochgeladen +- Beim nächsten Stundenlauf findet APEX Automation den Marker und verarbeitet + +**DB: Verarbeitung einer einzelnen Datei schlägt fehl** +- Rollback — Datei bleibt in `eingang//` +- ERROR in `lg_app_log` mit `log_object_ref = eingang//datei.csv` +- Marker bleibt erhalten → nächste Dateien im Batch werden weiterverarbeitet +- Beim nächsten Lauf wird die fehlgeschlagene Datei erneut versucht + +**DB: p_move_object schlägt nach erfolgreichem Import fehl** +- Rollback des Imports → sauberer Ausgangszustand +- Datei bleibt in `eingang//`, Marker bleibt erhalten +- Nächster Lauf versucht es erneut + +--- + +## Warum kein Fehlerordner, keine Status-Tabelle? + +Der Zustand steckt im Dateisystem: +- Unterordner mit Marker = Batch bereit oder teilweise verarbeitet +- Unterordner ohne Marker = unvollständiger n8n-Upload, wird ignoriert +- Datei im Zielordner = erfolgreich verarbeitet +- Datei noch in `eingang//` = noch ausstehend oder fehlgeschlagen + +Fehlerdetails stehen in `lg_app_log`. Über `log_object_ref` ist jede Datei +eindeutig einer ZIP zugeordnet. Kein Verhalten wird aus dem Log abgeleitet — +es dient ausschließlich dem Audit-Trail. + +--- + +## Zeitplan + +APEX Automation läuft **1x pro Stunde**. n8n-Workflow wird dabei neu angestoßen — +läuft also ebenfalls stündlich, zeitlich versetzt nach dem Automation-Start.