Skip to content

Commit

Permalink
Added support for multiple payments
Browse files Browse the repository at this point in the history
- Updated Invoice and Payment classes
- Updated UblReader and UblWriter classes
- Updated validation rules

> Closes #62
  • Loading branch information
josemmo committed Sep 22, 2024
1 parent f4ec9fd commit 26437c2
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 63 deletions.
77 changes: 74 additions & 3 deletions src/Invoice.php
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,9 @@ class Invoice {
protected $buyer = null;
protected $payee = null;
protected $delivery = null;
protected $payment = null;
/** @var Payment[] */
protected $payments = [];
protected $paymentTerms = null;
/** @var InvoiceLine[] */
protected $lines = [];

Expand Down Expand Up @@ -841,22 +843,91 @@ public function setDelivery(?Delivery $delivery): self {
}


/**
* Get invoice payments
* @return Payment[] Invoice payments
*/
public function getPayments(): array {
return $this->payments;
}


/**
* Add invoice payment
* @param Payment $payment Invoice payment
* @return self Invoice instance
*/
public function addPayment(Payment $payment): self {
$this->payments[] = $payment;
return $this;
}


/**
* Remove invoice payment
* @param int $index Invoice payment index
* @return self Invoice instance
* @throws OutOfBoundsException if invoice payment index is out of bounds
*/
public function removePayment(int $index): self {
if ($index < 0 || $index >= count($this->payments)) {
throw new OutOfBoundsException('Could not find invoice payment by index');
}
array_splice($this->payments, $index, 1);
return $this;
}


/**
* Clear all invoice payments
* @return self Invoice instance
*/
public function clearPayments(): self {
$this->payments = [];
return $this;
}


/**
* Get payment information
* @return Payment|null Payment instance
* @deprecated 0.2.8
* @see Invoice::getPayments()
*/
public function getPayment(): ?Payment {
return $this->payment;
return $this->payments[0] ?? null;
}


/**
* Set payment information
* @param Payment|null $payment Payment instance
* @return self Invoice instance
* @deprecated 0.2.8
* @see Invoice::addPayment()
*/
public function setPayment(?Payment $payment): self {
$this->payment = $payment;
$this->payments = ($payment === null) ? [] : [$payment];
return $this;
}


/**
* Get payment terms
* @return string|null Payment terms
*/
public function getPaymentTerms(): ?string {
return $this->paymentTerms;
}


/**
* Set payment terms
* @param string|null $paymentTerms Payment terms
* @return self Invoice instance
*/
public function setPaymentTerms(?string $paymentTerms): self {
$this->paymentTerms = $paymentTerms;
return $this;
}

Expand Down
4 changes: 4 additions & 0 deletions src/Payments/Payment.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ public function setMeansText(?string $meansText): self {
/**
* Get payment terms
* @return string|null Payment terms
* @deprecated 0.2.8
* @see Invoice::getPaymentTerms()
*/
public function getTerms(): ?string {
return $this->terms;
Expand All @@ -87,6 +89,8 @@ public function getTerms(): ?string {
* Set payment terms
* @param string|null $terms Payment terms
* @return self Payment instance
* @deprecated 0.2.8
* @see Invoice::setPaymentTerms()
*/
public function setTerms(?string $terms): self {
$this->terms = $terms;
Expand Down
15 changes: 9 additions & 6 deletions src/Presets/Peppol.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,18 @@ public function getRules(): array {
return "A buyer reference or purchase order reference MUST be provided.";
};
$res['PEPPOL-EN16931-R061'] = static function(Invoice $inv) {
if ($inv->getPayment() === null) return;
if ($inv->getPayment()->getMandate() === null) return;
if ($inv->getPayment()->getMandate()->getReference() === null) {
return "Mandate reference MUST be provided for direct debit";
foreach ($inv->getPayments() as $payment) {
if ($payment->getMandate() === null) continue;
if ($payment->getMandate()->getReference() === null) {
return "Mandate reference MUST be provided for direct debit";
}
}
};
$res['BG-17'] = static function(Invoice $inv) {
if ($inv->getPayment() !== null && count($inv->getPayment()->getTransfers()) > 1) {
return "An Invoice shall not have multiple credit transfers";
foreach ($inv->getPayments() as $payment) {
if (count($payment->getTransfers()) > 1) {
return "An Invoice shall not have multiple credit transfers";
}
}
};

Expand Down
47 changes: 24 additions & 23 deletions src/Readers/UblReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,21 @@ public function import(string $document): Invoice {
$invoice->setDelivery($this->parseDeliveryNode($deliveryNode));
}

// Payment nodes
$payment = $this->parsePaymentNodes($xml);
$invoice->setPayment($payment);
// Payment means nodes
foreach ($xml->getAll("{{$cac}}PaymentMeans") as $paymentMeansNode) {
$payment = $this->parsePaymentMeansNode($paymentMeansNode);
$invoice->addPayment($payment);
}

// BT-20: Payment terms
$termsNode = $xml->get("{{$cac}}PaymentTerms/{{$cbc}}Note");
if ($termsNode !== null) {
$invoice->setPaymentTerms($termsNode->asText());
$firstPayment = $invoice->getPayment(); // @phan-suppress-current-line PhanDeprecatedFunction
if ($firstPayment !== null) {
$firstPayment->setTerms($termsNode->asText()); // @phan-suppress-current-line PhanDeprecatedFunction
}
}

// Allowances and charges
foreach ($xml->getAll("{{$cac}}AllowanceCharge") as $node) {
Expand Down Expand Up @@ -497,24 +509,18 @@ private function parseDeliveryNode(UXML $xml): Delivery {


/**
* Parse payment nodes
* @param UXML $xml XML node
* @return Payment|null Payment instance or NULL if not found
* Parse payment means node
* @param UXML $xml Payment means node
* @return Payment Payment instance
*/
private function parsePaymentNodes(UXML $xml): ?Payment {
private function parsePaymentMeansNode(UXML $xml): Payment {
$cac = UblWriter::NS_CAC;
$cbc = UblWriter::NS_CBC;

// Get root nodes
$meansNode = $xml->get("{{$cac}}PaymentMeans");
$termsNode = $xml->get("{{$cac}}PaymentTerms/{{$cbc}}Note");
if ($meansNode === null && $termsNode === null) return null;

$payment = new Payment();

// BT-81: Payment means code
// BT-82: Payment means name
$meansCodeNode = $xml->get("{{$cac}}PaymentMeans/{{$cbc}}PaymentMeansCode");
$meansCodeNode = $xml->get("{{$cbc}}PaymentMeansCode");
if ($meansCodeNode !== null) {
$payment->setMeansCode($meansCodeNode->asText());
if ($meansCodeNode->element()->hasAttribute('name')) {
Expand All @@ -523,34 +529,29 @@ private function parsePaymentNodes(UXML $xml): ?Payment {
}

// BT-83: Payment ID
$paymentIdNode = $xml->get("{{$cac}}PaymentMeans/{{$cbc}}PaymentID");
$paymentIdNode = $xml->get("{{$cbc}}PaymentID");
if ($paymentIdNode !== null) {
$payment->setId($paymentIdNode->asText());
}

// BG-18: Payment card
$cardNode = $xml->get("{{$cac}}PaymentMeans/{{$cac}}CardAccount");
$cardNode = $xml->get("{{$cac}}CardAccount");
if ($cardNode !== null) {
$payment->setCard($this->parsePaymentCardNode($cardNode));
}

// BG-17: Payment transfers
$transferNodes = $xml->getAll("{{$cac}}PaymentMeans/{{$cac}}PayeeFinancialAccount");
$transferNodes = $xml->getAll("{{$cac}}PayeeFinancialAccount");
foreach ($transferNodes as $transferNode) {
$payment->addTransfer($this->parsePaymentTransferNode($transferNode));
}

// BG-19: Payment mandate
$mandateNode = $xml->get("{{$cac}}PaymentMeans/{{$cac}}PaymentMandate");
$mandateNode = $xml->get("{{$cac}}PaymentMandate");
if ($mandateNode !== null) {
$payment->setMandate($this->parsePaymentMandateNode($mandateNode));
}

// BT-20: Payment terms
if ($termsNode !== null) {
$payment->setTerms($termsNode->asText());
}

return $payment;
}

Expand Down
40 changes: 22 additions & 18 deletions src/Traits/InvoiceValidationTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -168,26 +168,30 @@ private function getDefaultRules(): array {
}
};
$res['BR-49'] = static function(Invoice $inv) {
if ($inv->getPayment() === null) return;
if ($inv->getPayment()->getMeansCode() === null && $inv->getPayment()->getTerms() === null) {
return "A Payment instruction (BG-16) shall specify the Payment means type code (BT-81)";
if ($inv->getPaymentTerms() !== null) return;
foreach ($inv->getPayments() as $payment) {
if ($payment->getMeansCode() === null) {
return "A Payment instruction (BG-16) shall specify the Payment means type code (BT-81)";
}
}
};
$res['BR-50'] = static function(Invoice $inv) {
if ($inv->getPayment() === null) return;
foreach ($inv->getPayment()->getTransfers() as $transfer) {
if ($transfer->getAccountId() === null) {
return "A Payment account identifier (BT-84) shall be present if Credit transfer (BG-17) " .
"information is provided in the Invoice";
foreach ($inv->getPayments() as $payment) {
foreach ($payment->getTransfers() as $transfer) {
if ($transfer->getAccountId() === null) {
return "A Payment account identifier (BT-84) shall be present if Credit transfer (BG-17) " .
"information is provided in the Invoice";
}
}
}
};
$res['BR-51'] = static function(Invoice $inv) {
if ($inv->getPayment() === null) return;
if ($inv->getPayment()->getCard() === null) return;
if ($inv->getPayment()->getCard()->getPan() === null) {
return "The last 4 to 6 digits of the Payment card primary account number (BT-87) " .
"shall be present if Payment card information (BG-18) is provided in the Invoice";
foreach ($inv->getPayments() as $payment) {
if ($payment->getCard() === null) continue;
if ($payment->getCard()->getPan() === null) {
return "The last 4 to 6 digits of the Payment card primary account number (BT-87) " .
"shall be present if Payment card information (BG-18) is provided in the Invoice";
}
}
};
$res['BR-52'] = static function(Invoice $inv) {
Expand All @@ -198,11 +202,11 @@ private function getDefaultRules(): array {
}
};
$res['BR-61'] = static function(Invoice $inv) {
if ($inv->getPayment() === null) return;
if (!in_array($inv->getPayment()->getMeansCode(), ['30', '58'])) return;
if (empty($inv->getPayment()->getTransfers())) {
return "If the Payment means type code (BT-81) means SEPA credit transfer, Local credit transfer or " .
"Non-SEPA international credit transfer, the Payment account identifier (BT-84) shall be present";
foreach ($inv->getPayments() as $payment) {
if (in_array($payment->getMeansCode(), ['30', '58']) && empty($payment->getTransfers())) {
return "If the Payment means type code (BT-81) means SEPA credit transfer, Local credit transfer or " .
"Non-SEPA international credit transfer, the Payment account identifier (BT-84) shall be present";
}
}
};
$res['BR-64'] = static function(Invoice $inv) {
Expand Down
24 changes: 12 additions & 12 deletions src/Writers/UblWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,16 @@ public function export(Invoice $invoice): string {
$this->addDeliveryNode($xml, $delivery);
}

// Payment nodes
$payment = $invoice->getPayment();
if ($payment !== null) {
$this->addPaymentNodes($xml, $payment, $isCreditNoteProfile ? $dueDate : null);
// Payment means nodes
foreach ($invoice->getPayments() as $payment) {
$this->addPaymentMeansNode($xml, $payment, $isCreditNoteProfile ? $dueDate : null);
}

// BT-20: Payment terms
$firstPayment = $invoice->getPayment(); // @phan-suppress-current-line PhanDeprecatedFunction
$paymentTerms = $invoice->getPaymentTerms() ?? ($firstPayment === null ? null : $firstPayment->getTerms()); // @phan-suppress-current-line PhanDeprecatedFunction
if ($paymentTerms !== null) {
$xml->add('cac:PaymentTerms')->add('cbc:Note', $paymentTerms);
}

// Allowances and charges
Expand Down Expand Up @@ -568,12 +574,12 @@ private function addDeliveryNode(UXML $parent, Delivery $delivery) {


/**
* Add payment nodes
* Add payment means node
* @param UXML $parent Invoice element
* @param Payment $payment Payment instance
* @param DateTime|null $dueDate Invoice due date (for credit note profile)
*/
private function addPaymentNodes(UXML $parent, Payment $payment, ?DateTime $dueDate) {
private function addPaymentMeansNode(UXML $parent, Payment $payment, ?DateTime $dueDate) {
$xml = $parent->add('cac:PaymentMeans');

// BT-81: Payment means code
Expand Down Expand Up @@ -617,12 +623,6 @@ private function addPaymentNodes(UXML $parent, Payment $payment, ?DateTime $dueD
if ($xml->isEmpty()) {
$xml->remove();
}

// BT-20: Payment terms
$terms = $payment->getTerms();
if ($terms !== null) {
$parent->add('cac:PaymentTerms')->add('cbc:Note', $terms);
}
}


Expand Down
2 changes: 1 addition & 1 deletion tests/Writers/UblWriterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ public function testCanGenerateValidInvoice(): void {
public function testCanGenerateValidCreditNote(): void {
$invoice = $this->getSampleInvoice();
$invoice->setType(Invoice::TYPE_CREDIT_NOTE);
$invoice->setPayment((new Payment)->setMeansCode('10')->setMeansText('In cash'));
$invoice->addPayment((new Payment)->setMeansCode('10')->setMeansText('In cash'));
$invoice->validate();
$contents = $this->writer->export($invoice);
$this->assertTrue($this->validateInvoice($contents, 'credit'));
Expand Down

0 comments on commit 26437c2

Please sign in to comment.