diff --git a/app/Casts/EncryptedCast.php b/app/Casts/EncryptedCast.php new file mode 100644 index 000000000000..e13c69625d32 --- /dev/null +++ b/app/Casts/EncryptedCast.php @@ -0,0 +1,27 @@ + 1 ? decrypt($value) : null; + } + + public function set($model, string $key, $value, array $attributes) + { + return [$key => ! is_null($value) ? encrypt($value) : null]; + } +} diff --git a/app/DataMapper/Tax/BaseRule.php b/app/DataMapper/Tax/BaseRule.php index 6f664dd8148f..1cf893d5dded 100644 --- a/app/DataMapper/Tax/BaseRule.php +++ b/app/DataMapper/Tax/BaseRule.php @@ -14,9 +14,9 @@ namespace App\DataMapper\Tax; use App\Models\Client; use App\Models\Invoice; use App\Models\Product; -use App\DataMapper\Tax\TaxData; use App\DataProviders\USStates; use App\DataMapper\Tax\ZipTax\Response; +use App\Services\Tax\Providers\TaxProvider; class BaseRule implements RuleInterface { @@ -104,9 +104,6 @@ class BaseRule implements RuleInterface /** EU TAXES */ - /** US TAXES */ - /** US TAXES */ - public string $tax_name1 = ''; public float $tax_rate1 = 0; @@ -130,70 +127,140 @@ class BaseRule implements RuleInterface { return $this; } - + + /** + * Initializes the tax rule for the entity. + * + * @param mixed $invoice + * @return self + */ public function setEntity(mixed $invoice): self { $this->invoice = $invoice; $this->client = $invoice->client; - $this->configTaxData() - ->resolveRegions(); + $this->resolveRegions(); + + if(!$this->isTaxableRegion()) + return $this; + + $this->configTaxData(); $this->tax_data = new Response($this->invoice->tax_data); return $this; } - + + /** + * Configigures the Tax Data for the entity + * + * @return 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)) { - $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("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) 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) { - $tax_data->originDestination = "D"; - $tax_data->geoState = $this->client_subregion; + if($this->seller_region == 'US' && $this->client_region == 'US'){ + + $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; + } - // Refactor to support switching between shipping / billing country / region / subregion + + /** + * Resolve Regions & Subregions + * + * @return self + */ private function resolveRegions(): self { + + $this->client_region = $this->region_codes[$this->client->country->iso_3166_2]; 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, 'AU' => $this->client_subregion = 'AU', default => $this->client_subregion = $this->client->country->iso_3166_2, }; - + return $this; + } private function getUSState(): string { 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); + } catch (\Exception $e) { 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 { - 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_name1 = "{$this->tax_data->geoState} Sales Tax"; @@ -235,18 +302,21 @@ class BaseRule implements RuleInterface { if ($this->client->is_tax_exempt) { - return $this->taxExempt(); + + return $this->taxExempt($item); + } elseif($this->client_region == $this->seller_region && $this->isTaxableRegion()) { - $this->taxByType($item->tax_id); + $this->taxByType($item); return $this; + } elseif($this->isTaxableRegion()) { //other regions outside of US match(intval($item->tax_id)) { - Product::PRODUCT_TYPE_EXEMPT => $this->taxExempt(), - Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced(), - Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override(), + Product::PRODUCT_TYPE_EXEMPT => $this->taxExempt($item), + Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced($item), + Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override($item), default => $this->defaultForeign(), }; @@ -260,42 +330,42 @@ class BaseRule implements RuleInterface return $this; } - public function taxReduced(): self + public function taxReduced($item): self { return $this; } - public function taxExempt(): self + public function taxExempt($item): self { return $this; } - public function taxDigital(): self + public function taxDigital($item): self { return $this; } - public function taxService(): self + public function taxService($item): self { return $this; } - public function taxShipping(): self + public function taxShipping($item): self { return $this; } - public function taxPhysical(): self + public function taxPhysical($item): self { return $this; } - public function default(): self + public function default($item): self { return $this; } - public function override(): self + public function override($item): self { return $this; } @@ -304,4 +374,10 @@ class BaseRule implements RuleInterface { 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))); + } + } diff --git a/app/DataMapper/Tax/DE/Rule.php b/app/DataMapper/Tax/DE/Rule.php index 2118adf02e98..7cb58bc938be 100644 --- a/app/DataMapper/Tax/DE/Rule.php +++ b/app/DataMapper/Tax/DE/Rule.php @@ -30,10 +30,10 @@ class Rule extends BaseRule implements RuleInterface public bool $eu_business_tax_exempt = true; /** @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 */ - public bool $foreign_consumer_tax_exempt = true; + public bool $foreign_consumer_tax_exempt = false; /** @var float $tax_rate */ 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. * - * @param mixed $product_tax_type + * @param mixed $item * @return self */ - public function taxByType($product_tax_type): self + public function taxByType($item): self { if ($this->client->is_tax_exempt) { - return $this->taxExempt(); + return $this->taxExempt($item); } - match(intval($product_tax_type)){ - Product::PRODUCT_TYPE_EXEMPT => $this->taxExempt(), - Product::PRODUCT_TYPE_DIGITAL => $this->taxDigital(), - Product::PRODUCT_TYPE_SERVICE => $this->taxService(), - Product::PRODUCT_TYPE_SHIPPING => $this->taxShipping(), - Product::PRODUCT_TYPE_PHYSICAL => $this->taxPhysical(), - Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced(), - Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override(), - default => $this->default(), + match(intval($item->tax_id)){ + Product::PRODUCT_TYPE_EXEMPT => $this->taxExempt($item), + Product::PRODUCT_TYPE_DIGITAL => $this->taxDigital($item), + Product::PRODUCT_TYPE_SERVICE => $this->taxService($item), + Product::PRODUCT_TYPE_SHIPPING => $this->taxShipping($item), + Product::PRODUCT_TYPE_PHYSICAL => $this->taxPhysical($item), + Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced($item), + Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override($item), + Product::PRODUCT_TYPE_ZERO_RATED => $this->zeroRated($item), + Product::PRODUCT_TYPE_REVERSE_TAX => $this->reverseTax($item), + default => $this->default($item), }; return $this; @@ -85,7 +87,20 @@ class Rule extends BaseRule implements RuleInterface * * @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_name1 = 'ermäßigte MwSt.'; @@ -93,12 +108,26 @@ class Rule extends BaseRule implements RuleInterface 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 * * @return self */ - public function taxExempt(): self + public function taxExempt($item): self { $this->tax_name1 = ''; $this->tax_rate1 = 0; @@ -111,7 +140,7 @@ class Rule extends BaseRule implements RuleInterface * * @return self */ - public function taxDigital(): self + public function taxDigital($item): self { $this->tax_rate1 = $this->tax_rate; @@ -125,7 +154,7 @@ class Rule extends BaseRule implements RuleInterface * * @return self */ - public function taxService(): self + public function taxService($item): self { $this->tax_rate1 = $this->tax_rate; @@ -139,7 +168,7 @@ class Rule extends BaseRule implements RuleInterface * * @return self */ - public function taxShipping(): self + public function taxShipping($item): self { $this->tax_rate1 = $this->tax_rate; @@ -153,7 +182,7 @@ class Rule extends BaseRule implements RuleInterface * * @return self */ - public function taxPhysical(): self + public function taxPhysical($item): self { $this->tax_rate1 = $this->tax_rate; @@ -167,7 +196,7 @@ class Rule extends BaseRule implements RuleInterface * * @return self */ - public function default(): self + public function default($item): self { $this->tax_name1 = ''; @@ -181,7 +210,7 @@ class Rule extends BaseRule implements RuleInterface * * @return self */ - public function override(): self + public function override($item): self { return $this; } @@ -194,38 +223,42 @@ class Rule extends BaseRule implements RuleInterface public function calculateRates(): self { if ($this->client->is_tax_exempt) { - // nlog("tax exempt"); + nlog("tax exempt"); $this->tax_rate = 0; $this->reduced_tax_rate = 0; } elseif($this->client_subregion != $this->client->company->tax_data->seller_subregion && in_array($this->client_subregion, $this->eu_country_codes) && $this->client->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->reduced_tax_rate = 0; } elseif(!in_array($this->client_subregion, $this->eu_country_codes) && ($this->foreign_consumer_tax_exempt || $this->foreign_business_tax_exempt)) //foreign + tax exempt { - // nlog("foreign and tax exempt"); + nlog("foreign and tax exempt"); $this->tax_rate = 0; $this->reduced_tax_rate = 0; } + elseif(!in_array($this->client_subregion, $this->eu_country_codes)) + { + $this->defaultForeign(); + } elseif(in_array($this->client_subregion, $this->eu_country_codes) && !$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) { - // 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->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->country->iso_3166_2}->reduced_tax_rate; } 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->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->reduced_tax_rate; } } 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->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->reduced_tax_rate; } diff --git a/app/DataMapper/Tax/RuleInterface.php b/app/DataMapper/Tax/RuleInterface.php index 6cd933567645..e4ff2c0e2da9 100644 --- a/app/DataMapper/Tax/RuleInterface.php +++ b/app/DataMapper/Tax/RuleInterface.php @@ -15,25 +15,25 @@ interface RuleInterface { public function init(); - public function tax($item = null); + public function tax($item); 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(); } \ No newline at end of file diff --git a/app/DataMapper/Tax/US/Rule.php b/app/DataMapper/Tax/US/Rule.php index 854a689cae2a..4a4527057b9f 100644 --- a/app/DataMapper/Tax/US/Rule.php +++ b/app/DataMapper/Tax/US/Rule.php @@ -41,31 +41,39 @@ class Rule extends BaseRule implements RuleInterface /** * Override tax class, we use this when we do not modify the input taxes * + * @param mixed $item * @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; + } /** * Sets the correct tax rate based on the product type. * - * @param mixed $product_tax_type + * @param mixed $item * @return self */ - public function taxByType($product_tax_type): self + public function taxByType($item): self { - match(intval($product_tax_type)) { - Product::PRODUCT_TYPE_EXEMPT => $this->taxExempt(), - Product::PRODUCT_TYPE_DIGITAL => $this->taxDigital(), - Product::PRODUCT_TYPE_SERVICE => $this->taxService(), - Product::PRODUCT_TYPE_SHIPPING => $this->taxShipping(), - Product::PRODUCT_TYPE_PHYSICAL => $this->taxPhysical(), - Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced(), - Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override(), - default => $this->default(), + match(intval($item->tax_id)) { + Product::PRODUCT_TYPE_EXEMPT => $this->taxExempt($item), + Product::PRODUCT_TYPE_DIGITAL => $this->taxDigital($item), + Product::PRODUCT_TYPE_SERVICE => $this->taxService($item), + Product::PRODUCT_TYPE_SHIPPING => $this->taxShipping($item), + Product::PRODUCT_TYPE_PHYSICAL => $this->taxPhysical($item), + Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced($item), + Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override($item), + Product::PRODUCT_TYPE_ZERO_RATED => $this->zeroRated($item), + default => $this->default($item), }; return $this; @@ -73,10 +81,11 @@ class Rule extends BaseRule implements RuleInterface /** * Sets the tax as exempt (0) + * @param mixed $item * * @return self */ - public function taxExempt(): self + public function taxExempt($item): self { $this->tax_name1 = ''; $this->tax_rate1 = 0; @@ -86,25 +95,27 @@ class Rule extends BaseRule implements RuleInterface /** * Calculates the tax rate for a digital product + * @param mixed $item * * @return self */ - public function taxDigital(): self + public function taxDigital($item): self { - $this->default(); + $this->default($item); return $this; } /** * Calculates the tax rate for a service product + * @param mixed $item * * @return self */ - public function taxService(): self + public function taxService($item): self { - if($this->tax_data?->txbService == 'Y') { - $this->default(); + if(in_array($this->tax_data?->txbService,['Y','L'])) { + $this->default($item); } return $this; @@ -112,13 +123,15 @@ class Rule extends BaseRule implements RuleInterface /** * Calculates the tax rate for a shipping product + * @param mixed $item * * @return self */ - public function taxShipping(): self + public function taxShipping($item): self { + if($this->tax_data?->txbFreight == 'Y') { - $this->default(); + $this->default($item); } return $this; @@ -126,12 +139,15 @@ class Rule extends BaseRule implements RuleInterface /** * Calculates the tax rate for a physical product + * @param mixed $item * * @return self */ - public function taxPhysical(): self + public function taxPhysical($item): self { - $this->default(); + nlog("tax physical"); + nlog($item); + $this->default($item); return $this; } @@ -141,23 +157,13 @@ class Rule extends BaseRule implements RuleInterface * * @return self */ - public function default(): self + public function default($item): self { - + if($this->tax_data?->stateSalesTax == 0) { - if($this->tax_data->originDestination == "O"){ - $tax_region = $this->client->company->tax_data->seller_subregion; - $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; - - } + $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; return $this; } @@ -165,22 +171,43 @@ class Rule extends BaseRule implements RuleInterface $this->tax_rate1 = $this->tax_data->taxSales * 100; $this->tax_name1 = "{$this->tax_data->geoState} Sales Tax"; - 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 * * @return self */ - public function taxReduced(): self + public function taxReduced($item): self { - $this->default(); + $this->default($item); 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 * @@ -190,4 +217,5 @@ class Rule extends BaseRule implements RuleInterface { return $this; } + } diff --git a/app/DataProviders/USStates.php b/app/DataProviders/USStates.php index d973bfe5f4e7..3f07c4ecf427 100644 --- a/app/DataProviders/USStates.php +++ b/app/DataProviders/USStates.php @@ -16,7 +16,7 @@ use Illuminate\Support\Facades\Http; class USStates { - protected static array $states = [ + public static array $states = [ 'AL' => 'Alabama', 'AK' => 'Alaska', 'AZ' => 'Arizona', diff --git a/app/Helpers/Invoice/InvoiceItemSum.php b/app/Helpers/Invoice/InvoiceItemSum.php index dd2d9b2e434e..3be7e878ceb8 100644 --- a/app/Helpers/Invoice/InvoiceItemSum.php +++ b/app/Helpers/Invoice/InvoiceItemSum.php @@ -29,6 +29,36 @@ class InvoiceItemSum use Discounter; 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 = [ // 'AT', // Austria // 'BE', // Belgium @@ -144,15 +174,15 @@ class InvoiceItemSum 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->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 + 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"; $this->rule = new $class(); + + if($this->rule->regionWithNoTaxCoverage($this->client->country->iso_3166_2)) + return $this; + $this->rule ->setEntity($this->invoice) ->init(); diff --git a/app/Http/Controllers/ChartController.php b/app/Http/Controllers/ChartController.php index 8800768fc81d..651ded493fbb 100644 --- a/app/Http/Controllers/ChartController.php +++ b/app/Http/Controllers/ChartController.php @@ -13,8 +13,6 @@ namespace App\Http\Controllers; use App\Http\Requests\Chart\ShowChartRequest; use App\Services\Chart\ChartService; -use Illuminate\Http\Request; -use Illuminate\Http\Response; class ChartController extends BaseController { @@ -67,14 +65,19 @@ class ChartController extends BaseController */ 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); } 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); } diff --git a/app/Http/Controllers/CompanyController.php b/app/Http/Controllers/CompanyController.php index 6b32a6bee2e4..a7900a3f173c 100644 --- a/app/Http/Controllers/CompanyController.php +++ b/app/Http/Controllers/CompanyController.php @@ -41,6 +41,7 @@ use App\Utils\Traits\Uploadable; use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Http\Response; use Illuminate\Support\Facades\Storage; +use Str; use Turbo124\Beacon\Facades\LightLogs; /** @@ -417,6 +418,13 @@ class CompanyController extends BaseController $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); return $this->itemResponse($company); diff --git a/app/Http/Controllers/EmailController.php b/app/Http/Controllers/EmailController.php index 8cd59930b6d2..b7e692d3726d 100644 --- a/app/Http/Controllers/EmailController.php +++ b/app/Http/Controllers/EmailController.php @@ -46,74 +46,6 @@ class EmailController extends BaseController 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) { $entity = $request->input('entity'); diff --git a/app/Http/Requests/Client/UpdateClientRequest.php b/app/Http/Requests/Client/UpdateClientRequest.php index 8b4059c9aac4..24cf207bad0f 100644 --- a/app/Http/Requests/Client/UpdateClientRequest.php +++ b/app/Http/Requests/Client/UpdateClientRequest.php @@ -143,8 +143,8 @@ class UpdateClientRequest extends Request * down to the free plan setting properties which * are saveable * - * @param object $settings - * @return stdClass $settings + * @param \stdClass $settings + * @return \stdClass $settings */ private function filterSaveableSettings($settings) { diff --git a/app/Http/Requests/Company/UpdateCompanyRequest.php b/app/Http/Requests/Company/UpdateCompanyRequest.php index f1c66b0a3a4f..4b72c48ee8bf 100644 --- a/app/Http/Requests/Company/UpdateCompanyRequest.php +++ b/app/Http/Requests/Company/UpdateCompanyRequest.php @@ -53,7 +53,8 @@ class UpdateCompanyRequest extends Request $rules['country_id'] = 'integer|nullable'; $rules['work_email'] = 'email|nullable'; $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'; 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']); } + if(array_key_exists('e_invoice_certificate_passphrase', $input) && empty($input['e_invoice_certificate_passphrase'])) { + unset($input['e_invoice_certificate_passphrase']); + } + $this->replace($input); } diff --git a/app/Http/Requests/RecurringQuote/UpdateRecurringQuoteRequest.php b/app/Http/Requests/RecurringQuote/UpdateRecurringQuoteRequest.php index d7004836bd9c..d74dee599711 100644 --- a/app/Http/Requests/RecurringQuote/UpdateRecurringQuoteRequest.php +++ b/app/Http/Requests/RecurringQuote/UpdateRecurringQuoteRequest.php @@ -81,7 +81,7 @@ class UpdateRecurringQuoteRequest extends Request * off / optin / optout will reset the status of this field to off to allow * 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 */ diff --git a/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php b/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php index f1e5f3b58082..f613c7881a55 100644 --- a/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php +++ b/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php @@ -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.entity' => ['bail', 'sometimes', 'string', 'in:invoice,credit,quote,purchase_order'], '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'], ]; @@ -60,6 +60,7 @@ class StoreSchedulerRequest extends Request $this->merge(['next_run_client' => $input['next_run']]); } - return $input; + $this->replace($input); + } } diff --git a/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php b/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php index d8bf21970622..48c52a3e3e0f 100644 --- a/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php +++ b/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php @@ -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.entity' => ['bail', 'sometimes', 'string', 'in:invoice,credit,quote,purchase_order'], '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'], ]; @@ -57,6 +57,6 @@ class UpdateSchedulerRequest extends Request $this->merge(['next_run_client' => $input['next_run']]); } - return $input; + $this->replace($input); } } diff --git a/app/Import/Transformer/Zoho/ClientTransformer.php b/app/Import/Transformer/Zoho/ClientTransformer.php index b5014960c4d7..6eafc140e6d3 100644 --- a/app/Import/Transformer/Zoho/ClientTransformer.php +++ b/app/Import/Transformer/Zoho/ClientTransformer.php @@ -28,7 +28,7 @@ class ClientTransformer extends BaseTransformer public function transform($data) { 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; @@ -40,7 +40,7 @@ class ClientTransformer extends BaseTransformer $client_id_proxy = array_key_exists('Customer ID', $data) ? 'Customer ID' : 'Primary Contact ID'; - return [ + $data = [ 'company_id' => $this->company->id, 'name' => $this->getString($data, 'Display Name'), 'phone' => $this->getString($data, 'Phone'), @@ -72,5 +72,7 @@ class ClientTransformer extends BaseTransformer ], ], ]; + + return $data; } } diff --git a/app/Import/Transformer/Zoho/InvoiceTransformer.php b/app/Import/Transformer/Zoho/InvoiceTransformer.php index 1ec99eca3520..45b72b39ce88 100644 --- a/app/Import/Transformer/Zoho/InvoiceTransformer.php +++ b/app/Import/Transformer/Zoho/InvoiceTransformer.php @@ -40,7 +40,8 @@ class InvoiceTransformer extends BaseTransformer $transformed = [ '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'), '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, @@ -80,4 +81,77 @@ class InvoiceTransformer extends BaseTransformer 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; + + } } diff --git a/app/Jobs/Client/UpdateTaxData.php b/app/Jobs/Client/UpdateTaxData.php new file mode 100644 index 000000000000..1f640f9a0b34 --- /dev/null +++ b/app/Jobs/Client/UpdateTaxData.php @@ -0,0 +1,78 @@ +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()); + } + + + } +} \ No newline at end of file diff --git a/app/Jobs/Company/CreateCompany.php b/app/Jobs/Company/CreateCompany.php index 1c0328539a60..f288b1b9f52f 100644 --- a/app/Jobs/Company/CreateCompany.php +++ b/app/Jobs/Company/CreateCompany.php @@ -13,12 +13,14 @@ namespace App\Jobs\Company; use App\Utils\Ninja; use App\Models\Company; +use App\Models\Country; use App\Libraries\MultiDB; use App\Utils\Traits\MakesHash; use App\DataMapper\Tax\TaxModel; use App\DataMapper\CompanySettings; use Illuminate\Foundation\Bus\Dispatchable; use App\DataMapper\ClientRegistrationFields; +use App\Factory\TaxRateFactory; class CreateCompany { @@ -53,6 +55,10 @@ class CreateCompany $settings->name = isset($this->request['name']) ? $this->request['name'] : ''; + if($country_id = $this->resolveCountry()){ + $settings->country_id = $country_id; + } + $company = new Company(); $company->account_id = $this->account->id; $company->company_key = $this->createHash(); @@ -74,8 +80,135 @@ class CreateCompany $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; } + + /** + * 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; + + } + } diff --git a/app/Jobs/Entity/EmailEntity.php b/app/Jobs/Entity/EmailEntity.php index 82cbef192937..41473f1b8fd1 100644 --- a/app/Jobs/Entity/EmailEntity.php +++ b/app/Jobs/Entity/EmailEntity.php @@ -150,23 +150,6 @@ class EmailEntity implements ShouldQueue 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 */ private function resolveEmailBuilder() { diff --git a/app/Listeners/Invoice/InvoiceEmailedNotification.php b/app/Listeners/Invoice/InvoiceEmailedNotification.php index bf0859e2da25..1f37e7938dcb 100644 --- a/app/Listeners/Invoice/InvoiceEmailedNotification.php +++ b/app/Listeners/Invoice/InvoiceEmailedNotification.php @@ -63,20 +63,6 @@ class InvoiceEmailedNotification implements ShouldQueue if (($key = array_search('mail', $methods)) !== false) { 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->mailable = new NinjaMailer((new EntitySentObject($event->invitation, 'invoice', $event->template))->build()); $nmo->company = $invoice->company; diff --git a/app/Listeners/Invoice/InvoiceReminderEmailActivity.php b/app/Listeners/Invoice/InvoiceReminderEmailActivity.php index 1cf801afec62..9b41a3b096b6 100644 --- a/app/Listeners/Invoice/InvoiceReminderEmailActivity.php +++ b/app/Listeners/Invoice/InvoiceReminderEmailActivity.php @@ -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; + $reminder = match($event->template){ + 'reminder1' => 63, + 'reminder2' => 64, + 'reminder3' => 65, + 'reminder_endless' => 66, + 'endless_reminder' => 66, + default => 6, + }; + $fields->user_id = $user_id; $fields->invoice_id = $event->invitation->invoice_id; $fields->company_id = $event->invitation->company_id; $fields->client_contact_id = $event->invitation->client_contact_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); } diff --git a/app/Models/Company.php b/app/Models/Company.php index 2b0d05a12cbc..c0bb8f2df705 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -11,18 +11,19 @@ namespace App\Models; -use App\DataMapper\CompanySettings; -use App\Models\Presenters\CompanyPresenter; -use App\Services\Notification\NotificationService; use App\Utils\Ninja; +use App\Casts\EncryptedCast; use App\Utils\Traits\AppSetup; -use App\Utils\Traits\CompanySettingsSaver; use App\Utils\Traits\MakesHash; -use Illuminate\Database\Eloquent\Relations\BelongsTo; -use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Notifications\Notification; +use App\DataMapper\CompanySettings; use Illuminate\Support\Facades\Cache; 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 @@ -339,6 +340,7 @@ class Company extends BaseModel 'notify_vendor_when_paid', 'calculate_taxes', 'tax_data', + 'e_invoice_certificate_passphrase', ]; protected $hidden = [ @@ -357,6 +359,8 @@ class Company extends BaseModel 'deleted_at' => 'timestamp', 'client_registration_fields' => 'array', 'tax_data' => 'object', + 'origin_tax_data' => 'object', + 'e_invoice_certificate_passphrase' => EncryptedCast::class, ]; protected $with = []; @@ -365,7 +369,6 @@ class Company extends BaseModel self::ENTITY_RECURRING_INVOICE => 1, self::ENTITY_CREDIT => 2, self::ENTITY_QUOTE => 4, - // @phpstan-ignore-next-line self::ENTITY_TASK => 8, self::ENTITY_EXPENSE => 16, self::ENTITY_PROJECT => 32, diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 54d37fb968ca..dfdf67c2691a 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -824,7 +824,7 @@ class Invoice extends BaseModel case 'custom1': case 'custom2': 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; default: // code... diff --git a/app/Observers/ClientObserver.php b/app/Observers/ClientObserver.php index ed0a1e7c3310..814ee7f0c421 100644 --- a/app/Observers/ClientObserver.php +++ b/app/Observers/ClientObserver.php @@ -11,9 +11,10 @@ namespace App\Observers; -use App\Jobs\Util\WebhookHandler; use App\Models\Client; use App\Models\Webhook; +use App\Jobs\Util\WebhookHandler; +use App\Jobs\Client\UpdateTaxData; class ClientObserver { @@ -27,6 +28,11 @@ class ClientObserver */ 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) ->where('event_id', Webhook::EVENT_CREATE_CLIENT) ->exists(); @@ -44,6 +50,11 @@ class ClientObserver */ 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; if ($client->getOriginal('deleted_at') && !$client->deleted_at) { @@ -53,8 +64,7 @@ class ClientObserver if ($client->is_deleted) { $event = Webhook::EVENT_DELETE_CLIENT; } - - + $subscriptions = Webhook::where('company_id', $client->company_id) ->where('event_id', $event) ->exists(); diff --git a/app/Observers/CompanyObserver.php b/app/Observers/CompanyObserver.php index 122518568224..93c2aeaf68c7 100644 --- a/app/Observers/CompanyObserver.php +++ b/app/Observers/CompanyObserver.php @@ -40,6 +40,12 @@ class CompanyObserver //fire event to build new custom portal domain \Modules\Admin\Jobs\Domain\CustomDomain::dispatch($company->getOriginal('portal_domain'), $company)->onQueue('domain'); } + + // if($company->wasChanged()) { + // nlog("updated event"); + // nlog($company->getChanges()); + // } + } /** diff --git a/app/PaymentDrivers/Stripe/Charge.php b/app/PaymentDrivers/Stripe/Charge.php index 05833ccecc63..749b52770903 100644 --- a/app/PaymentDrivers/Stripe/Charge.php +++ b/app/PaymentDrivers/Stripe/Charge.php @@ -43,7 +43,7 @@ class Charge * Create a charge against a payment method. * @param ClientGatewayToken $cgt * @param PaymentHash $payment_hash - * @return bool success/failure + * @return mixed success/failure * @throws \Laracasts\Presenter\Exceptions\PresenterException */ public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash) @@ -86,7 +86,7 @@ class Charge $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); } catch (\Exception $e) { diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index bdac90e9c0b2..e5cbc4b895c4 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -777,7 +777,8 @@ class StripePaymentDriver extends BaseDriver ->where('token', $request->data['object']['payment_method']) ->first(); - $clientgateway->delete(); + if($clientgateway) + $clientgateway->delete(); return response()->json([], 200); } elseif ($request->data['object']['status'] == "pending") { diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 44cd2b1b70a0..9bcdc1f4682a 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -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()); + } + }); + } /** diff --git a/app/Services/Chart/ChartQueries.php b/app/Services/Chart/ChartQueries.php index f4d0fb509d39..7f284736c58b 100644 --- a/app/Services/Chart/ChartQueries.php +++ b/app/Services/Chart/ChartQueries.php @@ -23,20 +23,26 @@ trait ChartQueries */ 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, IFNULL(expenses.currency_id, :company_currency) as currency_id FROM expenses WHERE expenses.is_deleted = 0 AND expenses.company_id = :company_id AND (expenses.date BETWEEN :start_date AND :end_date) + {$user_filter} 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) { - 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 total, expenses.date, @@ -45,9 +51,10 @@ trait ChartQueries WHERE (expenses.date BETWEEN :start_date AND :end_date) AND expenses.company_id = :company_id AND expenses.is_deleted = 0 + {$user_filter} GROUP BY expenses.date HAVING currency_id = :currency_id - '), [ + "), [ 'company_currency' => $this->company->settings->currency_id, 'currency_id' => $currency_id, 'company_id' => $this->company->id, @@ -61,15 +68,19 @@ trait ChartQueries */ 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, IFNULL(payments.currency_id, :company_currency) as currency_id FROM payments WHERE payments.is_deleted = 0 + {$user_filter} AND payments.company_id = :company_id AND (payments.date BETWEEN :start_date AND :end_date) GROUP BY currency_id - '), [ + "), [ 'company_currency' => $this->company->settings->currency_id, 'company_id' => $this->company->id, 'start_date' => $start_date, @@ -79,7 +90,10 @@ trait ChartQueries 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 sum(payments.amount - payments.refunded) as total, payments.date, @@ -87,11 +101,12 @@ trait ChartQueries FROM payments WHERE payments.company_id = :company_id AND payments.is_deleted = 0 + {$user_filter} AND payments.status_id IN (4,5,6) AND (payments.date BETWEEN :start_date AND :end_date) GROUP BY payments.date HAVING currency_id = :currency_id - '), [ + "), [ 'company_currency' => $this->company->settings->currency_id, 'currency_id' => $currency_id, 'company_id' => $this->company->id, @@ -105,6 +120,9 @@ trait ChartQueries */ public function getOutstandingQuery($start_date, $end_date) { + + $user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id; + return DB::select(DB::raw(" SELECT sum(invoices.balance) as amount, @@ -116,6 +134,7 @@ trait ChartQueries WHERE invoices.status_id IN (2,3) AND invoices.company_id = :company_id AND clients.is_deleted = 0 + {$user_filter} AND invoices.is_deleted = 0 AND invoices.balance > 0 AND (invoices.date BETWEEN :start_date AND :end_date) @@ -125,6 +144,8 @@ trait ChartQueries public function getRevenueQuery($start_date, $end_date) { + $user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id; + return DB::select(DB::raw(" SELECT sum(invoices.paid_to_date) as paid_to_date, @@ -134,6 +155,7 @@ trait ChartQueries on invoices.client_id = clients.id WHERE invoices.company_id = :company_id AND clients.is_deleted = 0 + {$user_filter} AND invoices.is_deleted = 0 AND invoices.amount > 0 AND invoices.status_id IN (3,4) @@ -144,6 +166,8 @@ trait ChartQueries public function getInvoicesQuery($start_date, $end_date) { + $user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id; + return DB::select(DB::raw(" SELECT sum(invoices.amount) as invoiced_amount, @@ -153,6 +177,7 @@ trait ChartQueries on invoices.client_id = clients.id WHERE invoices.status_id IN (2,3,4) AND invoices.company_id = :company_id + {$user_filter} AND invoices.amount > 0 AND clients.is_deleted = 0 AND invoices.is_deleted = 0 @@ -163,6 +188,8 @@ trait ChartQueries 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(" SELECT sum(invoices.balance) as total, @@ -175,6 +202,7 @@ trait ChartQueries AND invoices.company_id = :company_id AND clients.is_deleted = 0 AND invoices.is_deleted = 0 + {$user_filter} AND (invoices.date BETWEEN :start_date AND :end_date) GROUP BY invoices.date HAVING currency_id = :currency_id @@ -190,6 +218,8 @@ trait ChartQueries 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(" SELECT sum(invoices.amount) as total, @@ -201,6 +231,7 @@ trait ChartQueries WHERE invoices.company_id = :company_id AND clients.is_deleted = 0 AND invoices.is_deleted = 0 + {$user_filter} AND invoices.status_id IN (2,3,4) AND (invoices.date BETWEEN :start_date AND :end_date) GROUP BY invoices.date diff --git a/app/Services/Chart/ChartService.php b/app/Services/Chart/ChartService.php index 52673fe5403b..c9a8be50eda7 100644 --- a/app/Services/Chart/ChartService.php +++ b/app/Services/Chart/ChartService.php @@ -11,6 +11,7 @@ namespace App\Services\Chart; +use App\Models\User; use App\Models\Client; use App\Models\Company; use App\Models\Expense; @@ -20,11 +21,8 @@ class ChartService { use ChartQueries; - public Company $company; - - public function __construct(Company $company) + public function __construct(public Company $company, private User $user, private bool $is_admin) { - $this->company = $company; } /** @@ -37,6 +35,9 @@ class ChartService $currencies = Client::withTrashed() ->where('company_id', $this->company->id) ->where('is_deleted', 0) + ->when(!$this->is_admin, function ($query) { + $query->where('user_id', $this->user->id); + }) ->distinct() ->pluck('settings->currency_id as id'); @@ -47,6 +48,9 @@ class ChartService $expense_currencies = Expense::withTrashed() ->where('company_id', $this->company->id) ->where('is_deleted', 0) + ->when(!$this->is_admin, function ($query) { + $query->where('user_id', $this->user->id); + }) ->distinct() ->pluck('currency_id as id'); diff --git a/app/Services/Invoice/EInvoice/FacturaEInvoice.php b/app/Services/Invoice/EInvoice/FacturaEInvoice.php index 5a2bcce7c35b..79840dad0beb 100644 --- a/app/Services/Invoice/EInvoice/FacturaEInvoice.php +++ b/app/Services/Invoice/EInvoice/FacturaEInvoice.php @@ -18,6 +18,7 @@ use josemmo\Facturae\FacturaeItem; use josemmo\Facturae\FacturaeParty; use Illuminate\Support\Facades\Storage; use josemmo\Facturae\Common\FacturaeSigner; +use josemmo\Facturae\FacturaeCentre; class FacturaEInvoice extends AbstractService { @@ -25,6 +26,24 @@ class FacturaEInvoice extends AbstractService 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_1 Invoice Format 3.2.1 // 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_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) { @@ -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 { if(strlen($this->invoice->po_number) > 1) { @@ -280,6 +345,7 @@ class FacturaEInvoice extends AbstractService "fax" => "", "website" => substr($company->settings->website, 0, 50), "contactPeople" => substr($company->owner()->present()->name(), 0, 40), + 'centres' => $this->setFace(), // "cnoCnae" => "04647", // Clasif. Nacional de Act. Económicas // "ineTownCode" => "280796" // Cód. de municipio del INE ]); diff --git a/app/Services/Scheduler/EmailReport.php b/app/Services/Scheduler/EmailReport.php index 8ec4c020630e..ce76e54fad04 100644 --- a/app/Services/Scheduler/EmailReport.php +++ b/app/Services/Scheduler/EmailReport.php @@ -77,13 +77,13 @@ class EmailReport match($this->scheduler->parameters['report_name']) { - 'product_sales_report' => $export = (new ProductSalesExport($this->scheduler->company, $data)), - 'email_ar_detailed_report' => $export = (new ARDetailReport($this->scheduler->company, $data)), - 'email_ar_summary_report' => $export = (new ARSummaryReport($this->scheduler->company, $data)), - 'email_tax_summary_report' => $export = (new TaxSummaryReport($this->scheduler->company, $data)), - 'email_client_balance_report' => $export = (new ClientBalanceReport($this->scheduler->company, $data)), - 'email_client_sales_report' => $export = (new ClientSalesReport($this->scheduler->company, $data)), - 'email_user_sales_report' => $export = (new UserSalesReport($this->scheduler->company, $data)), + 'product_sales' => $export = (new ProductSalesExport($this->scheduler->company, $data)), + 'ar_detailed' => $export = (new ARDetailReport($this->scheduler->company, $data)), + 'ar_summary' => $export = (new ARSummaryReport($this->scheduler->company, $data)), + 'tax_summary' => $export = (new TaxSummaryReport($this->scheduler->company, $data)), + 'client_balance' => $export = (new ClientBalanceReport($this->scheduler->company, $data)), + 'client_sales' => $export = (new ClientSalesReport($this->scheduler->company, $data)), + 'user_sales' => $export = (new UserSalesReport($this->scheduler->company, $data)), 'clients' => $export = (new ClientExport($this->scheduler->company, $data)), 'client_contacts' => $export = (new ContactExport($this->scheduler->company, $data)), 'credits' => $export = (new CreditExport($this->scheduler->company, $data)), diff --git a/app/Services/Tax/Providers/TaxProvider.php b/app/Services/Tax/Providers/TaxProvider.php index 4d71522cf617..ff92d455ed65 100644 --- a/app/Services/Tax/Providers/TaxProvider.php +++ b/app/Services/Tax/Providers/TaxProvider.php @@ -52,15 +52,14 @@ class TaxProvider 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 { - $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 = [ 'address1' => $this->company->settings->address1, @@ -77,7 +76,7 @@ class TaxProvider $tax_data = $tax_provider->run(); - $this->company->tax_data = $tax_data; + $this->company->origin_tax_data = $tax_data; $this->company->save(); @@ -87,7 +86,7 @@ class TaxProvider 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 =[ '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_data = $tax_provider->run(); + + $this->client->tax_data = $tax_data; - $this->company->tax_data = $tax_data; - - $this->company->save(); + $this->client->save(); 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(), "AT" => $this->configureEuTax(), "BE" => $this->configureEuTax(), @@ -168,11 +167,11 @@ class TaxProvider return $this; } - private function noTaxRegionDefined(): self + private function noTaxRegionDefined() { throw new \Exception("No tax region defined for this country"); - return $this; + // return $this; } private function configureZipTax(): self diff --git a/app/Services/Tax/Providers/ZipTax.php b/app/Services/Tax/Providers/ZipTax.php index b6f90815db11..9f0e40b8b4de 100644 --- a/app/Services/Tax/Providers/ZipTax.php +++ b/app/Services/Tax/Providers/ZipTax.php @@ -27,17 +27,21 @@ class ZipTax implements TaxProviderInterface 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()) - return $response->json(); + if($response->successful()){ + + return $this->parseResponse($response->json()); + + } if(isset($this->address['postal_code'])) { $response = $this->callApi(['key' => $this->api_key, 'address' => $this->address['postal_code']]); if($response->successful()) - return $response->json(); + return $this->parseResponse($response->json()); } @@ -65,4 +69,13 @@ class ZipTax implements TaxProviderInterface return $response; } + + private function parseResponse($response) + { + if(isset($response['results']['0'])) + return $response['results']['0']; + + throw new \Exception("Error resolving tax (code) = " . $response['rCode']); + + } } diff --git a/app/Services/Tax/TaxService.php b/app/Services/Tax/TaxService.php index 7213b023c5f1..89dd1ba7c31c 100644 --- a/app/Services/Tax/TaxService.php +++ b/app/Services/Tax/TaxService.php @@ -31,4 +31,9 @@ class TaxService return $this; } + + public function initTaxProvider() + { + + } } \ No newline at end of file diff --git a/app/Transformers/CompanyTransformer.php b/app/Transformers/CompanyTransformer.php index eff5e27b6be7..4152fa1988ff 100644 --- a/app/Transformers/CompanyTransformer.php +++ b/app/Transformers/CompanyTransformer.php @@ -199,6 +199,8 @@ class CompanyTransformer extends EntityTransformer 'invoice_task_hours' => (bool) $company->invoice_task_hours, 'calculate_taxes' => (bool) $company->calculate_taxes, '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, ]; } diff --git a/app/Utils/Traits/CleanLineItems.php b/app/Utils/Traits/CleanLineItems.php index 921f66c32e1b..19938c0dce0b 100644 --- a/app/Utils/Traits/CleanLineItems.php +++ b/app/Utils/Traits/CleanLineItems.php @@ -66,7 +66,12 @@ trait CleanLineItems $item['tax_id'] = '1'; } 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'; + } } diff --git a/config/ninja.php b/config/ninja.php index 297e6880d877..f61cf2828fd5 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -219,8 +219,4 @@ return [ 'client_id' => env('SHOPIFY_CLIENT_ID', null), 'client_secret' => env('SHOPIFY_CLIENT_SECRET', null), ], - 'tax_api' => [ - 'provider' => env('TAX_API_PROVIDER', false), - 'api_key' => env('TAX_API_KEY', false), - ] ]; diff --git a/database/migrations/2023_05_15_103212_e_invoice_ssl_storage.php b/database/migrations/2023_05_15_103212_e_invoice_ssl_storage.php new file mode 100644 index 000000000000..5f13774326e0 --- /dev/null +++ b/database/migrations/2023_05_15_103212_e_invoice_ssl_storage.php @@ -0,0 +1,34 @@ +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() + { + // + } +}; diff --git a/lang/ca/texts.php b/lang/ca/texts.php index 3918a8576834..7d268f0c3eec 100644 --- a/lang/ca/texts.php +++ b/lang/ca/texts.php @@ -20,17 +20,17 @@ $LANG = array( 'additional_info' => 'Informació adicional', 'payment_terms' => 'Condicions de pagament', 'currency_id' => 'Moneda', - 'size_id' => 'Tamany de l\'empresa', - 'industry_id' => 'Industria', + 'size_id' => 'Mida de l\'empresa', + 'industry_id' => 'Sector industrial', 'private_notes' => 'Notes privades', 'invoice' => 'Factura', 'client' => 'Client', 'invoice_date' => 'Data factura', 'due_date' => 'Data venciment', 'invoice_number' => 'Número de factura', - 'invoice_number_short' => 'Factura #', + 'invoice_number_short' => 'Núm. factura', 'po_number' => 'Apartat de correus', - 'po_number_short' => 'Apartat de correus #', + 'po_number_short' => 'Núm apt correus', 'frequency_id' => 'Quant sovint', 'discount' => 'Descompte', 'taxes' => 'Impostos', @@ -44,7 +44,7 @@ $LANG = array( 'net_subtotal' => 'Net', 'paid_to_date' => 'Pagat', 'balance_due' => 'Pendent', - 'invoice_design_id' => 'Diseny', + 'invoice_design_id' => 'Disseny', 'terms' => 'Condicions', 'your_invoice' => 'La teva factura', 'remove_contact' => 'Esborra contacte', @@ -54,22 +54,22 @@ $LANG = array( 'enable' => 'Activa', 'learn_more' => 'Aprèn més', 'manage_rates' => 'Administrar tarifes', - 'note_to_client' => 'Nota al client', - 'invoice_terms' => 'Condicions factura', + 'note_to_client' => 'Nota per al client', + 'invoice_terms' => 'Condicions de la factura', 'save_as_default_terms' => 'Guarda com a condicions per defecte', 'download_pdf' => 'Descarrega PDF', 'pay_now' => 'Paga ara', 'save_invoice' => 'Guarda factura', - 'clone_invoice' => 'Clonar a fatura', - 'archive_invoice' => 'Arxivar factura', + 'clone_invoice' => 'Clona a fatura', + 'archive_invoice' => 'Arxiva factura', 'delete_invoice' => 'Suprimex factura', - 'email_invoice' => 'Enviar factura per correu electrónic', - 'enter_payment' => 'Introduir pagament', + 'email_invoice' => 'Envia factura per correu electrònic', + 'enter_payment' => 'Introdueix pagament', 'tax_rates' => 'Impostos', 'rate' => 'Preu', 'settings' => 'Paràmetres', - 'enable_invoice_tax' => 'Activar especificar impost', - 'enable_line_item_tax' => 'Activar especificar impost per línea', + 'enable_invoice_tax' => 'Activa especificar impost', + 'enable_line_item_tax' => 'Activa especificar impost per línea', '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.', 'clients' => 'Clients', @@ -83,28 +83,33 @@ $LANG = array( 'company_details' => 'Detalls de l\'empresa', 'online_payments' => 'Pagaments en línia', 'notifications' => 'Notificacions', - 'import_export' => 'Importar | Exportar', + 'import_export' => 'Importació | Exportació', 'done' => 'Fet', 'save' => 'Desa', - 'create' => 'Crear', - 'upload' => 'Penjar', - 'import' => 'Importar', - 'download' => 'Baixar', - 'cancel' => 'Cancel·lar', - 'close' => 'Tancar', + 'create' => 'Crea', + 'upload' => 'Penja', + 'import' => 'Importa', + 'download' => 'Baixa', + 'cancel' => 'Cancel·la', + 'close' => 'Tanca', 'provide_email' => 'Si us plau, indica una adreça de correu electrònic vàlida', 'powered_by' => 'Funciona amb', - 'no_items' => 'No hi ha conceptes', + 'no_items' => 'No hi ha cap element', 'recurring_invoices' => 'Factures recurrents', 'recurring_help' => '

Envieu automàticament als clients les mateixes factures setmanalment, bimensuals, mensuals, trimestrals o anuals.

-

Utilitzeu: MONTH,: TRIMESTRE o: YEAR per a dates dinàmiques. Les funcions matemàtiques bàsiques també funcionen, per exemple: MES-1

-', +

Utilitzeu :MONTH, :QUARTER o :YEAR per a dates dinàmiques. Les funcions matemàtiques bàsiques també funcionen, per exemple: :MONTH-1

+

Exemples de variables dinàmiques de factures:

+', 'recurring_quotes' => 'Pressupostos recurrents', 'in_total_revenue' => 'en ingressos totals', 'billed_client' => 'client facturat', 'billed_clients' => 'clients facturats', - 'active_client' => 'Client actiu', - 'active_clients' => 'Clients actius', + 'active_client' => 'client actiu', + 'active_clients' => 'clients actius', 'invoices_past_due' => 'Factures vençudes', 'upcoming_invoices' => 'Properes factures', 'average_invoice' => 'Mitjana de facturació', @@ -2407,7 +2412,7 @@ $LANG = array( 'currency_vanuatu_vatu' => 'Vanuatu Vatu', '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.
If you\'d consider :link we\'d greatly appreciate it!', '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_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_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', 'assigned_user' => 'Assigned User', 'setup_steps_notice' => 'To proceed to next step, make sure you test each section.', @@ -4255,9 +4261,9 @@ $LANG = array( 'klarna' => 'Klarna', 'eps' => 'EPS', 'becs' => 'BECS Direct Debit', - 'bacs' => 'BACS Direct Debit', - 'payment_type_BACS' => 'BACS Direct Debit', - 'missing_payment_method' => 'Please add a payment method first, before trying to pay.', + 'bacs' => 'Dèbit directe BACS', + 'payment_type_BACS' => 'Dèbit directe BACS', + '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 Direct Debit Request and the Direct Debit Request service agreement, 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.', 'direct_debit' => 'Direct Debit', @@ -4384,7 +4390,7 @@ $LANG = array( 'imported_customers' => 'Successfully started importing customers', 'login_success' => 'Successful 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_help' => 'Load records belonging to deleted clients', 'step_1_sign_in' => 'Step 1: Sign In', @@ -4473,7 +4479,7 @@ $LANG = array( 'activity_123' => ':user deleted recurring expense :recurring_expense', 'activity_124' => ':user restored recurring expense :recurring_expense', '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', 'unsubscribed' => 'Unsubscribed', '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_short' => 'Purchase Order #', '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_131' => ':user updated 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_help' => 'Enable vendors to upload documents', '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', '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?', @@ -4908,75 +4914,75 @@ $LANG = array( 'update_payment' => 'Update Payment', 'markup' => 'Markup', 'unlock_pro' => 'Unlock Pro', - 'upgrade_to_paid_plan_to_schedule' => 'Upgrade to a paid plan to create schedules', - 'next_run' => 'Next Run', - 'all_clients' => 'All Clients', - 'show_aging_table' => 'Show Aging Table', - 'show_payments_table' => 'Show Payments Table', + 'upgrade_to_paid_plan_to_schedule' => 'Actualitzeu a un pla de pagament per a crear calendaris', + 'next_run' => 'Següent volta', + 'all_clients' => 'Tots els clients', + 'show_aging_table' => 'Veure taula de compliment', + 'show_payments_table' => 'Veure taula de pagaments', 'email_statement' => 'Email Statement', - 'once' => 'Once', - 'schedules' => 'Schedules', - 'new_schedule' => 'New Schedule', - 'edit_schedule' => 'Edit Schedule', - 'created_schedule' => 'Successfully created schedule', - 'updated_schedule' => 'Successfully updated schedule', - 'archived_schedule' => 'Successfully archived schedule', - 'deleted_schedule' => 'Successfully deleted schedule', - 'removed_schedule' => 'Successfully removed schedule', - 'restored_schedule' => 'Successfully restored schedule', - 'search_schedule' => 'Search Schedule', - 'search_schedules' => 'Search Schedules', - 'update_product' => 'Update Product', - 'create_purchase_order' => 'Create Purchase Order', - 'update_purchase_order' => 'Update Purchase Order', - 'sent_invoice' => 'Sent Invoice', - 'sent_quote' => 'Sent Quote', - 'sent_credit' => 'Sent Credit', - 'sent_purchase_order' => 'Sent Purchase Order', - 'image_url' => 'Image URL', - 'max_quantity' => 'Max Quantity', - 'test_url' => 'Test URL', - 'auto_bill_help_off' => 'Option is not shown', - 'auto_bill_help_optin' => 'Option is shown but not selected', - 'auto_bill_help_optout' => 'Option is shown and selected', - 'auto_bill_help_always' => 'Option is not shown', - 'view_all' => 'View All', - 'edit_all' => 'Edit All', - 'accept_purchase_order_number' => 'Accept Purchase Order Number', - 'accept_purchase_order_number_help' => 'Enable clients to provide a PO number when approving a quote', - 'from_email' => 'From Email', - 'show_preview' => 'Show Preview', - 'show_paid_stamp' => 'Show Paid Stamp', - 'show_shipping_address' => 'Show Shipping Address', - 'no_documents_to_download' => 'There are no documents in the selected records to download', - 'pixels' => 'Pixels', - 'logo_size' => 'Logo Size', - 'failed' => 'Failed', - 'client_contacts' => 'Client Contacts', - 'sync_from' => 'Sync From', - 'gateway_payment_text' => 'Invoices: :invoices for :amount for client :client', - 'gateway_payment_text_no_invoice' => 'Payment with no invoice for amount :amount for client :client', - 'click_to_variables' => 'Client here to see all variables.', - 'ship_to' => 'Ship to', - 'stripe_direct_debit_details' => 'Please transfer into the nominated bank account above.', - 'branch_name' => 'Branch Name', - 'branch_code' => 'Branch Code', - 'bank_name' => 'Bank Name', - 'bank_code' => 'Bank Code', + 'once' => 'Una volta', + 'schedules' => 'Calendaris', + 'new_schedule' => 'Nou calendari', + 'edit_schedule' => 'Edita calendari', + 'created_schedule' => 'Calendari creat correctament', + 'updated_schedule' => 'Calendari editat correctament', + 'archived_schedule' => 'Calendari arxivat correctament', + 'deleted_schedule' => 'Calendari esborrat correctament', + 'removed_schedule' => 'Calendari eliminat correctament ', + 'restored_schedule' => 'Calendari restaurat correctament', + 'search_schedule' => 'Cerca calendari', + 'search_schedules' => 'Cerca calendaris', + 'update_product' => 'Actualitza producte', + 'create_purchase_order' => 'Crea ordre de compra', + 'update_purchase_order' => 'Actualitza ordre de compra', + 'sent_invoice' => 'Factura enviada', + 'sent_quote' => 'Pressupost enviat', + 'sent_credit' => 'Crèdit enviat', + 'sent_purchase_order' => 'Ordre de compra enviada', + 'image_url' => 'URL de la imatge', + 'max_quantity' => 'Quantitat màxims', + 'test_url' => 'URL de prova', + 'auto_bill_help_off' => 'L\'opció no es mostra', + 'auto_bill_help_optin' => 'L\'opció es mostra però no se selecciona', + 'auto_bill_help_optout' => 'L\'opció es mostra i selecciona', + 'auto_bill_help_always' => 'L\'opció no es mostra', + 'view_all' => 'Mostra-ho tot', + 'edit_all' => 'Edita-ho tot', + 'accept_purchase_order_number' => 'Accepta el número d\'ordre de compra', + 'accept_purchase_order_number_help' => 'Permet als clients afegir un número d\'ordre de compra quan aprovin un pressupost', + 'from_email' => 'De correu electrònic', + 'show_preview' => 'Mostra previsualització', + 'show_paid_stamp' => 'Mostra segell de "Pagat"', + 'show_shipping_address' => 'Mostra adreça d\'enviament', + 'no_documents_to_download' => 'No hi ha cap documents a descarregar en cap dels elements selecctionats', + 'pixels' => 'Píxels', + 'logo_size' => 'Mida del logo', + 'failed' => 'Fallat', + 'client_contacts' => 'Contactes del client', + 'sync_from' => 'Sincronitza de', + 'gateway_payment_text' => 'Factures: :invoices de :amount per al client :client', + 'gateway_payment_text_no_invoice' => 'Pagament sense factura de :amount per al client :client', + 'click_to_variables' => 'Pitgeu aquí per veure totes les variables.', + 'ship_to' => 'Envia a', + 'stripe_direct_debit_details' => 'Transferiu al compte bancari especificat a dalt, si us plau.', + 'branch_name' => 'Nom de l\'oficina', + 'branch_code' => 'Codi de l\'oficina', + 'bank_name' => 'Nom del banc', + 'bank_code' => 'Codi del banc', 'bic' => 'BIC', - 'change_plan_description' => 'Upgrade or downgrade your current plan.', - 'add_company_logo' => 'Add Logo', - 'add_stripe' => 'Add Stripe', - 'invalid_coupon' => 'Invalid Coupon', - 'no_assigned_tasks' => 'No billable tasks for this project', - 'authorization_failure' => 'Insufficient permissions to perform this action', - 'authorization_sms_failure' => 'Please verify your account to send emails.', - 'white_label_body' => 'Thank you for purchasing a white label license.

Your license key is:

:license_key', + 'change_plan_description' => 'Actualitzeu el vostre pla.', + 'add_company_logo' => 'Afegiu logo', + 'add_stripe' => 'Afegiu Stripe', + 'invalid_coupon' => 'Cupó invàlid', + 'no_assigned_tasks' => 'No hi ha cap tasca cobrable a aquest projecte', + 'authorization_failure' => 'Permisos insuficients per a realitzar aquesta acció', + 'authorization_sms_failure' => 'Verifiqueu el vostre compte per a poder enviar missatges de correu.', + 'white_label_body' => 'Gràcies per comprar una llicència de marca blanca.

La vostra clau de llicència és:

:license_key', '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_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', 'number_of_payments' => 'Number of payments', '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_subject' => 'Payment :payment was emailed', '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', 'client_initiated_payments' => 'Client Initiated Payments', 'client_initiated_payments_help' => 'Support making a payment in the client portal without an invoice', @@ -5057,6 +5058,34 @@ $LANG = array( 'here' => 'here', 'industry_Restaurant & Catering' => 'Restaurant & Catering', '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', ); diff --git a/lang/en/texts.php b/lang/en/texts.php index 34915a268f85..009e9dee94e6 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5086,9 +5086,10 @@ $LANG = array( 'e_invoice' => 'E-Invoice', 'light_dark_mode' => 'Light/Dark Mode', 'activities' => 'Activities', + 'recent_transactions' => "Here are your company's most recent transactions:", ); return $LANG; -?> +?> \ No newline at end of file diff --git a/lang/fr_CA/texts.php b/lang/fr_CA/texts.php index 4af6cff18301..380a7c18a10c 100644 --- a/lang/fr_CA/texts.php +++ b/lang/fr_CA/texts.php @@ -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', 'payment_manual' => 'Paiement manuel', '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', ); diff --git a/routes/api.php b/routes/api.php index 350e547333e5..0806d44c6ac5 100644 --- a/routes/api.php +++ b/routes/api.php @@ -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/balance', [YodleeController::class, 'balanceWebhook'])->middleware('throttle:100,1'); -Route::fallback([BaseController::class, 'notFound']); \ No newline at end of file +Route::fallback([BaseController::class, 'notFound'])->middleware('throttle:404'); \ No newline at end of file diff --git a/routes/client.php b/routes/client.php index d8fd795cc9b7..ecb1dd11226f 100644 --- a/routes/client.php +++ b/routes/client.php @@ -158,4 +158,4 @@ Route::fallback(function () { abort(404); -}); +})->middleware('throttle:404'); diff --git a/tests/Feature/CompanyTest.php b/tests/Feature/CompanyTest.php index a8eb0c8e894c..af02f493ea4b 100644 --- a/tests/Feature/CompanyTest.php +++ b/tests/Feature/CompanyTest.php @@ -143,6 +143,8 @@ class CompanyTest extends TestCase $company->settings = $settings; + nlog($company->toArray()); + $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, diff --git a/tests/Feature/Scheduler/SchedulerTest.php b/tests/Feature/Scheduler/SchedulerTest.php index 579186e7c597..c4f58e235ff5 100644 --- a/tests/Feature/Scheduler/SchedulerTest.php +++ b/tests/Feature/Scheduler/SchedulerTest.php @@ -100,7 +100,7 @@ class SchedulerTest extends TestCase 'clients' => [], 'report_keys' => [], '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], 'report_keys' => [], 'client_id' => null, - 'report_name' => 'product_sales_report', + 'report_name' => 'product_sales', ], ]; @@ -193,7 +193,7 @@ class SchedulerTest extends TestCase 'clients' => [], 'report_keys' => [], 'client_id' => null, - 'report_name' => 'product_sales_report', + 'report_name' => 'product_sales', ], ]; @@ -234,7 +234,7 @@ class SchedulerTest extends TestCase 'parameters' => [ 'date_range' => EmailStatement::LAST_MONTH, 'clients' => [], - 'report_name' => 'product_sales_report', + 'report_name' => 'product_sales', ], ]; diff --git a/tests/Unit/Chart/ChartCurrencyTest.php b/tests/Unit/Chart/ChartCurrencyTest.php index 888d5c35f0a1..ed9558d6d79b 100644 --- a/tests/Unit/Chart/ChartCurrencyTest.php +++ b/tests/Unit/Chart/ChartCurrencyTest.php @@ -50,7 +50,7 @@ class ChartCurrencyTest extends TestCase $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'))); $data = [ @@ -86,7 +86,7 @@ class ChartCurrencyTest extends TestCase 'settings' => $settings, ]); - $cs = new ChartService($this->company); + $cs = new ChartService($this->company, $this->user, true); $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())); diff --git a/tests/Unit/Tax/EuTaxTest.php b/tests/Unit/Tax/EuTaxTest.php index 537cfaa5fd19..b497ddee4d2d 100644 --- a/tests/Unit/Tax/EuTaxTest.php +++ b/tests/Unit/Tax/EuTaxTest.php @@ -70,6 +70,8 @@ class EuTaxTest extends TestCase 'user_id' => $this->user->id, 'company_id' => $company->id, 'country_id' => 840, + 'state' => 'CA', + 'postal_code' => '90210', 'shipping_country_id' => 840, 'has_valid_vat_number' => false, 'is_tax_exempt' => false, @@ -113,13 +115,314 @@ class EuTaxTest extends TestCase 'taxSales' => 0.07, ]), ]); - + $invoice = $invoice->calc()->getInvoice()->service()->markSent()->save(); $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() { @@ -444,6 +747,8 @@ class EuTaxTest extends TestCase 'company_id' => $company->id, 'country_id' => 840, 'shipping_country_id' => 840, + 'state' => 'CA', + 'postal_code' => '90210', 'has_valid_vat_number' => false, ]); @@ -453,7 +758,7 @@ class EuTaxTest extends TestCase 'user_id' => $this->user->id, 'status_id' => Invoice::STATUS_SENT, 'tax_data' => new Response([ - 'geoState' => 'CA', + 'geoState' => 'CA', ]), ]); @@ -750,6 +1055,8 @@ class EuTaxTest extends TestCase 'user_id' => $this->user->id, 'company_id' => $company->id, 'country_id' => 840, + 'state' => 'CA', + 'postal_code' => '90210', 'shipping_country_id' => 840, 'has_valid_vat_number' => true, 'is_tax_exempt' => true, @@ -761,7 +1068,7 @@ class EuTaxTest extends TestCase 'user_id' => $this->user->id, 'status_id' => Invoice::STATUS_SENT, 'tax_data' => new Response([ - 'geoState' => 'CA', + 'geoState' => 'CA', ]), ]); diff --git a/tests/Unit/Tax/SumTaxTest.php b/tests/Unit/Tax/SumTaxTest.php index ec3b3047237f..c57add9afac4 100644 --- a/tests/Unit/Tax/SumTaxTest.php +++ b/tests/Unit/Tax/SumTaxTest.php @@ -13,6 +13,7 @@ namespace Tests\Unit\Tax; use Tests\TestCase; use App\Models\Client; +use App\Models\Company; use App\Models\Invoice; use App\Models\Product; use Tests\MockAccountData; @@ -20,6 +21,7 @@ use App\DataMapper\InvoiceItem; use App\DataMapper\Tax\TaxData; use App\Factory\InvoiceFactory; use App\DataMapper\Tax\TaxModel; +use App\DataMapper\CompanySettings; use App\DataMapper\Tax\ZipTax\Response; use Illuminate\Routing\Middleware\ThrottleRequests; use Illuminate\Foundation\Testing\DatabaseTransactions; @@ -92,26 +94,32 @@ class SumTaxTest extends TestCase public function testCalcInvoiceNoTax() { + $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; - $this->company->calculate_taxes = false; - $this->company->tax_data = $tax_data; - $this->company->save(); - - $tax_data = new TaxData($this->response); + $company = Company::factory()->create([ + 'account_id' => $this->account->id, + 'settings' => $settings, + 'tax_data' => $tax_data, + 'calculate_taxes' => false, + 'origin_tax_data' => new Response($this->resp), + ]); $client = Client::factory()->create([ 'user_id' => $this->user->id, - 'company_id' => $this->company->id, + 'company_id' => $company->id, '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->uses_inclusive_taxes = false; @@ -143,35 +151,39 @@ class SumTaxTest extends TestCase public function testCalcInvoiceTax() { - + $settings = CompanySettings::defaults(); + $settings->country_id = '840'; + $settings->currency_id = '1'; $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; - $this->company->calculate_taxes = true; - $this->company->tax_data = $tax_data; - $this->company->save(); + $company = Company::factory()->create([ + 'account_id' => $this->account->id, + '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([ - 'user_id' => $this->user->id, - 'company_id' => $this->company->id, - 'country_id' => 840, - 'tax_data' => $tax_data, -]); + $client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $company->id, + 'country_id' => 840, + 'postal_code' => '90210', + 'state' => 'CA', + 'tax_data' => new Response($this->resp), + ]); -$invoice = InvoiceFactory::create($this->company->id, $this->user->id); -$invoice->client_id = $client->id; -$invoice->uses_inclusive_taxes = false; + $invoice = InvoiceFactory::create($company->id, $this->user->id); + $invoice->client_id = $client->id; + $invoice->uses_inclusive_taxes = false; -$line_items = []; + $line_items = []; -$invoice->tax_data = $tax_data; - - $line_item = new InvoiceItem; $line_item->quantity = 1; $line_item->cost = 10; @@ -187,7 +199,6 @@ $invoice->tax_data = $tax_data; $line_items = $invoice->line_items; - $this->assertEquals(10.88, $invoice->amount); $this->assertEquals("CA Sales Tax", $line_items[0]->tax_name1); $this->assertEquals(8.75, $line_items[0]->tax_rate1); diff --git a/tests/Unit/Tax/TaxConfigTest.php b/tests/Unit/Tax/TaxConfigTest.php new file mode 100644 index 000000000000..459c36240391 --- /dev/null +++ b/tests/Unit/Tax/TaxConfigTest.php @@ -0,0 +1,75 @@ +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(); + + } + +} \ No newline at end of file diff --git a/tests/Unit/Tax/UsTaxTest.php b/tests/Unit/Tax/UsTaxTest.php index 610b1b5218c0..15a0c10f1d5a 100644 --- a/tests/Unit/Tax/UsTaxTest.php +++ b/tests/Unit/Tax/UsTaxTest.php @@ -98,6 +98,8 @@ class UsTaxTest extends TestCase 'settings' => $settings, 'tax_data' => $tax_data, 'calculate_taxes' => true, + 'origin_tax_data' => new Response($this->mock_response), + ]); $client = Client::factory()->create([ @@ -147,6 +149,591 @@ class UsTaxTest extends TestCase 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() { @@ -166,6 +753,7 @@ class UsTaxTest extends TestCase 'settings' => $settings, 'tax_data' => $tax_data, 'calculate_taxes' => true, + 'origin_tax_data' => new Response($this->mock_response), ]); $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() { $settings = CompanySettings::defaults(); @@ -236,6 +896,7 @@ class UsTaxTest extends TestCase 'settings' => $settings, 'tax_data' => $tax_data, 'calculate_taxes' => true, + 'origin_tax_data' => new Response($this->mock_response), ]); $client = Client::factory()->create([ @@ -305,6 +966,7 @@ class UsTaxTest extends TestCase 'settings' => $settings, 'tax_data' => $tax_data, 'calculate_taxes' => true, + 'origin_tax_data' => new Response($this->mock_response), ]); $client = Client::factory()->create([ @@ -373,6 +1035,7 @@ class UsTaxTest extends TestCase 'settings' => $settings, 'tax_data' => $tax_data, 'calculate_taxes' => true, + 'origin_tax_data' => new Response($this->mock_response), ]); $client = Client::factory()->create([