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

Updates for taxes

This commit is contained in:
David Bomba 2023-06-02 15:53:33 +10:00
parent f299033896
commit 66aa198cf4
12 changed files with 207 additions and 105 deletions

View File

@ -16,7 +16,6 @@ use App\Models\Invoice;
use App\Models\Product;
use App\DataProviders\USStates;
use App\DataMapper\Tax\ZipTax\Response;
use App\Services\Tax\Providers\TaxProvider;
class BaseRule implements RuleInterface
{
@ -203,18 +202,9 @@ class BaseRule implements RuleInterface
$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();
// }
if($this->client->tax_data)
$tax_data = $this->client->tax_data;
elseif($this->client->tax_data){
$tax_data = $this->client->tax_data;
}

View File

@ -164,7 +164,6 @@ class Rule extends BaseRule implements RuleInterface
$this->tax_rate1 = $this->invoice->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->tax_rate;
$this->tax_name1 = "Sales Tax";
// $this->tax_name1 = $this->invoice->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->tax_name;
return $this;
}
@ -220,4 +219,4 @@ class Rule extends BaseRule implements RuleInterface
return $this;
}
}
}

View File

@ -67,8 +67,8 @@ class Response
public float $taxSales = 0;
public string $taxName = "";
public float $taxUse = 0;
public string $txbService = ""; // N = No, Y = Yes
public string $txbFreight = ""; // N = No, Y = Yes
public string $txbService = "Y"; // N = No, Y = Yes
public string $txbFreight = "Y"; // N = No, Y = Yes
public float $stateSalesTax = 0;
public float $stateUseTax = 0;
public float $citySalesTax = 0;
@ -98,7 +98,7 @@ class Response
public float $district5UseTax = 0;
/* US SPECIFIC TAX CODES */
public string $originDestination = ""; // defines if the client origin is the locale where the tax is remitted to
public string $originDestination = "D"; // defines if the client origin is the locale where the tax is remitted to
public function __construct($data = null)
{

View File

@ -11,6 +11,7 @@
namespace App\Jobs\Client;
use App\DataMapper\Tax\ZipTax\Response;
use App\Models\Client;
use App\Models\Company;
use App\Libraries\MultiDB;
@ -51,9 +52,9 @@ class UpdateTaxData implements ShouldQueue
{
MultiDB::setDb($this->company->db);
if(!config('services.tax.zip_tax.key'))
if($this->company->account->isFreeHostedClient())
return;
$tax_provider = new \App\Services\Tax\Providers\TaxProvider($this->company, $this->client);
try {
@ -63,8 +64,7 @@ class UpdateTaxData implements ShouldQueue
if (!$this->client->state && $this->client->postal_code) {
$this->client->state = USStates::getState($this->client->postal_code);
$this->client->save();
$this->client->saveQuietly();
}
@ -73,11 +73,80 @@ class UpdateTaxData implements ShouldQueue
nlog("problem getting tax data => ".$e->getMessage());
}
/** Set static tax information */
if(!$tax_provider->updatedTaxStatus() && $this->client->country_id == 840){
$calculated_state = false;
/** State must be calculated else default to the company state for taxes */
if(array_key_exists($this->client->shipping_state, USStates::get())) {
$calculated_state = $this->client->shipping_state;
$calculated_postal_code = $this->client->shipping_postal_code;
$calculated_city = $this->client->shipping_city;
}
elseif(array_key_exists($this->client->state, USStates::get())){
$calculated_state = $this->client->state;
$calculated_postal_code = $this->client->postal_code;
$calculated_city = $this->client->city;
}
else {
try{
$calculated_state = USStates::getState($this->client->shipping_postal_code);
$calculated_postal_code = $this->client->shipping_postal_code;
$calculated_city = $this->client->shipping_city;
}
catch(\Exception $e){
nlog("could not calculate state from postal code => {$this->client->shipping_postal_code} or from state {$this->client->shipping_state}");
}
if(!$calculated_state) {
try {
$calculated_state = USStates::getState($this->client->postal_code);
$calculated_postal_code = $this->client->postal_code;
$calculated_city = $this->client->city;
} catch(\Exception $e) {
nlog("could not calculate state from postal code => {$this->client->postal_code} or from state {$this->client->state}");
}
}
if($this->company->tax_data?->seller_subregion)
$calculated_state = $this->company->tax_data?->seller_subregion;
nlog("i am trying");
if(!$calculated_state) {
nlog("could not determine state");
return;
}
}
$data = [
'seller_subregion' => $this->company->origin_tax_data?->seller_subregion ?: '',
'geoPostalCode' => $this->client->postal_code ?? '',
'geoCity' => $this->client->city ?? '',
'geoState' => $calculated_state,
'taxSales' => $this->company->tax_data->regions->US->subregions?->{$calculated_state}?->taxSales ?? 0,
];
$tax_data = new Response($data);
$this->client->tax_data = $tax_data;
$this->client->saveQuietly();
}
}
public function middleware()
{
return [new WithoutOverlapping($this->company->id)];
return [new WithoutOverlapping($this->client->id.$this->company->id)];
}
public function failed($exception)
{
nlog("UpdateTaxData failed => ".$exception->getMessage());
}
}

View File

@ -11,12 +11,12 @@
namespace App\Jobs\Company;
use App\Models\Client;
use App\Models\Company;
use App\Libraries\MultiDB;
use Illuminate\Bus\Queueable;
use App\Jobs\Client\UpdateTaxData;
use App\DataProviders\USStates;
use Illuminate\Queue\SerializesModels;
use App\DataMapper\Tax\ZipTax\Response;
use Illuminate\Queue\InteractsWithQueue;
use App\Services\Tax\Providers\TaxProvider;
use Illuminate\Contracts\Queue\ShouldQueue;
@ -40,33 +40,52 @@ class CompanyTaxRate implements ShouldQueue
public function handle()
{
if(!config('services.tax.zip_tax.key')) {
return;
}
MultiDB::setDB($this->company->db);
$tp = new TaxProvider($this->company);
$tp->updateCompanyTaxData();
$tp = null;
Client::query()
->where('company_id', $this->company->id)
->where('is_deleted', false)
->where('country_id', 840)
->whereNotNull('postal_code')
->whereNull('tax_data')
->where('is_tax_exempt', false)
->cursor()
->each(function ($client) {
(new UpdateTaxData($client, $this->company))->handle();
});
if(!$tp->updatedTaxStatus() && $this->company->settings->country_id == '840') {
$calculated_state = false;
/** State must be calculated else default to the company state for taxes */
if(array_key_exists($this->company->settings->state, USStates::get())) {
$calculated_state = $this->company->setting->state;
}
else {
try{
$calculated_state = USStates::getState($this->company->settings->postal_code);
}
catch(\Exception $e){
nlog("could not calculate state from postal code => {$this->company->settings->postal_code} or from state {$this->company->settings->state}");
}
if(!$calculated_state && $this->company->tax_data?->seller_subregion)
$calculated_state = $this->company->tax_data?->seller_subregion;
if(!$calculated_state)
return;
}
$data = [
'seller_subregion' => $this->company->origin_tax_data?->seller_subregion ?: '',
'geoPostalCode' => $this->company->settings->postal_code ?? '',
'geoCity' => $this->company->settings->city ?? '',
'geoState' => $calculated_state,
'taxSales' => $this->company->tax_data->regions->US->subregions?->{$calculated_state}?->taxSales ?? 0,
];
$tax_data = new Response($data);
$this->company->origin_tax_data = $tax_data;
$this->company->saveQuietly();
}
}
public function middleware()
@ -74,4 +93,7 @@ class CompanyTaxRate implements ShouldQueue
return [new WithoutOverlapping($this->company->id)];
}
public function failed($e){
nlog($e->getMessage());
}
}

View File

@ -100,10 +100,10 @@ class ClientPresenter extends EntityPresenter
if ($address2 = $client->shipping_address2) {
$str .= e($address2).'<br/>';
}
if ($cityState = $this->getCityState()) {
if ($cityState = $this->getShippingCityState()) {
$str .= e($cityState).'<br/>';
}
if ($country = $client->country) {
if ($country = $client->shipping_country) {
$str .= e($country->name).'<br/>';
}
@ -194,4 +194,6 @@ class ClientPresenter extends EntityPresenter
return false;
}
}
}

View File

@ -11,7 +11,6 @@
namespace App\Observers;
use App\Utils\Ninja;
use App\Models\Client;
use App\Models\Webhook;
use App\Jobs\Client\CheckVat;
@ -60,11 +59,12 @@ class ClientObserver
*/
public function created(Client $client)
{
if ($client->country_id == 840 && $client->company->calculate_taxes) {
/** Fix Tax Data for Clients */
if ($client->country_id == 840 && $client->company->calculate_taxes && !$client->company->account->isFreeHostedClient()) {
UpdateTaxData::dispatch($client, $client->company);
}
/** Check VAT records for client */
if(in_array($client->country_id, $this->eu_country_codes) && $client->company->calculate_taxes) {
CheckVat::dispatch($client, $client->company);
}
@ -88,7 +88,7 @@ class ClientObserver
{
/** Monitor postal code changes for US based clients for tax calculations */
if(Ninja::isHosted() && $client->getOriginal('postal_code') != $client->postal_code && $client->country_id == 840 && $client->company->calculate_taxes) {
if($client->getOriginal('postal_code') != $client->postal_code && $client->country_id == 840 && $client->company->calculate_taxes && !$client->company->account->isFreeHostedClient()) {
UpdateTaxData::dispatch($client, $client->company);
}

View File

@ -36,15 +36,8 @@ class CompanyObserver
*/
public function updated(Company $company)
{
if (Ninja::isHosted() && $company->portal_mode == 'domain' && $company->isDirty('portal_domain')) {
//fire event to build new custom portal domain
if (Ninja::isHosted() && $company->portal_mode == 'domain' && $company->isDirty('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

@ -11,18 +11,19 @@
namespace App\Repositories;
use App\Jobs\Product\UpdateOrCreateProduct;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Company;
use App\Models\Credit;
use App\Models\Invoice;
use App\Models\Quote;
use App\Models\RecurringInvoice;
use App\Utils\Helpers;
use App\Utils\Ninja;
use App\Models\Quote;
use App\Models\Client;
use App\Models\Credit;
use App\Utils\Helpers;
use App\Models\Company;
use App\Models\Invoice;
use App\Models\ClientContact;
use App\Utils\Traits\MakesHash;
use App\Models\RecurringInvoice;
use App\Jobs\Client\UpdateTaxData;
use App\Utils\Traits\SavesDocuments;
use App\Jobs\Product\UpdateOrCreateProduct;
class BaseRepository
{
@ -308,6 +309,11 @@ class BaseRepository
} else {
event('eloquent.updated: App\Models\Invoice', $model);
}
/** If the client does not have tax_data - then populate this now */
if($client->country_id == 840 && !$client->tax_data && $model->company->calculate_taxes && !$model->company->account->isFreeHostedClient())
UpdateTaxData::dispatch($client, $client->company);
}
if ($model instanceof Credit) {

View File

@ -52,11 +52,22 @@ class TaxProvider
private mixed $api_credentials;
private bool $updated_client = false;
public function __construct(public Company $company, public ?Client $client = null)
{
}
/**
* Flag if tax has been updated successfull.
*
* @return bool
*/
public function updatedTaxStatus(): bool
{
return $this->updated_client;
}
/**
* updateCompanyTaxData
*
@ -67,23 +78,31 @@ class TaxProvider
$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,
'address2' => $this->company->settings->address2,
'address1' => $this->company->settings->address1,
'city' => $this->company->settings->city,
'state' => $this->company->settings->state,
'postal_code' => $this->company->settings->postal_code,
'country_id' => $this->company->settings->country_id,
'country' => $this->company->country()->name,
];
$tax_provider = new $this->provider($company_details);
try {
$tax_provider = new $this->provider($company_details);
$tax_provider->setApiCredentials($this->api_credentials);
$tax_data = $tax_provider->run();
$this->company->origin_tax_data = $tax_data;
$this->company->save();
$tax_provider->setApiCredentials($this->api_credentials);
$tax_data = $tax_provider->run();
if($tax_data) {
$this->company->origin_tax_data = $tax_data;
$this->company->saveQuietly();
$this->updated_client = true;
}
}
catch(\Exception $e){
nlog("Could not updated company tax data: " . $e->getMessage());
}
return $this;
@ -99,21 +118,21 @@ class TaxProvider
$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,
'address2' => $this->client->address2,
'address1' => $this->client->address1,
'city' => $this->client->city,
'state' => $this->client->state,
'postal_code' => $this->client->postal_code,
'country_id' => $this->client->country_id,
'country' => $this->client->country->name,
];
$shipping_details =[
'address1' => $this->client->shipping_address1,
'address2' => $this->client->shipping_address2,
'address1' => $this->client->shipping_address1,
'city' => $this->client->shipping_city,
'state' => $this->client->shipping_state,
'postal_code' => $this->client->shipping_postal_code,
'country_id' => $this->client->shipping_country_id,
'country' => $this->client->shipping_country->name,
];
$taxable_address = $this->taxShippingAddress() ? $shipping_details : $billing_details;
@ -123,10 +142,14 @@ class TaxProvider
$tax_provider->setApiCredentials($this->api_credentials);
$tax_data = $tax_provider->run();
$this->client->tax_data = $tax_data;
nlog($tax_data);
$this->client->save();
if($tax_data) {
$this->client->tax_data = $tax_data;
$this->client->saveQuietly();
$this->updated_client = true;
}
return $this;
@ -224,10 +247,12 @@ class TaxProvider
*/
private function configureZipTax(): self
{
if(!config('services.tax.zip_tax.key'))
throw new \Exception("ZipTax API key not set in .env file");
$this->provider = ZipTax::class;
$this->api_credentials = config('services.tax.zip_tax.key');
$this->provider = ZipTax::class;
return $this;

View File

@ -32,9 +32,7 @@ class ZipTax implements TaxProviderInterface
$response = $this->callApi(['key' => $this->api_key, 'address' => $string_address]);
if($response->successful()){
return $this->parseResponse($response->json());
}
if(isset($this->address['postal_code'])) {
@ -45,8 +43,7 @@ class ZipTax implements TaxProviderInterface
}
// $response->throw();
return null;
}
public function setApiCredentials($api_key): self
@ -64,18 +61,21 @@ class ZipTax implements TaxProviderInterface
*/
private function callApi(array $parameters): Response
{
$response = Http::retry(3, 1000)->withHeaders([])->get($this->endpoint, $parameters);
return $response;
return Http::retry(3, 1000)->withHeaders([])->get($this->endpoint, $parameters);
}
private function parseResponse($response)
{
if(isset($response['results']['0']))
if(isset($response['rCode']) && $response['rCode'] == 100)
return $response['results']['0'];
if(isset($response['rCode']) && class_exists(\Modules\Admin\Events\TaxProviderException::class))
event(new \Modules\Admin\Events\TaxProviderException($response['rCode']));
return null;
// throw new \Exception("Error resolving tax (code) = " . $response['rCode']);
}
}

View File

@ -79,22 +79,18 @@ trait CompanySettingsSaver
$entity->settings = $company_settings;
if( $entity?->calculate_taxes && $company_settings->country_id == "840" && array_key_exists('settings', $entity->getDirty()))
if($entity?->calculate_taxes && $company_settings->country_id == "840" && array_key_exists('settings', $entity->getDirty()) && !$entity?->account->isFreeHostedClient())
{
$old_settings = $entity->getOriginal()['settings'];
/** Monitor changes of the Postal code */
if($old_settings->postal_code != $company_settings->postal_code)
{
nlog("postal code change");
CompanyTaxRate::dispatch($entity);
}
}
elseif( $entity?->calculate_taxes && $company_settings->country_id == "840" && array_key_exists('calculate_taxes', $entity->getDirty()) && $entity->getOriginal('calculate_taxes') == 0)
elseif( $entity?->calculate_taxes && $company_settings->country_id == "840" && array_key_exists('calculate_taxes', $entity->getDirty()) && $entity->getOriginal('calculate_taxes') == 0 && !$entity?->account->isFreeHostedClient())
{
nlog("calc taxes change");
nlog($entity->getOriginal('calculate_taxes'));
CompanyTaxRate::dispatch($entity);
}