From c8b113bb9c7c43466cd59b8415d0c62ef6840707 Mon Sep 17 00:00:00 2001 From: jwtue Date: Mon, 7 Sep 2026 14:10:43 +0300 Subject: [PATCH] Expose the credit card merchant name as a dedicated field GetCreditCardStatement so far exposed the Verwendungszweck only as a single joined string via getPurpose(), which concatenates the merchant name with the merchant location and the masked card number. That string is unsuitable as a counterparty name, because the same merchant yields a different purpose per location and per card. Keep the individual Verwendungszweck lines on CreditCardTransaction and add getPurposeLines() plus getMerchant() (the first line, usually the merchant). getPurpose() is unchanged. --- Tests/Unit/Segment/DIKKUTest.php | 6 +++ .../CreditCardTransaction.php | 44 ++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/Tests/Unit/Segment/DIKKUTest.php b/Tests/Unit/Segment/DIKKUTest.php index 03fac8a1..c2366323 100644 --- a/Tests/Unit/Segment/DIKKUTest.php +++ b/Tests/Unit/Segment/DIKKUTest.php @@ -174,6 +174,10 @@ public function testMapsToModel() $this->assertEquals('2026-07-18', $domestic->getBookingDate()->format('Y-m-d')); $this->assertEquals('2026-07-17', $domestic->getValutaDate()->format('Y-m-d')); $this->assertEquals('EXAMPLE SHOP BERLIN 555500******2233', $domestic->getPurpose()); + // getMerchant() returns just the first line, without the location and masked card number that + // getPurpose() carries, so the same merchant yields a stable name across bookings. + $this->assertEquals('EXAMPLE SHOP', $domestic->getMerchant()); + $this->assertEquals(['EXAMPLE SHOP', 'BERLIN 555500******2233'], $domestic->getPurposeLines()); $this->assertEquals('5411', $domestic->getMerchantCategoryCode()); // No conversion took place, so no original amount is reported. $this->assertNull($domestic->getOriginalAmount()); @@ -189,6 +193,8 @@ public function testMapsToModel() $this->assertEquals(1000.0, $settlement->getAmount()); $this->assertEquals(CreditCardTransaction::CD_CREDIT, $settlement->getCreditDebit()); $this->assertNull($settlement->getMerchantCategoryCode()); + // A booking without a real merchant still exposes its first line via getMerchant(). + $this->assertEquals('Ausgleich Kreditkartenabrechnung', $settlement->getMerchant()); } public function testEmptyStatement() diff --git a/src/Model/CreditCardStatement/CreditCardTransaction.php b/src/Model/CreditCardStatement/CreditCardTransaction.php index 382f8f1a..37786706 100644 --- a/src/Model/CreditCardStatement/CreditCardTransaction.php +++ b/src/Model/CreditCardStatement/CreditCardTransaction.php @@ -29,6 +29,12 @@ class CreditCardTransaction /** The exchange rate applied, or null if no conversion took place. */ protected ?float $exchangeRate = null; protected string $purpose = ''; + /** + * The individual Verwendungszweck lines, in order, non-empty only. {@link $purpose} is these + * joined by spaces. Usually the first line is the merchant name and the second the merchant + * location followed by the masked card number. + */ + protected array $purposeLines = []; protected ?string $reference = null; /** ISO 18245 merchant category code, e.g. 5411 for grocery stores. Null if the booking has no merchant. */ protected ?string $merchantCategoryCode = null; @@ -110,6 +116,41 @@ public function setPurpose(string $purpose): static return $this; } + /** + * @return string[] The individual Verwendungszweck lines, in order, non-empty only. Usually the + * first line is the merchant name and the second the merchant location followed by the masked + * card number. {@link getPurpose()} returns these joined by spaces. + */ + public function getPurposeLines(): array + { + return $this->purposeLines; + } + + /** + * @param string[] $purposeLines + */ + public function setPurposeLines(array $purposeLines): static + { + $this->purposeLines = $purposeLines; + return $this; + } + + /** + * The merchant name, taken from the first Verwendungszweck line. Prefer this over + * {@link getPurpose()} when you need a stable counterparty name (e.g. to name an expense account), + * because the full purpose also contains the location and the masked card number, which vary + * between bookings of the same merchant. + * + * For bookings without a merchant, such as the monthly settlement, this returns whatever + * descriptive text the bank placed in the first line (e.g. "Ausgleich Kreditkartenabrechnung"). + * + * @return string|null Null only if the record carries no Verwendungszweck at all. + */ + public function getMerchant(): ?string + { + return $this->purposeLines[0] ?? null; + } + public function getReference(): ?string { return $this->reference; @@ -193,7 +234,8 @@ public static function fromSegment(Kreditkartenumsatz $umsatz): CreditCardTransa $result->exchangeRate = $umsatz->umrechnungskurs; } - $result->purpose = trim(implode(' ', $umsatz->getVerwendungszweckLines())); + $result->purposeLines = $umsatz->getVerwendungszweckLines(); + $result->purpose = trim(implode(' ', $result->purposeLines)); $result->reference = $umsatz->referenz; $result->merchantCategoryCode = $umsatz->branchenschluessel; return $result;