Merge pull request #8510 from turbo124/v5-develop

Tax Tests
This commit is contained in:
David Bomba 2023-05-17 18:06:27 +10:00 committed by GitHub
commit 94838cbdc4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 2120 additions and 435 deletions

View File

@ -0,0 +1,27 @@
<?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 App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
class EncryptedCast implements CastsAttributes
{
public function get($model, string $key, $value, array $attributes)
{
return strlen($value) > 1 ? decrypt($value) : null;
}
public function set($model, string $key, $value, array $attributes)
{
return [$key => ! is_null($value) ? encrypt($value) : null];
}
}

View File

@ -14,9 +14,9 @@ namespace App\DataMapper\Tax;
use App\Models\Client; use App\Models\Client;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Product; use App\Models\Product;
use App\DataMapper\Tax\TaxData;
use App\DataProviders\USStates; use App\DataProviders\USStates;
use App\DataMapper\Tax\ZipTax\Response; use App\DataMapper\Tax\ZipTax\Response;
use App\Services\Tax\Providers\TaxProvider;
class BaseRule implements RuleInterface class BaseRule implements RuleInterface
{ {
@ -104,9 +104,6 @@ class BaseRule implements RuleInterface
/** EU TAXES */ /** EU TAXES */
/** US TAXES */
/** US TAXES */
public string $tax_name1 = ''; public string $tax_name1 = '';
public float $tax_rate1 = 0; public float $tax_rate1 = 0;
@ -130,70 +127,140 @@ class BaseRule implements RuleInterface
{ {
return $this; return $this;
} }
/**
* Initializes the tax rule for the entity.
*
* @param mixed $invoice
* @return self
*/
public function setEntity(mixed $invoice): self public function setEntity(mixed $invoice): self
{ {
$this->invoice = $invoice; $this->invoice = $invoice;
$this->client = $invoice->client; $this->client = $invoice->client;
$this->configTaxData() $this->resolveRegions();
->resolveRegions();
if(!$this->isTaxableRegion())
return $this;
$this->configTaxData();
$this->tax_data = new Response($this->invoice->tax_data); $this->tax_data = new Response($this->invoice->tax_data);
return $this; return $this;
} }
/**
* Configigures the Tax Data for the entity
*
* @return self
*/
private function configTaxData(): self private function configTaxData(): self
{ {
/* We should only apply taxes for configured states */
if(!array_key_exists($this->client->country->iso_3166_2, $this->region_codes)) { if(!array_key_exists($this->client->country->iso_3166_2, $this->region_codes)) {
$this->client->country_id = $this->invoice->company->settings->country_id;
$this->client->saveQuietly();
nlog('Automatic tax calculations not supported for this country - defaulting to company country'); nlog('Automatic tax calculations not supported for this country - defaulting to company country');
nlog("With new logic, we should never see this");
} }
$this->client_region = $this->region_codes[$this->client->country->iso_3166_2]; /** Harvest the client_region */
/** If the tax data is already set and the invoice is marked as sent, do not adjust the rates */
if($this->invoice->tax_data && $this->invoice->status_id > 1) if($this->invoice->tax_data && $this->invoice->status_id > 1)
return $this; return $this;
//determine if we are taxing locally or if we are taxing globally /**
$tax_data = is_object($this->invoice->client->tax_data) ? $this->invoice->client->tax_data : new Response([]); * Origin - Company Tax Data
* Destination - Client Tax Data
*
*/
// $tax_data = new Response([]);
$tax_data = false;
if(strlen($this->invoice->tax_data?->originDestination) == 0 && $this->client->company->tax_data->seller_subregion != $this->client_subregion) { if($this->seller_region == 'US' && $this->client_region == 'US'){
$tax_data->originDestination = "D";
$tax_data->geoState = $this->client_subregion; $company = $this->invoice->company;
/** If no company tax data has been configured, lets do that now. */
if(!$company->origin_tax_data && \DB::transactionLevel() == 0)
{
$tp = new TaxProvider($company);
$tp->updateCompanyTaxData();
$company->fresh();
if($this->invoice instanceof Invoice) {
$this->invoice->tax_data = $tax_data;
$this->invoice->saveQuietly();
} }
/** If we are in a Origin based state, force the company tax here */
if($company->origin_tax_data->originDestination == 'O' && ($company->tax_data->seller_subregion == $this->client_subregion)) {
$tax_data = $company->origin_tax_data;
}
else{
/** Ensures the client tax data has been updated */
if(!$this->client->tax_data && \DB::transactionLevel() == 0) {
$tp = new TaxProvider($company, $this->client);
$tp->updateClientTaxData();
$this->client->fresh();
}
$tax_data = $this->client->tax_data;
}
} }
/** Applies the tax data to the invoice */
if($this->invoice instanceof Invoice && $tax_data) {
$this->invoice->tax_data = $tax_data ;
if(\DB::transactionLevel() == 0)
$this->invoice->saveQuietly();
}
return $this; return $this;
} }
// Refactor to support switching between shipping / billing country / region / subregion
/**
* Resolve Regions & Subregions
*
* @return self
*/
private function resolveRegions(): self private function resolveRegions(): self
{ {
$this->client_region = $this->region_codes[$this->client->country->iso_3166_2];
match($this->client_region){ match($this->client_region){
'US' => $this->client_subregion = strlen($this->invoice?->tax_data?->geoState) > 1 ? $this->invoice?->tax_data?->geoState : $this->getUSState(), 'US' => $this->client_subregion = strlen($this->invoice?->client?->tax_data?->geoState) > 1 ? $this->invoice->client->tax_data->geoState : $this->getUSState(),
'EU' => $this->client_subregion = $this->client->country->iso_3166_2, 'EU' => $this->client_subregion = $this->client->country->iso_3166_2,
'AU' => $this->client_subregion = 'AU', 'AU' => $this->client_subregion = 'AU',
default => $this->client_subregion = $this->client->country->iso_3166_2, default => $this->client_subregion = $this->client->country->iso_3166_2,
}; };
return $this; return $this;
} }
private function getUSState(): string private function getUSState(): string
{ {
try { try {
$states = USStates::$states;
if(isset($states[$this->client->state]))
return $this->client->state;
return USStates::getState(strlen($this->client->postal_code) > 1 ? $this->client->postal_code : $this->client->shipping_postal_code); return USStates::getState(strlen($this->client->postal_code) > 1 ? $this->client->postal_code : $this->client->shipping_postal_code);
} catch (\Exception $e) { } catch (\Exception $e) {
return $this->client->company->country()->iso_3166_2 == 'US' ? $this->client->company->tax_data->seller_subregion : 'CA'; return $this->client->company->country()->iso_3166_2 == 'US' ? $this->client->company->tax_data->seller_subregion : 'CA';
} }
@ -207,7 +274,7 @@ class BaseRule implements RuleInterface
public function defaultForeign(): self public function defaultForeign(): self
{ {
if($this->client_region == 'US') { if($this->client_region == 'US' && isset($this->tax_data?->taxSales)) {
$this->tax_rate1 = $this->tax_data->taxSales * 100; $this->tax_rate1 = $this->tax_data->taxSales * 100;
$this->tax_name1 = "{$this->tax_data->geoState} Sales Tax"; $this->tax_name1 = "{$this->tax_data->geoState} Sales Tax";
@ -235,18 +302,21 @@ class BaseRule implements RuleInterface
{ {
if ($this->client->is_tax_exempt) { if ($this->client->is_tax_exempt) {
return $this->taxExempt();
return $this->taxExempt($item);
} elseif($this->client_region == $this->seller_region && $this->isTaxableRegion()) { } elseif($this->client_region == $this->seller_region && $this->isTaxableRegion()) {
$this->taxByType($item->tax_id); $this->taxByType($item);
return $this; return $this;
} elseif($this->isTaxableRegion()) { //other regions outside of US } elseif($this->isTaxableRegion()) { //other regions outside of US
match(intval($item->tax_id)) { match(intval($item->tax_id)) {
Product::PRODUCT_TYPE_EXEMPT => $this->taxExempt(), Product::PRODUCT_TYPE_EXEMPT => $this->taxExempt($item),
Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced(), Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced($item),
Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override(), Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override($item),
default => $this->defaultForeign(), default => $this->defaultForeign(),
}; };
@ -260,42 +330,42 @@ class BaseRule implements RuleInterface
return $this; return $this;
} }
public function taxReduced(): self public function taxReduced($item): self
{ {
return $this; return $this;
} }
public function taxExempt(): self public function taxExempt($item): self
{ {
return $this; return $this;
} }
public function taxDigital(): self public function taxDigital($item): self
{ {
return $this; return $this;
} }
public function taxService(): self public function taxService($item): self
{ {
return $this; return $this;
} }
public function taxShipping(): self public function taxShipping($item): self
{ {
return $this; return $this;
} }
public function taxPhysical(): self public function taxPhysical($item): self
{ {
return $this; return $this;
} }
public function default(): self public function default($item): self
{ {
return $this; return $this;
} }
public function override(): self public function override($item): self
{ {
return $this; return $this;
} }
@ -304,4 +374,10 @@ class BaseRule implements RuleInterface
{ {
return $this; return $this;
} }
public function regionWithNoTaxCoverage(string $iso_3166_2): bool
{
return ! in_array($iso_3166_2, array_merge($this->eu_country_codes, array_keys($this->region_codes)));
}
} }

View File

@ -30,10 +30,10 @@ class Rule extends BaseRule implements RuleInterface
public bool $eu_business_tax_exempt = true; public bool $eu_business_tax_exempt = true;
/** @var bool $foreign_business_tax_exempt */ /** @var bool $foreign_business_tax_exempt */
public bool $foreign_business_tax_exempt = true; public bool $foreign_business_tax_exempt = false;
/** @var bool $foreign_consumer_tax_exempt */ /** @var bool $foreign_consumer_tax_exempt */
public bool $foreign_consumer_tax_exempt = true; public bool $foreign_consumer_tax_exempt = false;
/** @var float $tax_rate */ /** @var float $tax_rate */
public float $tax_rate = 0; public float $tax_rate = 0;
@ -56,25 +56,27 @@ class Rule extends BaseRule implements RuleInterface
/** /**
* Sets the correct tax rate based on the product type. * Sets the correct tax rate based on the product type.
* *
* @param mixed $product_tax_type * @param mixed $item
* @return self * @return self
*/ */
public function taxByType($product_tax_type): self public function taxByType($item): self
{ {
if ($this->client->is_tax_exempt) { if ($this->client->is_tax_exempt) {
return $this->taxExempt(); return $this->taxExempt($item);
} }
match(intval($product_tax_type)){ match(intval($item->tax_id)){
Product::PRODUCT_TYPE_EXEMPT => $this->taxExempt(), Product::PRODUCT_TYPE_EXEMPT => $this->taxExempt($item),
Product::PRODUCT_TYPE_DIGITAL => $this->taxDigital(), Product::PRODUCT_TYPE_DIGITAL => $this->taxDigital($item),
Product::PRODUCT_TYPE_SERVICE => $this->taxService(), Product::PRODUCT_TYPE_SERVICE => $this->taxService($item),
Product::PRODUCT_TYPE_SHIPPING => $this->taxShipping(), Product::PRODUCT_TYPE_SHIPPING => $this->taxShipping($item),
Product::PRODUCT_TYPE_PHYSICAL => $this->taxPhysical(), Product::PRODUCT_TYPE_PHYSICAL => $this->taxPhysical($item),
Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced(), Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced($item),
Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override(), Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override($item),
default => $this->default(), Product::PRODUCT_TYPE_ZERO_RATED => $this->zeroRated($item),
Product::PRODUCT_TYPE_REVERSE_TAX => $this->reverseTax($item),
default => $this->default($item),
}; };
return $this; return $this;
@ -85,7 +87,20 @@ class Rule extends BaseRule implements RuleInterface
* *
* @return self * @return self
*/ */
public function taxReduced(): self public function reverseTax($item): self
{
$this->tax_rate1 = 0;
$this->tax_name1 = 'ermäßigte MwSt.';
return $this;
}
/**
* Calculates the tax rate for a reduced tax product
*
* @return self
*/
public function taxReduced($item): self
{ {
$this->tax_rate1 = $this->reduced_tax_rate; $this->tax_rate1 = $this->reduced_tax_rate;
$this->tax_name1 = 'ermäßigte MwSt.'; $this->tax_name1 = 'ermäßigte MwSt.';
@ -93,12 +108,26 @@ class Rule extends BaseRule implements RuleInterface
return $this; return $this;
} }
/**
* Calculates the tax rate for a zero rated tax product
*
* @return self
*/
public function zeroRated($item): self
{
$this->tax_rate1 = 0;
$this->tax_name1 = 'ermäßigte MwSt.';
return $this;
}
/** /**
* Calculates the tax rate for a tax exempt product * Calculates the tax rate for a tax exempt product
* *
* @return self * @return self
*/ */
public function taxExempt(): self public function taxExempt($item): self
{ {
$this->tax_name1 = ''; $this->tax_name1 = '';
$this->tax_rate1 = 0; $this->tax_rate1 = 0;
@ -111,7 +140,7 @@ class Rule extends BaseRule implements RuleInterface
* *
* @return self * @return self
*/ */
public function taxDigital(): self public function taxDigital($item): self
{ {
$this->tax_rate1 = $this->tax_rate; $this->tax_rate1 = $this->tax_rate;
@ -125,7 +154,7 @@ class Rule extends BaseRule implements RuleInterface
* *
* @return self * @return self
*/ */
public function taxService(): self public function taxService($item): self
{ {
$this->tax_rate1 = $this->tax_rate; $this->tax_rate1 = $this->tax_rate;
@ -139,7 +168,7 @@ class Rule extends BaseRule implements RuleInterface
* *
* @return self * @return self
*/ */
public function taxShipping(): self public function taxShipping($item): self
{ {
$this->tax_rate1 = $this->tax_rate; $this->tax_rate1 = $this->tax_rate;
@ -153,7 +182,7 @@ class Rule extends BaseRule implements RuleInterface
* *
* @return self * @return self
*/ */
public function taxPhysical(): self public function taxPhysical($item): self
{ {
$this->tax_rate1 = $this->tax_rate; $this->tax_rate1 = $this->tax_rate;
@ -167,7 +196,7 @@ class Rule extends BaseRule implements RuleInterface
* *
* @return self * @return self
*/ */
public function default(): self public function default($item): self
{ {
$this->tax_name1 = ''; $this->tax_name1 = '';
@ -181,7 +210,7 @@ class Rule extends BaseRule implements RuleInterface
* *
* @return self * @return self
*/ */
public function override(): self public function override($item): self
{ {
return $this; return $this;
} }
@ -194,38 +223,42 @@ class Rule extends BaseRule implements RuleInterface
public function calculateRates(): self public function calculateRates(): self
{ {
if ($this->client->is_tax_exempt) { if ($this->client->is_tax_exempt) {
// nlog("tax exempt"); nlog("tax exempt");
$this->tax_rate = 0; $this->tax_rate = 0;
$this->reduced_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->has_valid_vat_number && $this->eu_business_tax_exempt) elseif($this->client_subregion != $this->client->company->tax_data->seller_subregion && in_array($this->client_subregion, $this->eu_country_codes) && $this->client->has_valid_vat_number && $this->eu_business_tax_exempt)
{ {
// nlog("euro zone and tax exempt"); nlog("euro zone and tax exempt");
$this->tax_rate = 0; $this->tax_rate = 0;
$this->reduced_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 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"); nlog("foreign and tax exempt");
$this->tax_rate = 0; $this->tax_rate = 0;
$this->reduced_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) && !$this->client->has_valid_vat_number) //eu country / no valid vat elseif(in_array($this->client_subregion, $this->eu_country_codes) && !$this->client->has_valid_vat_number) //eu country / no valid vat
{ {
if(($this->client->company->tax_data->seller_subregion != $this->client_subregion) && $this->client->company->tax_data->regions->EU->has_sales_above_threshold) if(($this->client->company->tax_data->seller_subregion != $this->client_subregion) && $this->client->company->tax_data->regions->EU->has_sales_above_threshold)
{ {
// nlog("eu zone with sales above threshold"); nlog("eu zone with sales above threshold");
$this->tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->country->iso_3166_2}->tax_rate; $this->tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->country->iso_3166_2}->tax_rate;
$this->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->country->iso_3166_2}->reduced_tax_rate; $this->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->country->iso_3166_2}->reduced_tax_rate;
} }
else { else {
// nlog("EU with intra-community supply ie DE to DE"); nlog("EU with intra-community supply ie DE to DE");
$this->tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->tax_rate; $this->tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->tax_rate;
$this->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->reduced_tax_rate; $this->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->reduced_tax_rate;
} }
} }
else { else {
// nlog("default tax"); nlog("default tax");
$this->tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->tax_rate; $this->tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->tax_rate;
$this->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->reduced_tax_rate; $this->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->reduced_tax_rate;
} }

View File

@ -15,25 +15,25 @@ interface RuleInterface
{ {
public function init(); public function init();
public function tax($item = null); public function tax($item);
public function taxByType($type); public function taxByType($type);
public function taxExempt(); public function taxExempt($item);
public function taxDigital(); public function taxDigital($item);
public function taxService(); public function taxService($item);
public function taxShipping(); public function taxShipping($item);
public function taxPhysical(); public function taxPhysical($item);
public function taxReduced(); public function taxReduced($item);
public function default(); public function default($item);
public function override(); public function override($item);
public function calculateRates(); public function calculateRates();
} }

View File

@ -41,31 +41,39 @@ class Rule extends BaseRule implements RuleInterface
/** /**
* Override tax class, we use this when we do not modify the input taxes * Override tax class, we use this when we do not modify the input taxes
* *
* @param mixed $item
* @return self * @return self
*/ */
public function override(): self public function override($item): self
{ {
$this->tax_rate1 = $item->tax_rate1;
$this->tax_name1 = $item->tax_name1;
return $this; return $this;
} }
/** /**
* Sets the correct tax rate based on the product type. * Sets the correct tax rate based on the product type.
* *
* @param mixed $product_tax_type * @param mixed $item
* @return self * @return self
*/ */
public function taxByType($product_tax_type): self public function taxByType($item): self
{ {
match(intval($product_tax_type)) { match(intval($item->tax_id)) {
Product::PRODUCT_TYPE_EXEMPT => $this->taxExempt(), Product::PRODUCT_TYPE_EXEMPT => $this->taxExempt($item),
Product::PRODUCT_TYPE_DIGITAL => $this->taxDigital(), Product::PRODUCT_TYPE_DIGITAL => $this->taxDigital($item),
Product::PRODUCT_TYPE_SERVICE => $this->taxService(), Product::PRODUCT_TYPE_SERVICE => $this->taxService($item),
Product::PRODUCT_TYPE_SHIPPING => $this->taxShipping(), Product::PRODUCT_TYPE_SHIPPING => $this->taxShipping($item),
Product::PRODUCT_TYPE_PHYSICAL => $this->taxPhysical(), Product::PRODUCT_TYPE_PHYSICAL => $this->taxPhysical($item),
Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced(), Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced($item),
Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override(), Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override($item),
default => $this->default(), Product::PRODUCT_TYPE_ZERO_RATED => $this->zeroRated($item),
default => $this->default($item),
}; };
return $this; return $this;
@ -73,10 +81,11 @@ class Rule extends BaseRule implements RuleInterface
/** /**
* Sets the tax as exempt (0) * Sets the tax as exempt (0)
* @param mixed $item
* *
* @return self * @return self
*/ */
public function taxExempt(): self public function taxExempt($item): self
{ {
$this->tax_name1 = ''; $this->tax_name1 = '';
$this->tax_rate1 = 0; $this->tax_rate1 = 0;
@ -86,25 +95,27 @@ class Rule extends BaseRule implements RuleInterface
/** /**
* Calculates the tax rate for a digital product * Calculates the tax rate for a digital product
* @param mixed $item
* *
* @return self * @return self
*/ */
public function taxDigital(): self public function taxDigital($item): self
{ {
$this->default(); $this->default($item);
return $this; return $this;
} }
/** /**
* Calculates the tax rate for a service product * Calculates the tax rate for a service product
* @param mixed $item
* *
* @return self * @return self
*/ */
public function taxService(): self public function taxService($item): self
{ {
if($this->tax_data?->txbService == 'Y') { if(in_array($this->tax_data?->txbService,['Y','L'])) {
$this->default(); $this->default($item);
} }
return $this; return $this;
@ -112,13 +123,15 @@ class Rule extends BaseRule implements RuleInterface
/** /**
* Calculates the tax rate for a shipping product * Calculates the tax rate for a shipping product
* @param mixed $item
* *
* @return self * @return self
*/ */
public function taxShipping(): self public function taxShipping($item): self
{ {
if($this->tax_data?->txbFreight == 'Y') { if($this->tax_data?->txbFreight == 'Y') {
$this->default(); $this->default($item);
} }
return $this; return $this;
@ -126,12 +139,15 @@ class Rule extends BaseRule implements RuleInterface
/** /**
* Calculates the tax rate for a physical product * Calculates the tax rate for a physical product
* @param mixed $item
* *
* @return self * @return self
*/ */
public function taxPhysical(): self public function taxPhysical($item): self
{ {
$this->default(); nlog("tax physical");
nlog($item);
$this->default($item);
return $this; return $this;
} }
@ -141,23 +157,13 @@ class Rule extends BaseRule implements RuleInterface
* *
* @return self * @return self
*/ */
public function default(): self public function default($item): self
{ {
if($this->tax_data?->stateSalesTax == 0) { if($this->tax_data?->stateSalesTax == 0) {
if($this->tax_data->originDestination == "O"){ $this->tax_rate1 = $this->invoice->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->tax_rate;
$tax_region = $this->client->company->tax_data->seller_subregion; $this->tax_name1 = $this->invoice->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->tax_name;
$this->tax_rate1 = $this->invoice->client->company->tax_data->regions->US->subregions->{$tax_region}->tax_rate;
$this->tax_name1 = "{$this->tax_data->geoState} Sales Tax";
} else {
$this->tax_rate1 = $this->invoice->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->tax_rate;
$this->tax_name1 = $this->invoice->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->tax_name;
if($this->client_region == 'US')
$this->tax_name1 = "{$this->client_subregion} ".$this->tax_name1;
}
return $this; return $this;
} }
@ -165,22 +171,43 @@ class Rule extends BaseRule implements RuleInterface
$this->tax_rate1 = $this->tax_data->taxSales * 100; $this->tax_rate1 = $this->tax_data->taxSales * 100;
$this->tax_name1 = "{$this->tax_data->geoState} Sales Tax"; $this->tax_name1 = "{$this->tax_data->geoState} Sales Tax";
return $this; return $this;
} }
public function zeroRated($item): self
{
$this->tax_rate1 = 0;
$this->tax_name1 = "{$this->tax_data->geoState} Zero Rated Tax";
return $this;
}
/** /**
* Calculates the tax rate for a reduced tax product * Calculates the tax rate for a reduced tax product
* *
* @return self * @return self
*/ */
public function taxReduced(): self public function taxReduced($item): self
{ {
$this->default(); $this->default($item);
return $this; return $this;
} }
/**
* Calculates the tax rate for a reverse tax product
*
* @return self
*/
public function reverseTax($item): self
{
$this->default($item);
return $this;
}
/** /**
* Calculates the tax rates to be applied * Calculates the tax rates to be applied
* *
@ -190,4 +217,5 @@ class Rule extends BaseRule implements RuleInterface
{ {
return $this; return $this;
} }
} }

View File

@ -16,7 +16,7 @@ use Illuminate\Support\Facades\Http;
class USStates class USStates
{ {
protected static array $states = [ public static array $states = [
'AL' => 'Alabama', 'AL' => 'Alabama',
'AK' => 'Alaska', 'AK' => 'Alaska',
'AZ' => 'Arizona', 'AZ' => 'Arizona',

View File

@ -29,6 +29,36 @@ class InvoiceItemSum
use Discounter; use Discounter;
use Taxer; use Taxer;
private array $eu_tax_jurisdictions = [
'AT', // Austria
'BE', // Belgium
'BG', // Bulgaria
'CY', // Cyprus
'CZ', // Czech Republic
'DE', // Germany
'DK', // Denmark
'EE', // Estonia
'ES', // Spain
'FI', // Finland
'FR', // France
'GR', // Greece
'HR', // Croatia
'HU', // Hungary
'IE', // Ireland
'IT', // Italy
'LT', // Lithuania
'LU', // Luxembourg
'LV', // Latvia
'MT', // Malta
'NL', // Netherlands
'PL', // Poland
'PT', // Portugal
'RO', // Romania
'SE', // Sweden
'SI', // Slovenia
'SK', // Slovakia
];
private array $tax_jurisdictions = [ private array $tax_jurisdictions = [
// 'AT', // Austria // 'AT', // Austria
// 'BE', // Belgium // 'BE', // Belgium
@ -144,15 +174,15 @@ class InvoiceItemSum
return $this; return $this;
} }
//should we be filtering by client country here? do we need to reflect at the company <=> client level? if (in_array($this->client->company->country()->iso_3166_2, $this->tax_jurisdictions) ) { //only calculate for supported tax jurisdictions
// if (in_array($this->client->country->iso_3166_2, $this->tax_jurisdictions)) { //only calculate for supported tax jurisdictions
if (in_array($this->client->company->country()->iso_3166_2, $this->tax_jurisdictions)) { //only calculate for supported tax jurisdictions
nlog($this->client->company->country()->iso_3166_2);
$class = "App\DataMapper\Tax\\".$this->client->company->country()->iso_3166_2."\\Rule"; $class = "App\DataMapper\Tax\\".$this->client->company->country()->iso_3166_2."\\Rule";
$this->rule = new $class(); $this->rule = new $class();
if($this->rule->regionWithNoTaxCoverage($this->client->country->iso_3166_2))
return $this;
$this->rule $this->rule
->setEntity($this->invoice) ->setEntity($this->invoice)
->init(); ->init();

View File

@ -13,8 +13,6 @@ namespace App\Http\Controllers;
use App\Http\Requests\Chart\ShowChartRequest; use App\Http\Requests\Chart\ShowChartRequest;
use App\Services\Chart\ChartService; use App\Services\Chart\ChartService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class ChartController extends BaseController class ChartController extends BaseController
{ {
@ -67,14 +65,19 @@ class ChartController extends BaseController
*/ */
public function totals(ShowChartRequest $request) public function totals(ShowChartRequest $request)
{ {
$cs = new ChartService(auth()->user()->company()); /** @var \App\Models\User auth()->user() */
$user = auth()->user();
$cs = new ChartService($user->company(), $user, $user->isAdmin());
return response()->json($cs->totals($request->input('start_date'), $request->input('end_date')), 200); return response()->json($cs->totals($request->input('start_date'), $request->input('end_date')), 200);
} }
public function chart_summary(ShowChartRequest $request) public function chart_summary(ShowChartRequest $request)
{ {
$cs = new ChartService(auth()->user()->company());
/** @var \App\Models\User auth()->user() */
$user = auth()->user();
$cs = new ChartService($user->company(), $user, $user->isAdmin());
return response()->json($cs->chart_summary($request->input('start_date'), $request->input('end_date')), 200); return response()->json($cs->chart_summary($request->input('start_date'), $request->input('end_date')), 200);
} }

View File

@ -41,6 +41,7 @@ use App\Utils\Traits\Uploadable;
use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Str;
use Turbo124\Beacon\Facades\LightLogs; use Turbo124\Beacon\Facades\LightLogs;
/** /**
@ -417,6 +418,13 @@ class CompanyController extends BaseController
$this->saveDocuments($request->input('documents'), $company, false); $this->saveDocuments($request->input('documents'), $company, false);
} }
if($request->has('e_invoice_certificate') && !is_null($request->file("e_invoice_certificate"))){
$company->e_invoice_certificate = base64_encode($request->file("e_invoice_certificate")->get());
$company->save();
}
$this->uploadLogo($request->file('company_logo'), $company, $company); $this->uploadLogo($request->file('company_logo'), $company, $company);
return $this->itemResponse($company); return $this->itemResponse($company);

View File

@ -46,74 +46,6 @@ class EmailController extends BaseController
parent::__construct(); parent::__construct();
} }
/**
* Returns a template filled with entity variables.
*
* @param SendEmailRequest $request
* @return Response
*
* @OA\Post(
* path="/api/v1/emails",
* operationId="sendEmailTemplate",
* tags={"emails"},
* summary="Sends an email for an entity",
* description="Sends an email for an entity",
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\RequestBody(
* description="The template subject and body",
* required=true,
* @OA\MediaType(
* mediaType="application/json",
* @OA\Schema(
* type="object",
* @OA\Property(
* property="subject",
* description="The email subject",
* type="string",
* ),
* @OA\Property(
* property="body",
* description="The email body",
* type="string",
* ),
* @OA\Property(
* property="entity",
* description="The entity name",
* type="string",
* ),
* @OA\Property(
* property="entity_id",
* description="The entity_id",
* type="string",
* ),
* @OA\Property(
* property="template",
* description="The template required",
* type="string",
* ),
* )
* )
* ),
* @OA\Response(
* response=200,
* description="success",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/Template"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function send(SendEmailRequest $request) public function send(SendEmailRequest $request)
{ {
$entity = $request->input('entity'); $entity = $request->input('entity');

View File

@ -143,8 +143,8 @@ class UpdateClientRequest extends Request
* down to the free plan setting properties which * down to the free plan setting properties which
* are saveable * are saveable
* *
* @param object $settings * @param \stdClass $settings
* @return stdClass $settings * @return \stdClass $settings
*/ */
private function filterSaveableSettings($settings) private function filterSaveableSettings($settings)
{ {

View File

@ -53,7 +53,8 @@ class UpdateCompanyRequest extends Request
$rules['country_id'] = 'integer|nullable'; $rules['country_id'] = 'integer|nullable';
$rules['work_email'] = 'email|nullable'; $rules['work_email'] = 'email|nullable';
$rules['matomo_id'] = 'nullable|integer'; $rules['matomo_id'] = 'nullable|integer';
$rules['e_invoice_certificate_passphrase'] = 'sometimes|nullable';
$rules['e_invoice_certificate'] = 'sometimes|nullable|file|mimes:p12,pfx,pem,cer,crt,der,txt,p7b,spc,bin';
// $rules['client_registration_fields'] = 'array'; // $rules['client_registration_fields'] = 'array';
if (isset($input['portal_mode']) && ($input['portal_mode'] == 'domain' || $input['portal_mode'] == 'iframe')) { if (isset($input['portal_mode']) && ($input['portal_mode'] == 'domain' || $input['portal_mode'] == 'iframe')) {
@ -82,6 +83,10 @@ class UpdateCompanyRequest extends Request
$input['settings'] = (array)$this->filterSaveableSettings($input['settings']); $input['settings'] = (array)$this->filterSaveableSettings($input['settings']);
} }
if(array_key_exists('e_invoice_certificate_passphrase', $input) && empty($input['e_invoice_certificate_passphrase'])) {
unset($input['e_invoice_certificate_passphrase']);
}
$this->replace($input); $this->replace($input);
} }

View File

@ -81,7 +81,7 @@ class UpdateRecurringQuoteRequest extends Request
* off / optin / optout will reset the status of this field to off to allow * off / optin / optout will reset the status of this field to off to allow
* the client to choose whether to auto_bill or not. * the client to choose whether to auto_bill or not.
* *
* @param enum $auto_bill off/always/optin/optout * @param string $auto_bill off/always/optin/optout
* *
* @return bool * @return bool
*/ */

View File

@ -45,7 +45,7 @@ class StoreSchedulerRequest extends Request
'parameters.end_date' => ['bail', 'sometimes', 'date:Y-m-d', 'required_if:parameters.date_rate,custom', 'after_or_equal:parameters.start_date'], 'parameters.end_date' => ['bail', 'sometimes', 'date:Y-m-d', 'required_if:parameters.date_rate,custom', 'after_or_equal:parameters.start_date'],
'parameters.entity' => ['bail', 'sometimes', 'string', 'in:invoice,credit,quote,purchase_order'], 'parameters.entity' => ['bail', 'sometimes', 'string', 'in:invoice,credit,quote,purchase_order'],
'parameters.entity_id' => ['bail', 'sometimes', 'string'], 'parameters.entity_id' => ['bail', 'sometimes', 'string'],
'parameters.report_name' => ['bail','sometimes', 'string', 'required_if:template,email_report', 'in:ar_summary_report,ar_detail_report,tax_summary_report,user_sales_report,client_sales_report,client_balance_report,product_sales_report'], 'parameters.report_name' => ['bail','sometimes', 'string', 'required_if:template,email_report', 'in:ar_detailed,ar_summary,client_balance,tax_summary,profitloss,client_sales,user_sales,product_sales,clients,client_contacts,credits,documents,expenses,invoices,invoice_items,quotes,quote_items,recurring_invoices,payments,products,tasks'],
'parameters.date_key' => ['bail','sometimes', 'string'], 'parameters.date_key' => ['bail','sometimes', 'string'],
]; ];
@ -60,6 +60,7 @@ class StoreSchedulerRequest extends Request
$this->merge(['next_run_client' => $input['next_run']]); $this->merge(['next_run_client' => $input['next_run']]);
} }
return $input; $this->replace($input);
} }
} }

View File

@ -42,7 +42,7 @@ class UpdateSchedulerRequest extends Request
'parameters.end_date' => ['bail', 'sometimes', 'date:Y-m-d', 'required_if:parameters.date_rate,custom', 'after_or_equal:parameters.start_date'], 'parameters.end_date' => ['bail', 'sometimes', 'date:Y-m-d', 'required_if:parameters.date_rate,custom', 'after_or_equal:parameters.start_date'],
'parameters.entity' => ['bail', 'sometimes', 'string', 'in:invoice,credit,quote,purchase_order'], 'parameters.entity' => ['bail', 'sometimes', 'string', 'in:invoice,credit,quote,purchase_order'],
'parameters.entity_id' => ['bail', 'sometimes', 'string'], 'parameters.entity_id' => ['bail', 'sometimes', 'string'],
'parameters.report_name' => ['bail','sometimes', 'string', 'required_if:template,email_report', 'in:ar_summary_report,ar_detail_report,tax_summary_report,user_sales_report,client_sales_report,client_balance_report,product_sales_report'], 'parameters.report_name' => ['bail','sometimes', 'string', 'required_if:template,email_report', 'in:ar_detailed,ar_summary,client_balance,tax_summary,profitloss,client_sales,user_sales,product_sales,clients,client_contacts,credits,documents,expenses,invoices,invoice_items,quotes,quote_items,recurring_invoices,payments,products,tasks'],
'parameters.date_key' => ['bail','sometimes', 'string'], 'parameters.date_key' => ['bail','sometimes', 'string'],
]; ];
@ -57,6 +57,6 @@ class UpdateSchedulerRequest extends Request
$this->merge(['next_run_client' => $input['next_run']]); $this->merge(['next_run_client' => $input['next_run']]);
} }
return $input; $this->replace($input);
} }
} }

View File

@ -28,7 +28,7 @@ class ClientTransformer extends BaseTransformer
public function transform($data) public function transform($data)
{ {
if (isset($data['Company Name']) && $this->hasClient($data['Company Name'])) { if (isset($data['Company Name']) && $this->hasClient($data['Company Name'])) {
throw new ImportException('Client already exists'); throw new ImportException('Client already exists => '. $data['Company Name']);
} }
$settings = new \stdClass; $settings = new \stdClass;
@ -40,7 +40,7 @@ class ClientTransformer extends BaseTransformer
$client_id_proxy = array_key_exists('Customer ID', $data) ? 'Customer ID' : 'Primary Contact ID'; $client_id_proxy = array_key_exists('Customer ID', $data) ? 'Customer ID' : 'Primary Contact ID';
return [ $data = [
'company_id' => $this->company->id, 'company_id' => $this->company->id,
'name' => $this->getString($data, 'Display Name'), 'name' => $this->getString($data, 'Display Name'),
'phone' => $this->getString($data, 'Phone'), 'phone' => $this->getString($data, 'Phone'),
@ -72,5 +72,7 @@ class ClientTransformer extends BaseTransformer
], ],
], ],
]; ];
return $data;
} }
} }

View File

@ -40,7 +40,8 @@ class InvoiceTransformer extends BaseTransformer
$transformed = [ $transformed = [
'company_id' => $this->company->id, 'company_id' => $this->company->id,
'client_id' => $this->getClient($this->getString($invoice_data, 'Customer ID'), $this->getString($invoice_data, 'Primary Contact EmailID')), // 'client_id' => $this->getClient($this->getString($invoice_data, 'Customer ID'), $this->getString($invoice_data, 'Primary Contact EmailID')),
'client_id' => $this->harvestClient($invoice_data),
'number' => $this->getString($invoice_data, 'Invoice Number'), 'number' => $this->getString($invoice_data, 'Invoice Number'),
'date' => isset($invoice_data['Invoice Date']) ? date('Y-m-d', strtotime($invoice_data['Invoice Date'])) : null, 'date' => isset($invoice_data['Invoice Date']) ? date('Y-m-d', strtotime($invoice_data['Invoice Date'])) : null,
'due_date' => isset($invoice_data['Due Date']) ? date('Y-m-d', strtotime($invoice_data['Due Date'])) : null, 'due_date' => isset($invoice_data['Due Date']) ? date('Y-m-d', strtotime($invoice_data['Due Date'])) : null,
@ -80,4 +81,77 @@ class InvoiceTransformer extends BaseTransformer
return $transformed; return $transformed;
} }
private function harvestClient($invoice_data)
{
$client_email = $this->getString($invoice_data, 'Primary Contact EmailID');
if (strlen($client_email) > 2) {
$contacts = \App\Models\ClientContact::whereHas('client', function ($query) {
$query->where('is_deleted', false);
})
->where('company_id', $this->company->id)
->where('email', $client_email);
if ($contacts->count() >= 1) {
return $contacts->first()->client_id;
}
}
$client_name = $this->getString($invoice_data, 'Customer Name');
if(strlen($client_name) >= 2) {
$client_name_search = \App\Models\Client::where('company_id', $this->company->id)
->where('is_deleted', false)
->whereRaw("LOWER(REPLACE(`name`, ' ' ,'')) = ?", [
strtolower(str_replace(' ', '', $client_name)),
]);
if ($client_name_search->count() >= 1) {
return $client_name_search->first()->id;
}
}
$customer_id = $this->getString($invoice_data, 'Customer ID');
$client_id_search = \App\Models\Client::where('company_id', $this->company->id)
->where('is_deleted', false)
->where('id_number', trim($customer_id));
if ($client_id_search->count() >= 1) {
return $client_id_search->first()->id;
}
$client_repository = app()->make(\App\Repositories\ClientRepository::class);
$client_repository->import_mode = true;
$client = $client_repository->save(
[
'name' => $client_name,
'contacts' => [
[
'first_name' => $client_name,
'email' => $client_email,
],
],
'address1' => $this->getString($invoice_data, 'Billing Address'),
'city' => $this->getString($invoice_data, 'Billing City'),
'state' => $this->getString($invoice_data, 'Billing State'),
'postal_code' => $this->getString($invoice_data, 'Billing Code'),
'country_id' => $this->getCountryId($this->getString($invoice_data, 'Billing Country')),
],
\App\Factory\ClientFactory::create(
$this->company->id,
$this->company->owner()->id
)
);
$client_repository = null;
return $client->id;
}
} }

View File

@ -0,0 +1,78 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Jobs\Client;
use App\DataProviders\USStates;
use App\Models\Client;
use App\Models\Company;
use App\Libraries\MultiDB;
use Illuminate\Bus\Queueable;
use App\Utils\Traits\MakesHash;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class UpdateTaxData implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
use MakesHash;
public $tries = 1;
/**
* Create a new job instance.
*
* @param Client $client
* @param Company $company
*/
public function __construct(public Client $client, protected Company $company)
{
}
/**
* Execute the job.
*
*/
public function handle()
{
MultiDB::setDb($this->company->db);
if(!config('services.tax.zip_tax.key'))
return;
$tax_provider = new \App\Services\Tax\Providers\TaxProvider($this->company, $this->client);
try {
$tax_provider->updateClientTaxData();
if (!$this->client->state && $this->client->postal_code) {
$this->client->state = USStates::getState($this->client->postal_code);
$this->client->save();
}
}catch(\Exception $e){
nlog("problem getting tax data => ".$e->getMessage());
}
}
}

View File

@ -13,12 +13,14 @@ namespace App\Jobs\Company;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Models\Company; use App\Models\Company;
use App\Models\Country;
use App\Libraries\MultiDB; use App\Libraries\MultiDB;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use App\DataMapper\Tax\TaxModel; use App\DataMapper\Tax\TaxModel;
use App\DataMapper\CompanySettings; use App\DataMapper\CompanySettings;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use App\DataMapper\ClientRegistrationFields; use App\DataMapper\ClientRegistrationFields;
use App\Factory\TaxRateFactory;
class CreateCompany class CreateCompany
{ {
@ -53,6 +55,10 @@ class CreateCompany
$settings->name = isset($this->request['name']) ? $this->request['name'] : ''; $settings->name = isset($this->request['name']) ? $this->request['name'] : '';
if($country_id = $this->resolveCountry()){
$settings->country_id = $country_id;
}
$company = new Company(); $company = new Company();
$company->account_id = $this->account->id; $company->account_id = $this->account->id;
$company->company_key = $this->createHash(); $company->company_key = $this->createHash();
@ -74,8 +80,135 @@ class CreateCompany
$company->subdomain = ''; $company->subdomain = '';
} }
$company->save(); /** Location Specific Configuration */
match($settings->country_id) {
'724' => $company = $this->spanishSetup($company),
'36' => $company = $this->australiaSetup($company),
default => $company->save(),
};
return $company; return $company;
} }
/**
* Resolve Country
*
* @return string
*/
private function resolveCountry(): string
{
try{
$ip = request()->ip();
if(request()->hasHeader('cf-ipcountry')){
$c = Country::where('iso_3166_2', request()->header('cf-ipcountry'))->first();
if($c)
return (string)$c->id;
}
$details = json_decode(file_get_contents("http://ip-api.com/json/{$ip}"));
if($details && property_exists($details, 'countryCode')){
$c = Country::where('iso_3166_2', $details->countryCode)->first();
if($c)
return (string)$c->id;
}
}
catch(\Exception $e){
nlog("Could not resolve country => {$e->getMessage()}");
}
return '840';
}
private function spanishSetup(Company $company): Company
{
try {
$custom_fields = new \stdClass;
$custom_fields->contact1 = "Rol|CONTABLE,FISCAL,GESTOR,RECEPTOR,TRAMITADOR,PAGADOR,PROPONENTE,B2B_FISCAL,B2B_PAYER,B2B_BUYER,B2B_COLLECTOR,B2B_SELLER,B2B_PAYMENT_RECEIVER,B2B_COLLECTION_RECEIVER,B2B_ISSUER";
$custom_fields->contact2 = "Code|single_line_text";
$custom_fields->contact3 = "Nombre|single_line_text";
$custom_fields->client1 = "Administración Pública|switch";
$company->custom_fields = $custom_fields;
$company->enabled_item_tax_rates = 1;
$settings = $company->settings;
$settings->language_id = '7';
$settings->e_invoice_type = 'Facturae_3.2.2';
$settings->currency_id = '3';
$settings->timezone_id = '42';
$company->settings = $settings;
$company->save();
$user = $this->account->users()->orderBy('id','asc')->first();
$tax_rate = TaxRateFactory::create($company->id, $user->id);
$tax_rate->name = $company->tax_data->regions->EU->subregions->ES->tax_name;
$tax_rate->rate = $company->tax_data->regions->EU->subregions->ES->tax_rate;
$tax_rate->save();
return $company;
}
catch(\Exception $e){
nlog("SETUP: could not complete setup for Spanish Locale");
}
$company->save();
return $company;
}
private function australiaSetup(Company $company): Company
{
try {
$company->enabled_item_tax_rates = 1;
$company->enabled_tax_rates = 1;
$translations = new \stdClass;
$translations->invoice = "Tax Invoice";
$settings = $company->settings;
$settings->currency_id = '12';
$settings->timezone_id = '109';
$settings->translations = $translations;
$company->settings = $settings;
$company->save();
$user = $company->account->users()->first();
$tax_rate = TaxRateFactory::create($company->id, $user->id);
$tax_rate->name = $company->tax_data->regions->AU->subregions->AU->tax_name;
$tax_rate->rate = $company->tax_data->regions->AU->subregions->AU->tax_rate;
$tax_rate->save();
return $company;
}
catch(\Exception $e){
nlog("SETUP: could not complete setup for Spanish Locale");
}
$company->save();
return $company;
}
} }

View File

@ -150,23 +150,6 @@ class EmailEntity implements ShouldQueue
return ''; return '';
} }
/**
* @deprecated
* @unused
*/
// private function entityEmailFailed($message)
// {
// switch ($this->entity_string) {
// case 'invoice':
// event(new InvoiceWasEmailedAndFailed($this->invitation, $this->company, $message, $this->reminder_template, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
// break;
// default:
// // code...
// break;
// }
// }
/* Builds the email builder object */ /* Builds the email builder object */
private function resolveEmailBuilder() private function resolveEmailBuilder()
{ {

View File

@ -63,20 +63,6 @@ class InvoiceEmailedNotification implements ShouldQueue
if (($key = array_search('mail', $methods)) !== false) { if (($key = array_search('mail', $methods)) !== false) {
unset($methods[$key]); unset($methods[$key]);
// $template = $event->template ?? '';
// if(isset($event->reminder)){
// $template = match($event->reminder){
// 63 => 'reminder1',
// 64 => 'reminder2',
// 65 => 'reminder3',
// 66 => 'endless_reminder',
// default => ''
// };
// }
$nmo = new NinjaMailerObject; $nmo = new NinjaMailerObject;
$nmo->mailable = new NinjaMailer((new EntitySentObject($event->invitation, 'invoice', $event->template))->build()); $nmo->mailable = new NinjaMailer((new EntitySentObject($event->invitation, 'invoice', $event->template))->build());
$nmo->company = $invoice->company; $nmo->company = $invoice->company;

View File

@ -46,12 +46,21 @@ class InvoiceReminderEmailActivity implements ShouldQueue
$user_id = array_key_exists('user_id', $event->event_vars) ? $event->event_vars['user_id'] : $event->invitation->invoice->user_id; $user_id = array_key_exists('user_id', $event->event_vars) ? $event->event_vars['user_id'] : $event->invitation->invoice->user_id;
$reminder = match($event->template){
'reminder1' => 63,
'reminder2' => 64,
'reminder3' => 65,
'reminder_endless' => 66,
'endless_reminder' => 66,
default => 6,
};
$fields->user_id = $user_id; $fields->user_id = $user_id;
$fields->invoice_id = $event->invitation->invoice_id; $fields->invoice_id = $event->invitation->invoice_id;
$fields->company_id = $event->invitation->company_id; $fields->company_id = $event->invitation->company_id;
$fields->client_contact_id = $event->invitation->client_contact_id; $fields->client_contact_id = $event->invitation->client_contact_id;
$fields->client_id = $event->invitation->invoice->client_id; $fields->client_id = $event->invitation->invoice->client_id;
$fields->activity_type_id = $event->reminder; $fields->activity_type_id = $reminder;
$this->activity_repo->save($fields, $event->invitation, $event->event_vars); $this->activity_repo->save($fields, $event->invitation, $event->event_vars);
} }

View File

@ -11,18 +11,19 @@
namespace App\Models; namespace App\Models;
use App\DataMapper\CompanySettings;
use App\Models\Presenters\CompanyPresenter;
use App\Services\Notification\NotificationService;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Casts\EncryptedCast;
use App\Utils\Traits\AppSetup; use App\Utils\Traits\AppSetup;
use App\Utils\Traits\CompanySettingsSaver;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use App\DataMapper\CompanySettings;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Notifications\Notification;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Laracasts\Presenter\PresentableTrait; use Laracasts\Presenter\PresentableTrait;
use App\Utils\Traits\CompanySettingsSaver;
use Illuminate\Notifications\Notification;
use App\Models\Presenters\CompanyPresenter;
use App\Services\Notification\NotificationService;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/** /**
* App\Models\Company * App\Models\Company
@ -339,6 +340,7 @@ class Company extends BaseModel
'notify_vendor_when_paid', 'notify_vendor_when_paid',
'calculate_taxes', 'calculate_taxes',
'tax_data', 'tax_data',
'e_invoice_certificate_passphrase',
]; ];
protected $hidden = [ protected $hidden = [
@ -357,6 +359,8 @@ class Company extends BaseModel
'deleted_at' => 'timestamp', 'deleted_at' => 'timestamp',
'client_registration_fields' => 'array', 'client_registration_fields' => 'array',
'tax_data' => 'object', 'tax_data' => 'object',
'origin_tax_data' => 'object',
'e_invoice_certificate_passphrase' => EncryptedCast::class,
]; ];
protected $with = []; protected $with = [];
@ -365,7 +369,6 @@ class Company extends BaseModel
self::ENTITY_RECURRING_INVOICE => 1, self::ENTITY_RECURRING_INVOICE => 1,
self::ENTITY_CREDIT => 2, self::ENTITY_CREDIT => 2,
self::ENTITY_QUOTE => 4, self::ENTITY_QUOTE => 4,
// @phpstan-ignore-next-line
self::ENTITY_TASK => 8, self::ENTITY_TASK => 8,
self::ENTITY_EXPENSE => 16, self::ENTITY_EXPENSE => 16,
self::ENTITY_PROJECT => 32, self::ENTITY_PROJECT => 32,

View File

@ -824,7 +824,7 @@ class Invoice extends BaseModel
case 'custom1': case 'custom1':
case 'custom2': case 'custom2':
case 'custom3': case 'custom3':
event(new InvoiceReminderWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $template)); event(new InvoiceWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $template));
break; break;
default: default:
// code... // code...

View File

@ -11,9 +11,10 @@
namespace App\Observers; namespace App\Observers;
use App\Jobs\Util\WebhookHandler;
use App\Models\Client; use App\Models\Client;
use App\Models\Webhook; use App\Models\Webhook;
use App\Jobs\Util\WebhookHandler;
use App\Jobs\Client\UpdateTaxData;
class ClientObserver class ClientObserver
{ {
@ -27,6 +28,11 @@ class ClientObserver
*/ */
public function created(Client $client) public function created(Client $client)
{ {
if ($client->country_id == 840 && $client->company->calculate_taxes) {
UpdateTaxData::dispatch($client, $client->company);
}
$subscriptions = Webhook::where('company_id', $client->company_id) $subscriptions = Webhook::where('company_id', $client->company_id)
->where('event_id', Webhook::EVENT_CREATE_CLIENT) ->where('event_id', Webhook::EVENT_CREATE_CLIENT)
->exists(); ->exists();
@ -44,6 +50,11 @@ class ClientObserver
*/ */
public function updated(Client $client) public function updated(Client $client)
{ {
if($client->getOriginal('postal_code') != $client->postal_code && $client->country_id == 840 && $client->company->calculate_taxes)
{
UpdateTaxData::dispatch($client, $client->company);
}
$event = Webhook::EVENT_UPDATE_CLIENT; $event = Webhook::EVENT_UPDATE_CLIENT;
if ($client->getOriginal('deleted_at') && !$client->deleted_at) { if ($client->getOriginal('deleted_at') && !$client->deleted_at) {
@ -53,8 +64,7 @@ class ClientObserver
if ($client->is_deleted) { if ($client->is_deleted) {
$event = Webhook::EVENT_DELETE_CLIENT; $event = Webhook::EVENT_DELETE_CLIENT;
} }
$subscriptions = Webhook::where('company_id', $client->company_id) $subscriptions = Webhook::where('company_id', $client->company_id)
->where('event_id', $event) ->where('event_id', $event)
->exists(); ->exists();

View File

@ -40,6 +40,12 @@ class CompanyObserver
//fire event to build new custom portal domain //fire event to build new custom portal domain
\Modules\Admin\Jobs\Domain\CustomDomain::dispatch($company->getOriginal('portal_domain'), $company)->onQueue('domain'); \Modules\Admin\Jobs\Domain\CustomDomain::dispatch($company->getOriginal('portal_domain'), $company)->onQueue('domain');
} }
// if($company->wasChanged()) {
// nlog("updated event");
// nlog($company->getChanges());
// }
} }
/** /**

View File

@ -43,7 +43,7 @@ class Charge
* Create a charge against a payment method. * Create a charge against a payment method.
* @param ClientGatewayToken $cgt * @param ClientGatewayToken $cgt
* @param PaymentHash $payment_hash * @param PaymentHash $payment_hash
* @return bool success/failure * @return mixed success/failure
* @throws \Laracasts\Presenter\Exceptions\PresenterException * @throws \Laracasts\Presenter\Exceptions\PresenterException
*/ */
public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash) public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)
@ -86,7 +86,7 @@ class Charge
$data['off_session'] = true; $data['off_session'] = true;
} }
$response = $this->stripe->createPaymentIntent($data, array_merge($this->stripe->stripe_connect_auth, ['idempotency_key' => uniqid("st", true)])); $response = $this->stripe->createPaymentIntent($data);
SystemLogger::dispatch($response, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_STRIPE, $this->stripe->client, $this->stripe->client->company); SystemLogger::dispatch($response, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_STRIPE, $this->stripe->client, $this->stripe->client->company);
} catch (\Exception $e) { } catch (\Exception $e) {

View File

@ -777,7 +777,8 @@ class StripePaymentDriver extends BaseDriver
->where('token', $request->data['object']['payment_method']) ->where('token', $request->data['object']['payment_method'])
->first(); ->first();
$clientgateway->delete(); if($clientgateway)
$clientgateway->delete();
return response()->json([], 200); return response()->json([], 200);
} elseif ($request->data['object']['status'] == "pending") { } elseif ($request->data['object']['status'] == "pending") {

View File

@ -78,6 +78,14 @@ class RouteServiceProvider extends ServiceProvider
} }
}); });
RateLimiter::for('404', function (Request $request) {
if (Ninja::isSelfHost()) {
return Limit::none();
} else {
return Limit::perMinute(25)->by($request->ip());
}
});
} }
/** /**

View File

@ -23,20 +23,26 @@ trait ChartQueries
*/ */
public function getExpenseQuery($start_date, $end_date) public function getExpenseQuery($start_date, $end_date)
{ {
return DB::select(DB::raw(' $user_filter = $this->is_admin ? '' : 'AND expenses.user_id = '.$this->user->id;
return DB::select(DB::raw("
SELECT sum(expenses.amount) as amount, SELECT sum(expenses.amount) as amount,
IFNULL(expenses.currency_id, :company_currency) as currency_id IFNULL(expenses.currency_id, :company_currency) as currency_id
FROM expenses FROM expenses
WHERE expenses.is_deleted = 0 WHERE expenses.is_deleted = 0
AND expenses.company_id = :company_id AND expenses.company_id = :company_id
AND (expenses.date BETWEEN :start_date AND :end_date) AND (expenses.date BETWEEN :start_date AND :end_date)
{$user_filter}
GROUP BY currency_id GROUP BY currency_id
'), ['company_currency' => $this->company->settings->currency_id, 'company_id' => $this->company->id, 'start_date' => $start_date, 'end_date' => $end_date]); "), ['company_currency' => $this->company->settings->currency_id, 'company_id' => $this->company->id, 'start_date' => $start_date, 'end_date' => $end_date]);
} }
public function getExpenseChartQuery($start_date, $end_date, $currency_id) public function getExpenseChartQuery($start_date, $end_date, $currency_id)
{ {
return DB::select(DB::raw('
$user_filter = $this->is_admin ? '' : 'AND expenses.user_id = '.$this->user->id;
return DB::select(DB::raw("
SELECT SELECT
sum(expenses.amount) as total, sum(expenses.amount) as total,
expenses.date, expenses.date,
@ -45,9 +51,10 @@ trait ChartQueries
WHERE (expenses.date BETWEEN :start_date AND :end_date) WHERE (expenses.date BETWEEN :start_date AND :end_date)
AND expenses.company_id = :company_id AND expenses.company_id = :company_id
AND expenses.is_deleted = 0 AND expenses.is_deleted = 0
{$user_filter}
GROUP BY expenses.date GROUP BY expenses.date
HAVING currency_id = :currency_id HAVING currency_id = :currency_id
'), [ "), [
'company_currency' => $this->company->settings->currency_id, 'company_currency' => $this->company->settings->currency_id,
'currency_id' => $currency_id, 'currency_id' => $currency_id,
'company_id' => $this->company->id, 'company_id' => $this->company->id,
@ -61,15 +68,19 @@ trait ChartQueries
*/ */
public function getPaymentQuery($start_date, $end_date) public function getPaymentQuery($start_date, $end_date)
{ {
return DB::select(DB::raw('
$user_filter = $this->is_admin ? '' : 'AND payments.user_id = '.$this->user->id;
return DB::select(DB::raw("
SELECT sum(payments.amount) as amount, SELECT sum(payments.amount) as amount,
IFNULL(payments.currency_id, :company_currency) as currency_id IFNULL(payments.currency_id, :company_currency) as currency_id
FROM payments FROM payments
WHERE payments.is_deleted = 0 WHERE payments.is_deleted = 0
{$user_filter}
AND payments.company_id = :company_id AND payments.company_id = :company_id
AND (payments.date BETWEEN :start_date AND :end_date) AND (payments.date BETWEEN :start_date AND :end_date)
GROUP BY currency_id GROUP BY currency_id
'), [ "), [
'company_currency' => $this->company->settings->currency_id, 'company_currency' => $this->company->settings->currency_id,
'company_id' => $this->company->id, 'company_id' => $this->company->id,
'start_date' => $start_date, 'start_date' => $start_date,
@ -79,7 +90,10 @@ trait ChartQueries
public function getPaymentChartQuery($start_date, $end_date, $currency_id) public function getPaymentChartQuery($start_date, $end_date, $currency_id)
{ {
return DB::select(DB::raw('
$user_filter = $this->is_admin ? '' : 'AND payments.user_id = '.$this->user->id;
return DB::select(DB::raw("
SELECT SELECT
sum(payments.amount - payments.refunded) as total, sum(payments.amount - payments.refunded) as total,
payments.date, payments.date,
@ -87,11 +101,12 @@ trait ChartQueries
FROM payments FROM payments
WHERE payments.company_id = :company_id WHERE payments.company_id = :company_id
AND payments.is_deleted = 0 AND payments.is_deleted = 0
{$user_filter}
AND payments.status_id IN (4,5,6) AND payments.status_id IN (4,5,6)
AND (payments.date BETWEEN :start_date AND :end_date) AND (payments.date BETWEEN :start_date AND :end_date)
GROUP BY payments.date GROUP BY payments.date
HAVING currency_id = :currency_id HAVING currency_id = :currency_id
'), [ "), [
'company_currency' => $this->company->settings->currency_id, 'company_currency' => $this->company->settings->currency_id,
'currency_id' => $currency_id, 'currency_id' => $currency_id,
'company_id' => $this->company->id, 'company_id' => $this->company->id,
@ -105,6 +120,9 @@ trait ChartQueries
*/ */
public function getOutstandingQuery($start_date, $end_date) public function getOutstandingQuery($start_date, $end_date)
{ {
$user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id;
return DB::select(DB::raw(" return DB::select(DB::raw("
SELECT SELECT
sum(invoices.balance) as amount, sum(invoices.balance) as amount,
@ -116,6 +134,7 @@ trait ChartQueries
WHERE invoices.status_id IN (2,3) WHERE invoices.status_id IN (2,3)
AND invoices.company_id = :company_id AND invoices.company_id = :company_id
AND clients.is_deleted = 0 AND clients.is_deleted = 0
{$user_filter}
AND invoices.is_deleted = 0 AND invoices.is_deleted = 0
AND invoices.balance > 0 AND invoices.balance > 0
AND (invoices.date BETWEEN :start_date AND :end_date) AND (invoices.date BETWEEN :start_date AND :end_date)
@ -125,6 +144,8 @@ trait ChartQueries
public function getRevenueQuery($start_date, $end_date) public function getRevenueQuery($start_date, $end_date)
{ {
$user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id;
return DB::select(DB::raw(" return DB::select(DB::raw("
SELECT SELECT
sum(invoices.paid_to_date) as paid_to_date, sum(invoices.paid_to_date) as paid_to_date,
@ -134,6 +155,7 @@ trait ChartQueries
on invoices.client_id = clients.id on invoices.client_id = clients.id
WHERE invoices.company_id = :company_id WHERE invoices.company_id = :company_id
AND clients.is_deleted = 0 AND clients.is_deleted = 0
{$user_filter}
AND invoices.is_deleted = 0 AND invoices.is_deleted = 0
AND invoices.amount > 0 AND invoices.amount > 0
AND invoices.status_id IN (3,4) AND invoices.status_id IN (3,4)
@ -144,6 +166,8 @@ trait ChartQueries
public function getInvoicesQuery($start_date, $end_date) public function getInvoicesQuery($start_date, $end_date)
{ {
$user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id;
return DB::select(DB::raw(" return DB::select(DB::raw("
SELECT SELECT
sum(invoices.amount) as invoiced_amount, sum(invoices.amount) as invoiced_amount,
@ -153,6 +177,7 @@ trait ChartQueries
on invoices.client_id = clients.id on invoices.client_id = clients.id
WHERE invoices.status_id IN (2,3,4) WHERE invoices.status_id IN (2,3,4)
AND invoices.company_id = :company_id AND invoices.company_id = :company_id
{$user_filter}
AND invoices.amount > 0 AND invoices.amount > 0
AND clients.is_deleted = 0 AND clients.is_deleted = 0
AND invoices.is_deleted = 0 AND invoices.is_deleted = 0
@ -163,6 +188,8 @@ trait ChartQueries
public function getOutstandingChartQuery($start_date, $end_date, $currency_id) public function getOutstandingChartQuery($start_date, $end_date, $currency_id)
{ {
$user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id;
return DB::select(DB::raw(" return DB::select(DB::raw("
SELECT SELECT
sum(invoices.balance) as total, sum(invoices.balance) as total,
@ -175,6 +202,7 @@ trait ChartQueries
AND invoices.company_id = :company_id AND invoices.company_id = :company_id
AND clients.is_deleted = 0 AND clients.is_deleted = 0
AND invoices.is_deleted = 0 AND invoices.is_deleted = 0
{$user_filter}
AND (invoices.date BETWEEN :start_date AND :end_date) AND (invoices.date BETWEEN :start_date AND :end_date)
GROUP BY invoices.date GROUP BY invoices.date
HAVING currency_id = :currency_id HAVING currency_id = :currency_id
@ -190,6 +218,8 @@ trait ChartQueries
public function getInvoiceChartQuery($start_date, $end_date, $currency_id) public function getInvoiceChartQuery($start_date, $end_date, $currency_id)
{ {
$user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id;
return DB::select(DB::raw(" return DB::select(DB::raw("
SELECT SELECT
sum(invoices.amount) as total, sum(invoices.amount) as total,
@ -201,6 +231,7 @@ trait ChartQueries
WHERE invoices.company_id = :company_id WHERE invoices.company_id = :company_id
AND clients.is_deleted = 0 AND clients.is_deleted = 0
AND invoices.is_deleted = 0 AND invoices.is_deleted = 0
{$user_filter}
AND invoices.status_id IN (2,3,4) AND invoices.status_id IN (2,3,4)
AND (invoices.date BETWEEN :start_date AND :end_date) AND (invoices.date BETWEEN :start_date AND :end_date)
GROUP BY invoices.date GROUP BY invoices.date

View File

@ -11,6 +11,7 @@
namespace App\Services\Chart; namespace App\Services\Chart;
use App\Models\User;
use App\Models\Client; use App\Models\Client;
use App\Models\Company; use App\Models\Company;
use App\Models\Expense; use App\Models\Expense;
@ -20,11 +21,8 @@ class ChartService
{ {
use ChartQueries; use ChartQueries;
public Company $company; public function __construct(public Company $company, private User $user, private bool $is_admin)
public function __construct(Company $company)
{ {
$this->company = $company;
} }
/** /**
@ -37,6 +35,9 @@ class ChartService
$currencies = Client::withTrashed() $currencies = Client::withTrashed()
->where('company_id', $this->company->id) ->where('company_id', $this->company->id)
->where('is_deleted', 0) ->where('is_deleted', 0)
->when(!$this->is_admin, function ($query) {
$query->where('user_id', $this->user->id);
})
->distinct() ->distinct()
->pluck('settings->currency_id as id'); ->pluck('settings->currency_id as id');
@ -47,6 +48,9 @@ class ChartService
$expense_currencies = Expense::withTrashed() $expense_currencies = Expense::withTrashed()
->where('company_id', $this->company->id) ->where('company_id', $this->company->id)
->where('is_deleted', 0) ->where('is_deleted', 0)
->when(!$this->is_admin, function ($query) {
$query->where('user_id', $this->user->id);
})
->distinct() ->distinct()
->pluck('currency_id as id'); ->pluck('currency_id as id');

View File

@ -18,6 +18,7 @@ use josemmo\Facturae\FacturaeItem;
use josemmo\Facturae\FacturaeParty; use josemmo\Facturae\FacturaeParty;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use josemmo\Facturae\Common\FacturaeSigner; use josemmo\Facturae\Common\FacturaeSigner;
use josemmo\Facturae\FacturaeCentre;
class FacturaEInvoice extends AbstractService class FacturaEInvoice extends AbstractService
{ {
@ -25,6 +26,24 @@ class FacturaEInvoice extends AbstractService
private $calc; private $calc;
private $centre_codes = [
'CONTABLE' => FacturaeCentre::ROLE_CONTABLE,
'FISCAL' => FacturaeCentre::ROLE_FISCAL,
'GESTOR' => FacturaeCentre::ROLE_GESTOR,
'RECEPTOR' => FacturaeCentre::ROLE_RECEPTOR,
'TRAMITADOR' => FacturaeCentre::ROLE_TRAMITADOR,
'PAGADOR' => FacturaeCentre::ROLE_PAGADOR,
'PROPONENTE' => FacturaeCentre::ROLE_PAGADOR,
'B2B_FISCAL' => FacturaeCentre::ROLE_B2B_FISCAL,
'B2B_PAYER' => FacturaeCentre::ROLE_B2B_PAYER,
'B2B_BUYER' => FacturaeCentre::ROLE_B2B_BUYER,
'B2B_COLLECTOR' => FacturaeCentre::ROLE_B2B_COLLECTOR,
'B2B_SELLER' => FacturaeCentre::ROLE_B2B_SELLER,
'B2B_PAYMENT_RECEIVER' => FacturaeCentre::ROLE_B2B_PAYMENT_RECEIVER ,
'B2B_COLLECTION_RECEIVER' => FacturaeCentre::ROLE_B2B_COLLECTION_RECEIVER ,
'B2B_ISSUER' => FacturaeCentre::ROLE_B2B_ISSUER,
];
// Facturae::SCHEMA_3_2 Invoice Format 3.2 // Facturae::SCHEMA_3_2 Invoice Format 3.2
// Facturae::SCHEMA_3_2_1 Invoice Format 3.2.1 // Facturae::SCHEMA_3_2_1 Invoice Format 3.2.1
// Facturae::SCHEMA_3_2_2 Invoice Format 3.2.2 // Facturae::SCHEMA_3_2_2 Invoice Format 3.2.2
@ -111,6 +130,25 @@ class FacturaEInvoice extends AbstractService
// FacturaeCentre::ROLE_B2B_COLLECTION_RECEIVER Collection receiver in FACeB2B // FacturaeCentre::ROLE_B2B_COLLECTION_RECEIVER Collection receiver in FACeB2B
// FacturaeCentre::ROLE_B2B_ISSUER Issuer in FACeB2B // FacturaeCentre::ROLE_B2B_ISSUER Issuer in FACeB2B
/*
const ROLE_CONTABLE = "01";
const ROLE_FISCAL = "01";
const ROLE_GESTOR = "02";
const ROLE_RECEPTOR = "02";
const ROLE_TRAMITADOR = "03";
const ROLE_PAGADOR = "03";
const ROLE_PROPONENTE = "04";
const ROLE_B2B_FISCAL = "Fiscal";
const ROLE_B2B_PAYER = "Payer";
const ROLE_B2B_BUYER = "Buyer";
const ROLE_B2B_COLLECTOR = "Collector";
const ROLE_B2B_SELLER = "Seller";
const ROLE_B2B_PAYMENT_RECEIVER = "Payment receiver";
const ROLE_B2B_COLLECTION_RECEIVER = "Collection receiver";
const ROLE_B2B_ISSUER = "Issuer";
*/
public function __construct(public Invoice $invoice, private mixed $profile) public function __construct(public Invoice $invoice, private mixed $profile)
{ {
@ -146,6 +184,33 @@ class FacturaEInvoice extends AbstractService
} }
/** Check if this is a public administration body */
private function setFace(): array
{
$facturae_centres = [];
if($this->invoice->client->custom_value1 == 'yes')
{
foreach($this->invoice->client->contacts() as $contact)
{
if(in_array($contact->custom_value1, array_keys($this->centre_codes)))
{
$facturae_centres[] = new FacturaeCentre([
'role' => $this->centre_codes[$contact->custom_value1],
'code' => $contact->custom_value2,
'name' => $contact->custom_value3,
]);
}
}
}
return $facturae_centres;
}
private function setPoNumber(): self private function setPoNumber(): self
{ {
if(strlen($this->invoice->po_number) > 1) { if(strlen($this->invoice->po_number) > 1) {
@ -280,6 +345,7 @@ class FacturaEInvoice extends AbstractService
"fax" => "", "fax" => "",
"website" => substr($company->settings->website, 0, 50), "website" => substr($company->settings->website, 0, 50),
"contactPeople" => substr($company->owner()->present()->name(), 0, 40), "contactPeople" => substr($company->owner()->present()->name(), 0, 40),
'centres' => $this->setFace(),
// "cnoCnae" => "04647", // Clasif. Nacional de Act. Económicas // "cnoCnae" => "04647", // Clasif. Nacional de Act. Económicas
// "ineTownCode" => "280796" // Cód. de municipio del INE // "ineTownCode" => "280796" // Cód. de municipio del INE
]); ]);

View File

@ -77,13 +77,13 @@ class EmailReport
match($this->scheduler->parameters['report_name']) match($this->scheduler->parameters['report_name'])
{ {
'product_sales_report' => $export = (new ProductSalesExport($this->scheduler->company, $data)), 'product_sales' => $export = (new ProductSalesExport($this->scheduler->company, $data)),
'email_ar_detailed_report' => $export = (new ARDetailReport($this->scheduler->company, $data)), 'ar_detailed' => $export = (new ARDetailReport($this->scheduler->company, $data)),
'email_ar_summary_report' => $export = (new ARSummaryReport($this->scheduler->company, $data)), 'ar_summary' => $export = (new ARSummaryReport($this->scheduler->company, $data)),
'email_tax_summary_report' => $export = (new TaxSummaryReport($this->scheduler->company, $data)), 'tax_summary' => $export = (new TaxSummaryReport($this->scheduler->company, $data)),
'email_client_balance_report' => $export = (new ClientBalanceReport($this->scheduler->company, $data)), 'client_balance' => $export = (new ClientBalanceReport($this->scheduler->company, $data)),
'email_client_sales_report' => $export = (new ClientSalesReport($this->scheduler->company, $data)), 'client_sales' => $export = (new ClientSalesReport($this->scheduler->company, $data)),
'email_user_sales_report' => $export = (new UserSalesReport($this->scheduler->company, $data)), 'user_sales' => $export = (new UserSalesReport($this->scheduler->company, $data)),
'clients' => $export = (new ClientExport($this->scheduler->company, $data)), 'clients' => $export = (new ClientExport($this->scheduler->company, $data)),
'client_contacts' => $export = (new ContactExport($this->scheduler->company, $data)), 'client_contacts' => $export = (new ContactExport($this->scheduler->company, $data)),
'credits' => $export = (new CreditExport($this->scheduler->company, $data)), 'credits' => $export = (new CreditExport($this->scheduler->company, $data)),

View File

@ -52,15 +52,14 @@ class TaxProvider
private mixed $api_credentials; private mixed $api_credentials;
public function __construct(protected Company $company, protected Client $client) public function __construct(public Company $company, public ?Client $client = null)
{ {
} }
public function updateCompanyTaxData(): self public function updateCompanyTaxData(): self
{ {
$this->configureProvider($this->provider); //hard coded for now to one provider, but we'll be able to swap these out later $this->configureProvider($this->provider, $this->company->country()->iso_3166_2); //hard coded for now to one provider, but we'll be able to swap these out later
$company_details = [ $company_details = [
'address1' => $this->company->settings->address1, 'address1' => $this->company->settings->address1,
@ -77,7 +76,7 @@ class TaxProvider
$tax_data = $tax_provider->run(); $tax_data = $tax_provider->run();
$this->company->tax_data = $tax_data; $this->company->origin_tax_data = $tax_data;
$this->company->save(); $this->company->save();
@ -87,7 +86,7 @@ class TaxProvider
public function updateClientTaxData(): self public function updateClientTaxData(): self
{ {
$this->configureProvider($this->provider); //hard coded for now to one provider, but we'll be able to swap these out later $this->configureProvider($this->provider, $this->client->country->iso_3166_2); //hard coded for now to one provider, but we'll be able to swap these out later
$billing_details =[ $billing_details =[
'address1' => $this->client->address1, 'address1' => $this->client->address1,
@ -108,24 +107,24 @@ class TaxProvider
]; ];
$tax_provider = new $this->provider(); $tax_provider = new $this->provider($billing_details);
$tax_provider->setApiCredentials($this->api_credentials); $tax_provider->setApiCredentials($this->api_credentials);
$tax_data = $tax_provider->run(); $tax_data = $tax_provider->run();
$this->client->tax_data = $tax_data;
$this->company->tax_data = $tax_data; $this->client->save();
$this->company->save();
return $this; return $this;
} }
private function configureProvider(?string $provider): self private function configureProvider(?string $provider, string $country_code): self
{ {
match($this->client->country->iso_3166_2){ match($country_code){
'US' => $this->configureZipTax(), 'US' => $this->configureZipTax(),
"AT" => $this->configureEuTax(), "AT" => $this->configureEuTax(),
"BE" => $this->configureEuTax(), "BE" => $this->configureEuTax(),
@ -168,11 +167,11 @@ class TaxProvider
return $this; return $this;
} }
private function noTaxRegionDefined(): self private function noTaxRegionDefined()
{ {
throw new \Exception("No tax region defined for this country"); throw new \Exception("No tax region defined for this country");
return $this; // return $this;
} }
private function configureZipTax(): self private function configureZipTax(): self

View File

@ -27,17 +27,21 @@ class ZipTax implements TaxProviderInterface
public function run() public function run()
{ {
$string_address = implode(" ", $this->address);
$response = $this->callApi(['key' => $this->api_key, 'address' => $this->address]); $response = $this->callApi(['key' => $this->api_key, 'address' => $string_address]);
if($response->successful()) if($response->successful()){
return $response->json();
return $this->parseResponse($response->json());
}
if(isset($this->address['postal_code'])) { if(isset($this->address['postal_code'])) {
$response = $this->callApi(['key' => $this->api_key, 'address' => $this->address['postal_code']]); $response = $this->callApi(['key' => $this->api_key, 'address' => $this->address['postal_code']]);
if($response->successful()) if($response->successful())
return $response->json(); return $this->parseResponse($response->json());
} }
@ -65,4 +69,13 @@ class ZipTax implements TaxProviderInterface
return $response; return $response;
} }
private function parseResponse($response)
{
if(isset($response['results']['0']))
return $response['results']['0'];
throw new \Exception("Error resolving tax (code) = " . $response['rCode']);
}
} }

View File

@ -31,4 +31,9 @@ class TaxService
return $this; return $this;
} }
public function initTaxProvider()
{
}
} }

View File

@ -199,6 +199,8 @@ class CompanyTransformer extends EntityTransformer
'invoice_task_hours' => (bool) $company->invoice_task_hours, 'invoice_task_hours' => (bool) $company->invoice_task_hours,
'calculate_taxes' => (bool) $company->calculate_taxes, 'calculate_taxes' => (bool) $company->calculate_taxes,
'tax_data' => $company->tax_data ?: new \stdClass, 'tax_data' => $company->tax_data ?: new \stdClass,
'has_e_invoice_certificate' => $company->e_invoice_certificate ? true : false,
'has_e_invoice_certificate_passphrase' => $company->e_invoice_certificate_passphrase ? true : false,
]; ];
} }

View File

@ -66,7 +66,12 @@ trait CleanLineItems
$item['tax_id'] = '1'; $item['tax_id'] = '1';
} }
elseif(array_key_exists('tax_id', $item) && $item['tax_id'] == '') { elseif(array_key_exists('tax_id', $item) && $item['tax_id'] == '') {
$item['tax_id'] = '1';
if($item['type_id'] == '2')
$item['tax_id'] = '2';
else
$item['tax_id'] = '1';
} }
} }

View File

@ -219,8 +219,4 @@ return [
'client_id' => env('SHOPIFY_CLIENT_ID', null), 'client_id' => env('SHOPIFY_CLIENT_ID', null),
'client_secret' => env('SHOPIFY_CLIENT_SECRET', null), 'client_secret' => env('SHOPIFY_CLIENT_SECRET', null),
], ],
'tax_api' => [
'provider' => env('TAX_API_PROVIDER', false),
'api_key' => env('TAX_API_KEY', false),
]
]; ];

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('companies', function (Illuminate\Database\Schema\Blueprint $table) {
$table->text('e_invoice_certificate')->nullable();
$table->text('e_invoice_certificate_passphrase')->nullable();
$table->text('origin_tax_data')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
};

View File

@ -20,17 +20,17 @@ $LANG = array(
'additional_info' => 'Informació adicional', 'additional_info' => 'Informació adicional',
'payment_terms' => 'Condicions de pagament', 'payment_terms' => 'Condicions de pagament',
'currency_id' => 'Moneda', 'currency_id' => 'Moneda',
'size_id' => 'Tamany de l\'empresa', 'size_id' => 'Mida de l\'empresa',
'industry_id' => 'Industria', 'industry_id' => 'Sector industrial',
'private_notes' => 'Notes privades', 'private_notes' => 'Notes privades',
'invoice' => 'Factura', 'invoice' => 'Factura',
'client' => 'Client', 'client' => 'Client',
'invoice_date' => 'Data factura', 'invoice_date' => 'Data factura',
'due_date' => 'Data venciment', 'due_date' => 'Data venciment',
'invoice_number' => 'Número de factura', 'invoice_number' => 'Número de factura',
'invoice_number_short' => 'Factura #', 'invoice_number_short' => 'Núm. factura',
'po_number' => 'Apartat de correus', 'po_number' => 'Apartat de correus',
'po_number_short' => 'Apartat de correus #', 'po_number_short' => 'Núm apt correus',
'frequency_id' => 'Quant sovint', 'frequency_id' => 'Quant sovint',
'discount' => 'Descompte', 'discount' => 'Descompte',
'taxes' => 'Impostos', 'taxes' => 'Impostos',
@ -44,7 +44,7 @@ $LANG = array(
'net_subtotal' => 'Net', 'net_subtotal' => 'Net',
'paid_to_date' => 'Pagat', 'paid_to_date' => 'Pagat',
'balance_due' => 'Pendent', 'balance_due' => 'Pendent',
'invoice_design_id' => 'Diseny', 'invoice_design_id' => 'Disseny',
'terms' => 'Condicions', 'terms' => 'Condicions',
'your_invoice' => 'La teva factura', 'your_invoice' => 'La teva factura',
'remove_contact' => 'Esborra contacte', 'remove_contact' => 'Esborra contacte',
@ -54,22 +54,22 @@ $LANG = array(
'enable' => 'Activa', 'enable' => 'Activa',
'learn_more' => 'Aprèn més', 'learn_more' => 'Aprèn més',
'manage_rates' => 'Administrar tarifes', 'manage_rates' => 'Administrar tarifes',
'note_to_client' => 'Nota al client', 'note_to_client' => 'Nota per al client',
'invoice_terms' => 'Condicions factura', 'invoice_terms' => 'Condicions de la factura',
'save_as_default_terms' => 'Guarda com a condicions per defecte', 'save_as_default_terms' => 'Guarda com a condicions per defecte',
'download_pdf' => 'Descarrega PDF', 'download_pdf' => 'Descarrega PDF',
'pay_now' => 'Paga ara', 'pay_now' => 'Paga ara',
'save_invoice' => 'Guarda factura', 'save_invoice' => 'Guarda factura',
'clone_invoice' => 'Clonar a fatura', 'clone_invoice' => 'Clona a fatura',
'archive_invoice' => 'Arxivar factura', 'archive_invoice' => 'Arxiva factura',
'delete_invoice' => 'Suprimex factura', 'delete_invoice' => 'Suprimex factura',
'email_invoice' => 'Enviar factura per correu electrónic', 'email_invoice' => 'Envia factura per correu electrònic',
'enter_payment' => 'Introduir pagament', 'enter_payment' => 'Introdueix pagament',
'tax_rates' => 'Impostos', 'tax_rates' => 'Impostos',
'rate' => 'Preu', 'rate' => 'Preu',
'settings' => 'Paràmetres', 'settings' => 'Paràmetres',
'enable_invoice_tax' => 'Activar especificar <b>impost</b>', 'enable_invoice_tax' => 'Activa especificar <b>impost</b>',
'enable_line_item_tax' => 'Activar especificar <b>impost per línea</b>', 'enable_line_item_tax' => 'Activa especificar <b>impost per línea</b>',
'dashboard' => 'Tauler de control', 'dashboard' => 'Tauler de control',
'dashboard_totals_in_all_currencies_help' => 'Nota: afegiu un :link anomenat ":name" per mostrar els totals utilitzant una moneda base única.', 'dashboard_totals_in_all_currencies_help' => 'Nota: afegiu un :link anomenat ":name" per mostrar els totals utilitzant una moneda base única.',
'clients' => 'Clients', 'clients' => 'Clients',
@ -83,28 +83,33 @@ $LANG = array(
'company_details' => 'Detalls de l\'empresa', 'company_details' => 'Detalls de l\'empresa',
'online_payments' => 'Pagaments en línia', 'online_payments' => 'Pagaments en línia',
'notifications' => 'Notificacions', 'notifications' => 'Notificacions',
'import_export' => 'Importar | Exportar', 'import_export' => 'Importació | Exportació',
'done' => 'Fet', 'done' => 'Fet',
'save' => 'Desa', 'save' => 'Desa',
'create' => 'Crear', 'create' => 'Crea',
'upload' => 'Penjar', 'upload' => 'Penja',
'import' => 'Importar', 'import' => 'Importa',
'download' => 'Baixar', 'download' => 'Baixa',
'cancel' => 'Cancel·lar', 'cancel' => 'Cancel·la',
'close' => 'Tancar', 'close' => 'Tanca',
'provide_email' => 'Si us plau, indica una adreça de correu electrònic vàlida', 'provide_email' => 'Si us plau, indica una adreça de correu electrònic vàlida',
'powered_by' => 'Funciona amb', 'powered_by' => 'Funciona amb',
'no_items' => 'No hi ha conceptes', 'no_items' => 'No hi ha cap element',
'recurring_invoices' => 'Factures recurrents', 'recurring_invoices' => 'Factures recurrents',
'recurring_help' => '<p>Envieu automàticament als clients les mateixes factures setmanalment, bimensuals, mensuals, trimestrals o anuals.</p> 'recurring_help' => '<p>Envieu automàticament als clients les mateixes factures setmanalment, bimensuals, mensuals, trimestrals o anuals.</p>
<p>Utilitzeu: MONTH,: TRIMESTRE o: YEAR per a dates dinàmiques. Les funcions matemàtiques bàsiques també funcionen, per exemple: MES-1 </p> <p>Utilitzeu :MONTH, :QUARTER o :YEAR per a dates dinàmiques. Les funcions matemàtiques bàsiques també funcionen, per exemple: :MONTH-1 </p>
', <p>Exemples de variables dinàmiques de factures:</p>
<ul>
<li>"Quota gimnàs pel mes de :MONTH" >> "Quota gimnàs pel mes de juliol"</li>
<li>"Subscripció anual :YEAR+1" >> "Subscripció anual 2015"</li>
<li>"Pagament consultor del :QUARTER+1" >> "Pagament consultor del Q2"</li>
</ul>',
'recurring_quotes' => 'Pressupostos recurrents', 'recurring_quotes' => 'Pressupostos recurrents',
'in_total_revenue' => 'en ingressos totals', 'in_total_revenue' => 'en ingressos totals',
'billed_client' => 'client facturat', 'billed_client' => 'client facturat',
'billed_clients' => 'clients facturats', 'billed_clients' => 'clients facturats',
'active_client' => 'Client actiu', 'active_client' => 'client actiu',
'active_clients' => 'Clients actius', 'active_clients' => 'clients actius',
'invoices_past_due' => 'Factures vençudes', 'invoices_past_due' => 'Factures vençudes',
'upcoming_invoices' => 'Properes factures', 'upcoming_invoices' => 'Properes factures',
'average_invoice' => 'Mitjana de facturació', 'average_invoice' => 'Mitjana de facturació',
@ -2407,7 +2412,7 @@ $LANG = array(
'currency_vanuatu_vatu' => 'Vanuatu Vatu', 'currency_vanuatu_vatu' => 'Vanuatu Vatu',
'currency_cuban_peso' => 'Cuban Peso', 'currency_cuban_peso' => 'Cuban Peso',
'currency_bz_dollar' => 'BZ Dollar', 'currency_bz_dollar' => 'Dòlar BZ',
'review_app_help' => 'We hope you\'re enjoying using the app.<br/>If you\'d consider :link we\'d greatly appreciate it!', 'review_app_help' => 'We hope you\'re enjoying using the app.<br/>If you\'d consider :link we\'d greatly appreciate it!',
'writing_a_review' => 'escriu una ressenya', 'writing_a_review' => 'escriu una ressenya',
@ -4007,6 +4012,7 @@ $LANG = array(
'notification_invoice_reminder1_sent_subject' => 'Reminder 1 for Invoice :invoice was sent to :client', 'notification_invoice_reminder1_sent_subject' => 'Reminder 1 for Invoice :invoice was sent to :client',
'notification_invoice_reminder2_sent_subject' => 'Reminder 2 for Invoice :invoice was sent to :client', 'notification_invoice_reminder2_sent_subject' => 'Reminder 2 for Invoice :invoice was sent to :client',
'notification_invoice_reminder3_sent_subject' => 'Reminder 3 for Invoice :invoice was sent to :client', 'notification_invoice_reminder3_sent_subject' => 'Reminder 3 for Invoice :invoice was sent to :client',
'notification_invoice_custom_sent_subject' => 'Custom reminder for Invoice :invoice was sent to :client',
'notification_invoice_reminder_endless_sent_subject' => 'Endless reminder for Invoice :invoice was sent to :client', 'notification_invoice_reminder_endless_sent_subject' => 'Endless reminder for Invoice :invoice was sent to :client',
'assigned_user' => 'Assigned User', 'assigned_user' => 'Assigned User',
'setup_steps_notice' => 'To proceed to next step, make sure you test each section.', 'setup_steps_notice' => 'To proceed to next step, make sure you test each section.',
@ -4255,9 +4261,9 @@ $LANG = array(
'klarna' => 'Klarna', 'klarna' => 'Klarna',
'eps' => 'EPS', 'eps' => 'EPS',
'becs' => 'BECS Direct Debit', 'becs' => 'BECS Direct Debit',
'bacs' => 'BACS Direct Debit', 'bacs' => 'Dèbit directe BACS',
'payment_type_BACS' => 'BACS Direct Debit', 'payment_type_BACS' => 'Dèbit directe BACS',
'missing_payment_method' => 'Please add a payment method first, before trying to pay.', 'missing_payment_method' => 'Introduïu un sistema de pagament primer, abans d\'intentar pagar.',
'becs_mandate' => 'By providing your bank account details, you agree to this <a class="underline" href="https://stripe.com/au-becs-dd-service-agreement/legal">Direct Debit Request and the Direct Debit Request service agreement</a>, and authorise Stripe Payments Australia Pty Ltd ACN 160 180 343 Direct Debit User ID number 507156 (“Stripe”) to debit your account through the Bulk Electronic Clearing System (BECS) on behalf of :company (the “Merchant”) for any amounts separately communicated to you by the Merchant. You certify that you are either an account holder or an authorised signatory on the account listed above.', 'becs_mandate' => 'By providing your bank account details, you agree to this <a class="underline" href="https://stripe.com/au-becs-dd-service-agreement/legal">Direct Debit Request and the Direct Debit Request service agreement</a>, and authorise Stripe Payments Australia Pty Ltd ACN 160 180 343 Direct Debit User ID number 507156 (“Stripe”) to debit your account through the Bulk Electronic Clearing System (BECS) on behalf of :company (the “Merchant”) for any amounts separately communicated to you by the Merchant. You certify that you are either an account holder or an authorised signatory on the account listed above.',
'you_need_to_accept_the_terms_before_proceeding' => 'You need to accept the terms before proceeding.', 'you_need_to_accept_the_terms_before_proceeding' => 'You need to accept the terms before proceeding.',
'direct_debit' => 'Direct Debit', 'direct_debit' => 'Direct Debit',
@ -4384,7 +4390,7 @@ $LANG = array(
'imported_customers' => 'Successfully started importing customers', 'imported_customers' => 'Successfully started importing customers',
'login_success' => 'Successful Login', 'login_success' => 'Successful Login',
'login_failure' => 'Failed Login', 'login_failure' => 'Failed Login',
'exported_data' => 'Once the file is ready you\'ll receive an email with a download link', 'exported_data' => 'Quan l\'arxiu estigui llest rebreu un missatge de correu amb l\'enllaç de descàrrega',
'include_deleted_clients' => 'Include Deleted Clients', 'include_deleted_clients' => 'Include Deleted Clients',
'include_deleted_clients_help' => 'Load records belonging to deleted clients', 'include_deleted_clients_help' => 'Load records belonging to deleted clients',
'step_1_sign_in' => 'Step 1: Sign In', 'step_1_sign_in' => 'Step 1: Sign In',
@ -4473,7 +4479,7 @@ $LANG = array(
'activity_123' => ':user deleted recurring expense :recurring_expense', 'activity_123' => ':user deleted recurring expense :recurring_expense',
'activity_124' => ':user restored recurring expense :recurring_expense', 'activity_124' => ':user restored recurring expense :recurring_expense',
'fpx' => "FPX", 'fpx' => "FPX",
'to_view_entity_set_password' => 'To view the :entity you need to set a password.', 'to_view_entity_set_password' => 'Per a veure :entity haureu d\'establir una contrasenya.',
'unsubscribe' => 'Unsubscribe', 'unsubscribe' => 'Unsubscribe',
'unsubscribed' => 'Unsubscribed', 'unsubscribed' => 'Unsubscribed',
'unsubscribed_text' => 'You have been removed from notifications for this document', 'unsubscribed_text' => 'You have been removed from notifications for this document',
@ -4571,7 +4577,7 @@ $LANG = array(
'purchase_order_number' => 'Purchase Order Number', 'purchase_order_number' => 'Purchase Order Number',
'purchase_order_number_short' => 'Purchase Order #', 'purchase_order_number_short' => 'Purchase Order #',
'inventory_notification_subject' => 'Inventory threshold notification for product: :product', 'inventory_notification_subject' => 'Inventory threshold notification for product: :product',
'inventory_notification_body' => 'Threshold of :amount has been reached for product: :product', 'inventory_notification_body' => 'S\'ha arribat al límit de :amount per al producte :product ',
'activity_130' => ':user created purchase order :purchase_order', 'activity_130' => ':user created purchase order :purchase_order',
'activity_131' => ':user updated purchase order :purchase_order', 'activity_131' => ':user updated purchase order :purchase_order',
'activity_132' => ':user archived purchase order :purchase_order', 'activity_132' => ':user archived purchase order :purchase_order',
@ -4603,7 +4609,7 @@ $LANG = array(
'vendor_document_upload' => 'Vendor Document Upload', 'vendor_document_upload' => 'Vendor Document Upload',
'vendor_document_upload_help' => 'Enable vendors to upload documents', 'vendor_document_upload_help' => 'Enable vendors to upload documents',
'are_you_enjoying_the_app' => 'Are you enjoying the app?', 'are_you_enjoying_the_app' => 'Are you enjoying the app?',
'yes_its_great' => 'Yes, it\'s great!', 'yes_its_great' => 'Sí, genial!',
'not_so_much' => 'Not so much', 'not_so_much' => 'Not so much',
'would_you_rate_it' => 'Great to hear! Would you like to rate it?', 'would_you_rate_it' => 'Great to hear! Would you like to rate it?',
'would_you_tell_us_more' => 'Sorry to hear it! Would you like to tell us more?', 'would_you_tell_us_more' => 'Sorry to hear it! Would you like to tell us more?',
@ -4908,75 +4914,75 @@ $LANG = array(
'update_payment' => 'Update Payment', 'update_payment' => 'Update Payment',
'markup' => 'Markup', 'markup' => 'Markup',
'unlock_pro' => 'Unlock Pro', 'unlock_pro' => 'Unlock Pro',
'upgrade_to_paid_plan_to_schedule' => 'Upgrade to a paid plan to create schedules', 'upgrade_to_paid_plan_to_schedule' => 'Actualitzeu a un pla de pagament per a crear calendaris',
'next_run' => 'Next Run', 'next_run' => 'Següent volta',
'all_clients' => 'All Clients', 'all_clients' => 'Tots els clients',
'show_aging_table' => 'Show Aging Table', 'show_aging_table' => 'Veure taula de compliment',
'show_payments_table' => 'Show Payments Table', 'show_payments_table' => 'Veure taula de pagaments',
'email_statement' => 'Email Statement', 'email_statement' => 'Email Statement',
'once' => 'Once', 'once' => 'Una volta',
'schedules' => 'Schedules', 'schedules' => 'Calendaris',
'new_schedule' => 'New Schedule', 'new_schedule' => 'Nou calendari',
'edit_schedule' => 'Edit Schedule', 'edit_schedule' => 'Edita calendari',
'created_schedule' => 'Successfully created schedule', 'created_schedule' => 'Calendari creat correctament',
'updated_schedule' => 'Successfully updated schedule', 'updated_schedule' => 'Calendari editat correctament',
'archived_schedule' => 'Successfully archived schedule', 'archived_schedule' => 'Calendari arxivat correctament',
'deleted_schedule' => 'Successfully deleted schedule', 'deleted_schedule' => 'Calendari esborrat correctament',
'removed_schedule' => 'Successfully removed schedule', 'removed_schedule' => 'Calendari eliminat correctament ',
'restored_schedule' => 'Successfully restored schedule', 'restored_schedule' => 'Calendari restaurat correctament',
'search_schedule' => 'Search Schedule', 'search_schedule' => 'Cerca calendari',
'search_schedules' => 'Search Schedules', 'search_schedules' => 'Cerca calendaris',
'update_product' => 'Update Product', 'update_product' => 'Actualitza producte',
'create_purchase_order' => 'Create Purchase Order', 'create_purchase_order' => 'Crea ordre de compra',
'update_purchase_order' => 'Update Purchase Order', 'update_purchase_order' => 'Actualitza ordre de compra',
'sent_invoice' => 'Sent Invoice', 'sent_invoice' => 'Factura enviada',
'sent_quote' => 'Sent Quote', 'sent_quote' => 'Pressupost enviat',
'sent_credit' => 'Sent Credit', 'sent_credit' => 'Crèdit enviat',
'sent_purchase_order' => 'Sent Purchase Order', 'sent_purchase_order' => 'Ordre de compra enviada',
'image_url' => 'Image URL', 'image_url' => 'URL de la imatge',
'max_quantity' => 'Max Quantity', 'max_quantity' => 'Quantitat màxims',
'test_url' => 'Test URL', 'test_url' => 'URL de prova',
'auto_bill_help_off' => 'Option is not shown', 'auto_bill_help_off' => 'L\'opció no es mostra',
'auto_bill_help_optin' => 'Option is shown but not selected', 'auto_bill_help_optin' => 'L\'opció es mostra però no se selecciona',
'auto_bill_help_optout' => 'Option is shown and selected', 'auto_bill_help_optout' => 'L\'opció es mostra i selecciona',
'auto_bill_help_always' => 'Option is not shown', 'auto_bill_help_always' => 'L\'opció no es mostra',
'view_all' => 'View All', 'view_all' => 'Mostra-ho tot',
'edit_all' => 'Edit All', 'edit_all' => 'Edita-ho tot',
'accept_purchase_order_number' => 'Accept Purchase Order Number', 'accept_purchase_order_number' => 'Accepta el número d\'ordre de compra',
'accept_purchase_order_number_help' => 'Enable clients to provide a PO number when approving a quote', 'accept_purchase_order_number_help' => 'Permet als clients afegir un número d\'ordre de compra quan aprovin un pressupost',
'from_email' => 'From Email', 'from_email' => 'De correu electrònic',
'show_preview' => 'Show Preview', 'show_preview' => 'Mostra previsualització',
'show_paid_stamp' => 'Show Paid Stamp', 'show_paid_stamp' => 'Mostra segell de "Pagat"',
'show_shipping_address' => 'Show Shipping Address', 'show_shipping_address' => 'Mostra adreça d\'enviament',
'no_documents_to_download' => 'There are no documents in the selected records to download', 'no_documents_to_download' => 'No hi ha cap documents a descarregar en cap dels elements selecctionats',
'pixels' => 'Pixels', 'pixels' => 'Píxels',
'logo_size' => 'Logo Size', 'logo_size' => 'Mida del logo',
'failed' => 'Failed', 'failed' => 'Fallat',
'client_contacts' => 'Client Contacts', 'client_contacts' => 'Contactes del client',
'sync_from' => 'Sync From', 'sync_from' => 'Sincronitza de',
'gateway_payment_text' => 'Invoices: :invoices for :amount for client :client', 'gateway_payment_text' => 'Factures: :invoices de :amount per al client :client',
'gateway_payment_text_no_invoice' => 'Payment with no invoice for amount :amount for client :client', 'gateway_payment_text_no_invoice' => 'Pagament sense factura de :amount per al client :client',
'click_to_variables' => 'Client here to see all variables.', 'click_to_variables' => 'Pitgeu aquí per veure totes les variables.',
'ship_to' => 'Ship to', 'ship_to' => 'Envia a',
'stripe_direct_debit_details' => 'Please transfer into the nominated bank account above.', 'stripe_direct_debit_details' => 'Transferiu al compte bancari especificat a dalt, si us plau.',
'branch_name' => 'Branch Name', 'branch_name' => 'Nom de l\'oficina',
'branch_code' => 'Branch Code', 'branch_code' => 'Codi de l\'oficina',
'bank_name' => 'Bank Name', 'bank_name' => 'Nom del banc',
'bank_code' => 'Bank Code', 'bank_code' => 'Codi del banc',
'bic' => 'BIC', 'bic' => 'BIC',
'change_plan_description' => 'Upgrade or downgrade your current plan.', 'change_plan_description' => 'Actualitzeu el vostre pla.',
'add_company_logo' => 'Add Logo', 'add_company_logo' => 'Afegiu logo',
'add_stripe' => 'Add Stripe', 'add_stripe' => 'Afegiu Stripe',
'invalid_coupon' => 'Invalid Coupon', 'invalid_coupon' => 'Cupó invàlid',
'no_assigned_tasks' => 'No billable tasks for this project', 'no_assigned_tasks' => 'No hi ha cap tasca cobrable a aquest projecte',
'authorization_failure' => 'Insufficient permissions to perform this action', 'authorization_failure' => 'Permisos insuficients per a realitzar aquesta acció',
'authorization_sms_failure' => 'Please verify your account to send emails.', 'authorization_sms_failure' => 'Verifiqueu el vostre compte per a poder enviar missatges de correu.',
'white_label_body' => 'Thank you for purchasing a white label license. <br><br> Your license key is: <br><br> :license_key', 'white_label_body' => 'Gràcies per comprar una llicència de marca blanca. <br><br>La vostra clau de llicència és: <br><br>:license_key',
'payment_type_Klarna' => 'Klarna', 'payment_type_Klarna' => 'Klarna',
'payment_type_Interac E Transfer' => 'Interac E Transfer', 'payment_type_Interac E Transfer' => 'Transferència Interac E',
'xinvoice_payable' => 'Payable within :payeddue days net until :paydate', 'xinvoice_payable' => 'Payable within :payeddue days net until :paydate',
'xinvoice_no_buyers_reference' => "No buyer's reference given", 'xinvoice_no_buyers_reference' => "No buyer's reference given",
'xinvoice_online_payment' => 'The invoice needs to be payed online via the provided link', 'xinvoice_online_payment' => 'The invoice needs to be paid online via the provided link',
'pre_payment' => 'Pre Payment', 'pre_payment' => 'Pre Payment',
'number_of_payments' => 'Number of payments', 'number_of_payments' => 'Number of payments',
'number_of_payments_helper' => 'The number of times this payment will be made', 'number_of_payments_helper' => 'The number of times this payment will be made',
@ -4984,11 +4990,6 @@ $LANG = array(
'notification_payment_emailed' => 'Payment :payment was emailed to :client', 'notification_payment_emailed' => 'Payment :payment was emailed to :client',
'notification_payment_emailed_subject' => 'Payment :payment was emailed', 'notification_payment_emailed_subject' => 'Payment :payment was emailed',
'record_not_found' => 'Record not found', 'record_not_found' => 'Record not found',
'product_tax_exempt' => 'Product Tax Exempt',
'product_type_physical' => 'Physical Goods',
'product_type_digital' => 'Digital Goods',
'product_type_service' => 'Services',
'product_type_freight' => 'Shipping',
'minimum_payment_amount' => 'Minimum Payment Amount', 'minimum_payment_amount' => 'Minimum Payment Amount',
'client_initiated_payments' => 'Client Initiated Payments', 'client_initiated_payments' => 'Client Initiated Payments',
'client_initiated_payments_help' => 'Support making a payment in the client portal without an invoice', 'client_initiated_payments_help' => 'Support making a payment in the client portal without an invoice',
@ -5057,6 +5058,34 @@ $LANG = array(
'here' => 'here', 'here' => 'here',
'industry_Restaurant & Catering' => 'Restaurant & Catering', 'industry_Restaurant & Catering' => 'Restaurant & Catering',
'show_credits_table' => 'Show Credits Table', 'show_credits_table' => 'Show Credits Table',
'manual_payment' => 'Payment Manual',
'tax_summary_report' => 'Tax Summary Report',
'tax_category' => 'Tax Category',
'physical_goods' => 'Physical Goods',
'digital_products' => 'Digital Products',
'services' => 'Services',
'shipping' => 'Shipping',
'tax_exempt' => 'Tax Exempt',
'late_fee_added_locked_invoice' => 'Late fee for invoice :invoice added on :date',
'lang_Khmer' => 'Khmer',
'routing_id' => 'Routing ID',
'enable_e_invoice' => 'Enable E-Invoice',
'e_invoice_type' => 'E-Invoice Type',
'reduced_tax' => 'Reduced Tax',
'override_tax' => 'Override Tax',
'zero_rated' => 'Zero Rated',
'reverse_tax' => 'Reverse Tax',
'updated_tax_category' => 'Successfully updated the tax category',
'updated_tax_categories' => 'Successfully updated the tax categories',
'set_tax_category' => 'Set Tax Category',
'payment_manual' => 'Payment Manual',
'expense_payment_type' => 'Expense Payment Type',
'payment_type_Cash App' => 'Cash App',
'rename' => 'Rename',
'renamed_document' => 'Successfully renamed document',
'e_invoice' => 'E-Invoice',
'light_dark_mode' => 'Light/Dark Mode',
'activities' => 'Activities',
); );

View File

@ -5086,9 +5086,10 @@ $LANG = array(
'e_invoice' => 'E-Invoice', 'e_invoice' => 'E-Invoice',
'light_dark_mode' => 'Light/Dark Mode', 'light_dark_mode' => 'Light/Dark Mode',
'activities' => 'Activities', 'activities' => 'Activities',
'recent_transactions' => "Here are your company's most recent transactions:",
); );
return $LANG; return $LANG;
?> ?>

View File

@ -5072,6 +5072,12 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette
'set_tax_category' => 'Définir la catégorie de taxe', 'set_tax_category' => 'Définir la catégorie de taxe',
'payment_manual' => 'Paiement manuel', 'payment_manual' => 'Paiement manuel',
'expense_payment_type' => 'Type de paiement de dépense', 'expense_payment_type' => 'Type de paiement de dépense',
'payment_type_Cash App' => 'Cash App',
'rename' => 'Renommer',
'renamed_document' => 'Le document a été renommé',
'e_invoice' => 'Facture électronique',
'light_dark_mode' => 'Mode clair/sombre',
'activities' => 'Activités',
); );

View File

@ -394,4 +394,4 @@ Route::post('api/v1/yodlee/data_updates', [YodleeController::class, 'dataUpdates
Route::post('api/v1/yodlee/refresh_updates', [YodleeController::class, 'refreshUpdatesWebhook'])->middleware('throttle:100,1'); Route::post('api/v1/yodlee/refresh_updates', [YodleeController::class, 'refreshUpdatesWebhook'])->middleware('throttle:100,1');
Route::post('api/v1/yodlee/balance', [YodleeController::class, 'balanceWebhook'])->middleware('throttle:100,1'); Route::post('api/v1/yodlee/balance', [YodleeController::class, 'balanceWebhook'])->middleware('throttle:100,1');
Route::fallback([BaseController::class, 'notFound']); Route::fallback([BaseController::class, 'notFound'])->middleware('throttle:404');

View File

@ -158,4 +158,4 @@ Route::fallback(function () {
abort(404); abort(404);
}); })->middleware('throttle:404');

View File

@ -143,6 +143,8 @@ class CompanyTest extends TestCase
$company->settings = $settings; $company->settings = $settings;
nlog($company->toArray());
$response = $this->withHeaders([ $response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'), 'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token, 'X-API-TOKEN' => $this->token,

View File

@ -100,7 +100,7 @@ class SchedulerTest extends TestCase
'clients' => [], 'clients' => [],
'report_keys' => [], 'report_keys' => [],
'client_id' => $this->client->hashed_id, 'client_id' => $this->client->hashed_id,
'report_name' => 'product_sales_report', 'report_name' => 'product_sales',
], ],
]; ];
@ -147,7 +147,7 @@ class SchedulerTest extends TestCase
'clients' => [$this->client->hashed_id], 'clients' => [$this->client->hashed_id],
'report_keys' => [], 'report_keys' => [],
'client_id' => null, 'client_id' => null,
'report_name' => 'product_sales_report', 'report_name' => 'product_sales',
], ],
]; ];
@ -193,7 +193,7 @@ class SchedulerTest extends TestCase
'clients' => [], 'clients' => [],
'report_keys' => [], 'report_keys' => [],
'client_id' => null, 'client_id' => null,
'report_name' => 'product_sales_report', 'report_name' => 'product_sales',
], ],
]; ];
@ -234,7 +234,7 @@ class SchedulerTest extends TestCase
'parameters' => [ 'parameters' => [
'date_range' => EmailStatement::LAST_MONTH, 'date_range' => EmailStatement::LAST_MONTH,
'clients' => [], 'clients' => [],
'report_name' => 'product_sales_report', 'report_name' => 'product_sales',
], ],
]; ];

View File

@ -50,7 +50,7 @@ class ChartCurrencyTest extends TestCase
$this->assertDatabaseHas('invoices', ['number' => 'db_record']); $this->assertDatabaseHas('invoices', ['number' => 'db_record']);
$cs = new ChartService($this->company); $cs = new ChartService($this->company, $this->user, true);
// nlog($cs->getRevenueQuery(now()->subDays(20)->format('Y-m-d'), now()->addDays(100)->format('Y-m-d'))); // nlog($cs->getRevenueQuery(now()->subDays(20)->format('Y-m-d'), now()->addDays(100)->format('Y-m-d')));
$data = [ $data = [
@ -86,7 +86,7 @@ class ChartCurrencyTest extends TestCase
'settings' => $settings, 'settings' => $settings,
]); ]);
$cs = new ChartService($this->company); $cs = new ChartService($this->company, $this->user, true);
$this->assertTrue(is_array($cs->getCurrencyCodes())); $this->assertTrue(is_array($cs->getCurrencyCodes()));
@ -131,7 +131,7 @@ class ChartCurrencyTest extends TestCase
], ],
]; ];
$cs = new ChartService($this->company); $cs = new ChartService($this->company, $this->user, true);
// nlog($cs->totals(now()->subYears(10), now())); // nlog($cs->totals(now()->subYears(10), now()));

View File

@ -70,6 +70,8 @@ class EuTaxTest extends TestCase
'user_id' => $this->user->id, 'user_id' => $this->user->id,
'company_id' => $company->id, 'company_id' => $company->id,
'country_id' => 840, 'country_id' => 840,
'state' => 'CA',
'postal_code' => '90210',
'shipping_country_id' => 840, 'shipping_country_id' => 840,
'has_valid_vat_number' => false, 'has_valid_vat_number' => false,
'is_tax_exempt' => false, 'is_tax_exempt' => false,
@ -113,13 +115,314 @@ class EuTaxTest extends TestCase
'taxSales' => 0.07, 'taxSales' => 0.07,
]), ]),
]); ]);
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->save(); $invoice = $invoice->calc()->getInvoice()->service()->markSent()->save();
$this->assertEquals(107, $invoice->amount); $this->assertEquals(107, $invoice->amount);
} }
public function testEuToBrazilTaxCalculations()
{
$settings = CompanySettings::defaults();
$settings->country_id = '276'; // germany
$tax_data = new TaxModel();
$tax_data->seller_subregion = 'DE';
$tax_data->regions->EU->has_sales_above_threshold = false;
$tax_data->regions->EU->tax_all_subregions = true;
$tax_data->regions->AU->tax_all_subregions = true;
$tax_data->regions->AU->has_sales_above_threshold = true;
$company = Company::factory()->create([
'account_id' => $this->account->id,
'settings' => $settings,
'tax_data' => $tax_data,
'calculate_taxes' => true,
]);
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $company->id,
'country_id' => 76,
'shipping_country_id' => 76,
'has_valid_vat_number' => false,
'is_tax_exempt' => false,
// 'tax_data' => new Response([
// 'geoState' => 'CA',
// 'taxSales' => 0.07,
// ]),
]);
$invoice = Invoice::factory()->create([
'company_id' => $company->id,
'client_id' => $client->id,
'status_id' => 1,
'user_id' => $this->user->id,
'uses_inclusive_taxes' => false,
'discount' => 0,
'line_items' => [
[
'product_key' => 'Test',
'notes' => 'Test',
'cost' => 100,
'quantity' => 1,
'tax_name1' => '',
'tax_rate1' => 0,
'tax_name2' => '',
'tax_rate2' => 0,
'tax_name3' => '',
'tax_rate3' => 0,
'type_id' => '1',
'tax_id' => Product::PRODUCT_TYPE_PHYSICAL,
],
],
'tax_rate1' => 0,
'tax_rate2' => 0,
'tax_rate3' => 0,
'tax_name1' => '',
'tax_name2' => '',
'tax_name3' => '',
// 'tax_data' => new Response([
// 'geoState' => 'CA',
// 'taxSales' => 0.07,
// ]),
]);
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->save();
$this->assertEquals(100, $invoice->amount);
}
public function testEuToAuTaxCalculationExemptProduct()
{
$settings = CompanySettings::defaults();
$settings->country_id = '276'; // germany
$tax_data = new TaxModel();
$tax_data->seller_subregion = 'DE';
$tax_data->regions->EU->has_sales_above_threshold = false;
$tax_data->regions->EU->tax_all_subregions = true;
$tax_data->regions->AU->tax_all_subregions = true;
$tax_data->regions->AU->has_sales_above_threshold = true;
$company = Company::factory()->create([
'account_id' => $this->account->id,
'settings' => $settings,
'tax_data' => $tax_data,
'calculate_taxes' => true,
]);
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $company->id,
'country_id' => 36,
'shipping_country_id' => 36,
'has_valid_vat_number' => false,
'is_tax_exempt' => false,
// 'tax_data' => new Response([
// 'geoState' => 'CA',
// 'taxSales' => 0.07,
// ]),
]);
$invoice = Invoice::factory()->create([
'company_id' => $company->id,
'client_id' => $client->id,
'status_id' => 1,
'user_id' => $this->user->id,
'uses_inclusive_taxes' => false,
'discount' => 0,
'line_items' => [
[
'product_key' => 'Test',
'notes' => 'Test',
'cost' => 100,
'quantity' => 1,
'tax_name1' => '',
'tax_rate1' => 0,
'tax_name2' => '',
'tax_rate2' => 0,
'tax_name3' => '',
'tax_rate3' => 0,
'type_id' => '1',
'tax_id' => Product::PRODUCT_TYPE_EXEMPT,
],
],
'tax_rate1' => 0,
'tax_rate2' => 0,
'tax_rate3' => 0,
'tax_name1' => '',
'tax_name2' => '',
'tax_name3' => '',
// 'tax_data' => new Response([
// 'geoState' => 'CA',
// 'taxSales' => 0.07,
// ]),
]);
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->save();
$this->assertEquals(100, $invoice->amount);
}
public function testEuToAuTaxCalculationExemptClient()
{
$settings = CompanySettings::defaults();
$settings->country_id = '276'; // germany
$tax_data = new TaxModel();
$tax_data->seller_subregion = 'DE';
$tax_data->regions->EU->has_sales_above_threshold = false;
$tax_data->regions->EU->tax_all_subregions = true;
$tax_data->regions->AU->tax_all_subregions = true;
$tax_data->regions->AU->has_sales_above_threshold = true;
$company = Company::factory()->create([
'account_id' => $this->account->id,
'settings' => $settings,
'tax_data' => $tax_data,
'calculate_taxes' => true,
]);
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $company->id,
'country_id' => 36,
'shipping_country_id' => 36,
'has_valid_vat_number' => false,
'is_tax_exempt' => true,
// 'tax_data' => new Response([
// 'geoState' => 'CA',
// 'taxSales' => 0.07,
// ]),
]);
$invoice = Invoice::factory()->create([
'company_id' => $company->id,
'client_id' => $client->id,
'status_id' => 1,
'user_id' => $this->user->id,
'uses_inclusive_taxes' => false,
'discount' => 0,
'line_items' => [
[
'product_key' => 'Test',
'notes' => 'Test',
'cost' => 100,
'quantity' => 1,
'tax_name1' => '',
'tax_rate1' => 0,
'tax_name2' => '',
'tax_rate2' => 0,
'tax_name3' => '',
'tax_rate3' => 0,
'type_id' => '1',
'tax_id' => Product::PRODUCT_TYPE_PHYSICAL,
],
],
'tax_rate1' => 0,
'tax_rate2' => 0,
'tax_rate3' => 0,
'tax_name1' => '',
'tax_name2' => '',
'tax_name3' => '',
// 'tax_data' => new Response([
// 'geoState' => 'CA',
// 'taxSales' => 0.07,
// ]),
]);
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->save();
$this->assertEquals(100, $invoice->amount);
}
public function testEuToAuTaxCalculation()
{
$settings = CompanySettings::defaults();
$settings->country_id = '276'; // germany
$tax_data = new TaxModel();
$tax_data->seller_subregion = 'DE';
$tax_data->regions->EU->has_sales_above_threshold = false;
$tax_data->regions->EU->tax_all_subregions = true;
$tax_data->regions->AU->tax_all_subregions = true;
$tax_data->regions->AU->has_sales_above_threshold = true;
$company = Company::factory()->create([
'account_id' => $this->account->id,
'settings' => $settings,
'tax_data' => $tax_data,
'calculate_taxes' => true,
]);
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $company->id,
'country_id' => 36,
'shipping_country_id' => 36,
'has_valid_vat_number' => false,
'is_tax_exempt' => false,
// 'tax_data' => new Response([
// 'geoState' => 'CA',
// 'taxSales' => 0.07,
// ]),
]);
$invoice = Invoice::factory()->create([
'company_id' => $company->id,
'client_id' => $client->id,
'status_id' => 1,
'user_id' => $this->user->id,
'uses_inclusive_taxes' => false,
'discount' => 0,
'line_items' => [
[
'product_key' => 'Test',
'notes' => 'Test',
'cost' => 100,
'quantity' => 1,
'tax_name1' => '',
'tax_rate1' => 0,
'tax_name2' => '',
'tax_rate2' => 0,
'tax_name3' => '',
'tax_rate3' => 0,
'type_id' => '1',
'tax_id' => Product::PRODUCT_TYPE_PHYSICAL,
],
],
'tax_rate1' => 0,
'tax_rate2' => 0,
'tax_rate3' => 0,
'tax_name1' => '',
'tax_name2' => '',
'tax_name3' => '',
// 'tax_data' => new Response([
// 'geoState' => 'CA',
// 'taxSales' => 0.07,
// ]),
]);
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->save();
$this->assertEquals(110, $invoice->amount);
}
public function testInvoiceTaxCalcDetoBeNoVat() public function testInvoiceTaxCalcDetoBeNoVat()
{ {
@ -444,6 +747,8 @@ class EuTaxTest extends TestCase
'company_id' => $company->id, 'company_id' => $company->id,
'country_id' => 840, 'country_id' => 840,
'shipping_country_id' => 840, 'shipping_country_id' => 840,
'state' => 'CA',
'postal_code' => '90210',
'has_valid_vat_number' => false, 'has_valid_vat_number' => false,
]); ]);
@ -453,7 +758,7 @@ class EuTaxTest extends TestCase
'user_id' => $this->user->id, 'user_id' => $this->user->id,
'status_id' => Invoice::STATUS_SENT, 'status_id' => Invoice::STATUS_SENT,
'tax_data' => new Response([ 'tax_data' => new Response([
'geoState' => 'CA', 'geoState' => 'CA',
]), ]),
]); ]);
@ -750,6 +1055,8 @@ class EuTaxTest extends TestCase
'user_id' => $this->user->id, 'user_id' => $this->user->id,
'company_id' => $company->id, 'company_id' => $company->id,
'country_id' => 840, 'country_id' => 840,
'state' => 'CA',
'postal_code' => '90210',
'shipping_country_id' => 840, 'shipping_country_id' => 840,
'has_valid_vat_number' => true, 'has_valid_vat_number' => true,
'is_tax_exempt' => true, 'is_tax_exempt' => true,
@ -761,7 +1068,7 @@ class EuTaxTest extends TestCase
'user_id' => $this->user->id, 'user_id' => $this->user->id,
'status_id' => Invoice::STATUS_SENT, 'status_id' => Invoice::STATUS_SENT,
'tax_data' => new Response([ 'tax_data' => new Response([
'geoState' => 'CA', 'geoState' => 'CA',
]), ]),
]); ]);

View File

@ -13,6 +13,7 @@ namespace Tests\Unit\Tax;
use Tests\TestCase; use Tests\TestCase;
use App\Models\Client; use App\Models\Client;
use App\Models\Company;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Product; use App\Models\Product;
use Tests\MockAccountData; use Tests\MockAccountData;
@ -20,6 +21,7 @@ use App\DataMapper\InvoiceItem;
use App\DataMapper\Tax\TaxData; use App\DataMapper\Tax\TaxData;
use App\Factory\InvoiceFactory; use App\Factory\InvoiceFactory;
use App\DataMapper\Tax\TaxModel; use App\DataMapper\Tax\TaxModel;
use App\DataMapper\CompanySettings;
use App\DataMapper\Tax\ZipTax\Response; use App\DataMapper\Tax\ZipTax\Response;
use Illuminate\Routing\Middleware\ThrottleRequests; use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\DatabaseTransactions;
@ -92,26 +94,32 @@ class SumTaxTest extends TestCase
public function testCalcInvoiceNoTax() public function testCalcInvoiceNoTax()
{ {
$settings = CompanySettings::defaults();
$settings->country_id = '840'; // germany
$tax_data = new TaxModel(); $tax_data = new TaxModel();
$tax_data->seller_subregion = 'CA'; $tax_data->seller_subregion = 'CA';
$tax_data->regions->US->has_sales_above_threshold = true; $tax_data->regions->US->has_sales_above_threshold = true;
$tax_data->regions->US->tax_all_subregions = true; $tax_data->regions->US->tax_all_subregions = true;
$this->company->calculate_taxes = false; $company = Company::factory()->create([
$this->company->tax_data = $tax_data; 'account_id' => $this->account->id,
$this->company->save(); 'settings' => $settings,
'tax_data' => $tax_data,
$tax_data = new TaxData($this->response); 'calculate_taxes' => false,
'origin_tax_data' => new Response($this->resp),
]);
$client = Client::factory()->create([ $client = Client::factory()->create([
'user_id' => $this->user->id, 'user_id' => $this->user->id,
'company_id' => $this->company->id, 'company_id' => $company->id,
'country_id' => 840, 'country_id' => 840,
'tax_data' => $tax_data, 'state' => 'CA',
'postal_code' => '90210',
'tax_data' => new Response($this->resp),
]); ]);
$invoice = InvoiceFactory::create($this->company->id, $this->user->id); $invoice = InvoiceFactory::create($company->id, $this->user->id);
$invoice->client_id = $client->id; $invoice->client_id = $client->id;
$invoice->uses_inclusive_taxes = false; $invoice->uses_inclusive_taxes = false;
@ -143,35 +151,39 @@ class SumTaxTest extends TestCase
public function testCalcInvoiceTax() public function testCalcInvoiceTax()
{ {
$settings = CompanySettings::defaults();
$settings->country_id = '840';
$settings->currency_id = '1';
$tax_data = new TaxModel(); $tax_data = new TaxModel();
$tax_data->seller_subregion = 'CA'; $tax_data->seller_subregion = 'CA';
$tax_data->regions->US->has_sales_above_threshold = true; $tax_data->regions->US->has_sales_above_threshold = true;
$tax_data->regions->US->tax_all_subregions = true; $tax_data->regions->US->tax_all_subregions = true;
$this->company->calculate_taxes = true; $company = Company::factory()->create([
$this->company->tax_data = $tax_data; 'account_id' => $this->account->id,
$this->company->save(); 'settings' => $settings,
'tax_data' => $tax_data,
'calculate_taxes' => true,
'origin_tax_data' => new Response($this->resp),
]);
$tax_data = new TaxData($this->response);
$client = Client::factory()->create([ $client = Client::factory()->create([
'user_id' => $this->user->id, 'user_id' => $this->user->id,
'company_id' => $this->company->id, 'company_id' => $company->id,
'country_id' => 840, 'country_id' => 840,
'tax_data' => $tax_data, 'postal_code' => '90210',
]); 'state' => 'CA',
'tax_data' => new Response($this->resp),
]);
$invoice = InvoiceFactory::create($this->company->id, $this->user->id); $invoice = InvoiceFactory::create($company->id, $this->user->id);
$invoice->client_id = $client->id; $invoice->client_id = $client->id;
$invoice->uses_inclusive_taxes = false; $invoice->uses_inclusive_taxes = false;
$line_items = []; $line_items = [];
$invoice->tax_data = $tax_data;
$line_item = new InvoiceItem; $line_item = new InvoiceItem;
$line_item->quantity = 1; $line_item->quantity = 1;
$line_item->cost = 10; $line_item->cost = 10;
@ -187,7 +199,6 @@ $invoice->tax_data = $tax_data;
$line_items = $invoice->line_items; $line_items = $invoice->line_items;
$this->assertEquals(10.88, $invoice->amount); $this->assertEquals(10.88, $invoice->amount);
$this->assertEquals("CA Sales Tax", $line_items[0]->tax_name1); $this->assertEquals("CA Sales Tax", $line_items[0]->tax_name1);
$this->assertEquals(8.75, $line_items[0]->tax_rate1); $this->assertEquals(8.75, $line_items[0]->tax_rate1);

View File

@ -0,0 +1,75 @@
<?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 App\DataProviders\USStates;
use Tests\TestCase;
use App\Models\Client;
use Tests\MockAccountData;
use App\Services\Tax\Providers\TaxProvider;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Foundation\Testing\DatabaseTransactions;
/**
* @test App\Services\Tax\Providers\EuTax
*/
class TaxConfigTest extends TestCase
{
use MockAccountData;
use DatabaseTransactions;
protected function setUp() :void
{
parent::setUp();
$this->withoutMiddleware(
ThrottleRequests::class
);
$this->withoutExceptionHandling();
$this->makeTestData();
if(!config('services.tax.zip_tax.key'))
$this->markTestSkipped('No API keys to test with.');
}
public TaxProvider $tp;
private function bootApi(Client $client)
{
$this->tp = new TaxProvider($this->company, $client);
}
public function testStateResolution()
{
//infer state from zip
$client = Client::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'address1' => '400 Evelyn Pl',
'city' => 'Beverley Hills',
'state' => '',
'postal_code' => '',
'country_id' => 840,
]);
// $this->assertEquals('CA', USStates::getState('90210'));
$this->bootApi($client);
$this->tp->updateClientTaxData();
}
}

View File

@ -98,6 +98,8 @@ class UsTaxTest extends TestCase
'settings' => $settings, 'settings' => $settings,
'tax_data' => $tax_data, 'tax_data' => $tax_data,
'calculate_taxes' => true, 'calculate_taxes' => true,
'origin_tax_data' => new Response($this->mock_response),
]); ]);
$client = Client::factory()->create([ $client = Client::factory()->create([
@ -147,6 +149,591 @@ class UsTaxTest extends TestCase
return $invoice; return $invoice;
} }
public function testTaxAuNoExemption()
{
$settings = CompanySettings::defaults();
$settings->country_id = '840'; // germany
$tax_data = new TaxModel();
$tax_data->seller_subregion = 'CA';
$tax_data->regions->US->has_sales_above_threshold = true;
$tax_data->regions->US->tax_all_subregions = false;
$tax_data->regions->AU->has_sales_above_threshold = true;
$tax_data->regions->AU->tax_all_subregions = true;
$company = Company::factory()->create([
'account_id' => $this->account->id,
'settings' => $settings,
'tax_data' => $tax_data,
'calculate_taxes' => true,
'origin_tax_data' => new Response($this->mock_response),
]);
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $company->id,
'country_id' => 36,
'postal_code' => '30002',
'shipping_country_id' => 36,
'shipping_postal_code' => '30002',
'shipping_state' => '30002',
'has_valid_vat_number' => false,
'is_tax_exempt' => false,
'state' => 'NSW'
]);
$invoice = Invoice::factory()->create([
'company_id' => $company->id,
'client_id' => $client->id,
'status_id' => 1,
'user_id' => $this->user->id,
'uses_inclusive_taxes' => false,
'discount' => 0,
'line_items' => [
[
'product_key' => 'Test',
'notes' => 'Test',
'cost' => 100,
'quantity' => 1,
'tax_name1' => '',
'tax_rate1' => 0,
'tax_name2' => '',
'tax_rate2' => 0,
'tax_name3' => '',
'tax_rate3' => 0,
'type_id' => '1',
'tax_id' => Product::PRODUCT_TYPE_PHYSICAL,
],
],
'tax_rate1' => 0,
'tax_rate2' => 0,
'tax_rate3' => 0,
'tax_name1' => '',
'tax_name2' => '',
'tax_name3' => '',
'tax_data' => new Response($this->mock_response),
]);
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->save();
$this->assertEquals(110, $invoice->amount);
}
public function testTaxAuClientExemption()
{
$settings = CompanySettings::defaults();
$settings->country_id = '840'; // germany
$tax_data = new TaxModel();
$tax_data->seller_subregion = 'CA';
$tax_data->regions->US->has_sales_above_threshold = true;
$tax_data->regions->US->tax_all_subregions = false;
$tax_data->regions->AU->has_sales_above_threshold = true;
$tax_data->regions->AU->tax_all_subregions = true;
$company = Company::factory()->create([
'account_id' => $this->account->id,
'settings' => $settings,
'tax_data' => $tax_data,
'calculate_taxes' => true,
'origin_tax_data' => new Response($this->mock_response),
]);
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $company->id,
'country_id' => 36,
'postal_code' => '30002',
'shipping_country_id' => 36,
'shipping_postal_code' => '30002',
'shipping_state' => '30002',
'has_valid_vat_number' => false,
'is_tax_exempt' => true,
'state' => 'NSW'
]);
$invoice = Invoice::factory()->create([
'company_id' => $company->id,
'client_id' => $client->id,
'status_id' => 1,
'user_id' => $this->user->id,
'uses_inclusive_taxes' => false,
'discount' => 0,
'line_items' => [
[
'product_key' => 'Test',
'notes' => 'Test',
'cost' => 100,
'quantity' => 1,
'tax_name1' => '',
'tax_rate1' => 0,
'tax_name2' => '',
'tax_rate2' => 0,
'tax_name3' => '',
'tax_rate3' => 0,
'type_id' => '1',
'tax_id' => Product::PRODUCT_TYPE_PHYSICAL,
],
],
'tax_rate1' => 0,
'tax_rate2' => 0,
'tax_rate3' => 0,
'tax_name1' => '',
'tax_name2' => '',
'tax_name3' => '',
'tax_data' => new Response($this->mock_response),
]);
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->save();
$this->assertEquals(100, $invoice->amount);
}
public function testTaxAuProductExemption()
{
$settings = CompanySettings::defaults();
$settings->country_id = '840'; // germany
$tax_data = new TaxModel();
$tax_data->seller_subregion = 'CA';
$tax_data->regions->US->has_sales_above_threshold = true;
$tax_data->regions->US->tax_all_subregions = false;
$tax_data->regions->AU->has_sales_above_threshold = true;
$tax_data->regions->AU->tax_all_subregions = true;
$company = Company::factory()->create([
'account_id' => $this->account->id,
'settings' => $settings,
'tax_data' => $tax_data,
'calculate_taxes' => true,
'origin_tax_data' => new Response($this->mock_response),
]);
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $company->id,
'country_id' => 36,
'postal_code' => '30002',
'shipping_country_id' => 36,
'shipping_postal_code' => '30002',
'shipping_state' => '30002',
'has_valid_vat_number' => false,
'is_tax_exempt' => false,
'state' => 'NSW'
]);
$invoice = Invoice::factory()->create([
'company_id' => $company->id,
'client_id' => $client->id,
'status_id' => 1,
'user_id' => $this->user->id,
'uses_inclusive_taxes' => false,
'discount' => 0,
'line_items' => [
[
'product_key' => 'Test',
'notes' => 'Test',
'cost' => 100,
'quantity' => 1,
'tax_name1' => '',
'tax_rate1' => 0,
'tax_name2' => '',
'tax_rate2' => 0,
'tax_name3' => '',
'tax_rate3' => 0,
'type_id' => '1',
'tax_id' => Product::PRODUCT_TYPE_EXEMPT,
],
],
'tax_rate1' => 0,
'tax_rate2' => 0,
'tax_rate3' => 0,
'tax_name1' => '',
'tax_name2' => '',
'tax_name3' => '',
'tax_data' => new Response($this->mock_response),
]);
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->save();
$this->assertEquals(100, $invoice->amount);
}
public function testTaxAuProductOverride()
{
$settings = CompanySettings::defaults();
$settings->country_id = '840'; // germany
$tax_data = new TaxModel();
$tax_data->seller_subregion = 'CA';
$tax_data->regions->US->has_sales_above_threshold = true;
$tax_data->regions->US->tax_all_subregions = false;
$tax_data->regions->AU->has_sales_above_threshold = true;
$tax_data->regions->AU->tax_all_subregions = true;
$company = Company::factory()->create([
'account_id' => $this->account->id,
'settings' => $settings,
'tax_data' => $tax_data,
'calculate_taxes' => true,
'origin_tax_data' => new Response($this->mock_response),
]);
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $company->id,
'country_id' => 36,
'postal_code' => '30002',
'shipping_country_id' => 36,
'shipping_postal_code' => '30002',
'shipping_state' => '30002',
'has_valid_vat_number' => false,
'is_tax_exempt' => false,
'state' => 'NSW'
]);
$invoice = Invoice::factory()->create([
'company_id' => $company->id,
'client_id' => $client->id,
'status_id' => 1,
'user_id' => $this->user->id,
'uses_inclusive_taxes' => false,
'discount' => 0,
'line_items' => [
[
'product_key' => 'Test',
'notes' => 'Test',
'cost' => 100,
'quantity' => 1,
'tax_name1' => 'OVERRIDE',
'tax_rate1' => 20,
'tax_name2' => '',
'tax_rate2' => 0,
'tax_name3' => '',
'tax_rate3' => 0,
'type_id' => '1',
'tax_id' => Product::PRODUCT_TYPE_OVERRIDE_TAX,
],
],
'tax_rate1' => 0,
'tax_rate2' => 0,
'tax_rate3' => 0,
'tax_name1' => '',
'tax_name2' => '',
'tax_name3' => '',
'tax_data' => new Response($this->mock_response),
]);
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->save();
$this->assertEquals(120, $invoice->amount);
}
public function testInterstateFreightNoTaxWithProductTax()
{
$settings = CompanySettings::defaults();
$settings->country_id = '840'; // germany
$tax_data = new TaxModel();
$tax_data->seller_subregion = 'CA';
$tax_data->regions->US->has_sales_above_threshold = true;
$tax_data->regions->US->tax_all_subregions = true;
$company = Company::factory()->create([
'account_id' => $this->account->id,
'settings' => $settings,
'tax_data' => $tax_data,
'calculate_taxes' => true,
'origin_tax_data' => new Response($this->mock_response),
]);
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $company->id,
'country_id' => 840,
'postal_code' => '30002',
'shipping_country_id' => 840,
'shipping_postal_code' => '30002',
'shipping_state' => '30002',
'has_valid_vat_number' => false,
'is_tax_exempt' => false,
'state' => 'GA'
]);
$invoice = Invoice::factory()->create([
'company_id' => $company->id,
'client_id' => $client->id,
'status_id' => 1,
'user_id' => $this->user->id,
'uses_inclusive_taxes' => false,
'discount' => 0,
'line_items' => [
[
'product_key' => 'Test',
'notes' => 'Test',
'cost' => 100,
'quantity' => 1,
'tax_name1' => '',
'tax_rate1' => 0,
'tax_name2' => '',
'tax_rate2' => 0,
'tax_name3' => '',
'tax_rate3' => 0,
'type_id' => '1',
'tax_id' => Product::PRODUCT_TYPE_SHIPPING,
],
[
'product_key' => 'Test',
'notes' => 'Test',
'cost' => 100,
'quantity' => 1,
'tax_name1' => '',
'tax_rate1' => 0,
'tax_name2' => '',
'tax_rate2' => 0,
'tax_name3' => '',
'tax_rate3' => 0,
'type_id' => '1',
'tax_id' => Product::PRODUCT_TYPE_PHYSICAL,
],
],
'tax_rate1' => 0,
'tax_rate2' => 0,
'tax_rate3' => 0,
'tax_name1' => '',
'tax_name2' => '',
'tax_name3' => '',
'tax_data' => new Response($this->mock_response),
]);
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->save();
$this->assertEquals(208.75, $invoice->amount);
}
public function testInterstateFreightProductNoTax()
{
$settings = CompanySettings::defaults();
$settings->country_id = '840'; // germany
$tax_data = new TaxModel();
$tax_data->seller_subregion = 'CA';
$tax_data->regions->US->has_sales_above_threshold = true;
$tax_data->regions->US->tax_all_subregions = false;
$company = Company::factory()->create([
'account_id' => $this->account->id,
'settings' => $settings,
'tax_data' => $tax_data,
'calculate_taxes' => true,
'origin_tax_data' => new Response($this->mock_response),
]);
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $company->id,
'country_id' => 840,
'postal_code' => '30002',
'shipping_country_id' => 840,
'shipping_postal_code' => '30002',
'shipping_state' => '30002',
'has_valid_vat_number' => false,
'is_tax_exempt' => false,
'state' => 'GA'
]);
$invoice = Invoice::factory()->create([
'company_id' => $company->id,
'client_id' => $client->id,
'status_id' => 1,
'user_id' => $this->user->id,
'uses_inclusive_taxes' => false,
'discount' => 0,
'line_items' => [
[
'product_key' => 'Test',
'notes' => 'Test',
'cost' => 100,
'quantity' => 1,
'tax_name1' => '',
'tax_rate1' => 0,
'tax_name2' => '',
'tax_rate2' => 0,
'tax_name3' => '',
'tax_rate3' => 0,
'type_id' => '1',
'tax_id' => Product::PRODUCT_TYPE_SHIPPING,
],
],
'tax_rate1' => 0,
'tax_rate2' => 0,
'tax_rate3' => 0,
'tax_name1' => '',
'tax_name2' => '',
'tax_name3' => '',
'tax_data' => new Response($this->mock_response),
]);
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->save();
$this->assertEquals(100, $invoice->amount);
}
public function testInterstateServiceProductNoTax()
{
$settings = CompanySettings::defaults();
$settings->country_id = '840'; // germany
$tax_data = new TaxModel();
$tax_data->seller_subregion = 'CA';
$tax_data->regions->US->has_sales_above_threshold = true;
$tax_data->regions->US->tax_all_subregions = false;
$company = Company::factory()->create([
'account_id' => $this->account->id,
'settings' => $settings,
'tax_data' => $tax_data,
'calculate_taxes' => true,
'origin_tax_data' => new Response($this->mock_response),
]);
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $company->id,
'country_id' => 840,
'postal_code' => '30002',
'shipping_country_id' => 840,
'shipping_postal_code' => '30002',
'shipping_state' => '30002',
'has_valid_vat_number' => false,
'is_tax_exempt' => false,
'state' => 'GA'
]);
$invoice = Invoice::factory()->create([
'company_id' => $company->id,
'client_id' => $client->id,
'status_id' => 1,
'user_id' => $this->user->id,
'uses_inclusive_taxes' => false,
'discount' => 0,
'line_items' => [
[
'product_key' => 'Test',
'notes' => 'Test',
'cost' => 100,
'quantity' => 1,
'tax_name1' => '',
'tax_rate1' => 0,
'tax_name2' => '',
'tax_rate2' => 0,
'tax_name3' => '',
'tax_rate3' => 0,
'type_id' => '2',
'tax_id' => Product::PRODUCT_TYPE_SERVICE,
],
],
'tax_rate1' => 0,
'tax_rate2' => 0,
'tax_rate3' => 0,
'tax_name1' => '',
'tax_name2' => '',
'tax_name3' => '',
'tax_data' => new Response($this->mock_response),
]);
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->save();
$this->assertEquals(100, $invoice->amount);
}
public function testInterstateWithNoTax()
{
$settings = CompanySettings::defaults();
$settings->country_id = '840'; // germany
$tax_data = new TaxModel();
$tax_data->seller_subregion = 'CA';
$tax_data->regions->US->has_sales_above_threshold = true;
$tax_data->regions->US->tax_all_subregions = false;
$company = Company::factory()->create([
'account_id' => $this->account->id,
'settings' => $settings,
'tax_data' => $tax_data,
'calculate_taxes' => true,
'origin_tax_data' => new Response($this->mock_response),
]);
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $company->id,
'country_id' => 840,
'postal_code' => '30002',
'shipping_country_id' => 840,
'shipping_postal_code' => '30002',
'shipping_state' => '30002',
'has_valid_vat_number' => false,
'is_tax_exempt' => false,
'state' => 'GA'
]);
$invoice = Invoice::factory()->create([
'company_id' => $company->id,
'client_id' => $client->id,
'status_id' => 1,
'user_id' => $this->user->id,
'uses_inclusive_taxes' => false,
'discount' => 0,
'line_items' => [
[
'product_key' => 'Test',
'notes' => 'Test',
'cost' => 100,
'quantity' => 1,
'tax_name1' => '',
'tax_rate1' => 0,
'tax_name2' => '',
'tax_rate2' => 0,
'tax_name3' => '',
'tax_rate3' => 0,
'type_id' => '1',
'tax_id' => Product::PRODUCT_TYPE_PHYSICAL,
],
],
'tax_rate1' => 0,
'tax_rate2' => 0,
'tax_rate3' => 0,
'tax_name1' => '',
'tax_name2' => '',
'tax_name3' => '',
'tax_data' => new Response($this->mock_response),
]);
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->save();
$this->assertEquals(100, $invoice->amount);
}
public function testSameSubregionAndExemptProduct() public function testSameSubregionAndExemptProduct()
{ {
@ -166,6 +753,7 @@ class UsTaxTest extends TestCase
'settings' => $settings, 'settings' => $settings,
'tax_data' => $tax_data, 'tax_data' => $tax_data,
'calculate_taxes' => true, 'calculate_taxes' => true,
'origin_tax_data' => new Response($this->mock_response),
]); ]);
$client = Client::factory()->create([ $client = Client::factory()->create([
@ -218,6 +806,78 @@ class UsTaxTest extends TestCase
} }
public function testSameSubregionAndExemptClient()
{
$settings = CompanySettings::defaults();
$settings->country_id = '840'; // germany
$tax_data = new TaxModel();
$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->EU->has_sales_above_threshold = true;
$tax_data->regions->EU->tax_all_subregions = true;
$tax_data->regions->EU->subregions->DE->tax_rate = 21;
$company = Company::factory()->create([
'account_id' => $this->account->id,
'settings' => $settings,
'tax_data' => $tax_data,
'calculate_taxes' => true,
'origin_tax_data' => new Response($this->mock_response),
]);
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $company->id,
'country_id' => 840,
'postal_code' => '90210',
'shipping_country_id' => 840,
'shipping_postal_code' => '90210',
'has_valid_vat_number' => false,
'is_tax_exempt' => true,
]);
$invoice = Invoice::factory()->create([
'company_id' => $company->id,
'client_id' => $client->id,
'status_id' => 1,
'user_id' => $this->user->id,
'uses_inclusive_taxes' => false,
'discount' => 0,
'line_items' => [
[
'product_key' => 'Test',
'notes' => 'Test',
'cost' => 100,
'quantity' => 1,
'tax_name1' => '',
'tax_rate1' => 0,
'tax_name2' => '',
'tax_rate2' => 0,
'tax_name3' => '',
'tax_rate3' => 0,
'type_id' => '1',
'tax_id' => Product::PRODUCT_TYPE_PHYSICAL,
],
],
'tax_rate1' => 0,
'tax_rate2' => 0,
'tax_rate3' => 0,
'tax_name1' => '',
'tax_name2' => '',
'tax_name3' => '',
'tax_data' => new Response($this->mock_response),
]);
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->save();
$this->assertEquals(100, $invoice->amount);
}
public function testForeignTaxesEnabledWithExemptProduct() public function testForeignTaxesEnabledWithExemptProduct()
{ {
$settings = CompanySettings::defaults(); $settings = CompanySettings::defaults();
@ -236,6 +896,7 @@ class UsTaxTest extends TestCase
'settings' => $settings, 'settings' => $settings,
'tax_data' => $tax_data, 'tax_data' => $tax_data,
'calculate_taxes' => true, 'calculate_taxes' => true,
'origin_tax_data' => new Response($this->mock_response),
]); ]);
$client = Client::factory()->create([ $client = Client::factory()->create([
@ -305,6 +966,7 @@ class UsTaxTest extends TestCase
'settings' => $settings, 'settings' => $settings,
'tax_data' => $tax_data, 'tax_data' => $tax_data,
'calculate_taxes' => true, 'calculate_taxes' => true,
'origin_tax_data' => new Response($this->mock_response),
]); ]);
$client = Client::factory()->create([ $client = Client::factory()->create([
@ -373,6 +1035,7 @@ class UsTaxTest extends TestCase
'settings' => $settings, 'settings' => $settings,
'tax_data' => $tax_data, 'tax_data' => $tax_data,
'calculate_taxes' => true, 'calculate_taxes' => true,
'origin_tax_data' => new Response($this->mock_response),
]); ]);
$client = Client::factory()->create([ $client = Client::factory()->create([