Skip to content

Commit

Permalink
Merge pull request #225 from RobinGeuze/addMatchesSupport
Browse files Browse the repository at this point in the history
Add support for matches field on transactions
  • Loading branch information
rojtjo authored Sep 12, 2023
2 parents e2b2e68 + 1c40a15 commit 34daa95
Show file tree
Hide file tree
Showing 6 changed files with 249 additions and 28 deletions.
21 changes: 20 additions & 1 deletion src/BaseTransactionLine.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
*/
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
100 changes: 78 additions & 22 deletions src/Mappers/TransactionMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PhpTwinfield\Mappers;

use DOMElement;
use DOMXPath;
use Money\Currency;
use Money\Money;
Expand All @@ -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
{
Expand Down Expand Up @@ -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()
)
);
Expand All @@ -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);

Expand All @@ -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 <basevalueopen>, but the examples use <openbasevalue>.
$baseValueOpen = self::getFieldAsMoney($transaction, $lineElement, 'basevalueopen', $currency) ?: self::getFieldAsMoney($transaction, $lineElement, 'openbasevalue', $currency);
if ($baseValueOpen) {
Expand Down Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -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();
Expand All @@ -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));
}
}
}
53 changes: 53 additions & 0 deletions src/Transactions/MatchLine.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace PhpTwinfield\Transactions;

use Money\Money;

class MatchLine
{
/** @var string */
private $code;

/** @var int */
private $number;

/** @var int */
private $line;

/** @var Money */
private $matchValue;

public function __construct(
string $code,
int $number,
int $line,
Money $matchValue
)
{
$this->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;
}
}
64 changes: 64 additions & 0 deletions src/Transactions/MatchSet.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

namespace PhpTwinfield\Transactions;

use DateTimeInterface;
use Money\Money;
use PhpTwinfield\Enums\Destiny;

class MatchSet
{
/** @var Destiny */
private $status;

/** @var DateTimeInterface */
private $matchDate;

/** @var Money */
private $matchValue;

/** @var MatchLine[] */
private $matchLines;

/**
* @param Destiny $status
* @param DateTimeInterface $matchDate
* @param Money $matchValue
* @param MatchLine[] $matchLines
*/
public function __construct(
Destiny $status,
DateTimeInterface $matchDate,
Money $matchValue,
array $matchLines
)
{
$this->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;
}
}
Loading

0 comments on commit 34daa95

Please sign in to comment.