# Firefly Import Preprocessor — Dokumentation **Version:** 1.0.0 **Datum:** 03. Mai 2026 **Status:** Production Ready 🌐 [English](README.md) --- ## 📋 Inhaltsverzeichnis 1. [Überblick](#überblick) 2. [Installation & Setup](#installation--setup) 3. [Schnellstart](#schnellstart) 4. [Konfiguration](#konfiguration) 5. [Transformationstypen](#transformationstypen) 6. [CLI-Referenz](#cli-referenz) 7. [Debug-Modus](#debug-modus) 8. [Firefly III Integration](#firefly-iii-integration) 9. [Architektur](#architektur) 10. [Fehlerbehandlung](#fehlerbehandlung) --- ## Überblick Der **Firefly Import Preprocessor** ist ein produktionsreifer PHP-Preprocessor für Banken-CSV-Exportdateien. Er transformiert Bankdaten in ein standardisiertes Format und kann sie optional in Firefly III importieren. ### Kernfeatures ✅ **Vollständige CSV-Transformation** mit komplexen Pipelines ✅ **Metadaten-Extraktion** mit Regex (IBAN, Währung, Kontoname) ✅ **14 Transformationstypen** für flexible Datenverarbeitung ✅ **Firefly III Integration** — CLI, Docker und HTTP-Upload ✅ **Debug-Modus** für Transparenz bei Verarbeitung ✅ **Production Ready** mit vollständiger Fehlerbehandlung ✅ **Zero Dependencies** für Core-Funktionalität ### Workflow ```text Input CSV ↓ Metadaten extrahieren (Regex) ↓ Datenzeilen transformieren (Pipeline) ↓ Output CSV schreiben ↓ [Optional] In Firefly III importieren ``` --- ## Installation & Setup ### Voraussetzungen - PHP 8.1+ - Composer (empfohlen) - [Optional] Docker für Firefly III Integration ### Installation ```bash # 1. Repository clonen/kopieren cd ff-imp-preprocessor # 2. Abhängigkeiten installieren (optional) composer install # 3. Konfiguration erstellen cp config/config.example.json config/config.json # Bearbeite config/config.json mit deinen Einstellungen # 4. Directories erstellen mkdir -p config/import/{source,output,archive,error} chmod 755 config/import/{source,output,archive,error} # 5. Test durchführen php bin/transformer.php validate config/config.json input.csv ``` --- ## Schnellstart ### 1. Konfiguration anpassen Bearbeite `config/config.json` und stelle sicher, dass die Extraction-Rules zu deinem CSV-Format passen: ```json { "metadata": { "extractionRules": [ { "name": "account_iban", "lineNumber": 2, "regex": "IBAN:\\s*([A-Z0-9 ]+)", "captureGroup": 1 } ] }, "csvStructure": { "headerLine": 5, "delimiter": ";", "encoding": "UTF-8" } } ``` ### 2. CSV validieren ```bash php bin/transformer.php validate config/config.json input.csv ``` ### 3. Transformation durchführen ```bash php bin/transformer.php transform input.csv config/config.json # Mit Debug-Modus für Fehlersuche php bin/transformer.php transform input.csv config/config.json --debug ``` ### 4. Output prüfen ```bash php bin/transformer.php test input.csv config/config.json --debug # Zeigt max. 10 transformierte Zeilen und Debug-Logs ``` --- ## Konfiguration ### config.json Struktur #### `metadata` - Metadaten-Extraktion ```json { "metadata": { "extractionRules": [ { "name": "account_iban", "lineNumber": 2, "regex": "IBAN:\\s*([A-Z0-9 ]+)", "captureGroup": 1 }, { "name": "currency_code", "lineNumber": 3, "regex": "Währung:\\s*([A-Z]{3})", "captureGroup": 1 } ] } } ``` | Feld | Typ | Beschreibung | | ------ | ----- | ------------- | | `name` | string | Name der Metadaten-Variable (verwendet in constantvalue) | | `lineNumber` | int | Zeilennummer in CSV (1-basiert, menschenlesbar) | | `regex` | string | Regex-Pattern zur Extraktion (ohne Delimiter) | | `captureGroup` | int | Nummer der Klammer-Gruppe (0=komplett, 1=erste Klammer, etc.) | **Beispiel Regex:** - Pattern: `IBAN:\s*([A-Z0-9 ]+)` - Input: `IBAN: CH93 0077 2020 6262 5252 7` - Capture Group 1: `CH93 0077 2020 6262 5252 7` #### `csvStructure` - CSV-Format ```json { "csvStructure": { "headerLine": 5, "delimiter": ";", "encoding": "UTF-8", "hasBom": false } } ``` | Feld | Typ | Default | Beschreibung | | ------ | ----- | --------- | ------------- | | `headerLine` | int | 5 | Zeilennummer der Header (1-basiert) | | `delimiter` | string | `;` | CSV-Delimiter | | `encoding` | string | `UTF-8` | Zeichenkodierung (UTF-8, ISO-8859-1, CP1252) | | `hasBom` | bool | false | Hat die Datei BOM (Byte Order Mark)? | #### `columnTransformations` - Spalten-Transformationen ```json { "columnTransformations": [ { "sourceColumn": "Buchungsdatum", "transformations": [ { "type": "dateformat", "fromFormat": "d.m.Y", "toFormat": "Y-m-d" } ], "outputColumn": "date", "outputAction": "overwrite" } ] } ``` **outputAction:** | Wert | Verhalten | |---|---| | `overwrite` | Ziel-Spalte mit dem Transformations-Ergebnis überschreiben (Standard) | | `create` | Ergebnis in eine neue Ausgabe-Spalte schreiben | | `append` | Ergebnis ans Ende des bestehenden Spalten-Werts anhängen. Mit `"appendDelimiter": " "` (beliebige Zeichenkette) wird ein Trennzeichen zwischen bestehendem und neuem Wert eingefügt — der Trennzeichen entfällt, wenn die Ziel-Spalte noch leer ist | | `append-if-not-empty` | Wie `append` (inkl. `appendDelimiter`), aber überspringt die Operation vollständig, wenn das Transformations-Ergebnis leer ist — geeignet für optionale Werte wie Tags oder Notiz-Zeilen | | `append-line` | Wie `append`, aber als Trennzeichen wird immer ein Zeilenumbruch `\n` verwendet; kein führender Zeilenumbruch wenn die Ziel-Spalte leer ist | | `overwrite-if-empty` | Ergebnis nur schreiben, wenn die Ziel-Spalte aktuell leer ist | | `overwrite-if-not-empty` | Ergebnis nur schreiben, wenn das Transformations-Ergebnis nicht leer ist | #### `directories` - Dateisystem ```json { "directories": { "source": "/opt/ff-imp-preprocessor/import/source", "output": "/opt/ff-imp-preprocessor/import/output", "archive": "/opt/ff-imp-preprocessor/import/archive", "error": "/opt/ff-imp-preprocessor/import/error" } } ``` | Feld | Beschreibung | | ------ | ------------- | | `source` | Eingabe-Verzeichnis | | `output` | Ausgabe-Verzeichnis | | `archive` | Archiv für verarbeitete Dateien | | `error` | Error-Verzeichnis für ungültige Dateien | #### `fireflyImport` - Firefly III Integration Optional. Mit dem Flag `--do-import` beim `transform`-Kommando (oder via `auto-import`) wird der Firefly III Data Importer nach dem Schreiben der Output-CSV aufgerufen. Details und vollständige Beispiele: [Firefly III Integration](#firefly-iii-integration). --- ## Transformationstypen Es gibt **14 unterstützte Transformationstypen**, die als Pipeline kombiniert werden können: ### 1. **trim** - Leerzeichen entfernen ```json { "type": "trim" } ``` - Input: ` Coop Pronto ` → Output: `Coop Pronto` --- ### 2. **lowercase** - Zu Kleinbuchstaben ```json { "type": "lowercase" } ``` - Input: `COOP PRONTO CHUR` → Output: `coop pronto chur` --- ### 3. **uppercase** - Zu Grossbuchstaben ```json { "type": "uppercase" } ``` - Input: `Coop Pronto Chur` → Output: `COOP PRONTO CHUR` --- ### 4. **ucwordsfirst** - Grossschreibung nach Trennzeichen ```json { "type": "ucwordsfirst" } ``` - `COOP PRONTO CHUR` → `Coop Pronto Chur` - `migros-rail city` → `Migros-Rail City` - `O'NEILL STORE` → `O'Neill Store` Trennzeichen: Leerzeichen, Bindestrich, Apostroph, Slash, Punkt, Komma, Semikolon, Doppelpunkt, Klammern. > **Guard:** Wenn der Eingabe-String bereits sowohl Groß- als auch Kleinbuchstaben enthält (gemischte Groß-/Kleinschreibung), wird er unverändert zurückgegeben. So werden bereits korrekt formatierte Strings wie `"Coop pronto chur"` nicht verändert. Vollständig groß- oder kleingeschriebene Strings werden weiterhin verarbeitet. --- ### 5. **replace** - String-Replacement ```json { "type": "replace", "search": " ", "replace": " " } ``` - Input: `Coop Pronto` → Output: `Coop Pronto` --- ### 6. **split** - Spalte teilen ```json { "type": "split", "delimiter": ";", "part": 0 } ``` - Input: `Coop Pronto Chur;7007 Chur` → Output: `Coop Pronto Chur` --- ### 7. **regex** - Regex-Ersetzung ```json { "type": "regex", "pattern": "^(.*?);.*$", "replace": "$1" } ``` - Kein Match → Originalwert bleibt **unverändert** (pipeline-sicher) --- ### 8. **regexextract** - Regex-Extraktion ```json { "type": "regexextract", "pattern": "(\\d{4,} [^;]+)" } ``` - Kein Match → leerer String (**nicht** pipeline-sicher) --- ### 9. **dateformat** - Datum-Umformat ```json { "type": "dateformat", "fromFormat": "d.m.Y", "toFormat": "Y-m-d" } ``` - Input: `10.12.2025` → Output: `2025-12-10` --- ### 10. **truncate** - String kürzen ```json { "type": "truncate", "maxLength": 100 } ``` --- ### 11. **constantvalue** - Konstanten-Wert aus Metadaten ```json { "sourceColumn": "_constant_", "transformations": [{ "type": "constantvalue", "metadataKey": "account_iban" }], "outputColumn": "account_iban", "outputAction": "create" } ``` --- ### 12. **map** - Spalte kopieren ```json { "type": "map" } ``` --- ### 13. **pipeline** - Verschachtelte Pipeline ```json { "type": "pipeline", "steps": [ { "type": "trim" }, { "type": "lowercase" }, { "type": "ucwordsfirst" } ] } ``` --- ### 14. **timeperiod** - Zeit einer Tagesperiode zuordnen Parst eine Zeitangabe und gibt das Label des passenden Perioden-Bereichs zurück. Unterstützt mitternachtübergreifende Bereiche (z. B. 22:00–03:59). Gibt `default` (standardmäßig leer) zurück, wenn keine Periode passt oder die Eingabe ungültig ist. ```json { "type": "timeperiod", "timeFormat": "H:i:s", "periods": [ { "from": "04:00:00", "to": "08:59:59", "label": "Morgen" }, { "from": "09:00:00", "to": "10:59:59", "label": "Vormittag" }, { "from": "11:00:00", "to": "13:59:59", "label": "Mittag" }, { "from": "14:00:00", "to": "17:59:59", "label": "Nachmittag" }, { "from": "18:00:00", "to": "21:59:59", "label": "Abend" }, { "from": "22:00:00", "to": "03:59:59", "label": "Nacht" } ], "default": "" } ``` - `"09:30:00"` → `"Vormittag"` - `"23:00:00"` → `"Nacht"` (mitternachtübergreifender Bereich) - `"02:00:00"` → `"Nacht"` (mitternachtübergreifender Bereich) - `""` oder nicht parsbare Eingabe → `""` `timeFormat` folgt der PHP-Syntax `DateTime::createFromFormat` (Standard: `H:i:s`). --- ### Zeilen-Filterung — `skipIf` Zeilen können durch einen Top-Level-Schlüssel `skipIf` in der Konfiguration ausgeschlossen werden. Der Wert ist ein Filter-Knoten — entweder eine einzelne Bedingung oder eine verschachtelte `and`/`or`-Gruppe. **Einzelne Bedingung:** ```json "skipIf": { "column": "Buchungstext", "operator": "equals", "value": "Saldovortrag" } ``` **AND-Gruppe:** ```json "skipIf": { "and": [ { "column": "Beschreibung1", "operator": "empty" }, { "column": "Beschreibung2", "operator": "empty" } ] } ``` **Verschachtelte AND/OR-Gruppen:** ```json "skipIf": { "or": [ { "column": "Amount", "operator": "gt", "value": "10000" }, { "and": [ { "column": "Type", "operator": "equals", "value": "Saldo" }, { "column": "Notes", "operator": "empty" } ] } ] } ``` **Unterstützte Operatoren:** | Operator | Passt wenn… | |---|---| | `empty` | Spaltenwert ist leer | | `not-empty` | Spaltenwert ist nicht leer | | `equals` | Spaltenwert gleich `"value"` | | `not-equals` | Spaltenwert ungleich `"value"` | | `contains` | Spaltenwert enthält `"value"` | | `not-contains` | Spaltenwert enthält `"value"` nicht | | `matches` | Spaltenwert entspricht Regex `"pattern"` | | `not-matches` | Spaltenwert entspricht Regex `"pattern"` nicht | | `gt` | `(float) Spalte > (float) value` | | `gte` | `(float) Spalte >= (float) value` | | `lt` | `(float) Spalte < (float) value` | | `lte` | `(float) Spalte <= (float) value` | --- ### Pipeline-Beispiel ```json { "sourceColumn": "Buchungstext", "transformations": [ { "type": "trim" }, { "type": "replace", "search": " ", "replace": " " }, { "type": "lowercase" }, { "type": "ucwordsfirst" } ], "outputColumn": "description", "outputAction": "overwrite" } ``` **Verarbeitung:** 1. `" COOP PRONTO "` → trim → `"COOP PRONTO"` 2. `"COOP PRONTO"` → replace → `"COOP PRONTO"` 3. `"COOP PRONTO"` → lowercase → `"coop pronto"` 4. `"coop pronto"` → ucwordsfirst → `"Coop Pronto"` --- ## CLI-Referenz ```bash php bin/transformer.php [input] [config] [options] ``` ### Kommandos | Kommando | Beschreibung | | -------- | ------------- | | `test` | Test-Run (max. 10 Zeilen) | | `transform` | Vollständige Transformation | | `validate` | Konfiguration validieren | | `auto-import` | Verzeichnis-Überwachung | | `help` | Hilfe anzeigen | ### Optionen | Option | Beschreibung | | -------- | ------------- | | `--debug`, `-d` | Debug-Modus aktivieren | | `--rows=N` | Max. N Zeilen (test-Kommando) | | `--output=FILE`, `-o` | Output-Pfad | | `--do-import` | Nach der Transformation in Firefly III importieren (`transform`) | | `--strict` | Strikte Validierung | | `--watch` | Kontinuierliche Überwachung | | `--interval=SEC` | Prüfintervall in Sekunden | | `--dry-run` | Simulationsmodus | --- ## Debug-Modus ```bash php bin/transformer.php test input.csv config/config.json --debug ``` Der Debug-Modus protokolliert Ereignisse in folgenden Kategorien: | Kategorie | Wann | | ----------- | ------ | | `transformer` | Anfang/Ende Transformation | | `csv_reader` | Beim CSV lesen | | `metadata` | Bei Metadaten-Extraktion | | `metadata_warning` | Bei Problemen | | `transformation` | Bei jeder Transformation | | `csv_writer` | Beim CSV schreiben | --- ## Firefly III Integration Drei Betriebsmodi decken alle typischen Deployment-Szenarien ab. **`chunkSize`** (optional, Standard: 0 = deaktiviert): Die Output-CSV wird vor dem Import in Blöcke von maximal N Datenzeilen aufgeteilt. Jeder Block wird als separate Anfrage gesendet. Das verhindert serverseitige Timeouts bei grossen Dateien (Faustregel: ~3–4 s/Transaktion im HTTP-Modus). Der `timeout`-Wert gilt pro Block, nicht für den gesamten Lauf. **`chunkRetries`** (optional, Standard: 0 = kein Retry): Anzahl zusätzlicher Importversuche pro Block nach dem ersten. Bei einem Fehler wiederholt der Importer den Upload bis zu dieser Anzahl, bevor er abbricht. Nur wirksam wenn `chunkSize > 0`. **`chunkRetryDelay`** (optional, Standard: 0 = keine Pause): Pause in Sekunden vor jedem Block-Request ab dem zweiten Block sowie zwischen Wiederholungsversuchen desselben fehlgeschlagenen Blocks. Ein einziger Wert für Cooldown und Retry-Back-off. Nur wirksam wenn `chunkSize > 0`. **`connectionTimeout`** (optional, Standard: 10): Maximale Wartezeit in Sekunden für den Aufbau der TCP-Verbindung zum Importer-Server. Unabhängig von `timeout` (der die gesamte Übertragungsdauer begrenzt). Nur im Modus `http`. ### Modus `cli` Transformer und Importer auf demselben Server. ```json "fireflyImport": { "mode": "cli", "jsonConfig": "/opt/firefly-data-importer/storage/configurations/ubs-import.json", "importerCommand": "php /opt/firefly-data-importer/artisan importer:import", "chunkSize": 50, "chunkRetries": 3, "chunkRetryDelay": 10, "timeout": 300, "environment": { "FIREFLY_III_URL": "https://localhost", "FIREFLY_III_ACCESS_TOKEN": "your-token-here" } } ``` ### Modus `docker` Transformer lokal, Importer in Docker. Das Ausgabeverzeichnis muss als Volume eingebunden sein. `jsonConfig` ist der Pfad **innerhalb des Containers**. ```json "fireflyImport": { "mode": "docker", "jsonConfig": "/import/configs/ubs-import.json", "importerCommand": "docker exec firefly-importer php artisan importer:import", "chunkSize": 50, "chunkRetries": 3, "chunkRetryDelay": 10, "timeout": 300 } ``` ### Modus `http` Transformer lokal, Importer über HTTP(S) erreichbar. Benötigt `ext-curl`. **Voraussetzungen auf dem Importer-Server:** ```text CAN_POST_FILES=true AUTO_IMPORT_SECRET= # mindestens 16 Zeichen ``` ```json "fireflyImport": { "mode": "http", "importerUrl": "https://importer.your-server.com", "personalSecret": "your-auto-import-secret-min-16-chars", "accessToken": "your-firefly-iii-personal-access-token", "jsonConfig": "config/ubs-import.json", "chunkSize": 50, "chunkRetries": 3, "chunkRetryDelay": 10, "connectionTimeout": 10, "timeout": 300 } ``` Die Anfrage geht an `POST {importerUrl}/autoupload?secret={personalSecret}` mit CSV und JSON-Config als Multipart-Felder. `accessToken` wird als `Authorization: Bearer` gesendet. Falls `FIREFLY_III_ACCESS_TOKEN` bereits in der Importer-Umgebung gesetzt ist, kann `accessToken` weggelassen werden. --- ### Serverseitige Konfiguration Bei grossen Importen liegt der Engpass meist auf dem Firefly III Data Importer-Server, nicht im Transformer. Die folgenden Einstellungen gehören in die Umgebung des Importers (`.env` oder `docker-compose.yml`): | Einstellung | Empfohlener Wert | Hinweis | |---|---|---| | `PHP_MEMORY_LIMIT` | `512M` – `2048M` | Docker-Umgebungsvariable. Erhöhen, wenn PHP mit „Allowed memory size exhausted" abbricht. | | `CONNECTION_TIMEOUT` | `60` | Sekunden für den TCP-Verbindungsaufbau zu Firefly III. Standard ~31 s (π × 10). | | `IGNORE_DUPLICATE_ERRORS` | `true` | Doppelte Transaktionswarnungen bei Wiederholungsimporten unterdrücken. | **nginx Reverse Proxy** (falls vorhanden): ```nginx proxy_read_timeout 600s; # muss länger sein als der längste Einzelblock-Import client_max_body_size 64M; # muss die grösste Chunk-CSV abdecken ``` **Docker Compose** Beispiel: ```yaml services: firefly-importer: environment: - PHP_MEMORY_LIMIT=1024M - CONNECTION_TIMEOUT=60 - IGNORE_DUPLICATE_ERRORS=true ``` --- ### Verwendung ```bash # Nur transformieren (kein Import) php bin/transformer.php transform input.csv config/config.json # Transformieren und in Firefly III importieren php bin/transformer.php transform input.csv config/config.json --do-import # Watch-Modus: automatisch transformieren und importieren bei neuer CSV php bin/transformer.php auto-import config/config.json --watch ``` --- ## Architektur ```text bin/transformer.php (CLI Entry Point) ↓ TransformerEngine (Orchestrierung) ├─ ConfigurationLoader (Config laden/validieren) ├─ CsvReader (CSV einlesen) ├─ MetadataExtractor (Metadaten mit Regex) ├─ ColumnTransformer (Transformationen anwenden) ├─ CsvWriter (CSV schreiben) ├─ FireflyImporter (Firefly III Integration) └─ DebugLogger (Debug-Protokolle) ``` | Klasse | Verantwortung | | -------- | --------------- | | `TransformerEngine` | Orchestriert gesamten Workflow | | `ConfigurationLoader` | Lädt und validiert JSON-Konfiguration | | `CsvReader` | Liest CSV mit Metadaten | | `MetadataExtractor` | Extrahiert Metadaten mit Regex | | `ColumnTransformer` | Transformiert Spalten (Pipeline) | | `CsvWriter` | Schreibt CSV | | `FireflyImporter` | Importiert in Firefly III | | `DebugLogger` | Statischer Logger für Debug | --- ## Fehlerbehandlung ### Häufige Fehler #### "Input file not found" ```bash # Prüfe Dateipfad ls -la input.csv # Nutze absoluten Pfad wenn relativ nicht funktioniert php bin/transformer.php transform /absolute/path/input.csv config.json ``` #### "Missing metadata: account_iban" ```bash # Prüfe erste Zeilen des CSV head -5 input.csv # Überprüfe lineNumber und regex in config.json php bin/transformer.php validate config.json input.csv --debug ``` #### "Invalid JSON" ```bash php -r "json_decode(file_get_contents('config/config.json'), true) or die('JSON invalid');" ``` #### "Configuration: 'csvStructure.headerLine' required" ```bash diff config/config.json config/config.example.json ``` --- ## Version & Änderungen **v1.0.0 (03. Mai 2026)** - ✅ Initial Release - ✅ 14 Transformationstypen - ✅ Metadaten-Extraktion mit Regex - ✅ Debug-Modus - ✅ Firefly III Integration (cli / docker / http) - ✅ Vollständige Dokumentation --- **Lizenz:** GPL-3.0 **Author:** PHP CSV Transformer Project **Repository:** [git.andare.ch/david.reindl/ff-imp-preprocessor](https://git.andare.ch/david.reindl/ff-imp-preprocessor)