From 1c40a15da4866a9f08a424affadbad23a4b0d3e0 Mon Sep 17 00:00:00 2001 From: Robin Geuze Date: Tue, 12 Sep 2023 14:28:29 +0200 Subject: [PATCH] Add support for matches field on all transactions except BankTransactions Bump supported PHP version to 8.0 Fix a bunch of deprecated warnings in tests Bump eloquent/liberate library to 3.0 version to support php 8.2 --- src/BaseTransactionLine.php | 21 +++- src/Mappers/TransactionMapper.php | 100 ++++++++++++++---- src/Transactions/MatchLine.php | 53 ++++++++++ src/Transactions/MatchSet.php | 64 +++++++++++ .../PurchaseTransactionIntegrationTest.php | 18 +++- .../purchaseTransactionGetResponse.xml | 21 +++- 6 files changed, 249 insertions(+), 28 deletions(-) create mode 100644 src/Transactions/MatchLine.php create mode 100644 src/Transactions/MatchSet.php diff --git a/src/BaseTransactionLine.php b/src/BaseTransactionLine.php index 880bda51..ca93ca98 100644 --- a/src/BaseTransactionLine.php +++ b/src/BaseTransactionLine.php @@ -4,6 +4,7 @@ use Money\Money; use PhpTwinfield\Enums\LineType; +use PhpTwinfield\Transactions\MatchSet; use PhpTwinfield\Transactions\TransactionLine; use PhpTwinfield\Transactions\TransactionLineFields\CommentField; use PhpTwinfield\Transactions\TransactionLineFields\FreeCharField; @@ -18,7 +19,7 @@ * @todo $vatRepValue Only if line type is detail. VAT amount in reporting currency. * @todo $destOffice Office code. Used for inter company transactions. * @todo $comment Comment set on the transaction line. - * @todo $matches Contains matching information. Read-only attribute. + * @todo $matches Implement for BankTransactionLine * * @link https://accounting.twinfield.com/webservices/documentation/#/ApiReference/Transactions/BankTransactions */ @@ -117,6 +118,11 @@ abstract class BaseTransactionLine implements TransactionLine */ protected $currencyDate; + /** + * @var MatchSet[] Empty if not available, readonly. + */ + protected $matches = []; + public function getLineType(): LineType { return $this->lineType; @@ -414,4 +420,17 @@ public function getReference(): MatchReferenceInterface $this->getId() ); } + + public function addMatch(MatchSet $match) + { + $this->matches[] = $match; + } + + /** + * @return MatchSet[] + */ + public function getMatches(): array + { + return $this->matches; + } } diff --git a/src/Mappers/TransactionMapper.php b/src/Mappers/TransactionMapper.php index ede8f4d9..7650cb03 100644 --- a/src/Mappers/TransactionMapper.php +++ b/src/Mappers/TransactionMapper.php @@ -2,6 +2,7 @@ namespace PhpTwinfield\Mappers; +use DOMElement; use DOMXPath; use Money\Currency; use Money\Money; @@ -18,16 +19,20 @@ use PhpTwinfield\Office; use PhpTwinfield\Response\Response; use PhpTwinfield\SalesTransaction; +use PhpTwinfield\Transactions\MatchLine; +use PhpTwinfield\Transactions\MatchSet; use PhpTwinfield\Transactions\TransactionFields\DueDateField; use PhpTwinfield\Transactions\TransactionFields\FreeTextFields; use PhpTwinfield\Transactions\TransactionFields\InvoiceNumberField; use PhpTwinfield\Transactions\TransactionFields\PaymentReferenceField; use PhpTwinfield\Transactions\TransactionFields\StatementNumberField; +use PhpTwinfield\Transactions\TransactionLine; use PhpTwinfield\Transactions\TransactionLineFields\MatchDateField; use PhpTwinfield\Transactions\TransactionLineFields\PerformanceFields; use PhpTwinfield\Transactions\TransactionLineFields\ValueOpenField; use PhpTwinfield\Transactions\TransactionLineFields\VatTotalFields; use PhpTwinfield\Util; +use UnexpectedValueException; class TransactionMapper { @@ -83,55 +88,60 @@ public static function map(string $transactionClassName, Response $response): Ba $transaction->setRaiseWarning(Util::parseBoolean($raiseWarning)); } + $header = self::getFirstChildElementByName($transactionElement, 'header'); + if ($header === null) { + throw new UnexpectedValueException('Transaction section is missing a header section'); + } + $office = new Office(); - $office->setCode(self::getField($transaction, $transactionElement, 'office')); + $office->setCode(self::getField($transaction, $header, 'office')); $transaction ->setOffice($office) - ->setCode(self::getField($transaction, $transactionElement, 'code')) - ->setPeriod(self::getField($transaction, $transactionElement, 'period')) - ->setDateFromString(self::getField($transaction, $transactionElement, 'date')) - ->setOrigin(self::getField($transaction, $transactionElement, 'origin')) - ->setFreetext1(self::getField($transaction, $transactionElement, 'freetext1')) - ->setFreetext2(self::getField($transaction, $transactionElement, 'freetext2')) - ->setFreetext3(self::getField($transaction, $transactionElement, 'freetext3')); - - $currency = new Currency(self::getField($transaction, $transactionElement, 'currency') ?? self::DEFAULT_CURRENCY_CODE); + ->setCode(self::getField($transaction, $header, 'code')) + ->setPeriod(self::getField($transaction, $header, 'period')) + ->setDateFromString(self::getField($transaction, $header, 'date')) + ->setOrigin(self::getField($transaction, $header, 'origin')) + ->setFreetext1(self::getField($transaction, $header, 'freetext1')) + ->setFreetext2(self::getField($transaction, $header, 'freetext2')) + ->setFreetext3(self::getField($transaction, $header, 'freetext3')); + + $currency = new Currency(self::getField($transaction, $header, 'currency') ?? self::DEFAULT_CURRENCY_CODE); $transaction->setCurrency($currency); - $number = self::getField($transaction, $transactionElement, 'number'); + $number = self::getField($transaction, $header, 'number'); if (!empty($number)) { $transaction->setNumber($number); } if (Util::objectUses(DueDateField::class, $transaction)) { - $value = self::getField($transaction, $transactionElement, 'duedate'); + $value = self::getField($transaction, $header, 'duedate'); if ($value !== null) { $transaction->setDueDateFromString($value); } } if (Util::objectUses(InvoiceNumberField::class, $transaction)) { - $transaction->setInvoiceNumber(self::getField($transaction, $transactionElement, 'invoicenumber')); + $transaction->setInvoiceNumber(self::getField($transaction, $header, 'invoicenumber')); } if (Util::objectUses(PaymentReferenceField::class, $transaction)) { $transaction - ->setPaymentReference(self::getField($transaction, $transactionElement, 'paymentreference')); + ->setPaymentReference(self::getField($transaction, $header, 'paymentreference')); } if (Util::objectUses(StatementNumberField::class, $transaction)) { - $transaction->setStatementnumber(self::getField($transaction, $transactionElement, 'statementnumber')); + $transaction->setStatementnumber(self::getField($transaction, $header, 'statementnumber')); } if ($transaction instanceof SalesTransaction) { - $transaction->setOriginReference(self::getField($transaction, $transactionElement, 'originreference')); + $transaction->setOriginReference(self::getField($transaction, $header, 'originreference')); } if ($transaction instanceof JournalTransaction) { - $transaction->setRegime(self::getField($transaction, $transactionElement, 'regime')); + $transaction->setRegime(self::getField($transaction, $header, 'regime')); } if ($transaction instanceof CashTransaction) { $transaction->setStartvalue( Util::parseMoney( - self::getField($transaction, $transactionElement, 'startvalue'), + self::getField($transaction, $header, 'startvalue'), $transaction->getCurrency() ) ); @@ -140,6 +150,7 @@ public static function map(string $transactionClassName, Response $response): Ba // Parse the transaction lines $transactionLineClassName = $transaction->getLineClassName(); + /** @var DOMElement $lineElement */ foreach ((new DOMXPath($document))->query('/transaction/lines/line') as $lineElement) { self::checkForMessage($transaction, $lineElement); @@ -163,6 +174,11 @@ public static function map(string $transactionClassName, Response $response): Ba ->setMatchLevel(self::getField($transaction, $lineElement, 'matchlevel')) ->setVatCode(self::getField($transaction, $lineElement, 'vatcode')); + $matches = $lineElement->getElementsByTagName('matches')->item(0); + if ($matches !== null) { + self::parseMatches($transaction, $transactionLine, $matches); + } + // TODO - according to the docs, the field is called , but the examples use . $baseValueOpen = self::getFieldAsMoney($transaction, $lineElement, 'basevalueopen', $currency) ?: self::getFieldAsMoney($transaction, $lineElement, 'openbasevalue', $currency); if ($baseValueOpen) { @@ -255,9 +271,22 @@ public static function map(string $transactionClassName, Response $response): Ba return $transaction; } - private static function getField(BaseTransaction $transaction, \DOMElement $element, string $fieldTagName): ?string + private static function getFirstChildElementByName(DOMElement $element, string $fieldTagName): ?DOMElement { - $fieldElement = $element->getElementsByTagName($fieldTagName)->item(0); + $fieldElement = null; + foreach ($element->childNodes as $node) { + if ($node->nodeName === $fieldTagName) { + $fieldElement = $node; + break; + } + } + + return $fieldElement; + } + + private static function getField(BaseTransaction $transaction, DOMElement $element, string $fieldTagName): ?string + { + $fieldElement = self::getFirstChildElementByName($element, $fieldTagName); if (!isset($fieldElement)) { return null; @@ -270,7 +299,7 @@ private static function getField(BaseTransaction $transaction, \DOMElement $elem private static function getFieldAsMoney( BaseTransaction $transaction, - \DOMElement $element, + DOMElement $element, string $fieldTagName, Currency $currency ): ?Money { @@ -283,7 +312,7 @@ private static function getFieldAsMoney( return new Money((string)(100 * $fieldValue), $currency); } - private static function checkForMessage(BaseTransaction $transaction, \DOMElement $element): void + private static function checkForMessage(BaseTransaction $transaction, DOMElement $element): void { if ($element->hasAttribute('msg')) { $message = new Message(); @@ -294,4 +323,31 @@ private static function checkForMessage(BaseTransaction $transaction, \DOMElemen $transaction->addMessage($message); } } + + private static function parseMatches(BaseTransaction $baseTransaction, BaseTransactionLine $transactionLine, DOMElement $element): void + { + /** @var DOMElement $set */ + foreach ($element->getElementsByTagName('set') as $set) { + $status = Destiny::from($set->getAttribute('status')); + $matchDate = Util::parseDate(self::getField($baseTransaction, $set, 'matchdate')); + $matchValue = self::getFieldAsMoney($baseTransaction, $set, 'matchvalue', $baseTransaction->getCurrency()); + + $matchLines = []; + /** @var DOMElement $lines */ + $lines = $set->getElementsByTagName('lines')->item(0); + /** @var DOMElement $line */ + foreach ($lines->childNodes as $line) { + if ($line->nodeName !== 'line') { + continue; + } + $code = (string)self::getField($baseTransaction, $line, 'code'); + $number = (int)self::getField($baseTransaction, $line, 'number'); + $lineNum = (int)self::getField($baseTransaction, $line, 'line'); + $lineMatchValue = self::getFieldAsMoney($baseTransaction, $line, 'matchvalue', $baseTransaction->getCurrency()); + + $matchLines[] = new MatchLine($code, $number, $lineNum, $lineMatchValue); + } + $transactionLine->addMatch(new MatchSet($status, $matchDate, $matchValue, $matchLines)); + } + } } diff --git a/src/Transactions/MatchLine.php b/src/Transactions/MatchLine.php new file mode 100644 index 00000000..0e3f8b43 --- /dev/null +++ b/src/Transactions/MatchLine.php @@ -0,0 +1,53 @@ +code = $code; + $this->number = $number; + $this->line = $line; + $this->matchValue = $matchValue; + } + + public function getCode(): string + { + return $this->code; + } + + public function getNumber(): int + { + return $this->number; + } + + public function getLine(): int + { + return $this->line; + } + + public function getMatchValue(): Money + { + return $this->matchValue; + } +} \ No newline at end of file diff --git a/src/Transactions/MatchSet.php b/src/Transactions/MatchSet.php new file mode 100644 index 00000000..fdaff94a --- /dev/null +++ b/src/Transactions/MatchSet.php @@ -0,0 +1,64 @@ +status = $status; + $this->matchDate = $matchDate; + $this->matchValue = $matchValue; + $this->matchLines = $matchLines; + } + + public function getStatus(): Destiny + { + return $this->status; + } + + public function getMatchDate(): DateTimeInterface + { + return $this->matchDate; + } + + public function getMatchValue(): Money + { + return $this->matchValue; + } + + /** + * @return MatchLine[] + */ + public function getMatchLines(): array + { + return $this->matchLines; + } +} \ No newline at end of file diff --git a/tests/IntegrationTests/PurchaseTransactionIntegrationTest.php b/tests/IntegrationTests/PurchaseTransactionIntegrationTest.php index 6bfb63f2..267e6d22 100644 --- a/tests/IntegrationTests/PurchaseTransactionIntegrationTest.php +++ b/tests/IntegrationTests/PurchaseTransactionIntegrationTest.php @@ -85,12 +85,12 @@ public function testGetPurchaseTransactionWorks() $this->assertSame('', $totalLine->getDescription()); $this->assertSame(PurchaseTransactionLine::MATCHSTATUS_AVAILABLE, $totalLine->getMatchStatus()); $this->assertSame(2, $totalLine->getMatchLevel()); - $this->assertEquals(Money::EUR(12100), $totalLine->getBaseValueOpen()); + $this->assertEquals(Money::EUR(12000), $totalLine->getBaseValueOpen()); $this->assertNull($totalLine->getVatCode()); $this->assertNull($totalLine->getVatValue()); $this->assertEquals(Money::EUR(2100), $totalLine->getVatTotal()); $this->assertEquals(Money::EUR(2100), $totalLine->getVatBaseTotal()); - $this->assertEquals(Money::EUR(12100), $totalLine->getValueOpen()); + $this->assertEquals(Money::EUR(12000), $totalLine->getValueOpen()); $this->assertEquals(new DateTime('2020-12-03'), $totalLine->getMatchDate()); $this->assertEquals(LineType::DETAIL(), $detailLine->getLineType()); @@ -134,6 +134,20 @@ public function testGetPurchaseTransactionWorks() $this->assertNull($vatLine->getVatBaseTotal()); $this->assertNull($vatLine->getValueOpen()); $this->assertNull($vatLine->getMatchDate()); + + $matches = $totalLine->getMatches(); + $this->assertCount(1, $matches); + $match = $matches[0]; + $this->assertEquals(Destiny::TEMPORARY(), $match->getStatus()); + $this->assertEquals(new DateTime('2020-12-03'), $match->getMatchDate()); + $this->assertEquals(Money::EUR(100), $match->getMatchValue()); + $matchLines = $match->getMatchLines(); + $this->assertCount(1, $matchLines); + $matchLine = $matchLines[0]; + $this->assertEquals('BNK', $matchLine->getCode()); + $this->assertEquals('201300022', $matchLine->getNumber()); + $this->assertEquals(1, $matchLine->getLine()); + $this->assertEquals(Money::EUR(-100), $matchLine->getMatchValue()); } public function testSendPurchaseTransactionWorks() diff --git a/tests/IntegrationTests/resources/purchaseTransactionGetResponse.xml b/tests/IntegrationTests/resources/purchaseTransactionGetResponse.xml index 13bbec2e..7305254d 100644 --- a/tests/IntegrationTests/resources/purchaseTransactionGetResponse.xml +++ b/tests/IntegrationTests/resources/purchaseTransactionGetResponse.xml @@ -27,11 +27,26 @@ 21.00 2 2 - 121.00 - 121.00 - 156.53 + 120.00 + 120.00 + 155.23 available 20201203 + + + 20201203 + + + BNK + 201300022 + 1 + payment + -1.00 + + + 1.00 + + 8020