From 0905bd2f58c3e858f7fa650153c802dbdaad0e91 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 28 Aug 2024 10:48:08 +1000 Subject: [PATCH] Storecove router --- app/Livewire/EInvoice/Portal.php | 53 +++++- .../EDocument/Gateway/Storecove/Storecove.php | 36 ++-- .../Gateway/Storecove/StorecoveRouter.php | 163 ++++++++++++++++++ app/Services/EDocument/Standards/Peppol.php | 71 ++++---- .../views/livewire/e-invoice/portal.blade.php | 36 ++-- .../Einvoice/Storecove/StorecoveTest.php | 2 +- 6 files changed, 301 insertions(+), 60 deletions(-) create mode 100644 app/Services/EDocument/Gateway/Storecove/StorecoveRouter.php diff --git a/app/Livewire/EInvoice/Portal.php b/app/Livewire/EInvoice/Portal.php index 1afa341c4938..aa3f4be43564 100644 --- a/app/Livewire/EInvoice/Portal.php +++ b/app/Livewire/EInvoice/Portal.php @@ -12,6 +12,7 @@ namespace App\Livewire\EInvoice; use Livewire\Component; +use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Auth; class Portal extends Component @@ -21,6 +22,8 @@ class Portal extends Component public array $companies; + private string $api_url = 'https://invoicing.co' + public function mount() { @@ -50,9 +53,24 @@ class Portal extends Component App::setLocale(auth()->guard('user')->user()->account->companies->first()->getLocale()); - $this->companies = auth()->user()->account->companies->map(function ($c){ - return ['name' => $c->settings->name, 'company_key' => $c->company_key, 'legal_entity_id' => $c->legal_entity_id]; - })->toArray(); + $this->companies = auth()->guard('user')->check() ? auth()->guard('user')->user()->account->companies->map(function ($company) { + return [ + 'key' => $company->company_key, + 'city' => $company->settings->city, + 'country' => $company->country()->iso_3166_2, + 'county' => $company->settings->state, + 'line1' => $company->settings->address1, + 'line2' => $company->settings->address2, + 'party_name' => $company->settings->name, + 'vat_number' => $company->settings->vat_number, + 'zip' => $company->settings->postal_code, + 'legal_entity_id' => $company->legal_entity_id, + 'tax_registered' => (bool) strlen($company->settings->vat_number ?? '') > 2, + 'tenant_id' => $company->company_key, + 'classification' => strlen($company->settings->classification ?? '') > 2 ? $company->settings->classification : 'business', + ]; + })->toArray() : []; + } else { session()->flash('error', 'Invalid credentials.'); @@ -69,8 +87,33 @@ class Portal extends Component public function register(string $company_key) { - nlog("Please register {$company_key}"); - sleep(5); + + $register_company = [ + 'acts_as_receiver' => true, + 'acts_as_sender' => true, + 'advertisements' => ['invoice'] + ]; + + foreach($this->companies as $company) + { + if($company['key'] == $company_key) + $register_company = array_merge($company, $register_company); + } + + $r = Http::withHeaders($this->getHeaders()) + ->post("{$api_url}/api/einvoice/createLegalEntity", $register_company); + + if($r->successful()) + { + $response = $r->json(); + } + } + + private function getHeaders() + { + return [ + 'X-API-SELF-HOST-TOKEN' => config('ninja.license_key'), + ]; } public function render() diff --git a/app/Services/EDocument/Gateway/Storecove/Storecove.php b/app/Services/EDocument/Gateway/Storecove/Storecove.php index a9cf02ab4c9b..ef7bf4de2279 100644 --- a/app/Services/EDocument/Gateway/Storecove/Storecove.php +++ b/app/Services/EDocument/Gateway/Storecove/Storecove.php @@ -49,8 +49,11 @@ class Storecove "identifier" => "1200109963131" ]; + public StorecoveRouter $router; + public function __construct() { + $this->router = new StorecoveRouter(); } /** @@ -190,23 +193,30 @@ class Storecove * * @return mixed */ - public function createLegalEntity(array $data, Company $company) + public function createLegalEntity(array $data, ?Company $company = null) { $uri = 'legal_entities'; + if($company){ + + $data = array_merge([ + 'city' => $company->settings->city, + 'country' => $company->country()->iso_3166_2, + 'county' => $company->settings->state, + 'line1' => $company->settings->address1, + 'line2' => $company->settings->address2, + 'party_name' => $company->settings->name, + 'tax_registered' => (bool)strlen($company->settings->vat_number ?? '') > 2, + 'tenant_id' => $company->company_key, + 'zip' => $company->settings->postal_code, + ], $data); + + } + $company_defaults = [ 'acts_as_receiver' => true, 'acts_as_sender' => true, 'advertisements' => ['invoice'], - 'city' => $company->settings->city, - 'country' => $company->country()->iso_3166_2, - 'county' => $company->settings->state, - 'line1' => $company->settings->address1, - 'line2' => $company->settings->address2, - 'party_name' => $company->settings->name, - 'tax_registered' => true, - 'tenant_id' => $company->company_key, - 'zip' => $company->settings->postal_code, ]; $payload = array_merge($company_defaults, $data); @@ -331,15 +341,15 @@ class Storecove catch (ClientException $e) { // 4xx errors nlog("Client error: " . $e->getMessage()); - nlog("\nResponse body: " . $e->getResponse()->getBody()->getContents()); + nlog("Response body: " . $e->getResponse()->getBody()->getContents()); } catch (ServerException $e) { // 5xx errors nlog("Server error: " . $e->getMessage()); - nlog("\nResponse body: " . $e->getResponse()->getBody()->getContents()); + nlog("Response body: " . $e->getResponse()->getBody()->getContents()); } catch (RequestException $e) { nlog("Request error: " . $e->getMessage()); if ($e->hasResponse()) { - nlog("\nResponse body: " . $e->getResponse()->getBody()->getContents()); + nlog("Response body: " . $e->getResponse()->getBody()->getContents()); } } diff --git a/app/Services/EDocument/Gateway/Storecove/StorecoveRouter.php b/app/Services/EDocument/Gateway/Storecove/StorecoveRouter.php new file mode 100644 index 000000000000..1ef8427c1d4b --- /dev/null +++ b/app/Services/EDocument/Gateway/Storecove/StorecoveRouter.php @@ -0,0 +1,163 @@ + [ + ["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"], + "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",false,"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",false,"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",false,"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","","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"], + "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",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"], + "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",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",false,"DUNS, GLN, LEI"], + ]; + + public function __construct() + { + } + + /** + * Return the routing code based on country and entity classification + * + * @param string $country + * @param string $classification + * @return string + */ + public function resolveRouting(string $country, string $classification): string + { + $rules = $this->routing_rules[$country]; + + if(is_array($rules) && !is_array($rules[0])) { + return $rules[3]; + } + + match($classification) { + "business" => $code = "B", + "government" => $code = "G", + "individual" => $code = "C", + default => $code = "B", + }; + + foreach($rules as $rule) { + if(stripos($rule[0], $code) !== false) { + return $rule[3]; + } + } + + return $rules[0][3]; + } + + /** + * resolveTaxScheme + * + * @param string $country + * @param string $classification + * @return string + */ + public function resolveTaxScheme(string $country, string $classification): string + { + + $rules = isset($this->routing_rules[$country]) ? $this->routing_rules[$country] : [false, false, false, false]; + + $code = "B"; + + match($classification) { + "business" => $code = "B", + "government" => $code = "G", + "individual" => $code = "C", + default => $code = "B", + }; + + //single array + if(is_array($rules) && !is_array($rules[0])) { + return $rules[2]; + } + + foreach($rules as $rule) { + if(stripos($rule[0], $code) !== false) { + return $rule[2]; + } + } + + return $rules[0][2]; + } +} \ No newline at end of file diff --git a/app/Services/EDocument/Standards/Peppol.php b/app/Services/EDocument/Standards/Peppol.php index cced78cc915b..64bfba964585 100644 --- a/app/Services/EDocument/Standards/Peppol.php +++ b/app/Services/EDocument/Standards/Peppol.php @@ -775,32 +775,39 @@ class Peppol extends AbstractService return $asp; } - - private function resolveTaxScheme(): mixed + + /** + * resolveTaxScheme + * + * @return string + */ + private function resolveTaxScheme(): string { - $rules = isset($this->routing_rules[$this->invoice->client->country->iso_3166_2]) ? $this->routing_rules[$this->invoice->client->country->iso_3166_2] : [false, false, false, false,]; + return (new StorecoveRouter())->resolveTaxScheme($this->invoice->client->country->iso_3166_2, $this->invoice->client->classification) - $code = false; + // $rules = isset($this->routing_rules[$this->invoice->client->country->iso_3166_2]) ? $this->routing_rules[$this->invoice->client->country->iso_3166_2] : [false, false, false, false,]; - match($this->invoice->client->classification) { - "business" => $code = "B", - "government" => $code = "G", - "individual" => $code = "C", - default => $code = false, - }; + // $code = false; - //single array - if(is_array($rules) && !is_array($rules[0])) { - return $rules[2]; - } + // match($this->invoice->client->classification) { + // "business" => $code = "B", + // "government" => $code = "G", + // "individual" => $code = "C", + // default => $code = false, + // }; - foreach($rules as $rule) { - if(stripos($rule[0], $code) !== false) { - return $rule[2]; - } - } + // //single array + // if(is_array($rules) && !is_array($rules[0])) { + // return $rules[2]; + // } - return false; + // foreach($rules as $rule) { + // if(stripos($rule[0], $code) !== false) { + // return $rule[2]; + // } + // } + + // return false; } private function getAccountingCustomerParty(): AccountingCustomerParty @@ -912,20 +919,22 @@ class Peppol extends AbstractService 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'; + // $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) { + // if(count($receiver_identifiers) > 1) { - foreach($receiver_identifiers as $ident) { - if(str_contains($ident[0], $client_classification)) { - return $ident[3]; - } - } + // foreach($receiver_identifiers as $ident) { + // if(str_contains($ident[0], $client_classification)) { + // return $ident[3]; + // } + // } - } elseif(count($receiver_identifiers) == 1) { - return $receiver_identifiers[3]; - } + // } elseif(count($receiver_identifiers) == 1) { + // return $receiver_identifiers[3]; + // } + + return (new StorecoveRouter())->resolveRouting($this->invoice->client->country->iso_3166_2, $this->invoice->client->classification) throw new \Exception("e-invoice generation halted:: Could not resolve the Tax Code for this client? {$this->invoice->client->hashed_id}"); diff --git a/resources/views/livewire/e-invoice/portal.blade.php b/resources/views/livewire/e-invoice/portal.blade.php index 95478a83a9d9..58d0af7764ce 100644 --- a/resources/views/livewire/e-invoice/portal.blade.php +++ b/resources/views/livewire/e-invoice/portal.blade.php @@ -1,18 +1,33 @@ -
+
@if (Auth::guard('user')->check()) -
-
+
+ +
+

E-Invoice Beta Phase

+

Hey there!

+

Thanks for joining us on our pilot program for e-invoicing for self hosted users. Our aim is to allow you to send your einvoices through the PEPPOL network via Invoice Ninja.

+

Our hosted servers will proxy your einvoices into the PEPPOL network for you, and also route einvoices back to you via Webhooks.

+

Configuration:

+

To start sending einvoices via the PEPPOL network, you are required to create a Legal Entity ID, this will be your network address in the PEPPOL network. The tabled data below is what will be used to register your legal entity, please confirm the details are correct prior to registering.

+

If you are in a region which requires routing directly to the government, such as Spain, Italy or Romania, you are required to have already registered with your government for the sending of einvoices.

+

In your .env file, add the variable LICENSE_KEY= with your self hosted white label license key - this is used for authentication with our servers, and to register the sending entity. You will also want to contact us to ensure we have configured your license for this beta test! +

For discussion, help and troubleshooting, please use the slack channel #einvoicing.

+
+ +

Welcome, {{ Auth::guard('user')->user()->first_name }}!

-
- -
+
+ +
+
-
+ +
Name
Legal Entity Id
@@ -25,7 +40,7 @@
{{ ctrans('texts.name') }}: - {{ $company['name'] }} + {{ $company['party_name'] }}
@@ -79,6 +94,7 @@
@endforeach +
diff --git a/tests/Integration/Einvoice/Storecove/StorecoveTest.php b/tests/Integration/Einvoice/Storecove/StorecoveTest.php index fb2979004255..2983da18de29 100644 --- a/tests/Integration/Einvoice/Storecove/StorecoveTest.php +++ b/tests/Integration/Einvoice/Storecove/StorecoveTest.php @@ -65,7 +65,7 @@ class StorecoveTest extends TestCase // ]; // $sc = new \App\Services\EDocument\Gateway\Storecove\Storecove(); - // $r = $sc->createLegalEntity($data, $this->company); + // $r = $sc->createLegalEntity($data); // $this->assertIsArray($r);