From 57be620c5912ab9fb9c74eccd25a466efe80e03d Mon Sep 17 00:00:00 2001 From: Cindy Emile Date: Thu, 13 Aug 2026 10:25:42 +0400 Subject: [PATCH 1/5] Extract remittance_information from Linxo transactions Linxo sends a remittance_information field with each transaction: the free-text reference the counterparty attaches to the payment. It was not extracted, so consumers could only reach it through the raw payload. The exact location in the v3 payload is not confirmed yet, so it is read from the root first and from the enrichments block as a fallback: the same move happened to `notes`, which Linxo relocated into `enrichments` in v3. Co-Authored-By: Claude Opus 5 (1M context) --- phpstan-baseline.neon | 6 ++--- src/Dto/TransactionDto.php | 24 +++++++++++++++++-- tests/Dto/TransactionDtoTest.php | 41 ++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 5 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 2634ffc..fd4c51e 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -13,7 +13,7 @@ parameters: path: src/ApiClient.php - - message: '#^Parameter \#1 \$data of class AssoConnect\\LinxoClient\\Dto\\TransactionDto constructor expects array\{id\: string, account_id\: string, amount\: array\{amount\: string, currency\: string\}, enrichments\: array\{display_label\?\: string, date\: string, notes\?\: string\}, type\?\: string\}, array given\.$#' + message: '#^Parameter \#1 \$data of class AssoConnect\\LinxoClient\\Dto\\TransactionDto constructor expects array\{id\: string, account_id\: string, amount\: array\{amount\: string, currency\: string\}, enrichments\: array\{display_label\?\: string, date\: string, notes\?\: string, remittance_information\?\: string\}, type\?\: string, remittance_information\?\: string\}, array given\.$#' identifier: argument.type count: 1 path: src/ApiClient.php @@ -67,9 +67,9 @@ parameters: path: tests/Dto/TransactionDtoTest.php - - message: '#^Parameter \#1 \$data of class AssoConnect\\LinxoClient\\Dto\\TransactionDto constructor expects array\{id\: string, account_id\: string, amount\: array\{amount\: string, currency\: string\}, enrichments\: array\{display_label\?\: string, date\: string, notes\?\: string\}, type\?\: string\}, array\ given\.$#' + message: '#^Parameter \#1 \$data of class AssoConnect\\LinxoClient\\Dto\\TransactionDto constructor expects array\{id\: string, account_id\: string, amount\: array\{amount\: string, currency\: string\}, enrichments\: array\{display_label\?\: string, date\: string, notes\?\: string, remittance_information\?\: string\}, type\?\: string, remittance_information\?\: string\}, array\ given\.$#' identifier: argument.type - count: 11 + count: 15 path: tests/Dto/TransactionDtoTest.php - diff --git a/src/Dto/TransactionDto.php b/src/Dto/TransactionDto.php index 4cb81a5..f0cfc86 100644 --- a/src/Dto/TransactionDto.php +++ b/src/Dto/TransactionDto.php @@ -13,8 +13,14 @@ * id: string, * account_id: string, * amount: array{amount: string, currency: string}, - * enrichments: array{display_label?: string, date: string, notes?: string}, - * type?: string + * enrichments: array{ + * display_label?: string, + * date: string, + * notes?: string, + * remittance_information?: string + * }, + * type?: string, + * remittance_information?: string * } */ class TransactionDto @@ -24,6 +30,7 @@ class TransactionDto private Money $amount; private ?string $label; private ?string $notes; + private ?string $remittanceInformation; private string $type; private AbsoluteDate $date; /** @var Transaction */ @@ -70,6 +77,11 @@ public function __construct(array $data) ); $this->label = $data['enrichments']['display_label'] ?? null; $this->notes = $data['enrichments']['notes'] ?? null; + // `notes` moved from the root to `enrichments` when Linxo switched to v3, so both locations + // are read here until we have observed where `remittance_information` actually lands + $this->remittanceInformation = $data['remittance_information'] + ?? $data['enrichments']['remittance_information'] + ?? null; $this->type = $data['type'] ?? self::TYPE_OTHER; $this->date = AbsoluteDate::createInTimezone( // Linxo uses timestamps but their servers' timezone is Europe/Paris @@ -104,6 +116,14 @@ public function getNotes(): ?string return $this->notes; } + /** + * Free-text reference sent along with the payment by the counterparty (ISO 20022 remittance information) + */ + public function getRemittanceInformation(): ?string + { + return $this->remittanceInformation; + } + public function getType(): string { return $this->type; diff --git a/tests/Dto/TransactionDtoTest.php b/tests/Dto/TransactionDtoTest.php index e83eea3..6a5c3d8 100644 --- a/tests/Dto/TransactionDtoTest.php +++ b/tests/Dto/TransactionDtoTest.php @@ -27,6 +27,7 @@ public function testConstructorSetsAllProperties(): void 'notes' => 'Weekly groceries', ], 'type' => TransactionDto::TYPE_POINT_OF_SALE, + 'remittance_information' => 'INVOICE 2024-0315', ]; $dto = new TransactionDto($data); @@ -36,6 +37,7 @@ public function testConstructorSetsAllProperties(): void self::assertSame('Grocery Store Purchase', $dto->getLabel()); self::assertSame('Weekly groceries', $dto->getNotes()); self::assertSame(TransactionDto::TYPE_POINT_OF_SALE, $dto->getType()); + self::assertSame('INVOICE 2024-0315', $dto->getRemittanceInformation()); } public function testAmountConvertedToMoneyInCents(): void @@ -119,6 +121,45 @@ public function testNotesIsNullWhenNotProvided(): void self::assertNull($dto->getNotes()); } + public function testRemittanceInformationReadFromRootOfThePayload(): void + { + $data = $this->createBaseData(['remittance_information' => 'INVOICE 2024-0117']); + + $dto = new TransactionDto($data); + + self::assertSame('INVOICE 2024-0117', $dto->getRemittanceInformation()); + } + + public function testRemittanceInformationReadFromEnrichments(): void + { + $data = $this->createBaseData(['enrichments' => ['remittance_information' => 'INVOICE 2024-0118']]); + + $dto = new TransactionDto($data); + + self::assertSame('INVOICE 2024-0118', $dto->getRemittanceInformation()); + } + + public function testRemittanceInformationAtRootTakesPrecedenceOverEnrichments(): void + { + $data = $this->createBaseData([ + 'remittance_information' => 'From the root', + 'enrichments' => ['remittance_information' => 'From the enrichments'], + ]); + + $dto = new TransactionDto($data); + + self::assertSame('From the root', $dto->getRemittanceInformation()); + } + + public function testRemittanceInformationIsNullWhenNotProvided(): void + { + $data = $this->createBaseData(); + + $dto = new TransactionDto($data); + + self::assertNull($dto->getRemittanceInformation()); + } + public function testDateParsedAsAbsoluteDate(): void { $data = $this->createBaseData(['enrichments' => [ From e2514013092a214bf2b306bcea2da937ff8bf468 Mon Sep 17 00:00:00 2001 From: Cindy Emile Date: Thu, 13 Aug 2026 10:37:25 +0400 Subject: [PATCH 2/5] Require php-quality-config ^2.4 to unblock the lowest CI legs The lowest dependency legs upgrade rector to its latest release while resolving php-quality-config to the ^2.2 floor. v2.2.0 still registers StaticClosureRector and StaticArrowFunctionRector, which rector 2.6.2 rejects outright, so those legs died on src/ApiClient.php before running a single check. v2.4.0 dropped both rules. Co-Authored-By: Claude Opus 5 (1M context) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 701f222..dfa1d83 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ "symfony/translation": "^7.0" }, "require-dev": { - "assoconnect/php-quality-config": "^2.2" + "assoconnect/php-quality-config": "^2.4" }, "config": { "allow-plugins": { From 8e2225cc45f6fbf74244a12a2727c4b2a254c939 Mon Sep 17 00:00:00 2001 From: Cindy Emile Date: Thu, 13 Aug 2026 10:37:25 +0400 Subject: [PATCH 3/5] Migrate @dataProvider annotations to the DataProvider attribute PHPUnit 13 removed support for annotation-based data providers, so the three tests using them errored with ArgumentCountError on the highest dependency legs. The attribute form works from PHPUnit 10 onwards, so both ends of the matrix are covered. Co-Authored-By: Claude Opus 5 (1M context) --- tests/Dto/AccountDtoTest.php | 5 ++--- tests/Dto/ConnectionDtoTest.php | 5 ++--- tests/Dto/TransactionDtoTest.php | 5 ++--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/tests/Dto/AccountDtoTest.php b/tests/Dto/AccountDtoTest.php index 33b8513..8d9c10a 100644 --- a/tests/Dto/AccountDtoTest.php +++ b/tests/Dto/AccountDtoTest.php @@ -7,6 +7,7 @@ use AssoConnect\LinxoClient\Dto\AccountDto; use Money\Currency; use Money\Money; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Translation\TranslatableMessage; @@ -161,9 +162,7 @@ public function testGetLocalizedStatusReturnsTranslatableMessage(): void self::assertSame('linxo', $message->getDomain()); } - /** - * @dataProvider statusProvider - */ + #[DataProvider('statusProvider')] public function testAllStatusConstants(string $status): void { $data = $this->createBaseData(['status' => $status]); diff --git a/tests/Dto/ConnectionDtoTest.php b/tests/Dto/ConnectionDtoTest.php index 7af4e84..6547ed0 100644 --- a/tests/Dto/ConnectionDtoTest.php +++ b/tests/Dto/ConnectionDtoTest.php @@ -5,6 +5,7 @@ namespace AssoConnect\LinxoClient\Tests\Dto; use AssoConnect\LinxoClient\Dto\ConnectionDto; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; class ConnectionDtoTest extends TestCase @@ -26,9 +27,7 @@ public function testConstructorSetsAllProperties(): void self::assertSame('https://example.com/bnp-logo.png', $dto->getLogoUrl()); } - /** - * @dataProvider statusProvider - */ + #[DataProvider('statusProvider')] public function testAllStatusConstants(string $status): void { $data = $this->createBaseData(['status' => $status]); diff --git a/tests/Dto/TransactionDtoTest.php b/tests/Dto/TransactionDtoTest.php index 6a5c3d8..b985e0e 100644 --- a/tests/Dto/TransactionDtoTest.php +++ b/tests/Dto/TransactionDtoTest.php @@ -8,6 +8,7 @@ use AssoConnect\PHPDate\AbsoluteDate; use Money\Currency; use Money\Money; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; class TransactionDtoTest extends TestCase @@ -206,9 +207,7 @@ public function testDifferentCurrencies(): void } } - /** - * @dataProvider transactionTypeProvider - */ + #[DataProvider('transactionTypeProvider')] public function testAllTransactionTypeConstants(string $type): void { $data = $this->createBaseData(['type' => $type]); From 94a4d11e33f314d284a065efd398716117bdc5f0 Mon Sep 17 00:00:00 2001 From: Cindy Emile Date: Thu, 13 Aug 2026 17:19:53 +0400 Subject: [PATCH 4/5] Read remittance_information as the list of lines Linxo sends The v3 payload carries this field at the root of the transaction as an array of unstructured ISO 20022 lines, not as a string, and never inside enrichments. Assigning it to a ?string property therefore threw "Cannot assign array to property TransactionDto::$remittanceInformation" on every transaction that carried one, which failed the whole import, not just that field. The lines are now joined by a space; an absent or empty array still gives null. The phpstan type is fixed accordingly and the two tests describing the enrichments location are dropped, as that location does not exist. Co-Authored-By: Claude Opus 5 (1M context) --- src/Dto/TransactionDto.php | 18 +++++++++--------- tests/Dto/TransactionDtoTest.php | 30 +++++++++--------------------- 2 files changed, 18 insertions(+), 30 deletions(-) diff --git a/src/Dto/TransactionDto.php b/src/Dto/TransactionDto.php index f0cfc86..f6d5ccb 100644 --- a/src/Dto/TransactionDto.php +++ b/src/Dto/TransactionDto.php @@ -16,11 +16,10 @@ * enrichments: array{ * display_label?: string, * date: string, - * notes?: string, - * remittance_information?: string + * notes?: string * }, * type?: string, - * remittance_information?: string + * remittance_information?: list * } */ class TransactionDto @@ -77,11 +76,11 @@ public function __construct(array $data) ); $this->label = $data['enrichments']['display_label'] ?? null; $this->notes = $data['enrichments']['notes'] ?? null; - // `notes` moved from the root to `enrichments` when Linxo switched to v3, so both locations - // are read here until we have observed where `remittance_information` actually lands - $this->remittanceInformation = $data['remittance_information'] - ?? $data['enrichments']['remittance_information'] - ?? null; + // Linxo sends the ISO 20022 remittance information as one entry per unstructured line + $remittanceInformationLines = $data['remittance_information'] ?? []; + $this->remittanceInformation = [] === $remittanceInformationLines + ? null + : implode(' ', $remittanceInformationLines); $this->type = $data['type'] ?? self::TYPE_OTHER; $this->date = AbsoluteDate::createInTimezone( // Linxo uses timestamps but their servers' timezone is Europe/Paris @@ -117,7 +116,8 @@ public function getNotes(): ?string } /** - * Free-text reference sent along with the payment by the counterparty (ISO 20022 remittance information) + * Free-text reference sent along with the payment by the counterparty (ISO 20022 remittance information), + * with all its unstructured lines joined by a space */ public function getRemittanceInformation(): ?string { diff --git a/tests/Dto/TransactionDtoTest.php b/tests/Dto/TransactionDtoTest.php index b985e0e..324329f 100644 --- a/tests/Dto/TransactionDtoTest.php +++ b/tests/Dto/TransactionDtoTest.php @@ -28,7 +28,7 @@ public function testConstructorSetsAllProperties(): void 'notes' => 'Weekly groceries', ], 'type' => TransactionDto::TYPE_POINT_OF_SALE, - 'remittance_information' => 'INVOICE 2024-0315', + 'remittance_information' => ['INVOICE 2024-0315'], ]; $dto = new TransactionDto($data); @@ -122,39 +122,27 @@ public function testNotesIsNullWhenNotProvided(): void self::assertNull($dto->getNotes()); } - public function testRemittanceInformationReadFromRootOfThePayload(): void + public function testRemittanceInformationLinesAreJoined(): void { - $data = $this->createBaseData(['remittance_information' => 'INVOICE 2024-0117']); + $data = $this->createBaseData(['remittance_information' => ['INVOICE 2024-0117', 'ORDER 42']]); $dto = new TransactionDto($data); - self::assertSame('INVOICE 2024-0117', $dto->getRemittanceInformation()); + self::assertSame('INVOICE 2024-0117 ORDER 42', $dto->getRemittanceInformation()); } - public function testRemittanceInformationReadFromEnrichments(): void - { - $data = $this->createBaseData(['enrichments' => ['remittance_information' => 'INVOICE 2024-0118']]); - - $dto = new TransactionDto($data); - - self::assertSame('INVOICE 2024-0118', $dto->getRemittanceInformation()); - } - - public function testRemittanceInformationAtRootTakesPrecedenceOverEnrichments(): void + public function testRemittanceInformationIsNullWhenNotProvided(): void { - $data = $this->createBaseData([ - 'remittance_information' => 'From the root', - 'enrichments' => ['remittance_information' => 'From the enrichments'], - ]); + $data = $this->createBaseData(); $dto = new TransactionDto($data); - self::assertSame('From the root', $dto->getRemittanceInformation()); + self::assertNull($dto->getRemittanceInformation()); } - public function testRemittanceInformationIsNullWhenNotProvided(): void + public function testRemittanceInformationIsNullWhenNoLineIsProvided(): void { - $data = $this->createBaseData(); + $data = $this->createBaseData(['remittance_information' => []]); $dto = new TransactionDto($data); From 1498a2c169f02d25473ae4a57ab5f8fa0b40d8b3 Mon Sep 17 00:00:00 2001 From: Cindy Emile Date: Thu, 13 Aug 2026 17:25:19 +0400 Subject: [PATCH 5/5] Refresh the phpstan baseline for the new remittance_information type Co-Authored-By: Claude Opus 5 (1M context) --- phpstan-baseline.neon | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index fd4c51e..191f302 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -13,7 +13,7 @@ parameters: path: src/ApiClient.php - - message: '#^Parameter \#1 \$data of class AssoConnect\\LinxoClient\\Dto\\TransactionDto constructor expects array\{id\: string, account_id\: string, amount\: array\{amount\: string, currency\: string\}, enrichments\: array\{display_label\?\: string, date\: string, notes\?\: string, remittance_information\?\: string\}, type\?\: string, remittance_information\?\: string\}, array given\.$#' + message: '#^Parameter \#1 \$data of class AssoConnect\\LinxoClient\\Dto\\TransactionDto constructor expects array\{id\: string, account_id\: string, amount\: array\{amount\: string, currency\: string\}, enrichments\: array\{display_label\?\: string, date\: string, notes\?\: string\}, type\?\: string, remittance_information\?\: list\\}, array given\.$#' identifier: argument.type count: 1 path: src/ApiClient.php @@ -67,9 +67,9 @@ parameters: path: tests/Dto/TransactionDtoTest.php - - message: '#^Parameter \#1 \$data of class AssoConnect\\LinxoClient\\Dto\\TransactionDto constructor expects array\{id\: string, account_id\: string, amount\: array\{amount\: string, currency\: string\}, enrichments\: array\{display_label\?\: string, date\: string, notes\?\: string, remittance_information\?\: string\}, type\?\: string, remittance_information\?\: string\}, array\ given\.$#' + message: '#^Parameter \#1 \$data of class AssoConnect\\LinxoClient\\Dto\\TransactionDto constructor expects array\{id\: string, account_id\: string, amount\: array\{amount\: string, currency\: string\}, enrichments\: array\{display_label\?\: string, date\: string, notes\?\: string\}, type\?\: string, remittance_information\?\: list\\}, array\ given\.$#' identifier: argument.type - count: 15 + count: 14 path: tests/Dto/TransactionDtoTest.php -