From 651b6147bfee0feb966f73c42a0b8a48770a4d5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCnrher?= Date: Wed, 9 Sep 2026 16:55:08 +0200 Subject: [PATCH 1/6] Zahlungsparser --- .../TenantPaymentMethodsGetController.php | 5 + .../Views/Partials/TenantPaymentMethods.vue | 5 + .../BookBankStatementPaymentsCommand.php | 142 ++++ .../BookBankStatementPaymentsRequest.php | 19 + .../BookBankStatementPaymentsResponse.php | 17 + .../ParseBankStatementCommand.php | 183 ++++++ .../ParseBankStatementRequest.php | 21 + .../ParseBankStatementResponse.php | 30 + .../SetPaymentMethodsCommand.php | 23 +- .../BankStatementBookController.php | 37 ++ .../BankStatementParseController.php | 43 ++ app/Domains/Event/Routes/api.php | 5 + .../Views/Partials/BankStatementImport.vue | 622 ++++++++++++++++++ app/Domains/Event/Views/Partials/Overview.vue | 17 + .../Views/Partials/ParticipationFees.vue | 18 +- .../AbstractEventPaymentModule.php | 49 ++ app/EventPaymentModules/CLAUDE.md | 46 +- .../DTO/TransactionMatch.php | 25 + .../Modules/AccountTransferPaymentModule.php | 243 ++++++- .../ProvidesStatementRuleset.php | 25 + .../ReadsBankStatements.php | 42 ++ app/Models/EventParticipant.php | 4 + app/Models/PaymentMethod.php | 20 +- app/Providers/BankStatementParseProvider.php | 212 ++++++ .../EventParticipantRepository.php | 34 + app/Repositories/PaymentMethodRepository.php | 27 + app/Scopes/CommonController.php | 3 + app/Support/BankStatementParseException.php | 17 + app/ValueObjects/BankStatementRuleset.php | 139 ++++ app/ValueObjects/BankTransaction.php | 49 ++ app/Views/Components/BankRulesetEditor.vue | 254 +++++++ config/bankStatement.php | 48 ++ ...payment_tracking_to_event_participants.php | 35 + tests/Feature/BankStatementImportTest.php | 343 ++++++++++ tests/Unit/BankStatementMatchTest.php | 225 +++++++ tests/Unit/BankStatementParseTest.php | 173 +++++ tests/Unit/BankStatementRulesetTest.php | 107 +++ tests/Unit/EventPaymentModuleRegistryTest.php | 23 +- 38 files changed, 3308 insertions(+), 22 deletions(-) create mode 100644 app/Domains/Event/Actions/BookBankStatementPayments/BookBankStatementPaymentsCommand.php create mode 100644 app/Domains/Event/Actions/BookBankStatementPayments/BookBankStatementPaymentsRequest.php create mode 100644 app/Domains/Event/Actions/BookBankStatementPayments/BookBankStatementPaymentsResponse.php create mode 100644 app/Domains/Event/Actions/ParseBankStatement/ParseBankStatementCommand.php create mode 100644 app/Domains/Event/Actions/ParseBankStatement/ParseBankStatementRequest.php create mode 100644 app/Domains/Event/Actions/ParseBankStatement/ParseBankStatementResponse.php create mode 100644 app/Domains/Event/Controllers/BankStatementBookController.php create mode 100644 app/Domains/Event/Controllers/BankStatementParseController.php create mode 100644 app/Domains/Event/Views/Partials/BankStatementImport.vue create mode 100644 app/EventPaymentModules/DTO/TransactionMatch.php create mode 100644 app/EventPaymentModules/ProvidesStatementRuleset.php create mode 100644 app/EventPaymentModules/ReadsBankStatements.php create mode 100644 app/Providers/BankStatementParseProvider.php create mode 100644 app/Repositories/PaymentMethodRepository.php create mode 100644 app/Support/BankStatementParseException.php create mode 100644 app/ValueObjects/BankStatementRuleset.php create mode 100644 app/ValueObjects/BankTransaction.php create mode 100644 app/Views/Components/BankRulesetEditor.vue create mode 100644 config/bankStatement.php create mode 100644 database/migrations/2026_09_13_140010_add_payment_tracking_to_event_participants.php create mode 100644 tests/Feature/BankStatementImportTest.php create mode 100644 tests/Unit/BankStatementMatchTest.php create mode 100644 tests/Unit/BankStatementParseTest.php create mode 100644 tests/Unit/BankStatementRulesetTest.php diff --git a/app/Domains/Admin/Controllers/TenantPaymentMethodsGetController.php b/app/Domains/Admin/Controllers/TenantPaymentMethodsGetController.php index 824a7d4..59d1380 100644 --- a/app/Domains/Admin/Controllers/TenantPaymentMethodsGetController.php +++ b/app/Domains/Admin/Controllers/TenantPaymentMethodsGetController.php @@ -5,6 +5,7 @@ namespace App\Domains\Admin\Controllers; use App\Models\AvailablePaymentMethod; use App\Resources\AvailablePaymentMethodResource; use App\Scopes\CommonController; +use App\ValueObjects\BankStatementRuleset; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -16,6 +17,10 @@ class TenantPaymentMethodsGetController extends CommonController 'paymentMethods' => AvailablePaymentMethod::all() ->map(fn (AvailablePaymentMethod $method) => new AvailablePaymentMethodResource($method)->toArray($request)), 'saveEndpoint' => '/api/v1/admin/tenant/payment-methods', + // Der app-weite Standard für das Kontoauszug-Format. Das Formular zeigt ihn als + // Vorbelegung an, damit erkennbar ist, wovon ein Override abweicht. + 'statementRulesetDefault' => BankStatementRuleset::default()->toArray(), + 'statementRulesetCharsets' => BankStatementRuleset::CHARSETS, ]); } } diff --git a/app/Domains/Admin/Views/Partials/TenantPaymentMethods.vue b/app/Domains/Admin/Views/Partials/TenantPaymentMethods.vue index 1f60daa..a660416 100644 --- a/app/Domains/Admin/Views/Partials/TenantPaymentMethods.vue +++ b/app/Domains/Admin/Views/Partials/TenantPaymentMethods.vue @@ -1,5 +1,6 @@ + + + + diff --git a/app/Domains/Event/Views/Partials/Overview.vue b/app/Domains/Event/Views/Partials/Overview.vue index 870365f..2630d20 100644 --- a/app/Domains/Event/Views/Partials/Overview.vue +++ b/app/Domains/Event/Views/Partials/Overview.vue @@ -6,6 +6,7 @@ import EventAddons from "./EventAddons.vue"; import ParticipationSummary from "./ParticipationSummary.vue"; import CommonSettings from "./CommonSettings.vue"; import EventManagement from "./EventManagement.vue"; +import BankStatementImport from "./BankStatementImport.vue"; import Modal from "../../../../Views/Components/Modal.vue"; import MailCompose from "./MailCompose.vue"; import FullScreenModal from "../../../../Views/Components/FullScreenModal.vue"; @@ -21,6 +22,12 @@ const props = defineProps({ const displayData = ref('main'); const showEventData = ref(false); + const showBankStatement = ref(false); + + // Nach dem Buchen stimmen die Beitragsstände der Übersicht nicht mehr -- neu laden. + async function bankStatementBooked() { + await showMain(); + } async function showMain() { @@ -117,9 +124,18 @@ async function showEventAddons() {
+ +
+ + + @@ -158,6 +174,7 @@ async function showEventAddons() {
+



diff --git a/app/Domains/Event/Views/Partials/ParticipationFees.vue b/app/Domains/Event/Views/Partials/ParticipationFees.vue index 318e68a..0fe5fb6 100644 --- a/app/Domains/Event/Views/Partials/ParticipationFees.vue +++ b/app/Domains/Event/Views/Partials/ParticipationFees.vue @@ -26,6 +26,18 @@ const paymentMethodConfigurations = reactive({}) const showOptionsModal = ref(false) const optionsMethod = ref(null) +/** + * Die Optionen, die pro Aktion eingefroren werden. + * + * Optionen mit `scope: 'tenant'` bleiben draußen: Sie gelten für den ganzen Mandanten. Das + * Kontoauszug-Format etwa beschreibt die Bank, nicht die Zusage an die Teilnehmenden — hier + * eingefroren ließe sich nach einem Bankwechsel für laufende Aktionen nichts mehr importieren. + * Der Server hält sich beim Speichern an dieselbe Regel. + */ +function eventOptions(method) { + return (method?.optionsSchema ?? []).filter(option => option.scope !== 'tenant') +} + function openOptions(method) { optionsMethod.value = method showOptionsModal.value = true @@ -47,7 +59,7 @@ onMounted(async () => { for (const method of availablePaymentMethods.value) { const source = eventConfigBySlug[method.slug] ?? method.configuration ?? {} const config = {} - for (const option of method.optionsSchema ?? []) { + for (const option of eventOptions(method)) { config[option.name] = source[option.name] ?? '' } paymentMethodConfigurations[method.slug] = config @@ -358,7 +370,7 @@ onMounted(async () => { paymentMethod.name }}