diff --git a/app/DataMapper/Tax/BaseRule.php b/app/DataMapper/Tax/BaseRule.php index 55666506218c..4aed440ef61e 100644 --- a/app/DataMapper/Tax/BaseRule.php +++ b/app/DataMapper/Tax/BaseRule.php @@ -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; } diff --git a/app/DataMapper/Tax/US/Rule.php b/app/DataMapper/Tax/US/Rule.php index eb1b55b86c80..572f8e449e12 100644 --- a/app/DataMapper/Tax/US/Rule.php +++ b/app/DataMapper/Tax/US/Rule.php @@ -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; } -} +} \ No newline at end of file diff --git a/app/DataMapper/Tax/ZipTax/Response.php b/app/DataMapper/Tax/ZipTax/Response.php index 2a7e9d347337..4a8e03634355 100644 --- a/app/DataMapper/Tax/ZipTax/Response.php +++ b/app/DataMapper/Tax/ZipTax/Response.php @@ -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) { diff --git a/app/Jobs/Client/UpdateTaxData.php b/app/Jobs/Client/UpdateTaxData.php index 314fbff04d24..fbe0a8527f89 100644 --- a/app/Jobs/Client/UpdateTaxData.php +++ b/app/Jobs/Client/UpdateTaxData.php @@ -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()); } } \ No newline at end of file diff --git a/app/Jobs/Company/CompanyTaxRate.php b/app/Jobs/Company/CompanyTaxRate.php index 7da21fb92e24..8fbc6a3e4452 100644 --- a/app/Jobs/Company/CompanyTaxRate.php +++ b/app/Jobs/Company/CompanyTaxRate.php @@ -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()); + } } \ No newline at end of file diff --git a/app/Models/Presenters/ClientPresenter.php b/app/Models/Presenters/ClientPresenter.php index 53450a587284..ac4b184d94e1 100644 --- a/app/Models/Presenters/ClientPresenter.php +++ b/app/Models/Presenters/ClientPresenter.php @@ -100,10 +100,10 @@ class ClientPresenter extends EntityPresenter if ($address2 = $client->shipping_address2) { $str .= e($address2).'
'; } - if ($cityState = $this->getCityState()) { + if ($cityState = $this->getShippingCityState()) { $str .= e($cityState).'
'; } - if ($country = $client->country) { + if ($country = $client->shipping_country) { $str .= e($country->name).'
'; } @@ -194,4 +194,6 @@ class ClientPresenter extends EntityPresenter return false; } } + + } diff --git a/app/Observers/ClientObserver.php b/app/Observers/ClientObserver.php index 4ab5d076e4a4..0b06b2cf3dee 100644 --- a/app/Observers/ClientObserver.php +++ b/app/Observers/ClientObserver.php @@ -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); } diff --git a/app/Observers/CompanyObserver.php b/app/Observers/CompanyObserver.php index 93c2aeaf68c7..15ed25274945 100644 --- a/app/Observers/CompanyObserver.php +++ b/app/Observers/CompanyObserver.php @@ -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()); - // } } diff --git a/app/Repositories/BaseRepository.php b/app/Repositories/BaseRepository.php index 005a117d02ee..15c93b5fe556 100644 --- a/app/Repositories/BaseRepository.php +++ b/app/Repositories/BaseRepository.php @@ -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) { diff --git a/app/Services/Tax/Providers/TaxProvider.php b/app/Services/Tax/Providers/TaxProvider.php index ccc5489f71f0..c43315d40920 100644 --- a/app/Services/Tax/Providers/TaxProvider.php +++ b/app/Services/Tax/Providers/TaxProvider.php @@ -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; diff --git a/app/Services/Tax/Providers/ZipTax.php b/app/Services/Tax/Providers/ZipTax.php index 8a6ffb78a2ef..0cc0dd1a5524 100644 --- a/app/Services/Tax/Providers/ZipTax.php +++ b/app/Services/Tax/Providers/ZipTax.php @@ -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']); + } } diff --git a/app/Utils/Traits/CompanySettingsSaver.php b/app/Utils/Traits/CompanySettingsSaver.php index 79d08059fc20..915b2e88bea7 100644 --- a/app/Utils/Traits/CompanySettingsSaver.php +++ b/app/Utils/Traits/CompanySettingsSaver.php @@ -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); }