Spaltenüberschrift. * * Zahlartneutral und damit bewusst außerhalb von {@see \App\EventPaymentModules}: Überweisung und * (künftig) SEPA-Lastschrift lesen denselben Kontoauszug, nur werten sie ihn verschieden aus. * * Ein Tenant überschreibt das Ruleset **ganz oder gar nicht** ({@see fromConfiguration()}). Ein * feldweiser Merge wäre die schlechtere Zusage: wer das Trennzeichen umstellt, weil seine Bank ein * anderes Format liefert, bekäme sonst weiterhin die Spaltennamen der GLS untergeschoben und suchte * den Fehler an der falschen Stelle. */ final readonly class BankStatementRuleset { /** Fachliche Felder, ohne die sich eine Zahlung nicht verarbeiten lässt. */ public const array REQUIRED_COLUMNS = ['payment_date', 'purpose', 'amount']; /** Auswählbare Zeichensätze; 'auto' probiert UTF-8 und fällt sonst auf Windows-1252 zurück. */ public const array CHARSETS = ['auto', 'UTF-8', 'Windows-1252', 'ISO-8859-15']; /** * @param array $columns fachliches Feld => Spaltenüberschrift */ public function __construct( public string $delimiter, public string $enclosure, public string $charset, public bool $hasHeader, public string $dateFormat, public string $decimalSeparator, public string $thousandsSeparator, public array $columns, ) { } /** Der app-weite Standard aus config/bankStatement.php. */ public static function default(): self { return self::fromArray((array) config('bankStatement.default_ruleset', [])); } /** * Ruleset aus einer Modul-Konfiguration. Leerer/fehlender Override -> App-Standard. * * @param array|null $override */ public static function fromConfiguration(?array $override): self { if ($override === null || $override === []) { return self::default(); } return self::fromArray($override); } /** * Einzelne fehlende Schlüssel werden aus dem Config-Standard ergänzt -- das ist kein Widerspruch * zum „ganz oder gar nicht": greift nur, wenn überhaupt ein Override existiert, und verhindert, * dass ein unvollständig gespeichertes Formular einen leeren Delimiter erzeugt. * * @param array $values */ private static function fromArray(array $values): self { $fallback = (array) config('bankStatement.default_ruleset', []); $columns = (array) ($values['columns'] ?? $fallback['columns'] ?? []); $columns = array_filter( array_map(static fn ($column): string => trim((string) $column), $columns), static fn (string $column): bool => $column !== '', ); $charset = (string) ($values['charset'] ?? $fallback['charset'] ?? 'auto'); return new self( delimiter: self::firstNonEmpty($values['delimiter'] ?? null, $fallback['delimiter'] ?? null, ';'), // Der Leerstring ist hier ein gültiger Wert (keine Anführungszeichen) und darf nicht // durch einen Fallback ersetzt werden. enclosure: (string) ($values['enclosure'] ?? $fallback['enclosure'] ?? ''), charset: in_array($charset, self::CHARSETS, true) ? $charset : 'auto', hasHeader: (bool) ($values['has_header'] ?? $fallback['has_header'] ?? true), dateFormat: self::firstNonEmpty($values['date_format'] ?? null, $fallback['date_format'] ?? null, 'd.m.Y'), decimalSeparator: self::firstNonEmpty($values['decimal_separator'] ?? null, $fallback['decimal_separator'] ?? null, ','), thousandsSeparator: (string) ($values['thousands_separator'] ?? $fallback['thousands_separator'] ?? '.'), columns: $columns, ); } private static function firstNonEmpty(mixed ...$candidates): string { foreach ($candidates as $candidate) { if ($candidate !== null && (string) $candidate !== '') { return (string) $candidate; } } return ''; } /** Spaltenüberschrift für ein fachliches Feld, oder null wenn nicht gemappt. */ public function column(string $field): ?string { return $this->columns[$field] ?? null; } /** * Pflichtfelder, für die keine Spalte gemappt ist. * * @return array */ public function missingRequiredColumns(): array { return array_values(array_filter( self::REQUIRED_COLUMNS, fn (string $field): bool => $this->column($field) === null, )); } /** @return array */ public function toArray(): array { return [ 'delimiter' => $this->delimiter, 'enclosure' => $this->enclosure, 'charset' => $this->charset, 'has_header' => $this->hasHeader, 'date_format' => $this->dateFormat, 'decimal_separator' => $this->decimalSeparator, 'thousands_separator' => $this->thousandsSeparator, 'columns' => $this->columns, ]; } }