Merge pull request #8510 from turbo124/v5-develop

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

View File

@ -0,0 +1,27 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
class EncryptedCast implements CastsAttributes
{
public function get($model, string $key, $value, array $attributes)
{
return strlen($value) > 1 ? decrypt($value) : null;
}
public function set($model, string $key, $value, array $attributes)
{
return [$key => ! is_null($value) ? encrypt($value) : null];
}
}

View File

@ -14,9 +14,9 @@ namespace App\DataMapper\Tax;
use App\Models\Client;
use App\Models\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;
@ -131,69 +128,139 @@ 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'){
if($this->invoice instanceof Invoice) {
$this->invoice->tax_data = $tax_data;
$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 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)));
}
}

View File

@ -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;
}

View File

@ -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();
}

View File

@ -41,31 +41,39 @@ class Rule extends BaseRule implements RuleInterface
/**
* Override tax class, we use this when we do not modify the input taxes
*
* @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,32 +157,31 @@ 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;
}
return $this;
}
$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;
}
/**
@ -174,9 +189,21 @@ class Rule extends BaseRule implements RuleInterface
*
* @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;
}
@ -190,4 +217,5 @@ class Rule extends BaseRule implements RuleInterface
{
return $this;
}
}

View File

@ -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',

View File

@ -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
nlog($this->client->company->country()->iso_3166_2);
if (in_array($this->client->company->country()->iso_3166_2, $this->tax_jurisdictions) ) { //only calculate for supported tax jurisdictions
$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();

View File

@ -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);
}

View File

@ -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);

View File

@ -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');

View File

@ -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)
{

View File

@ -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);
}

View File

@ -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
*/

View File

@ -45,7 +45,7 @@ class StoreSchedulerRequest extends Request
'parameters.end_date' => ['bail', 'sometimes', 'date:Y-m-d', 'required_if:parameters.date_rate,custom', 'after_or_equal:parameters.start_date'],
'parameters.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);
}
}

View File

@ -42,7 +42,7 @@ class UpdateSchedulerRequest extends Request
'parameters.end_date' => ['bail', 'sometimes', 'date:Y-m-d', 'required_if:parameters.date_rate,custom', 'after_or_equal:parameters.start_date'],
'parameters.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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

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

View File

@ -13,12 +13,14 @@ namespace App\Jobs\Company;
use App\Utils\Ninja;
use App\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;
}
}

View File

@ -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()
{

View File

@ -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;

View File

@ -46,12 +46,21 @@ class InvoiceReminderEmailActivity implements ShouldQueue
$user_id = array_key_exists('user_id', $event->event_vars) ? $event->event_vars['user_id'] : $event->invitation->invoice->user_id;
$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);
}

View File

@ -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,

View File

@ -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...

View File

@ -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) {
@ -54,7 +65,6 @@ class ClientObserver
$event = Webhook::EVENT_DELETE_CLIENT;
}
$subscriptions = Webhook::where('company_id', $client->company_id)
->where('event_id', $event)
->exists();

View File

@ -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());
// }
}
/**

View File

@ -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) {

View File

@ -777,6 +777,7 @@ class StripePaymentDriver extends BaseDriver
->where('token', $request->data['object']['payment_method'])
->first();
if($clientgateway)
$clientgateway->delete();
return response()->json([], 200);

View File

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

View File

@ -23,20 +23,26 @@ trait ChartQueries
*/
public function getExpenseQuery($start_date, $end_date)
{
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

View File

@ -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');

View File

@ -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
]);

View File

@ -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)),

View File

@ -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->company->tax_data = $tax_data;
$this->client->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

View File

@ -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']);
}
}

View File

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

View File

@ -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,
];
}

View File

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

View File

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

View File

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

View File

@ -20,17 +20,17 @@ $LANG = array(
'additional_info' => 'Informació adicional',
'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 <b>impost</b>',
'enable_line_item_tax' => 'Activar especificar <b>impost per línea</b>',
'enable_invoice_tax' => 'Activa especificar <b>impost</b>',
'enable_line_item_tax' => 'Activa especificar <b>impost per línea</b>',
'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' => '<p>Envieu automàticament als clients les mateixes factures setmanalment, bimensuals, mensuals, trimestrals o anuals.</p>
<p>Utilitzeu: MONTH,: TRIMESTRE o: YEAR per a dates dinàmiques. Les funcions matemàtiques bàsiques també funcionen, per exemple: MES-1 </p>
',
<p>Utilitzeu :MONTH, :QUARTER o :YEAR per a dates dinàmiques. Les funcions matemàtiques bàsiques també funcionen, per exemple: :MONTH-1 </p>
<p>Exemples de variables dinàmiques de factures:</p>
<ul>
<li>"Quota gimnàs pel mes de :MONTH" >> "Quota gimnàs pel mes de juliol"</li>
<li>"Subscripció anual :YEAR+1" >> "Subscripció anual 2015"</li>
<li>"Pagament consultor del :QUARTER+1" >> "Pagament consultor del Q2"</li>
</ul>',
'recurring_quotes' => 'Pressupostos recurrents',
'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.<br/>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 <a class="underline" href="https://stripe.com/au-becs-dd-service-agreement/legal">Direct Debit Request and the Direct Debit Request service agreement</a>, and authorise Stripe Payments Australia Pty Ltd ACN 160 180 343 Direct Debit User ID number 507156 (“Stripe”) to debit your account through the Bulk Electronic Clearing System (BECS) on behalf of :company (the “Merchant”) for any amounts separately communicated to you by the Merchant. You certify that you are either an account holder or an authorised signatory on the account listed above.',
'you_need_to_accept_the_terms_before_proceeding' => 'You need to accept the terms before proceeding.',
'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. <br><br> Your license key is: <br><br> :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. <br><br>La vostra clau de llicència és: <br><br>: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',
);

View File

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

View File

@ -5072,6 +5072,12 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette
'set_tax_category' => 'Définir la catégorie de taxe',
'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',
);

View File

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

View File

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

View File

@ -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,

View File

@ -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',
],
];

View File

@ -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()));

View File

@ -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,
@ -120,6 +122,307 @@ class EuTaxTest extends TestCase
}
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,
]);
@ -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,

View File

@ -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,34 +151,38 @@ 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();
$tax_data = new TaxData($this->response);
$client = Client::factory()->create([
'user_id' => $this->user->id,
'company_id' => $this->company->id,
'country_id' => 840,
$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),
]);
$invoice = InvoiceFactory::create($this->company->id, $this->user->id);
$invoice->client_id = $client->id;
$invoice->uses_inclusive_taxes = false;
$line_items = [];
$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->tax_data = $tax_data;
$invoice = InvoiceFactory::create($company->id, $this->user->id);
$invoice->client_id = $client->id;
$invoice->uses_inclusive_taxes = false;
$line_items = [];
$line_item = new InvoiceItem;
$line_item->quantity = 1;
@ -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);

View File

@ -0,0 +1,75 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Unit\Tax;
use App\DataProviders\USStates;
use Tests\TestCase;
use App\Models\Client;
use Tests\MockAccountData;
use App\Services\Tax\Providers\TaxProvider;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Foundation\Testing\DatabaseTransactions;
/**
* @test App\Services\Tax\Providers\EuTax
*/
class TaxConfigTest extends TestCase
{
use MockAccountData;
use DatabaseTransactions;
protected function setUp() :void
{
parent::setUp();
$this->withoutMiddleware(
ThrottleRequests::class
);
$this->withoutExceptionHandling();
$this->makeTestData();
if(!config('services.tax.zip_tax.key'))
$this->markTestSkipped('No API keys to test with.');
}
public TaxProvider $tp;
private function bootApi(Client $client)
{
$this->tp = new TaxProvider($this->company, $client);
}
public function testStateResolution()
{
//infer state from zip
$client = Client::factory()->create([
'company_id' => $this->company->id,
'user_id' => $this->user->id,
'address1' => '400 Evelyn Pl',
'city' => 'Beverley Hills',
'state' => '',
'postal_code' => '',
'country_id' => 840,
]);
// $this->assertEquals('CA', USStates::getState('90210'));
$this->bootApi($client);
$this->tp->updateClientTaxData();
}
}

View File

@ -98,6 +98,8 @@ class UsTaxTest extends TestCase
'settings' => $settings,
'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([