From 5bd411b3949231dd07620bd0e74f9a4906ee6e00 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 8 Aug 2024 12:32:31 +1000 Subject: [PATCH] Working on Peppol --- app/Mail/Engine/PaymentEmailEngine.php | 1 + .../EDocument/Gateway/Storecove/Storecove.php | 6 +- app/Services/EDocument/Standards/Peppol.php | 231 ++++++++++++++++-- composer.json | 3 +- composer.lock | 19 +- lang/en/texts.php | 2 +- .../Einvoice/Storecove/StorecoveTest.php | 22 +- 7 files changed, 243 insertions(+), 41 deletions(-) diff --git a/app/Mail/Engine/PaymentEmailEngine.php b/app/Mail/Engine/PaymentEmailEngine.php index d23772402310..c5832db31fdb 100644 --- a/app/Mail/Engine/PaymentEmailEngine.php +++ b/app/Mail/Engine/PaymentEmailEngine.php @@ -262,6 +262,7 @@ class PaymentEmailEngine extends BaseEmailEngine $data['$client.email'] = &$data['$email']; $data['$client.balance'] = ['value' => Number::formatMoney($this->client->balance, $this->client), 'label' => ctrans('texts.account_balance')]; + $data['$client.payment_balance'] = ['value' => Number::formatMoney($this->client->payment_balance, $this->client), 'label' => ctrans('texts.payment_balance_on_file')]; $data['$outstanding'] = ['value' => Number::formatMoney($this->client->balance, $this->client), 'label' => ctrans('texts.account_balance')]; $data['$client_balance'] = ['value' => Number::formatMoney($this->client->balance, $this->client), 'label' => ctrans('texts.account_balance')]; $data['$paid_to_date'] = ['value' => Number::formatMoney($this->client->paid_to_date, $this->client), 'label' => ctrans('texts.paid_to_date')]; diff --git a/app/Services/EDocument/Gateway/Storecove/Storecove.php b/app/Services/EDocument/Gateway/Storecove/Storecove.php index 397e8813ff2b..ede48f486e8e 100644 --- a/app/Services/EDocument/Gateway/Storecove/Storecove.php +++ b/app/Services/EDocument/Gateway/Storecove/Storecove.php @@ -137,14 +137,14 @@ class Storecove { } - public function sendDocument(string $document, int $routing_id, array $identifiers = []) + public function sendDocument(string $document, int $routing_id, array $override_payload = []) { $payload = [ "legalEntityId" => $routing_id, "idempotencyGuid"=> \Illuminate\Support\Str::uuid(), "routing" => [ - "eIdentifiers" => $identifiers, + "eIdentifiers" => [], "emails" => ["david@invoiceninja.com"] ], "document"=> [ @@ -157,6 +157,8 @@ class Storecove { ], ]; + $payload = array_merge($payload, $override_payload); + $uri = "document_submissions"; nlog($payload); diff --git a/app/Services/EDocument/Standards/Peppol.php b/app/Services/EDocument/Standards/Peppol.php index d6c6e8f538ee..4e58949853b9 100644 --- a/app/Services/EDocument/Standards/Peppol.php +++ b/app/Services/EDocument/Standards/Peppol.php @@ -13,22 +13,25 @@ namespace App\Services\EDocument\Standards; use App\Models\Company; use App\Models\Invoice; +use App\Helpers\Invoice\Taxer; use App\Services\AbstractService; use App\Helpers\Invoice\InvoiceSum; use InvoiceNinja\EInvoice\EInvoice; +use App\Utils\Traits\NumberFormatter; use App\Helpers\Invoice\InvoiceSumInclusive; -use App\Helpers\Invoice\Taxer; use InvoiceNinja\EInvoice\Models\Peppol\PaymentMeans; use InvoiceNinja\EInvoice\Models\Peppol\ItemType\Item; use InvoiceNinja\EInvoice\Models\Peppol\PartyType\Party; use InvoiceNinja\EInvoice\Models\Peppol\PriceType\Price; +use InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\ID; use InvoiceNinja\EInvoice\Models\Peppol\AddressType\Address; use InvoiceNinja\EInvoice\Models\Peppol\ContactType\Contact; use InvoiceNinja\EInvoice\Models\Peppol\CountryType\Country; +use InvoiceNinja\EInvoice\Models\Peppol\PartyIdentification; use InvoiceNinja\EInvoice\Models\Peppol\AmountType\TaxAmount; +use InvoiceNinja\EInvoice\Models\Peppol\Party as PeppolParty; use InvoiceNinja\EInvoice\Models\Peppol\TaxTotalType\TaxTotal; use App\Services\EDocument\Standards\Settings\PropertyResolver; -use App\Utils\Traits\NumberFormatter; use InvoiceNinja\EInvoice\Models\Peppol\AmountType\PriceAmount; use InvoiceNinja\EInvoice\Models\Peppol\PartyNameType\PartyName; use InvoiceNinja\EInvoice\Models\Peppol\TaxSchemeType\TaxScheme; @@ -42,14 +45,13 @@ use InvoiceNinja\EInvoice\Models\Peppol\TaxScheme as PeppolTaxScheme; use InvoiceNinja\EInvoice\Models\Peppol\AmountType\TaxExclusiveAmount; use InvoiceNinja\EInvoice\Models\Peppol\AmountType\TaxInclusiveAmount; use InvoiceNinja\EInvoice\Models\Peppol\AmountType\LineExtensionAmount; +use InvoiceNinja\EInvoice\Models\Peppol\OrderReferenceType\OrderReference; use InvoiceNinja\EInvoice\Models\Peppol\MonetaryTotalType\LegalMonetaryTotal; use InvoiceNinja\EInvoice\Models\Peppol\TaxCategoryType\ClassifiedTaxCategory; use InvoiceNinja\EInvoice\Models\Peppol\CustomerPartyType\AccountingCustomerParty; use InvoiceNinja\EInvoice\Models\Peppol\SupplierPartyType\AccountingSupplierParty; use InvoiceNinja\EInvoice\Models\Peppol\FinancialAccountType\PayeeFinancialAccount; -use InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\ID; -use InvoiceNinja\EInvoice\Models\Peppol\Party as PeppolParty; -use InvoiceNinja\EInvoice\Models\Peppol\PartyIdentification; +use InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\CustomerAssignedAccountID; class Peppol extends AbstractService { @@ -103,7 +105,7 @@ class Peppol extends AbstractService 'DE' => 'VAT', //tested - requires Payment Means to be defined. 'DK' => 'ERST', 'EE' => 'VAT', - 'ES' => 'VAT', + 'ES' => 'VAT', //tested - B2G pending 'FI' => 'VAT', 'FR' => 'VAT', 'GR' => 'VAT', @@ -165,6 +167,8 @@ class Peppol extends AbstractService private EInvoice $e; + private array $storecove_meta = []; + /** * @param Invoice $invoice */ @@ -649,7 +653,7 @@ class Peppol extends AbstractService { $acp = new AccountingCustomerParty(); - + $party = new Party(); if(strlen($this->invoice->client->vat_number ?? '') > 1) { @@ -740,7 +744,15 @@ class Peppol extends AbstractService return $total; } + + ///////////////// Helper Methods ///////////////////////// + /** + * setInvoiceDefaults + * + * Stubs a default einvoice + * @return self + */ public function setInvoiceDefaults(): self { $settings = [ @@ -769,7 +781,15 @@ class Peppol extends AbstractService return $this; } - + + /** + * getSetting + * + * Attempts to harvest and return a preconfigured prop from company / client / invoice settings + * + * @param string $property_path + * @return mixed + */ public function getSetting(string $property_path): mixed { @@ -783,8 +803,14 @@ class Peppol extends AbstractService return null; } - - public function countryLevelMutators():self + + /** + * countryLevelMutators + * + * Runs country level specific requirements for the e-invoice + * @return self + */ + private function countryLevelMutators():self { if(method_exists($this, $this->invoice->company->country()->iso_3166_2)) @@ -792,7 +818,14 @@ class Peppol extends AbstractService return $this; } - + + /** + * setPaymentMeans + * + * Sets the payment means - if it exists + * @param bool $required + * @return self + */ private function setPaymentMeans(bool $required = false): self { @@ -803,12 +836,132 @@ class Peppol extends AbstractService return $this; } - if($required) - throw new \Exception('e-invoice generation halted:: Payment Means required'); + return $this->checkRequired($required, "Payment Means"); + + } + + /** + * setOrderReference + * + * sets the order reference - if it exists (Never rely on settings for this) + * + * @param bool $required + * @return self + */ + private function setOrderReference(bool $required = false): self + { + $this->p_invoice->BuyerReference = $this->invoice->po_number ?? ''; + + if(strlen($this->invoice->po_number ?? '') > 1) + { + $order_reference = new OrderReference(); + $id = new ID(); + $id->value = $this->invoice->po_number; + + $order_reference->ID = $id; + + $this->p_invoice->OrderReference = $order_reference; + + $this->setStorecoveMeta(["document" => [ + "invoice" => [ + "references" => [ + "documentType" => "purchase_order", + "documentId" => $this->invoice->po_number, + ], + ], + ] + ]); + + return $this; + } + + return $this->checkRequired($required, 'Order Reference'); - return $this; } + /** + * setCustomerAssignedAccountId + * + * Sets the client id_number CAN rely on settings + * + * @param bool $required + * @return self + */ + private function setCustomerAssignedAccountId(bool $required = false): self + { + //@phpstan-ignore-next-line + if(isset($this->p_invoice->AccountingCustomerParty->CustomerAssignedAccountID)){ + return $this; + } + elseif($customer_assigned_account_id = $this->getSetting('Invoice.AccountingCustomerParty.CustomerAssignedAccountID')){ + + $this->p_invoice->AccountingCustomerParty->CustomerAssignedAccountID = $customer_assigned_account_id; + return $this; + } + elseif(strlen($this->invoice->client->id_number ?? '') > 1){ + + $customer_assigned_account_id = new CustomerAssignedAccountID(); + $customer_assigned_account_id->value = $this->invoice->client->id_number; + + $this->p_invoice->AccountingCustomerParty->CustomerAssignedAccountID = $customer_assigned_account_id; + return $this; + } + + //@phpstan-ignore-next-line + return $this->checkRequired($required, 'Client ID Number'); + + } + + /** + * Check Required + * + * Throws if a required field is missing. + * + * @param bool $required + * @param string $section + * @return self + */ + private function checkRequired(bool $required, string $section): self + { + + return $required ? throw new \Exception("e-invoice generation halted:: {$section} required") : $this; + + } + + + /** + * Builds the Routing object for StoreCove + * + * @param string $schemeId + * @param string $id + * @return array + */ + private function buildRouting(string $schemeId, string $id): array + { + + return + [ + "routing" => [ + "publicIdentifiers" => [ + [ + "scheme" => $schemeId, + "id" => $id + ] + ] + ] + ]; + } + + + + + + + + + + ////////////////////////// Country level mutators ///////////////////////////////////// + /** * DE * @@ -824,7 +977,21 @@ class Peppol extends AbstractService return $this; } - + + /** + * setStorecoveMeta + * + * updates the storecove payload for sending documents + * + * @param array $meta + * @return self + */ + private function setStorecoveMeta(array $meta): self + { + $this->storecove_meta = array_merge($this->storecove_meta, $meta); + + return $this; + } /** * CH * @@ -867,10 +1034,10 @@ class Peppol extends AbstractService * ES * * @Pending + * B2G configuration + * B2G Testing * - * ES:DIRE - routing identifier - * - * testing. //293098 + * testing. // routing identifier - 293098 * * @return self */ @@ -923,17 +1090,37 @@ class Peppol extends AbstractService private function FR(): self { + // When sending invoices to the French government (Chorus Pro): - // All invoices have to be routed to SIRET 0009:11000201100044. There is no test environment for sending to public entities. - // The SIRET / 0009 identifier of the final recipient is to be included in the invoice.accountingCustomerParty.publicIdentifiers array. + if($this->invoice->client->classification == 'government'){ + //route to SIRET 0009:11000201100044 + $this->setStorecoveMeta($this->buildRouting('FR:SIRET', "0009:11000201100044")); + + // The SIRET / 0009 identifier of the final recipient is to be included in the invoice.accountingCustomerParty.publicIdentifiers array. + $this->setCustomerAssignedAccountId(true); + + } + + + if(strlen($this->invoice->client->id_number ?? '') == 9) { + //SIREN + $this->setStorecoveMeta($this->buildRouting('FR:SIREN', "0002:{$this->invoice->client->id_number}")); + } + else { + //SIRET + $this->setStorecoveMeta($this->buildRouting('FR:SIRET', "0009:{$this->invoice->client->id_number}")); + } + + // ??????????????????????? //@TODO // The service code must be sent in invoice.buyerReference (deprecated) or the invoice.references array (documentType buyer_reference) - // The commitment number must be sent in the invoice.orderReference (deprecated) or the invoice.references array (documentType purchase_order). + if(strlen($this->invoice->po_number ?? '') >1) { + $this->setOrderReference(false); + } - // Invoices to companies (SIRET / 0009 or SIRENE / 0002) are routed directly to that identifier. return $this; } diff --git a/composer.json b/composer.json index f33899c9ee3c..cba4ad290b11 100644 --- a/composer.json +++ b/composer.json @@ -40,6 +40,7 @@ "asm/php-ansible": "dev-main", "authorizenet/authorizenet": "^2.0", "awobaz/compoships": "^2.1", + "aws/aws-sdk-php": "^3.319", "bacon/bacon-qr-code": "^2.0", "beganovich/snappdf": "dev-master", "braintree/braintree_php": "^6.0", @@ -201,4 +202,4 @@ ], "minimum-stability": "dev", "prefer-stable": true -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index 55d090faa651..4fb0ae985936 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6eda3a2962158b87dab46711e65a8438", + "content-hash": "95e7bd229644d1d8e768ecfbc78582cd", "packages": [ { "name": "adrienrn/php-mimetyper", @@ -535,16 +535,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.317.1", + "version": "3.319.0", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "dc1e3031c2721a25beb2e8fbb175b576e3d60ab9" + "reference": "a5c408d4cd1945d5fc817f45e46383634b610497" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/dc1e3031c2721a25beb2e8fbb175b576e3d60ab9", - "reference": "dc1e3031c2721a25beb2e8fbb175b576e3d60ab9", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a5c408d4cd1945d5fc817f45e46383634b610497", + "reference": "a5c408d4cd1945d5fc817f45e46383634b610497", "shasum": "" }, "require": { @@ -597,7 +597,10 @@ ], "psr-4": { "Aws\\": "src/" - } + }, + "exclude-from-classmap": [ + "src/data/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -624,9 +627,9 @@ "support": { "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.317.1" + "source": "https://github.com/aws/aws-sdk-php/tree/3.319.0" }, - "time": "2024-08-02T18:09:42+00:00" + "time": "2024-08-07T18:05:51+00:00" }, { "name": "bacon/bacon-qr-code", diff --git a/lang/en/texts.php b/lang/en/texts.php index 6a1dcd6af06a..37bbb24a3f46 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5313,7 +5313,7 @@ $lang = array( 'forever_free' => 'Forever Free', 'comments_only' => 'Comments Only', 'payment_balance_on_file' => 'Payment Balance On File', - + 'ubl_email_attachment_help' => 'For more e-invoice settings please navigate :here', ); return $lang; diff --git a/tests/Integration/Einvoice/Storecove/StorecoveTest.php b/tests/Integration/Einvoice/Storecove/StorecoveTest.php index d227c6848254..861f428994d3 100644 --- a/tests/Integration/Einvoice/Storecove/StorecoveTest.php +++ b/tests/Integration/Einvoice/Storecove/StorecoveTest.php @@ -639,10 +639,14 @@ $x = ' nlog($xml); $identifiers = [ - [ - 'scheme' => 'ES:VAT', - 'id' => 'ESB53625999' - ], + "routing" => [ + "eIdentifiers" => [ + [ + 'scheme' => 'ES:VAT', + 'id' => 'ESB53625999' + ], + ] + ] ]; $sc = new \App\Services\EDocument\Gateway\Storecove\Storecove(); @@ -673,9 +677,13 @@ $x = ' nlog($xml); $identifiers = [ - [ - 'scheme' => 'DE:VAT', - 'id' => 'DE010101010' + "routing" => [ + "eIdentifiers" => [ + [ + 'scheme' => 'DE:VAT', + 'id' => 'DE010101010' + ] + ] ] ];