From e1a7158931e94592adaa2b67749869f3bf210290 Mon Sep 17 00:00:00 2001 From: bsiggel Date: Fri, 23 Jan 2026 16:20:26 +0100 Subject: [PATCH] Add IBAN and BIC validation with custom hooks; update localization messages and cache timestamps --- README.md | 181 ++++++++++++++++++ .../CBankverbindungen/BankdatenValidation.php | 95 +++++++++ .../i18n/de_DE/CBankverbindungen.json | 7 +- .../i18n/en_US/CBankverbindungen.json | 5 + .../entityDefs/CBankverbindungen.json | 1 - .../metadata/formula/CBankverbindungen.json | 2 +- data/config.php | 4 +- 7 files changed, 290 insertions(+), 5 deletions(-) create mode 100644 custom/Espo/Custom/Hooks/CBankverbindungen/BankdatenValidation.php diff --git a/README.md b/README.md index 96cc5845..d9d47565 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,187 @@ EspoCRM verwendet ein hierarchisches Mehrsprachen-System mit **en_US als Basis-F - Teste Tooltips nach jedem Rebuild in beiden Sprachen - Bei neuen Feldern: erst i18n-Dateien vollständig ausfüllen, dann Rebuild +## Formula-Scripts und Custom PHP-Erweiterungen + +EspoCRM bietet mächtige Erweiterungsmöglichkeiten durch Formula-Scripts und Custom PHP-Funktionen. Diese ermöglichen Validierungen, Berechnungen und Business-Logik direkt beim Speichern von Datensätzen. + +### Formula-Scripts: Grundlagen + +**WICHTIG: Dateistruktur** + +❌ **FALSCH** - Formula in entityDefs: +```json +// custom/Espo/Custom/Resources/metadata/entityDefs/Entity.json +{ + "fields": {...}, + "formula": { + "beforeSaveApiScript": "..." // FUNKTIONIERT NICHT! + } +} +``` + +✅ **RICHTIG** - Separate Formula-Datei: +```json +// custom/Espo/Custom/Resources/metadata/formula/Entity.json +{ + "beforeSaveApiScript": "if (field != null) { ... }" +} +``` + +### Verfügbare Formula-Script-Typen + +1. **`beforeSaveApiScript`** - Wird vor dem Speichern ausgeführt (UI + API, ab v7.5+) +2. **`beforeSaveCustomScript`** - Nur bei internen Saves (ohne API) +3. **`afterSaveScript`** - Nach dem Speichern + +**Verwendung**: Validierungen, Berechnungen, Daten-Transformation vor dem Speichern + +### Verfügbare Formula-Funktionen + +**String-Funktionen:** +- `string\concatenate(str1, str2)` - Strings verbinden +- `string\replace(text, search, replace)` - Ersetzen +- `string\substring(text, start, length)` - Teilstring +- `string\length(text)` - Länge +- `string\test(text, pattern)` - Regex-Test +- ⚠️ **NICHT verfügbar**: `string\isEmpty()` → Verwende `field != null && field != ''` + +**Logik:** +- `if (condition) { ... }` +- `&&`, `||`, `!` (AND, OR, NOT) +- `==`, `!=`, `>`, `<`, `>=`, `<=` + +**Fehlerbehandlung:** +- `recordService\throwBadRequest('Fehlermeldung')` - Speichern abbrechen mit Fehlermeldung + +### Custom Formula-Funktionen erstellen + +Beispiel: IBAN-Validierung mit Modulo-97-Algorithmus + +**1. PHP-Klasse erstellen:** + +Pfad: `custom/Espo/Custom/Classes/FormulaFunctions/IbanGroup/ValidateType.php` + +```php +evaluate($args[0]); + + if (!$iban || !is_string($iban)) { + return false; + } + + // IBAN-Validierungs-Logik hier + // ... (siehe ValidateType.php für vollständige Implementierung) + + return $remainder === 1; // Modulo-97-Check + } +} +``` + +**2. Funktion registrieren:** + +Pfad: `custom/Espo/Custom/Resources/metadata/app/formula.json` + +```json +{ + "functionList": [ + "__APPEND__", + { + "name": "iban\\validate", + "insertText": "iban\\validate(IBAN)" + } + ], + "functionClassNameMap": { + "iban\\validate": "Espo\\Custom\\Classes\\FormulaFunctions\\IbanGroup\\ValidateType" + } +} +``` + +**3. Funktion verwenden:** + +Pfad: `custom/Espo/Custom/Resources/metadata/formula/CBankverbindungen.json` + +```json +{ + "beforeSaveApiScript": "if (iban != null && iban != '') {\n $ibanClean = string\\replace(iban, ' ', '');\n if (!iban\\validate($ibanClean)) {\n recordService\\throwBadRequest('Ungültige IBAN!');\n }\n}" +} +``` + +### Wichtige Hinweise zur Formula-Entwicklung + +**Namespace-Struktur:** +- Verzeichnis: `custom/Espo/Custom/Classes/FormulaFunctions/{GroupName}/` +- Namespace: `Espo\Custom\Classes\FormulaFunctions\{GroupName}` +- Klassenname: `{FunctionName}Type` (z.B. `ValidateType`) +- Muss `BaseFunction` erweitern + +**Funktionsnamen:** +- Format: `group\functionName` (z.B. `iban\validate`, `string\replace`) +- Backslash `\` wird verwendet (nicht `::` oder `/`) + +**Häufige Fehler:** + +| Fehler | Symptom | Lösung | +|--------|---------|--------| +| Formula in entityDefs statt formula/ | Script wird nicht ausgeführt | Separate `formula/{Entity}.json` erstellen | +| `string\isEmpty()` verwendet | Error: "Unknown function" | Verwende `field != null && field != ''` | +| Falsche Namespace-Struktur | Funktion nicht gefunden | Prüfe Namespace, Klassenname, Pfad | +| Funktion nicht registriert | "Unknown function" | Eintrag in `app/formula.json` erstellen | +| Keine `__APPEND__` in functionList | Überschreibt Core-Funktionen | Immer `"__APPEND__"` als erstes Element | + +### Debugging von Formula-Scripts + +**Logs prüfen:** +```bash +tail -n 100 /var/www/html/data/logs/espo-*.log | grep -i "formula\|error" +``` + +**Häufige Fehlermeldungen:** +- `Unknown function: xxx` → Funktion existiert nicht oder nicht registriert +- `Error 500` → Syntax-Fehler im Formula-Script oder PHP-Fehler +- `validationFailure` → `throwBadRequest()` wurde aufgerufen + +**Test-Workflow:** +1. Formula-Script schreiben +2. Rebuild ausführen: `bash custom/scripts/check_and_rebuild.sh` +3. Cache leeren (falls nötig): `rm -rf data/cache/*` +4. Testdaten speichern und Logs prüfen + +### Best Practices + +✅ **Empfohlen:** +- Separate Formula-Dateien pro Entity in `metadata/formula/` +- Wiederverwendbare Logik in Custom PHP-Funktionen auslagern +- Aussagekräftige Fehlermeldungen mit `throwBadRequest()` +- Null-Checks vor Operationen: `field != null && field != ''` +- Code kommentieren für spätere Wartung + +❌ **Vermeiden:** +- Formula-Scripts in `entityDefs` ablegen +- Nicht-existente String-Funktionen wie `isEmpty()` +- Komplexe Logik direkt in Formula (besser: PHP-Funktion) +- Fehlende Registrierung in `app/formula.json` + +### Beispiel-Anwendungsfälle + +1. **Validierung**: IBAN-Check, Email-Format, Telefonnummern +2. **Berechnung**: Gesamtpreise, Datumsberechnungen, Provisionen +3. **Daten-Transformation**: Großbuchstaben, Formatierungen, Normalisierungen +4. **Business Rules**: Status-Überprüfungen, Pflichtfeld-Logik, Abhängigkeiten + 3. Auslösen von Änderungen und Rebuild-Prozess Was Änderungen auslösen: diff --git a/custom/Espo/Custom/Hooks/CBankverbindungen/BankdatenValidation.php b/custom/Espo/Custom/Hooks/CBankverbindungen/BankdatenValidation.php new file mode 100644 index 00000000..8670e44a --- /dev/null +++ b/custom/Espo/Custom/Hooks/CBankverbindungen/BankdatenValidation.php @@ -0,0 +1,95 @@ +language = $language; + } + + public function beforeSave(Entity $entity, array $options): void + { + // IBAN-Normalisierung und Validierung + $iban = $entity->get('iban'); + if ($iban !== null && $iban !== '') { + // IBAN normalisieren: Leerzeichen entfernen und Großbuchstaben + $ibanClean = strtoupper(str_replace(' ', '', $iban)); + $entity->set('iban', $ibanClean); + + // Mathematische IBAN-Prüfung mit Modulo-97 + if (!$this->validateIban($ibanClean)) { + $message = $this->language->translateLabel('invalidIbanChecksum', 'messages', 'CBankverbindungen'); + throw new BadRequest($message); + } + } + + // BIC-Normalisierung und Validierung + $bic = $entity->get('bic'); + if ($bic !== null && $bic !== '') { + // BIC normalisieren: Leerzeichen entfernen und Großbuchstaben + $bicClean = strtoupper(str_replace(' ', '', $bic)); + $entity->set('bic', $bicClean); + + // BIC-Format prüfen: 8 oder 11 Zeichen + $bicLength = strlen($bicClean); + if ($bicLength !== 8 && $bicLength !== 11) { + $message = $this->language->translateLabel('invalidBicLength', 'messages', 'CBankverbindungen'); + throw new BadRequest($message); + } + + // BIC-Format prüfen: Regex + if (!preg_match('/^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$/', $bicClean)) { + $message = $this->language->translateLabel('invalidBicFormat', 'messages', 'CBankverbindungen'); + throw new BadRequest($message); + } + } + } + + private function validateIban(string $iban): bool + { + // IBAN-Länge prüfen (mindestens 15 Zeichen) + if (strlen($iban) < 15) { + return false; + } + + // IBAN umstellen: erste 4 Zeichen ans Ende + $rearranged = substr($iban, 4) . substr($iban, 0, 4); + + // Buchstaben in Zahlen umwandeln (A=10, B=11, ..., Z=35) + $numeric = ''; + for ($i = 0; $i < strlen($rearranged); $i++) { + $char = $rearranged[$i]; + if (ctype_alpha($char)) { + $numeric .= (string)(ord($char) - ord('A') + 10); + } else { + $numeric .= $char; + } + } + + // Modulo-97-Prüfung + return $this->bcmod($numeric, '97') === '1'; + } + + private function bcmod(string $number, string $modulus): string + { + // Für große Zahlen: Modulo in Schritten berechnen + $take = 9; + $mod = ''; + + do { + $a = (int)($mod . substr($number, 0, $take)); + $number = substr($number, $take); + $mod = (string)($a % (int)$modulus); + } while (strlen($number) > 0); + + return $mod; + } +} diff --git a/custom/Espo/Custom/Resources/i18n/de_DE/CBankverbindungen.json b/custom/Espo/Custom/Resources/i18n/de_DE/CBankverbindungen.json index 9a93b04b..2c0ba31d 100644 --- a/custom/Espo/Custom/Resources/i18n/de_DE/CBankverbindungen.json +++ b/custom/Espo/Custom/Resources/i18n/de_DE/CBankverbindungen.json @@ -48,5 +48,10 @@ "unclean": "Abweichungen", "failed": "Fehlgeschlagen" } + }, + "messages": { + "invalidIbanChecksum": "Die eingegebene IBAN ist ungueltig. Bitte pruefen Sie die Nummer auf Tippfehler.", + "invalidBicLength": "Der BIC/SWIFT-Code muss 8 oder 11 Zeichen lang sein.", + "invalidBicFormat": "Der eingegebene BIC/SWIFT-Code hat ein ungueltiges Format. Beispiel: COBADEFFXXX" } -} \ No newline at end of file +} diff --git a/custom/Espo/Custom/Resources/i18n/en_US/CBankverbindungen.json b/custom/Espo/Custom/Resources/i18n/en_US/CBankverbindungen.json index cf0203fa..e677e586 100644 --- a/custom/Espo/Custom/Resources/i18n/en_US/CBankverbindungen.json +++ b/custom/Espo/Custom/Resources/i18n/en_US/CBankverbindungen.json @@ -37,5 +37,10 @@ "unclean": "Discrepancies", "failed": "Failed" } + }, + "messages": { + "invalidIbanChecksum": "The entered IBAN is invalid. Please check for typos.", + "invalidBicLength": "The BIC/SWIFT code must be 8 or 11 characters long.", + "invalidBicFormat": "The entered BIC/SWIFT code has an invalid format. Example: COBADEFFXXX" } } \ No newline at end of file diff --git a/custom/Espo/Custom/Resources/metadata/entityDefs/CBankverbindungen.json b/custom/Espo/Custom/Resources/metadata/entityDefs/CBankverbindungen.json index 1da4499e..21f2bbca 100644 --- a/custom/Espo/Custom/Resources/metadata/entityDefs/CBankverbindungen.json +++ b/custom/Espo/Custom/Resources/metadata/entityDefs/CBankverbindungen.json @@ -19,7 +19,6 @@ "type": "varchar", "required": false, "maxLength": 11, - "pattern": "/^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$/i", "tooltip": true, "isCustom": true }, diff --git a/custom/Espo/Custom/Resources/metadata/formula/CBankverbindungen.json b/custom/Espo/Custom/Resources/metadata/formula/CBankverbindungen.json index 744c52e4..9c82339a 100644 --- a/custom/Espo/Custom/Resources/metadata/formula/CBankverbindungen.json +++ b/custom/Espo/Custom/Resources/metadata/formula/CBankverbindungen.json @@ -1,3 +1,3 @@ { - "beforeSaveApiScript": "// IBAN-Validierung mit Modulo-97-Algorithmus\nif (iban != null && iban != '') {\n // Leerzeichen entfernen für Validierung\n $ibanClean = string\\replace(iban, ' ', '');\n \n // Mathematische IBAN-Prüfung\n if (!iban\\validate($ibanClean)) {\n recordService\\throwBadRequest('Ungültige IBAN: Die Prüfziffer ist mathematisch falsch. Bitte überprüfen Sie die eingegebene IBAN.');\n }\n}" + "beforeSaveApiScript": "" } \ No newline at end of file diff --git a/data/config.php b/data/config.php index 1f700971..a15097b4 100644 --- a/data/config.php +++ b/data/config.php @@ -349,8 +349,8 @@ return [ 0 => 'youtube.com', 1 => 'google.com' ], - 'cacheTimestamp' => 1769180353, - 'microtime' => 1769180353.404299, + 'cacheTimestamp' => 1769181610, + 'microtime' => 1769181610.43814, 'siteUrl' => 'https://crm.bitbylaw.com', 'fullTextSearchMinLength' => 4, 'appTimestamp' => 1768843902,