Skip to content

Commit

Permalink
Add support for camt.054.001.08
Browse files Browse the repository at this point in the history
V08 introduces substructure for related parties (`Party40Choice` in the
spec). They can now be either a private party (like before) or a
financial institution (new).

Because our existing model is too simple, we will read the new financial
institution name and postal address into a `RelatedPartyTypeInterface`,
but we will not read anything else, and we will not be able to know if
it is a private party or a financial institution.

Similarly, an `Entry` status code now support proprietary code. It will
be read, but we will not be able to know if it is a standard one (eg:
`"BOOK"`), or a proprietary one.

Fixes #126
  • Loading branch information
PowerKiKi committed Feb 12, 2023
1 parent 7688ead commit 5de970a
Show file tree
Hide file tree
Showing 17 changed files with 3,248 additions and 30 deletions.
2,009 changes: 2,009 additions & 0 deletions assets/camt.054.001.08.xsd

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions src/Camt054/Decoder/V08/Message.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Genkgo\Camt\Camt054\Decoder\V08;

use Genkgo\Camt\Camt054\Decoder\V04\Message as BaseMessage;

class Message extends BaseMessage
{
}
38 changes: 38 additions & 0 deletions src/Camt054/MessageFormat/V08.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace Genkgo\Camt\Camt054\MessageFormat;

use Genkgo\Camt\Camt054;
use Genkgo\Camt\Decoder;
use Genkgo\Camt\DecoderInterface;
use Genkgo\Camt\MessageFormatInterface;

final class V08 implements MessageFormatInterface
{
public function getXmlNs(): string
{
return 'urn:iso:std:iso:20022:tech:xsd:camt.054.001.08';
}

public function getMsgId(): string
{
return 'camt.054.001.08';
}

public function getName(): string
{
return 'BankToCustomerDebitCreditNotificationV08';
}

public function getDecoder(): DecoderInterface
{
$entryTransactionDetailDecoder = new Camt054\Decoder\EntryTransactionDetail(new Decoder\Date());
$entryDecoder = new Decoder\Entry($entryTransactionDetailDecoder);
$recordDecoder = new Decoder\Record($entryDecoder, new Decoder\Date());
$messageDecoder = new Camt054\Decoder\V08\Message($recordDecoder, new Decoder\Date());

return new Decoder($messageDecoder, sprintf('/assets/%s.xsd', $this->getMsgId()));
}
}
1 change: 1 addition & 0 deletions src/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ public static function getDefault(): self
$config->addMessageFormat(new Camt053\MessageFormat\V04());
$config->addMessageFormat(new Camt054\MessageFormat\V02());
$config->addMessageFormat(new Camt054\MessageFormat\V04());
$config->addMessageFormat(new Camt054\MessageFormat\V08());

return $config;
}
Expand Down
5 changes: 1 addition & 4 deletions src/DTO/Creditor.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,10 @@

class Creditor implements RelatedPartyTypeInterface
{
private string $name;

private ?Address $address = null;

public function __construct(string $name)
public function __construct(private ?string $name)
{
$this->name = $name;
}

public function setAddress(Address $address): void
Expand Down
5 changes: 1 addition & 4 deletions src/DTO/Debtor.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,10 @@

class Debtor implements RelatedPartyTypeInterface
{
private string $name;

private ?Address $address = null;

public function __construct(string $name)
public function __construct(private ?string $name)
{
$this->name = $name;
}

public function setAddress(Address $address): void
Expand Down
6 changes: 4 additions & 2 deletions src/DTO/Recipient.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ class Recipient implements RelatedPartyTypeInterface
{
private ?Address $address = null;

private ?string $name = null;

private ?string $countryOfResidence = null;

private ?ContactDetails $contactDetails = null;

private ?Identification $identification = null;

public function __construct(private ?string $name = null)
{
}

public function getAddress(): ?Address
{
return $this->address;
Expand Down
2 changes: 2 additions & 0 deletions src/DTO/RelatedPartyTypeInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
*/
interface RelatedPartyTypeInterface
{
public function __construct(?string $name);

public function setAddress(Address $address): void;

public function getAddress(): ?Address;
Expand Down
34 changes: 17 additions & 17 deletions src/Decoder/EntryTransactionDetail.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Genkgo\Camt\Decoder\Factory\DTO as DTOFactory;
use Genkgo\Camt\DTO;
use Genkgo\Camt\DTO\RelatedParty;
use Genkgo\Camt\DTO\RelatedPartyTypeInterface;
use Genkgo\Camt\Util\MoneyFactory;
use SimpleXMLElement;

Expand Down Expand Up @@ -69,50 +70,49 @@ public function addRelatedParties(DTO\EntryTransactionDetail $detail, SimpleXMLE
if (isset($xmlRelatedParty->Cdtr)) {
$xmlRelatedPartyType = $xmlRelatedParty->Cdtr;
$xmlRelatedPartyTypeAccount = $xmlRelatedParty->CdtrAcct;
$xmlRelatedPartyName = (isset($xmlRelatedPartyType->Nm)) ? (string) $xmlRelatedPartyType->Nm : '';
$relatedPartyType = new DTO\Creditor($xmlRelatedPartyName);

$this->addRelatedParty($detail, $xmlRelatedPartyType, $relatedPartyType, $xmlRelatedPartyTypeAccount);
$this->addRelatedParty($detail, $xmlRelatedPartyType, DTO\Creditor::class, $xmlRelatedPartyTypeAccount);
}

if (isset($xmlRelatedParty->UltmtCdtr)) {
$xmlRelatedPartyType = $xmlRelatedParty->UltmtCdtr;
$xmlRelatedPartyName = (isset($xmlRelatedPartyType->Nm)) ? (string) $xmlRelatedPartyType->Nm : '';
$relatedPartyType = new DTO\UltimateCreditor($xmlRelatedPartyName);

$this->addRelatedParty($detail, $xmlRelatedPartyType, $relatedPartyType);
$this->addRelatedParty($detail, $xmlRelatedPartyType, DTO\UltimateCreditor::class);
}

if (isset($xmlRelatedParty->Dbtr)) {
$xmlRelatedPartyType = $xmlRelatedParty->Dbtr;
$xmlRelatedPartyTypeAccount = $xmlRelatedParty->DbtrAcct;
$xmlRelatedPartyName = (isset($xmlRelatedPartyType->Nm)) ? (string) $xmlRelatedPartyType->Nm : '';
$relatedPartyType = $debtor = new DTO\Debtor($xmlRelatedPartyName);

$this->addRelatedParty($detail, $xmlRelatedPartyType, $relatedPartyType, $xmlRelatedPartyTypeAccount);
$this->addRelatedParty($detail, $xmlRelatedPartyType, DTO\Debtor::class, $xmlRelatedPartyTypeAccount);
}

if (isset($xmlRelatedParty->UltmtDbtr)) {
$xmlRelatedPartyType = $xmlRelatedParty->UltmtDbtr;
$xmlRelatedPartyName = (isset($xmlRelatedPartyType->Nm)) ? (string) $xmlRelatedPartyType->Nm : '';
$relatedPartyType = new DTO\UltimateDebtor($xmlRelatedPartyName);

$this->addRelatedParty($detail, $xmlRelatedPartyType, $relatedPartyType);
$this->addRelatedParty($detail, $xmlRelatedPartyType, DTO\UltimateDebtor::class);
}
}
}

protected function addRelatedParty(DTO\EntryTransactionDetail $detail, SimpleXMLElement $xmlRelatedPartyType, DTO\RelatedPartyTypeInterface $relatedPartyType, ?SimpleXMLElement $xmlRelatedPartyTypeAccount = null): RelatedParty
/**
* @param class-string<RelatedPartyTypeInterface> $relatedPartyTypeClass
*/
protected function addRelatedParty(DTO\EntryTransactionDetail $detail, SimpleXMLElement $xmlRelatedPartyType, string $relatedPartyTypeClass, ?SimpleXMLElement $xmlRelatedPartyTypeAccount = null): void
{
if (isset($xmlRelatedPartyType->PstlAdr)) {
$relatedPartyType->setAddress(DTOFactory\Address::createFromXml($xmlRelatedPartyType->PstlAdr));
// CAMT v08 uses substructure, so we check for its existence or fallback to the element itself to keep compatibility with CAMT v04
$xmlPartyDetail = $xmlRelatedPartyType->Pty ?: $xmlRelatedPartyType->Agt?->FinInstnId ?: $xmlRelatedPartyType;

$xmlRelatedPartyName = (isset($xmlPartyDetail->Nm)) ? (string) $xmlPartyDetail->Nm : null;
$relatedPartyType = new $relatedPartyTypeClass($xmlRelatedPartyName);

if (isset($xmlPartyDetail->PstlAdr)) {
$relatedPartyType->setAddress(DTOFactory\Address::createFromXml($xmlPartyDetail->PstlAdr));
}

$relatedParty = new RelatedParty($relatedPartyType, $this->getRelatedPartyAccount($xmlRelatedPartyTypeAccount));

$detail->addRelatedParty($relatedParty);

return $relatedParty;
}

public function addRelatedAgents(DTO\EntryTransactionDetail $detail, SimpleXMLElement $xmlDetail): void
Expand Down
15 changes: 12 additions & 3 deletions src/Decoder/Record.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,7 @@ public function addEntries(DTO\Record $record, SimpleXMLElement $xmlRecord): voi
$entry->setBatchPaymentId((string) $xmlEntry->NtryDtls->TxDtls->Refs->PmtInfId);
}

if (isset($xmlEntry->Sts) && (string) $xmlEntry->Sts) {
$entry->setStatus((string) $xmlEntry->Sts);
}
$entry->setStatus($this->readStatus($xmlEntry));

if (isset($xmlEntry->BkTxCd)) {
$bankTransactionCode = new DTO\BankTransactionCode();
Expand Down Expand Up @@ -194,4 +192,15 @@ public function addEntries(DTO\Record $record, SimpleXMLElement $xmlRecord): voi
++$index;
}
}

private function readStatus(SimpleXMLElement $xmlEntry): ?string
{
$xmlStatus = $xmlEntry->Sts;

// CAMT v08 uses substructure, so we check for its existence or fallback to the element itself to keep compatibility with CAMT v04
return (string) $xmlStatus?->Cd
?: (string) $xmlStatus?->Prtry
?: (string) $xmlStatus
?: null;
}
}
12 changes: 12 additions & 0 deletions test/Unit/Camt054/EndToEndTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,20 @@ protected function getV4Message(): Message
return (new MessageFormat\V04())->getDecoder()->decode($dom);
}

protected function getv8Message(): Message
{
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->load('test/data/camt054.v8.xml');

return (new MessageFormat\V08())->getDecoder()->decode($dom);
}

public function testGroupHeader(): void
{
$messages = [
$this->getV2Message(),
$this->getV4Message(),
$this->getV8Message(),
];

/** @var Message $message */
Expand Down Expand Up @@ -86,6 +95,7 @@ public function testNotifications(): void
$messages = [
$this->getV2Message(),
$this->getV4Message(),
$this->getV8Message(),
];

foreach ($messages as $message) {
Expand Down Expand Up @@ -117,6 +127,7 @@ public function testEntries(): void
$messages = [
$this->getV2Message(),
$this->getV4Message(),
$this->getV8Message(),
];

foreach ($messages as $message) {
Expand Down Expand Up @@ -145,6 +156,7 @@ public function testTransactionDetails(): void
$messages = [
$this->getV2Message(),
$this->getV4Message(),
$this->getV8Message(),
];

foreach ($messages as $message) {
Expand Down
1 change: 1 addition & 0 deletions test/Unit/ConfigTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public function testDefaultConfigHasMessageFormats(): void
Camt053\MessageFormat\V04::class,
Camt054\MessageFormat\V02::class,
Camt054\MessageFormat\V04::class,
Camt054\MessageFormat\V08::class,
];

$actualMessageFormats = array_map(static fn (MessageFormatInterface $messageFormat): string => get_class($messageFormat), $messageFormats);
Expand Down
2 changes: 2 additions & 0 deletions test/Unit/RegressionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,5 +71,7 @@ public static function providerRegression(): iterable
yield ['test/data/camt053.v4.xml'];
yield ['test/data/camt054.v2.xml'];
yield ['test/data/camt054.v4.xml'];
yield ['test/data/camt054.v8.xml'];
yield ['test/data/camt054.v8-with-financial-institution.xml'];
}
}
116 changes: 116 additions & 0 deletions test/data/camt054.v8-with-financial-institution.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
{
"__CLASS__": "Genkgo\\Camt\\DTO\\Message",
"getEntries": [
{
"__CLASS__": "Genkgo\\Camt\\DTO\\Entry",
"getAccountServicerReference": "ACSR160617103200001",
"getAdditionalInfo": "",
"getAmount": {
"__CLASS__": "Money\\Money",
"getAmount": "-20000000",
"getCurrency": {
"__CLASS__": "Money\\Currency",
"getCode": "SEK"
}
},
"getBankTransactionCode": {
"__CLASS__": "Genkgo\\Camt\\DTO\\BankTransactionCode",
"getDomain": null,
"getProprietary": null
},
"getBatchPaymentId": null,
"getBookingDate": null,
"getCharges": null,
"getIndex": 0,
"getRecord": {
"__CLASS__": "Genkgo\\Camt\\Camt054\\DTO\\Notification",
"getAccount": {
"__CLASS__": "Genkgo\\Camt\\DTO\\IbanAccount",
"getIban": {
"__CLASS__": "Genkgo\\Camt\\Iban",
"getIban": "CH2801234000123456789"
},
"getIdentification": "CH2801234000123456789"
},
"getAdditionalInformation": null,
"getCopyDuplicateIndicator": null,
"getCreatedOn": {
"__CLASS__": "DateTimeImmutable",
"0": "2007-10-18T11:30:00+00:00"
},
"getElectronicSequenceNumber": null,
"getEntries": [
"__RECURSIVITY__"
],
"getFromDate": null,
"getId": "AAAASESS-FP-ACCR001",
"getLegalSequenceNumber": null,
"getPagination": null,
"getToDate": null
},
"getReference": null,
"getReversalIndicator": false,
"getStatus": "BOOK",
"getTransactionDetail": {
"__CLASS__": "Genkgo\\Camt\\DTO\\EntryTransactionDetail",
"getAdditionalTransactionInformation": null,
"getAmount": null,
"getAmountDetails": null,
"getBankTransactionCode": {
"__CLASS__": "Genkgo\\Camt\\DTO\\BankTransactionCode",
"getDomain": null,
"getProprietary": null
},
"getCharges": null,
"getReference": null,
"getRelatedAgent": null,
"getRelatedAgents": [],
"getRelatedDates": null,
"getRelatedParties": [
{
"__CLASS__": "Genkgo\\Camt\\DTO\\RelatedParty",
"getAccount": null,
"getRelatedPartyType": {
"__CLASS__": "Genkgo\\Camt\\DTO\\Creditor",
"getAddress": {
"__CLASS__": "Genkgo\\Camt\\DTO\\Address",
"getAddressLines": [],
"getBuildingNumber": "31",
"getCountry": "CH",
"getCountrySubDivision": null,
"getDepartment": null,
"getPostCode": "3131",
"getStreetName": "Example Street 3 - V1",
"getSubDepartment": null,
"getTownName": "Example Town 3 - V1"
},
"getName": "Example Creditor 3 - V1"
}
}
],
"getRelatedParty": "__RECURSIVITY__",
"getRemittanceInformation": null,
"getReturnInformation": null
},
"getTransactionDetails": [
"__RECURSIVITY__"
],
"getValueDate": null
}
],
"getGroupHeader": {
"__CLASS__": "Genkgo\\Camt\\Camt054\\DTO\\V04\\GroupHeader",
"getAdditionalInformation": null,
"getCreatedOn": {
"__CLASS__": "DateTimeImmutable",
"0": "2007-10-18T11:30:00+00:00"
},
"getMessageId": "AAAASESS-FP-ACCR001",
"getMessageRecipient": null,
"getOriginalBusinessQuery": null,
"getPagination": null
},
"getRecords": [
"__RECURSIVITY__"
]
}
Loading

0 comments on commit 5de970a

Please sign in to comment.