From be00f173c4b6fbecd1fab245e84efda536eadf41 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 12 Aug 2024 14:14:31 +1000 Subject: [PATCH 1/5] Updated mail queries --- .../Controllers/EmailHistoryController.php | 2 +- app/Services/EDocument/Standards/Peppol.php | 17 +- .../Einvoice/Storecove/StorecoveTest.php | 221 ++++++++++-------- 3 files changed, 141 insertions(+), 99 deletions(-) diff --git a/app/Http/Controllers/EmailHistoryController.php b/app/Http/Controllers/EmailHistoryController.php index 4e7aaeb45c..564b338e88 100644 --- a/app/Http/Controllers/EmailHistoryController.php +++ b/app/Http/Controllers/EmailHistoryController.php @@ -51,9 +51,9 @@ class EmailHistoryController extends BaseController /** @var \App\Models\User $user */ $user = auth()->user(); - $data = SystemLog::where('company_id', $user->company()->id) ->where('category_id', SystemLog::CATEGORY_MAIL) + ->whereJsonContains('log->history->entity', $this->encodePrimaryKey($request->entity)) ->whereJsonContains('log->history->entity_id', $this->encodePrimaryKey($request->entity_id)) ->orderBy('id', 'DESC') ->cursor() diff --git a/app/Services/EDocument/Standards/Peppol.php b/app/Services/EDocument/Standards/Peppol.php index 8540e21480..d76802277b 100644 --- a/app/Services/EDocument/Standards/Peppol.php +++ b/app/Services/EDocument/Standards/Peppol.php @@ -90,7 +90,7 @@ class Peppol extends AbstractService 'NO' => 'VAT', 'AD' => 'VAT', 'AL' => 'VAT', - 'AT' => 'VAT', + 'AT' => 'VAT', //Pending Tests. 'BA' => 'VAT', 'BE' => 'VAT', 'BG' => 'VAT', @@ -102,12 +102,12 @@ class Peppol extends AbstractService 'SA' => 'TIN', //South Africa 'CY' => 'VAT', 'CZ' => 'VAT', - 'DE' => 'VAT', //tested - requires Payment Means to be defined. + 'DE' => 'VAT', //tested - Requires Payment Means to be defined. 'DK' => 'ERST', 'EE' => 'VAT', 'ES' => 'VAT', //tested - B2G pending 'FI' => 'VAT', - 'FR' => 'VAT', + 'FR' => 'VAT', //tested - Need to ensure Siren/Siret routing 'GR' => 'VAT', 'HR' => 'VAT', 'HU' => 'VAT', @@ -1026,12 +1026,21 @@ class Peppol extends AbstractService private function AT(): self { //special fields for sending to AT:GOV + + if($this->invoice->client->classification == 'government') { + //routing "b" for production "test" for test environment + $this->setStorecoveMeta($this->buildRouting('AT:GOV', "test")); + + //for government clients this must be set. + $this->setCustomerAssignedAccountId(true); + } + return $this; } private function AU(): self { - + //if payment means are included, they must be the same `type` return $this; } diff --git a/tests/Integration/Einvoice/Storecove/StorecoveTest.php b/tests/Integration/Einvoice/Storecove/StorecoveTest.php index e6e619dd8e..8fadf573be 100644 --- a/tests/Integration/Einvoice/Storecove/StorecoveTest.php +++ b/tests/Integration/Einvoice/Storecove/StorecoveTest.php @@ -43,27 +43,27 @@ class StorecoveTest extends TestCase // public function testCreateLegalEntity() // { - // $data = [ - // 'acts_as_receiver' => true, - // 'acts_as_sender' => true, - // 'advertisements' => ['invoice'], - // 'city' => $this->company->settings->city, - // 'country' => 'DE', - // 'county' => $this->company->settings->state, - // 'line1' => $this->company->settings->address1, - // 'line2' => $this->company->settings->address2, - // 'party_name' => $this->company->present()->name(), - // 'tax_registered' => true, - // 'tenant_id' => $this->company->company_key, - // 'zip' => $this->company->settings->postal_code, - // 'peppol_identifiers' => [ - // 'scheme' => 'DE:VAT', - // 'id' => 'DE:VAT' - // ], - // ]; + // $data = [ + // 'acts_as_receiver' => true, + // 'acts_as_sender' => true, + // 'advertisements' => ['invoice'], + // 'city' => $this->company->settings->city, + // 'country' => 'DE', + // 'county' => $this->company->settings->state, + // 'line1' => $this->company->settings->address1, + // 'line2' => $this->company->settings->address2, + // 'party_name' => $this->company->present()->name(), + // 'tax_registered' => true, + // 'tenant_id' => $this->company->company_key, + // 'zip' => $this->company->settings->postal_code, + // 'peppol_identifiers' => [ + // 'scheme' => 'DE:VAT', + // 'id' => 'DE:VAT' + // ], + // ]; - // $sc = new \App\Services\EDocument\Gateway\Storecove\Storecove(); - // $r = $sc->createLegalEntity($data, $this->company); + // $sc = new \App\Services\EDocument\Gateway\Storecove\Storecove(); + // $r = $sc->createLegalEntity($data, $this->company); // $this->assertIsArray($r); @@ -356,7 +356,7 @@ $x = ' } */ - public function testCreateCHClient() + public function XXestCreateCHClient() { Client::unguard(); @@ -616,30 +616,31 @@ $x = ' } - private function createDEData() + private function createATData() { - $this->routing_id = 290868; + $this->routing_id = 293801; $settings = CompanySettings::defaults(); $settings->company_logo = 'https://pdf.invoicing.co/favicon-v2.png'; - $settings->website = 'www.invoiceninja.de'; + $settings->website = 'www.invoiceninja.at'; $settings->address1 = 'Musterstraße 1'; - $settings->address2 = 'Etage 2, Büro 3'; - $settings->city = 'Berlin'; - $settings->state = 'Berlin'; - $settings->postal_code = '10115'; - $settings->phone = '030 1234567'; + $settings->address2 = 'Stockwerk 2, Büro 3'; + $settings->city = 'Vienna'; + $settings->state = 'Vienna'; + $settings->postal_code = '1010'; + $settings->phone = '+43 1 23456789'; $settings->email = $this->faker->unique()->safeEmail(); - $settings->country_id = '276'; // Germany's ISO country code - $settings->vat_number = 'DE123456789'; - $settings->id_number = 'HRB 12345'; + $settings->country_id = '40'; // Austria's ISO country code + $settings->vat_number = 'ATU92335648'; + $settings->id_number = 'FN 123456x'; $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'; + $company = Company::factory()->create([ 'account_id' => $this->account->id, 'settings' => $settings, @@ -655,77 +656,110 @@ $x = ' 'settings' => null, ]); - Client::unguard(); + Client::unguard(); - $c = - Client::create([ - 'company_id' => $company->id, - 'user_id' => $this->user->id, - 'name' => 'Beispiel Firma GmbH', - 'website' => 'https://www.beispiel-firma.de', - 'private_notes' => 'Dies sind private Notizen zum Testkunden.', - 'balance' => 0, - 'paid_to_date' => 0, - 'vat_number' => 'DE654321987', - 'id_number' => 'HRB 12345', // Typical format for German company registration numbers - 'custom_value1' => '2024-07-22 10:00:00', - 'custom_value2' => 'blau', - 'custom_value3' => 'musterwort', - 'custom_value4' => 'test@example.com', - 'address1' => 'Musterstraße 123', - 'address2' => '2. Etage, Büro 45', - 'city' => 'München', - 'state' => 'Bayern', - 'postal_code' => '80331', - 'country_id' => '276', // Germany - 'shipping_address1' => 'Musterstraße 123', - 'shipping_address2' => '2. Etage, Büro 45', - 'shipping_city' => 'München', - 'shipping_state' => 'Bayern', - 'shipping_postal_code' => '80331', - 'shipping_country_id' => '276', // Germany - 'settings' => ClientSettings::Defaults(), - 'client_hash' => \Illuminate\Support\Str::random(32), - 'routing_id' => '', - ]); - - - $item = new InvoiceItem(); - $item->product_key = "Product Key"; - $item->notes = "Product Description"; - $item->cost = 10; - $item->quantity = 10; - $item->tax_rate1 = 19; - $item->tax_name1 = 'mwst'; - - $invoice = Invoice::factory()->create([ + $c = + Client::create([ 'company_id' => $company->id, 'user_id' => $this->user->id, - 'client_id' => $c->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' => 'DE-'.rand(1000, 100000), - 'date' => now()->format('Y-m-d'), - 'due_date' => now()->addDays(14)->format('Y-m-d'), - ]); + 'name' => 'Beispiel Firma GmbH', + 'website' => 'https://www.beispiel-firma.at', + 'private_notes' => 'Dies sind private Notizen zum Testkunden.', + 'balance' => 0, + 'paid_to_date' => 0, + 'vat_number' => 'ATU87654321', + 'id_number' => 'FN 123456x', // Example format for Austrian company registration numbers + 'custom_value1' => '2024-07-22 10:00:00', + 'custom_value2' => 'blau', + 'custom_value3' => 'musterwort', + 'custom_value4' => 'test@example.com', + 'address1' => 'Musterstraße 123', + 'address2' => '2. Etage, Büro 45', + 'city' => 'Vienna', + 'state' => 'Vienna', + 'postal_code' => '1010', + 'country_id' => '40', // Austria + 'shipping_address1' => 'Musterstraße 123', + 'shipping_address2' => '2. Etage, Büro 45', + 'shipping_city' => 'Vienna', + 'shipping_state' => 'Vienna', + 'shipping_postal_code' => '1010', + 'shipping_country_id' => '40', // Austria + 'settings' => ClientSettings::Defaults(), + 'client_hash' => \Illuminate\Support\Str::random(32), + 'routing_id' => '', + 'classification' => 'business', + ]); - $invoice = $invoice->calc()->getInvoice(); - $invoice->service()->markSent()->save(); + $item = new InvoiceItem(); + $item->product_key = "Product Key"; + $item->notes = "Product Description"; + $item->cost = 10; + $item->quantity = 10; + $item->tax_rate1 = 20; + $item->tax_name1 = 'VAT'; + + $invoice = Invoice::factory()->create([ + 'company_id' => $company->id, + 'user_id' => $this->user->id, + 'client_id' => $c->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' => 'DE-'.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; } - public function testFrRules() + public function testAtRules() + { + $this->routing_id = 293801; + + $invoice = $this->createATData(); + + $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); + + } + + + public function RestFrRules() { $invoice = $this->createFRData(); @@ -756,7 +790,6 @@ $x = ' } - public function RtestEsRules() { From a8c7d49528f26f707fe0f15ff972ab0efc0dcc71 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 12 Aug 2024 14:30:19 +1000 Subject: [PATCH 2/5] entity history query --- app/Http/Controllers/EmailHistoryController.php | 2 +- tests/Feature/InvoiceEmailTest.php | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/EmailHistoryController.php b/app/Http/Controllers/EmailHistoryController.php index 564b338e88..605398ebab 100644 --- a/app/Http/Controllers/EmailHistoryController.php +++ b/app/Http/Controllers/EmailHistoryController.php @@ -53,7 +53,7 @@ class EmailHistoryController extends BaseController $data = SystemLog::where('company_id', $user->company()->id) ->where('category_id', SystemLog::CATEGORY_MAIL) - ->whereJsonContains('log->history->entity', $this->encodePrimaryKey($request->entity)) + ->whereJsonContains('log->history->entity', $request->entity) ->whereJsonContains('log->history->entity_id', $this->encodePrimaryKey($request->entity_id)) ->orderBy('id', 'DESC') ->cursor() diff --git a/tests/Feature/InvoiceEmailTest.php b/tests/Feature/InvoiceEmailTest.php index a41ec3cd81..dd3009859f 100644 --- a/tests/Feature/InvoiceEmailTest.php +++ b/tests/Feature/InvoiceEmailTest.php @@ -69,7 +69,7 @@ class InvoiceEmailTest extends TestCase $system_log->log = [ 'history' => [ 'entity_id' => $this->invoice->hashed_id, - 'entity_type' => 'invoice', + 'entity' => 'invoice', 'subject' => 'Invoice #1', 'events' => [ [ @@ -96,7 +96,7 @@ class InvoiceEmailTest extends TestCase $arr = $response->json(); - $this->assertEquals('invoice', $arr[0]['entity_type']); + $this->assertEquals('invoice', $arr[0]['entity']); $count = SystemLog::where('client_id', $this->client->id) ->where('category_id', SystemLog::CATEGORY_MAIL) @@ -117,7 +117,7 @@ class InvoiceEmailTest extends TestCase $system_log->log = [ 'history' => [ 'entity_id' => $this->invoice->hashed_id, - 'entity_type' => 'invoice', + 'entity' => 'invoice', 'subject' => 'Invoice #1', 'events' => [ [ @@ -147,8 +147,8 @@ class InvoiceEmailTest extends TestCase $response->assertStatus(200); $arr = $response->json(); - - $this->assertEquals('invoice', $arr[0]['entity_type']); + nlog($arr); + $this->assertEquals('invoice', $arr[0]['entity']); $this->assertEquals($this->invoice->hashed_id, $arr[0]['entity_id']); $count = SystemLog::where('company_id', $this->company->id) From 913a3e3ad973868baa3ab339f4a8dbb33307580c Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 12 Aug 2024 17:37:26 +1000 Subject: [PATCH 3/5] Add identifiers for storecove --- app/Services/EDocument/Standards/Peppol.php | 152 ++++++++++++++++-- .../Einvoice/Storecove/StorecoveTest.php | 40 ++++- 2 files changed, 178 insertions(+), 14 deletions(-) diff --git a/app/Services/EDocument/Standards/Peppol.php b/app/Services/EDocument/Standards/Peppol.php index d76802277b..89d50f2bd2 100644 --- a/app/Services/EDocument/Standards/Peppol.php +++ b/app/Services/EDocument/Standards/Peppol.php @@ -90,7 +90,7 @@ class Peppol extends AbstractService 'NO' => 'VAT', 'AD' => 'VAT', 'AL' => 'VAT', - 'AT' => 'VAT', //Pending Tests. + 'AT' => 'VAT', //Tested - Routing GOV + Business 'BA' => 'VAT', 'BE' => 'VAT', 'BG' => 'VAT', @@ -155,6 +155,82 @@ class Peppol extends AbstractService "896" => "Debit note related to self-billed invoice" ]; + // 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"], + "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"], + "LI" => ["B+G","","LI:VAT","LI:VAT"], + "NO" => ["B+G","NO:ORG","NO:VAT","NO:ORG"], + "AD" => ["B+G","","AD:VAT","AD:VAT"], + "AL" => ["B+G","","AL:VAT","AL:VAT"], + "AT" => [ + ["G","AT:GOV","","9915:b"], + ["B","","AT:VAT","AT:VAT"], + ], + "BA" => ["B+G","","BA:VAT","BA:VAT"], + "BE" => ["B+G","BE:EN","BE:VAT","BE:EN"], + "BG" => ["B+G","","BG:VAT","BG:VAT"], + "CY" => ["B+G","","CY:VAT","CY:VAT"], + "CZ" => ["B+G","","CZ:VAT","CZ:VAT"], + "DE" => [ + ["G","DE:LWID","","DE:LWID"], + ["B","","DE:VAT","DE:VAT"], + ], + "DK" => ["B+G","DK:DIGST","DK:ERST","DK:DIGST"], + "EE" => ["B+G","EE:CC","EE:VAT","EE:CC"], + "ES" => ["B","","ES:VAT","ES:VAT"], + "FI" => ["B+G","FI:OVT","FI:VAT","FI:OVT"], + "FR" => [ + ["G","FR:SIRET + customerAssignedAccountIdValue","","0009:11000201100044"], + ["B","FR:SIRENE or FR:SIRET","FR:VAT","FR:SIRENE or FR:SIRET"], + ], + "GR" => ["B+G","","GR:VAT","GR:VAT"], + "HR" => ["B+G","","HR:VAT","HR:VAT"], + "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"], + ], + "LT" => ["B+G","LT:LEC","LT:VAT","LT:LEC"], + "LU" => ["B+G","LU:MAT","LU:VAT","LU:VAT"], + "LV" => ["B+G","","LV:VAT","LV:VAT"], + "MC" => ["B+G","","MC:VAT","MC:VAT"], + "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" => ["B","NL:KVK","NL:VAT","NL:KVK or NL:VAT"], + "PL" => ["G+B","","PL:VAT","PL:VAT"], + "PT" => ["G+B","","PT:VAT","PT:VAT"], + "RO" => ["G+B","","RO:VAT","RO:VAT"], + "RS" => ["G+B","","RS:VAT","RS:VAT"], + "SE" => ["G+B","SE:ORGNR","SE:VAT","SE:ORGNR"], + "SI" => ["G+B","","SI:VAT","SI:VAT"], + "SK" => ["G+B","","SK:VAT","SK:VAT"], + "SM" => ["G+B","","SM:VAT","SM:VAT"], + "TR" => ["G+B","","TR:VAT","TR:VAT"], + "VA" => ["G+B","","VA:VAT","VA:VAT"], + "IN" => ["B","","IN:GSTIN","Email"], + "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"], + ], + "GB" => ["B","","GB:VAT","GB:VAT"], + "SA" => ["B","","SA:TIN","Email"], + "Other" => ["B","DUNS, GLN, LEI","","DUNS, GLN, LEI"], + ]; + private Company $company; private InvoiceSum | InvoiceSumInclusive $calc; @@ -270,7 +346,8 @@ class Peppol extends AbstractService // $this->p_invoice->TaxTotal = $this->getTotalTaxes(); it only wants the aggregate here!! $this->p_invoice->LegalMonetaryTotal = $this->getLegalMonetaryTotal(); - $this->countryLevelMutators(); + $this->senderSpecificLevelMutators() + ->receiverSpecificLevelMutators(); return $this; @@ -805,12 +882,15 @@ class Peppol extends AbstractService } /** - * countryLevelMutators + * senderSpecificLevelMutators * - * Runs country level specific requirements for the e-invoice + * Runs sender level specific requirements for the e-invoice, + * + * ie, mutations that are required by the senders country. + * * @return self */ - private function countryLevelMutators():self + private function senderSpecificLevelMutators():self { if(method_exists($this, $this->invoice->company->country()->iso_3166_2)) @@ -819,6 +899,24 @@ class Peppol extends AbstractService return $this; } + /** + * receiverSpecificLevelMutators + * + * Runs receiver level specific requirements for the e-invoice + * + * ie mutations that are required by the receiving country + * @return self + */ + private function receiverSpecificLevelMutators():self + { + + if(method_exists($this, "client_{$this->invoice->company->country()->iso_3166_2}")) + $this->{"client_{$this->invoice->company->country()->iso_3166_2}"}(); + + return $this; + } + + /** * setPaymentMeans * @@ -1029,7 +1127,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', "test")); + $this->setStorecoveMeta($this->buildRouting('AT:GOV', "b")); //for government clients this must be set. $this->setCustomerAssignedAccountId(true); @@ -1134,7 +1232,8 @@ class Peppol extends AbstractService $this->setStorecoveMeta($this->buildRouting('FR:SIRET', "0009:{$this->invoice->client->id_number}")); } - // ??????????????????????? //@TODO + // Apparently this is not a special field according to support + // sounds like it is optional // The service code must be sent in invoice.buyerReference (deprecated) or the invoice.references array (documentType buyer_reference) if(strlen($this->invoice->po_number ?? '') >1) { @@ -1146,22 +1245,55 @@ class Peppol extends AbstractService private function IT(): self { + // 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)); + + 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)); + + return $this; + } + // IT Sender, non-IT Receiver // Provide the receiver tax identifier and any routing identifier applicable to the receiving country (see Receiver Identifiers). + if($this->invoice->client->country->iso_3166_2 != 'IT' && $this->invoice->company->country()->iso_3166_2 == 'IT') { + + $code = $this->buildForeignRoutingCode(); + + $this->setStorecoveMeta($this->buildRouting($code, $this->invoice->client->vat_number)); + + return $this; + } + + return $this; + } + + private function client_IT(): self + { + + // non-IT Sender, IT Receiver, B2C + // Provide the receiver IT:CF and an optional email. The invoice will be eReported and sent via email. Note that this cannot be a PEC email address. + if(in_array($this->invoice->client->classification, ['individual']) && $this->invoice->company->country()->iso_3166_2 != 'IT') { + + return $this; + } + // non-IT Sender, IT Receiver, B2B/B2G // Provide the receiver IT:VAT and the receiver IT:CUUO (codice destinatario) - // non-IT Sender, IT Receiver, B2C - // Provide the receiver IT:CF and an optional email. The invoice will be eReported and sent via email. Note that this cannot be a PEC email address. - return $this; + } private function MY(): self diff --git a/tests/Integration/Einvoice/Storecove/StorecoveTest.php b/tests/Integration/Einvoice/Storecove/StorecoveTest.php index 8fadf573be..92acb6ad09 100644 --- a/tests/Integration/Einvoice/Storecove/StorecoveTest.php +++ b/tests/Integration/Einvoice/Storecove/StorecoveTest.php @@ -616,7 +616,7 @@ $x = ' } - private function createATData() + private function createATData(bool $is_gov = false) { $this->routing_id = 293801; @@ -668,7 +668,7 @@ $x = ' 'balance' => 0, 'paid_to_date' => 0, 'vat_number' => 'ATU87654321', - 'id_number' => 'FN 123456x', // Example format for Austrian company registration numbers + 'id_number' => $is_gov ? 'ATU12312321' : 'FN 123456x', // Example format for Austrian company registration numbers 'custom_value1' => '2024-07-22 10:00:00', 'custom_value2' => 'blau', 'custom_value3' => 'musterwort', @@ -688,7 +688,7 @@ $x = ' 'settings' => ClientSettings::Defaults(), 'client_hash' => \Illuminate\Support\Str::random(32), 'routing_id' => '', - 'classification' => 'business', + 'classification' => $is_gov ? 'government' : 'business', ]); @@ -726,7 +726,39 @@ $x = ' } - public function testAtRules() + public function testAtGovernmentRules() + { + $this->routing_id = 293801; + + $invoice = $this->createATData(true); + + $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); + + } + + public function PestAtRules() { $this->routing_id = 293801; From 9554de63b2095a48c68075e9b68b6f59393f2e77 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 12 Aug 2024 20:51:01 +1000 Subject: [PATCH 4/5] Harvest client routing codes --- app/Services/EDocument/Standards/Peppol.php | 25 ++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/app/Services/EDocument/Standards/Peppol.php b/app/Services/EDocument/Standards/Peppol.php index 89d50f2bd2..9b0dbfba70 100644 --- a/app/Services/EDocument/Standards/Peppol.php +++ b/app/Services/EDocument/Standards/Peppol.php @@ -824,6 +824,29 @@ class Peppol extends AbstractService ///////////////// Helper Methods ///////////////////////// + private function getClientRoutingCode(): string + { + $receiver_identifiers = $this->routing_rules[$this->invoice->client->country->iso_3166_2]; + $client_classification = $this->invoice->client->classification == 'government' ? 'G' : 'B'; + + if(count($receiver_identifiers) > 1) { + + foreach($receiver_identifiers as $ident) + { + if(str_contains($ident[0], $client_classification)) + { + return $ident[3]; + } + } + + } + elseif(count($receiver_identifiers) == 1) + return $receiver_identifiers[3]; + + throw new \Exception("e-invoice generation halted:: Could not resolve the Tax Code for this client? {$this->invoice->client->hashed_id}"); + + } + /** * setInvoiceDefaults * @@ -1268,7 +1291,7 @@ class Peppol extends AbstractService // Provide the receiver tax identifier and any routing identifier applicable to the receiving country (see Receiver Identifiers). if($this->invoice->client->country->iso_3166_2 != 'IT' && $this->invoice->company->country()->iso_3166_2 == 'IT') { - $code = $this->buildForeignRoutingCode(); + $code = $this->getClientRoutingCode(); $this->setStorecoveMeta($this->buildRouting($code, $this->invoice->client->vat_number)); From 244975702196095f52205837b9da3170a76a2086 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 13 Aug 2024 09:35:31 +1000 Subject: [PATCH 5/5] Update entity filters --- app/Filters/CreditFilters.php | 9 ++++++++- app/Filters/InvoiceFilters.php | 9 ++++++++- app/Filters/PurchaseOrderFilters.php | 9 ++++++++- app/Filters/QuoteFilters.php | 9 ++++++++- app/Filters/RecurringInvoiceFilters.php | 9 ++++++++- app/Models/Invoice.php | 2 +- app/Services/Invoice/AddGatewayFee.php | 7 ++++--- config/liap.php | 24 ++++++++++++++---------- 8 files changed, 59 insertions(+), 19 deletions(-) diff --git a/app/Filters/CreditFilters.php b/app/Filters/CreditFilters.php index 54ba0fb05c..578a83ddcf 100644 --- a/app/Filters/CreditFilters.php +++ b/app/Filters/CreditFilters.php @@ -98,7 +98,14 @@ class CreditFilters extends QueryFilters ->orWhere('last_name', 'like', '%'.$filter.'%') ->orWhere('email', 'like', '%'.$filter.'%'); }) - ->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')) LIKE ?", ['%'.$filter.'%']); + ->orWhereRaw(" + JSON_UNQUOTE(JSON_EXTRACT( + JSON_ARRAY( + JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')), + JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].product_key')) + ), '$[*]') + ) LIKE ?", ['%'.$filter.'%']); + // ->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')) LIKE ?", ['%'.$filter.'%']); }); } diff --git a/app/Filters/InvoiceFilters.php b/app/Filters/InvoiceFilters.php index de5a932fb7..4685590b1c 100644 --- a/app/Filters/InvoiceFilters.php +++ b/app/Filters/InvoiceFilters.php @@ -125,7 +125,14 @@ class InvoiceFilters extends QueryFilters ->orWhere('last_name', 'like', '%'.$filter.'%') ->orWhere('email', 'like', '%'.$filter.'%'); }) - ->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')) LIKE ?", ['%'.$filter.'%']); + ->orWhereRaw(" + JSON_UNQUOTE(JSON_EXTRACT( + JSON_ARRAY( + JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')), + JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].product_key')) + ), '$[*]') + ) LIKE ?", ['%'.$filter.'%']); + // ->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')) LIKE ?", ['%'.$filter.'%']); }); } diff --git a/app/Filters/PurchaseOrderFilters.php b/app/Filters/PurchaseOrderFilters.php index 700335242f..82fe3f8aa9 100644 --- a/app/Filters/PurchaseOrderFilters.php +++ b/app/Filters/PurchaseOrderFilters.php @@ -96,7 +96,14 @@ class PurchaseOrderFilters extends QueryFilters ->orWhere('custom_value4', 'like', '%'.$filter.'%') ->orWhereHas('vendor', function ($q) use ($filter) { $q->where('name', 'like', '%'.$filter.'%'); - }); + }) + ->orWhereRaw(" + JSON_UNQUOTE(JSON_EXTRACT( + JSON_ARRAY( + JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')), + JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].product_key')) + ), '$[*]') + ) LIKE ?", ['%'.$filter.'%']); }); } diff --git a/app/Filters/QuoteFilters.php b/app/Filters/QuoteFilters.php index 048669db92..d2c5763187 100644 --- a/app/Filters/QuoteFilters.php +++ b/app/Filters/QuoteFilters.php @@ -46,7 +46,14 @@ class QuoteFilters extends QueryFilters ->orWhere('last_name', 'like', '%'.$filter.'%') ->orWhere('email', 'like', '%'.$filter.'%'); }) - ->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')) LIKE ?", ['%'.$filter.'%']); + ->orWhereRaw(" + JSON_UNQUOTE(JSON_EXTRACT( + JSON_ARRAY( + JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')), + JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].product_key')) + ), '$[*]') + ) LIKE ?", ['%'.$filter.'%']); + // ->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')) LIKE ?", ['%'.$filter.'%']); }); } diff --git a/app/Filters/RecurringInvoiceFilters.php b/app/Filters/RecurringInvoiceFilters.php index 983582cb9a..976739912b 100644 --- a/app/Filters/RecurringInvoiceFilters.php +++ b/app/Filters/RecurringInvoiceFilters.php @@ -49,7 +49,14 @@ class RecurringInvoiceFilters extends QueryFilters ->orWhere('last_name', 'like', '%'.$filter.'%') ->orWhere('email', 'like', '%'.$filter.'%'); }) - ->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')) LIKE ?", ['%'.$filter.'%']); + ->orWhereRaw(" + JSON_UNQUOTE(JSON_EXTRACT( + JSON_ARRAY( + JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')), + JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].product_key')) + ), '$[*]') + ) LIKE ?", ['%'.$filter.'%']); + //->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')) LIKE ?", ['%'.$filter.'%']); }); } diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index fe2b1222f3..4b98faa0c6 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -431,7 +431,7 @@ class Invoice extends BaseModel public function isPayable(): bool { - if($this->is_deleted) + if($this->is_deleted || $this->status_id == self::STATUS_PAID) return false; elseif ($this->status_id == self::STATUS_DRAFT && $this->is_deleted == false) { return true; diff --git a/app/Services/Invoice/AddGatewayFee.php b/app/Services/Invoice/AddGatewayFee.php index b95dd6841e..9601d671f6 100644 --- a/app/Services/Invoice/AddGatewayFee.php +++ b/app/Services/Invoice/AddGatewayFee.php @@ -26,15 +26,16 @@ class AddGatewayFee extends AbstractService public function run() { + + // Removes existing stale gateway fees + $this->cleanPendingGatewayFees(); + $gateway_fee = round($this->company_gateway->calcGatewayFee($this->amount, $this->gateway_type_id, $this->invoice->uses_inclusive_taxes), $this->invoice->client->currency()->precision); if (! $gateway_fee || $gateway_fee == 0) { return $this->invoice; } - // Removes existing stale gateway fees - $this->cleanPendingGatewayFees(); - // If a gateway fee is > 0 insert the line item if ($gateway_fee > 0) { return $this->processGatewayFee($gateway_fee); diff --git a/config/liap.php b/config/liap.php index 4483546d3f..a27e843342 100644 --- a/config/liap.php +++ b/config/liap.php @@ -1,19 +1,22 @@ class_exists(\Modules\Admin\Listeners\Subscription\AppleSubscribed::class) ? [\Modules\Admin\Listeners\Subscription\AppleSubscribed::class] : [], DidRenew::class => class_exists(\Modules\Admin\Listeners\Subscription\AppleAutoRenew::class) ? [\Modules\Admin\Listeners\Subscription\AppleAutoRenew::class] : [], SubscriptionRenewed::class => class_exists(\Modules\Admin\Listeners\Subscription\GoogleAutoRenew::class) ? [\Modules\Admin\Listeners\Subscription\GoogleAutoRenew::class] : [], - DidChangeRenewalStatus::class => class_exists(\Modules\Admin\Listeners\Subscription\GoogleChangeRenewalStaus::class) ? [\Modules\Admin\Listeners\Subscription\GoogleChangeRenewalStaus::class] : [], + // DidChangeRenewalStatus::class => class_exists(\Modules\Admin\Listeners\Subscription\GoogleChangeRenewalStaus::class) ? [\Modules\Admin\Listeners\Subscription\GoogleChangeRenewalStaus::class] : [], DidFailToRenew::class => class_exists(\Modules\Admin\Listeners\Subscription\GoogleFailedToRenew::class) ? [\Modules\Admin\Listeners\Subscription\GoogleFailedToRenew::class] : [], Refund::class => class_exists(\Modules\Admin\Listeners\Subscription\AppleRefund::class) ? [\Modules\Admin\Listeners\Subscription\AppleRefund::class] : [], SubscriptionRecovered::class => class_exists(\Modules\Admin\Listeners\Subscription\GoogleSubscriptionRecovered::class) ? [\Modules\Admin\Listeners\Subscription\GoogleSubscriptionRecovered::class] : [],