setRawAttributes([ 'identifier' => $identifier, 'firstname' => $firstname, 'lastname' => $lastname, 'payment_purpose' => $purpose !== '' ? $purpose : "Sommerlager - Beitrag {$firstname} {$lastname}", ], true); $participant->amount = new Amount($amount, 'Euro'); $participant->amount_paid = new Amount($amountPaid, 'Euro'); $participant->payment_options = $paymentOptions; return $participant; } private function transaction( string $purpose, float $amount = 120.0, string $payerName = '', string $payerIban = '', ): BankTransaction { return new BankTransaction( paymentDate: CarbonImmutable::create(2026, 9, 9), amount: new Amount($amount, 'Euro'), purpose: $purpose, payerName: $payerName, payerIban: $payerIban, rowNumber: 2, ); } /** @param array $participants */ private function candidates(array $participants): Collection { return new Collection($participants); } public function test_only_credits_are_relevant_for_a_transfer(): void { $module = new AccountTransferPaymentModule(); $this->assertTrue($module->isRelevantTransaction($this->transaction('Beitrag', 120.0))); $this->assertFalse($module->isRelevantTransaction($this->transaction('Erstattung', -120.0))); } /** Regel 1: der beim Anmelden erzeugte Verwendungszweck steht unverändert im Auszug. */ public function test_matches_the_generated_payment_purpose(): void { $module = new AccountTransferPaymentModule(); $candidates = $this->candidates([ $this->participant('a', 'Max', 'Meier'), $this->participant('b', 'Lena', 'Kunze'), ]); $match = $module->matchTransaction($this->transaction('SOMMERLAGER - BEITRAG MAX MEIER'), $candidates); $this->assertNotNull($match); $this->assertSame('a', $match->participant->identifier); $this->assertSame(TransactionMatch::CONFIDENCE_CERTAIN, $match->confidence); } /** Regel 2: abgetippter Zweck, aber beide Namen sind noch drin -- auch mit Umlaut. */ public function test_matches_both_names_in_a_retyped_purpose(): void { $module = new AccountTransferPaymentModule(); $candidates = $this->candidates([$this->participant('a', 'Jörg', 'Müller')]); $match = $module->matchTransaction($this->transaction('Beitrag fuer Joerg Mueller, Lager'), $candidates); $this->assertNotNull($match); $this->assertSame('a', $match->participant->identifier); } /** Regel 3: Folgezahlung vom Konto, von dem schon einmal etwas kam. */ public function test_matches_a_follow_up_payment_by_known_iban(): void { $module = new AccountTransferPaymentModule(); $candidates = $this->candidates([ $this->participant('a', 'Max', 'Meier', paymentOptions: ['payer_iban' => 'DE02120300000000202051']), $this->participant('b', 'Lena', 'Kunze'), ]); // Zweck sagt nichts, IBAN in der Schreibweise der Bank (mit Leerzeichen). $match = $module->matchTransaction( $this->transaction('Restzahlung', 60.0, payerIban: 'DE02 1203 0000 0000 2020 51'), $candidates, ); $this->assertNotNull($match); $this->assertSame('a', $match->participant->identifier); } /** Regel 4: nur der Nachname im Zweck, aber der Betrag trifft den offenen Rest. */ public function test_matches_lastname_plus_exact_open_amount(): void { $module = new AccountTransferPaymentModule(); $candidates = $this->candidates([ $this->participant('a', 'Max', 'Meier', amount: 120.0, amountPaid: 40.0), $this->participant('b', 'Lena', 'Kunze', amount: 120.0), ]); $match = $module->matchTransaction($this->transaction('Restbetrag Meier', 80.0), $candidates); $this->assertNotNull($match); $this->assertSame('a', $match->participant->identifier); } /** Regel 5: das Konto läuft auf den Namen -- trägt oft, bei Elternkonten aber nicht. */ public function test_matches_payer_name_only_as_uncertain(): void { $module = new AccountTransferPaymentModule(); $candidates = $this->candidates([$this->participant('a', 'Max', 'Meier')]); $match = $module->matchTransaction($this->transaction('Ueberweisung', 99.0, payerName: 'Max Meier'), $candidates); $this->assertNotNull($match); $this->assertSame(TransactionMatch::CONFIDENCE_UNCERTAIN, $match->confidence); } /** * Zwei Namensgleiche: lieber kein Vorschlag als der falsche. In der Prüfansicht wäre die * Verwechslung nicht zu erkennen und würde durchgewinkt. */ public function test_no_suggestion_when_two_candidates_fit(): void { $module = new AccountTransferPaymentModule(); $candidates = $this->candidates([ $this->participant('a', 'Max', 'Meier'), $this->participant('b', 'Max', 'Meier'), ]); $this->assertNull($module->matchTransaction($this->transaction('Beitrag Max Meier'), $candidates)); } public function test_no_suggestion_when_nothing_fits(): void { $module = new AccountTransferPaymentModule(); $candidates = $this->candidates([$this->participant('a', 'Max', 'Meier')]); $this->assertNull($module->matchTransaction($this->transaction('Rechnung 4711', 60.0, payerName: 'ACME GmbH'), $candidates)); } public function test_records_the_payer_account(): void { $module = new AccountTransferPaymentModule(); $participant = $this->participant('a', 'Max', 'Meier'); $module->recordTransaction( $participant, $this->transaction('Beitrag', 120.0, payerName: 'Max Meier', payerIban: 'DE02 1203 0000 0000 2020 51'), ); $this->assertSame('DE02120300000000202051', $participant->payment_options['payer_iban']); $this->assertSame('Max Meier', $participant->payment_options['payer_account_owner']); $this->assertTrue($participant->refund_data); } /** * Manche Banken lassen den Namen bei Folgezahlungen leer. Der bereits bekannte Kontoinhaber darf * dadurch nicht verlorengehen -- ohne ihn ist die IBAN für die Erstattung wertlos. */ public function test_a_follow_up_without_payer_name_keeps_the_known_owner(): void { $module = new AccountTransferPaymentModule(); $participant = $this->participant('a', 'Max', 'Meier', paymentOptions: [ 'payer_iban' => 'DE02120300000000202051', 'payer_account_owner' => 'Max Meier', ]); $participant->refund_data = true; $module->recordTransaction( $participant, $this->transaction('Restzahlung', 60.0, payerName: '', payerIban: 'DE02120300000000202051'), ); $this->assertSame('Max Meier', $participant->payment_options['payer_account_owner']); $this->assertTrue($participant->refund_data); } /** * Eine IBAN mit Zahlendreher besteht die Prüfziffer nicht und wird nicht übernommen -- sonst * ginge die Erstattung später an eine fremde Person. */ public function test_ignores_an_invalid_iban(): void { $module = new AccountTransferPaymentModule(); $participant = $this->participant('a', 'Max', 'Meier'); $module->recordTransaction( $participant, $this->transaction('Beitrag', 120.0, payerName: 'Max Meier', payerIban: 'DE02120300000000202015'), ); $this->assertSame([], $participant->payment_options); $this->assertNotTrue($participant->refund_data); } }