diff --git a/app/Services/EDocument/Standards/Peppol.php b/app/Services/EDocument/Standards/Peppol.php index 6afe2024807a..b797e58eaef0 100644 --- a/app/Services/EDocument/Standards/Peppol.php +++ b/app/Services/EDocument/Standards/Peppol.php @@ -19,6 +19,7 @@ use App\Helpers\Invoice\InvoiceSum; use InvoiceNinja\EInvoice\EInvoice; use App\Utils\Traits\NumberFormatter; use App\Helpers\Invoice\InvoiceSumInclusive; +use App\Services\EDocument\Standards\Peppol\RO; use InvoiceNinja\EInvoice\Models\Peppol\PaymentMeans; use InvoiceNinja\EInvoice\Models\Peppol\ItemType\Item; use InvoiceNinja\EInvoice\Models\Peppol\PartyType\Party; @@ -52,6 +53,7 @@ use InvoiceNinja\EInvoice\Models\Peppol\CustomerPartyType\AccountingCustomerPart use InvoiceNinja\EInvoice\Models\Peppol\SupplierPartyType\AccountingSupplierParty; use InvoiceNinja\EInvoice\Models\Peppol\FinancialAccountType\PayeeFinancialAccount; use InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\CustomerAssignedAccountID; +use InvoiceNinja\EInvoice\Models\Peppol\LocationType\PhysicalLocation; class Peppol extends AbstractService { @@ -112,8 +114,8 @@ class Peppol extends AbstractService 'HR' => 'VAT', 'HU' => 'VAT', 'IE' => 'VAT', - '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.' + 'IT' => 'IVA', //tested - Requires a Customer Party Identification (VAT number) - 'IT senders must first be provisioned in the partner system.' Cannot test currently + 'IT' => 'CF', //tested - Requires a Customer Party Identification (VAT number) - 'IT senders must first be provisioned in the partner system.' Cannot test currently 'LT' => 'VAT', 'LU' => 'VAT', 'LV' => 'VAT', @@ -732,7 +734,9 @@ class Peppol extends AbstractService 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; + $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,]; + + $code = false; match($this->invoice->client->classification){ "business" => $code = "B", @@ -741,18 +745,18 @@ class Peppol extends AbstractService default => $code = false, }; - if(count($rules) > 1){ + //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]; - } + foreach($rules as $rule) + { + if(stripos($rule[0], $code) !== false) { + return $rule[2]; } } return false; - } private function getAccountingCustomerParty(): AccountingCustomerParty @@ -796,7 +800,11 @@ class Peppol extends AbstractService $address->Country = $country; $party->PostalAddress = $address; - $party->PhysicalLocation = $address; + + $physical_location = new PhysicalLocation(); + $physical_location->Address = $address; + + $party->PhysicalLocation = $physical_location;; $contact = new Contact(); $contact->ElectronicMail = $this->invoice->client->present()->email(); @@ -935,7 +943,16 @@ class Peppol extends AbstractService return null; } + + private function getClientSetting(string $property_path): mixed + { + return PropertyResolver::resolve($this->_client_settings, $property_path); + } + private function getCompanySetting(string $property_path): mixed + { + return PropertyResolver::resolve($this->_company_settings, $property_path); + } /** * senderSpecificLevelMutators * @@ -1105,12 +1122,20 @@ class Peppol extends AbstractService private function setEmailRouting(string $email): self { + nlog($email); + $meta = $this->getStorecoveMeta(); - $emails = isset($meta['routing']['emails']) ? $meta['routing']['emails'] : ($meta['routing']['emails'] = []); - array_push($emails, $email); + if(isset($meta['routing']['emails'])){ + $emails = $meta['routing']['emails']; + array_push($emails, $email); + $meta['routing']['emails'] = $emails; + } + else { + $meta['routing']['emails'] = [$email]; + } - $this->setStorecoveMeta($emails); + $this->setStorecoveMeta($meta); return $this; } @@ -1125,8 +1150,9 @@ class Peppol extends AbstractService */ private function setStorecoveMeta(array $meta): self { - $this->storecove_meta = array_merge($this->storecove_meta, $meta); + $this->storecove_meta = array_merge($this->storecove_meta, $meta); + return $this; } @@ -1324,24 +1350,26 @@ class Peppol extends AbstractService // IT Sender, IT Receiver, B2B/B2G // Provide the receiver IT:VAT and the receiver IT:CUUO (codice destinatario) 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') { - 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] + // ["scheme" => 'IT:CUUO', "id" => $this->invoice->client->routing_id] ])); + $this->setEmailRouting($this->invoice->client->present()->email()); + return $this; } @@ -1429,27 +1457,34 @@ class Peppol extends AbstractService private function RO(): self { - // Because using this network is not yet mandatory, the default workflow is to not use this network. Therefore, you have to force its use, as follows: + // Because using this network is not yet mandatory, the default workflow is to not use this network. Therefore, you have to force its use, as follows: + $meta = ["networks" => [ + [ + "application" => "ro-anaf", + "settings"=> [ + "enabled" => true + ], + ], + ]]; + + $this->setStorecoveMeta($meta); + + $this->setStorecoveMeta($this->buildRouting([ + ["scheme" => 'RO:VAT', "id" => $this->invoice->client->vat_number], + ])); - // "routing": { - // "eIdentifiers": [ - // { - // "scheme": "RO:VAT", - // "id": "RO010101010" - // } - // ], - // "networks": [ - // { - // "application": "ro-anaf", - // "settings": { - // "enabled": true - // } - // } - // ] - // } - // Note this will only work if your LegalEntity has been setup for this network. - // The county field for a Romania address must use the ISO3166-2:RO codes, e.g. "RO-AB, RO-AR". Don’t omit the country prefix! - // The city field for county RO-B must be SECTOR1 - SECTOR6. + $ro = new RO($this->invoice); + + $client_state = $this->getClientSetting('Invoice.AccountingSupplierParty.Party.PostalAddress.Address.CountrySubentity'); + $client_city = $this->getClientSetting('Invoice.AccountingCustomerParty.Party.PostalAddress.Address.CityName'); + + $resolved_state = $ro->getStateCode($client_state); + $resolved_city = $ro->getSectorCode($client_city); + + $this->p_invoice->AccountingCustomerParty->Party->PostalAddress->CountrySubentity = $resolved_state; + $this->p_invoice->AccountingCustomerParty->Party->PostalAddress->CityName = $resolved_city; + $this->p_invoice->AccountingCustomerParty->Party->PhysicalLocation->Address->CountrySubentity = $resolved_state; + $this->p_invoice->AccountingCustomerParty->Party->PhysicalLocation->Address->CityName = $resolved_city; return $this; } diff --git a/app/Services/EDocument/Standards/Peppol/RO.php b/app/Services/EDocument/Standards/Peppol/RO.php new file mode 100644 index 000000000000..936fc413e0ad --- /dev/null +++ b/app/Services/EDocument/Standards/Peppol/RO.php @@ -0,0 +1,149 @@ + 'Alba', + 'RO-AG' => 'Argeș', + 'RO-AR' => 'Arad', + 'RO-B' => 'Bucharest', + 'RO-BC' => 'Bacău', + 'RO-BH' => 'Bihor', + 'RO-BN' => 'Bistrița-Năsăud', + 'RO-BR' => 'Brăila', + 'RO-BT' => 'Botoșani', + 'RO-BV' => 'Brașov', + 'RO-BZ' => 'Buzău', + 'RO-CJ' => 'Cluj', + 'RO-CL' => 'Călărași', + 'RO-CS' => 'Caraș-Severin', + 'RO-CT' => 'Constanța', + 'RO-CV' => 'Covasna', + 'RO-DB' => 'Dâmbovița', + 'RO-DJ' => 'Dolj', + 'RO-GJ' => 'Gorj', + 'RO-GL' => 'Galați', + 'RO-GR' => 'Giurgiu', + 'RO-HD' => 'Hunedoara', + 'RO-HR' => 'Harghita', + 'RO-IF' => 'Ilfov', + 'RO-IL' => 'Ialomița', + 'RO-IS' => 'Iași', + 'RO-MH' => 'Mehedinți', + 'RO-MM' => 'Maramureș', + 'RO-MS' => 'Mureș', + 'RO-NT' => 'Neamț', + 'RO-OT' => 'Olt', + 'RO-PH' => 'Prahova', + 'RO-SB' => 'Sibiu', + 'RO-SJ' => 'Sălaj', + 'RO-SM' => 'Satu Mare', + 'RO-SV' => 'Suceava', + 'RO-TL' => 'Tulcea', + 'RO-TM' => 'Timiș', + 'RO-TR' => 'Teleorman', + 'RO-VL' => 'Vâlcea', + 'RO-VN' => 'Vaslui', + 'RO-VS' => 'Vrancea', + ]; + + protected array $sectorList = [ + 'SECTOR1' => 'Agriculture', + 'SECTOR2' => 'Manufacturing', + 'SECTOR3' => 'Tourism', + 'SECTOR4' => 'Information Technology (IT):', + 'SECTOR5' => 'Energy', + 'SECTOR6' => 'Healthcare', + 'SECTOR7' => 'Education', + ]; + + protected array $sectorCodes = [ + 'RO-AB' => 'Manufacturing, Agriculture', + 'RO-AG' => 'Manufacturing, Agriculture', + 'RO-AR' => 'Manufacturing, Agriculture', + 'RO-B' => 'Information Technology (IT), Education, Tourism', + 'RO-BC' => 'Manufacturing, Agriculture', + 'RO-BH' => 'Agriculture, Manufacturing', + 'RO-BN' => 'Agriculture', + 'RO-BR' => 'Agriculture', + 'RO-BT' => 'Agriculture', + 'RO-BV' => 'Tourism, Agriculture', + 'RO-BZ' => 'Agriculture', + 'RO-CJ' => 'Information Technology (IT), Education, Tourism', + 'RO-CL' => 'Agriculture', + 'RO-CS' => 'Manufacturing, Agriculture', + 'RO-CT' => 'Tourism, Agriculture', + 'RO-CV' => 'Agriculture', + 'RO-DB' => 'Agriculture', + 'RO-DJ' => 'Agriculture', + 'RO-GJ' => 'Manufacturing, Agriculture', + 'RO-GL' => 'Energy, Manufacturing', + 'RO-GR' => 'Agriculture', + 'RO-HD' => 'Energy, Manufacturing', + 'RO-HR' => 'Agriculture', + 'RO-IF' => 'Information Technology (IT), Education', + 'RO-IL' => 'Agriculture', + 'RO-IS' => 'Information Technology (IT), Education, Agriculture', + 'RO-MH' => 'Manufacturing, Agriculture', + 'RO-MM' => 'Agriculture', + 'RO-MS' => 'Energy, Manufacturing, Agriculture', + 'RO-NT' => 'Agriculture', + 'RO-OT' => 'Agriculture', + 'RO-PH' => 'Energy, Manufacturing', + 'RO-SB' => 'Manufacturing, Agriculture', + 'RO-SJ' => 'Agriculture', + 'RO-SM' => 'Agriculture', + 'RO-SV' => 'Agriculture', + 'RO-TL' => 'Agriculture', + 'RO-TM' => 'Agriculture, Manufacturing', + 'RO-TR' => 'Agriculture', + 'RO-VL' => 'Agriculture', + 'RO-VN' => 'Agriculture', + 'RO-VS' => 'Agriculture', + ]; + + + public function __construct(protected Invoice $invoice){} + + public function getStateCode(?string $state_code): string + { + $state_code = strlen($state_code ?? '') > 1 ? $state_code : $this->invoice->client->state; + + //codes are configured by default + if(isset($this->countrySubEntity[$state_code])) + return $state_code; + + $key = array_search($state_code, $this->countrySubEntity); + + if ($key !== false) { + return $key; + } + + return 'RO-B'; + } + + public function getSectorCode(?string $client_city): string + { + $client_sector_code = $client_city ?? $this->invoice->client->city; + + if(in_array($this->getStateCode($this->invoice->client->state), ['BUCHAREST', 'RO-B'])) + return in_array(strtoupper($this->invoice->client->city), array_keys($this->sectorList)) ? strtoupper($this->invoice->client->city) : 'SECTOR1'; + + return $client_sector_code; + } + +} \ No newline at end of file diff --git a/tests/Integration/Einvoice/Storecove/StorecoveTest.php b/tests/Integration/Einvoice/Storecove/StorecoveTest.php index 36bcf1bb4fde..32c373d70647 100644 --- a/tests/Integration/Einvoice/Storecove/StorecoveTest.php +++ b/tests/Integration/Einvoice/Storecove/StorecoveTest.php @@ -891,6 +891,158 @@ $x = ' } + private function createROData() + { + $this->routing_id =294639; + + $settings = CompanySettings::defaults(); + $settings->company_logo = 'https://pdf.invoicing.co/favicon-v2.png'; + $settings->website = 'www.invoiceninja.ro'; + $settings->address1 = 'Strada Exemplu, 28'; + $settings->address2 = 'Clădirea Exemplu'; + $settings->city = 'Bucharest'; + $settings->state = 'Bucharest'; + $settings->postal_code = '010101'; + $settings->phone = '021 1234567'; + $settings->email = $this->faker->unique()->safeEmail(); + $settings->country_id = '642'; // Romania's ISO country code + $settings->vat_number = 'RO92443356490'; // Romanian VAT number format + $settings->id_number = 'B12345678'; // Typical Romanian 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.R.L.', + 'website' => 'https://www.impresa-esempio.ro', + 'private_notes' => 'Acestea sunt note private pentru clientul de test.', + 'balance' => 0, + 'paid_to_date' => 0, + 'vat_number' => 'RO9244336489', // Romanian VAT number with RO prefix + 'id_number' => 'J40/12345/2024', // Typical format for Romanian company registration numbers + 'custom_value1' => '2024-07-22 10:00:00', + 'custom_value2' => 'albastru', // Romanian for blue + 'custom_value3' => 'cuvantexemplu', // Romanian for sample word + 'custom_value4' => 'test@exemplu.ro', + 'address1' => 'Strada Exemplu 123', + 'address2' => 'Etaj 2, Birou 45', + 'city' => 'Bucharest', + 'state' => 'Bucharest', + 'postal_code' => '010101', + 'country_id' => '642', // Romania + 'shipping_address1' => 'Strada Exemplu 123', + 'shipping_address2' => 'Etaj 2, Birou 45', + 'shipping_city' => 'Bucharest', + 'shipping_state' => 'Bucharest', + 'shipping_postal_code' => '010101', + 'shipping_country_id' => '642', // Romania + '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', + ]); + + + $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 = 'TVA'; + + $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' => '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; + + } + + public function testRoRules() + { + $invoice = $this->createROData(); + + $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 PestAtGovernmentRules() { $this->routing_id = 293801; @@ -923,7 +1075,7 @@ $x = ' } - public function testItRules() + public function PtestItRules() { $invoice = $this->createITData(); @@ -956,28 +1108,28 @@ $x = ' nlog("Individual"); -$invoice = $this->createITData(false); + $invoice = $this->createITData(false); -$e_invoice = new \InvoiceNinja\EInvoice\Models\Peppol\Invoice(); + $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; -} + $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(); + $invoice->e_invoice = $e_invoice; + $invoice->save(); -$p = new Peppol($invoice); + $p = new Peppol($invoice); -$p->run(); -$xml = $p->toXml(); -nlog($xml); + $p->run(); + $xml = $p->toXml(); + nlog($xml); -$identifiers = $p->getStorecoveMeta(); + $identifiers = $p->getStorecoveMeta(); -$sc = new \App\Services\EDocument\Gateway\Storecove\Storecove(); -$sc->sendDocument($xml, $this->routing_id, $identifiers); + $sc = new \App\Services\EDocument\Gateway\Storecove\Storecove(); + $sc->sendDocument($xml, $this->routing_id, $identifiers); }