tenant = Tenant::create([ 'slug' => 'tv', 'name' => 'Test', 'email' => 't@example.com', 'email_finance' => 'finance@example.com', 'url' => 'test.local', 'account_name' => 'Test e.V.', 'account_iban' => 'DE00', 'account_bic' => 'XY', 'city' => 'Stadt', 'postcode' => '00000', 'is_active_local_group' => true, 'has_active_instance' => true, ]); app()->instance('tenant', $this->tenant); PaymentMethod::create(['slug' => PaymentMethod::PAYMENT_ACCOUNT_TRANSACTION]); PaymentMethod::create(['slug' => PaymentMethod::PAYMENT_NOT_DEFINED]); PaymentMethod::create(['slug' => PaymentMethod::PAYMENT_SEPA_DIRECT_DEBIT]); DB::table('participation_fee_types')->insert([ 'slug' => 'FIXED', 'name' => 'Fixed', 'created_at' => now(), 'updated_at' => now(), ]); } private function availableMethod(string $slug, bool $active = false, array $config = []): AvailablePaymentMethod { return AvailablePaymentMethod::create([ 'tenant' => $this->tenant->slug, 'slug' => $slug, 'name' => 'Konto', 'description' => null, 'active' => $active, 'configuration' => $config, ]); } public function test_activation_is_blocked_when_required_options_missing(): void { $method = $this->availableMethod(PaymentMethod::PAYMENT_ACCOUNT_TRANSACTION); $response = new UpdateAvailablePaymentMethodAction(new UpdateAvailablePaymentMethodRequest( availablePaymentMethod: $method, name: 'Konto', description: null, active: true, configuration: ['iban' => 'DE123'], // account_owner fehlt ))->execute(); $this->assertFalse($response->success); $this->assertFalse($method->fresh()->active); } public function test_activation_succeeds_when_configuration_complete(): void { $method = $this->availableMethod(PaymentMethod::PAYMENT_ACCOUNT_TRANSACTION); $response = new UpdateAvailablePaymentMethodAction(new UpdateAvailablePaymentMethodRequest( availablePaymentMethod: $method, name: 'Konto', description: null, active: true, configuration: ['iban' => 'DE123', 'account_owner' => 'Kasse', 'bic' => 'XY', 'evil' => 'x'], ))->execute(); $this->assertTrue($response->success); $fresh = $method->fresh(); $this->assertTrue($fresh->active); $this->assertSame('DE123', $fresh->configuration['iban']); $this->assertArrayNotHasKey('evil', $fresh->configuration); // unbekannte Keys verworfen } public function test_event_assignment_copies_snapshot_and_preserves_overrides(): void { $this->availableMethod( PaymentMethod::PAYMENT_ACCOUNT_TRANSACTION, active: true, config: ['iban' => 'DE-TENANT', 'account_owner' => 'Kasse'], ); $event = $this->createEvent(); // 1. Zuweisung -> Snapshot der Tenant-Config landet im Pivot. $this->runUpdateEvent($event, [PaymentMethod::PAYMENT_ACCOUNT_TRANSACTION]); $pivotConfig = $event->fresh()->paymentMethods()->first()->pivot->configuration; $this->assertSame('DE-TENANT', $pivotConfig['iban']); // 2. Pro-Event-Override setzen. $this->runUpdateEvent($event, [PaymentMethod::PAYMENT_ACCOUNT_TRANSACTION], [ PaymentMethod::PAYMENT_ACCOUNT_TRANSACTION => ['iban' => 'DE-EVENT', 'account_owner' => 'Kasse'], ]); $this->assertSame('DE-EVENT', $event->fresh()->paymentMethods()->first()->pivot->configuration['iban']); // 3. Erneutes Speichern OHNE Override -> Override bleibt erhalten (kein Reset-Wipe). $this->runUpdateEvent($event, [PaymentMethod::PAYMENT_ACCOUNT_TRANSACTION]); $this->assertSame('DE-EVENT', $event->fresh()->paymentMethods()->first()->pivot->configuration['iban']); } public function test_sepa_activation_reports_invalid_values(): void { $method = $this->availableMethod(PaymentMethod::PAYMENT_SEPA_DIRECT_DEBIT); $config = $this->validSepaConfiguration(); foreach ([ 'creditor_id' => ['', 'Pflichtfelder'], 'iban' => ['DE02120300000000202015', 'IBAN ist ungültig'], 'pre_notification_days' => ['1', 'Vorlauf'], ] as $key => [$value, $message]) { $response = $this->updateMethod($method, true, array_merge($config, [$key => $value])); $this->assertFalse($response->success, $key); $this->assertStringContainsString($message, $response->message, $key); $this->assertFalse($method->fresh()->active, $key); } $response = $this->updateMethod($method, true, array_merge($config, ['creditor_id' => 'DE97ZZZ09999999999'])); $this->assertStringContainsString('Gläubiger-ID ist ungültig', $response->message); } public function test_sepa_can_be_saved_as_draft_and_activated_once_valid(): void { $method = $this->availableMethod(PaymentMethod::PAYMENT_SEPA_DIRECT_DEBIT); // Inaktiv darf auch eine unfertige Konfiguration gespeichert werden. $draft = $this->updateMethod($method, false, ['creditor_id' => 'unfertig']); $this->assertTrue($draft->success); $this->assertSame('unfertig', $method->fresh()->configuration['creditor_id']); $response = $this->updateMethod($method->fresh(), true, $this->validSepaConfiguration()); $this->assertTrue($response->success, $response->message); $this->assertTrue($method->fresh()->active); } public function test_tenant_bank_data_is_synced_into_transfer_and_direct_debit(): void { $transfer = $this->availableMethod(PaymentMethod::PAYMENT_ACCOUNT_TRANSACTION, config: ['icon' => 'coins']); $directDebit = $this->availableMethod(PaymentMethod::PAYMENT_SEPA_DIRECT_DEBIT, config: [ 'creditor_id' => 'DE98ZZZ09999999999', 'pre_notification_days' => 7, ]); new UpdateTenantPaymentAction(new UpdateTenantPaymentRequest( $this->tenant, 'DE02120300000000202051', 'BYLADEM1001', 'Stamm e.V.', ))->execute(); $this->assertSame('DE02120300000000202051', $transfer->fresh()->configuration['iban']); $this->assertSame('coins', $transfer->fresh()->configuration['icon']); $config = $directDebit->fresh()->configuration; $this->assertSame('Stamm e.V.', $config['account_owner']); $this->assertSame('DE02120300000000202051', $config['iban']); $this->assertSame('BYLADEM1001', $config['bic']); // Gläubiger-ID und Vorlauf gehören nicht zu den Bankdaten und bleiben stehen. $this->assertSame('DE98ZZZ09999999999', $config['creditor_id']); $this->assertSame(7, $config['pre_notification_days']); } public function test_event_assignment_rejects_an_incomplete_configuration(): void { $this->availableMethod(PaymentMethod::PAYMENT_SEPA_DIRECT_DEBIT, true, $this->validSepaConfiguration()); $event = $this->createEvent(); $response = new SetPaymentMethodsCommand(new SetPaymentMethodsRequest( $event, [PaymentMethod::PAYMENT_SEPA_DIRECT_DEBIT], [PaymentMethod::PAYMENT_SEPA_DIRECT_DEBIT => array_merge($this->validSepaConfiguration(), ['creditor_id' => 'DE97ZZZ09999999999'])], ))->execute(); $this->assertFalse($response->success); $this->assertStringContainsString('SEPA-Lastschrift', $response->message); $this->assertSame(0, $event->paymentMethods()->count()); // nichts geschrieben // Ohne Override greift der (gültige) Snapshot der Tenant-Config. $this->runUpdateEvent($event, [PaymentMethod::PAYMENT_SEPA_DIRECT_DEBIT]); $this->assertSame( 'DE98ZZZ09999999999', $event->fresh()->paymentMethods()->first()->pivot->configuration['creditor_id'], ); } public function test_new_tenant_gets_payment_methods_inactive_until_configured(): void { $parentGroup =ParentGroup::where('is_fallback', true)->first() ?? ParentGroup::create(['name' => 'Ohne Zuordnung', 'is_fallback' => true]); $response = new CreateTenantAction(new CreateTenantRequest( name: 'Neu', slug: 'neu', url: 'neu.local', parentGroup: $parentGroup, ))->execute(); $this->assertTrue($response->success, $response->message); $methods = AvailablePaymentMethod::withoutGlobalScope(SiteScope::class)->where('tenant', 'neu')->get(); $this->assertCount(3, $methods); $this->assertTrue($methods->every(fn (AvailablePaymentMethod $method) => !$method->active)); } /** @return array */ private function validSepaConfiguration(): array { return [ 'account_owner' => 'Test e.V.', 'iban' => 'DE02120300000000202051', 'bic' => '', 'creditor_id' => 'DE98ZZZ09999999999', 'pre_notification_days' => '5', ]; } /** @param array $configuration */ private function updateMethod(AvailablePaymentMethod $method, bool $active, array $configuration): UpdateAvailablePaymentMethodResponse { return new UpdateAvailablePaymentMethodAction(new UpdateAvailablePaymentMethodRequest( availablePaymentMethod: $method, name: 'SEPA-Lastschrift', description: null, active: $active, configuration: $configuration, ))->execute(); } private function createEvent(): Event { return Event::create([ 'tenant' => $this->tenant->slug, 'name' => 'Event', 'identifier' => 'evt-1', 'location' => 'Ort', 'postal_code' => '00000', 'email' => 'e@example.com', 'start_date' => '2026-08-01', 'end_date' => '2026-08-05', 'early_bird_end' => '2026-07-01', 'registration_final_end' => '2026-07-20', 'early_bird_end_amount_increase' => 0, 'account_owner' => 'Owner', 'account_iban' => 'DE00', 'participation_fee_type' => 'FIXED', 'pay_per_day' => false, 'pay_direct' => false, ]); } /** * @param array $slugs * @param array> $configs */ private function runUpdateEvent(Event $event, array $slugs, array $configs = []): void { $request = new SetPaymentMethodsRequest($event, $slugs, $configs); $response = new SetPaymentMethodsCommand($request)->execute(); $this->assertTrue($response->success, $response->message); } }