1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-08 12:12:48 +01:00

Merge pull request #10226 from turbo124/v5-develop

Improve helper text for stripe payment methods
This commit is contained in:
David Bomba 2024-11-07 07:43:23 +11:00 committed by GitHub
commit 87c788563e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
84 changed files with 4638 additions and 1699 deletions

View File

@ -155,7 +155,15 @@ class BaseRule implements RuleInterface
$this->resolveRegions();
if(!$this->isTaxableRegion()) {
$this->tax_data = null;
$this->tax_rate1 = 0;
$this->tax_name1 = '';
$this->tax_rate2 = 0;
$this->tax_name2 = '';
$this->tax_rate3 = 0;
$this->tax_name3 = '';
return $this;
}
@ -317,8 +325,32 @@ class BaseRule implements RuleInterface
}
if(isset($this->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion})) {
$this->tax_rate1 = $this->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->tax_rate;
$this->tax_name1 = $this->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->tax_name;
if ($this->client_region === 'EU') {
$company_country_code = $this->client->company->country()->iso_3166_2;
$client_country_code = $this->client->country->iso_3166_2;
$is_over_threshold = isset($this->client->company->tax_data->regions->EU->has_sales_above_threshold) &&
$this->client->company->tax_data->regions->EU->has_sales_above_threshold;
$is_b2c = strlen($this->client->vat_number) < 2 ||
!($this->client->has_valid_vat_number ?? false) ||
$this->client->classification == 'individual';
// Use destination VAT only for B2C transactions over threshold
if ($is_b2c && $is_over_threshold) {
$this->tax_rate1 = $this->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->tax_rate;
$this->tax_name1 = $this->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->tax_name;
}
// Otherwise, use origin country tax rates
elseif (in_array($company_country_code, $this->eu_country_codes)) {
$this->tax_rate1 = $this->client->company->tax_data->regions->{$this->client_region}->subregions->{$company_country_code}->tax_rate;
$this->tax_name1 = $this->client->company->tax_data->regions->{$this->client_region}->subregions->{$company_country_code}->tax_name;
}
}
else {
$this->tax_rate1 = $this->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->tax_rate;
$this->tax_name1 = $this->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->tax_name;
}
}
return $this;
@ -344,10 +376,17 @@ class BaseRule implements RuleInterface
Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced($item),
Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override($item),
Product::PRODUCT_TYPE_ZERO_RATED => $this->zeroRated($item),
Product::PRODUCT_TYPE_REVERSE_TAX => $this->zeroRated($item),
Product::PRODUCT_INTRA_COMMUNITY => $this->zeroRated($item),
default => $this->defaultForeign(),
};
return $this;
}
$this->taxByType($item);
return $this;
}

View File

@ -237,38 +237,64 @@ class Rule extends BaseRule implements RuleInterface
*/
public function calculateRates(): self
{
// Tax exempt clients always get zero tax
if ($this->client->is_tax_exempt) {
// nlog("tax exempt");
$this->tax_rate = 0;
$this->reduced_tax_rate = 0;
} elseif($this->client_subregion != $this->client->company->tax_data->seller_subregion && in_array($this->client_subregion, $this->eu_country_codes) && $this->client->vat_number && $this->client->has_valid_vat_number && $this->eu_business_tax_exempt) {
// nlog("euro zone and tax exempt");
return $this;
}
// B2B within EU with valid VAT
if ($this->client_subregion != $this->client->company->tax_data->seller_subregion &&
in_array($this->client_subregion, $this->eu_country_codes) &&
$this->client->vat_number &&
$this->client->has_valid_vat_number &&
$this->eu_business_tax_exempt) {
$this->tax_rate = 0;
$this->reduced_tax_rate = 0;
} elseif(!in_array($this->client_subregion, $this->eu_country_codes) && ($this->foreign_consumer_tax_exempt || $this->foreign_business_tax_exempt)) { //foreign + tax exempt
// nlog("foreign and tax exempt");
$this->tax_rate = 0;
$this->reduced_tax_rate = 0;
} elseif(!in_array($this->client_subregion, $this->eu_country_codes)) {
$this->defaultForeign();
} elseif(in_array($this->client_subregion, $this->eu_country_codes) && ((strlen($this->client->vat_number ?? '') == 1) || !$this->client->has_valid_vat_number)) { //eu country / no valid vat
if($this->client->company->tax_data->seller_subregion != $this->client_subregion) {
// nlog("eu zone with sales above threshold");
return $this;
}
// Non-EU transactions
if (!in_array($this->client_subregion, $this->eu_country_codes)) {
if ($this->foreign_consumer_tax_exempt || $this->foreign_business_tax_exempt) {
$this->tax_rate = 0;
$this->reduced_tax_rate = 0;
} else {
$this->defaultForeign();
}
return $this;
}
// B2C or invalid VAT within EU
$is_b2c = strlen($this->client->vat_number ?? '') <= 1 ||
!$this->client->has_valid_vat_number ||
$this->client->classification == 'individual';
if ($is_b2c) {
$is_over_threshold = $this->client->company->tax_data->regions->EU->has_sales_above_threshold ?? false;
if ($is_over_threshold && $this->client->company->tax_data->seller_subregion != $this->client_subregion) {
// Over threshold - use destination country rates
$this->tax_name = $this->client->company->tax_data->regions->EU->subregions->{$this->client->country->iso_3166_2}->tax_name;
$this->tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->country->iso_3166_2}->tax_rate ?? 0;
$this->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->country->iso_3166_2}->reduced_tax_rate ?? 0;
} else {
// nlog("EU with intra-community supply ie DE to DE");
// Under threshold or domestic - use origin country rates
$this->tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->tax_rate ?? 0;
$this->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->reduced_tax_rate ?? 0;
}
} else {
// nlog("default tax");
$this->tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->tax_rate ?? 0;
$this->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->reduced_tax_rate ?? 0;
return $this;
}
// Default case (B2B without valid VAT)
$this->tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->tax_rate ?? 0;
$this->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->reduced_tax_rate ?? 0;
return $this;
}
}

View File

@ -41,6 +41,7 @@ class InvoiceItemFactory
$item->custom_value3 = '';
$item->custom_value4 = '';
$item->type_id = '1';
$item->tax_id = '1';
return $item;
}
@ -58,11 +59,11 @@ class InvoiceItemFactory
for ($x = 0; $x < $items; $x++) {
$item = self::create();
$item->quantity = $faker->numberBetween(1, 10);
$item->quantity = rand(1, 10);
$item->cost = $faker->randomFloat(2, 1, 1000);
$item->line_total = $item->quantity * $item->cost;
$item->is_amount_discount = true;
$item->discount = $faker->numberBetween(1, 10);
$item->discount = rand(1, 10);
$item->notes = str_replace(['"',"'"], ['',""], $faker->realText(20));
$item->product_key = $faker->word();
// $item->custom_value1 = $faker->realText(10);
@ -78,11 +79,11 @@ class InvoiceItemFactory
$item = self::create();
$item->quantity = $faker->numberBetween(1, 10);
$item->quantity = rand(1, 10);
$item->cost = $faker->randomFloat(2, 1, 1000);
$item->line_total = $item->quantity * $item->cost;
$item->is_amount_discount = true;
$item->discount = $faker->numberBetween(1, 10);
$item->discount = rand(1, 10);
$item->notes = str_replace(['"',"'"], ['',""], $faker->realText(20));
$item->product_key = $faker->word();
// $item->custom_value1 = $faker->realText(10);
@ -112,7 +113,7 @@ class InvoiceItemFactory
for ($x = 0; $x < $items; $x++) {
$item = self::create();
$item->quantity = $faker->numberBetween(-1, -10);
$item->quantity = rand(-1, -10);
$item->cost = $faker->randomFloat(2, -1, -1000);
$item->line_total = $item->quantity * $item->cost;
$item->is_amount_discount = true;

View File

@ -141,8 +141,8 @@ class InvoiceItemSum
$this->invoice = $invoice;
$this->client = $invoice->client ?? $invoice->vendor;
if ($this->invoice->client) {
$this->currency = $this->invoice->client->currency();
if ($this->client) {
$this->currency = $this->client->currency();
$this->shouldCalculateTax();
} else {
$this->currency = $this->invoice->vendor->currency();
@ -158,7 +158,7 @@ class InvoiceItemSum
return $this;
}
$this->calcLineItems();
$this->calcLineItems()->getPeppolSurchargeTaxes();
return $this;
}
@ -186,7 +186,6 @@ class InvoiceItemSum
if (in_array($this->client->company->country()->iso_3166_2, $this->tax_jurisdictions)) { //only calculate for supported tax jurisdictions
/** @var \App\DataMapper\Tax\BaseRule $class */
$class = "App\DataMapper\Tax\\".str_replace("-","_",$this->client->company->country()->iso_3166_2)."\\Rule";
@ -296,7 +295,7 @@ class InvoiceItemSum
$item_tax += $item_tax_rate1_total;
if (strlen($this->item->tax_name1) > 1) {
$this->groupTax($this->item->tax_name1, $this->item->tax_rate1, $item_tax_rate1_total);
$this->groupTax($this->item->tax_name1, $this->item->tax_rate1, $item_tax_rate1_total, $amount, $this->item->tax_id);
}
$item_tax_rate2_total = $this->calcAmountLineTax($this->item->tax_rate2, $amount);
@ -304,7 +303,7 @@ class InvoiceItemSum
$item_tax += $item_tax_rate2_total;
if (strlen($this->item->tax_name2) > 1) {
$this->groupTax($this->item->tax_name2, $this->item->tax_rate2, $item_tax_rate2_total);
$this->groupTax($this->item->tax_name2, $this->item->tax_rate2, $item_tax_rate2_total, $amount, $this->item->tax_id);
}
$item_tax_rate3_total = $this->calcAmountLineTax($this->item->tax_rate3, $amount);
@ -312,7 +311,7 @@ class InvoiceItemSum
$item_tax += $item_tax_rate3_total;
if (strlen($this->item->tax_name3) > 1) {
$this->groupTax($this->item->tax_name3, $this->item->tax_rate3, $item_tax_rate3_total);
$this->groupTax($this->item->tax_name3, $this->item->tax_rate3, $item_tax_rate3_total, $amount, $this->item->tax_id);
}
$this->setTotalTaxes($this->formatValue($item_tax, $this->currency->precision));
@ -324,13 +323,61 @@ class InvoiceItemSum
return $this;
}
private function groupTax($tax_name, $tax_rate, $tax_total)
private function getPeppolSurchargeTaxes(): self
{
if(!$this->client->getSetting('e_invoice_type') == 'PEPPOL')
return $this;
collect($this->invoice->line_items)
->flatMap(function ($item) {
return collect([1, 2, 3])
->map(fn ($i) => [
'name' => $item->{"tax_name{$i}"} ?? '',
'percentage' => $item->{"tax_rate{$i}"} ?? 0,
'tax_id' => $item->tax_id ?? '1',
])
->filter(fn ($tax) => strlen($tax['name']) > 1);
})
->unique(fn ($tax) => $tax['percentage'] . '_' . $tax['name'])
->values()
->each(function ($tax){
$tax_component = 0;
if ($this->invoice->custom_surcharge1) {
$tax_component += round($this->invoice->custom_surcharge1 * ($tax['percentage'] / 100), 2);
}
if ($this->invoice->custom_surcharge2) {
$tax_component += round($this->invoice->custom_surcharge2 * ($tax['percentage'] / 100), 2);
}
if ($this->invoice->custom_surcharge3) {
$tax_component += round($this->invoice->custom_surcharge3 * ($tax['percentage'] / 100), 2);
}
if ($this->invoice->custom_surcharge4) {
$tax_component += round($this->invoice->custom_surcharge4 * ($tax['percentage'] / 100), 2);
}
$amount = $this->invoice->custom_surcharge4 + $this->invoice->custom_surcharge3 + $this->invoice->custom_surcharge2 + $this->invoice->custom_surcharge1;
if($tax_component > 0)
$this->groupTax($tax['name'], $tax['percentage'], $tax_component, $amount, $tax['tax_id']);
});
return $this;
}
private function groupTax($tax_name, $tax_rate, $tax_total, $amount, $tax_id = '')
{
$group_tax = [];
$key = str_replace(' ', '', $tax_name.$tax_rate);
$group_tax = ['key' => $key, 'total' => $tax_total, 'tax_name' => $tax_name.' '.Number::formatValueNoTrailingZeroes(floatval($tax_rate), $this->client).'%'];
$group_tax = ['key' => $key, 'total' => $tax_total, 'tax_name' => $tax_name.' '.Number::formatValueNoTrailingZeroes(floatval($tax_rate), $this->client).'%', 'tax_id' => $tax_id, 'tax_rate' => $tax_rate, 'base_amount' => $amount];
$this->tax_collection->push(collect($group_tax));
}
@ -425,14 +472,12 @@ class InvoiceItemSum
$amount = $this->item->line_total;
}
//$amount = ($this->sub_total > 0) ? $this->item->line_total - ($this->invoice->discount * ($this->item->line_total / $this->sub_total)) : 0;
$item_tax_rate1_total = $this->calcAmountLineTax($this->item->tax_rate1, $amount);
$item_tax += $item_tax_rate1_total;
if ($item_tax_rate1_total != 0) {
$this->groupTax($this->item->tax_name1, $this->item->tax_rate1, $item_tax_rate1_total);
$this->groupTax($this->item->tax_name1, $this->item->tax_rate1, $item_tax_rate1_total, $amount, $this->item->tax_id);
}
$item_tax_rate2_total = $this->calcAmountLineTax($this->item->tax_rate2, $amount);
@ -440,7 +485,7 @@ class InvoiceItemSum
$item_tax += $item_tax_rate2_total;
if ($item_tax_rate2_total != 0) {
$this->groupTax($this->item->tax_name2, $this->item->tax_rate2, $item_tax_rate2_total);
$this->groupTax($this->item->tax_name2, $this->item->tax_rate2, $item_tax_rate2_total, $amount, $this->item->tax_id);
}
$item_tax_rate3_total = $this->calcAmountLineTax($this->item->tax_rate3, $amount);
@ -448,7 +493,7 @@ class InvoiceItemSum
$item_tax += $item_tax_rate3_total;
if ($item_tax_rate3_total != 0) {
$this->groupTax($this->item->tax_name3, $this->item->tax_rate3, $item_tax_rate3_total);
$this->groupTax($this->item->tax_name3, $this->item->tax_rate3, $item_tax_rate3_total, $amount, $this->item->tax_id);
}
$this->item->gross_line_total = $this->getLineTotal() + $item_tax;

View File

@ -244,7 +244,7 @@ class InvoiceItemSumInclusive
$item_tax += $this->formatValue($item_tax_rate1_total, $this->currency->precision);
if (strlen($this->item->tax_name1) > 1) {
$this->groupTax($this->item->tax_name1, $this->item->tax_rate1, $item_tax_rate1_total);
$this->groupTax($this->item->tax_name1, $this->item->tax_rate1, $item_tax_rate1_total, $amount, $this->item->tax_id);
}
$item_tax_rate2_total = $this->calcInclusiveLineTax($this->item->tax_rate2, $amount);
@ -252,7 +252,7 @@ class InvoiceItemSumInclusive
$item_tax += $this->formatValue($item_tax_rate2_total, $this->currency->precision);
if (strlen($this->item->tax_name2) > 1) {
$this->groupTax($this->item->tax_name2, $this->item->tax_rate2, $item_tax_rate2_total);
$this->groupTax($this->item->tax_name2, $this->item->tax_rate2, $item_tax_rate2_total, $amount, $this->item->tax_id);
}
$item_tax_rate3_total = $this->calcInclusiveLineTax($this->item->tax_rate3, $amount);
@ -260,7 +260,7 @@ class InvoiceItemSumInclusive
$item_tax += $this->formatValue($item_tax_rate3_total, $this->currency->precision);
if (strlen($this->item->tax_name3) > 1) {
$this->groupTax($this->item->tax_name3, $this->item->tax_rate3, $item_tax_rate3_total);
$this->groupTax($this->item->tax_name3, $this->item->tax_rate3, $item_tax_rate3_total, $amount, $this->item->tax_id);
}
$this->item->tax_amount = $this->formatValue($item_tax, $this->currency->precision);
@ -270,13 +270,13 @@ class InvoiceItemSumInclusive
return $this;
}
private function groupTax($tax_name, $tax_rate, $tax_total)
private function groupTax($tax_name, $tax_rate, $tax_total, $amount, $tax_id = '')
{
$group_tax = [];
$key = str_replace(' ', '', $tax_name.$tax_rate);
$group_tax = ['key' => $key, 'total' => $tax_total, 'tax_name' => $tax_name.' '.Number::formatValueNoTrailingZeroes(floatval($tax_rate), $this->client).'%'];
$group_tax = ['key' => $key, 'total' => $tax_total, 'tax_name' => $tax_name.' '.Number::formatValueNoTrailingZeroes(floatval($tax_rate), $this->client).'%', 'tax_id' => $tax_id, 'base_amount' => $amount];
$this->tax_collection->push(collect($group_tax));
}
@ -376,7 +376,7 @@ class InvoiceItemSumInclusive
$item_tax += $item_tax_rate1_total;
if ($item_tax_rate1_total != 0) {
$this->groupTax($this->item->tax_name1, $this->item->tax_rate1, $item_tax_rate1_total);
$this->groupTax($this->item->tax_name1, $this->item->tax_rate1, $item_tax_rate1_total, $amount, $this->item->tax_id);
}
$item_tax_rate2_total = $this->calcInclusiveLineTax($this->item->tax_rate2, $amount);
@ -384,7 +384,7 @@ class InvoiceItemSumInclusive
$item_tax += $item_tax_rate2_total;
if ($item_tax_rate2_total != 0) {
$this->groupTax($this->item->tax_name2, $this->item->tax_rate2, $item_tax_rate2_total);
$this->groupTax($this->item->tax_name2, $this->item->tax_rate2, $item_tax_rate2_total, $amount, $this->item->tax_id);
}
$item_tax_rate3_total = $this->calcInclusiveLineTax($this->item->tax_rate3, $amount);
@ -392,7 +392,7 @@ class InvoiceItemSumInclusive
$item_tax += $item_tax_rate3_total;
if ($item_tax_rate3_total != 0) {
$this->groupTax($this->item->tax_name3, $this->item->tax_rate3, $item_tax_rate3_total);
$this->groupTax($this->item->tax_name3, $this->item->tax_rate3, $item_tax_rate3_total, $amount, $this->item->tax_id);
}
$this->setTotalTaxes($this->getTotalTaxes() + $item_tax);

View File

@ -240,15 +240,11 @@ class InvoiceSum
public function getRecurringInvoice()
{
// $this->invoice->amount = $this->formatValue($this->getTotal(), $this->precision);
// $this->invoice->total_taxes = $this->getTotalTaxes();
$this->setCalculatedAttributes();
$this->invoice->balance = $this->invoice->amount;
$this->invoice->saveQuietly();
// $this->invoice->saveQuietly();
return $this->invoice;
}
@ -352,12 +348,26 @@ class InvoiceSum
$tax_name = $values->filter(function ($value, $k) use ($key) {
return $value['key'] == $key;
})->pluck('tax_name')->first();
$tax_rate = $values->filter(function ($value, $k) use ($key) {
return $value['key'] == $key;
})->pluck('tax_rate')->first();
$tax_id = $values->filter(function ($value, $k) use ($key) {
return $value['key'] == $key;
})->pluck('tax_id')->first();
$total_line_tax = $values->filter(function ($value, $k) use ($key) {
return $value['key'] == $key;
})->sum('total');
$base_amount = $values->filter(function ($value, $k) use ($key) {
return $value['key'] == $key;
})->sum('base_amount');
$this->tax_map[] = ['name' => $tax_name, 'total' => $total_line_tax];
$tax_id = $values->first()['tax_id'] ?? '';
$this->tax_map[] = ['name' => $tax_name, 'total' => $total_line_tax, 'tax_id' => $tax_id, 'tax_rate' => $tax_rate, 'base_amount' => round($base_amount,2)];
$this->total_taxes += $total_line_tax;
}
@ -426,4 +436,14 @@ class InvoiceSum
return $this;
}
public function getNetSubtotal()
{
return $this->getSubTotal() - $this->getTotalDiscount();
}
public function getSubtotalWithSurcharges()
{
return $this->getSubTotal() + $this->getTotalSurcharges();
}
}

View File

@ -368,19 +368,33 @@ class InvoiceSumInclusive
$values = $this->invoice_items->getGroupedTaxes();
foreach ($keys as $key) {
$tax_name = $values->filter(function ($value, $k) use ($key) {
return $value['key'] == $key;
})->pluck('tax_name')->first();
$tax_rate = $values->filter(function ($value, $k) use ($key) {
return $value['key'] == $key;
})->pluck('tax_rate')->first();
$tax_id = $values->filter(function ($value, $k) use ($key) {
return $value['key'] == $key;
})->pluck('tax_id')->first();
$total_line_tax = $values->filter(function ($value, $k) use ($key) {
return $value['key'] == $key;
})->sum('total');
$base_amount = $values->filter(function ($value, $k) use ($key) {
return $value['key'] == $key;
})->sum('base_amount');
//$total_line_tax -= $this->discount($total_line_tax);
$tax_id = $values->first()['tax_id'] ?? '';
$this->tax_map[] = ['name' => $tax_name, 'total' => $total_line_tax];
$this->tax_map[] = ['name' => $tax_name, 'total' => $total_line_tax, 'tax_id' => $tax_id, 'tax_rate' => $tax_rate, 'base_amount' => round($base_amount,2)];
$this->total_taxes += $total_line_tax;
}
return $this;
@ -401,6 +415,11 @@ class InvoiceSumInclusive
return $this->getTotalTaxes();
}
public function getNetSubtotal()
{
return $this->getSubTotal() - $this->getTotalDiscount();
}
public function purgeTaxes()
{
return $this;

View File

@ -283,6 +283,18 @@ class InvitationController extends Controller
$invoice = $invitation->invoice->service()->removeUnpaidGatewayFees()->save();
if (! $invitation->viewed_date) {
$invitation->markViewed();
if (!session()->get('is_silent')) {
event(new InvitationWasViewed($invitation->invoice, $invitation, $invitation->invoice->company, Ninja::eventVars()));
}
if (!session()->get('is_silent')) {
$this->fireEntityViewedEvent($invitation, $invoice);
}
}
if ($invoice->partial > 0) {
$amount = round($invoice->partial, (int)$invoice->client->currency()->precision);
} else {

View File

@ -11,10 +11,12 @@
namespace App\Http\Controllers;
use App\Http\Requests\EInvoice\ShowQuotaRequest;
use App\Http\Requests\EInvoice\ValidateEInvoiceRequest;
use App\Http\Requests\EInvoice\UpdateEInvoiceConfiguration;
use App\Services\EDocument\Standards\Validation\Peppol\EntityLevel;
use InvoiceNinja\EInvoice\Models\Peppol\BranchType\FinancialInstitutionBranch;
use InvoiceNinja\EInvoice\Models\Peppol\FinancialInstitutionType\FinancialInstitution;
use InvoiceNinja\EInvoice\Models\Peppol\FinancialAccountType\PayeeFinancialAccount;
use InvoiceNinja\EInvoice\Models\Peppol\PaymentMeans;
use InvoiceNinja\EInvoice\Models\Peppol\CardAccountType\CardAccount;
@ -41,55 +43,91 @@ class EInvoiceController extends BaseController
default => $data['passes'] = false,
};
nlog($data);
return response()->json($data, $data['passes'] ? 200 : 400);
}
public function configurations(UpdateEInvoiceConfiguration $request)
{
$einvoice = new \InvoiceNinja\EInvoice\Models\Peppol\Invoice();
$pm = new PaymentMeans();
$pmc = new PaymentMeansCode();
$pmc->value = $request->input('payment_means.code', null);
if($this->input('payment_means.code') == '48')
foreach($request->input('payment_means', []) as $payment_means)
{
$ctc = new CardTypeCode();
$ctc->value = $request->input('payment_means.card_type', null);
$card_account = new CardAccount();
$card_account->HolderName = $request->input('payment_means.cardholder_name', '');
$card_account->CardTypeCode = $ctc;
$pm->CardAccount = $card_account;
}
$pm = new PaymentMeans();
if($this->input('payment_means.iban'))
{
$fib = new FinancialInstitutionBranch();
$bic_id = new ID();
$bic_id->value = $request->input('payment_means.bic', null);
$fib->ID = $bic_id;
$pfa = new PayeeFinancialAccount();
$iban_id = new ID();
$iban_id->value = $request->input('payment_means.iban', null);
$pfa->ID = $iban_id;
$pfa->Name = $request->input('payment_means.account_name', null);
$pfa->FinancialInstitutionBranch = $fib;
$pm->PayeeFinancialAccount = $pfa;
$pmc = new PaymentMeansCode();
$pmc->value = $payment_means['code'];
$pm->PaymentMeansCode = $pmc;
if(in_array($payment_means['code'], ['54,55']))
{
$ctc = new CardTypeCode();
$ctc->value = $payment_means['card_type'];
$card_account = new CardAccount();
$card_account->HolderName = $payment_means['card_holder'];
$card_account->CardTypeCode = $ctc;
$pm->CardAccount = $card_account;
}
if(isset($payment_means['iban']))
{
$fib = new FinancialInstitutionBranch();
$fi = new FinancialInstitution();
$bic_id = new ID();
$bic_id->value = $payment_means['bic_swift'];
$fi->ID = $bic_id;
$fib->FinancialInstitution = $fi;
$pfa = new PayeeFinancialAccount();
$iban_id = new ID();
$iban_id->value = $payment_means['iban'];
$pfa->ID = $iban_id;
$pfa->Name = $payment_means['payer_bank_account'];
$pfa->FinancialInstitutionBranch = $fib;
$pm->PayeeFinancialAccount = $pfa;
}
if(isset($payment_means['information']))
$pm->InstructionNote = $payment_means['information'];
$einvoice->PaymentMeans[] = $pm;
}
$pm->InstructionNote = $request->input('payment_means.information', '');
$einvoice->PaymentMeans[] = $pm;
$stub = new \stdClass();
$stub->Invoice = $einvoice;
$company = auth()->user()->company();
$company->e_invoice = $stub;
$company->save();
}
public function quota(ShowQuotaRequest $request): \Illuminate\Http\Response
{
/**
* @var \App\Models\Company
*/
$company = auth()->user()->company();
$response = \Illuminate\Support\Facades\Http::baseUrl(config('ninja.hosted_ninja_url'))
->withHeaders([
'Content-Type' => 'application/json',
'Accept' => 'application/json',
])
->post('/api/einvoice/quota', data: [
'license_key' => config('ninja.license_key'),
'e_invoicing_token' => $company->account->e_invoicing_token,
'account_key' => $company->account->key,
]);
if ($response->successful()) {
return response($response->body());
}
if ($response->getStatusCode() === 400) {
return response($response->body(), 400);
}
return response()->noContent(500);
}
}

View File

@ -11,6 +11,8 @@
namespace App\Http\Controllers;
use Http;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
use App\Http\Requests\EInvoice\Peppol\StoreEntityRequest;
use App\Services\EDocument\Gateway\Storecove\Storecove;
@ -20,7 +22,7 @@ use App\Http\Requests\EInvoice\Peppol\ShowEntityRequest;
use App\Http\Requests\EInvoice\Peppol\UpdateEntityRequest;
class EInvoicePeppolController extends BaseController
{
{
/**
* Returns the legal entity ID
*
@ -71,16 +73,23 @@ class EInvoicePeppolController extends BaseController
*
*
* @param ShowEntityRequest $request
* @param Storecove $storecove
* @return mixed
* @return JsonResponse
*/
public function show(ShowEntityRequest $request, Storecove $storecove)
public function show(ShowEntityRequest $request): JsonResponse
{
$company = auth()->user()->company();
$response = $storecove->getLegalEntity($company->legal_entity_id);
$response = Http::baseUrl(config('ninja.hosted_ninja_url'))
->withHeaders([
'Content-Type' => 'application/json',
'Accept' => 'application/json',
])
->post('/api/einvoice/peppol/legal_entity', data: [
'legal_entity_id' => $company->legal_entity_id,
'e_invoicing_token' => $company->account->e_invoicing_token,
]);
return response()->json($response, 200);
return response()->json($response->json(), 200);
}
/**
@ -98,18 +107,21 @@ class EInvoicePeppolController extends BaseController
*/
$company = auth()->user()->company();
$legal_entity_response = $storecove->createLegalEntity($request->validated(), $company);
$response = Http::baseUrl(config('ninja.hosted_ninja_url'))
->withHeaders([
'Content-Type' => 'application/json',
'Accept' => 'application/json',
])
->post('/api/einvoice/peppol/setup', data: [
...$request->validated(),
'classification' => $request->classification ?? $company->settings->classification,
'vat_number' => $request->vat_number ?? $company->settings->vat_number,
'id_number' => $request->id_number ?? $company->settings->id_number,
'e_invoicing_token' => $company->account->e_invoicing_token,
]);
$scheme = $storecove->router->resolveRouting($request->country, $company->settings->classification);
$add_identifier_response = $storecove->addIdentifier(
legal_entity_id: $legal_entity_response['id'],
identifier: $company->settings->vat_number,
scheme: $scheme,
);
if ($add_identifier_response) {
$company->legal_entity_id = $legal_entity_response['id'];
if ($response->successful()) {
$company->legal_entity_id = $response->json('legal_entity_id');
$tax_data = $company->tax_data;
@ -117,8 +129,13 @@ class EInvoicePeppolController extends BaseController
$tax_data->acts_as_receiver = $request->acts_as_receiver;
$settings = $company->settings;
$settings->e_invoice_type = 'PEPPOL';
$settings->vat_number = $request->vat_number ?? $company->settings->vat_number;
$settings->id_number = $request->id_number ?? $company->settings->id_number;
$settings->classification = $request->classification ?? $company->settings->classification;
$settings->enable_e_invoice = true;
$company->tax_data = $tax_data;
$company->settings = $settings;
@ -127,11 +144,9 @@ class EInvoicePeppolController extends BaseController
return response()->noContent();
}
// @todo: Improve with proper error.
return response()->noContent(status: 422);
return response()->noContent(status: 500);
}
/**
* Add an additional tax identifier to
* an existing legal entity id
@ -144,7 +159,8 @@ class EInvoicePeppolController extends BaseController
*/
public function addAdditionalTaxIdentifier(AddTaxIdentifierRequest $request, Storecove $storecove): \Illuminate\Http\JsonResponse
{
// @todo: check with dave, since this method has 0 references and it's not being used.
$company = auth()->user()->company();
$tax_data = $company->tax_data;
@ -165,14 +181,29 @@ class EInvoicePeppolController extends BaseController
return response()->json(['message' => 'ok'], 200);
}
public function updateLegalEntity(UpdateEntityRequest $request, Storecove $storecove)
/**
* Update legal properties such as acting as sender or receiver.
*
* @param \App\Http\Requests\EInvoice\Peppol\UpdateEntityRequest $request
* @return JsonResponse|mixed|Response
*/
public function updateLegalEntity(UpdateEntityRequest $request)
{
$company = auth()->user()->company();
$r = $storecove->updateLegalEntity($company->legal_entity_id, $request->validated());
$response = Http::baseUrl(config('ninja.hosted_ninja_url'))
->withHeaders([
'Content-Type' => 'application/json',
'Accept' => 'application/json',
])
->put('/api/einvoice/peppol/update', data: [
...$request->validated(),
'legal_entity_id' => $company->legal_entity_id,
'e_invoicing_token' => $company->account->e_invoicing_token,
]);
if ($r->successful()) {
if ($response->successful()) {
$tax_data = $company->tax_data;
$tax_data->acts_as_sender = $request->acts_as_sender;
@ -184,7 +215,7 @@ class EInvoicePeppolController extends BaseController
return response()->noContent();
}
return response()->json(['message' => 'Error updating identifier'], 422);
}
@ -192,21 +223,27 @@ class EInvoicePeppolController extends BaseController
* Removed the legal identity from the Peppol network
*
* @param DisconnectRequest $request
* @param Storecove $storecove
* @return \Illuminate\Http\Response
*/
public function disconnect(DisconnectRequest $request, Storecove $storecove): \Illuminate\Http\Response
public function disconnect(DisconnectRequest $request): \Illuminate\Http\Response
{
/**
* @var \App\Models\Company $company
*/
$company = auth()->user()->company();
$response = $storecove->deleteIdentifier(
legal_entity_id: $company->legal_entity_id,
);
$response = Http::baseUrl(config('ninja.hosted_ninja_url'))
->withHeaders([
'Content-Type' => 'application/json',
'Accept' => 'application/json',
])
->post('/api/einvoice/peppol/disconnect', data: [
'company_key' => $company->company_key,
'legal_entity_id' => $company->legal_entity_id,
'e_invoicing_token' => $company->account->e_invoicing_token,
]);
if ($response) {
if ($response->successful()) {
$company->legal_entity_id = null;
$company->tax_data = $this->unsetVatNumbers($company->tax_data);
@ -218,12 +255,9 @@ class EInvoicePeppolController extends BaseController
$company->save();
return response()->noContent();
}
// @todo: Improve with proper error.
return response()->noContent(status: 422);
return response()->noContent(status: 500);
}
private function unsetVatNumbers(mixed $taxData): mixed

View File

@ -15,17 +15,33 @@ namespace App\Http\Controllers;
use App\Http\Controllers\BaseController;
use App\Http\Requests\EInvoice\UpdateTokenRequest;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Http;
class EInvoiceTokenController extends BaseController
{
public function __invoke(UpdateTokenRequest $request): Response
{
/** @var \App\Models\Company $company */
$company = auth()->user()->company();
/** @var \App\Models\User $user */
$user = auth()->user();
// $company->e_invoicing_token = $request->get('token');
$company->save();
$response = Http::baseUrl(config('ninja.hosted_ninja_url'))
->withHeaders([
'Content-Type' => 'application/json',
'Accept' => 'application/json',
])
->post('/api/einvoice/tokens/rotate', data: [
'license' => config('ninja.license_key'),
'account_key' => $user->account->key,
]);
return response()->noContent();
if ($response->successful()) {
$user->account->update([
'e_invoicing_token' => $response->json('token'),
]);
return response()->noContent();
}
return response()->noContent(status: 422);
}
}

View File

@ -11,9 +11,11 @@
namespace App\Http\Controllers;
use App\Http\Requests\License\CheckRequest;
use App\Models\Account;
use App\Utils\CurlUtils;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Http;
use stdClass;
@ -224,4 +226,26 @@ class LicenseController extends BaseController
$account->save();
}
}
public function check(CheckRequest $request): Response|JsonResponse
{
if (! config('ninja.license_key')) {
return response()->json(['message' => 'License not found. Make sure to update LICENSE_KEY in .env!'], status: 422);
}
$response = Http::baseUrl(config('ninja.hosted_ninja_url'))
->withHeaders([
'Content-Type' => 'application/json',
'Accept' => 'application/json',
])
->post('/api/check', data: [
'license' => config('ninja.license_key'),
]);
if ($response->successful()) {
return response()->json($response->json());
}
return response()->json(['message' => 'Invalid license'], status: 422);
}
}

View File

@ -517,7 +517,7 @@ class QuoteController extends BaseController
{
/** @var \App\Models\User $user */
$user = auth()->user();
nlog("booop");
$action = $request->input('action');
$ids = $request->input('ids');

View File

@ -81,7 +81,7 @@ class SearchController extends Controller
],
],
],
'size' => 1000,
'size' => 100,
],
];

View File

@ -273,14 +273,16 @@ class TaskController extends BaseController
return $request->disallowUpdate();
}
$old_task = json_decode(json_encode($task));
$old_task_status_order = $task->status_order;
// $old_task = json_decode(json_encode($task));
$task = $this->task_repo->save($request->all(), $task);
$task = $this->task_repo->triggeredActions($request, $task);
if ($task->status_order != $old_task->status_order) {
$this->task_repo->sortStatuses($old_task, $task);
if ($task->status_order != $old_task_status_order) {
// if ($task->status_order != $old_task->status_order) {
$this->task_repo->sortStatuses($task);
}
event(new TaskWasUpdated($task, $task->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));

View File

@ -20,8 +20,7 @@ use Illuminate\Validation\Rule;
class StoreEntityRequest extends FormRequest
{
private array $vat_regex_patterns = [
public static array $vat_regex_patterns = [
'DE' => '/^DE\d{9}$/',
'AT' => '/^ATU\d{8}$/',
'BE' => '/^BE0\d{9}$/',
@ -62,7 +61,7 @@ class StoreEntityRequest extends FormRequest
return true;
}
return $user->account->isPaid() && $user->isAdmin() &&
return $user->account->isPaid() && $user->isAdmin() &&
$user->company()->legal_entity_id === null;
}
@ -76,12 +75,15 @@ class StoreEntityRequest extends FormRequest
'line1' => ['required', 'string'],
'line2' => ['nullable', 'string'],
'city' => ['required', 'string'],
'country' => ['required', 'bail', Rule::in(array_keys($this->vat_regex_patterns))],
'country' => ['required', 'bail', Rule::in(array_keys(self::$vat_regex_patterns))],
'zip' => ['required', 'string'],
'county' => ['required', 'string'],
'acts_as_receiver' => ['required', 'bool'],
'acts_as_sender' => ['required', 'bool'],
'tenant_id' => ['required'],
'classification' => ['required', 'string'],
'vat_number' => [Rule::requiredIf(fn() => $this->input('classification') !== 'individual')],
'id_number' => [Rule::requiredIf(fn() => $this->input('classification') === 'individual')],
];
}
@ -96,7 +98,7 @@ class StoreEntityRequest extends FormRequest
{
$input = $this->all();
if(isset($input['country'])) {
if (isset($input['country'])) {
$country = $this->country();
$input['country'] = $country->iso_3166_2;
}
@ -104,19 +106,18 @@ class StoreEntityRequest extends FormRequest
$input['acts_as_receiver'] = $input['acts_as_receiver'] ?? true;
$input['acts_as_sender'] = $input['acts_as_sender'] ?? true;
$this->replace($input);
$input['classification'] = $input['classification'] ?? 'individual';
$this->replace($input);
}
public function country(): Country
{
/** @var \Illuminate\Support\Collection<\App\Models\Country> */
$countries = app('countries');
return $countries->first(function ($c){
return $countries->first(function ($c) {
return $this->country == $c->id;
});
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Http\Requests\EInvoice;
use Illuminate\Foundation\Http\FormRequest;
class ShowQuotaRequest extends FormRequest
{
public function authorize(): bool
{
if (app()->isLocal()) {
return true;
}
/** @var \App\Models\User $user */
$user = auth()->user();
return \App\Utils\Ninja::isSelfHost() && $user->account->isPaid();
}
public function rules(): array
{
return [
//
];
}
}

View File

@ -37,41 +37,72 @@ class UpdateEInvoiceConfiguration extends Request
public function rules()
{
/** @var \App\Models\User $user */
$user = auth()->user();
return [
'entity' => 'required|bail|in:invoice,client,company',
'payment_means' => 'sometimes|bail|array',
'payment_means.code' => ['required_with:payment_means', 'bail', Rule::in(PaymentMeans::getPaymentMeansCodelist())],
'payment_means.bic' => ['bail',
Rule::requiredIf(function () {
return in_array($this->input('payment_means.code'), ['58', '59', '49', '42', '30']);
}),
],
'payment_means.iban' => ['bail', 'string', 'min:8', 'max:11',
Rule::requiredIf(function () {
return in_array($this->input('payment_means.code'), ['58', '59', '49', '42', '30']);
}),
],
'payment_means.account_name' => ['bail', 'string', 'min:15', 'max:34',
Rule::requiredIf(function () {
return in_array($this->input('payment_means.code'), ['58', '59', '49', '42', '30']);
}),
],
'payment_means.information' => ['bail', 'sometimes', 'string'],
'payment_means.card_type' => ['bail', 'string', 'min:4',
Rule::requiredIf(function () {
return in_array($this->input('payment_means.code'), ['48']);
}),
],
'payment_means.cardholder_name' => ['bail','string', 'min:4',
Rule::requiredIf(function () {
return in_array($this->input('payment_means.code'), ['48']);
}),
],
];
'entity' => 'required|bail|in:invoice,client,company',
'payment_means' => 'sometimes|bail|array',
'payment_means.*.code' => ['required_with:payment_means', 'bail', Rule::in(PaymentMeans::getPaymentMeansCodelist())],
'payment_means.*.bic_swift' => Rule::forEach(function (string|null $value, string $attribute) {
$index = explode('.', $attribute)[1];
$code = $this->input("payment_means.{$index}.code");
$requirements = PaymentMeans::$payment_means_requirements_codes[$code] ?? [];
$rules = ['bail', 'string', 'min:8', 'max:11'];
if (in_array('bic_swift', $requirements)) {
return [...$rules, 'required'];
}
return [...$rules, 'nullable'];
}),
'payment_means.*.iban' => Rule::forEach(function (string|null $value, string $attribute) {
$index = explode('.', $attribute)[1];
$code = $this->input("payment_means.{$index}.code");
$requirements = PaymentMeans::$payment_means_requirements_codes[$code] ?? [];
$rules = ['bail', 'sometimes', 'string', 'min:15', 'max:34'];
if (in_array('iban', $requirements)) {
return [...$rules, 'required'];
}
return [...$rules, 'nullable'];
}),
'payment_means.*.payer_bank_account' => Rule::forEach(function (string|null $value, string $attribute) {
$index = explode('.', $attribute)[1];
$code = $this->input("payment_means.{$index}.code");
$requirements = PaymentMeans::$payment_means_requirements_codes[$code] ?? [];
$rules = ['bail', 'sometimes', 'string', 'max:255'];
if (in_array('payer_bank_account', $requirements)) {
return [...$rules, 'required'];
}
return [...$rules, 'nullable'];
}),
'payment_means.*.information' => ['bail', 'sometimes', 'nullable', 'string'],
'payment_means.*.card_type' => Rule::forEach(function (string|null $value, string $attribute) {
$index = explode('.', $attribute)[1];
$code = $this->input("payment_means.{$index}.code");
$requirements = PaymentMeans::$payment_means_requirements_codes[$code] ?? [];
$rules = ['bail', 'sometimes', 'nullable', 'string', 'min:4'];
if (in_array('card_type', $requirements)) {
return [...$rules, 'required'];
}
return [...$rules, 'nullable'];
}),
'payment_means.*.card_holder' => Rule::forEach(function (string|null $value, string $attribute) {
$index = explode('.', $attribute)[1];
$code = $this->input("payment_means.{$index}.code");
$requirements = PaymentMeans::$payment_means_requirements_codes[$code] ?? [];
$rules = ['bail', 'sometimes', 'nullable', 'string', 'min:4'];
if (in_array('card_holder', $requirements)) {
return [...$rules, 'required'];
}
return [...$rules, 'nullable'];
}),
];
}
public function prepareForValidation()

View File

@ -36,9 +36,7 @@ class UpdateTokenRequest extends Request
public function rules(): array
{
return [
'token' => 'required',
];
return [];
}
protected function failedAuthorization(): void

View File

@ -0,0 +1,33 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\License;
use App\Utils\Ninja;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Foundation\Http\FormRequest;
class CheckRequest extends FormRequest
{
public function authorize(): bool
{
return Ninja::isSelfHost() && auth()->check();
}
/**
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [];
}
}

View File

@ -1,74 +0,0 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\ValidationRules\Invoice;
use App\Models\Invoice;
use Illuminate\Contracts\Validation\Rule;
use Illuminate\Support\Facades\DB;
/**
* Class LockedInvoiceRule.
*/
class InvoiceBalanceSanity implements Rule
{
public $invoice;
public $input;
private $message;
public function __construct(Invoice $invoice, $input)
{
$this->invoice = $invoice;
$this->input = $input;
}
/**
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
return $this->checkIfInvoiceBalanceIsSane(); //if it exists, return false!
}
/**
* @return string
*/
public function message()
{
return $this->message;
}
/**
* @return bool
*/
private function checkIfInvoiceBalanceIsSane(): bool
{
DB::connection(config('database.default'))->beginTransaction();
$this->invoice = Invoice::on(config('database.default'))->withTrashed()->find($this->invoice->id);
$this->invoice->line_items = $this->input['line_items'];
$temp_invoice = $this->invoice->calc()->getTempEntity();
DB::connection(config('database.default'))->rollBack();
if ($temp_invoice->balance < 0) {
$this->message = 'Invoice balance cannot go negative';
return false;
}
return true;
}
}

View File

@ -67,8 +67,6 @@ class ValidInvoicesRules implements Rule
/////
$inv = $inv_collection->firstWhere('id', $invoice['invoice_id']);
// $inv = Invoice::withTrashed()->whereId($invoice['invoice_id'])->first();
if (! $inv) {
$this->error_msg = ctrans('texts.invoice_not_found');

View File

@ -159,10 +159,6 @@ class InvoicePay extends Component
$this->setContext('fields', $fields); // $this->context['fields'] = $fields;
if ($company_gateway->always_show_required_fields) {
return $this->required_fields = true;
}
/** @var \App\Models\ClientContact $contact */
$contact = $this->getContext()['contact'];
@ -184,6 +180,10 @@ class InvoicePay extends Component
}
}
}
if ($company_gateway->always_show_required_fields) {
return $this->required_fields = true;
}
return $this->required_fields = false;

View File

@ -96,13 +96,16 @@ class RequiredFields extends Component
/** @var \App\Models\ClientContact $contact */
$rff->check($contact);
if ($rff->unfilled_fields === 0) {
if ($rff->unfilled_fields === 0 && !$this->company_gateway->always_show_required_fields)
$this->dispatch('required-fields');
}
if ($rff->unfilled_fields > 0) {
else
$this->is_loading = false;
}
// }
// if ($rff->unfilled_fields > 0) {
// $this->is_loading = false;
// }
}
public function handleSubmit(array $data)

View File

@ -128,6 +128,7 @@ class Account extends BaseModel
'platform',
'set_react_as_default_ap',
'inapp_transaction_id',
'e_invoicing_token',
];
protected $casts = [

View File

@ -0,0 +1,27 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class EInvoicingToken extends Model
{
protected $fillable = [
'license',
'token',
'account_key',
];
}

View File

@ -154,6 +154,11 @@ class CompanyPresenter extends EntityPresenter
}
}
public function phone()
{
return $this->entity->settings->phone ?? ' ';
}
public function address1()
{
return $this->entity->settings->address1;

View File

@ -77,6 +77,7 @@ class Product extends BaseModel
public const PRODUCT_TYPE_OVERRIDE_TAX = 7;
public const PRODUCT_TYPE_ZERO_RATED = 8;
public const PRODUCT_TYPE_REVERSE_TAX = 9;
public const PRODUCT_INTRA_COMMUNITY = 10;
protected $fillable = [
'custom_value1',

View File

@ -212,11 +212,10 @@ class TaskRepository extends BaseRepository
/**
* Sorts the task status order IF the old status has changed between requests
*
* @param \stdCLass $old_task The old task object
* @param Task $new_task The new Task model
* @return void
*/
public function sortStatuses($old_task, $new_task)
public function sortStatuses($new_task)
{
if (! $new_task->project()->exists()) {
return;

View File

@ -102,6 +102,365 @@ class PaymentMeans implements PaymentMeansInterface
'ZZZ' => 'Mutually defined',
];
public static array $payment_means_requirements = [
'1' => [], // Instrument not defined
'2' => [
'PayeeFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // ACH credit
'3' => [
'PaymentMandate.PayerFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // ACH debit
'4' => [
'PaymentMandate.PayerFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // ACH demand debit reversal
'5' => [
'PayeeFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // ACH demand credit reversal
'6' => [
'PayeeFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // ACH demand credit
'7' => [
'PaymentMandate.PayerFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // ACH demand debit
'8' => [], // Hold
'9' => [
'PayeeFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // National or regional clearing
'10' => [], // In cash
'11' => [
'PayeeFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // ACH savings credit reversal
'12' => [
'PaymentMandate.PayerFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // ACH savings debit reversal
'13' => [
'PayeeFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // ACH savings credit
'14' => [
'PaymentMandate.PayerFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // ACH savings debit
'15' => [
'PayeeFinancialAccount.Name',
'PayeeFinancialAccount.ProprietaryID'
], // Bookentry credit
'16' => [
'PayeeFinancialAccount.Name',
'PayeeFinancialAccount.ProprietaryID'
], // Bookentry debit
'17' => [
'PayeeFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // ACH demand CCD credit
'18' => [
'PaymentMandate.PayerFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // ACH demand CCD debit
'19' => [
'PayeeFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // ACH demand CTP credit
'20' => [], // Cheque
'21' => [], // Banker's draft
'22' => [], // Certified banker's draft
'23' => [], // Bank cheque
'24' => [], // Bill of exchange awaiting acceptance
'25' => [], // Certified cheque
'26' => [], // Local cheque
'27' => [
'PaymentMandate.PayerFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // ACH demand CTP debit
'28' => [
'PayeeFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // ACH demand CTX credit
'29' => [
'PaymentMandate.PayerFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // ACH demand CTX debit
'30' => [
'PayeeFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // Credit transfer
'31' => [
'PayeeFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // Debit transfer
'32' => [
'PayeeFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // ACH demand CCD+ credit
'33' => [
'PaymentMandate.PayerFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // ACH demand CCD+ debit
'34' => [
'PayeeFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // ACH PPD
'35' => [
'PayeeFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // ACH savings CCD credit
'36' => [
'PaymentMandate.PayerFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // ACH savings CCD debit
'37' => [
'PayeeFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // ACH savings CTP credit
'38' => [
'PaymentMandate.PayerFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // ACH savings CTP debit
'39' => [
'PayeeFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // ACH savings CTX credit
'40' => [
'PaymentMandate.PayerFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // ACH savings CTX debit
'41' => [
'PayeeFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // ACH savings CCD+ credit
'42' => [
'PayeeFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // Payment to bank account
'43' => [
'PaymentMandate.PayerFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // ACH savings CCD+ debit
'44' => [], // Accepted bill of exchange
'45' => [
'PayeeFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // Referenced home-banking credit transfer
'46' => [
'PayeeFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // Interbank debit transfer
'47' => [
'PayeeFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // Home-banking debit transfer
'48' => [
'PayeeFinancialAccount.NetworkID',
'PayeeFinancialAccount.ID'
], // Bank card
'49' => [
'PaymentMandate.PayerFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // Direct debit
'50' => [
'PayeeFinancialAccount.Name'
], // Payment by postgiro
'51' => [
'PayeeFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // FR, norme 6 97-Telereglement CFONB
'52' => [
'PayeeFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // Urgent commercial payment
'53' => [
'PayeeFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // Urgent Treasury Payment
'54' => [
'CardAccount.NetworkID',
'CardAccount.PrimaryAccountNumberID',
'CardAccount.HolderName'
], // Credit card
'55' => [
'CardAccount.NetworkID',
'CardAccount.PrimaryAccountNumberID',
'CardAccount.HolderName'
], // Debit card
'56' => [
'PayeeFinancialAccount.Name'
], // Bankgiro
'57' => [
'PayeeFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // Standing agreement
'58' => [
'PayeeFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // SEPA credit transfer
'59' => [
'PaymentMandate.PayerFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // SEPA direct debit
'60' => [], // Promissory note
'61' => [], // Promissory note signed by debtor
'62' => [
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // Promissory note signed by debtor and endorsed by bank
'63' => [], // Promissory note signed by debtor and endorsed
'64' => [
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // Promissory note signed by bank
'65' => [
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // Promissory note signed by bank and endorsed by another
'66' => [], // Promissory note signed by third party
'67' => [
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // Promissory note signed by third party and endorsed by bank
'68' => [
'PayeeFinancialAccount.ID'
], // Online payment service
'69' => [
'PayeeFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // Transfer Advice
'70' => [], // Bill drawn by creditor on debtor
'74' => [
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // Bill drawn by creditor on bank
'75' => [
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // Bill drawn by creditor, endorsed by bank
'76' => [
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // Bill drawn by creditor on bank and endorsed
'77' => [], // Bill drawn by creditor on third party
'78' => [], // Bill drawn by creditor on third party, accepted
'91' => [], // Not transferable banker's draft
'92' => [], // Not transferable local cheque
'93' => [
'PayeeFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // Reference giro
'94' => [
'PayeeFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // Urgent giro
'95' => [
'PayeeFinancialAccount.ID',
'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID'
], // Free format giro
'96' => [], // Requested method not used
'97' => [
'PayeeFinancialAccount.Name'
], // Clearing between partners
'ZZZ' => [], // Mutually defined
];
public static array $payment_means_requirements_codes = [
'1' => [], // Instrument not defined
'2' => ['iban', 'bic_swift'], // ACH credit
'3' => ['payer_bank_account', 'iban', 'bic_swift'], // ACH debit
'4' => ['payer_bank_account', 'iban', 'bic_swift'], // ACH demand debit reversal
'5' => ['iban', 'bic_swift'], // ACH demand credit reversal
'6' => ['iban', 'bic_swift'], // ACH demand credit
'7' => ['payer_bank_account', 'iban', 'bic_swift'], // ACH demand debit
'8' => [], // Hold
'9' => ['iban', 'bic_swift'], // National or regional clearing
'10' => [], // In cash
'11' => ['iban', 'bic_swift'], // ACH savings credit reversal
'12' => ['payer_bank_account', 'iban', 'bic_swift'], // ACH savings debit reversal
'13' => ['iban', 'bic_swift'], // ACH savings credit
'14' => ['payer_bank_account', 'iban', 'bic_swift'], // ACH savings debit
'15' => ['account_holder', 'bsb_sort'], // Bookentry credit
'16' => ['account_holder', 'bsb_sort'], // Bookentry debit
'17' => ['iban', 'bic_swift'], // ACH demand CCD credit
'18' => ['payer_bank_account', 'iban', 'bic_swift'], // ACH demand CCD debit
'19' => ['iban', 'bic_swift'], // ACH demand CTP credit
'20' => [], // Cheque
'21' => [], // Banker's draft
'22' => [], // Certified banker's draft
'23' => [], // Bank cheque
'24' => [], // Bill of exchange awaiting acceptance
'25' => [], // Certified cheque
'26' => [], // Local cheque
'27' => ['payer_bank_account', 'iban', 'bic_swift'], // ACH demand CTP debit
'28' => ['iban', 'bic_swift'], // ACH demand CTX credit
'29' => ['payer_bank_account', 'iban', 'bic_swift'], // ACH demand CTX debit
'30' => ['iban', 'bic_swift'], // Credit transfer
'31' => ['iban', 'bic_swift'], // Debit transfer
'32' => ['iban', 'bic_swift'], // ACH demand CCD+ credit
'33' => ['payer_bank_account', 'iban', 'bic_swift'], // ACH demand CCD+ debit
'34' => ['iban', 'bic_swift'], // ACH PPD
'35' => ['iban', 'bic_swift'], // ACH savings CCD credit
'36' => ['payer_bank_account', 'iban', 'bic_swift'], // ACH savings CCD debit
'37' => ['iban', 'bic_swift'], // ACH savings CTP credit
'38' => ['payer_bank_account', 'iban', 'bic_swift'], // ACH savings CTP debit
'39' => ['iban', 'bic_swift'], // ACH savings CTX credit
'40' => ['payer_bank_account', 'iban', 'bic_swift'], // ACH savings CTX debit
'41' => ['iban', 'bic_swift'], // ACH savings CCD+ credit
'42' => ['iban', 'bic_swift'], // Payment to bank account
'43' => ['payer_bank_account', 'iban', 'bic_swift'], // ACH savings CCD+ debit
'44' => [], // Accepted bill of exchange
'45' => ['iban', 'bic_swift'], // Referenced home-banking credit transfer
'46' => ['iban', 'bic_swift'], // Interbank debit transfer
'47' => ['iban', 'bic_swift'], // Home-banking debit transfer
'48' => ['card_type', 'card_number'], // Bank card
'49' => ['payer_bank_account', 'iban', 'bic_swift'], // Direct debit
'50' => ['account_holder'], // Payment by postgiro
'51' => ['iban', 'bic_swift'], // FR, norme 6 97-Telereglement CFONB
'52' => ['iban', 'bic_swift'], // Urgent commercial payment
'53' => ['iban', 'bic_swift'], // Urgent Treasury Payment
'54' => ['card_type', 'card_number', 'card_holder'], // Credit card
'55' => ['card_type', 'card_number', 'card_holder'], // Debit card
'56' => ['account_holder'], // Bankgiro
'57' => ['iban', 'bic_swift'], // Standing agreement
'58' => ['iban', 'bic_swift'], // SEPA credit transfer
'59' => ['payer_bank_account', 'iban', 'bic_swift'], // SEPA direct debit
'60' => [], // Promissory note
'61' => [], // Promissory note signed by debtor
'62' => ['bic_swift'], // Promissory note signed by debtor and endorsed by bank
'63' => [], // Promissory note signed by debtor and endorsed
'64' => ['bic_swift'], // Promissory note signed by bank
'65' => ['bic_swift'], // Promissory note signed by bank and endorsed by another
'66' => [], // Promissory note signed by third party
'67' => ['bic_swift'], // Promissory note signed by third party and endorsed by bank
'68' => ['iban'], // Online payment service
'69' => ['iban', 'bic_swift'], // Transfer Advice
'70' => [], // Bill drawn by creditor on debtor
'74' => ['bic_swift'], // Bill drawn by creditor on bank
'75' => ['bic_swift'], // Bill drawn by creditor, endorsed by bank
'76' => ['bic_swift'], // Bill drawn by creditor on bank and endorsed
'77' => [], // Bill drawn by creditor on third party
'78' => [], // Bill drawn by creditor on third party, accepted
'91' => [], // Not transferable banker's draft
'92' => [], // Not transferable local cheque
'93' => ['iban', 'bic_swift'], // Reference giro
'94' => ['iban', 'bic_swift'], // Urgent giro
'95' => ['iban', 'bic_swift'], // Free format giro
'96' => [], // Requested method not used
'97' => ['account_holder'], // Clearing between partners
'ZZZ' => [], // Mutually defined
];
public static array $payment_means_field_map = [
'iban' => 'PayeeFinancialAccount.ID',
'bic_swift' => 'PayeeFinancialAccount.FinancialInstitutionBranch.FinancialInstitution.ID',
'payer_bank_account' => 'PaymentMandate.PayerFinancialAccount.ID',
'account_holder' => 'PayeeFinancialAccount.Name',
'bsb_sort' => 'PayeeFinancialAccount.ProprietaryID',
'card_type' => 'CardAccount.NetworkID',
'card_number' => 'CardAccount.PrimaryAccountNumberID',
'card_holder' => 'CardAccount.HolderName'
];
public string $code = '1';
public ?string $information = null;
@ -197,6 +556,6 @@ class PaymentMeans implements PaymentMeansInterface
public static function getPaymentMeansCodelist()
{
return array_keys(self::$payment_means_codelist);
return array_keys(self::$payment_means_requirements_codes);
}
}

View File

@ -50,4 +50,10 @@ class AccountingCustomerParty
$this->public_identifiers = $public_identifiers;
return $this;
}
public function addPublicIdentifiers($public_identifier): self
{
$this->public_identifiers[] = $public_identifier;
return $this;
}
}

View File

@ -51,4 +51,10 @@ class AccountingSupplierParty
$this->public_identifiers = $public_identifiers;
return $this;
}
public function addPublicIdentifiers($public_identifier): self
{
$this->public_identifiers[] = $public_identifier;
return $this;
}
}

View File

@ -2,25 +2,28 @@
namespace App\Services\EDocument\Gateway\Storecove\Models;
use Symfony\Component\Serializer\Attribute\Context;
use Symfony\Component\Serializer\Attribute\SerializedName;
use Symfony\Component\Serializer\Attribute\SerializedPath;
class AllowanceCharges
{
// #[SerializedPath('[cbc:Amount][#]')]
public ?float $amount_excluding_vat;
#[Context(['input_format' => 'float'])]
#[SerializedPath('[cbc:Amount][#]')]
public ?string $amount_excluding_vat;
public ?float $amount_excluding_tax;
#[Context(['input_format' => 'float'])]
#[SerializedPath('[cbc:BaseAmount][#]')]
public ?float $base_amount_excluding_tax;
// #[SerializedPath('[cbc:Amount][#]')]
public ?float $amount_including_tax;
// #[SerializedPath('[cbc:BaseAmount][#]')]
public ?string $amount_excluding_tax;
#[SerializedPath('[cbc:BaseAmount][#]')]
public ?string $base_amount_excluding_tax;
#[SerializedPath('[cbc:Amount][@currencyID]')]
public ?string $amount_including_tax;
#[SerializedPath('[cbc:BaseAmount][@currencyID]')]
public ?string $base_amount_including_tax;
public ?float $base_amount_including_tax;
// #[SerializedPath('[cac:TaxCategory]')]
// public ?Tax $tax;
@ -39,11 +42,11 @@ class AllowanceCharges
* @param TaxesDutiesFees[] $taxes_duties_fees
*/
public function __construct(
?string $amount_excluding_vat,
?string $amount_excluding_tax,
?string $base_amount_excluding_tax,
?string $amount_including_tax,
?string $base_amount_including_tax,
?float $amount_excluding_vat,
?float $amount_excluding_tax,
?float $base_amount_excluding_tax,
?float $amount_including_tax,
?float $base_amount_including_tax,
// ?Tax $tax,
?array $taxes_duties_fees,
?string $reason,
@ -60,27 +63,27 @@ class AllowanceCharges
$this->reason_code = $reason_code;
}
public function getAmountExcludingVat(): ?string
public function getAmountExcludingVat(): ?float
{
return $this->amount_excluding_vat;
}
public function getAmountExcludingTax(): ?string
public function getAmountExcludingTax(): ?float
{
return $this->amount_excluding_tax;
}
public function getBaseAmountExcludingTax(): ?string
public function getBaseAmountExcludingTax(): ?float
{
return $this->base_amount_excluding_tax;
}
public function getAmountIncludingTax(): ?string
public function getAmountIncludingTax(): ?float
{
return $this->amount_including_tax;
}
public function getBaseAmountIncludingTax(): ?string
public function getBaseAmountIncludingTax(): ?float
{
return $this->base_amount_including_tax;
}
@ -103,31 +106,31 @@ class AllowanceCharges
return $this->reason_code;
}
public function setAmountExcludingVat(?string $amount_excluding_vat): self
public function setAmountExcludingVat(?float $amount_excluding_vat): self
{
$this->amount_excluding_vat = $amount_excluding_vat;
return $this;
}
public function setAmountExcludingTax(?string $amount_excluding_tax): self
public function setAmountExcludingTax(?float $amount_excluding_tax): self
{
$this->amount_excluding_tax = $amount_excluding_tax;
return $this;
}
public function setBaseAmountExcludingTax(?string $base_amount_excluding_tax): self
public function setBaseAmountExcludingTax(?float $base_amount_excluding_tax): self
{
$this->base_amount_excluding_tax = $base_amount_excluding_tax;
return $this;
}
public function setAmountIncludingTax(?string $amount_including_tax): self
public function setAmountIncludingTax(?float $amount_including_tax): self
{
$this->amount_including_tax = $amount_including_tax;
return $this;
}
public function setBaseAmountIncludingTax(?string $base_amount_including_tax): self
public function setBaseAmountIncludingTax(?float $base_amount_including_tax): self
{
$this->base_amount_including_tax = $base_amount_including_tax;
return $this;

View File

@ -40,7 +40,7 @@ class Invoice
#[SerializedPath('[cac:AllowanceCharge]')]
/** @var AllowanceCharges[] */
public array $allowance_charges;
public ?array $allowance_charges = [];
//this is an experimental prop
// #[SerializedPath('[cac:LegalMonetaryTotal][cbc:TaxInclusiveAmount][#]')]
@ -67,7 +67,7 @@ class Invoice
// /** @var ?\DateTime */
#[SerializedPath('[cbc:DueDate]')]
// #[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
public $due_date;
public ?string $due_date;
//may need something custom for this
public ?string $invoice_period;
@ -166,7 +166,7 @@ class Invoice
public function __construct(
?string $invoice_number,
$issue_date,
?string $issue_date,
?AccountingCustomerParty $accounting_customer_party,
?array $invoice_lines,
?string $accounting_cost,
@ -176,14 +176,14 @@ class Invoice
?string $accounting_currency_tax_amount_currency,
?AccountingSupplierParty $accounting_supplier_party,
?array $allowance_charges,
?string $amount_including_tax,
?string $amount_including_vat,
?float $amount_including_tax,
?float $amount_including_vat,
?array $attachments,
?bool $consumer_tax_mode,
?Delivery $delivery,
?DeliveryTerms $delivery_terms,
?string $document_currency_code,
$due_date,
?string $due_date,
?string $invoice_period,
?array $issue_reasons,
?string $issue_time,
@ -335,12 +335,12 @@ class Invoice
return $this->allowance_charges;
}
public function getAmountIncludingTax(): ?string
public function getAmountIncludingTax(): ?float
{
return $this->amount_including_tax;
}
public function getAmountIncludingVat(): ?string
public function getAmountIncludingVat(): ?float
{
return $this->amount_including_vat;
}
@ -638,13 +638,13 @@ class Invoice
return $this;
}
public function setAmountIncludingTax(?string $amount_including_tax): self
public function setAmountIncludingTax(?float $amount_including_tax): self
{
$this->amount_including_tax = $amount_including_tax;
return $this;
}
public function setAmountIncludingVat(?string $amount_including_vat): self
public function setAmountIncludingVat(?float $amount_including_vat): self
{
$this->amount_including_vat = $amount_including_vat;
return $this;
@ -683,7 +683,7 @@ class Invoice
return $this;
}
public function setDueDate($due_date): self
public function setDueDate(?string $due_date): self
{
$this->due_date = $due_date;
return $this;

View File

@ -38,20 +38,20 @@ class InvoiceLines
#[SerializedPath('[cac:AllowanceCharge]')]
/** @var AllowanceCharges[] */ //todo
public ?array $charges;
public ?array $allowance_charges;
#[SerializedPath('[cbc:LineExtensionAmount][#]')]
public ?string $amount_excluding_vat;
public ?float $amount_excluding_vat;
#[SerializedPath('[cbc:TaxExclusiveAmount][#]')]
public ?string $amount_excluding_tax;
#[SerializedPath('[cac:Price][cbc:PriceAmount][value]')]
public ?float $amount_excluding_tax;
#[SerializedPath('[cbc:TaxInclusiveAmount][#]')]
public ?string $amount_including_tax;
public ?float $amount_including_tax;
#[SerializedPath('[cac:Item][cac:ClassifiedTaxCategory]')]
/** @var TaxesDutiesFees[] */
public array $taxes_duties_fees;
public ?array $taxes_duties_fees = [];
#[SerializedPath('[cbc:AccountingCost]')]
public ?string $accounting_cost;
@ -83,7 +83,7 @@ class InvoiceLines
public ?string $note;
/**
* @param AllowanceCharges[] $charges
* @param AllowanceCharges[] $allowance_charges
* @param TaxesDutiesFees[] $taxes_duties_fees
* @param References[] $references
* @param AdditionalItemProperties[] $additional_item_properties
@ -98,10 +98,10 @@ class InvoiceLines
?float $quantity,
?float $base_quantity,
?string $quantity_unit_code,
?array $charges,
?string $amount_excluding_vat,
?string $amount_excluding_tax,
?string $amount_including_tax,
?array $allowance_charges,
?float $amount_excluding_vat,
?float $amount_excluding_tax,
?float $amount_including_tax,
?array $taxes_duties_fees,
?string $accounting_cost,
?array $references,
@ -122,7 +122,7 @@ class InvoiceLines
$this->quantity = $quantity;
$this->base_quantity = $base_quantity;
$this->quantity_unit_code = $quantity_unit_code;
$this->charges = $charges;
$this->allowance_charges = $allowance_charges;
$this->amount_excluding_vat = $amount_excluding_vat;
$this->amount_excluding_tax = $amount_excluding_tax;
$this->amount_including_tax = $amount_including_tax;
@ -188,20 +188,20 @@ class InvoiceLines
*/
public function getAllowanceCharges(): ?array
{
return $this->charges;
return $this->allowance_charges;
}
public function getAmountExcludingVat(): ?string
public function getAmountExcludingVat(): ?float
{
return $this->amount_excluding_vat;
}
public function getAmountExcludingTax(): ?string
public function getAmountExcludingTax(): ?float
{
return $this->amount_excluding_tax;
}
public function getAmountIncludingTax(): ?string
public function getAmountIncludingTax(): ?float
{
return $this->amount_including_tax;
}
@ -320,27 +320,27 @@ class InvoiceLines
}
/**
* @param AllowanceCharges[] $charges
* @param AllowanceCharges[] $allowance_charges
*/
public function setAllowanceCharges(?array $charges): self
public function setAllowanceCharges(?array $allowance_charges): self
{
$this->charges = $charges;
$this->allowance_charges = $allowance_charges;
return $this;
}
public function setAmountExcludingVat(?string $amount_excluding_vat): self
public function setAmountExcludingVat(?float $amount_excluding_vat): self
{
$this->amount_excluding_vat = $amount_excluding_vat;
return $this;
}
public function setAmountExcludingTax(?string $amount_excluding_tax): self
public function setAmountExcludingTax(?float $amount_excluding_tax): self
{
$this->amount_excluding_tax = $amount_excluding_tax;
return $this;
}
public function setAmountIncludingTax(?string $amount_including_tax): self
public function setAmountIncludingTax(?float $amount_including_tax): self
{
$this->amount_including_tax = $amount_including_tax;
return $this;

View File

@ -5,15 +5,15 @@ namespace App\Services\EDocument\Gateway\Storecove\Models;
class Tax
{
public ?string $country;
public ?string $amount;
public ?string $percentage;
public ?float $amount;
public ?float $percentage;
public ?string $category;
public ?string $type;
public function __construct(
?string $country,
?string $amount,
?string $percentage,
?float $amount,
?float $percentage,
?string $category,
?string $type
) {
@ -29,12 +29,12 @@ class Tax
return $this->country;
}
public function getAmount(): ?string
public function getAmount(): ?float
{
return $this->amount;
}
public function getPercentage(): ?string
public function getPercentage(): ?float
{
return $this->percentage;
}
@ -55,13 +55,13 @@ class Tax
return $this;
}
public function setAmount(?string $amount): self
public function setAmount(?float $amount): self
{
$this->amount = $amount;
return $this;
}
public function setPercentage(?string $percentage): self
public function setPercentage(?float $percentage): self
{
$this->percentage = $percentage;
return $this;

View File

@ -9,24 +9,24 @@ class TaxSubtotals
{
#[SerializedPath('[cbc:TaxAmount][#]')]
public ?string $tax_amount;
public ?float $tax_amount;
public ?string $country;
#[SerializedPath('[cbc:TaxableAmount][#]')]
public ?string $taxable_amount;
public ?float $taxable_amount;
#[SerializedPath('[cac:TaxCategory][cbc:Percent]')]
public ?string $percentage;
public ?float $percentage;
#[SerializedPath('[cac:TaxCategory][cbc:ID][#]')]
public ?string $category;
public function __construct(
?string $tax_amount,
?float $tax_amount,
?string $country,
?string $taxable_amount,
?string $percentage,
?float $taxable_amount,
?float $percentage,
?string $category
) {
$this->tax_amount = $tax_amount;
@ -36,7 +36,7 @@ class TaxSubtotals
$this->category = $category;
}
public function getTaxAmount(): ?string
public function getTaxAmount(): ?float
{
return $this->tax_amount;
}
@ -46,12 +46,12 @@ class TaxSubtotals
return $this->country;
}
public function getTaxableAmount(): ?string
public function getTaxableAmount(): ?float
{
return $this->taxable_amount;
}
public function getPercentage(): ?string
public function getPercentage(): ?float
{
return $this->percentage;
}
@ -61,7 +61,7 @@ class TaxSubtotals
return $this->category;
}
public function setTaxAmount(?string $tax_amount): self
public function setTaxAmount(?float $tax_amount): self
{
$this->tax_amount = $tax_amount;
return $this;
@ -73,13 +73,13 @@ class TaxSubtotals
return $this;
}
public function setTaxableAmount(?string $taxable_amount): self
public function setTaxableAmount(?float $taxable_amount): self
{
$this->taxable_amount = $taxable_amount;
return $this;
}
public function setPercentage(?string $percentage): self
public function setPercentage(?float $percentage): self
{
$this->percentage = $percentage;
return $this;

View File

@ -2,16 +2,18 @@
namespace App\Services\EDocument\Gateway\Storecove\Models;
use Symfony\Component\Serializer\Attribute\Context;
use Symfony\Component\Serializer\Attribute\SerializedName;
use Symfony\Component\Serializer\Attribute\SerializedPath;
class TaxesDutiesFees
{
public ?string $country; //need to run postprocessing on this
public ?string $amount;
public ?float $amount;
#[Context(['input_format' => 'float'])]
#[SerializedName('cbc:Percent')]
public ?string $percentage;
public ?float $percentage = 0;
#[SerializedPath('[cbc:ID][#]')]
public ?string $category;
@ -21,8 +23,8 @@ class TaxesDutiesFees
public function __construct(
?string $country,
?string $amount,
?string $percentage,
?float $amount,
?float $percentage,
?string $category,
?string $type
) {
@ -38,12 +40,12 @@ class TaxesDutiesFees
return $this->country;
}
public function getAmount(): ?string
public function getAmount(): ?float
{
return $this->amount;
}
public function getPercentage(): ?string
public function getPercentage(): ?float
{
return $this->percentage;
}
@ -64,13 +66,13 @@ class TaxesDutiesFees
return $this;
}
public function setAmount(?string $amount): self
public function setAmount(?float $amount): self
{
$this->amount = $amount;
return $this;
}
public function setPercentage(?string $percentage): self
public function setPercentage(?float $percentage): self
{
$this->percentage = $percentage;
return $this;

View File

@ -574,6 +574,25 @@ class Mutator implements MutatorInterface
/////////////// Storecove Helpers ///////////////
public function setClientRoutingCode(): self
{
$code = $this->getClientRoutingCode();
if(strlen($this->invoice->client->vat_number) < 2 || strlen($this->invoice->client->id_number) < 2)
return $this->setEmailRouting($this->invoice->client->present()->email());
if($this->invoice->client->country->iso_3166_2 == 'FR')
$vat = $this->invoice->client->id_number;
else
$vat = $this->invoice->client->vat_number;
$this->setStorecoveMeta($this->buildRouting([
["scheme" => $code, "id" => $vat]
]));
return $this;
}
/**
* getClientRoutingCode
*
@ -581,7 +600,7 @@ class Mutator implements MutatorInterface
*/
private function getClientRoutingCode(): string
{
return (new StorecoveRouter())->resolveRouting($this->invoice->client->country->iso_3166_2, $this->invoice->client->classification);
return (new StorecoveRouter())->setInvoice($this->invoice)->resolveRouting($this->invoice->client->country->iso_3166_2, $this->invoice->client->classification);
}

View File

@ -74,11 +74,18 @@ class Storecove
*/
public function build($model): mixed
{
return
// return
$this->adapter
->transform($model)
->decorate()
->validate();
return $this;
}
public function getResult(): array
{
return $this->adapter->getDocument();
}
/**
@ -141,33 +148,23 @@ class Storecove
/**
* Unused as yet
* @todo
* @param mixed $document
* @return string|bool
* @param array $payload
*/
public function sendJsonDocument($document)
public function sendJsonDocument(array $payload)
{
$payload = [
// "legalEntityId" => 290868,
"idempotencyGuid" => \Illuminate\Support\Str::uuid(),
"routing" => [
"eIdentifiers" => [],
"emails" => ["david@invoiceninja.com"]
],
"document" => [
"documentType" => "invoice",
"invoice" => $document,
],
];
$uri = "document_submissions";
$r = $this->httpClient($uri, (HttpVerb::POST)->value, $payload, $this->getHeaders());
if($r->successful()) {
nlog("sent! GUID = {$r->json()['guid']}");
return $r->json()['guid'];
}
nlog($payload);
nlog($r->body());
return false;
}

View File

@ -1,4 +1,13 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\EDocument\Gateway\Storecove;
@ -19,10 +28,10 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
use InvoiceNinja\EInvoice\Models\Peppol\Invoice as PeppolInvoice;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use App\Services\EDocument\Gateway\Transformers\StorecoveTransformer;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use App\Services\EDocument\Gateway\Storecove\PeppolToStorecoveNormalizer;
use App\Services\EDocument\Standards\Peppol;
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
@ -41,66 +50,8 @@ class StorecoveAdapter
private string $nexus;
/**
* transform
*
* @param \App\Models\Invoice $invoice
* @return self
*/
public function transform($invoice): self
{
$this->ninja_invoice = $invoice;
$this->buildNexus();
$context = [
DateTimeNormalizer::FORMAT_KEY => 'Y-m-d',
AbstractObjectNormalizer::SKIP_NULL_VALUES => true,
];
$serializer = $this->getSerializer();
// @phpstan-ignore-next-line
$this->storecove_invoice = $serializer->deserialize($invoice->e_invoice, Invoice::class, 'json', $context);
return $this;
}
public function decorate(): self
{
//set all taxmap countries - resolve the taxing country
$lines = $this->storecove_invoice->getInvoiceLines();
foreach($lines as $line)
{
foreach($line->taxes_duties_fees as &$tax)
{
$tax->country = $this->nexus;
}
unset($tax);
}
$this->storecove_invoice->setInvoiceLines($lines);
$tax_subtotals = $this->storecove_invoice->getTaxSubtotals();
foreach($tax_subtotals as &$tax)
{
$tax->country = $this->nexus;
}
unset($tax);
$this->storecove_invoice->setTaxSubtotals($tax_subtotals);
//configure identifiers
//set additional identifier if required (ie de => FR with FR vat)
return $this;
}
public function validate(): self
{
// $this->valid_document
return $this;
}
@ -113,10 +64,203 @@ class StorecoveAdapter
{
return $this->errors;
}
/**
* addError
*
* Adds an error to the errors array.
*
* @param string $error
* @return self
*/
private function addError(string $error): self
{
$this->errors[] = $error;
return $this;
}
public function deserialize($storecove_object)
{
$context = [
DateTimeNormalizer::FORMAT_KEY => 'Y-m-d',
AbstractObjectNormalizer::SKIP_NULL_VALUES => true,
];
$serializer = $this->getSerializer();
$obj['Invoice'] = $storecove_object['document']['invoice'];
$storecove_object = $serializer->normalize($obj, null, [\Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer::SKIP_NULL_VALUES => true]);
// return $storecove_object;
// $storecove_object = $serializer->encode($storecove_object, 'json', $context);
// return $storecove_object;
// return $data;
// $object = $serializer->denormalize(json_encode($storecove_object['document']['invoice']), \App\Services\EDocument\Gateway\Storecove\Models\Invoice::class, 'json', [\Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer::SKIP_NULL_VALUES => true]);
// return $storecove_object;
return $serializer->deserialize(json_encode($storecove_object), \App\Services\EDocument\Gateway\Storecove\Models\Invoice::class, 'json', $context);
}
/**
* transform
*
* @param \App\Models\Invoice $invoice
* @return self
*/
public function transform($invoice): self
{
$this->ninja_invoice = $invoice;
$serializer = $this->getSerializer();
/** Currently - due to class structures, the serialization process goes like this:
*
* e-invoice => Peppol -> XML -> Peppol Decoded -> encode to Peppol -> deserialize to Storecove
*/
$p = (new Peppol($invoice))->run()->toXml();
$context = [
DateTimeNormalizer::FORMAT_KEY => 'Y-m-d',
AbstractObjectNormalizer::SKIP_NULL_VALUES => true,
];
$e = new \InvoiceNinja\EInvoice\EInvoice();
$peppolInvoice = $e->decode('Peppol', $p, 'xml');
$parent = \App\Services\EDocument\Gateway\Storecove\Models\Invoice::class;
$peppolInvoice = $e->encode($peppolInvoice, 'json');
$this->storecove_invoice = $serializer->deserialize($peppolInvoice, $parent, 'json', $context);
$this->buildNexus();
return $this;
}
public function getNexus(): string
{
return $this->nexus;
}
public function decorate(): self
{
//set all taxmap countries - resolve the taxing country
$lines = $this->storecove_invoice->getInvoiceLines();
foreach($lines as &$line)
{
if(isset($line->taxes_duties_fees))
{
foreach($line->taxes_duties_fees as &$tax)
{
$tax->country = $this->nexus;
$tax->percentage = $tax->percentage ?? 0;
if(property_exists($tax,'category'))
$tax->category = $this->tranformTaxCode($tax->category);
}
unset($tax);
}
if(isset($line->allowance_charges))
{
foreach($line->allowance_charges as &$allowance)
{
if($allowance->reason == ctrans('texts.discount'))
$allowance->amount_excluding_tax = $allowance->amount_excluding_tax * -1;
foreach($allowance->getTaxesDutiesFees() ?? [] as &$tax)
{
if (property_exists($tax, 'category')) {
$tax->category = $this->tranformTaxCode($tax->category);
}
}
unset($tax);
}
unset($allowance);
}
}
$this->storecove_invoice->setInvoiceLines($lines);
$tax_subtotals = $this->storecove_invoice->getTaxSubtotals();
foreach($tax_subtotals as &$tax)
{
$tax->country = $this->nexus;
$tax->percentage = $tax->percentage ?? 0;
if (property_exists($tax, 'category'))
$tax->category = $this->tranformTaxCode($tax->category);
}
unset($tax);
$this->storecove_invoice->setTaxSubtotals($tax_subtotals);
//configure identifiers
//update payment means codes to storecove equivalents
$payment_means = $this->storecove_invoice->getPaymentMeansArray();
foreach($payment_means as &$pm)
{
$pm->code = $this->transformPaymentMeansCode($pm->code);
}
$this->storecove_invoice->setPaymentMeansArray($payment_means);
$allowances = $this->storecove_invoice->getAllowanceCharges() ?? [];
foreach($allowances as &$allowance)
{
$taxes = $allowance->getTaxesDutiesFees() ?? [];
foreach($taxes as &$tax)
{
$tax->country = $this->nexus;
$tax->percentage = $tax->percentage ?? 0;
if (property_exists($tax, 'category')) {
$tax->category = $this->tranformTaxCode($tax->category);
}
}
unset($tax);
if ($allowance->reason == ctrans('texts.discount')) {
$allowance->amount_excluding_tax = $allowance->amount_excluding_tax * -1;
}
$allowance->setTaxesDutiesFees($taxes);
}
unset($allowance);
$this->storecove_invoice->setAllowanceCharges($allowances);
$this->storecove_invoice->setTaxSystem('tax_line_percentages');
//resolve and set the public identifier for the customer
$accounting_customer_party = $this->storecove_invoice->getAccountingCustomerParty();
if(strlen($this->ninja_invoice->client->vat_number) > 2)
{
// $id = str_ireplace("fr","", $this->ninja_invoice->client->vat_number);
$id = $this->ninja_invoice->client->vat_number;
$scheme = $this->storecove->router->setInvoice($this->ninja_invoice)->resolveTaxScheme($this->ninja_invoice->client->country->iso_3166_2, $this->ninja_invoice->client->classification ?? 'individual');
$pi = new \App\Services\EDocument\Gateway\Storecove\Models\PublicIdentifiers($scheme, $id);
$accounting_customer_party->addPublicIdentifiers($pi);
$this->storecove_invoice->setAccountingCustomerParty($accounting_customer_party);
}
return $this;
}
@ -125,11 +269,8 @@ class StorecoveAdapter
$phpDocExtractor = new PhpDocExtractor();
$reflectionExtractor = new ReflectionExtractor();
// list of PropertyListExtractorInterface (any iterable)
$typeExtractors = [$reflectionExtractor,$phpDocExtractor];
// list of PropertyDescriptionExtractorInterface (any iterable)
$descriptionExtractors = [$phpDocExtractor];
// list of PropertyAccessExtractorInterface (any iterable)
$propertyInitializableExtractors = [$reflectionExtractor];
$propertyInfo = new PropertyInfoExtractor(
$propertyInitializableExtractors,
@ -150,7 +291,42 @@ class StorecoveAdapter
return $serializer;
}
/**
* Builds the document and appends an errors prop
*
* @return array
*/
public function getDocument(): mixed
{
$serializer = $this->getSerializer();
$context = [
DateTimeNormalizer::FORMAT_KEY => 'Y-m-d',
AbstractObjectNormalizer::SKIP_NULL_VALUES => true,
];
$s_invoice = $serializer->encode($this->storecove_invoice, 'json', $context);
$s_invoice = json_decode($s_invoice, true);
$s_invoice = $this->removeEmptyValues($s_invoice);
$data = [
'errors' => $this->getErrors(),
'document' => $s_invoice,
];
return $data;
}
/**
* RemoveEmptyValues
*
* @param array $array
* @return array
*/
private function removeEmptyValues(array $array): array
{
foreach ($array as $key => $value) {
@ -169,7 +345,7 @@ class StorecoveAdapter
private function buildNexus(): self
{
nlog("building nexus");
//Calculate nexus
$company_country_code = $this->ninja_invoice->company->country()->iso_3166_2;
$client_country_code = $this->ninja_invoice->client->country->iso_3166_2;
@ -177,27 +353,128 @@ class StorecoveAdapter
$eu_countries = $br->eu_country_codes;
if ($client_country_code == $company_country_code) {
//Domestic Sales
//Domestic Sales
nlog("domestic sales");
$this->nexus = $company_country_code;
} elseif (in_array($company_country_code, $eu_countries) && !in_array($client_country_code, $eu_countries)) {
//NON-EU Sale
nlog("non eu");
$this->nexus = $company_country_code;
} elseif (in_array($company_country_code, $eu_countries) && in_array($client_country_code, $eu_countries)) {
} elseif (in_array($client_country_code, $eu_countries)) {
//EU Sale
// Invalid VAT number = seller country nexus
if(!$this->ninja_invoice->client->has_valid_vat_number)
//EU Sale where Company country != Client Country
// First, determine if we're over threshold
$is_over_threshold = isset($this->ninja_invoice->company->tax_data->regions->EU->has_sales_above_threshold) &&
$this->ninja_invoice->company->tax_data->regions->EU->has_sales_above_threshold;
// Is this B2B or B2C?
$is_b2c = strlen($this->ninja_invoice->client->vat_number) < 2 ||
!($this->ninja_invoice->client->has_valid_vat_number ?? false) ||
$this->ninja_invoice->client->classification == 'individual';
// B2C, under threshold, no Company VAT Registerd - must charge origin country VAT
if ($is_b2c && !$is_over_threshold && strlen($this->ninja_invoice->company->settings->vat_number) < 2) {
nlog("no company vat");
$this->nexus = $company_country_code;
} elseif ($is_b2c) {
if ($is_over_threshold) {
// B2C over threshold - need destination VAT number
if (!isset($this->ninja_invoice->company->tax_data->regions->EU->subregions->{$client_country_code}->vat_number)) {
$this->nexus = $client_country_code;
$this->addError("Tax Nexus is client country ({$client_country_code}) - however VAT number not present for this region. Document not sent!");
return $this;
}
nlog("B2C");
$this->nexus = $client_country_code;
$this->setupDestinationVAT($client_country_code);
} else {
nlog("under threshold origin country");
// B2C under threshold - origin country VAT
$this->nexus = $company_country_code;
}
} else {
nlog("B2B with valid vat");
// B2B with valid VAT - origin country
$this->nexus = $company_country_code;
else if ($this->ninja_invoice->company->tax_data->regions->EU->has_sales_above_threshold && isset($this->ninja_invoice->company->tax_data->regions->EU->subregions->{$client_country_code}->vat_number)) { //over threshold - tax in buyer country
$this->nexus = $client_country_code;
}
//If we reach here? We are in an invalid state!
$this->nexus = $company_country_code;
$this->addError("Tax Nexus is client country ({$client_country_code}) - however VAT number not present for this region. Document not sent!");
}
return $this;
}
private function setupDestinationVAT($client_country_code):self
{
nlog("configuring destination tax");
$this->storecove_invoice->setConsumerTaxMode(true);
$id = $this->ninja_invoice->company->tax_data->regions->EU->subregions->{$client_country_code}->vat_number;
$scheme = $this->storecove->router->setInvoice($this->ninja_invoice)->resolveTaxScheme($client_country_code, $this->ninja_invoice->client->classification ?? 'individual');
$pi = new \App\Services\EDocument\Gateway\Storecove\Models\PublicIdentifiers($scheme, $id);
$asp = $this->storecove_invoice->getAccountingSupplierParty();
$asp->addPublicIdentifiers($pi);
$this->storecove_invoice->setAccountingSupplierParty($asp);
return $this;
}
private function tranformTaxCode(string $code): ?string
{
return match($code){
'S' => 'standard',
'Z' => 'zero_rated',
'E' => 'exempt',
'AE' => 'reverse_charge',
'K' => 'intra_community',
'G' => 'export',
'O' => 'outside_scope',
'L' => 'cgst',
'I' => 'igst',
'SS' => 'sgst',
'B' => 'deemed_supply',
'SR' => 'srca_s',
'SC' => 'srca_c',
'NR' => 'not_registered',
default => null
};
}
private function transformPaymentMeansCode(?string $code): string
{
return match($code){
'30' => 'credit_transfer',
'58' => 'sepa_credit_transfer',
'31' => 'debit_transfer',
'49' => 'direct_debit',
'59' => 'sepa_direct_debit',
'48' => 'card', // Generic card payment
'54' => 'bank_card',
'55' => 'credit_card',
'57' => 'standing_agreement',
'10' => 'cash',
'20' => 'bank_cheque',
'21' => 'cashiers_cheque',
'97' => 'aunz_npp',
'98' => 'aunz_npp_payid',
'99' => 'aunz_npp_payto',
'71' => 'aunz_bpay',
'72' => 'aunz_postbillpay',
'73' => 'aunz_uri',
'50' => 'se_bankgiro',
'51' => 'se_plusgiro',
'74' => 'sg_giro',
'75' => 'sg_card',
'76' => 'sg_paynow',
'77' => 'it_mav',
'78' => 'it_pagopa',
'42' => 'nl_ga_beneficiary',
'43' => 'nl_ga_gaccount',
'1' => 'undefined', // Instrument not defined
default => 'undefined',
};
}
}

View File

@ -73,7 +73,7 @@ class StorecoveRouter
"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"],
"NL" => ["B","NL:KVK","NL:VAT","NL:VAT"],
"PL" => ["G+B","","PL:VAT","PL:VAT"],
"PT" => ["G+B","","PT:VAT","PT:VAT"],
"RO" => ["G+B","","RO:VAT","RO:VAT"],
@ -96,6 +96,8 @@ class StorecoveRouter
"Other" => ["B","DUNS, GLN, LEI",false,"DUNS, GLN, LEI"],
];
private $invoice;
public function __construct()
{
}
@ -110,11 +112,7 @@ class StorecoveRouter
public function resolveRouting(string $country, ?string $classification = 'business'): string
{
$rules = $this->routing_rules[$country];
if(is_array($rules) && !is_array($rules[0])) {
return $rules[3];
}
$code = 'B';
match($classification) {
@ -123,6 +121,22 @@ class StorecoveRouter
"individual" => $code = "C",
default => $code = "B",
};
if ($this->invoice && $country == 'FR') {
if ($code == 'B' && strlen($this->invoice->client->id_number) == 9) {
return 'FR:SIRENE';
} elseif ($code == 'B' && strlen($this->invoice->client->id_number) == 14) {
return 'FR:SIRET';
} elseif ($code == 'G') {
return '0009:11000201100044';
}
}
if (is_array($rules) && !is_array($rules[0])) {
return $rules[3];
}
foreach($rules as $rule) {
if(stripos($rule[0], $code) !== false) {
@ -133,6 +147,11 @@ class StorecoveRouter
return $rules[0][3];
}
public function setInvoice($invoice):self
{
$this->invoice = $invoice;
return $this;
}
/**
* resolveTaxScheme
*
@ -154,6 +173,17 @@ class StorecoveRouter
default => $code = "B",
};
// if($this->invoice && $country == 'FR'){
// if($code == 'B' && strlen(trim(str_ireplace("fr", "", $this->invoice->client->vat_number))) == 9)
// return 'FR:SIRENE';
// elseif($code == 'B' && strlen(trim(str_ireplace("fr", "", $this->invoice->client->vat_number))) == 14)
// return 'FR:SIRET';
// elseif($code == 'G')
// return 'FR:SIRET'; //@todo need to add customerAssignedAccountIdValue
// }
//single array
if(is_array($rules) && !is_array($rules[0])) {
return $rules[2];

View File

@ -1,37 +0,0 @@
<?php
namespace App\Services\EDocument\Gateway\Transformers;
use App\Helpers\Invoice\Taxer;
use App\Utils\Traits\NumberFormatter;
use App\Services\EDocument\Gateway\Storecove\Models\Tax;
use App\Services\EDocument\Gateway\Storecove\Models\Party;
use App\Services\EDocument\Gateway\Storecove\Models\Address;
use App\Services\EDocument\Gateway\Storecove\Models\Contact;
use App\Services\EDocument\Gateway\Storecove\Models\References;
use App\Services\EDocument\Gateway\Storecove\Models\InvoiceLines;
use App\Services\EDocument\Gateway\Storecove\Models\PaymentMeans;
use App\Services\EDocument\Gateway\Storecove\Models\TaxSubtotals;
use App\Services\EDocument\Gateway\Storecove\Models\AllowanceCharges;
use App\Services\EDocument\Gateway\Storecove\Models\AccountingCustomerParty;
use App\Services\EDocument\Gateway\Storecove\Models\AccountingSupplierParty;
use App\Services\EDocument\Gateway\Storecove\Models\Invoice as StorecoveInvoice;
use Illuminate\Support\Str;
class StorecoveNinjaTransformer implements TransformerInterface
{
public function transform(mixed $invoice)
{
$document = data_get($invoice, 'document.invoice');
}
public function getInvoice()
{
}
public function toJson()
{
}
}

View File

@ -1,174 +0,0 @@
<?php
namespace App\Services\EDocument\Gateway\Transformers;
use App\Helpers\Invoice\Taxer;
use App\Utils\Traits\NumberFormatter;
use App\Services\EDocument\Gateway\Storecove\Models\Tax;
use App\Services\EDocument\Gateway\Storecove\Models\Party;
use App\Services\EDocument\Gateway\Storecove\Models\Address;
use App\Services\EDocument\Gateway\Storecove\Models\Contact;
use App\Services\EDocument\Gateway\Storecove\Models\References;
use App\Services\EDocument\Gateway\Storecove\Models\InvoiceLines;
use App\Services\EDocument\Gateway\Storecove\Models\PaymentMeans;
use App\Services\EDocument\Gateway\Storecove\Models\TaxSubtotals;
use App\Services\EDocument\Gateway\Storecove\Models\AllowanceCharges;
use App\Services\EDocument\Gateway\Storecove\Models\AccountingCustomerParty;
use App\Services\EDocument\Gateway\Storecove\Models\AccountingSupplierParty;
use App\Services\EDocument\Gateway\Storecove\Models\Invoice as StorecoveInvoice;
use App\Services\EDocument\Gateway\Storecove\Storecove;
use Illuminate\Support\Str;
class StorecoveTransformer implements TransformerInterface
{
use Taxer;
use NumberFormatter;
private StorecoveInvoice $s_invoice;
private array $tax_map = [];
public function setInvoice($s_invoice): self
{
$this->s_invoice = $s_invoice;
return $this;
}
public function getInvoice(): StorecoveInvoice
{
return $this->s_invoice;
}
public function createNewStorecoveInvoice(): self
{
$this->s_invoice = (new \ReflectionClass(StorecoveInvoice::class))->newInstanceWithoutConstructor();
return $this;
}
//$invoice = inbound peppol
public function transform(mixed $invoice)
{
$this->s_invoice->setTaxPointDate($invoice->IssueDate->format('Y-m-d'));
// Only use this if we are billing for services between a period.
if (isset($invoice->InvoicePeriod[0]) &&
isset($invoice->InvoicePeriod[0]->StartDate) &&
isset($invoice->InvoicePeriod[0]->EndDate)) {
$this->s_invoice->setInvoicePeriod("{$invoice->InvoicePeriod[0]->StartDate->format('Y-m-d')} - {$invoice->InvoicePeriod[0]->EndDate->format('Y-m-d')}");
}
$lines = [];
foreach($invoice->InvoiceLine as $peppolLine)
{
// Tax handling
if(isset($peppolLine->Item->ClassifiedTaxCategory) && is_array($peppolLine->Item->ClassifiedTaxCategory)){
foreach($peppolLine->Item->ClassifiedTaxCategory as $ctc)
{
$this->setTaxMap($ctc, $peppolLine, $invoice);
}
}
// //discounts
// if(isset($peppolLine->Price->AllowanceCharge) && is_array($peppolLine->Price->AllowanceCharge)){
// foreach($peppolLine->Price->AllowanceCharge as $allowance)
// {
// $reason = isset($allowance->ChargeIndicator) ? ctrans('texts.discount') : ctrans('texts.fee');
// $amount = $allowance->Amount->amount;
// $ac = new AllowanceCharges(reason: $reason, amountExcludingTax: $amount);
// $line->addAllowanceCharge($ac);
// }
// }
// $lines[] = $line;
// }
// $this->s_invoice->invoiceLines = $lines;
}
$sub_taxes = collect($this->tax_map)
->groupBy('percentage')
->map(function ($group) {
return new TaxSubtotals(
taxable_amount: $group->sum('taxableAmount'),
tax_amount: $group->sum('taxAmount'),
percentage: $group->first()['percentage'],
country: $group->first()['country'],
category: null
);
})->toArray();
$this->s_invoice->setTaxSubtotals($sub_taxes);
// $this->s_invoice->setAmountIncludingVat($invoice->LegalMonetaryTotal->TaxInclusiveAmount->amount);
// $this->s_invoice->setPrepaidAmount(0);
return $this->s_invoice;
}
private function setTaxMap($ctc, $peppolLine, $invoice): self
{
$taxAmount = 0;
$taxableAmount = 0;
foreach($peppolLine->Item as $item)
{
$_taxAmount = $this->calcAmountLineTax($ctc->Percent, $peppolLine->LineExtensionAmount->amount);
$taxAmount += $_taxAmount;
$taxableAmount += $peppolLine->LineExtensionAmount->amount;
}
$this->tax_map[] = [
'percentage' => $ctc->Percent,
'country' => $this->resolveJurisdication($ctc, $invoice),
'taxAmount' => $taxAmount,
'taxableAmount' => $taxableAmount,
];
return $this;
}
private function resolveJurisdication($ctc, $invoice): string
{
if(isset($ctc->TaxTotal[0]->JurisdictionRegionAddress->Country->IdentificationCode->value))
return $ctc->TaxTotal[0]->JurisdictionRegionAddress->Country->IdentificationCode->value;
return $invoice->AccountingSupplierParty->Party->PostalAddress->Country->IdentificationCode->value;
}
public function buildDocument(): mixed
{
$doc = new \stdClass;
$doc->document->documentType = "invoice";
$doc->document->invoice = $this->getInvoice();
$doc->attachments = [];
$doc->legalEntityId = '';
$doc->idempotencyGuid = Str::uuid();
$doc->routing->eIdentifiers = [];
$doc->emails = [];
return $doc;
}
public function toJson(): string
{
return json_encode($this->s_invoice, JSON_PRETTY_PRINT);
}
}

View File

@ -1,22 +0,0 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Services\EDocument\Gateway\Transformers;
interface TransformerInterface
{
public function transform(mixed $invoice);
public function getInvoice();
public function toJson();
}

View File

@ -48,32 +48,58 @@ class SendEDocument implements ShouldQueue
public function handle(Storecove $storecove)
{
MultiDB::setDB($this->db);
nlog("trying");
$model = $this->entity::find($this->id);
/** Concrete implementation current linked to Storecove only */
$p = new Peppol($model);
$p->run();
$identifiers = $p->gateway->mutator->setClientRoutingCode()->getStorecoveMeta();
$result = $storecove->build($model)->getResult();
if (count($result['errors']) > 0) {
nlog($result);
return $result['errors'];
}
$payload = [
'legal_entity_id' => $model->company->legal_entity_id,
"idempotencyGuid" => \Illuminate\Support\Str::uuid(),
'document' => [
'document_type' => 'invoice',
'invoice' => $result['document'],
],
'tenant_id' => $model->company->company_key,
'routing' => $identifiers['routing'],
//
'account_key' => $model->company->account->key,
'e_invoicing_token' => $model->company->account->e_invoicing_token,
'identifiers' => $identifiers,
];
/** Concrete implementation current linked to Storecove only */
//@testing only
$sc = new \App\Services\EDocument\Gateway\Storecove\Storecove();
$r = $sc->sendJsonDocument($payload);
if (is_string($r)) {
return $this->writeActivity($model, $r);
}
else {
// nlog($r->body());
}
return;
if(Ninja::isSelfHost() && ($model instanceof Invoice) && $model->company->legal_entity_id)
{
$p = new Peppol($model);
$p->run();
$identifiers = $p->getStorecoveMeta();
$result = $storecove->build($model);
/**************************** Legacy */
$xml = $p->toXml();
$payload = [
'legal_entity_id' => $model->company->legal_entity_id,
'document' => base64_encode($xml),
'tenant_id' => $model->company->company_key,
'identifiers' => $identifiers,
// 'e_invoicing_token' => $model->company->e_invoicing_token,
// include whitelabel key.
];
/**************************** Legacy */
$r = Http::withHeaders($this->getHeaders())
->post(config('ninja.hosted_ninja_url')."/api/einvoice/submission", $payload);
@ -95,15 +121,9 @@ class SendEDocument implements ShouldQueue
if(Ninja::isHosted() && ($model instanceof Invoice) && $model->company->legal_entity_id)
{
//hosted sender
$p = new Peppol($model);
$p->run();
$xml = $p->toXml();
$identifiers = $p->getStorecoveMeta();
$sc = new \App\Services\EDocument\Gateway\Storecove\Storecove();
$r = $sc->sendDocument($xml, $model->company->legal_entity_id, $identifiers);
$r = $sc->sendJsonDocument($payload);
if(is_string($r))
return $this->writeActivity($model, $r);

View File

@ -64,6 +64,8 @@ class Peppol extends AbstractService
* Exclusive Taxes
*
*/
private ?string $override_vat_number;
/** @var array $InvoiceTypeCodes */
private array $InvoiceTypeCodes = [
@ -147,6 +149,12 @@ class Peppol extends AbstractService
private array $tax_map = [];
private float $allowance_total = 0;
private $globalTaxCategories;
private string $tax_category_id;
public function __construct(public Invoice $invoice)
{
$this->company = $invoice->company;
@ -163,6 +171,9 @@ class Peppol extends AbstractService
*/
public function run(): self
{
$this->getJurisdiction(); //Sets the nexus object into the Peppol document.
$this->getAllUsedTaxes(); //Maps all used line item taxes
/** Invoice Level Props */
$id = new \InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\CustomizationID();
$id->value = $this->customizationID;
@ -187,7 +198,7 @@ class Peppol extends AbstractService
$ip = new InvoicePeriod();
$ip->StartDate = new \DateTime($this->invoice->date);
$ip->EndDate = new \DateTime($this->invoice->due_date);
$this->p_invoice->InvoicePeriod[] = $ip;
$this->p_invoice->InvoicePeriod = [$ip];
}
if ($this->invoice->project_id) {
@ -195,7 +206,7 @@ class Peppol extends AbstractService
$id = new \InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\ID();
$id->value = $this->invoice->project->number;
$pr->ID = $id;
$this->p_invoice->ProjectReference[] = $pr;
$this->p_invoice->ProjectReference = [$pr];
}
/** Auto switch between Invoice / Credit based on the amount value */
@ -204,8 +215,9 @@ class Peppol extends AbstractService
$this->p_invoice->AccountingSupplierParty = $this->getAccountingSupplierParty();
$this->p_invoice->AccountingCustomerParty = $this->getAccountingCustomerParty();
$this->p_invoice->InvoiceLine = $this->getInvoiceLines();
$this->p_invoice->LegalMonetaryTotal = $this->getLegalMonetaryTotal();
$this->p_invoice->AllowanceCharge = $this->getAllowanceCharges();
$this->p_invoice->LegalMonetaryTotal = $this->getLegalMonetaryTotal();
$this->p_invoice->Delivery = $this->getDelivery();
$this->setOrderReference()->setTaxBreakdown();
@ -217,11 +229,11 @@ class Peppol extends AbstractService
->getPeppol();
//** @todo double check this logic, this will only ever write the doc once */
if(strlen($this->invoice->backup ?? '') == 0)
{
$this->invoice->e_invoice = $this->toObject();
$this->invoice->save();
}
// if(is_null($this->invoice->backup))
// {
// $this->invoice->e_invoice = $this->toObject();
// $this->invoice->save();
// }
return $this;
@ -426,18 +438,27 @@ class Peppol extends AbstractService
$allowanceCharge->ChargeIndicator = 'false'; // false = discount
$allowanceCharge->Amount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\Amount();
$allowanceCharge->Amount->currencyID = $this->invoice->client->currency()->code;
$allowanceCharge->Amount->amount = (string)$this->calc->getTotalDiscount();
$allowanceCharge->BaseAmount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\BaseAmount();
$allowanceCharge->BaseAmount->currencyID = $this->invoice->client->currency()->code;
$allowanceCharge->BaseAmount->amount = (string) $this->calc->getSubTotal();
$allowanceCharge->Amount->amount = (string)number_format($this->calc->getTotalDiscount(),2, '.', '');
// Add percentage if available
if ($this->invoice->discount > 0 && !$this->invoice->is_amount_discount) {
$allowanceCharge->BaseAmount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\BaseAmount();
$allowanceCharge->BaseAmount->currencyID = $this->invoice->client->currency()->code;
$allowanceCharge->BaseAmount->amount = (string) number_format($this->calc->getSubtotalWithSurcharges(), 2, '.', '');
$mfn = new \InvoiceNinja\EInvoice\Models\Peppol\NumericType\MultiplierFactorNumeric();
$mfn->value = (string) ($this->invoice->discount / 100);
$mfn->value = (string)number_format(round(($this->invoice->discount), 2), 2, '.', ''); // Format to always show 2 decimals
$allowanceCharge->MultiplierFactorNumeric = $mfn; // Convert percentage to decimal
}
$tc = clone $this->globalTaxCategories[0];
// $tc->Percent = '0';
unset($tc->TaxExemptionReasonCode);
unset($tc->TaxExemptionReason);
$allowanceCharge->TaxCategory[] = $tc;
$allowanceCharge->AllowanceChargeReason = ctrans('texts.discount');
$allowances[] = $allowanceCharge;
}
@ -446,14 +467,17 @@ class Peppol extends AbstractService
// Add Allowance Charge to Price
$allowanceCharge = new \InvoiceNinja\EInvoice\Models\Peppol\AllowanceChargeType\AllowanceCharge();
// $allowanceCharge->ChargeIndicator = true; // false = discount
$allowanceCharge->Amount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\Amount();
$allowanceCharge->Amount->currencyID = $this->invoice->client->currency()->code;
$allowanceCharge->Amount->amount = (string)$this->invoice->custom_surcharge1;
$allowanceCharge->BaseAmount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\BaseAmount();
$allowanceCharge->BaseAmount->currencyID = $this->invoice->client->currency()->code;
$allowanceCharge->BaseAmount->amount = (string) $this->calc->getSubTotal();
$allowanceCharge->BaseAmount->amount = (string) $this->calc->getSubtotalWithSurcharges();
$this->calculateTaxMap($this->invoice->custom_surcharge1);
$allowanceCharge->TaxCategory = $this->globalTaxCategories;
$allowanceCharge->AllowanceChargeReason = ctrans('texts.surcharge');
$allowances[] = $allowanceCharge;
}
@ -462,14 +486,18 @@ class Peppol extends AbstractService
// Add Allowance Charge to Price
$allowanceCharge = new \InvoiceNinja\EInvoice\Models\Peppol\AllowanceChargeType\AllowanceCharge();
// $allowanceCharge->ChargeIndicator = true; // false = discount
$allowanceCharge->Amount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\Amount();
$allowanceCharge->Amount->currencyID = $this->invoice->client->currency()->code;
$allowanceCharge->Amount->amount = (string)$this->invoice->custom_surcharge2;
$allowanceCharge->BaseAmount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\BaseAmount();
$allowanceCharge->BaseAmount->currencyID = $this->invoice->client->currency()->code;
$allowanceCharge->BaseAmount->amount = (string) $this->calc->getSubTotal();
$allowanceCharge->BaseAmount->amount = (string) $this->calc->getSubtotalWithSurcharges();
$this->calculateTaxMap($this->invoice->custom_surcharge2);
$allowanceCharge->TaxCategory = $this->globalTaxCategories;
$allowanceCharge->AllowanceChargeReason = ctrans('texts.surcharge');
$allowances[] = $allowanceCharge;
}
@ -478,14 +506,17 @@ class Peppol extends AbstractService
// Add Allowance Charge to Price
$allowanceCharge = new \InvoiceNinja\EInvoice\Models\Peppol\AllowanceChargeType\AllowanceCharge();
// $allowanceCharge->ChargeIndicator = true; // false = discount
$allowanceCharge->Amount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\Amount();
$allowanceCharge->Amount->currencyID = $this->invoice->client->currency()->code;
$allowanceCharge->Amount->amount = (string)$this->invoice->custom_surcharge3;
$allowanceCharge->BaseAmount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\BaseAmount();
$allowanceCharge->BaseAmount->currencyID = $this->invoice->client->currency()->code;
$allowanceCharge->BaseAmount->amount = (string) $this->calc->getSubTotal();
$allowanceCharge->BaseAmount->amount = (string) $this->calc->getSubtotalWithSurcharges();
$this->calculateTaxMap($this->invoice->custom_surcharge3);
$allowanceCharge->TaxCategory = $this->globalTaxCategories;
$allowanceCharge->AllowanceChargeReason = ctrans('texts.surcharge');
$allowances[] = $allowanceCharge;
}
@ -494,14 +525,17 @@ class Peppol extends AbstractService
// Add Allowance Charge to Price
$allowanceCharge = new \InvoiceNinja\EInvoice\Models\Peppol\AllowanceChargeType\AllowanceCharge();
// $allowanceCharge->ChargeIndicator = true; // false = discount
$allowanceCharge->Amount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\Amount();
$allowanceCharge->Amount->currencyID = $this->invoice->client->currency()->code;
$allowanceCharge->Amount->amount = (string)$this->invoice->custom_surcharge4;
$allowanceCharge->BaseAmount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\BaseAmount();
$allowanceCharge->BaseAmount->currencyID = $this->invoice->client->currency()->code;
$allowanceCharge->BaseAmount->amount = (string) $this->calc->getSubTotal();
$allowanceCharge->BaseAmount->amount = (string) $this->calc->getSubtotalWithSurcharges();
$this->calculateTaxMap($this->invoice->custom_surcharge4);
$allowanceCharge->TaxCategory = $this->globalTaxCategories;
$allowanceCharge->AllowanceChargeReason = ctrans('texts.surcharge');
$allowances[] = $allowanceCharge;
}
@ -523,12 +557,13 @@ class Peppol extends AbstractService
$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;
$lea->amount = $this->invoice->uses_inclusive_taxes ? round($this->invoice->amount - $this->invoice->total_taxes, 2) : $this->calc->getSubTotal();
$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;
$tea->amount = round($this->invoice->amount - $this->invoice->total_taxes, 2);
$lmt->TaxExclusiveAmount = $tea;
$tia = new TaxInclusiveAmount();
@ -541,6 +576,11 @@ class Peppol extends AbstractService
$pa->amount = $this->invoice->amount;
$lmt->PayableAmount = $pa;
$am = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\AllowanceTotalAmount();
$am->currencyID = $this->invoice->client->currency()->code;
$am->amount = (string)$this->calc->getTotalDiscount();
$lmt->AllowanceTotalAmount = $am;
return $lmt;
}
@ -572,6 +612,8 @@ class Peppol extends AbstractService
break;
case Product::PRODUCT_TYPE_REVERSE_TAX:
$tax_type = 'AE';
case Product::PRODUCT_INTRA_COMMUNITY:
$tax_type = 'K';
break;
}
@ -579,7 +621,7 @@ class Peppol extends AbstractService
if (empty($tax_type)) {
if ((in_array($this->company->country()->iso_3166_2, $eu_states) && in_array($this->invoice->client->country->iso_3166_2, $eu_states)) && $this->invoice->company->country()->iso_3166_2 != $this->invoice->client->country->iso_3166_2) {
$tax_type = 'K'; //EEA Exempt
$tax_type = 'K'; // EEA Exempt
} elseif (!in_array($this->invoice->client->country->iso_3166_2, $eu_states)) {
$tax_type = 'G'; //Free export item, VAT not charged
} else {
@ -600,7 +642,83 @@ class Peppol extends AbstractService
return $tax_type;
}
/**
private function resolveTaxExemptReason($item, $ctc = null): mixed
{
$eu_states = ["AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "EL", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL", "PL", "PT", "RO", "SK", "SI", "ES", "ES-CE", "ES-ML", "ES-CN", "SE", "IS", "LI", "NO", "CH"];
if($item->tax_id == '9') {
$tax_type = 'AE'; // EEA Exempt
$reason_code = 'vatex-eu-ae';
$reason = 'Reverse charge';
} elseif ((in_array($this->company->country()->iso_3166_2, $eu_states) &&
in_array($this->invoice->client->country->iso_3166_2, $eu_states)) &&
$this->invoice->company->country()->iso_3166_2 != $this->invoice->client->country->iso_3166_2) {
$tax_type = 'K'; // EEA Exempt
$reason_code = 'vatex-eu-ic';
$reason = 'Intra-Community supply';
} elseif (!in_array($this->invoice->client->country->iso_3166_2, $eu_states)) {
$tax_type = 'G'; //Free export item, VAT not charged
$reason_code = 'vatex-eu-g';
$reason = 'Export outside the EU';
} else {
$tax_type = 'O'; //Standard rate
$reason_code = "vatex-eu-o";
$reason = 'Services outside scope of tax';
}
$this->tax_category_id = $tax_type;
//no vat, build a single tax category for tax exemption
$taxCategory = new \InvoiceNinja\EInvoice\Models\Peppol\TaxCategoryType\TaxCategory();
$taxCategory->ID = new \InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\ID();
$taxCategory->ID->value = $tax_type;
if($this->tax_category_id != 'O')
$taxCategory->Percent = '0';
$taxScheme = new \InvoiceNinja\EInvoice\Models\Peppol\TaxSchemeType\TaxScheme();
$taxScheme->ID = new \InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\ID();
$taxScheme->ID->value = $this->standardizeTaxSchemeId('vat');
$taxCategory->TaxScheme = $taxScheme;
$terc = new \InvoiceNinja\EInvoice\Models\Peppol\CodeType\TaxExemptionReasonCode();
$terc->value = $reason_code;
$taxCategory->TaxExemptionReasonCode = $terc;
$taxCategory->TaxExemptionReason = $reason;
$this->globalTaxCategories = [$taxCategory];
if($this->tax_category_id == 'O' && isset($this->p_invoice->AccountingSupplierParty->Party->PartyTaxScheme))
unset($this->p_invoice->AccountingSupplierParty->Party->PartyTaxScheme);
if ($this->tax_category_id == 'O' && isset($this->p_invoice->AccountingCustomerParty->Party->PartyTaxScheme)) {
unset($this->p_invoice->AccountingCustomerParty->Party->PartyTaxScheme);
}
if($ctc) {
$ctc->ID->value = $tax_type;
if($this->tax_category_id != 'O')
$ctc->Percent = '0';
// $terc = new \InvoiceNinja\EInvoice\Models\Peppol\CodeType\TaxExemptionReasonCode();
// $terc->value = $reason_code;
// $ctc->TaxExemptionReasonCode = $terc;
// $ctc->TaxExemptionReason = $reason;
return $ctc;
}
return $tax_type;
}
/**
* getInvoiceLines
*
* Compiles the invoice line items of the document
@ -612,32 +730,42 @@ class Peppol extends AbstractService
$lines = [];
foreach($this->invoice->line_items as $key => $item) {
$_item = new Item();
$_item->Name = $item->product_key;
$_item->Description = $item->notes;
$ctc = new ClassifiedTaxCategory();
$ctc->ID = new ID();
$ctc->ID->value = $this->getTaxType($item->tax_id);
if($item->tax_rate1 > 0)
$ctc->Percent = (string)$item->tax_rate1;
$ts = new TaxScheme();
$id = new ID();
$id->value = $this->standardizeTaxSchemeId($item->tax_name1);
$ts->ID = $id;
$ctc->TaxScheme = $ts;
if(floatval($item->tax_rate1) === 0.0)
{
$ctc = new ClassifiedTaxCategory();
$ctc->ID = new ID();
$ctc->ID->value = $this->getTaxType($item->tax_id);
$ctc->Percent = $item->tax_rate1;
$ctc = $this->resolveTaxExemptReason($item, $ctc);
$ts = new TaxScheme();
$id = new ID();
$id->value = $this->standardizeTaxSchemeId($item->tax_name1);
$ts->ID = $id;
$ctc->TaxScheme = $ts;
$_item->ClassifiedTaxCategory[] = $ctc;
if($this->tax_category_id == 'O')
unset($ctc->Percent);
}
$_item->ClassifiedTaxCategory[] = $ctc;
if ($item->tax_rate2 > 0) {
$ctc = new ClassifiedTaxCategory();
$ctc->ID = new ID();
$ctc->ID->value = $this->getTaxType($item->tax_id);
$ctc->Percent = $item->tax_rate2;
$ctc->Percent = (string)$item->tax_rate2;
$ts = new TaxScheme();
$id = new ID();
@ -652,7 +780,7 @@ class Peppol extends AbstractService
$ctc = new ClassifiedTaxCategory();
$ctc->ID = new ID();
$ctc->ID->value = $this->getTaxType($item->tax_id);
$ctc->Percent = $item->tax_rate3;
$ctc->Percent = (string)$item->tax_rate3;
$ts = new TaxScheme();
$id = new ID();
@ -681,15 +809,16 @@ class Peppol extends AbstractService
$line->Item = $_item;
/** Builds the tax map for the document */
$this->getItemTaxes($item);
// $this->getItemTaxes($item);
// Handle Price and Discounts
if ($item->discount > 0) {
// Base Price (before discount)
$basePrice = new Price();
$basePriceAmount = new PriceAmount();
$basePriceAmount->currencyID = $this->invoice->client->currency()->code;
$basePriceAmount->amount = (string)($item->cost - $this->calculateDiscountAmount($item));
$basePriceAmount->amount = (string)$item->cost;
$basePrice->PriceAmount = $basePriceAmount;
// Add Allowance Charge to Price
@ -697,29 +826,35 @@ class Peppol extends AbstractService
$allowanceCharge->ChargeIndicator = 'false'; // false = discount
$allowanceCharge->Amount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\Amount();
$allowanceCharge->Amount->currencyID = $this->invoice->client->currency()->code;
$allowanceCharge->Amount->amount = (string)$this->calculateDiscountAmount($item);
$allowanceCharge->BaseAmount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\BaseAmount();
$allowanceCharge->BaseAmount->currencyID = $this->invoice->client->currency()->code;
$allowanceCharge->BaseAmount->amount = (string)$item->cost;
$allowanceCharge->Amount->amount = (string)number_format($this->calculateTotalItemDiscountAmount($item),2, '.', '');
$this->allowance_total += $this->calculateTotalItemDiscountAmount($item);
// Add percentage if available
if ($item->discount > 0 && !$item->is_amount_discount) {
$allowanceCharge->BaseAmount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\BaseAmount();
$allowanceCharge->BaseAmount->currencyID = $this->invoice->client->currency()->code;
$allowanceCharge->BaseAmount->amount = (string)round(($item->cost * $item->quantity),2);
$mfn = new \InvoiceNinja\EInvoice\Models\Peppol\NumericType\MultiplierFactorNumeric();
$mfn->value = (string) ($item->discount / 100);
$mfn->value = (string) round($item->discount,2);
$allowanceCharge->MultiplierFactorNumeric = $mfn; // Convert percentage to decimal
}
// }
// Required reason
$allowanceCharge->AllowanceChargeReason = ctrans('texts.discount');
$basePrice->AllowanceCharge[] = $allowanceCharge;
$line->Price = $basePrice;
$line->AllowanceCharge[] = $allowanceCharge;
} else {
// No discount case
$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));
$pa->amount = (string)$item->cost;
$price->PriceAmount = $pa;
$line->Price = $price;
}
@ -730,207 +865,88 @@ class Peppol extends AbstractService
return $lines;
}
/**
* calculateDiscountAmount
*
* Helper method to determine the discount amount to be used.
*
* @param mixed $item
* @return float
*/
private function calculateDiscountAmount($item): float
private function calculateTotalItemDiscountAmount($item):float
{
if ($item->is_amount_discount) {
return $item->discount / $item->quantity; // Per unit discount amount
return $item->discount;
}
return ($item->cost / $item->quantity) * ($item->discount / 100);
return ($item->cost * $item->quantity) * ($item->discount / 100);
}
/**
* costWithDiscount
* calculateTaxMap
*
* Helper method to determine the cost INCLUDING discount
* Generates a standard tax_map entry for a given $amount
*
* @param mixed $item
* @return float
* Iterates through all of the globalTaxCategories found in the document
*
* @param float $amount
* @return self
*/
private function costWithDiscount($item): float
private function calculateTaxMap($amount): self
{
$cost = $item->cost;
if ($item->discount != 0) {
if ($this->invoice->is_amount_discount) {
$cost -= $item->discount / $item->quantity;
} else {
$cost -= $cost * $item->discount / 100;
}
foreach($this->globalTaxCategories as $tc)
{
$this->tax_map[] = [
'taxableAmount' => $amount,
'taxAmount' => $amount * ($tc->Percent/100),
'percentage' => $tc->Percent,
];
}
return $cost;
return $this;
}
/**
* getItemTaxes
* getAllUsedTaxes
*
* Builds a tax map for later use when
* collating taxes
* Build a full tax category property based on all
* of the item taxes that have been applied to the invoice.
*
* @param object $item
* @return array
* @return self
*/
private function getItemTaxes(object $item): array
private function getAllUsedTaxes(): self
{
$item_taxes = [];
$this->globalTaxCategories = [];
if(strlen($item->tax_name1 ?? '') > 1) {
collect($this->invoice->line_items)
->flatMap(function ($item) {
return collect([1, 2, 3])
->map(fn ($i) => [
'name' => $item->{"tax_name{$i}"} ?? '',
'percentage' => $item->{"tax_rate{$i}"} ?? 0,
'scheme' => $this->getTaxType($item->tax_id),
])
->filter(fn ($tax) => strlen($tax['name']) > 1);
})
->unique(fn ($tax) => $tax['percentage'] . '_' . $tax['name'])
->values()
->each(function ($tax){
$taxCategory = new \InvoiceNinja\EInvoice\Models\Peppol\TaxCategoryType\TaxCategory();
$taxCategory->ID = new \InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\ID();
$taxCategory->ID->value = $tax['scheme'];
$taxCategory->Percent = (string)$tax['percentage'];
$taxScheme = new \InvoiceNinja\EInvoice\Models\Peppol\TaxSchemeType\TaxScheme();
$taxScheme->ID = new \InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\ID();
$taxScheme->ID->value = $this->standardizeTaxSchemeId($tax['name']);
$taxCategory->TaxScheme = $taxScheme;
$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;
$this->globalTaxCategories[] = $taxCategory;
});
$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 = $this->getTaxType($item->tax_id);
$tc->ID = $id;
$tc->Percent = $item->tax_rate1;
$ts = new TaxScheme();
return $this;
$id = new ID();
$id->value = $this->standardizeTaxSchemeId($item->tax_name1);
$jurisdiction = $this->getJurisdiction();
$ts->JurisdictionRegionAddress[] = $jurisdiction;
$ts->ID = $id;
$tc->TaxScheme = $ts;
$tax_subtotal->TaxCategory = $tc;
$tax_total = new TaxTotal();
$tax_total->TaxAmount = $tax_amount;
$tax_total->TaxSubtotal[] = $tax_subtotal;
$this->tax_map[] = [
'taxableAmount' => $taxable_amount->amount,
'taxAmount' => $tax_amount->amount,
'percentage' => $item->tax_rate1,
];
$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 = $this->getTaxType($item->tax_id);
$tc->ID = $id;
$tc->Percent = $item->tax_rate2;
$ts = new TaxScheme();
$id = new ID();
$id->value = $this->standardizeTaxSchemeId($item->tax_name2);
$jurisdiction = $this->getJurisdiction();
$ts->JurisdictionRegionAddress[] = $jurisdiction;
$ts->ID = $id;
$tc->TaxScheme = $ts;
$tax_subtotal->TaxCategory = $tc;
$tax_total = new TaxTotal();
$tax_total->TaxAmount = $tax_amount;
$tax_total->TaxSubtotal[] = $tax_subtotal;
$this->tax_map[] = [
'taxableAmount' => $taxable_amount->amount,
'taxAmount' => $tax_amount->amount,
'percentage' => $item->tax_rate2,
];
$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 = $this->getTaxType($item->tax_id);
$tc->ID = $id;
$tc->Percent = $item->tax_rate3;
$ts = new TaxScheme();
$id = new ID();
$id->value = $this->standardizeTaxSchemeId($item->tax_name3);
$jurisdiction = $this->getJurisdiction();
$ts->JurisdictionRegionAddress[] = $jurisdiction;
$ts->ID = $id;
$tc->TaxScheme = $ts;
$tax_subtotal->TaxCategory = $tc;
$tax_total = new TaxTotal();
$tax_total->TaxAmount = $tax_amount;
$tax_total->TaxSubtotal[] = $tax_subtotal;
$this->tax_map[] = [
'taxableAmount' => $taxable_amount->amount,
'taxAmount' => $tax_amount->amount,
'percentage' => $item->tax_rate3,
];
$item_taxes[] = $tax_total;
}
return $item_taxes;
}
/**
* getAccountingSupplierParty
*
@ -951,18 +967,20 @@ class Peppol extends AbstractService
$pi = new PartyIdentification();
$vatID = new ID();
$vatID->schemeID = $this->resolveScheme();
$vatID->value = $this->company->settings->vat_number; //todo if we are cross border - switch to the supplier local vat number
$vatID->value = $this->override_vat_number ?? $this->company->settings->vat_number; //todo if we are cross border - switch to the supplier local vat number
$pi->ID = $vatID;
$party->PartyIdentification[] = $pi;
$pts = new \InvoiceNinja\EInvoice\Models\Peppol\PartyTaxSchemeType\PartyTaxScheme();
$companyID = new \InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\CompanyID();
$companyID->value = $this->invoice->company->settings->vat_number;
$companyID->value = $this->override_vat_number ?? $this->company->settings->vat_number;
$pts->CompanyID = $companyID;
$ts = new TaxScheme();
$ts->ID = $vatID;
$id = new ID();
$id->value = $this->standardizeTaxSchemeId('vat');
$ts->ID = $id;
$pts->TaxScheme = $ts;
//@todo if we have an exact GLN/routing number we should update this, otherwise Storecove will proxy and update on transit
@ -1033,6 +1051,21 @@ class Peppol extends AbstractService
$party->PartyIdentification[] = $pi;
$pts = new \InvoiceNinja\EInvoice\Models\Peppol\PartyTaxSchemeType\PartyTaxScheme();
$companyID = new \InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\CompanyID();
$companyID->value = $this->invoice->client->vat_number;
$pts->CompanyID = $companyID;
$ts = new TaxScheme();
$id = new ID();
$id->value = $this->standardizeTaxSchemeId('vat');
$ts->ID = $id;
$pts->TaxScheme = $ts;
$party->PartyTaxScheme[] = $pts;
}
$party_name = new PartyName();
@ -1086,6 +1119,37 @@ class Peppol extends AbstractService
return $acp;
}
private function getDelivery(): array
{
$delivery = new \InvoiceNinja\EInvoice\Models\Peppol\DeliveryType\Delivery();
$location = new \InvoiceNinja\EInvoice\Models\Peppol\LocationType\DeliveryLocation();
$address = new Address();
// $address->CityName = $this->invoice->client->city;
// $address->StreetName = $this->invoice->client->address1;
// if (strlen($this->invoice->client->address2 ?? '') > 1) {
// $address->AdditionalStreetName = $this->invoice->client->address2;
// }
// $address->PostalZone = $this->invoice->client->postal_code;
// $address->CountrySubentity = $this->invoice->client->state;
$country = new Country();
$ic = new IdentificationCode();
$shipping = $this->invoice->client->shipping_country ? $this->invoice->client->shipping_country->iso_3166_2 : $this->invoice->client->country->iso_3166_2;
$ic->value = $shipping;
$country->IdentificationCode = $ic;
$address->Country = $country;
$location->Address = $address;
$delivery->DeliveryLocation = $location;
return [$delivery];
}
/**
* getTaxable
*
@ -1109,12 +1173,14 @@ class Peppol extends AbstractService
$total += $line_total;
}
$total = round($total, 2);
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);
}
}
@ -1134,7 +1200,7 @@ class Peppol extends AbstractService
$total += $this->invoice->custom_surcharge4;
}
return $total;
return round($total,2);
}
///////////////// Helper Methods /////////////////////////
@ -1198,41 +1264,29 @@ class Peppol extends AbstractService
{
$tax_total = new TaxTotal();
$taxes = $this->calc->getTaxMap();
$taxes = collect($this->tax_map)
->groupBy('percentage')
->map(function ($group) {
return [
'taxableAmount' => $group->sum('taxableAmount'),
'taxAmount' => $group->sum('taxAmount'),
'percentage' => $group->first()['percentage'],
];
});
foreach($taxes as $grouped_tax)
if(count($taxes) < 1)
{
// Required: TaxAmount (BT-110)
$tax_amount = new TaxAmount();
$tax_amount->currencyID = $this->invoice->client->currency()->code;
$tax_amount->amount = (string)$grouped_tax['taxAmount'];
$tax_amount->amount = (string)0;
$tax_total->TaxAmount = $tax_amount;
// Required: TaxSubtotal (BG-23)
$tax_subtotal = new TaxSubtotal();
// Required: TaxableAmount (BT-116)
$taxable_amount = new TaxableAmount();
$taxable_amount->currencyID = $this->invoice->client->currency()->code;
$taxable_amount->amount = (string)$grouped_tax['taxableAmount'];
$taxable_amount->amount = (string)round($this->invoice->amount,2);
$tax_subtotal->TaxableAmount = $taxable_amount;
// Required: TaxAmount (BT-117)
$subtotal_tax_amount = new TaxAmount();
$subtotal_tax_amount->currencyID = $this->invoice->client->currency()->code;
$subtotal_tax_amount->amount = (string)$grouped_tax['taxAmount'];
$subtotal_tax_amount->amount = (string)0;
$tax_subtotal->TaxAmount = $subtotal_tax_amount;
@ -1241,11 +1295,9 @@ class Peppol extends AbstractService
// Required: TaxCategory ID (BT-118)
$category_id = new ID();
$category_id->value = 'S'; // Standard rate
$tax_category->ID = $category_id;
$category_id->value = $this->tax_category_id; // Exempt
// Required: TaxCategory Rate (BT-119)
$tax_category->Percent = (string)$grouped_tax['percentage'];
$tax_category->ID = $category_id;
// Required: TaxScheme (BG-23)
$tax_scheme = new TaxScheme();
@ -1254,7 +1306,67 @@ class Peppol extends AbstractService
$tax_scheme->ID = $scheme_id;
$tax_category->TaxScheme = $tax_scheme;
$tax_subtotal->TaxCategory = $tax_category;
$tax_subtotal->TaxCategory = $this->globalTaxCategories[0];
$tax_total->TaxSubtotal[] = $tax_subtotal;
$this->p_invoice->TaxTotal[] = $tax_total;
return $this;
}
foreach($taxes as $key => $grouped_tax)
{
// Required: TaxAmount (BT-110)
$tax_amount = new TaxAmount();
$tax_amount->currencyID = $this->invoice->client->currency()->code;
$tax_amount->amount = (string)$grouped_tax['total'];
$tax_total->TaxAmount = $tax_amount;
// Required: TaxSubtotal (BG-23)
$tax_subtotal = new TaxSubtotal();
// Required: TaxableAmount (BT-116)
$taxable_amount = new TaxableAmount();
$taxable_amount->currencyID = $this->invoice->client->currency()->code;
if(floatval($grouped_tax['total']) === 0.0)
$taxable_amount->amount = (string)round($this->invoice->amount, 2);
else
$taxable_amount->amount = (string)round($grouped_tax['base_amount'],2);
$tax_subtotal->TaxableAmount = $taxable_amount;
// Required: TaxAmount (BT-117)
$subtotal_tax_amount = new TaxAmount();
$subtotal_tax_amount->currencyID = $this->invoice->client->currency()->code;
$subtotal_tax_amount->amount = (string)round($grouped_tax['total'],2);
$tax_subtotal->TaxAmount = $subtotal_tax_amount;
// Required: TaxCategory (BG-23)
$tax_category = new TaxCategory();
// Required: TaxCategory ID (BT-118)
$category_id = new ID();
$category_id->value = $this->getTaxType($grouped_tax['tax_id']); // Standard rate
$tax_category->ID = $category_id;
// Required: TaxCategory Rate (BT-119)
if($grouped_tax['tax_rate'] > 0)
$tax_category->Percent = (string)$grouped_tax['tax_rate'];
// Required: TaxScheme (BG-23)
$tax_scheme = new TaxScheme();
$scheme_id = new ID();
$scheme_id->value = $this->standardizeTaxSchemeId("taxname");
$tax_scheme->ID = $scheme_id;
$tax_category->TaxScheme = $tax_scheme;
$tax_subtotal->TaxCategory = $this->globalTaxCategories[0];
$tax_total->TaxSubtotal[] = $tax_subtotal;
$this->p_invoice->TaxTotal[] = $tax_total;
@ -1268,7 +1380,7 @@ class Peppol extends AbstractService
//calculate nexus
$country_code = $this->company->country()->iso_3166_2;
$br = new BaseRule();
$br = new \App\DataMapper\Tax\BaseRule();
$eu_countries = $br->eu_country_codes;
if($this->invoice->client->country->iso_3166_2 == $this->company->country()->iso_3166_2){
@ -1282,6 +1394,9 @@ class Peppol extends AbstractService
//EU Sale
if($this->company->tax_data->regions->EU->has_sales_above_threshold || !$this->invoice->client->has_valid_vat_number){ //over threshold - tax in buyer country
$country_code = $this->invoice->client->country->iso_3166_2;
if(isset($this->ninja_invoice->company->tax_data->regions->EU->subregions->{$country_code}->vat_number))
$this->override_vat_number = $this->ninja_invoice->company->tax_data->regions->EU->subregions->{$country_code}->vat_number;
}
}

View File

@ -102,7 +102,7 @@ class EntityLevel
}
catch(PeppolValidationException $e) {
$this->errors['invoice'] = ['field' => $e->getInvalidField()];
$this->errors['invoice'] = ['field' => $e->getInvalidField(), 'label' => $e->getInvalidField()];
};
@ -126,13 +126,13 @@ class EntityLevel
if($field == 'country_id' && $client->country_id >=1)
continue;
$errors[] = ['field' => ctrans("texts.{$field}")];
$errors[] = ['field' => $field, 'label' => ctrans("texts.{$field}")];
}
//If not an individual, you MUST have a VAT number
if ($client->classification != 'individual' && !$this->validString($client->vat_number)) {
$errors[] = ['field' => ctrans("texts.vat_number")];
$errors[] = ['field' => 'vat_number', 'label' => ctrans("texts.vat_number")];
}
return $errors;
@ -180,7 +180,7 @@ class EntityLevel
if($this->validString($settings_object->getSetting($field)))
continue;
$errors[] = ['field' => ctrans("texts.{$field}")];
$errors[] = ['field' => $field, 'label' => ctrans("texts.{$field}")];
}
@ -191,8 +191,12 @@ class EntityLevel
//If not an individual, you MUST have a VAT number
if($company->getSetting('classification') != 'individual' && !$this->validString($company->getSetting('vat_number')))
{
$errors[] = ['field' => ctrans("texts.vat_number")];
$errors[] = ['field' => 'vat_number', 'label' => ctrans("texts.vat_number")];
}
elseif ($company->getSetting('classification') == 'individual' && !$this->validString($company->getSetting('id_number'))) {
$errors[] = ['field' => 'id_number', 'label' => ctrans("texts.id_number")];
}
// foreach($this->company_fields as $field)
// {

View File

@ -65,11 +65,13 @@ class ZugferdEDokument extends AbstractService
$this->xdocument = ZugferdDocumentBuilder::CreateNew($profile);
$user_or_company_phone = strlen($this->document->user->present()->phone()) > 3 ? $this->document->user->present()->phone() : $company->present()->phone;
$this->xdocument
->setDocumentSupplyChainEvent(date_create($this->document->date ?? now()->format('Y-m-d')))
->setDocumentSeller($company->getSetting('name'))
->setDocumentSellerAddress($company->getSetting("address1"), $company->getSetting("address2"), "", $company->getSetting("postal_code"), $company->getSetting("city"), $company->country()->iso_3166_2, $company->getSetting("state"))
->setDocumentSellerContact($this->document->user->present()->getFullName(), "", $this->document->user->present()->phone(), "", $this->document->user->email)
->setDocumentSellerContact($this->document->user->present()->getFullName(), "", $user_or_company_phone, "", $this->document->user->email)
->setDocumentSellerCommunication("EM", $this->document->user->email)
->setDocumentBuyer($client->present()->name(), $client->number)
->setDocumentBuyerAddress($client->address1, "", "", $client->postal_code, $client->city, $client->country->iso_3166_2, $client->state)
@ -121,7 +123,7 @@ class ZugferdEDokument extends AbstractService
if (isset($client->shipping_address1) && $client->shipping_country) {
$this->xdocument->setDocumentShipToAddress($client->shipping_address1, $client->shipping_address2, "", $client->shipping_postal_code, $client->shipping_city, $client->shipping_country->iso_3166_2, $client->shipping_state);
}
$custom_value1=$company->settings->custom_value1;
$custom_value1 = $company->settings->custom_value1;
//BR-DE-23 - If „Payment means type code“ (BT-81) contains a code for credit transfer (30, 58), „CREDIT TRANSFER“ (BG-17) shall be provided.
//Payment Means - Switcher
if(isset($custom_value1) && !empty($custom_value1) && ($custom_value1 == '30'|| $custom_value1=='58')) {

View File

@ -138,6 +138,7 @@ class TemplateAction implements ShouldQueue
if($this->send_email) {
$this->sendEmail($pdf, $template);
return;
} else {
$filename = "templates/{$this->hash}.pdf";
Storage::disk(config('filesystems.default'))->put($filename, $pdf);

View File

@ -94,6 +94,7 @@ class AccountTransformer extends EntityTransformer
'tax_api_enabled' => (bool) config('services.tax.zip_tax.key') ? true : false,
'nordigen_enabled' => (bool) (config('ninja.nordigen.secret_id') && config('ninja.nordigen.secret_key')) ? true : false,
'upload_extensions' => (string) "png,ai,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx,webp,xml,zip,csv,ods,odt,odp,".config('ninja.upload_extensions'),
'e_invoice_quota' => (int) $account->e_invoice_quota,
];
}

View File

@ -223,7 +223,6 @@ class CompanyTransformer extends EntityTransformer
'has_quickbooks_token' => $company->quickbooks ? true : false,
'is_quickbooks_token_active' => $company->quickbooks?->accessTokenKey ?? false,
'legal_entity_id' => $company->legal_entity_id ?? null,
'e_invoicing_token' => $company->e_invoicing_token ?? null,
];
}

View File

@ -831,7 +831,7 @@ class HtmlEngine
}
}
if (!$this->entity->company->tax_data->regions->EU->has_sales_above_threshold ?? false){ //@phpstan-ignore-line
if (isset($this->entity->company->tax_data->regions->EU->has_sales_above_threshold) && !$this->entity->company->tax_data->regions->EU->has_sales_above_threshold){
$tax_label .= ctrans('text.small_company_info') ."<br>";
}

View File

@ -294,151 +294,160 @@ class TemplateEngine
$this->entity = 'payment';
}
DB::connection(config('database.default'))->beginTransaction();
try {
DB::connection(config('database.default'))->beginTransaction();
/** @var \App\Models\User $user */
$user = auth()->user();
/** @var \App\Models\User $user */
$user = auth()->user();
$vendor = false;
/** @var \App\Models\Client $client */
$client = Client::factory()->create([
'user_id' => $user->id,
'company_id' => $user->company()->id,
]);
$vendor = false;
/** @var \App\Models\Client $client */
$client = Client::factory()->create([
'user_id' => $user->id,
'company_id' => $user->company()->id,
]);
/** @var \App\Models\ClientContact $contact */
$contact = ClientContact::factory()->create([
'user_id' => $user->id,
'company_id' => $user->company()->id,
'client_id' => $client->id,
'is_primary' => 1,
'send_email' => true,
]);
if ($this->entity == 'payment') {
/** @var \App\Models\Payment $payment */
$payment = Payment::factory()->create([
/** @var \App\Models\ClientContact $contact */
$contact = ClientContact::factory()->create([
'user_id' => $user->id,
'company_id' => $user->company()->id,
'client_id' => $client->id,
'amount' => 10,
'applied' => 10,
'refunded' => 5,
]);
$this->entity_obj = $payment;
/** @var \App\Models\Invoice $invoice */
$invoice = Invoice::factory()->create([
'user_id' => $user->id,
'company_id' => $user->company()->id,
'client_id' => $client->id,
'amount' => 10,
'balance' => 10,
'number' => rand(1, 10000)
]);
/** @var \App\Models\InvoiceInvitation $invitation */
$invitation = InvoiceInvitation::factory()->create([
'user_id' => $user->id,
'company_id' => $user->company()->id,
'invoice_id' => $invoice->id,
'client_contact_id' => $contact->id,
]);
/** @var \App\Models\Invoice $invoice */
$this->entity_obj->invoices()->attach($invoice->id, [
'amount' => 10,
]);
}
if (!$this->entity || $this->entity == 'invoice') {
/** @var \App\Models\Invoice $invoice */
$invoice = Invoice::factory()->create([
'user_id' => $user->id,
'company_id' => $user->company()->id,
'client_id' => $client->id,
'amount' => '10',
'balance' => '10',
]);
$this->entity_obj = $invoice;
$invitation = InvoiceInvitation::factory()->create([
'user_id' => $user->id,
'company_id' => $user->company()->id,
'invoice_id' => $this->entity_obj->id,
'client_contact_id' => $contact->id,
]);
}
if ($this->entity == 'quote') {
/** @var \App\Models\Quote $quote */
$quote = Quote::factory()->create([
'user_id' => $user->id,
'company_id' => $user->company()->id,
'client_id' => $client->id,
]);
$this->entity_obj = $quote;
$invitation = QuoteInvitation::factory()->create([
'user_id' => $user->id,
'company_id' => $user->company()->id,
'quote_id' => $this->entity_obj->id,
'client_contact_id' => $contact->id,
]);
}
if ($this->entity == 'purchaseOrder') {
/** @var \App\Models\Vendor $vendor **/
$vendor = Vendor::factory()->create([
'user_id' => $user->id,
'company_id' => $user->company()->id,
]);
/** @var \App\Models\VendorContact $contact **/
$contact = VendorContact::factory()->create([
'user_id' => $user->id,
'company_id' => $user->company()->id,
'vendor_id' => $vendor->id,
'is_primary' => 1,
'send_email' => true,
]);
/** @var \App\Models\PurchaseOrder $purchase_order **/
$purchase_order = PurchaseOrder::factory()->create([
'user_id' => $user->id,
'company_id' => $user->company()->id,
'vendor_id' => $vendor->id,
]);
if ($this->entity == 'payment') {
/** @var \App\Models\Payment $payment */
$payment = Payment::factory()->create([
'user_id' => $user->id,
'company_id' => $user->company()->id,
'client_id' => $client->id,
'amount' => 10,
'applied' => 10,
'refunded' => 5,
]);
$this->entity_obj = $purchase_order;
$this->entity_obj = $payment;
/** @var \App\Models\Invoice $invoice */
$invoice = Invoice::factory()->create([
'user_id' => $user->id,
'company_id' => $user->company()->id,
'client_id' => $client->id,
'amount' => 10,
'balance' => 10,
'number' => rand(1, 10000)
]);
/** @var \App\Models\InvoiceInvitation $invitation */
$invitation = InvoiceInvitation::factory()->create([
'user_id' => $user->id,
'company_id' => $user->company()->id,
'invoice_id' => $invoice->id,
'client_contact_id' => $contact->id,
]);
/** @var \App\Models\Invoice $invoice */
$this->entity_obj->invoices()->attach($invoice->id, [
'amount' => 10,
]);
}
if (!$this->entity || $this->entity == 'invoice') {
/** @var \App\Models\Invoice $invoice */
$invoice = Invoice::factory()->create([
'user_id' => $user->id,
'company_id' => $user->company()->id,
'client_id' => $client->id,
'amount' => '10',
'balance' => '10',
]);
$this->entity_obj = $invoice;
$invitation = InvoiceInvitation::factory()->create([
'user_id' => $user->id,
'company_id' => $user->company()->id,
'invoice_id' => $this->entity_obj->id,
'client_contact_id' => $contact->id,
]);
}
if ($this->entity == 'quote') {
/** @var \App\Models\Quote $quote */
$quote = Quote::factory()->create([
'user_id' => $user->id,
'company_id' => $user->company()->id,
'client_id' => $client->id,
]);
$this->entity_obj = $quote;
$invitation = QuoteInvitation::factory()->create([
'user_id' => $user->id,
'company_id' => $user->company()->id,
'quote_id' => $this->entity_obj->id,
'client_contact_id' => $contact->id,
]);
}
if ($this->entity == 'purchaseOrder') {
/** @var \App\Models\Vendor $vendor **/
$vendor = Vendor::factory()->create([
'user_id' => $user->id,
'company_id' => $user->company()->id,
]);
/** @var \App\Models\VendorContact $contact **/
$contact = VendorContact::factory()->create([
'user_id' => $user->id,
'company_id' => $user->company()->id,
'vendor_id' => $vendor->id,
'is_primary' => 1,
'send_email' => true,
]);
/** @var \App\Models\PurchaseOrder $purchase_order **/
$purchase_order = PurchaseOrder::factory()->create([
'user_id' => $user->id,
'company_id' => $user->company()->id,
'vendor_id' => $vendor->id,
]);
$this->entity_obj = $purchase_order;
/** @var \App\Models\PurchaseOrderInvitation $invitation **/
$invitation = PurchaseOrderInvitation::factory()->create([
'user_id' => $user->id,
'company_id' => $user->company()->id,
'purchase_order_id' => $this->entity_obj->id,
'vendor_contact_id' => $contact->id,
]);
}
if ($vendor) {
$this->entity_obj->setRelation('invitations', $invitation);
$this->entity_obj->setRelation('vendor', $vendor);
$this->entity_obj->setRelation('company', $user->company());
$this->entity_obj->load('vendor');
$vendor->setRelation('company', $user->company());
$vendor->load('company');
} else {
$this->entity_obj->setRelation('invitations', $invitation);
$this->entity_obj->setRelation('client', $client);
$this->entity_obj->setRelation('company', $user->company());
$this->entity_obj->load('client');
$client->setRelation('company', $user->company());
$client->load('company');
}
}catch(\Throwable $th){
nlog("Throwable:: transaction:: TemplateEngine MockEntity");
DB::connection(config('database.default'))->rollBack();
/** @var \App\Models\PurchaseOrderInvitation $invitation **/
$invitation = PurchaseOrderInvitation::factory()->create([
'user_id' => $user->id,
'company_id' => $user->company()->id,
'purchase_order_id' => $this->entity_obj->id,
'vendor_contact_id' => $contact->id,
]);
}
if ($vendor) {
$this->entity_obj->setRelation('invitations', $invitation);
$this->entity_obj->setRelation('vendor', $vendor);
$this->entity_obj->setRelation('company', $user->company());
$this->entity_obj->load('vendor');
$vendor->setRelation('company', $user->company());
$vendor->load('company');
} else {
$this->entity_obj->setRelation('invitations', $invitation);
$this->entity_obj->setRelation('client', $client);
$this->entity_obj->setRelation('company', $user->company());
$this->entity_obj->load('client');
$client->setRelation('company', $user->company());
$client->load('company');
}
}
private function tearDown()

View File

@ -24,17 +24,21 @@ trait WithSecureContext
*/
public function getContext(): mixed
{
return session()->get('secureContext.invoice-pay');
return \Illuminate\Support\Facades\Cache::get(session()->getId()) ?? [];
// return session()->get('secureContext.invoice-pay');
}
public function setContext(string $property, $value): array
{
$clone = session()->pull('secureContext.invoice-pay', default: []);
$clone = $this->getContext();
// $clone = session()->pull('secureContext.invoice-pay', default: []);
data_set($clone, $property, $value);
session()->put('secureContext.invoice-pay', $clone);
// session()->put('secureContext.invoice-pay', $clone);
\Illuminate\Support\Facades\Cache::put(session()->getId(), $clone, now()->addHour());
$this->dispatch(self::CONTEXT_UPDATE);
return $clone;
@ -42,6 +46,7 @@ trait WithSecureContext
public function resetContext(): void
{
\Illuminate\Support\Facades\Cache::forget(session()->getId());
session()->forget('secureContext.invoice-pay');
}
}

View File

@ -226,4 +226,4 @@
],
"minimum-stability": "dev",
"prefer-stable": true
}
}

379
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -55,7 +55,7 @@ return [
|
*/
'after_commit' => false,
'after_commit' => true,
/*
|--------------------------------------------------------------------------

View File

@ -27,7 +27,7 @@ class InvoiceFactory extends Factory
return [
'status_id' => Invoice::STATUS_SENT,
'number' => $this->faker->ean13(),
'discount' => $this->faker->numberBetween(1, 10),
'discount' => rand(1,10),
'is_amount_discount' => (bool) random_int(0, 1),
'tax_name1' => 'GST',
'tax_rate1' => 10,

View File

@ -0,0 +1,42 @@
<?php
use App\Utils\Ninja;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
public function up(): void
{
Schema::create('e_invoicing_tokens', function (Blueprint $table) {
$table->id();
$table->string('license',64);
$table->uuid('token')->unique()->index();
$table->string('account_key',64);
$table->timestamps();
});
if (Ninja::isSelfHost()) {
Schema::table('companies', function (Blueprint $table) {
if (Schema::hasColumn('companies', 'e_invoicing_token')) {
$table->dropColumn('e_invoicing_token');
}
});
}
if (!Schema::hasColumn('accounts', 'e_invoicing_token')) {
Schema::table('accounts', function (Blueprint $table) {
$table->string('e_invoicing_token')->nullable();
});
}
}
public function down(): void
{
}
};

View File

@ -1,5 +1,6 @@
<?php
use App\Utils\Ninja;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
@ -8,8 +9,23 @@ return new class extends Migration
{
public function up(): void
{
// Schema::table('companies', function (Blueprint $table) {
// $table->string('e_invoicing_token')->nullable();
// });
if(Ninja::isSelfHost())
{
Schema::table('companies', function (Blueprint $table) {
if (Schema::hasColumn('companies', 'e_invoicing_token')) {
$table->dropColumn('e_invoicing_token');
}
});
}
if (!Schema::hasColumn('accounts', 'e_invoicing_token')) {
Schema::table('accounts', function (Blueprint $table) {
$table->string('e_invoicing_token')->nullable();
});
}
}
};

View File

@ -5405,6 +5405,47 @@ $lang = array(
'invalid_vat_number' => "The VAT number is not valid for the selected country. Format should be Country Code followed by number only ie, DE123456789",
'acts_as_sender' => 'Acts as Sender',
'acts_as_receiver' => 'Acts as Receiver',
'peppol_token_generated' => 'PEPPOL token successfully generated.',
'peppol_token_description' => 'Token is used as another step to make sure invoices are sent securely. Unlike white-label licenses, token can be rotated at any point without need to wait on Invoice Ninja support.',
'peppol_token_warning' => 'You need to generate a token to continue.',
'generate_token' => 'Generate Token',
'total_credits_amount' => 'Amount of Credits',
'sales_above_threshold' => 'Sales above threshold',
'changing_vat_and_id_number_note' => 'You can\'t change your VAT number or ID number once PEPPOL is set up.',
'iban_help' => 'The full IBAN number',
'bic_swift' => 'BIC/Swift code',
'bic_swift_help' => 'The Bank identifer',
'payer_bank_account' => 'Payer Bank Account Number',
'payer_bank_account_help' => 'The bank account number of the payer',
'bsb_sort' => 'BSB / Sort Code',
'bsb_sort_help' => 'Bank Branch Code',
'card_type' => 'Card Type',
'card_type_help' => 'ie. VISA, AMEX',
'card_number_help' => 'last 4 digits only',
'card_holder' => 'Card Holder Name',
'tokenize' => 'Tokenize',
'tokenize_help' => 'Tokenize payment method for future use.',
'credit_card_stripe_help' => 'Accept credit card payments using Stripe.',
'bank_transfer_stripe_help' => 'ACH direct debit. USD payments, instant verification available.',
'alipay_stripe_help' => 'Alipay allows users in China to pay securely using their mobile wallets.',
'sofort_stripe_help' => 'Sofort is a popular European payment method that enables bank transfers in real-time, primarily used in Germany and Austria.',
'apple_pay_stripe_help' => 'Apple/Google Pay for users with Apple/Android devices, using saved card information for easy checkout.',
'sepa_stripe_help' => 'SEPA Direct Debit (Single Euro Payments Area).',
'bancontact_stripe_help' => 'Bancontact is a widely used payment method in Belgium.',
'ideal_stripe_help' => 'iDEAL is the most popular payment method in the Netherlands.',
'giropay_stripe_help' => 'Giropay is a German payment method that facilitates secure and immediate online bank transfers.',
'przelewy24_stripe_help' => 'Przelewy24 is a common payment method in Poland.',
'direct_debit_stripe_help' => 'Stripe Bank Transfers using Stripes virtual bank accounts, available in Japan, UK, USA, Europe and Mexico. Ensure this is enabled in Stripe!',
'eps_stripe_help' => 'EPS is an Austrian online payment system.',
'acss_stripe_help' => 'ACSS (Automated Clearing Settlement System) Direct Debit for Canadian bank accounts.',
'becs_stripe_help' => 'BECS Direct Debit for Australian bank accounts.',
'klarna_stripe_help' => 'Klarna buy now and pay later in installments or on a set schedule.',
'bacs_stripe_help' => 'BACS Direct Debit for UK bank accounts, commonly used for subscription billing.',
'fpx_stripe_help' => 'FPX is a popular online payment method in Malaysia.',
'payment_means' => 'Payment Means',
'act_as_sender' => 'Send E-Invoice',
'act_as_receiver' => 'Receive E-Invoice',
'saved_einvoice_details' => 'Saved E-Invoice Settings',
'sales_above_threshold' => 'Sales above threshold',
);

View File

@ -5403,6 +5403,7 @@ Développe automatiquement la section des notes dans le tableau de produits pour
'invalid_vat_number' => "Le numéro de TVA n'est pas valide pour le pays sélectionné. Le format doit être le code du pays suivi uniquement du numéro, par exemple DE123456789.",
'acts_as_sender' => 'Agit en tant qu\'expéditeur',
'acts_as_receiver' => 'Agit en tant que destinataire',
'sales_above_threshold' => 'Ventes dépassant le seuil',
);
return $lang;

View File

@ -5405,6 +5405,7 @@ $lang = array(
'invalid_vat_number' => "Mã số VAT không hợp lệ đối với quốc gia đã chọn. Định dạng phải là Mã quốc gia theo sau là số, ví dụ: DE123456789",
'acts_as_sender' => 'Hoạt động như Người gửi',
'acts_as_receiver' => 'Hoạt động như Người nhận',
'sales_above_threshold' => 'Doanh số vượt ngưỡng',
);
return $lang;

File diff suppressed because one or more lines are too long

109
public/build/assets/app-780c4e5a.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -12,7 +12,7 @@
"file": "assets/wait-8f4ae121.js"
},
"resources/js/app.js": {
"file": "assets/app-2353b88b.js",
"file": "assets/app-780c4e5a.js",
"imports": [
"_index-08e160a7.js",
"__commonjsHelpers-725317a4.js"

View File

@ -17,9 +17,9 @@ var __copyProps = (to, from, except, desc) => {
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod));
// node_modules/alpinejs/dist/module.cjs.js
// ../alpine/packages/alpinejs/dist/module.cjs.js
var require_module_cjs = __commonJS({
"node_modules/alpinejs/dist/module.cjs.js"(exports, module) {
"../alpine/packages/alpinejs/dist/module.cjs.js"(exports, module) {
var __create2 = Object.create;
var __defProp2 = Object.defineProperty;
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
@ -1432,10 +1432,10 @@ var require_module_cjs = __commonJS({
});
}
function cleanupElement(el) {
if (el._x_cleanups) {
while (el._x_cleanups.length)
el._x_cleanups.pop()();
}
var _a, _b;
(_a = el._x_effects) == null ? void 0 : _a.forEach(dequeueJob);
while ((_b = el._x_cleanups) == null ? void 0 : _b.length)
el._x_cleanups.pop()();
}
var observer = new MutationObserver(onMutate);
var currentlyObserving = false;
@ -1673,27 +1673,23 @@ var require_module_cjs = __commonJS({
magics[name] = callback;
}
function injectMagics(obj, el) {
let memoizedUtilities = getUtilities(el);
Object.entries(magics).forEach(([name, callback]) => {
let memoizedUtilities = null;
function getUtilities() {
if (memoizedUtilities) {
return memoizedUtilities;
} else {
let [utilities, cleanup] = getElementBoundUtilities(el);
memoizedUtilities = { interceptor, ...utilities };
onElRemoved(el, cleanup);
return memoizedUtilities;
}
}
Object.defineProperty(obj, `$${name}`, {
get() {
return callback(el, getUtilities());
return callback(el, memoizedUtilities);
},
enumerable: false
});
});
return obj;
}
function getUtilities(el) {
let [utilities, cleanup] = getElementBoundUtilities(el);
let utils = { interceptor, ...utilities };
onElRemoved(el, cleanup);
return utils;
}
function tryCatch(el, expression, callback, ...args) {
try {
return callback(...args);
@ -2067,8 +2063,8 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}
function destroyTree(root, walker = walk) {
walker(root, (el) => {
cleanupAttributes(el);
cleanupElement(el);
cleanupAttributes(el);
});
}
function warnAboutMissingPlugins() {
@ -2561,7 +2557,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}
}
function bindInputValue(el, value) {
if (el.type === "radio") {
if (isRadio(el)) {
if (el.attributes.value === void 0) {
el.value = value;
}
@ -2572,7 +2568,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
el.checked = checkedAttrLooseCompare(el.value, value);
}
}
} else if (el.type === "checkbox") {
} else if (isCheckbox(el)) {
if (Number.isInteger(value)) {
el.value = value;
} else if (!Array.isArray(value) && typeof value !== "boolean" && ![null, void 0].includes(value)) {
@ -2648,34 +2644,37 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}
return rawValue ? Boolean(rawValue) : null;
}
var booleanAttributes = /* @__PURE__ */ new Set([
"allowfullscreen",
"async",
"autofocus",
"autoplay",
"checked",
"controls",
"default",
"defer",
"disabled",
"formnovalidate",
"inert",
"ismap",
"itemscope",
"loop",
"multiple",
"muted",
"nomodule",
"novalidate",
"open",
"playsinline",
"readonly",
"required",
"reversed",
"selected",
"shadowrootclonable",
"shadowrootdelegatesfocus",
"shadowrootserializable"
]);
function isBooleanAttr(attrName) {
const booleanAttributes = [
"disabled",
"checked",
"required",
"readonly",
"open",
"selected",
"autofocus",
"itemscope",
"multiple",
"novalidate",
"allowfullscreen",
"allowpaymentrequest",
"formnovalidate",
"autoplay",
"controls",
"loop",
"muted",
"playsinline",
"default",
"ismap",
"reversed",
"async",
"defer",
"nomodule"
];
return booleanAttributes.includes(attrName);
return booleanAttributes.has(attrName);
}
function attributeShouldntBePreservedIfFalsy(name) {
return !["aria-pressed", "aria-checked", "aria-expanded", "aria-selected"].includes(name);
@ -2708,6 +2707,12 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}
return attr;
}
function isCheckbox(el) {
return el.type === "checkbox" || el.localName === "ui-checkbox" || el.localName === "ui-switch";
}
function isRadio(el) {
return el.type === "radio" || el.localName === "ui-radio";
}
function debounce2(func, wait) {
var timeout;
return function() {
@ -2776,10 +2781,10 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
return stores[name];
}
stores[name] = value;
initInterceptors(stores[name]);
if (typeof value === "object" && value !== null && value.hasOwnProperty("init") && typeof value.init === "function") {
stores[name].init();
}
initInterceptors(stores[name]);
}
function getStores() {
return stores;
@ -2861,7 +2866,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
get raw() {
return raw;
},
version: "3.14.1",
version: "3.14.3",
flushAndStopDeferringMutations,
dontAutoEvaluateFunctions,
disableEffectScheduling,
@ -3070,7 +3075,10 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
placeInDom(el._x_teleport, target2, modifiers);
});
};
cleanup(() => clone2.remove());
cleanup(() => mutateDom(() => {
clone2.remove();
destroyTree(clone2);
}));
});
var teleportContainerDuringClone = document.createElement("div");
function getTarget(expression) {
@ -3294,7 +3302,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
setValue(getInputValue(el, modifiers, e, getValue()));
});
if (modifiers.includes("fill")) {
if ([void 0, null, ""].includes(getValue()) || el.type === "checkbox" && Array.isArray(getValue()) || el.tagName.toLowerCase() === "select" && el.multiple) {
if ([void 0, null, ""].includes(getValue()) || isCheckbox(el) && Array.isArray(getValue()) || el.tagName.toLowerCase() === "select" && el.multiple) {
setValue(getInputValue(el, modifiers, { target: el }, getValue()));
}
}
@ -3334,7 +3342,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
return mutateDom(() => {
if (event instanceof CustomEvent && event.detail !== void 0)
return event.detail !== null && event.detail !== void 0 ? event.detail : event.target.value;
else if (el.type === "checkbox") {
else if (isCheckbox(el)) {
if (Array.isArray(currentValue)) {
let newValue = null;
if (modifiers.includes("number")) {
@ -3365,7 +3373,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
});
} else {
let newValue;
if (el.type === "radio") {
if (isRadio(el)) {
if (event.target.checked) {
newValue = event.target.value;
} else {
@ -3558,7 +3566,10 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
el._x_lookup = {};
effect3(() => loop(el, iteratorNames, evaluateItems, evaluateKey));
cleanup(() => {
Object.values(el._x_lookup).forEach((el2) => el2.remove());
Object.values(el._x_lookup).forEach((el2) => mutateDom(() => {
destroyTree(el2);
el2.remove();
}));
delete el._x_prevKeys;
delete el._x_lookup;
});
@ -3627,11 +3638,12 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}
for (let i = 0; i < removes.length; i++) {
let key = removes[i];
if (!!lookup[key]._x_effects) {
lookup[key]._x_effects.forEach(dequeueJob);
}
lookup[key].remove();
lookup[key] = null;
if (!(key in lookup))
continue;
mutateDom(() => {
destroyTree(lookup[key]);
lookup[key].remove();
});
delete lookup[key];
}
for (let i = 0; i < moves.length; i++) {
@ -3752,12 +3764,10 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
});
el._x_currentIfEl = clone2;
el._x_undoIf = () => {
walk(clone2, (node) => {
if (!!node._x_effects) {
node._x_effects.forEach(dequeueJob);
}
mutateDom(() => {
destroyTree(clone2);
clone2.remove();
});
clone2.remove();
delete el._x_currentIfEl;
};
return clone2;
@ -3812,9 +3822,9 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}
});
// node_modules/@alpinejs/collapse/dist/module.cjs.js
// ../../../../usr/local/lib/node_modules/@alpinejs/collapse/dist/module.cjs.js
var require_module_cjs2 = __commonJS({
"node_modules/@alpinejs/collapse/dist/module.cjs.js"(exports, module) {
"../../../../usr/local/lib/node_modules/@alpinejs/collapse/dist/module.cjs.js"(exports, module) {
var __defProp2 = Object.defineProperty;
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
var __getOwnPropNames2 = Object.getOwnPropertyNames;
@ -3887,7 +3897,7 @@ var require_module_cjs2 = __commonJS({
start: { height: current + "px" },
end: { height: full + "px" }
}, () => el._x_isShown = true, () => {
if (Math.abs(el.getBoundingClientRect().height - full) < 1) {
if (el.getBoundingClientRect().height == full) {
el.style.overflow = null;
}
});
@ -3933,9 +3943,9 @@ var require_module_cjs2 = __commonJS({
}
});
// node_modules/@alpinejs/focus/dist/module.cjs.js
// ../../../../usr/local/lib/node_modules/@alpinejs/focus/dist/module.cjs.js
var require_module_cjs3 = __commonJS({
"node_modules/@alpinejs/focus/dist/module.cjs.js"(exports, module) {
"../../../../usr/local/lib/node_modules/@alpinejs/focus/dist/module.cjs.js"(exports, module) {
var __create2 = Object.create;
var __defProp2 = Object.defineProperty;
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
@ -4935,9 +4945,9 @@ var require_module_cjs3 = __commonJS({
}
});
// node_modules/@alpinejs/persist/dist/module.cjs.js
// ../../../../usr/local/lib/node_modules/@alpinejs/persist/dist/module.cjs.js
var require_module_cjs4 = __commonJS({
"node_modules/@alpinejs/persist/dist/module.cjs.js"(exports, module) {
"../../../../usr/local/lib/node_modules/@alpinejs/persist/dist/module.cjs.js"(exports, module) {
var __defProp2 = Object.defineProperty;
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
var __getOwnPropNames2 = Object.getOwnPropertyNames;
@ -5024,9 +5034,9 @@ var require_module_cjs4 = __commonJS({
}
});
// node_modules/@alpinejs/intersect/dist/module.cjs.js
// ../../../../usr/local/lib/node_modules/@alpinejs/intersect/dist/module.cjs.js
var require_module_cjs5 = __commonJS({
"node_modules/@alpinejs/intersect/dist/module.cjs.js"(exports, module) {
"../../../../usr/local/lib/node_modules/@alpinejs/intersect/dist/module.cjs.js"(exports, module) {
var __defProp2 = Object.defineProperty;
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
var __getOwnPropNames2 = Object.getOwnPropertyNames;
@ -5178,9 +5188,9 @@ var require_module_cjs6 = __commonJS({
}
});
// node_modules/@alpinejs/anchor/dist/module.cjs.js
// ../alpine/packages/anchor/dist/module.cjs.js
var require_module_cjs7 = __commonJS({
"node_modules/@alpinejs/anchor/dist/module.cjs.js"(exports, module) {
"../alpine/packages/anchor/dist/module.cjs.js"(exports, module) {
var __defProp2 = Object.defineProperty;
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
var __getOwnPropNames2 = Object.getOwnPropertyNames;
@ -6716,9 +6726,9 @@ var require_nprogress = __commonJS({
}
});
// node_modules/@alpinejs/morph/dist/module.cjs.js
// ../alpine/packages/morph/dist/module.cjs.js
var require_module_cjs8 = __commonJS({
"node_modules/@alpinejs/morph/dist/module.cjs.js"(exports, module) {
"../alpine/packages/morph/dist/module.cjs.js"(exports, module) {
var __defProp2 = Object.defineProperty;
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
var __getOwnPropNames2 = Object.getOwnPropertyNames;
@ -7078,9 +7088,9 @@ var require_module_cjs8 = __commonJS({
}
});
// node_modules/@alpinejs/mask/dist/module.cjs.js
// ../../../../usr/local/lib/node_modules/@alpinejs/mask/dist/module.cjs.js
var require_module_cjs9 = __commonJS({
"node_modules/@alpinejs/mask/dist/module.cjs.js"(exports, module) {
"../../../../usr/local/lib/node_modules/@alpinejs/mask/dist/module.cjs.js"(exports, module) {
var __defProp2 = Object.defineProperty;
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
var __getOwnPropNames2 = Object.getOwnPropertyNames;
@ -8855,8 +8865,10 @@ function restoreScrollPositionOrScrollToTop() {
}
};
queueMicrotask(() => {
scroll(document.body);
document.querySelectorAll(["[x-navigate\\:scroll]", "[wire\\:scroll]"]).forEach(scroll);
queueMicrotask(() => {
scroll(document.body);
document.querySelectorAll(["[x-navigate\\:scroll]", "[wire\\:scroll]"]).forEach(scroll);
});
});
}
@ -9005,6 +9017,44 @@ function injectStyles() {
document.head.appendChild(style);
}
// js/plugins/navigate/popover.js
function packUpPersistedPopovers(persistedEl) {
persistedEl.querySelectorAll(":popover-open").forEach((el) => {
el.setAttribute("data-navigate-popover-open", "");
let animations = el.getAnimations();
el._pausedAnimations = animations.map((animation) => ({
keyframes: animation.effect.getKeyframes(),
options: {
duration: animation.effect.getTiming().duration,
easing: animation.effect.getTiming().easing,
fill: animation.effect.getTiming().fill,
iterations: animation.effect.getTiming().iterations
},
currentTime: animation.currentTime,
playState: animation.playState
}));
animations.forEach((i) => i.pause());
});
}
function unPackPersistedPopovers(persistedEl) {
persistedEl.querySelectorAll("[data-navigate-popover-open]").forEach((el) => {
el.removeAttribute("data-navigate-popover-open");
queueMicrotask(() => {
if (!el.isConnected)
return;
el.showPopover();
el.getAnimations().forEach((i) => i.finish());
if (el._pausedAnimations) {
el._pausedAnimations.forEach(({ keyframes, options, currentTime, now, playState }) => {
let animation = el.animate(keyframes, options);
animation.currentTime = currentTime;
});
delete el._pausedAnimations;
}
});
});
}
// js/plugins/navigate/page.js
var oldBodyScriptTagHashes = [];
var attributesExemptFromScriptTagHashing = [
@ -9143,7 +9193,7 @@ var autofocus = false;
function navigate_default(Alpine19) {
Alpine19.navigate = (url) => {
let destination = createUrlObjectFromString(url);
let prevented = fireEventForOtherLibariesToHookInto("alpine:navigate", {
let prevented = fireEventForOtherLibrariesToHookInto("alpine:navigate", {
url: destination,
history: false,
cached: false
@ -9174,7 +9224,7 @@ function navigate_default(Alpine19) {
storeThePrefetchedHtmlForWhenALinkIsClicked(html, destination, finalDestination);
});
whenItIsReleased(() => {
let prevented = fireEventForOtherLibariesToHookInto("alpine:navigate", {
let prevented = fireEventForOtherLibrariesToHookInto("alpine:navigate", {
url: destination,
history: false,
cached: false
@ -9188,7 +9238,7 @@ function navigate_default(Alpine19) {
function navigateTo(destination, shouldPushToHistoryState = true) {
showProgressBar && showAndStartProgressBar();
fetchHtmlOrUsePrefetchedHtml(destination, (html, finalDestination) => {
fireEventForOtherLibariesToHookInto("alpine:navigating");
fireEventForOtherLibrariesToHookInto("alpine:navigating");
restoreScroll && storeScrollInformationInHtmlBeforeNavigatingAway();
showProgressBar && finishAndHideProgressBar();
cleanupAlpineElementsOnThePageThatArentInsideAPersistedElement();
@ -9196,6 +9246,7 @@ function navigate_default(Alpine19) {
preventAlpineFromPickingUpDomChanges(Alpine19, (andAfterAllThis) => {
enablePersist && storePersistantElementsForLater((persistedEl) => {
packUpPersistedTeleports(persistedEl);
packUpPersistedPopovers(persistedEl);
});
if (shouldPushToHistoryState) {
updateUrlAndStoreLatestHtmlForFutureBackButtons(html, finalDestination);
@ -9206,6 +9257,7 @@ function navigate_default(Alpine19) {
removeAnyLeftOverStaleTeleportTargets(document.body);
enablePersist && putPersistantElementsBack((persistedEl, newStub) => {
unPackPersistedTeleports(persistedEl);
unPackPersistedPopovers(persistedEl);
});
restoreScrollPositionOrScrollToTop();
afterNewScriptsAreDoneLoading(() => {
@ -9214,7 +9266,7 @@ function navigate_default(Alpine19) {
autofocus && autofocusElementsWithTheAutofocusAttribute();
});
nowInitializeAlpineOnTheNewPage(Alpine19);
fireEventForOtherLibariesToHookInto("alpine:navigated");
fireEventForOtherLibrariesToHookInto("alpine:navigated");
});
});
});
@ -9224,7 +9276,7 @@ function navigate_default(Alpine19) {
whenTheBackOrForwardButtonIsClicked((ifThePageBeingVisitedHasntBeenCached) => {
ifThePageBeingVisitedHasntBeenCached((url) => {
let destination = createUrlObjectFromString(url);
let prevented = fireEventForOtherLibariesToHookInto("alpine:navigate", {
let prevented = fireEventForOtherLibrariesToHookInto("alpine:navigate", {
url: destination,
history: true,
cached: false
@ -9236,7 +9288,7 @@ function navigate_default(Alpine19) {
});
}, (html, url, currentPageUrl, currentPageKey) => {
let destination = createUrlObjectFromString(url);
let prevented = fireEventForOtherLibariesToHookInto("alpine:navigate", {
let prevented = fireEventForOtherLibrariesToHookInto("alpine:navigate", {
url: destination,
history: true,
cached: true
@ -9244,29 +9296,31 @@ function navigate_default(Alpine19) {
if (prevented)
return;
storeScrollInformationInHtmlBeforeNavigatingAway();
fireEventForOtherLibariesToHookInto("alpine:navigating");
fireEventForOtherLibrariesToHookInto("alpine:navigating");
updateCurrentPageHtmlInSnapshotCacheForLaterBackButtonClicks(currentPageUrl, currentPageKey);
preventAlpineFromPickingUpDomChanges(Alpine19, (andAfterAllThis) => {
enablePersist && storePersistantElementsForLater((persistedEl) => {
packUpPersistedTeleports(persistedEl);
packUpPersistedPopovers(persistedEl);
});
swapCurrentPageWithNewHtml(html, () => {
removeAnyLeftOverStaleProgressBars();
removeAnyLeftOverStaleTeleportTargets(document.body);
enablePersist && putPersistantElementsBack((persistedEl, newStub) => {
unPackPersistedTeleports(persistedEl);
unPackPersistedPopovers(persistedEl);
});
restoreScrollPositionOrScrollToTop();
andAfterAllThis(() => {
autofocus && autofocusElementsWithTheAutofocusAttribute();
nowInitializeAlpineOnTheNewPage(Alpine19);
fireEventForOtherLibariesToHookInto("alpine:navigated");
fireEventForOtherLibrariesToHookInto("alpine:navigated");
});
});
});
});
setTimeout(() => {
fireEventForOtherLibariesToHookInto("alpine:navigated");
fireEventForOtherLibrariesToHookInto("alpine:navigated");
});
}
function fetchHtmlOrUsePrefetchedHtml(fromDestination, callback) {
@ -9283,7 +9337,7 @@ function preventAlpineFromPickingUpDomChanges(Alpine19, callback) {
});
});
}
function fireEventForOtherLibariesToHookInto(name, detail) {
function fireEventForOtherLibrariesToHookInto(name, detail) {
let event = new CustomEvent(name, {
cancelable: true,
bubbles: true,
@ -9799,6 +9853,7 @@ function morph2(component, el, html) {
},
lookahead: false
});
trigger("morphed", { el, component });
}
function isntElement(el) {
return typeof el.hasAttribute !== "function";
@ -10868,3 +10923,4 @@ focus-trap/dist/focus-trap.js:
* @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
*)
*/
//# sourceMappingURL=livewire.esm.js.map

File diff suppressed because one or more lines are too long

View File

@ -712,7 +712,7 @@
uploadManager.cancelUpload(name, cancelledCallback);
}
// node_modules/alpinejs/dist/module.esm.js
// ../alpine/packages/alpinejs/dist/module.esm.js
var flushPending = false;
var flushing = false;
var queue = [];
@ -851,10 +851,9 @@
});
}
function cleanupElement(el) {
if (el._x_cleanups) {
while (el._x_cleanups.length)
el._x_cleanups.pop()();
}
el._x_effects?.forEach(dequeueJob);
while (el._x_cleanups?.length)
el._x_cleanups.pop()();
}
var observer = new MutationObserver(onMutate);
var currentlyObserving = false;
@ -1092,27 +1091,23 @@
magics[name] = callback;
}
function injectMagics(obj, el) {
let memoizedUtilities = getUtilities(el);
Object.entries(magics).forEach(([name, callback]) => {
let memoizedUtilities = null;
function getUtilities() {
if (memoizedUtilities) {
return memoizedUtilities;
} else {
let [utilities, cleanup2] = getElementBoundUtilities(el);
memoizedUtilities = { interceptor, ...utilities };
onElRemoved(el, cleanup2);
return memoizedUtilities;
}
}
Object.defineProperty(obj, `$${name}`, {
get() {
return callback(el, getUtilities());
return callback(el, memoizedUtilities);
},
enumerable: false
});
});
return obj;
}
function getUtilities(el) {
let [utilities, cleanup2] = getElementBoundUtilities(el);
let utils = { interceptor, ...utilities };
onElRemoved(el, cleanup2);
return utils;
}
function tryCatch(el, expression, callback, ...args) {
try {
return callback(...args);
@ -1486,8 +1481,8 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}
function destroyTree(root, walker = walk) {
walker(root, (el) => {
cleanupAttributes(el);
cleanupElement(el);
cleanupAttributes(el);
});
}
function warnAboutMissingPlugins() {
@ -1980,7 +1975,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}
}
function bindInputValue(el, value) {
if (el.type === "radio") {
if (isRadio(el)) {
if (el.attributes.value === void 0) {
el.value = value;
}
@ -1991,7 +1986,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
el.checked = checkedAttrLooseCompare(el.value, value);
}
}
} else if (el.type === "checkbox") {
} else if (isCheckbox(el)) {
if (Number.isInteger(value)) {
el.value = value;
} else if (!Array.isArray(value) && typeof value !== "boolean" && ![null, void 0].includes(value)) {
@ -2067,34 +2062,37 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}
return rawValue ? Boolean(rawValue) : null;
}
var booleanAttributes = /* @__PURE__ */ new Set([
"allowfullscreen",
"async",
"autofocus",
"autoplay",
"checked",
"controls",
"default",
"defer",
"disabled",
"formnovalidate",
"inert",
"ismap",
"itemscope",
"loop",
"multiple",
"muted",
"nomodule",
"novalidate",
"open",
"playsinline",
"readonly",
"required",
"reversed",
"selected",
"shadowrootclonable",
"shadowrootdelegatesfocus",
"shadowrootserializable"
]);
function isBooleanAttr(attrName) {
const booleanAttributes = [
"disabled",
"checked",
"required",
"readonly",
"open",
"selected",
"autofocus",
"itemscope",
"multiple",
"novalidate",
"allowfullscreen",
"allowpaymentrequest",
"formnovalidate",
"autoplay",
"controls",
"loop",
"muted",
"playsinline",
"default",
"ismap",
"reversed",
"async",
"defer",
"nomodule"
];
return booleanAttributes.includes(attrName);
return booleanAttributes.has(attrName);
}
function attributeShouldntBePreservedIfFalsy(name) {
return !["aria-pressed", "aria-checked", "aria-expanded", "aria-selected"].includes(name);
@ -2127,6 +2125,12 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}
return attr;
}
function isCheckbox(el) {
return el.type === "checkbox" || el.localName === "ui-checkbox" || el.localName === "ui-switch";
}
function isRadio(el) {
return el.type === "radio" || el.localName === "ui-radio";
}
function debounce(func, wait) {
var timeout;
return function() {
@ -2195,10 +2199,10 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
return stores[name];
}
stores[name] = value;
initInterceptors(stores[name]);
if (typeof value === "object" && value !== null && value.hasOwnProperty("init") && typeof value.init === "function") {
stores[name].init();
}
initInterceptors(stores[name]);
}
function getStores() {
return stores;
@ -2280,7 +2284,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
get raw() {
return raw;
},
version: "3.14.1",
version: "3.14.3",
flushAndStopDeferringMutations,
dontAutoEvaluateFunctions,
disableEffectScheduling,
@ -3136,7 +3140,10 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
placeInDom(el._x_teleport, target2, modifiers);
});
};
cleanup2(() => clone2.remove());
cleanup2(() => mutateDom(() => {
clone2.remove();
destroyTree(clone2);
}));
});
var teleportContainerDuringClone = document.createElement("div");
function getTarget(expression) {
@ -3360,7 +3367,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
setValue(getInputValue(el, modifiers, e, getValue()));
});
if (modifiers.includes("fill")) {
if ([void 0, null, ""].includes(getValue()) || el.type === "checkbox" && Array.isArray(getValue()) || el.tagName.toLowerCase() === "select" && el.multiple) {
if ([void 0, null, ""].includes(getValue()) || isCheckbox(el) && Array.isArray(getValue()) || el.tagName.toLowerCase() === "select" && el.multiple) {
setValue(getInputValue(el, modifiers, { target: el }, getValue()));
}
}
@ -3400,7 +3407,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
return mutateDom(() => {
if (event instanceof CustomEvent && event.detail !== void 0)
return event.detail !== null && event.detail !== void 0 ? event.detail : event.target.value;
else if (el.type === "checkbox") {
else if (isCheckbox(el)) {
if (Array.isArray(currentValue)) {
let newValue = null;
if (modifiers.includes("number")) {
@ -3431,7 +3438,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
});
} else {
let newValue;
if (el.type === "radio") {
if (isRadio(el)) {
if (event.target.checked) {
newValue = event.target.value;
} else {
@ -3624,7 +3631,10 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
el._x_lookup = {};
effect3(() => loop(el, iteratorNames, evaluateItems, evaluateKey));
cleanup2(() => {
Object.values(el._x_lookup).forEach((el2) => el2.remove());
Object.values(el._x_lookup).forEach((el2) => mutateDom(() => {
destroyTree(el2);
el2.remove();
}));
delete el._x_prevKeys;
delete el._x_lookup;
});
@ -3693,11 +3703,12 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}
for (let i = 0; i < removes.length; i++) {
let key = removes[i];
if (!!lookup[key]._x_effects) {
lookup[key]._x_effects.forEach(dequeueJob);
}
lookup[key].remove();
lookup[key] = null;
if (!(key in lookup))
continue;
mutateDom(() => {
destroyTree(lookup[key]);
lookup[key].remove();
});
delete lookup[key];
}
for (let i = 0; i < moves.length; i++) {
@ -3818,12 +3829,10 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
});
el._x_currentIfEl = clone2;
el._x_undoIf = () => {
walk(clone2, (node) => {
if (!!node._x_effects) {
node._x_effects.forEach(dequeueJob);
}
mutateDom(() => {
destroyTree(clone2);
clone2.remove();
});
clone2.remove();
delete el._x_currentIfEl;
};
return clone2;
@ -4762,7 +4771,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}
};
// node_modules/@alpinejs/collapse/dist/module.esm.js
// ../../../../usr/local/lib/node_modules/@alpinejs/collapse/dist/module.esm.js
function src_default2(Alpine3) {
Alpine3.directive("collapse", collapse);
collapse.inline = (el, { modifiers }) => {
@ -4812,7 +4821,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
start: { height: current + "px" },
end: { height: full + "px" }
}, () => el._x_isShown = true, () => {
if (Math.abs(el.getBoundingClientRect().height - full) < 1) {
if (el.getBoundingClientRect().height == full) {
el.style.overflow = null;
}
});
@ -4856,7 +4865,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}
var module_default2 = src_default2;
// node_modules/@alpinejs/focus/dist/module.esm.js
// ../../../../usr/local/lib/node_modules/@alpinejs/focus/dist/module.esm.js
var candidateSelectors = ["input", "select", "textarea", "a[href]", "button", "[tabindex]:not(slot)", "audio[controls]", "video[controls]", '[contenteditable]:not([contenteditable="false"])', "details>summary:first-of-type", "details"];
var candidateSelector = /* @__PURE__ */ candidateSelectors.join(",");
var NoElement = typeof Element === "undefined";
@ -4968,11 +4977,11 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
var checked = getCheckedRadio(radioSet, node.form);
return !checked || checked === node;
};
var isRadio = function isRadio2(node) {
var isRadio2 = function isRadio22(node) {
return isInput(node) && node.type === "radio";
};
var isNonTabbableRadio = function isNonTabbableRadio2(node) {
return isRadio(node) && !isTabbableRadio(node);
return isRadio2(node) && !isTabbableRadio(node);
};
var isZeroArea = function isZeroArea2(node) {
var _node$getBoundingClie = node.getBoundingClientRect(), width = _node$getBoundingClie.width, height = _node$getBoundingClie.height;
@ -5805,7 +5814,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}
var module_default3 = src_default3;
// node_modules/@alpinejs/persist/dist/module.esm.js
// ../../../../usr/local/lib/node_modules/@alpinejs/persist/dist/module.esm.js
function src_default4(Alpine3) {
let persist = () => {
let alias;
@ -5867,7 +5876,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}
var module_default4 = src_default4;
// node_modules/@alpinejs/intersect/dist/module.esm.js
// ../../../../usr/local/lib/node_modules/@alpinejs/intersect/dist/module.esm.js
function src_default5(Alpine3) {
Alpine3.directive("intersect", Alpine3.skipDuringClone((el, { value, expression, modifiers }, { evaluateLater: evaluateLater2, cleanup: cleanup2 }) => {
let evaluate3 = evaluateLater2(expression);
@ -5967,7 +5976,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}
var module_default6 = src_default6;
// node_modules/@alpinejs/anchor/dist/module.esm.js
// ../alpine/packages/anchor/dist/module.esm.js
var min = Math.min;
var max = Math.max;
var round = Math.round;
@ -7471,8 +7480,10 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}
};
queueMicrotask(() => {
scroll(document.body);
document.querySelectorAll(["[x-navigate\\:scroll]", "[wire\\:scroll]"]).forEach(scroll);
queueMicrotask(() => {
scroll(document.body);
document.querySelectorAll(["[x-navigate\\:scroll]", "[wire\\:scroll]"]).forEach(scroll);
});
});
}
@ -7620,6 +7631,44 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
document.head.appendChild(style);
}
// js/plugins/navigate/popover.js
function packUpPersistedPopovers(persistedEl) {
persistedEl.querySelectorAll(":popover-open").forEach((el) => {
el.setAttribute("data-navigate-popover-open", "");
let animations = el.getAnimations();
el._pausedAnimations = animations.map((animation) => ({
keyframes: animation.effect.getKeyframes(),
options: {
duration: animation.effect.getTiming().duration,
easing: animation.effect.getTiming().easing,
fill: animation.effect.getTiming().fill,
iterations: animation.effect.getTiming().iterations
},
currentTime: animation.currentTime,
playState: animation.playState
}));
animations.forEach((i) => i.pause());
});
}
function unPackPersistedPopovers(persistedEl) {
persistedEl.querySelectorAll("[data-navigate-popover-open]").forEach((el) => {
el.removeAttribute("data-navigate-popover-open");
queueMicrotask(() => {
if (!el.isConnected)
return;
el.showPopover();
el.getAnimations().forEach((i) => i.finish());
if (el._pausedAnimations) {
el._pausedAnimations.forEach(({ keyframes, options, currentTime, now, playState }) => {
let animation = el.animate(keyframes, options);
animation.currentTime = currentTime;
});
delete el._pausedAnimations;
}
});
});
}
// js/plugins/navigate/page.js
var oldBodyScriptTagHashes = [];
var attributesExemptFromScriptTagHashing = [
@ -7758,7 +7807,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
function navigate_default(Alpine3) {
Alpine3.navigate = (url) => {
let destination = createUrlObjectFromString(url);
let prevented = fireEventForOtherLibariesToHookInto("alpine:navigate", {
let prevented = fireEventForOtherLibrariesToHookInto("alpine:navigate", {
url: destination,
history: false,
cached: false
@ -7789,7 +7838,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
storeThePrefetchedHtmlForWhenALinkIsClicked(html, destination, finalDestination);
});
whenItIsReleased(() => {
let prevented = fireEventForOtherLibariesToHookInto("alpine:navigate", {
let prevented = fireEventForOtherLibrariesToHookInto("alpine:navigate", {
url: destination,
history: false,
cached: false
@ -7803,7 +7852,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
function navigateTo(destination, shouldPushToHistoryState = true) {
showProgressBar && showAndStartProgressBar();
fetchHtmlOrUsePrefetchedHtml(destination, (html, finalDestination) => {
fireEventForOtherLibariesToHookInto("alpine:navigating");
fireEventForOtherLibrariesToHookInto("alpine:navigating");
restoreScroll && storeScrollInformationInHtmlBeforeNavigatingAway();
showProgressBar && finishAndHideProgressBar();
cleanupAlpineElementsOnThePageThatArentInsideAPersistedElement();
@ -7811,6 +7860,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
preventAlpineFromPickingUpDomChanges(Alpine3, (andAfterAllThis) => {
enablePersist && storePersistantElementsForLater((persistedEl) => {
packUpPersistedTeleports(persistedEl);
packUpPersistedPopovers(persistedEl);
});
if (shouldPushToHistoryState) {
updateUrlAndStoreLatestHtmlForFutureBackButtons(html, finalDestination);
@ -7821,6 +7871,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
removeAnyLeftOverStaleTeleportTargets(document.body);
enablePersist && putPersistantElementsBack((persistedEl, newStub) => {
unPackPersistedTeleports(persistedEl);
unPackPersistedPopovers(persistedEl);
});
restoreScrollPositionOrScrollToTop();
afterNewScriptsAreDoneLoading(() => {
@ -7829,7 +7880,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
autofocus && autofocusElementsWithTheAutofocusAttribute();
});
nowInitializeAlpineOnTheNewPage(Alpine3);
fireEventForOtherLibariesToHookInto("alpine:navigated");
fireEventForOtherLibrariesToHookInto("alpine:navigated");
});
});
});
@ -7839,7 +7890,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
whenTheBackOrForwardButtonIsClicked((ifThePageBeingVisitedHasntBeenCached) => {
ifThePageBeingVisitedHasntBeenCached((url) => {
let destination = createUrlObjectFromString(url);
let prevented = fireEventForOtherLibariesToHookInto("alpine:navigate", {
let prevented = fireEventForOtherLibrariesToHookInto("alpine:navigate", {
url: destination,
history: true,
cached: false
@ -7851,7 +7902,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
});
}, (html, url, currentPageUrl, currentPageKey) => {
let destination = createUrlObjectFromString(url);
let prevented = fireEventForOtherLibariesToHookInto("alpine:navigate", {
let prevented = fireEventForOtherLibrariesToHookInto("alpine:navigate", {
url: destination,
history: true,
cached: true
@ -7859,29 +7910,31 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
if (prevented)
return;
storeScrollInformationInHtmlBeforeNavigatingAway();
fireEventForOtherLibariesToHookInto("alpine:navigating");
fireEventForOtherLibrariesToHookInto("alpine:navigating");
updateCurrentPageHtmlInSnapshotCacheForLaterBackButtonClicks(currentPageUrl, currentPageKey);
preventAlpineFromPickingUpDomChanges(Alpine3, (andAfterAllThis) => {
enablePersist && storePersistantElementsForLater((persistedEl) => {
packUpPersistedTeleports(persistedEl);
packUpPersistedPopovers(persistedEl);
});
swapCurrentPageWithNewHtml(html, () => {
removeAnyLeftOverStaleProgressBars();
removeAnyLeftOverStaleTeleportTargets(document.body);
enablePersist && putPersistantElementsBack((persistedEl, newStub) => {
unPackPersistedTeleports(persistedEl);
unPackPersistedPopovers(persistedEl);
});
restoreScrollPositionOrScrollToTop();
andAfterAllThis(() => {
autofocus && autofocusElementsWithTheAutofocusAttribute();
nowInitializeAlpineOnTheNewPage(Alpine3);
fireEventForOtherLibariesToHookInto("alpine:navigated");
fireEventForOtherLibrariesToHookInto("alpine:navigated");
});
});
});
});
setTimeout(() => {
fireEventForOtherLibariesToHookInto("alpine:navigated");
fireEventForOtherLibrariesToHookInto("alpine:navigated");
});
}
function fetchHtmlOrUsePrefetchedHtml(fromDestination, callback) {
@ -7898,7 +7951,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
});
});
}
function fireEventForOtherLibariesToHookInto(name, detail) {
function fireEventForOtherLibrariesToHookInto(name, detail) {
let event = new CustomEvent(name, {
cancelable: true,
bubbles: true,
@ -8132,7 +8185,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
return data2;
}
// node_modules/@alpinejs/morph/dist/module.esm.js
// ../alpine/packages/morph/dist/module.esm.js
function morph(from, toHtml, options) {
monkeyPatchDomSetAttributeToAllowAtSymbols();
let fromEl;
@ -8467,7 +8520,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
}
var module_default8 = src_default8;
// node_modules/@alpinejs/mask/dist/module.esm.js
// ../../../../usr/local/lib/node_modules/@alpinejs/mask/dist/module.esm.js
function src_default9(Alpine3) {
Alpine3.directive("mask", (el, { value, expression }, { effect: effect3, evaluateLater: evaluateLater2, cleanup: cleanup2 }) => {
let templateFn = () => expression;
@ -8905,6 +8958,7 @@ ${expression ? 'Expression: "' + expression + '"\n\n' : ""}`, el);
},
lookahead: false
});
trigger2("morphed", { el, component });
}
function isntElement(el) {
return typeof el.hasAttribute !== "function";

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
{"/livewire.js":"cc800bf4"}
{"/livewire.js":"38dc8241"}

View File

@ -78,7 +78,7 @@ $scrollIntoViewJsSnippet = ($scrollTo !== false)
{{-- "Three Dots" Separator --}}
@if (is_string($element))
<span aria-disabled="true">
<span class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-gray-700 bg-white border border-gray-300 cursor-default leading-5 dark:bg-gray-800 dark:border-gray-600 dark:bg-gray-800 dark:border-gray-600">{{ $element }}</span>
<span class="relative inline-flex items-center px-4 py-2 -ml-px text-sm font-medium text-gray-700 bg-white border border-gray-300 cursor-default leading-5 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300">{{ $element }}</span>
</span>
@endif

View File

@ -173,6 +173,7 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale']
Route::post('charts/calculated_fields', [ChartController::class, 'calculatedFields'])->name('chart.calculated_fields');
Route::post('claim_license', [LicenseController::class, 'index'])->name('license.index');
Route::post('check_license', [LicenseController::class, 'check'])->name('license.check');
Route::resource('clients', ClientController::class); // name = (clients. index / create / show / update / destroy / edit
Route::put('clients/{client}/upload', [ClientController::class, 'upload'])->name('clients.upload');
@ -239,7 +240,8 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale']
Route::post('einvoice/peppol/disconnect', [EInvoicePeppolController::class, 'disconnect'])->name('einvoice.peppol.disconnect');
Route::put('einvoice/peppol/update', [EInvoicePeppolController::class, 'updateLegalEntity'])->name('einvoice.peppol.update_legal_entity');
Route::put('einvoice/token/update', EInvoiceTokenController::class)->name('einvoice.token.update');
Route::post('einvoice/token/update', EInvoiceTokenController::class)->name('einvoice.token.update');
Route::get('einvoice/quota', [EInvoiceController::class, 'quota'])->name('einvoice.quota');
Route::post('emails', [EmailController::class, 'send'])->name('email.send')->middleware('user_verified');
Route::post('emails/clientHistory/{client}', [EmailHistoryController::class, 'clientHistory'])->name('email.clientHistory');

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,183 @@
<?php
namespace Tests\Feature\EInvoice\RequestValidation;
use App\Http\Requests\EInvoice\UpdateEInvoiceConfiguration;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Tests\TestCase;
use Illuminate\Support\Facades\Validator;
class UpdateEInvoiceConfigurationTest extends TestCase
{
protected UpdateEInvoiceConfiguration $request;
protected function setUp(): void
{
parent::setUp();
$this->withoutMiddleware(
ThrottleRequests::class
);
$this->request = new UpdateEInvoiceConfiguration();
}
public function testConfigValidationFails()
{
$data = [
'entddity' => 'invoice',
];
$this->request->initialize($data);
$validator = Validator::make($data, $this->request->rules());
$this->assertFalse($validator->passes());
}
public function testConfigValidation()
{
$data = [
'entity' => 'invoice',
];
$this->request->initialize($data);
$validator = Validator::make($data, $this->request->rules());
$this->assertTrue($validator->passes());
}
public function testConfigValidationInvalidcode()
{
$data = [
'entity' => 'invoice',
'payment_means' => [[
'code' => 'invalidcodehere'
]]
];
$this->request->initialize($data);
$validator = Validator::make($data, $this->request->rules());
$this->assertFalse($validator->passes());
}
public function testValidatesPaymentMeansForBankTransfer()
{
$data = [
'entity' => 'invoice',
'payment_means' => [[
'code' => '30',
'iban' => '123456789101112254',
'bic_swift' => 'DEUTDEFF',
'account_holder' => 'John Doe Company Limited'
]]
];
$this->request->initialize($data);
$validator = Validator::make($data, $this->request->rules());
$this->assertTrue($validator->passes());
}
public function testValidatesPaymentMeansForCardPayment()
{
$data = [
'entity' => 'invoice',
'payment_means' => [[
'code' => '48',
'card_type' => 'VISA',
'iban' => '12345678'
]]
];
$this->request->initialize($data);
$validator = Validator::make($data, $this->request->rules());
$this->assertFalse($validator->passes());
}
public function testValidatesPaymentMeansForCreditCard()
{
$data = [
'entity' => 'invoice',
'payment_means' => [[
'code' => '54',
'card_type' => 'VISA',
'card_number' => '************1234',
'card_holder' => 'John Doe'
]]
];
$this->request->initialize($data);
$validator = Validator::make($data, $this->request->rules());
$this->assertTrue($validator->passes());
}
public function testFailsValidationWhenRequiredFieldsAreMissing()
{
$data = [
'entity' => 'invoice',
'payment_means' => [[
'code' => '30',
]]
];
$this->request->initialize($data);
$validator = Validator::make($data, $this->request->rules());
$this->assertFalse($validator->passes());
$this->assertTrue($validator->errors()->has('payment_means.0.bic_swift'));
}
public function testFailsValidationWithInvalidPaymentMeansCode()
{
$data = [
'entity' => 'invoice',
'payment_means' => [[
'code' => '999',
]]
];
$this->request->initialize($data);
$validator = Validator::make($data, $this->request->rules());
$this->assertFalse($validator->passes());
$this->assertTrue($validator->errors()->has('payment_means.0.code'));
}
public function testValidatesPaymentMeansForDirectDebit()
{
$data = [
'entity' => 'invoice',
'payment_means' => [[
'code' => '49',
'payer_bank_account' => '12345678',
'bic_swift' => 'DEUTDEFF'
]]
];
$this->request->initialize($data);
$validator = Validator::make($data, $this->request->rules());
$this->assertTrue($validator->passes());
}
public function testValidatesPaymentMeansForBookEntry()
{
$data = [
'entity' => 'invoice',
'payment_means' => [[
'code' => '15',
'account_holder' => 'John Doe Company Limited',
'bsb_sort' => '123456'
]]
];
$this->request->initialize($data);
$validator = Validator::make($data, $this->request->rules());
$this->assertTrue($validator->passes());
}
}

View File

@ -10,7 +10,7 @@ class AddTaxIdentifierRequestTest extends TestCase
{
protected AddTaxIdentifierRequest $request;
private array $vat_regex_patterns = [
private array $vat_regex_patterns = [
'DE' => '/^DE\d{9}$/',
'AT' => '/^ATU\d{8}$/',
'BE' => '/^BE0\d{9}$/',
@ -48,20 +48,26 @@ class AddTaxIdentifierRequestTest extends TestCase
public function testValidInput()
{
$validator = Validator::make([
$data = [
'country' => 'DE',
'vat_number' => 'DE123456789',
], $this->request->rules());
];
$this->request->initialize($data);
$validator = Validator::make($data, $this->request->rules());
$this->assertTrue($validator->passes());
}
public function testInvalidCountry()
{
$validator = Validator::make([
$data = [
'country' => 'US',
'vat_number' => 'DE123456789',
], $this->request->rules());
];
$this->request->initialize($data);
$validator = Validator::make($data, $this->request->rules());
$this->assertFalse($validator->passes());
$this->assertArrayHasKey('country', $validator->errors()->toArray());
@ -73,24 +79,9 @@ class AddTaxIdentifierRequestTest extends TestCase
'country' => 'DE',
'vat_number' => 'DE12345', // Too short
];
$rules = [
'country' => ['required', 'bail'],
'vat_number' => [
'required',
'string',
'bail',
function ($attribute, $value, $fail) use ($data){
if ( isset($this->vat_regex_patterns[$data['country']])) {
if (!preg_match($this->vat_regex_patterns[$data['country']], $value)) {
$fail(ctrans('texts.invalid_vat_number'));
}
}
},
]
];
$validator = Validator::make($data, $rules);
$this->request->initialize($data);
$validator = Validator::make($data, $this->request->rules());
$this->assertFalse($validator->passes());
$this->assertArrayHasKey('vat_number', $validator->errors()->toArray());
@ -98,9 +89,12 @@ class AddTaxIdentifierRequestTest extends TestCase
public function testMissingCountry()
{
$validator = Validator::make([
$data = [
'vat_number' => 'DE123456789',
], $this->request->rules());
];
$this->request->initialize($data);
$validator = Validator::make($data, $this->request->rules());
$this->assertFalse($validator->passes());
$this->assertArrayHasKey('country', $validator->errors()->toArray());
@ -108,9 +102,12 @@ class AddTaxIdentifierRequestTest extends TestCase
public function testMissingVatNumber()
{
$validator = Validator::make([
$data = [
'country' => 'DE',
], $this->request->rules());
];
$this->request->initialize($data);
$validator = Validator::make($data, $this->request->rules());
$this->assertFalse($validator->passes());
$this->assertArrayHasKey('vat_number', $validator->errors()->toArray());

View File

@ -5,8 +5,6 @@ namespace Tests\Feature\EInvoice\Validation;
use Tests\TestCase;
use Illuminate\Support\Facades\Validator;
use App\Http\Requests\EInvoice\Peppol\StoreEntityRequest;
use App\Models\Country;
use Illuminate\Support\Collection;
class CreateRequestTest extends TestCase
{
@ -20,36 +18,43 @@ class CreateRequestTest extends TestCase
public function testValidInput()
{
$validator = Validator::make([
$data = [
'party_name' => 'Test Company',
'line1' => '123 Test St',
'city' => 'Test City',
'country' => 'DE', // Assuming 1 is the ID for Germany
'country' => 'DE',
'zip' => '12345',
'county' => 'Test County',
'acts_as_sender' => true,
'acts_as_receiver' => true,
'tenant_id' => 'testcompanykey',
'classification' => 'individual',
'id_number' => 'xx',
];
], $this->request->rules());
$this->request->initialize($data);
$validator = Validator::make($data, $this->request->rules());
$this->assertTrue($validator->passes());
}
public function testInvalidCountry()
{
$validator = Validator::make([
$data = [
'party_name' => 'Test Company',
'line1' => '123 Test St',
'city' => 'Test City',
'country' => 999, // Invalid country ID
'country' => 999,
'zip' => '12345',
'county' => 'Test County',
'acts_as_sender' => true,
'acts_as_receiver' => true,
'tenant_id' => 'testcompanykey',
], $this->request->rules());
];
$this->request->initialize($data);
$validator = Validator::make($data, $this->request->rules());
$this->assertFalse($validator->passes());
$this->assertArrayHasKey('country', $validator->errors()->toArray());
@ -57,9 +62,12 @@ class CreateRequestTest extends TestCase
public function testMissingRequiredFields()
{
$validator = Validator::make([
$data = [
'line2' => 'Optional line',
], $this->request->rules());
];
$this->request->initialize($data);
$validator = Validator::make($data, $this->request->rules());
$this->assertFalse($validator->passes());
$errors = $validator->errors()->toArray();
@ -73,7 +81,7 @@ class CreateRequestTest extends TestCase
public function testOptionalLine2()
{
$validator = Validator::make([
$data = [
'party_name' => 'Test Company',
'line1' => '123 Test St',
'line2' => 'Optional line',
@ -81,20 +89,27 @@ class CreateRequestTest extends TestCase
'country' => 'AT',
'zip' => '12345',
'county' => 'Test County',
'tenant_id' => 'testcompanykey',
'tenant_id' => 'testcompanykey',
'acts_as_sender' => true,
'acts_as_receiver' => true,
], $this->request->rules());
'classification' => 'business',
'vat_number' => '234234',
];
$this->request->initialize($data);
$validator = Validator::make($data, $this->request->rules());
$this->assertTrue($validator->passes());
}
public function testCountryPreparation()
{
$request = new StoreEntityRequest([
'country' => '276', // Assuming 1 is the ID for Germany
]);
$data = [
'country' => '276', // Numeric code for Germany
];
$request = new StoreEntityRequest();
$request->initialize($data);
$request->prepareForValidation();
$this->assertEquals('DE', $request->input('country'));

View File

@ -0,0 +1,142 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Integration\Einvoice\Storecove;
use Tests\TestCase;
use App\Models\Client;
use App\Models\Company;
use App\Models\Country;
use App\Models\Invoice;
use Tests\MockAccountData;
use Illuminate\Support\Str;
use App\Models\ClientContact;
use App\DataMapper\InvoiceItem;
use App\DataMapper\Tax\TaxModel;
use App\DataMapper\ClientSettings;
use App\DataMapper\CompanySettings;
use App\Services\EDocument\Standards\Peppol;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use InvoiceNinja\EInvoice\Models\Peppol\PaymentMeans;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use App\Services\EDocument\Gateway\Storecove\Storecove;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
use InvoiceNinja\EInvoice\Models\Peppol\Invoice as PeppolInvoice;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use App\Services\EDocument\Gateway\Storecove\PeppolToStorecoveNormalizer;
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
use App\Services\EDocument\Gateway\Storecove\Models\Invoice as StorecoveInvoice;
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
use Illuminate\Routing\Middleware\ThrottleRequests;
class StorecoveIngestTest extends TestCase
{
use MockAccountData;
use DatabaseTransactions;
private int $routing_id = 0;
protected function setUp(): void
{
parent::setUp();
$this->makeTestData();
if (config('ninja.testvars.travis') !== false || !config('ninja.storecove_api_key')) {
$this->markTestSkipped("do not run in CI");
}
$this->withoutMiddleware(
ThrottleRequests::class
);
}
public function testIngestStorecoveDocument()
{
$s = new Storecove();
$x = $s->getDocument('3f0981f1-5105-4970-81f2-6b7482ad27d7');
$doc = $x['document']['invoice'];
$x = json_encode($doc);
$classMetadataFactory = new ClassMetadataFactory(new AttributeLoader());
// Create a proper PropertyInfoExtractor
$phpDocExtractor = new PhpDocExtractor();
$reflectionExtractor = new ReflectionExtractor();
$propertyInfo = new PropertyInfoExtractor(
// List of extractors for type info
[$reflectionExtractor, $phpDocExtractor],
// List of extractors for descriptions
[$phpDocExtractor],
// List of extractors for access info
[$reflectionExtractor],
// List of extractors for mutation info
[$reflectionExtractor],
// List of extractors for initialization info
[$reflectionExtractor]
);
$normalizers = [
new DateTimeNormalizer(),
new ArrayDenormalizer(),
new ObjectNormalizer(
$classMetadataFactory,
null,
null,
$propertyInfo
)
];
$context = [
DateTimeNormalizer::FORMAT_KEY => 'Y-m-d',
AbstractObjectNormalizer::SKIP_NULL_VALUES => true,
AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT => false,
AbstractObjectNormalizer::DEEP_OBJECT_TO_POPULATE => true, // Add this
];
$encoders = [new JsonEncoder()];
$serializer = new Serializer($normalizers, $encoders);
$context = [
DateTimeNormalizer::FORMAT_KEY => 'Y-m-d',
AbstractObjectNormalizer::SKIP_NULL_VALUES => true,
AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT => false, // Enforce types
];
$object = $serializer->deserialize(
$x,
StorecoveInvoice::class,
'json',
$context
);
$this->assertInstanceOf(StorecoveInvoice::class, $object);
}
}

View File

@ -14,6 +14,7 @@ namespace Tests\Integration\Einvoice\Storecove;
use Tests\TestCase;
use App\Models\Client;
use App\Models\Company;
use App\Models\Country;
use App\Models\Invoice;
use Tests\MockAccountData;
use Illuminate\Support\Str;
@ -38,20 +39,19 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
use InvoiceNinja\EInvoice\Models\Peppol\Invoice as PeppolInvoice;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use App\Services\EDocument\Gateway\Transformers\StorecoveTransformer;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use App\Services\EDocument\Gateway\Storecove\PeppolToStorecoveNormalizer;
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
use App\Services\EDocument\Gateway\Storecove\Models\Invoice as StorecoveInvoice;
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
use Illuminate\Routing\Middleware\ThrottleRequests;
class StorecoveTest extends TestCase
{
use MockAccountData;
use DatabaseTransactions;
private int $routing_id;
private int $routing_id = 0;
protected function setUp(): void
{
@ -62,6 +62,282 @@ class StorecoveTest extends TestCase
if (config('ninja.testvars.travis') !== false || !config('ninja.storecove_api_key')) {
$this->markTestSkipped("do not run in CI");
}
$this->withoutMiddleware(
ThrottleRequests::class
);
}
private function setupTestData(array $params = []): array
{
$settings = CompanySettings::defaults();
$settings->vat_number = $params['company_vat'] ?? 'DE123456789';
$settings->country_id = Country::where('iso_3166_2', 'DE')->first()->id;
$settings->email = $this->faker->safeEmail();
$settings->currency_id = '3';
$tax_data = new TaxModel();
$tax_data->regions->EU->has_sales_above_threshold = $params['over_threshold'] ?? false;
$tax_data->regions->EU->tax_all_subregions = true;
$tax_data->seller_subregion = $params['company_country'] ?? 'DE';
$einvoice = new \InvoiceNinja\EInvoice\Models\Peppol\Invoice();
$fib = new \InvoiceNinja\EInvoice\Models\Peppol\BranchType\FinancialInstitutionBranch();
$fib->ID = "DEUTDEMMXXX"; //BIC
$pfa = new \InvoiceNinja\EInvoice\Models\Peppol\FinancialAccountType\PayeeFinancialAccount();
$id = new \InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\ID();
$id->value = 'DE89370400440532013000';
$pfa->ID = $id;
$pfa->Name = 'PFA-NAME';
$pfa->FinancialInstitutionBranch = $fib;
$pm = new \InvoiceNinja\EInvoice\Models\Peppol\PaymentMeans();
$pm->PayeeFinancialAccount = $pfa;
$pmc = new \InvoiceNinja\EInvoice\Models\Peppol\CodeType\PaymentMeansCode();
$pmc->value = '30';
$pm->PaymentMeansCode = $pmc;
$einvoice->PaymentMeans[] = $pm;
$stub = new \stdClass();
$stub->Invoice = $einvoice;
$this->company->settings = $settings;
$this->company->tax_data = $tax_data;
$this->company->calculate_taxes = true;
$this->company->legal_entity_id = 290868;
$this->company->e_invoice = $stub;
$this->company->save();
$company = $this->company;
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $this->company->id,
'country_id' => Country::where('iso_3166_2', $params['client_country'] ?? 'FR')->first()->id,
'vat_number' => $params['client_vat'] ?? '',
'classification' => $params['classification'] ?? 'individual',
'has_valid_vat_number' => $params['has_valid_vat'] ?? false,
'name' => 'Test Client',
'is_tax_exempt' => $params['is_tax_exempt'] ?? false,
'id_number' => $params['client_id_number'] ?? '',
]);
$contact = ClientContact::factory()->create([
'client_id' => $client->id,
'company_id' =>$client->company_id,
'user_id' => $client->user_id,
'first_name' => $this->faker->firstName(),
'last_name' => $this->faker->lastName(),
'email' => $this->faker->safeEmail()
]);
$invoice = \App\Models\Invoice::factory()->create([
'client_id' => $client->id,
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'date' => now()->addDay()->format('Y-m-d'),
'due_date' => now()->addDays(2)->format('Y-m-d'),
'uses_inclusive_taxes' => false,
'tax_rate1' => 0,
'tax_name1' => '',
'tax_rate2' => 0,
'tax_name2' => '',
'tax_rate3' => 0,
'tax_name3' => '',
]);
$items = $invoice->line_items;
foreach($items as &$item)
{
$item->tax_name2 = '';
$item->tax_rate2 = 0;
$item->tax_name3 = '';
$item->tax_rate3 = 0;
$item->uses_inclusive_taxes = false;
}
unset($item);
$invoice->line_items = array_values($items);
$invoice = $invoice->calc()->getInvoice();
return compact('company', 'client', 'invoice');
}
public function testDeToFrClientTaxExemptSending()
{
$this->routing_id = 290868;
$scenario = [
'company_vat' => 'DE923356489',
'company_country' => 'DE',
'client_country' => 'FR',
'client_vat' => 'FRAA123456789',
'client_id_number' => '123456789',
'classification' => 'business',
'has_valid_vat' => true,
'over_threshold' => true,
'legal_entity_id' => 290868,
'is_tax_exempt' => false,
];
$data = $this->setupTestData($scenario);
$invoice = $data['invoice'];
$invoice = $invoice->calc()->getInvoice();
$company = $data['company'];
$client = $data['client'];
$client->save();
$this->assertEquals('DE', $company->country()->iso_3166_2);
$this->assertEquals('FR', $client->country->iso_3166_2);
foreach($invoice->line_items as $item)
{
$this->assertEquals('1', $item->tax_id);
$this->assertEquals(0, $item->tax_rate1);
}
$this->assertEquals(floatval(0), floatval($invoice->total_taxes));
$this->sendDocument($invoice);
}
/**
* PtestDeToDeClientTaxExemptSending
*
* Disabled for now - there is an issue with internal tax exempt client in same country
* @return void
*/
public function PtestDeToDeClientTaxExemptSending()
{
$this->routing_id = 290868;
$scenario = [
'company_vat' => 'DE923356489',
'company_country' => 'DE',
'client_country' => 'DE',
'client_vat' => 'DE173755434',
'classification' => 'business',
'has_valid_vat' => true,
'over_threshold' => true,
'legal_entity_id' => 290868,
'is_tax_exempt' => true,
];
$data = $this->setupTestData($scenario);
$invoice = $data['invoice'];
$invoice = $invoice->calc()->getInvoice();
$company = $data['company'];
$client = $data['client'];
$client->save();
$this->assertEquals('DE', $company->country()->iso_3166_2);
$this->assertEquals('DE', $client->country->iso_3166_2);
foreach($invoice->line_items as $item)
{
$this->assertEquals('1', $item->tax_id);
$this->assertEquals(0, $item->tax_rate1);
}
$this->assertEquals(floatval(0), floatval($invoice->total_taxes));
$this->sendDocument($invoice);
}
public function testDeToDeSending()
{
$this->routing_id = 290868;
$scenario = [
'company_vat' => 'DE923356489',
'company_country' => 'DE',
'client_country' => 'DE',
'client_vat' => '',
'classification' => 'individual',
'has_valid_vat' => false,
'over_threshold' => true,
'legal_entity_id' => 290868,
];
$data = $this->setupTestData($scenario);
$invoice = $data['invoice'];
$invoice = $invoice->calc()->getInvoice();
$company = $data['company'];
$client = $data['client'];
$tax_rate = $company->tax_data->regions->EU->subregions->DE->tax_rate;
$this->assertEquals('DE', $company->country()->iso_3166_2);
$this->assertEquals('DE', $client->country->iso_3166_2);
foreach($invoice->line_items as $item)
{
$this->assertEquals('1', $item->tax_id);
$this->assertEquals($tax_rate, $item->tax_rate1);
}
$this->sendDocument($invoice);
}
private function sendDocument($model)
{
$storecove = new Storecove();
$p = new Peppol($model);
$p->run();
try {
$processor = new \Saxon\SaxonProcessor();
} catch (\Throwable $e) {
$this->markTestSkipped('saxon not installed');
}
$validator = new \App\Services\EDocument\Standards\Validation\XsltDocumentValidator($p->toXml());
$validator->validate();
if (count($validator->getErrors()) > 0) {
nlog($p->toXml());
nlog($validator->getErrors());
}
$this->assertCount(0, $validator->getErrors());
$identifiers = $p->gateway->mutator->setClientRoutingCode()->getStorecoveMeta();
$result = $storecove->build($model)->getResult();
if (count($result['errors']) > 0) {
nlog("errors!");
nlog($result);
return $result['errors'];
}
$payload = [
'legal_entity_id' => $model->company->legal_entity_id,
"idempotencyGuid" => \Illuminate\Support\Str::uuid(),
'document' => [
'document_type' => 'invoice',
'invoice' => $result['document'],
],
'tenant_id' => $model->company->company_key,
'routing' => $identifiers['routing'],
];
/** Concrete implementation current linked to Storecove only */
//@testing only
$sc = new \App\Services\EDocument\Gateway\Storecove\Storecove();
$r = $sc->sendJsonDocument($payload);
nlog($r);
}
public function testTransformPeppolToStorecove()
@ -105,7 +381,6 @@ class StorecoveTest extends TestCase
$this->assertInstanceOf(\InvoiceNinja\EInvoice\Models\Peppol\Invoice::class, $peppolInvoice);
$parent = \App\Services\EDocument\Gateway\Storecove\Models\Invoice::class;
// $peppolInvoice = $e->encode($peppolInvoice, 'json');
$peppolInvoice = $data = $e->encode($peppolInvoice, 'json', $context);
@ -159,15 +434,6 @@ class StorecoveTest extends TestCase
$p->run();
$peppolInvoice = $p->getInvoice();
$s_transformer = new StorecoveTransformer();
$s_transformer->transform($peppolInvoice);
$json = $s_transformer->toJson();
$this->assertJson($json);
nlog($json);
}
// public function testStorecoveTransformer()
@ -245,17 +511,7 @@ class StorecoveTest extends TestCase
$p->run();
$peppolInvoice = $p->getInvoice();
$s_transformer = new StorecoveTransformer();
$s_transformer->transform($peppolInvoice);
$json = $s_transformer->toJson();
$this->assertJson($json);
nlog("percentage");
nlog($json);
$this->assertNotNull($peppolInvoice);
}
@ -1680,4 +1936,5 @@ class StorecoveTest extends TestCase
}
}

View File

@ -524,6 +524,8 @@ class InvoiceItemTest extends TestCase
$item_calc = new InvoiceItemSum($this->invoice, $settings);
$item_calc->process();
nlog($item_calc->getGroupedTaxes());
$this->assertEquals($item_calc->getTotalTaxes(), 2.06);
$this->assertEquals($item_calc->getGroupedTaxes()->count(), 2);
}

View File

@ -0,0 +1,179 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Unit\Tax;
use Tests\TestCase;
use App\Models\Client;
use App\Models\Company;
use App\Models\Country;
use App\Models\Invoice;
use Tests\MockAccountData;
use App\DataMapper\Tax\BaseRule;
use App\DataMapper\Tax\TaxModel;
use App\DataMapper\CompanySettings;
use App\Models\ClientContact;
use App\Services\Tax\StorecoveAdapter;
use Illuminate\Routing\Middleware\ThrottleRequests;
use App\Services\EDocument\Gateway\Storecove\Storecove;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class TaxRuleConsistencyTest extends TestCase
{
use MockAccountData;
use DatabaseTransactions;
protected $faker;
protected function setUp(): void
{
parent::setUp();
$this->withoutMiddleware(
ThrottleRequests::class
);
$this->withoutExceptionHandling();
$this->makeTestData();
$this->faker = \Faker\Factory::create();
}
private function setupTestData(array $params = []): array
{
$settings = CompanySettings::defaults();
$settings->vat_number = $params['company_vat'] ?? 'DE123456789';
$settings->country_id = Country::where('iso_3166_2', 'DE')->first()->id;
$settings->email = $this->faker->safeEmail();
$tax_data = new TaxModel();
$tax_data->regions->EU->has_sales_above_threshold = $params['over_threshold'] ?? false;
$tax_data->regions->EU->tax_all_subregions = true;
$this->company->settings = $settings;
$this->company->tax_data = $tax_data;
$this->company->save();
$company = $this->company;
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $this->company->id,
'country_id' => Country::where('iso_3166_2', $params['client_country'] ?? 'FR')->first()->id,
'vat_number' => $params['client_vat'] ?? '',
'classification' => $params['classification'] ?? 'individual',
'has_valid_vat_number' => $params['has_valid_vat'] ?? false,
'name' => 'Test Client'
]);
$contact = ClientContact::factory()->create([
'client_id' => $client->id,
'company_id' =>$client->company_id,
'user_id' => $client->user_id,
'first_name' => $this->faker->firstName(),
'last_name' => $this->faker->lastName(),
'email' => $this->faker->safeEmail()
]);
$invoice = Invoice::factory()->create([
'client_id' => $client->id,
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'discount' => rand(1,10),
]);
$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();
$invoice->setRelation('company', $this->company);
return compact('company', 'client', 'invoice');
}
public function testScenarios()
{
$scenarios = [
'B2C Over Threshold' => [
'params' => [
'company_country' => 'DE',
'client_country' => 'FR',
'company_vat' => 'DE123456789',
'client_vat' => '',
'classification' => 'individual',
'has_valid_vat' => false,
'over_threshold' => true,
],
'expected_rate' => 20, // Should use French VAT
'expected_nexus' => 'FR',
],
'B2C Under Threshold' => [
'params' => [
'company_country' => 'DE',
'client_country' => 'FR',
'company_vat' => 'DE123456789',
'client_vat' => '',
'classification' => 'individual',
'has_valid_vat' => false,
'over_threshold' => false,
],
'expected_rate' => 19, // Should use German VAT
'expected_nexus' => 'DE',
],
'B2B Transaction' => [
'params' => [
'company_country' => 'DE',
'client_country' => 'FR',
'company_vat' => 'DE123456789',
'client_vat' => 'FR123456789',
'classification' => 'business',
'has_valid_vat' => true,
'over_threshold' => true,
],
'expected_rate' => 19, // Should use German VAT
'expected_nexus' => 'DE',
],
];
foreach ($scenarios as $name => $scenario) {
$data = $this->setupTestData($scenario['params']);
// Test BaseRule
$baseRule = new BaseRule();
$baseRule->setEntity($data['invoice']);
$baseRule->defaultForeign();
// Test StorecoveAdapter
$storecove = new Storecove();
$storecove->build($data['invoice']);
$this->assertEquals(
$scenario['expected_rate'],
$baseRule->tax_rate1
);
$this->assertEquals(
$storecove->adapter->getNexus(),
$scenario['expected_nexus']
);
}
}
}

View File

@ -1025,6 +1025,10 @@ class UsTaxTest extends TestCase
$tax_data->seller_subregion = 'CA';
$tax_data->regions->US->has_sales_above_threshold = true;
$tax_data->regions->US->tax_all_subregions = true;
$tax_data->regions->US->subregions->CA->tax_rate = 6;
$tax_data->regions->US->subregions->CA->tax_name = 'Sales Tax';
$tax_data->regions->EU->has_sales_above_threshold = true;
$tax_data->regions->EU->tax_all_subregions = true;
$tax_data->regions->EU->subregions->DE->tax_rate = 21;
@ -1045,6 +1049,7 @@ class UsTaxTest extends TestCase
'has_valid_vat_number' => false,
'postal_code' => 'xx',
'tax_data' => new Response($this->mock_response),
'is_tax_exempt' => false,
]);
$invoice = Invoice::factory()->create([