From 4e8197a6232ba1254f852c9d2edfcad36145b9f6 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 19 Aug 2024 11:45:34 +1000 Subject: [PATCH] peppol --- .../EDocument/Gateway/Storecove/Storecove.php | 1 - app/Services/EDocument/Standards/Peppol.php | 140 ++++++++--- .../Einvoice/Storecove/StorecoveTest.php | 233 +++++++++++++++++- 3 files changed, 328 insertions(+), 46 deletions(-) diff --git a/app/Services/EDocument/Gateway/Storecove/Storecove.php b/app/Services/EDocument/Gateway/Storecove/Storecove.php index 9734abfa587e..be840d084625 100644 --- a/app/Services/EDocument/Gateway/Storecove/Storecove.php +++ b/app/Services/EDocument/Gateway/Storecove/Storecove.php @@ -98,7 +98,6 @@ class Storecove { public function sendJsonDocument($document) { - $payload = [ "legalEntityId" => 290868, "idempotencyGuid" => \Illuminate\Support\Str::uuid(), diff --git a/app/Services/EDocument/Standards/Peppol.php b/app/Services/EDocument/Standards/Peppol.php index 9b0dbfba7092..6afe2024807a 100644 --- a/app/Services/EDocument/Standards/Peppol.php +++ b/app/Services/EDocument/Standards/Peppol.php @@ -112,8 +112,8 @@ class Peppol extends AbstractService 'HR' => 'VAT', 'HU' => 'VAT', 'IE' => 'VAT', - 'IT' => 'IVA', //tested - Requires a Customer Party Identification (VAT number) - 'IT' => 'CF', //tested - Requires a Customer Party Identification (VAT number) + 'IT' => 'IVA', //tested - Requires a Customer Party Identification (VAT number) - 'IT senders must first be provisioned in the partner system.' + 'IT' => 'CF', //tested - Requires a Customer Party Identification (VAT number) - 'IT senders must first be provisioned in the partner system.' 'LT' => 'VAT', 'LU' => 'VAT', 'LV' => 'VAT', @@ -158,10 +158,13 @@ class Peppol extends AbstractService // 0 1 2 3 // ["Country" => ["B2X","Legal","Tax","Routing"], private array $routing_rules = [ - "US" => ["B","DUNS, GLN, LEI","US:EIN, US:SSN","DUNS, GLN, LEI"], - "CA" => ["B","CA:CBN","","CA:CBN"], - "MX" => ["B","MX:RFC","","MX:RFC"], - "AU" => ["B+G","AU:ABN","","AU:ABN"], + "US" => [ + ["B","DUNS, GLN, LEI","US:EIN","DUNS, GLN, LEI"], + // ["B","DUNS, GLN, LEI","US:SSN","DUNS, GLN, LEI"], + ], + "CA" => ["B","CA:CBN",false,"CA:CBN"], + "MX" => ["B","MX:RFC",false,"MX:RFC"], + "AU" => ["B+G","AU:ABN",false,"AU:ABN"], "NZ" => ["B+G","GLN","NZ:GST","GLN"], "CH" => ["B+G","CH:UIDB","CH:VAT","CH:UIDB"], "IS" => ["B+G","IS:KTNR","IS:VAT","IS:KTNR"], @@ -170,7 +173,7 @@ class Peppol extends AbstractService "AD" => ["B+G","","AD:VAT","AD:VAT"], "AL" => ["B+G","","AL:VAT","AL:VAT"], "AT" => [ - ["G","AT:GOV","","9915:b"], + ["G","AT:GOV",false,"9915:b"], ["B","","AT:VAT","AT:VAT"], ], "BA" => ["B+G","","BA:VAT","BA:VAT"], @@ -179,7 +182,7 @@ class Peppol extends AbstractService "CY" => ["B+G","","CY:VAT","CY:VAT"], "CZ" => ["B+G","","CZ:VAT","CZ:VAT"], "DE" => [ - ["G","DE:LWID","","DE:LWID"], + ["G","DE:LWID",false,"DE:LWID"], ["B","","DE:VAT","DE:VAT"], ], "DK" => ["B+G","DK:DIGST","DK:ERST","DK:DIGST"], @@ -187,7 +190,7 @@ class Peppol extends AbstractService "ES" => ["B","","ES:VAT","ES:VAT"], "FI" => ["B+G","FI:OVT","FI:VAT","FI:OVT"], "FR" => [ - ["G","FR:SIRET + customerAssignedAccountIdValue","","0009:11000201100044"], + ["G","FR:SIRET + customerAssignedAccountIdValue",false,"0009:11000201100044"], ["B","FR:SIRENE or FR:SIRET","FR:VAT","FR:SIRENE or FR:SIRET"], ], "GR" => ["B+G","","GR:VAT","GR:VAT"], @@ -195,10 +198,11 @@ class Peppol extends AbstractService "HU" => ["B+G","","HU:VAT","HU:VAT"], "IE" => ["B+G","","IE:VAT","IE:VAT"], "IT" => [ - ["G (Peppol)","","IT:IVA","IT:CUUO"], - ["B (SDI)","","IT:CF and/or IT:IVA","IT:CUUO"], - ["C (SDI)","","IT:CF","Email"], - ["G (SDI)","","IT:IVA","IT:CUUO"], + ["G","","IT:IVA","IT:CUUO"], // (Peppol) + ["B","","IT:IVA","IT:CUUO"], // (SDI) + // ["B","","IT:CF","IT:CUUO"], // (SDI) + ["C","","IT:CF","Email"],// (SDI) + ["G","","IT:IVA","IT:CUUO"],// (SDI) ], "LT" => ["B+G","LT:LEC","LT:VAT","LT:LEC"], "LU" => ["B+G","LU:MAT","LU:VAT","LU:VAT"], @@ -207,7 +211,7 @@ class Peppol extends AbstractService "ME" => ["B+G","","ME:VAT","ME:VAT"], "MK" => ["B+G","","MK:VAT","MK:VAT"], "MT" => ["B+G","","MT:VAT","MT:VAT"], - "NL" => ["G","NL:OINO","","NL:OINO"], + "NL" => ["G","NL:OINO",false,"NL:OINO"], "NL" => ["B","NL:KVK","NL:VAT","NL:KVK or NL:VAT"], "PL" => ["G+B","","PL:VAT","PL:VAT"], "PT" => ["G+B","","PT:VAT","PT:VAT"], @@ -223,12 +227,12 @@ class Peppol extends AbstractService "JP" => ["B","JP:SST","JP:IIN","JP:SST"], "MY" => ["B","MY:EIF","MY:TIN","MY:EIF"], "SG" => [ - ["G","SG:UEN","","0195:SGUENT08GA0028A"], - ["B","SG:UEN","SG:GST (optional)","SG:UEN"], + ["G","SG:UEN",false,"0195:SGUENT08GA0028A"], + ["B","SG:UEN","SG:GST","SG:UEN"], ], "GB" => ["B","","GB:VAT","GB:VAT"], "SA" => ["B","","SA:TIN","Email"], - "Other" => ["B","DUNS, GLN, LEI","","DUNS, GLN, LEI"], + "Other" => ["B","DUNS, GLN, LEI",false,"DUNS, GLN, LEI"], ]; private Company $company; @@ -726,6 +730,31 @@ class Peppol extends AbstractService return $asp; } + private function resolveTaxScheme(): mixed + { + $rules = isset($this->routing_rules[$this->invoice->client->country->iso_3166_2]) ? $this->routing_rules[$this->invoice->client->country->iso_3166_2] : false; + + match($this->invoice->client->classification){ + "business" => $code = "B", + "government" => $code = "G", + "individual" => $code = "C", + default => $code = false, + }; + + if(count($rules) > 1){ + + foreach($rules as $rule) + { + if(stripos($rule[0], $code) !== false) { + return $rule[2]; + } + } + } + + return false; + + } + private function getAccountingCustomerParty(): AccountingCustomerParty { @@ -736,10 +765,13 @@ class Peppol extends AbstractService if(strlen($this->invoice->client->vat_number ?? '') > 1) { $pi = new PartyIdentification; + $vatID = new ID; - $vatID->schemeID = 'CH:MWST'; + + if($scheme = $this->resolveTaxScheme()) + $vatID->schemeID = $scheme; + $vatID->value = $this->invoice->client->vat_number; - $pi->ID = $vatID; $party->PartyIdentification[] = $pi; @@ -1055,26 +1087,34 @@ class Peppol extends AbstractService /** * Builds the Routing object for StoreCove * - * @param string $schemeId - * @param string $id + * @param array $identifiers * @return array */ - private function buildRouting(string $schemeId, string $id): array + private function buildRouting(array $identifiers): array { return [ "routing" => [ - "publicIdentifiers" => [ - [ - "scheme" => $schemeId, - "id" => $id - ] - ] + "eIdentifiers" => + $identifiers, + ] ]; } + private function setEmailRouting(string $email): self + { + $meta = $this->getStorecoveMeta(); + $emails = isset($meta['routing']['emails']) ? $meta['routing']['emails'] : ($meta['routing']['emails'] = []); + + array_push($emails, $email); + + $this->setStorecoveMeta($emails); + + return $this; + } + /** * setStorecoveMeta * @@ -1150,7 +1190,7 @@ class Peppol extends AbstractService if($this->invoice->client->classification == 'government') { //routing "b" for production "test" for test environment - $this->setStorecoveMeta($this->buildRouting('AT:GOV', "b")); + $this->setStorecoveMeta($this->buildRouting(["scheme" => 'AT:GOV', "id" => 'b'])); //for government clients this must be set. $this->setCustomerAssignedAccountId(true); @@ -1239,7 +1279,11 @@ class Peppol extends AbstractService if($this->invoice->client->classification == 'government'){ //route to SIRET 0009:11000201100044 - $this->setStorecoveMeta($this->buildRouting('FR:SIRET', "0009:11000201100044")); + $this->setStorecoveMeta($this->buildRouting([ + ["scheme" => 'FR:SIRET', "id" => '11000201100044'] + + // ["scheme" => 'FR:SIRET', "id" => '0009:11000201100044'] + ])); // The SIRET / 0009 identifier of the final recipient is to be included in the invoice.accountingCustomerParty.publicIdentifiers array. $this->setCustomerAssignedAccountId(true); @@ -1248,11 +1292,19 @@ class Peppol extends AbstractService if(strlen($this->invoice->client->id_number ?? '') == 9) { //SIREN - $this->setStorecoveMeta($this->buildRouting('FR:SIREN', "0002:{$this->invoice->client->id_number}")); + $this->setStorecoveMeta($this->buildRouting([ + ["scheme" => 'FR:SIRET', "id" => "{$this->invoice->client->id_number}"] + + // ["scheme" => 'FR:SIRET', "id" => "0002:{$this->invoice->client->id_number}"] + ])); } else { //SIRET - $this->setStorecoveMeta($this->buildRouting('FR:SIRET', "0009:{$this->invoice->client->id_number}")); + $this->setStorecoveMeta($this->buildRouting([ + ["scheme" => 'FR:SIRET', "id" => "{$this->invoice->client->id_number}"] + + // ["scheme" => 'FR:SIRET', "id" => "0009:{$this->invoice->client->id_number}"] + ])); } // Apparently this is not a special field according to support @@ -1271,18 +1323,24 @@ class Peppol extends AbstractService // IT Sender, IT Receiver, B2B/B2G // Provide the receiver IT:VAT and the receiver IT:CUUO (codice destinatario) - if($this->invoice->client->classification == 'government' && $this->invoice->company->country()->iso_3166_2 == 'IT') { - - $this->setStorecoveMeta($this->buildRouting('IT:VAT', $this->invoice->client->routing_id)); - + if(in_array($this->invoice->client->classification, ['business','government']) && $this->invoice->company->country()->iso_3166_2 == 'IT') { + nlog("italian business/government receiver"); + $this->setStorecoveMeta($this->buildRouting([ + ["scheme" => 'IT:IVA', "id" => $this->invoice->client->vat_number], + ["scheme" => 'IT:CUUO', "id" => $this->invoice->client->routing_id] + ])); return $this; } // IT Sender, IT Receiver, B2C // Provide the receiver IT:CF and the receiver IT:CUUO (codice destinatario) if($this->invoice->client->classification == 'individual' && $this->invoice->company->country()->iso_3166_2 == 'IT') { - - $this->setStorecoveMeta($this->buildRouting('IT:CF', $this->invoice->client->routing_id)); + nlog("business receiver"); + + $this->setStorecoveMeta($this->buildRouting([ + ["scheme" => 'IT:CF', "id" => $this->invoice->client->vat_number], + ["scheme" => 'IT:CUUO', "id" => $this->invoice->client->routing_id] + ])); return $this; } @@ -1293,7 +1351,10 @@ class Peppol extends AbstractService $code = $this->getClientRoutingCode(); - $this->setStorecoveMeta($this->buildRouting($code, $this->invoice->client->vat_number)); + nlog("foreign receiver"); + $this->setStorecoveMeta($this->buildRouting([ + ["scheme" => $code, "id" => $this->invoice->client->vat_number] + ])); return $this; } @@ -1311,7 +1372,6 @@ class Peppol extends AbstractService return $this; } - // non-IT Sender, IT Receiver, B2B/B2G // Provide the receiver IT:VAT and the receiver IT:CUUO (codice destinatario) diff --git a/tests/Integration/Einvoice/Storecove/StorecoveTest.php b/tests/Integration/Einvoice/Storecove/StorecoveTest.php index 92acb6ad09e0..36bcf1bb4fde 100644 --- a/tests/Integration/Einvoice/Storecove/StorecoveTest.php +++ b/tests/Integration/Einvoice/Storecove/StorecoveTest.php @@ -19,6 +19,7 @@ use Tests\MockAccountData; use App\DataMapper\InvoiceItem; use App\DataMapper\ClientSettings; use App\DataMapper\CompanySettings; +use App\Models\ClientContact; use App\Services\EDocument\Standards\Peppol; use Illuminate\Foundation\Testing\DatabaseTransactions; use InvoiceNinja\EInvoice\Models\Peppol\PaymentMeans; @@ -398,6 +399,170 @@ $x = ' } + + private function createITData($business = true) + { + + $this->routing_id = 294636; + + $settings = CompanySettings::defaults(); + $settings->company_logo = 'https://pdf.invoicing.co/favicon-v2.png'; + $settings->website = 'www.invoiceninja.it'; + $settings->address1 = 'Via del Corso, 28'; + $settings->address2 = 'Palazzo delle Telecomunicazioni'; + $settings->city = 'Roma'; + $settings->state = 'Lazio'; + $settings->postal_code = '00187'; + $settings->phone = '06 1234567'; + $settings->email = $this->faker->unique()->safeEmail(); + $settings->country_id = '380'; // Italy's ISO country code + $settings->vat_number = 'IT92443356490'; // Italian VAT number + $settings->id_number = 'RM 123456'; // Typical Italian company registration format + $settings->use_credits_payment = 'always'; + $settings->timezone_id = '1'; // CET (Central European Time) + $settings->entity_send_time = 0; + $settings->e_invoice_type = 'PEPPOL'; + $settings->currency_id = '3'; // Euro (EUR) + $settings->classification = 'business'; + + + $company = Company::factory()->create([ + 'account_id' => $this->account->id, + 'settings' => $settings, + ]); + + $this->user->companies()->attach($company->id, [ + 'account_id' => $this->account->id, + 'is_owner' => true, + 'is_admin' => 1, + 'is_locked' => 0, + 'permissions' => '', + 'notifications' => CompanySettings::notificationAdminDefaults(), + 'settings' => null, + ]); + + Client::unguard(); + + $c = + Client::create([ + 'company_id' => $company->id, + 'user_id' => $this->user->id, + 'name' => 'Impresa Esempio S.p.A.', + 'website' => 'https://www.impresa-esempio.it', + 'private_notes' => 'Queste sono note private per il cliente di prova.', + 'balance' => 0, + 'paid_to_date' => 0, + 'vat_number' => 'IT92443356489', // Italian VAT number with IT prefix + 'id_number' => 'B12345678', // Typical format for Italian company registration numbers + 'custom_value1' => '2024-07-22 10:00:00', + 'custom_value2' => 'blu', // Italian for blue + 'custom_value3' => 'parolaesempio', // Italian for sample word + 'custom_value4' => 'test@esempio.it', + 'address1' => 'Via Esempio 123', + 'address2' => '2º Piano, Ufficio 45', + 'city' => 'Roma', + 'state' => 'Lazio', + 'postal_code' => '00187', + 'country_id' => '380', // Italy + 'shipping_address1' => 'Via Esempio 123', + 'shipping_address2' => '2º Piano, Ufficio 45', + 'shipping_city' => 'Roma', + 'shipping_state' => 'Lazio', + 'shipping_postal_code' => '00187', + 'shipping_country_id' => '380', // Italy + 'settings' => ClientSettings::defaults(), + 'client_hash' => \Illuminate\Support\Str::random(32), + 'routing_id' => 'SCSCSCS', + 'classification' => 'business', + ]); + + ClientContact::factory()->create([ + 'company_id' => $company->id, + 'user_id' => $this->user->id, + 'client_id' => $c->id, + 'first_name' => 'Contact First', + 'last_name' => 'Contact Last', + 'email' => 'david+c1@invoiceninja.com', + ]); + + $c2 = + Client::create([ + 'company_id' => $company->id, + 'user_id' => $this->user->id, + 'name' => 'Impresa Esempio S.p.A.', + 'website' => 'https://www.impresa-esempio.it', + 'private_notes' => 'Queste sono note private per il cliente di prova.', + 'balance' => 0, + 'paid_to_date' => 0, + 'vat_number' => 'RSSMRA85M01H501Z', // Italian VAT number with IT prefix + 'id_number' => 'B12345678', // Typical format for Italian company registration numbers + 'custom_value1' => '2024-07-22 10:00:00', + 'custom_value2' => 'blu', // Italian for blue + 'custom_value3' => 'parolaesempio', // Italian for sample word + 'custom_value4' => 'test@esempio.it', + 'address1' => 'Via Esempio 123', + 'address2' => '2º Piano, Ufficio 45', + 'city' => 'Roma', + 'state' => 'Lazio', + 'postal_code' => '00187', + 'country_id' => '380', // Italy + 'shipping_address1' => 'Via Esempio 123', + 'shipping_address2' => '2º Piano, Ufficio 45', + 'shipping_city' => 'Roma', + 'shipping_state' => 'Lazio', + 'shipping_postal_code' => '00187', + 'shipping_country_id' => '380', // Italy + 'settings' => ClientSettings::defaults(), + 'client_hash' => \Illuminate\Support\Str::random(32), + 'routing_id' => 'SCSCSCS', + 'classification' => 'individual', + ]); + + + ClientContact::factory()->create([ + 'company_id' => $company->id, + 'user_id' => $this->user->id, + 'client_id' => $c2->id, + 'first_name' => 'Contact First', + 'last_name' => 'Contact Last', + 'email' => 'david+c2@invoiceninja.com', + ]); + + + $item = new InvoiceItem(); + $item->product_key = "Product Key"; + $item->notes = "Product Description"; + $item->cost = 10; + $item->quantity = 10; + $item->tax_rate1 = 22; + $item->tax_name1 = 'IVA'; + + $invoice = Invoice::factory()->create([ + 'company_id' => $company->id, + 'user_id' => $this->user->id, + 'client_id' => $business ? $c->id : $c2->id, + 'discount' => 0, + 'uses_inclusive_taxes' => false, + 'status_id' => 1, + 'tax_rate1' => 0, + 'tax_name1' => '', + 'tax_rate2' => 0, + 'tax_rate3' => 0, + 'tax_name2' => '', + 'tax_name3' => '', + 'line_items' => [$item], + 'number' => 'IT-'.rand(1000, 100000), + 'date' => now()->format('Y-m-d'), + 'due_date' => now()->addDays(14)->format('Y-m-d'), + ]); + + $invoice = $invoice->calc()->getInvoice(); + $invoice->service()->markSent()->save(); + + return $invoice; + + } + private function createESData() { $this->routing_id = 293098; @@ -468,7 +633,7 @@ $x = ' 'shipping_country_id' => '724', // Spain 'settings' => ClientSettings::Defaults(), 'client_hash' => \Illuminate\Support\Str::random(32), - 'routing_id' => '', + 'routing_id' => 'SCSCSC', ]); $item = new InvoiceItem(); @@ -726,7 +891,7 @@ $x = ' } - public function testAtGovernmentRules() + public function PestAtGovernmentRules() { $this->routing_id = 293801; @@ -758,6 +923,65 @@ $x = ' } + public function testItRules() + { + $invoice = $this->createITData(); + + $e_invoice = new \InvoiceNinja\EInvoice\Models\Peppol\Invoice(); + + $stub = json_decode('{"Invoice":{"Note":"Nooo","PaymentMeans":[{"ID":{"value":"afdasfasdfasdfas"},"PayeeFinancialAccount":{"Name":"PFA-NAME","ID":{"value":"DE89370400440532013000"},"AliasName":"PFA-Alias","AccountTypeCode":{"value":"CHECKING"},"AccountFormatCode":{"value":"IBAN"},"CurrencyCode":{"value":"EUR"},"FinancialInstitutionBranch":{"ID":{"value":"DEUTDEMMXXX"},"Name":"Deutsche Bank"}}}]}}'); + foreach($stub as $key => $value) { + $e_invoice->{$key} = $value; + } + + $invoice->e_invoice = $e_invoice; + $invoice->save(); + + $this->assertInstanceOf(Invoice::class, $invoice); + $this->assertInstanceof(\InvoiceNinja\EInvoice\Models\Peppol\Invoice::class, $e_invoice); + + $p = new Peppol($invoice); + + $p->run(); + $xml = $p->toXml(); + nlog($xml); + + $identifiers = $p->getStorecoveMeta(); + + $sc = new \App\Services\EDocument\Gateway\Storecove\Storecove(); + $sc->sendDocument($xml, $this->routing_id, $identifiers); + + + //test individual sending + + nlog("Individual"); + +$invoice = $this->createITData(false); + +$e_invoice = new \InvoiceNinja\EInvoice\Models\Peppol\Invoice(); + +$stub = json_decode('{"Invoice":{"Note":"Nooo","PaymentMeans":[{"ID":{"value":"afdasfasdfasdfas"},"PayeeFinancialAccount":{"Name":"PFA-NAME","ID":{"value":"DE89370400440532013000"},"AliasName":"PFA-Alias","AccountTypeCode":{"value":"CHECKING"},"AccountFormatCode":{"value":"IBAN"},"CurrencyCode":{"value":"EUR"},"FinancialInstitutionBranch":{"ID":{"value":"DEUTDEMMXXX"},"Name":"Deutsche Bank"}}}]}}'); +foreach($stub as $key => $value) { + $e_invoice->{$key} = $value; +} + +$invoice->e_invoice = $e_invoice; +$invoice->save(); + +$p = new Peppol($invoice); + +$p->run(); +$xml = $p->toXml(); +nlog($xml); + +$identifiers = $p->getStorecoveMeta(); + +$sc = new \App\Services\EDocument\Gateway\Storecove\Storecove(); +$sc->sendDocument($xml, $this->routing_id, $identifiers); + + + } + public function PestAtRules() { $this->routing_id = 293801; @@ -790,8 +1014,7 @@ $x = ' } - - public function RestFrRules() + public function PtestFrRules() { $invoice = $this->createFRData(); @@ -822,7 +1045,7 @@ $x = ' } - public function RtestEsRules() + public function PtestEsRules() { $invoice = $this->createESData();