'EIN', 'US' => 'SSN', 'NZ' => 'GST', 'CH' => 'VAT', // VAT number = CHE - 999999999 - MWST|IVA|VAT 'IS' => 'VAT', 'LI' => 'VAT', 'NO' => 'VAT', 'AD' => 'VAT', 'AL' => 'VAT', 'AT' => 'VAT', //Tested - Routing GOV + Business 'BA' => 'VAT', 'BE' => 'VAT', 'BG' => 'VAT', 'AU' => 'ABN', //Australia 'CA' => 'CBN', //Canada 'MX' => 'RFC', //Mexico 'NZ' => 'GST', //Nuuu zulund 'GB' => 'VAT', //Great Britain 'SA' => 'TIN', //South Africa 'CY' => 'VAT', 'CZ' => 'VAT', 'DE' => 'VAT', //tested - Requires Payment Means to be defined. 'DK' => 'ERST', 'EE' => 'VAT', 'ES' => 'VAT', //tested - B2G pending 'FI' => 'VAT', 'FR' => 'VAT', //tested - Need to ensure Siren/Siret routing 'GR' => 'VAT', '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.' 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', 'MC' => 'VAT', 'ME' => 'VAT', 'MK' => 'VAT', 'MT' => 'VAT', 'NL' => 'VAT', 'PL' => 'VAT', 'PT' => 'VAT', 'RO' => 'VAT', 'RS' => 'VAT', 'SE' => 'VAT', 'SI' => 'VAT', 'SK' => 'VAT', 'SM' => 'VAT', 'TR' => 'VAT', 'VA' => 'VAT', 'IN' => 'GSTIN', 'JP' => 'IIN', 'MY' => 'TIN', 'SG' => 'GST', 'GB' => 'VAT', 'SA' => 'TIN', ]; private array $InvoiceTypeCodes = [ "380" => "Commercial invoice", "381" => "Credit note", "383" => "Corrected invoice", "384" => "Prepayment invoice", "386" => "Proforma invoice", "875" => "Self-billed invoice", "976" => "Factored invoice", "84" => "Invoice for cross border services", "82" => "Simplified invoice", "80" => "Debit note", "875" => "Self-billed credit note", "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","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"], ]; private Company $company; private InvoiceSum | InvoiceSumInclusive $calc; private \InvoiceNinja\EInvoice\Models\Peppol\Invoice $p_invoice; private ?\InvoiceNinja\EInvoice\Models\Peppol\Invoice $_client_settings; private ?\InvoiceNinja\EInvoice\Models\Peppol\Invoice $_company_settings; private EInvoice $e; private array $storecove_meta = []; /** * @param Invoice $invoice */ public function __construct(public Invoice $invoice) { $this->company = $invoice->company; $this->calc = $this->invoice->calc(); $this->e = new EInvoice(); $this->setSettings()->setInvoice(); } /** * Entry point for building document * * @return self */ public function run(): self { $this->p_invoice->ID = $this->invoice->number; $this->p_invoice->IssueDate = new \DateTime($this->invoice->date); if($this->invoice->due_date) { $this->p_invoice->DueDate = new \DateTime($this->invoice->due_date); } $this->p_invoice->InvoiceTypeCode = ($this->invoice->amount >= 0) ? 380 : 381; // $this->p_invoice->AccountingSupplierParty = $this->getAccountingSupplierParty(); $this->p_invoice->AccountingCustomerParty = $this->getAccountingCustomerParty(); $this->p_invoice->InvoiceLine = $this->getInvoiceLines(); // $this->p_invoice->TaxTotal = $this->getTotalTaxes(); it only wants the aggregate here!! $this->p_invoice->LegalMonetaryTotal = $this->getLegalMonetaryTotal(); $this->senderSpecificLevelMutators() ->receiverSpecificLevelMutators(); if(strlen($this->invoice->backup ?? '') == 0) { $this->invoice->e_invoice = $this->toObject(); $this->invoice->save(); } return $this; } /** * Rehydrates an existing e invoice - or - scaffolds a new one * * @return self */ private function setInvoice(): self { if($this->invoice->e_invoice) { $this->p_invoice = $this->e->decode('Peppol', json_encode($this->invoice->e_invoice), 'json'); return $this; } $this->p_invoice = new \InvoiceNinja\EInvoice\Models\Peppol\Invoice(); $this->setInvoiceDefaults(); return $this; } /** * Transforms the settings props into usable models we can merge. * * @return self */ private function setSettings(): self { $this->_client_settings = isset($this->invoice->client->e_invoice->Invoice) ? $this->e->decode('Peppol', json_encode($this->invoice->client->e_invoice->Invoice), 'json') : null; $this->_company_settings = isset($this->invoice->company->e_invoice->Invoice) ? $this->e->decode('Peppol', json_encode($this->invoice->company->e_invoice->Invoice), 'json') : null; return $this; } /** * getInvoice * * @return \InvoiceNinja\EInvoice\Models\Peppol\Invoice */ public function getInvoice(): \InvoiceNinja\EInvoice\Models\Peppol\Invoice { //@todo - need to process this and remove null values return $this->p_invoice; } /** * toXml * * @return string */ public function toXml(): string { $e = new EInvoice(); $xml = $e->encode($this->p_invoice, 'xml'); $prefix = ' '; return str_ireplace(['\n',''], ['', $prefix], $xml); } /** * toJson * * @return string */ public function toJson(): string { $e = new EInvoice(); $json = $e->encode($this->p_invoice, 'json'); return $json; } /** * toObject * * @return mixed */ public function toObject(): mixed { return json_decode($this->toJson()); } /** * toArray * * @return array */ public function toArray(): array { return json_decode($this->toJson(), true); } /** * getLegalMonetaryTotal * * @return LegalMonetaryTotal */ private function getLegalMonetaryTotal(): LegalMonetaryTotal { $taxable = $this->getTaxable(); $lmt = new LegalMonetaryTotal(); $lea = new LineExtensionAmount(); $lea->currencyID = $this->invoice->client->currency()->code; $lea->amount = $this->invoice->uses_inclusive_taxes ? round($this->invoice->amount - $this->invoice->total_taxes, 2) : $taxable; $lmt->LineExtensionAmount = $lea; $tea = new TaxExclusiveAmount(); $tea->currencyID = $this->invoice->client->currency()->code; $tea->amount = $this->invoice->uses_inclusive_taxes ? round($this->invoice->amount - $this->invoice->total_taxes, 2) : $taxable; $lmt->TaxExclusiveAmount = $tea; $tia = new TaxInclusiveAmount(); $tia->currencyID = $this->invoice->client->currency()->code; $tia->amount = $this->invoice->amount; $lmt->TaxInclusiveAmount = $tia; $pa = new PayableAmount(); $pa->currencyID = $this->invoice->client->currency()->code; $pa->amount = $this->invoice->amount; $lmt->PayableAmount = $pa; return $lmt; } /** * getTotalTaxAmount * * @return float */ private function getTotalTaxAmount(): float { if(!$this->invoice->total_taxes) { return 0; } elseif($this->invoice->uses_inclusive_taxes) { return $this->invoice->total_taxes; } return $this->calcAmountLineTax($this->invoice->tax_rate1, $this->invoice->amount) ?? 0; } /** * getTotalTaxes * * @return array */ private function getTotalTaxes(): array { $taxes = []; $type_id = $this->invoice->line_items[0]->type_id; // if(strlen($this->invoice->tax_name1 ?? '') > 1) { $tax_amount = new TaxAmount(); $tax_amount->currencyID = $this->invoice->client->currency()->code; $tax_amount->amount = $this->getTotalTaxAmount(); $tax_subtotal = new TaxSubtotal(); $tax_subtotal->TaxAmount = $tax_amount; $taxable_amount = new TaxableAmount(); $taxable_amount->currencyID = $this->invoice->client->currency()->code; $taxable_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->invoice->amount - $this->invoice->total_taxes : $this->invoice->amount; $tax_subtotal->TaxableAmount = $taxable_amount; $tc = new TaxCategory(); $id = new ID(); $id->value = $type_id == '2' ? 'HUR' : 'C62'; $tc->ID = $id; $tc->Percent = $this->invoice->tax_rate1; $ts = new TaxScheme(); $id = new ID(); $id->value = strlen($this->invoice->tax_name1 ?? '') > 1 ? $this->invoice->tax_name1 : '0'; $ts->ID = $id; $tc->TaxScheme = $ts; $tax_subtotal->TaxCategory = $tc; $tax_total = new TaxTotal(); $tax_total->TaxAmount = $tax_amount; $tax_total->TaxSubtotal[] = $tax_subtotal; $taxes[] = $tax_total; // } if(strlen($this->invoice->tax_name2 ?? '') > 1) { $tax_amount = new TaxAmount(); $tax_amount->currencyID = $this->invoice->client->currency()->code; $tax_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->calcInclusiveLineTax($this->invoice->tax_rate2, $this->invoice->amount) : $this->calcAmountLineTax($this->invoice->tax_rate2, $this->invoice->amount); $tax_subtotal = new TaxSubtotal(); $tax_subtotal->TaxAmount = $tax_amount; $taxable_amount = new TaxableAmount(); $taxable_amount->currencyID = $this->invoice->client->currency()->code; $taxable_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->invoice->amount - $this->invoice->total_taxes : $this->invoice->amount; $tax_subtotal->TaxableAmount = $taxable_amount; $tc = new TaxCategory(); $id = new ID(); $id->value = $type_id == '2' ? 'HUR' : 'C62'; $tc->ID = $id; $tc->Percent = $this->invoice->tax_rate2; $ts = new TaxScheme(); $id = new ID(); $id->value = $this->invoice->tax_name2; $ts->ID = $id; $tc->TaxScheme = $ts; $tax_subtotal->TaxCategory = $tc; $tax_total = new TaxTotal(); $tax_total->TaxAmount = $tax_amount; $tax_total->TaxSubtotal[] = $tax_subtotal; $taxes[] = $tax_total; } if(strlen($this->invoice->tax_name3 ?? '') > 1) { $tax_amount = new TaxAmount(); $tax_amount->currencyID = $this->invoice->client->currency()->code; $tax_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->calcInclusiveLineTax($this->invoice->tax_rate3, $this->invoice->amount) : $this->calcAmountLineTax($this->invoice->tax_rate3, $this->invoice->amount); $tax_subtotal = new TaxSubtotal(); $tax_subtotal->TaxAmount = $tax_amount; $taxable_amount = new TaxableAmount(); $taxable_amount->currencyID = $this->invoice->client->currency()->code; $taxable_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->invoice->amount - $this->invoice->total_taxes : $this->invoice->amount; $tax_subtotal->TaxableAmount = $taxable_amount; $tc = new TaxCategory(); $id = new ID(); $id->value = $type_id == '2' ? 'HUR' : 'C62'; $tc->ID = $id; $tc->Percent = $this->invoice->tax_rate3; $ts = new TaxScheme(); $id = new ID(); $id->value = $this->invoice->tax_name3; $ts->ID = $id; $tc->TaxScheme = $ts; $tax_subtotal->TaxCategory = $tc; $tax_total = new TaxTotal(); $tax_total->TaxAmount = $tax_amount; $tax_total->TaxSubtotal[] = $tax_subtotal; $taxes[] = $tax_total; } return $taxes; } private function getInvoiceLines(): array { $lines = []; foreach($this->invoice->line_items as $key => $item) { $_item = new Item(); $_item->Name = $item->product_key; $_item->Description = $item->notes; $line = new InvoiceLine(); $id = new ID(); $id->value = (string) ($key+1); $line->ID = $id; $line->InvoicedQuantity = $item->quantity; $lea = new LineExtensionAmount(); $lea->currencyID = $this->invoice->client->currency()->code; // $lea->amount = $item->line_total; $lea->amount = $this->invoice->uses_inclusive_taxes ? $item->line_total - $this->calcInclusiveLineTax($item->tax_rate1, $item->line_total) : $item->line_total; $line->LineExtensionAmount = $lea; $line->Item = $_item; $item_taxes = $this->getItemTaxes($item); if(count($item_taxes) > 0) { $line->TaxTotal = $item_taxes; } // else { // $line->TaxTotal = $this->zeroTaxAmount(); // } $price = new Price(); $pa = new PriceAmount(); $pa->currencyID = $this->invoice->client->currency()->code; $pa->amount = (string) ($this->costWithDiscount($item) - ($this->invoice->uses_inclusive_taxes ? ($this->calcInclusiveLineTax($item->tax_rate1, $item->line_total) / $item->quantity) : 0)); $price->PriceAmount = $pa; $line->Price = $price; $lines[] = $line; } return $lines; } /** * costWithDiscount * * @param mixed $item * @return float */ private function costWithDiscount($item): float { $cost = $item->cost; if ($item->discount != 0) { if ($this->invoice->is_amount_discount) { $cost -= $item->discount / $item->quantity; } else { $cost -= $cost * $item->discount / 100; } } return $cost; } /** * zeroTaxAmount * * @return array */ private function zeroTaxAmount(): array { $blank_tax = []; $tax_amount = new TaxAmount(); $tax_amount->currencyID = $this->invoice->client->currency()->code; $tax_amount->amount = '0'; $tax_subtotal = new TaxSubtotal(); $tax_subtotal->TaxAmount = $tax_amount; $taxable_amount = new TaxableAmount(); $taxable_amount->currencyID = $this->invoice->client->currency()->code; $taxable_amount->amount = '0'; $tax_subtotal->TaxableAmount = $taxable_amount; $tc = new TaxCategory(); $id = new ID(); $id->value = 'Z'; $tc->ID = $id; $tc->Percent = '0'; $ts = new TaxScheme(); $id = new ID(); $id->value = '0'; $ts->ID = $id; $tc->TaxScheme = $ts; $tax_subtotal->TaxCategory = $tc; $tax_total = new TaxTotal(); $tax_total->TaxAmount = $tax_amount; $tax_total->TaxSubtotal[] = $tax_subtotal; $blank_tax[] = $tax_total; return $blank_tax; } /** * getItemTaxes * * @param object $item * @return array */ private function getItemTaxes(object $item): array { $item_taxes = []; if(strlen($item->tax_name1 ?? '') > 1) { $tax_amount = new TaxAmount(); $tax_amount->currencyID = $this->invoice->client->currency()->code; $tax_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->calcInclusiveLineTax($item->tax_rate1, $item->line_total) : $this->calcAmountLineTax($item->tax_rate1, $item->line_total); $tax_subtotal = new TaxSubtotal(); $tax_subtotal->TaxAmount = $tax_amount; $taxable_amount = new TaxableAmount(); $taxable_amount->currencyID = $this->invoice->client->currency()->code; $taxable_amount->amount = $this->invoice->uses_inclusive_taxes ? $item->line_total - $tax_amount->amount : $item->line_total; $tax_subtotal->TaxableAmount = $taxable_amount; $tc = new TaxCategory(); $id = new ID(); $id->value = $item->type_id == '2' ? 'HUR' : 'C62'; $tc->ID = $id; $tc->Percent = $item->tax_rate1; $ts = new TaxScheme(); $id = new ID(); $id->value = $item->tax_name1; $ts->ID = $id; $tc->TaxScheme = $ts; $tax_subtotal->TaxCategory = $tc; $tax_total = new TaxTotal(); $tax_total->TaxAmount = $tax_amount; $tax_total->TaxSubtotal[] = $tax_subtotal; $item_taxes[] = $tax_total; } if(strlen($item->tax_name2 ?? '') > 1) { $tax_amount = new TaxAmount(); $tax_amount->currencyID = $this->invoice->client->currency()->code; $tax_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->calcInclusiveLineTax($item->tax_rate2, $item->line_total) : $this->calcAmountLineTax($item->tax_rate2, $item->line_total); $tax_subtotal = new TaxSubtotal(); $tax_subtotal->TaxAmount = $tax_amount; $taxable_amount = new TaxableAmount(); $taxable_amount->currencyID = $this->invoice->client->currency()->code; $taxable_amount->amount = $item->line_total; $tax_subtotal->TaxableAmount = $taxable_amount; $tc = new TaxCategory(); $id = new ID(); $id->value = $item->type_id == '2' ? 'HUR' : 'C62'; $tc->ID = $id; $tc->Percent = $item->tax_rate2; $ts = new TaxScheme(); $id = new ID(); $id->value = $item->tax_name2; $ts->ID = $id; $tc->TaxScheme = $ts; $tax_subtotal->TaxCategory = $tc; $tax_total = new TaxTotal(); $tax_total->TaxAmount = $tax_amount; $tax_total->TaxSubtotal[] = $tax_subtotal; $item_taxes[] = $tax_total; } if(strlen($item->tax_name3 ?? '') > 1) { $tax_amount = new TaxAmount(); $tax_amount->currencyID = $this->invoice->client->currency()->code; $tax_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->calcInclusiveLineTax($item->tax_rate3, $item->line_total) : $this->calcAmountLineTax($item->tax_rate3, $item->line_total); $tax_subtotal = new TaxSubtotal(); $tax_subtotal->TaxAmount = $tax_amount; $taxable_amount = new TaxableAmount(); $taxable_amount->currencyID = $this->invoice->client->currency()->code; $taxable_amount->amount = $item->line_total; $tax_subtotal->TaxableAmount = $taxable_amount; $tc = new TaxCategory(); $id = new ID(); $id->value = $item->type_id == '2' ? 'HUR' : 'C62'; $tc->ID = $id; $tc->Percent = $item->tax_rate3; $ts = new TaxScheme(); $id = new ID(); $id->value = $item->tax_name3; $ts->ID = $id; $tc->TaxScheme = $ts; $tax_subtotal->TaxCategory = $tc; $tax_total = new TaxTotal(); $tax_total->TaxAmount = $tax_amount; $tax_total->TaxSubtotal[] = $tax_subtotal; $item_taxes[] = $tax_total; } return $item_taxes; } /** * getAccountingSupplierParty * * @return AccountingSupplierParty */ private function getAccountingSupplierParty(): AccountingSupplierParty { $asp = new AccountingSupplierParty(); $party = new Party(); $party_name = new PartyName(); $party_name->Name = $this->invoice->company->present()->name(); $party->PartyName[] = $party_name; $address = new Address(); $address->CityName = $this->invoice->company->settings->city; $address->StreetName = $this->invoice->company->settings->address1; // $address->BuildingName = $this->invoice->company->settings->address2; $address->PostalZone = $this->invoice->company->settings->postal_code; $address->CountrySubentity = $this->invoice->company->settings->state; // $address->CountrySubentityCode = $this->invoice->company->settings->state; $country = new Country(); $country->IdentificationCode = $this->invoice->company->country()->iso_3166_2; $address->Country = $country; $party->PostalAddress = $address; $party->PhysicalLocation = $address; $contact = new Contact(); $contact->ElectronicMail = $this->getSetting('Invoice.AccountingSupplierParty.Party.Contact') ?? $this->invoice->company->owner()->present()->email(); $contact->Telephone = $this->getSetting('Invoice.AccountingSupplierParty.Party.Telephone') ?? $this->invoice->company->getSetting('phone'); $contact->Name = $this->getSetting('Invoice.AccountingSupplierParty.Party.Name') ?? $this->invoice->company->owner()->present()->name(); $party->Contact = $contact; $asp->Party = $party; return $asp; } /** * resolveTaxScheme * * @return string */ private function resolveTaxScheme(): string { return (new StorecoveRouter())->resolveTaxScheme($this->invoice->client->country->iso_3166_2, $this->invoice->client->classification); // $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", // "government" => $code = "G", // "individual" => $code = "C", // default => $code = false, // }; // //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 false; } /** * getAccountingCustomerParty * * @return AccountingCustomerParty */ private function getAccountingCustomerParty(): AccountingCustomerParty { $acp = new AccountingCustomerParty(); $party = new Party(); if(strlen($this->invoice->client->vat_number ?? '') > 1) { $pi = new PartyIdentification(); $vatID = new ID(); if($scheme = $this->resolveTaxScheme()) { $vatID->schemeID = $scheme; } $vatID->value = $this->invoice->client->vat_number; $pi->ID = $vatID; $party->PartyIdentification[] = $pi; } $party_name = new PartyName(); $party_name->Name = $this->invoice->client->present()->name(); $party->PartyName[] = $party_name; $address = new Address(); $address->CityName = $this->invoice->client->city; $address->StreetName = $this->invoice->client->address1; // $address->BuildingName = $this->invoice->client->address2; $address->PostalZone = $this->invoice->client->postal_code; $address->CountrySubentity = $this->invoice->client->state; // $address->CountrySubentityCode = $this->invoice->client->state; $country = new Country(); $country->IdentificationCode = $this->invoice->client->country->iso_3166_2; $address->Country = $country; $party->PostalAddress = $address; $physical_location = new PhysicalLocation(); $physical_location->Address = $address; $party->PhysicalLocation = $physical_location; ; $contact = new Contact(); $contact->ElectronicMail = $this->invoice->client->present()->email(); $party->Contact = $contact; $acp->Party = $party; return $acp; } /** * getTaxable * * @return float */ private function getTaxable(): float { $total = 0; foreach ($this->invoice->line_items as $item) { $line_total = $item->quantity * $item->cost; if ($item->discount != 0) { if ($this->invoice->is_amount_discount) { $line_total -= $item->discount; } else { $line_total -= $line_total * $item->discount / 100; } } $total += $line_total; } if ($this->invoice->discount > 0) { if ($this->invoice->is_amount_discount) { $total -= $this->invoice->discount; } else { $total *= (100 - $this->invoice->discount) / 100; $total = round($total, 2); } } if ($this->invoice->custom_surcharge1 && $this->invoice->custom_surcharge_tax1) { $total += $this->invoice->custom_surcharge1; } if ($this->invoice->custom_surcharge2 && $this->invoice->custom_surcharge_tax2) { $total += $this->invoice->custom_surcharge2; } if ($this->invoice->custom_surcharge3 && $this->invoice->custom_surcharge_tax3) { $total += $this->invoice->custom_surcharge3; } if ($this->invoice->custom_surcharge4 && $this->invoice->custom_surcharge_tax4) { $total += $this->invoice->custom_surcharge4; } return $total; } ///////////////// Helper Methods ///////////////////////// /** * getClientRoutingCode * * @return string */ 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]; // } 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}"); } /** * setInvoiceDefaults * * Stubs a default einvoice * @return self */ public function setInvoiceDefaults(): self { $settings = [ 'AccountingCostCode' => 7, 'AccountingCost' => 7, 'BuyerReference' => 6, 'AccountingSupplierParty' => 1, 'AccountingCustomerParty' => 2, 'PayeeParty' => 1, 'BuyerCustomerParty' => 2, 'SellerSupplierParty' => 1, 'TaxRepresentativeParty' => 1, 'Delivery' => 1, 'DeliveryTerms' => 7, 'PaymentMeans' => 7, 'PaymentTerms' => 7, ]; //only scans for top level props foreach($settings as $prop => $visibility) { if($prop_value = $this->getSetting($prop)) { $this->p_invoice->{$prop} = $prop_value; } } 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 { if($prop_value = PropertyResolver::resolve($this->p_invoice, $property_path)) { return $prop_value; } elseif($prop_value = PropertyResolver::resolve($this->_client_settings, $property_path)) { return $prop_value; } elseif($prop_value = PropertyResolver::resolve($this->_company_settings, $property_path)) { return $prop_value; } return null; } /** * getClientSetting * * @param string $property_path * @return mixed */ private function getClientSetting(string $property_path): mixed { return PropertyResolver::resolve($this->_client_settings, $property_path); } /** * getCompanySetting * * @param string $property_path * @return mixed */ private function getCompanySetting(string $property_path): mixed { return PropertyResolver::resolve($this->_company_settings, $property_path); } /** * senderSpecificLevelMutators * * Runs sender level specific requirements for the e-invoice, * * ie, mutations that are required by the senders country. * * @return self */ private function senderSpecificLevelMutators(): self { if(method_exists($this, $this->invoice->company->country()->iso_3166_2)) { $this->{$this->invoice->company->country()->iso_3166_2}(); } 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 * * Sets the payment means - if it exists * @param bool $required * @return self */ private function setPaymentMeans(bool $required = false): self { if(isset($this->p_invoice->PaymentMeans)) { return $this; } elseif($paymentMeans = $this->getSetting('Invoice.PaymentMeans')) { $this->p_invoice->PaymentMeans = is_array($paymentMeans) ? $paymentMeans : [$paymentMeans]; return $this; } 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'); } /** * 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 array $identifiers * @return array */ private function buildRouting(array $identifiers): array { return [ "routing" => [ "eIdentifiers" => $identifiers, ] ]; } /** * setEmailRouting * * @param string $email * @return self */ private function setEmailRouting(string $email): self { nlog($email); $meta = $this->getStorecoveMeta(); if(isset($meta['routing']['emails'])) { $emails = $meta['routing']['emails']; array_push($emails, $email); $meta['routing']['emails'] = $emails; } else { $meta['routing']['emails'] = [$email]; } $this->setStorecoveMeta($meta); 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; } /** * getStorecoveMeta * * @return array */ public function getStorecoveMeta(): array { return $this->storecove_meta; } ////////////////////////// Country level mutators ///////////////////////////////////// /** * DE * * @Completed * @Tested * * @return self */ private function DE(): self { $this->setPaymentMeans(true); return $this; } /** * CH * * @Completed * * Completed - QR-Bill to be implemented at a later date. * @return self */ private function CH(): self { return $this; } /** * AT * * @Pending * * Need to ensure when sending to government entities that we route appropriately * Also need to ensure customerAssignedAccountIdValue is set so that the sender can be resolved. * * Need a way to define if the client is a government entity. * * @return self */ 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(["scheme" => 'AT:GOV', "id" => 'b'])); //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; } /** * ES * * @Pending * B2G configuration * B2G Testing * * testing. // routing identifier - 293098 * * @return self */ private function ES(): self { if(!isset($this->invoice->due_date)) { $this->p_invoice->DueDate = new \DateTime($this->invoice->date); } if($this->invoice->client->classification == 'business' && $this->invoice->company->getSetting('classification') == 'business') { //must have a paymentmeans as credit_transfer $this->setPaymentMeans(true); } // For B2G, provide three ES:FACE identifiers in the routing object, // as well as the ES:VAT tax identifier in the accountingCustomerParty.publicIdentifiers. // The invoice will then be routed through the FACe network. The three required ES:FACE identifiers are as follows: // "routing": { // "eIdentifiers":[ // { // "scheme": "ES:FACE", // "id": "L01234567", // "role": "ES-01-FISCAL" // }, // { // "scheme": "ES:FACE", // "id": "L01234567", // "role": "ES-02-RECEPTOR" // }, // { // "scheme": "ES:FACE", // "id": "L01234567", // "role": "ES-03-PAGADOR" // } // ] // } return $this; } /** * FI * * @return self */ private function FI(): self { // For Finvoice, provide an FI:OPID routing identifier and an FI:OVT legal identifier. // An FI:VAT is recommended. In many cases (depending on the sender/receiver country and the type of service/goods) // an FI:VAT is required. So we recommend always including this. return $this; } /** * FR * @Pending - clarification on codes needed * * @return self */ 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([ ["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); } if(strlen($this->invoice->client->id_number ?? '') == 9) { //SIREN $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([ ["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 // 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) { $this->setOrderReference(false); } return $this; } /** * IT * * @return self */ private function IT(): self { // 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') { $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([ ["scheme" => 'IT:CF', "id" => $this->invoice->client->vat_number], // ["scheme" => 'IT:CUUO', "id" => $this->invoice->client->routing_id] ])); $this->setEmailRouting($this->invoice->client->present()->email()); 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->getClientRoutingCode(); nlog("foreign receiver"); $this->setStorecoveMeta($this->buildRouting([ ["scheme" => $code, "id" => $this->invoice->client->vat_number] ])); return $this; } return $this; } /** * client_IT * * @return self */ 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) return $this; } /** * MY * * @return self */ private function MY(): self { //way too much to digest here, delayed. return $this; } /** * NL * * @return self */ private function NL(): self { // When sending to public entities, the invoice.accountingSupplierParty.party.contact.email is mandatory. // Dutch senders and receivers require a legal identifier. For companies, this is NL:KVK, for public entities this is NL:OINO. return $this; } /** * NZ * * @return self */ private function NZ(): self { // New Zealand uses a GLN to identify businesses. In addition, when sending invoices to a New Zealand customer, make sure you include the pseudo identifier NZ:GST as their tax identifier. return $this; } /** * PL * * @return self */ private function PL(): 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: // "routing": { // "eIdentifiers": [ // { // "scheme": "PL:VAT", // "id": "PL0101010101" // } // ], // "networks": [ // { // "application": "pl-ksef", // "settings": { // "enabled": true // } // } // ] // } // Note this will only work if your LegalEntity has been setup for this network. return $this; } /** * RO * * @return self */ 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: $meta = ["networks" => [ [ "application" => "ro-anaf", "settings" => [ "enabled" => true ], ], ]]; $this->setStorecoveMeta($meta); $this->setStorecoveMeta($this->buildRouting([ ["scheme" => 'RO:VAT', "id" => $this->invoice->client->vat_number], ])); $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; } /** * SG * * @return self */ private function SG(): self { //delayed - stage 2 return $this; } //Sweden private function SE(): self { // Deliver invoices to the "Svefaktura" co-operation of local Swedish service providers. // Routing is through the SE:ORGNR together with a network specification: // "routing": { // "eIdentifiers": [ // { // "scheme": "SE:ORGNR", // "id": "0012345678" // } // ], // "networks": [ // { // "application": "svefaktura", // "settings": { // "enabled": true // } // } // ] // } // Use of the "Svefaktura" co-operation can also be induced by specifying an operator id, as follows: // "routing": { // "eIdentifiers": [ // { // "scheme": "SE:ORGNR", // "id": "0012345678" // }, // { // "scheme": "SE:OPID", // "id": "1234567890" // } // ] // } return $this; } }