PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.3.20
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.3.20
1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk All 48 releases
fluent-cart / app / Modules / Tax / TaxModule.php

TaxModule.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.3.20, at app/Modules/Tax/TaxModule.php

924 lines 36.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Modules\Tax;
4
5 use FluentCart\App\App;
6 use FluentCart\Api\StoreSettings;
7 use FluentCart\App\Helpers\Helper;
8 use FluentCart\Framework\Support\Arr;
9 use FluentCart\App\Helpers\CartHelper;
10 use FluentCart\App\Models\Cart;
11 use FluentCart\App\Models\OrderTaxRate;
12 use FluentCart\App\Services\Renderer\EUVatRenderer;
13 use FluentCart\App\Services\Renderer\CartSummaryRender;
14 use FluentCart\Api\Resource\FrontendResource\CartResource;
15 use FluentCart\App\Services\Localization\LocalizationManager;
16
17 class TaxModule
18 {
19
20 protected $taxSettings = [];
21
22 public function register()
23 {
24 if (!$this->isEnabled()) {
25 return;
26 }
27
28 add_filter('fluent_cart/cart/estimated_total', function ($total, $data) {
29 $cart = $data['cart'];
30 if (Arr::get($cart->checkout_data, 'tax_data.tax_behavior', 0) == 1) {
31 $taxTotal = (int)Arr::get($cart->checkout_data, 'tax_data.tax_total', 0);
32 $total += (int)$taxTotal;
33 // adds shipping tax as well
34 $shippingTax = (int)Arr::get($cart->checkout_data, 'tax_data.shipping_tax', 0);
35 $total += (int)$shippingTax;
36 }
37 return $total;
38 }, 10, 2);
39
40 //new hook to get changes
41 add_filter('fluent_cart/checkout/before_patch_checkout_data', [$this, 'maybeRecalculateTaxAmount'], 10, 2);
42
43 add_filter('fluent_cart/cart/tax_behavior', function ($behavior, $data) {
44 $cart = $data['cart'];
45 return Arr::get($cart->checkout_data, 'tax_data.tax_behavior', $behavior);
46 }, 10, 2);
47
48 add_action('fluent_cart/checkout/before_summary_total', function ($data) {
49 $cart = $data['cart'];
50
51 if (empty($cart->checkout_data['tax_data'])) {
52 return;
53 }
54
55 $taxAmount = (int)Arr::get($cart->checkout_data, 'tax_data.tax_total', 0);
56 $taxLines = Arr::get($cart->checkout_data, 'tax_data.tax_lines', []);
57 $shippingTax = (int)Arr::get($cart->checkout_data, 'tax_data.shipping_tax', 0);
58 $isInclusive = Arr::get($cart->checkout_data, 'tax_data.tax_behavior', 2) === 2;
59 $isReverseCharge = Arr::get($cart->checkout_data, 'tax_data.valid', false);
60
61 // if (!$taxAmount && !$isReverseCharge && !$shippingTax) {
62 // return;
63 // }
64
65
66 // Show breakdown by tax rate if available
67 if (!empty($taxLines) && !$isReverseCharge) {
68 $this->renderCompoundTaxRow($cart);
69 } else {
70 // Fallback to showing total tax if no breakdown available
71 $this->renderTaxRow($cart);
72 ?>
73 <?php
74 }
75
76 $this->renderShippingTaxRow($cart);
77 });
78
79 add_action('fluent_cart/checkout/prepare_other_data', [$this, 'prepareOtherData'], 10, 1);
80
81 add_action('fluent_cart/product/after_price', function () {
82 $priceSuffix = Arr::get($this->taxSettings, 'price_suffix', '');
83 if ($priceSuffix) {
84 echo '<span class="fct_price_suffix">' . wp_kses_post($priceSuffix) . '</span>';
85 }
86 });
87
88 add_filter('fluent_cart/product/price_suffix_atts', function ($suffix) {
89 $priceSuffix = Arr::get($this->taxSettings, 'price_suffix', '');
90 if ($priceSuffix) {
91 return $priceSuffix;
92 }
93 return $suffix;
94 });
95
96 add_filter('fluent_cart/checkout/after_patch_checkout_data_fragments', [$this, 'maybeRerenderEuVatField'], 10, 2);
97
98 $this->initCheckoutActions();
99 $this->registerAjaxHandlers();
100
101 add_action('fluent_cart/cart/cart_data_items_updated', [$this, 'recalculateTax']);
102 }
103
104 public function renderTaxRow($cart, $atts = '')
105 {
106 $taxAmount = (int)Arr::get($cart->checkout_data, 'tax_data.tax_total', 0);
107 $shippingTax = (int)Arr::get($cart->checkout_data, 'tax_data.shipping_tax', 0);
108 $isInclusive = Arr::get($cart->checkout_data, 'tax_data.tax_behavior', 2) === 2;
109 $isReverseCharge = Arr::get($cart->checkout_data, 'tax_data.valid', false);
110 ?>
111 <li <?php echo $atts; ?>>
112 <span class="fct_summary_label">
113 <?php if ($isReverseCharge && $taxAmount == 0): ?>
114 <?php echo esc_html__('Reverse Charge (No Tax)', 'fluent-cart'); ?>
115 <?php elseif ($isInclusive) : ?>
116 <?php echo esc_html__('Tax Estimate (Included)', 'fluent-cart'); ?>
117 <?php else : ?>
118 <?php echo esc_html__('Tax Estimate (Excluded)', 'fluent-cart'); ?>
119 <?php endif; ?>
120 </span>
121 <span class="fct_summary_value"><?php echo esc_html(Helper::toDecimal($taxAmount)); ?></span>
122 </li>
123 <?php
124 }
125
126 public function renderShippingTaxRow($cart, $atts = '')
127 {
128
129 $shippingTax = (int)Arr::get($cart->checkout_data, 'tax_data.shipping_tax', 0);
130 $isInclusive = Arr::get($cart->checkout_data, 'tax_data.tax_behavior', 2) === 2;
131
132 if ($shippingTax == 0) {
133 return '';
134 }
135 ?>
136 <li <?php echo $atts; ?>>
137 <!-- shipping tax -->
138 <span class="fct_summary_label">
139 <?php if ($isInclusive) : ?>
140 <?php echo esc_html__('Shipping Tax (Included)', 'fluent-cart'); ?>
141 <?php else : ?>
142 <?php echo esc_html__('Shipping Tax (Excluded)', 'fluent-cart'); ?>
143 <?php endif; ?>
144 </span>
145 <span class="fct_summary_value"><?php echo esc_html(Helper::toDecimal($shippingTax)); ?></span>
146 </li>
147 <?php
148 }
149
150 public function renderCompoundTaxRow($cart, $atts = '')
151 {
152 $taxAmount = (int)Arr::get($cart->checkout_data, 'tax_data.tax_total', 0);
153 $taxLines = Arr::get($cart->checkout_data, 'tax_data.tax_lines', []);
154 $shippingTax = (int)Arr::get($cart->checkout_data, 'tax_data.shipping_tax', 0);
155 $isInclusive = Arr::get($cart->checkout_data, 'tax_data.tax_behavior', 2) === 2;
156 $isReverseCharge = Arr::get($cart->checkout_data, 'tax_data.valid', false);
157
158 foreach ($taxLines as $taxLine) {
159 $rateLabel = Arr::get($taxLine, 'label', 'Tax');
160 $rateTaxAmount = (int)Arr::get($taxLine, 'tax_amount', 0);
161 $isCompound = Arr::get($taxLine, 'is_compound', false);
162 $taxableBase = (int)Arr::get($taxLine, 'taxable_amount', 0);
163 $ratePercent = Arr::get($taxLine, 'rate_percent', 0);
164
165 // Build the label with compound indicator if needed
166 $displayLabel = $rateLabel;
167 if ($isCompound) {
168 $displayLabel .= ' (' . __('Compound', 'fluent-cart') . ')';
169 }
170
171 // Add calculation details if available
172 if ($taxableBase && $ratePercent) {
173 $displayLabel .= ' [' . Helper::toDecimal($taxableBase) . ' × ' . number_format($ratePercent, 2) . '%]';
174 }
175 ?>
176 <li>
177 <span class="fct_summary_label">
178 <?php if ($isInclusive) : ?>
179 <?php echo esc_html($displayLabel . ' (' . __('Included', 'fluent-cart') . ')'); ?>
180 <?php else : ?>
181 <?php echo esc_html($displayLabel . ' (' . __('Excluded', 'fluent-cart') . ')'); ?>
182 <?php endif; ?>
183 </span>
184 <span class="fct_summary_value"><?php echo esc_html(Helper::toDecimal($rateTaxAmount)); ?></span>
185 </li>
186 <?php
187 }
188 }
189
190 public function recalculateTax($data)
191 {
192 $cart = Arr::get($data, 'cart');
193
194 // Persist dynamic fees so tax pipeline can see them
195 $checkoutData = $cart->checkout_data;
196 $checkoutData['fees'] = $cart->getFees();
197
198 $fillData = $this->calculateCartTax([
199 'cart_data' => $cart->cart_data,
200 'checkout_data' => $checkoutData
201 ]);
202
203 $cart->fill($fillData);
204 $cart->save();
205 }
206
207 public function maybeRecalculateTaxAmount($fillData, $data)
208 {
209 $changes = Arr::get($data, 'changes', []);
210
211 $watchings = array_filter($changes, function ($value, $key) {
212 return preg_match('/^(billing_|shipping_|ship_to_|fct_billing_tax)/i', $key);
213 }, ARRAY_FILTER_USE_BOTH);
214
215 if (empty($watchings)) {
216 return $fillData;
217 }
218
219 // Persist dynamic fees so tax pipeline can see them
220 $cart = Arr::get($data, 'cart');
221 if ($cart) {
222 $checkoutData = Arr::get($fillData, 'checkout_data', []);
223 $checkoutData['fees'] = $cart->getFees();
224 $fillData['checkout_data'] = $checkoutData;
225 }
226
227 return $this->calculateCartTax($fillData);
228 }
229
230 public function getSettings()
231 {
232 if (!empty($this->taxSettings)) {
233 return $this->taxSettings;
234 }
235
236 $defaultSettings = [
237 'tax_inclusion' => 'included',
238 'tax_calculation_basis' => 'shipping',
239 'tax_rounding' => 'item',
240 'enable_tax' => 'no',
241 'eu_vat_settings' => [
242 'require_vat_number' => 'no',
243 'local_reverse_charge' => 'yes',
244 'vat_reverse_excluded_categories' => []
245 ]
246 ];
247
248 $savedSettings = get_option('fluent_cart_tax_configuration_settings', []);
249 return $this->taxSettings = wp_parse_args($savedSettings, $defaultSettings);
250 }
251
252 public function isEnabled()
253 {
254 $settings = $this->getSettings();
255 return Arr::get($settings, 'enable_tax', 'no') === 'yes';
256 }
257
258 public function calculateCartTax($fillData)
259 {
260 $lineItems = Arr::get($fillData, 'cart_data', []);
261 $checkoutData = Arr::get($fillData, 'checkout_data', []);
262 $country = '';
263 $state = '';
264 $postCode = '';
265
266 $taxSettings = $this->getSettings();
267
268 $taxCalculationBasis = Arr::get($taxSettings, 'tax_calculation_basis', 'shipping');
269
270 //$checkoutData = $cart->checkout_data;
271 if ($taxCalculationBasis === 'shipping' && Arr::get($checkoutData, 'form_data.ship_to_different', '') !== 'yes') {
272 $taxCalculationBasis = 'billing';
273 }
274
275 if ($taxCalculationBasis === 'shipping') {
276 $country = Arr::get($checkoutData, 'form_data.shipping_country', '');
277 $state = Arr::get($checkoutData, 'form_data.shipping_state', '');
278 $city = Arr::get($checkoutData, 'form_data.shipping_city', '');
279 $postCode = Arr::get($checkoutData, 'form_data.shipping_postcode', '');
280 } elseif ($taxCalculationBasis === 'billing') {
281 $country = Arr::get($checkoutData, 'form_data.billing_country', '');
282 $state = Arr::get($checkoutData, 'form_data.billing_state', '');
283 $city = Arr::get($checkoutData, 'form_data.billing_city', '');
284 $postCode = Arr::get($checkoutData, 'form_data.billing_postcode', '');
285 } elseif ($taxCalculationBasis === 'store') {
286 $storeSettings = new StoreSettings();
287 $country = $storeSettings->get('store_country');
288 $state = $storeSettings->get('store_state');
289 $city = $storeSettings->get('store_city');
290 $postCode = $storeSettings->get('store_postcode');
291 }
292
293 // Add taxable fee items to the tax calculation pipeline
294 $fees = (array)Arr::get($checkoutData, 'fees', []);
295 $feeItems = [];
296 foreach ($fees as $fee) {
297 if (!empty($fee['taxable']) && !empty($fee['amount'])) {
298 $feeItems[] = Cart::buildFeeCartItem($fee);
299 }
300 }
301
302 $allItems = array_merge($lineItems, $feeItems);
303
304 $taxCalculator = new TaxCalculator($allItems, [
305 'inclusive' => false,
306 'country' => $country,
307 'state' => $state,
308 'city' => $city,
309 'postcode' => $postCode,
310 ]);
311
312 if (empty($checkoutData['tax_data'])) {
313 $checkoutData['tax_data'] = [];
314 }
315
316 $taxTotal = $taxCalculator->getTotalTax();
317 $shippingTax = $taxCalculator->getShippingTax();
318 $taxCountry = $taxCalculator->getTaxCountry();
319 $taxLines = $taxCalculator->getTaxLinesByRates();
320
321 // Separate product and fee items from taxed lines
322 $allTaxedLines = $taxCalculator->getTaxedLines();
323 $productLines = [];
324 $feeTax = 0;
325 foreach ($allTaxedLines as $taxedLine) {
326 if (!empty($taxedLine['is_fee'])) {
327 $feeTax += (int)Arr::get($taxedLine, 'tax_amount', 0);
328 } else {
329 $productLines[] = $taxedLine;
330 }
331 }
332
333 $checkoutData['tax_data']['tax_total'] = $taxTotal;
334 $checkoutData['tax_data']['tax_behavior'] = $taxCalculator->getTaxBahaviorValue();
335 $checkoutData['tax_data']['tax_country'] = $taxCountry;
336 $checkoutData['tax_data']['shipping_tax'] = $shippingTax;
337 $checkoutData['tax_data']['fee_tax'] = $feeTax;
338 $checkoutData['tax_data']['tax_lines'] = $taxLines;
339 $fillData['checkout_data'] = $checkoutData;
340
341 // Only product lines go back into cart_data
342 $fillData['cart_data'] = $productLines;
343
344 if (isset($fillData['hook_changes'])) {
345 Arr::set($fillData, 'hook_changes.tax', true);
346 }
347
348 return $fillData;
349 }
350
351 public function maybeRerenderEuVatField($fragments, $args)
352 {
353 $taxSettings = $this->getSettings();
354 $taxEnabled = Arr::get($taxSettings, 'enable_tax', 'no') === 'yes';
355 $euVatEnabled = Arr::get($taxSettings, 'eu_vat_settings.require_vat_number', 'no') === 'yes';
356
357 $cart = Arr::get($args, 'cart');
358
359 // all changes are relevant for tax , so not checking changes for now
360
361
362 if (!$taxEnabled || !$euVatEnabled) {
363 return $fragments;
364 }
365
366 $taxApplicableCountry = $this->getTaxApplicableCountry(Arr::get($this->taxSettings, 'tax_calculation_basis'), $cart->checkout_data['form_data']);
367
368 $euCountryCodes = LocalizationManager::getInstance()->taxContinents('EU');
369 $euCountryCodes = Arr::get($euCountryCodes, 'countries');
370 $isEuCountry = in_array($taxApplicableCountry, $euCountryCodes);
371
372 ob_start();
373 (new EUVatRenderer($isEuCountry, $taxApplicableCountry))->render($cart);
374 $euVatView = ob_get_clean();
375
376 $fragments[] = [
377 'selector' => '[data-fluent-cart-checkout-page-tax-wrapper]',
378 'content' => $euVatView,
379 'type' => 'replace'
380 ];
381
382 return $fragments;
383 }
384
385 public function prepareOtherData($data)
386 {
387 $cart = Arr::get($data, 'cart');
388 $order = Arr::get($data, 'order');
389
390 if (empty($cart->checkout_data['tax_data']) || !$order->id || !$cart) {
391 return;
392 }
393
394 $checkoutData = $cart->checkout_data;
395 $taxCountry = Arr::get($checkoutData, 'tax_data.tax_country', '');
396
397 // add store vat number into tax data
398 $taxSettings = $this->getSettings();
399 $isEuCountry = LocalizationManager::getInstance()->continentFromCountry($taxCountry ?? '') === 'EU';
400
401 $storeVatNumber = '';
402 if ($isEuCountry) {
403 $euVatMethod = Arr::get($taxSettings, 'eu_vat_settings.method');
404 if ($euVatMethod === 'home') {
405 $storeVatNumber = Arr::get($taxSettings, 'eu_vat_settings.home_vat', '');
406 } elseif ($euVatMethod === 'oss') {
407 $storeVatNumber = Arr::get($taxSettings, 'eu_vat_settings.oss_vat', '');
408 }
409 }
410 if (empty($storeVatNumber)) {
411 $key = 'fluent_cart_tax_id_' . $taxCountry;
412 $taxCountryData = \FluentCart\App\Models\Meta::query()->where('meta_key', $key)->where('object_type', 'tax')->first();
413 if ($taxCountryData) {
414 $storeVatNumber = Arr::get($taxCountryData->meta_value, 'tax_id', '');
415 }
416 }
417
418 $customerVatData = [];
419 if (Arr::get($cart->checkout_data, 'tax_data.valid', false)) {
420 $customerVatData = Arr::get($checkoutData, 'tax_data');
421 $order = $data['order'];
422 $order->customer->updateMeta('customer_tax_info',
423 Arr::only($customerVatData, ['vat_number', 'country', 'valid', 'name', 'address'])
424 );
425 }
426
427 foreach ($cart->cart_data as $item) {
428 $taxMeta = Arr::get($item, 'line_meta.tax_config.rates.0', []);
429 $rateId = Arr::get($taxMeta, 'rate_id');
430
431 if (!$rateId) {
432 $rateId = 0; // assume this rate is from JSON tax rate
433 }
434
435 $shippingTax = (int)Arr::get($taxMeta, 'shipping_tax', 0);
436 $taxAmount = (int)Arr::get($taxMeta, 'tax_amount', 0);
437 $taxMeta = Arr::get($item, 'line_meta.tax_config', []);
438 $taxMeta['tax_country'] = $taxCountry;
439 $taxMeta['store_vat_number'] = $storeVatNumber;
440
441 if (!empty($customerVatData)) {
442 $taxMeta['vat_reverse'] = $customerVatData;
443 $taxAmount = 0;
444 $shippingTax = 0;
445 }
446
447 $orderTaxRate = OrderTaxRate::query()->where('order_id', $order->id)
448 ->where('tax_rate_id', $rateId)
449 ->first();
450
451 if (!$orderTaxRate) {
452 $orderTaxRate = OrderTaxRate::create([
453 'order_id' => $order->id,
454 'tax_rate_id' => $rateId,
455 'shipping_tax' => $shippingTax,
456 'order_tax' => $taxAmount,
457 'total_tax' => $shippingTax + $taxAmount,
458 'meta' => $taxMeta
459 ]);
460 } else {
461 $orderTaxRate->update([
462 'shipping_tax' => $shippingTax,
463 'order_tax' => $taxAmount,
464 'total_tax' => $shippingTax + $taxAmount,
465 'meta' => $taxMeta
466 ]);
467 }
468 }
469 }
470
471
472 public function initCheckoutActions()
473 {
474 add_action('fluent_cart/before_payment_methods', [$this, 'renderTaxField'], 10, 1);
475 }
476
477 public function registerAjaxHandlers()
478 {
479 add_action('wp_ajax_fluent_cart_validate_vat', [$this, 'handleVatValidation']);
480 add_action('wp_ajax_nopriv_fluent_cart_validate_vat', [$this, 'handleVatValidation']);
481
482 add_action('wp_ajax_fluent_cart_remove_vat', [$this, 'removeVat']);
483 add_action('wp_ajax_nopriv_fluent_cart_remove_vat', [$this, 'removeVat']);
484 }
485
486 public function renderTaxField($data)
487 {
488 $cart = Arr::get($data, 'cart');
489
490 $checkoutData = $cart->checkout_data;
491 $vatNumber = Arr::get($checkoutData, 'tax_data.vat_number', '');
492
493 $euVatEnabled = Arr::get($this->taxSettings, 'eu_vat_settings.require_vat_number', 'no');
494 $taxApplicableCountry = $this->getTaxApplicableCountry(
495 Arr::get($this->taxSettings, 'tax_calculation_basis'),
496 Arr::get($checkoutData, 'form_data')
497 );
498
499 $isEuCountry = LocalizationManager::getInstance()->continentFromCountry($taxApplicableCountry) === 'EU';
500
501 $isEuVatRcAvailable = false;
502 if ($isEuCountry && $euVatEnabled === 'yes') {
503 $isEuVatRcAvailable = true;
504 }
505
506 ?>
507 <div class="fct_checkout_tax_wrapper" data-fluent-cart-checkout-page-tax-wrapper>
508 <?php if ($isEuVatRcAvailable): ?>
509 <div class="fct_checkout_form_section">
510 <div class="fct_form_section_header">
511 <h4 class="fct_form_section_header_label"><?php echo esc_html__('EU VAT', 'fluent-cart'); ?></h4>
512 </div>
513
514 <div class="fct_form_section_body">
515 <div class="fct_tax_field">
516 <div data-fluent-cart-checkout-page-form-input-wrapper class="fct_tax_input_wrapper"
517 id="fct_billing_tax_id_wrapper">
518 <input
519 data-fluent-cart-checkout-page-tax-id
520 type="text"
521 name="fct_billing_tax_id"
522 autocomplete="tax-id"
523 placeholder="<?php echo esc_html__('Enter Tax ID', 'fluent-cart'); ?>"
524 id="fct_billing_tax_id"
525 value="<?php echo esc_attr($vatNumber) ?? ''; ?>"
526 />
527
528 <button data-fluent-cart-checkout-page-tax-apply-btn>
529 <?php echo esc_html__('Apply', 'fluent-cart'); ?>
530 </button>
531 </div>
532 <span data-fluent-cart-checkout-page-tax-loading class="fct_tax_loading"></span>
533 <span data-fluent-cart-checkout-page-form-error class="fct_form_error"></span>
534 <?php $this->renderValidNote($checkoutData); ?>
535 </div>
536 </div>
537 </div>
538 <?php endif; ?>
539 </div>
540 <?php
541 }
542
543 public function getTaxApplicableCountry($calculationBasis, $formData)
544 {
545 $country = '';
546
547 $shipToDifferent = Arr::get($formData, 'ship_to_different', 'no') === 'yes';
548
549 if ($calculationBasis === 'store') {
550 $country = (new StoreSettings())->get('store_country') ?? '';
551 } else if ($calculationBasis === 'billing' || ($calculationBasis === 'shipping' && !$shipToDifferent)) {
552 $country = Arr::get($formData, 'billing_country') ?? '';
553 } else {
554 $country = Arr::get($formData, 'shipping_country') ?? '';
555 }
556 return $country;
557
558 }
559
560 public function renderValidNote($checkoutData)
561 {
562 $isValid = Arr::get($checkoutData, 'tax_data.valid', false);
563 ?>
564
565 <div class="fct_vat_valid_note <?php echo !$isValid ? 'is-hidden' : ''; ?>"
566 data-fluent-cart-tax-valid-note-wrapper>
567 <span data-fluent-cart-tax-valid-note>
568 <?php echo esc_html__('Valid VAT number', 'fluent-cart'); ?>
569 <?php $name = Arr::get($checkoutData, 'tax_data.name', '');
570 if ($name): ?>
571 — <?php echo esc_html($name); ?>
572 <?php endif; ?>
573 </span>
574
575 <?php if (Arr::get($checkoutData, 'tax_data.tax_total') != 0): ?>
576 <span class="ml-2" style="color: #ffa500;">
577 <?php echo esc_html__('(Reverse Charge not Applied)', 'fluent-cart'); ?>
578 </span>
579 <?php endif; ?>
580
581 <button data-fluent-cart-tax-remove-btn>
582 <?php echo esc_html__('Remove', 'fluent-cart'); ?>
583 </button>
584 </div>
585
586 <?php
587 }
588
589 public function handleVatValidation()
590 {
591 nocache_headers();
592
593 if (!isset($_REQUEST['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['_wpnonce'])), 'fluentcart')) {
594 wp_send_json(['message' => __('Security check failed', 'fluent-cart')], 403);
595 }
596
597 if (Arr::get($this->taxSettings, 'eu_vat_settings.require_vat_number', 'yes') !== 'yes') {
598 wp_send_json(['message' => __('EU VAT reverse charge is not enabled!', 'fluent-cart')], 422);
599 }
600
601 $cart = CartHelper::getCart();
602
603 $taxCalculationBasis = Arr::get($this->taxSettings, 'tax_calculation_basis');
604 $formData = Arr::get($cart->checkout_data, 'form_data', []);
605 $shipToDifferent = Arr::get($formData, 'ship_to_different', '');
606
607 if ($taxCalculationBasis === 'billing' || ($taxCalculationBasis === 'shipping' && $shipToDifferent !== 'yes')) {
608 $countryCode = Arr::get($formData, 'billing_country', '');
609 } elseif ($taxCalculationBasis === 'shipping') {
610 $countryCode = Arr::get($formData, 'shipping_country', '');
611 }
612
613 $vatNumber = isset($_REQUEST['vat_number']) ? sanitize_text_field(wp_unslash($_REQUEST['vat_number'])) : '';
614
615 $storeCountry = (new StoreSettings())->get('store_country');
616 if ($taxCalculationBasis === 'store') {
617 $countryCode = $storeCountry;
618 }
619
620 if (!$countryCode || !$vatNumber) {
621 wp_send_json(['message' => __('Missing required data', 'fluent-cart')], 422);
622 }
623
624 // check if the $vatNumber contains any EU country code prefix remove it and get only vat number
625 $euCountry = LocalizationManager::getInstance()->taxContinents('EU');
626 $euCountryCodes = Arr::get($euCountry, 'countries', []);
627
628 foreach ($euCountryCodes as $i => $code) {
629 if (strpos($vatNumber, $code) === 0) { // Check if $code is at the beginning of $vatNumber
630 $vatNumber = substr($vatNumber, strlen($code)); // Remove the prefix
631 break;
632 }
633 }
634
635 $taxData = $this->validateEuVatNumber($countryCode, $vatNumber);
636
637 if (is_wp_error($taxData)) {
638 wp_send_json(['message' => $taxData->get_error_message()], 422);
639 }
640
641 if (!Arr::get($taxData, 'valid')) {
642 wp_send_json(['message' => __('VAT number is not valid!', 'fluent-cart')], 422);
643 }
644
645 // if there is any excluded category in the cart, then don't apply VAT reverse charge
646 $excludedCategories = Arr::get($this->taxSettings, 'eu_vat_settings.vat_reverse_excluded_categories', []);
647 $productIds = array_column($cart->cart_data, 'post_id');
648 $productTerms = $this->getTermsByProductIds($productIds);
649
650 $isExcluded = false;
651 foreach ($productTerms as $productId => $terms) {
652 if (array_intersect($terms, $excludedCategories)) {
653 $isExcluded = true;
654 break;
655 }
656 }
657
658 //make existing tax zero if local reverse charge is enabled
659 if (!$isExcluded) {
660 if (Arr::get($this->taxSettings, 'eu_vat_settings.local_reverse_charge', 'yes') === 'yes'
661 || $countryCode !== $storeCountry) {
662 $taxData['tax_total'] = 0;
663 $taxData['shipping_tax'] = 0;
664 $taxData['tax_behavior'] = 0;
665 }
666 }
667
668 $checkoutData = $cart->checkout_data;
669 if (!isset($checkoutData['tax_data']) || !is_array($checkoutData['tax_data'])) {
670 $checkoutData['tax_data'] = [];
671 }
672 $checkoutData['tax_data'] = array_merge($checkoutData['tax_data'], $taxData);
673 $cart->checkout_data = $checkoutData;
674 $cart->update();
675
676 ob_start();
677 (new CartSummaryRender($cart))->render(false);
678 $cartSummaryInner = ob_get_clean();
679
680 ob_start();
681 (new EUVatRenderer(true, Arr::get($taxData, 'tax_country')))->render($cart);
682 $euVatView = ob_get_clean();
683
684 wp_send_json([
685 'success' => true,
686 'message' => __('VAT has been applied successfully', 'fluent-cart'),
687 'tax_data' => $taxData,
688 'fragments' => [
689 [
690 'selector' => '[data-fluent-cart-checkout-page-cart-items-wrapper]',
691 'content' => $cartSummaryInner,
692 'type' => 'replace'
693 ],
694 [
695 'selector' => '[data-fluent-cart-checkout-page-tax-wrapper]',
696 'content' => $euVatView,
697 'type' => 'replace'
698 ]
699 ],
700 ], 200);
701 }
702
703 public function removeVat()
704 {
705 nocache_headers();
706
707 if (!isset($_REQUEST['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['_wpnonce'])), 'fluentcart')) {
708 wp_send_json(['message' => __('Security check failed', 'fluent-cart')], 403);
709 }
710
711 if (isset($_REQUEST['fct_cart_hash'])) {
712 $cart = CartResource::get(['hash' => sanitize_text_field(wp_unslash($_REQUEST['fct_cart_hash']))]);
713 } else {
714 $cart = CartResource::get();
715 }
716
717 // recalculate tax amount
718 do_action('fluent_cart/checkout/cart_amount_updated', [
719 'cart' => $cart
720 ]);
721
722 $checkoutData = $cart->checkout_data;
723
724 // Reset VAT-related fields
725 if (isset($checkoutData)) {
726 unset($checkoutData['tax_data']['valid']);
727 unset($checkoutData['tax_data']['name']);
728 unset($checkoutData['tax_data']['address']);
729 unset($checkoutData['tax_data']['vat_number']);
730 unset($checkoutData['tax_data']['country']);
731 }
732
733 $cart->checkout_data = $checkoutData;
734 $cart->save();
735
736 ob_start();
737 (new CartSummaryRender($cart))->render(false);
738 $cartSummaryInner = ob_get_clean();
739
740 wp_send_json([
741 'success' => true,
742 'message' => __('VAT has been removed successfully', 'fluent-cart'),
743 'checkout_data' => [],
744 'fragments' => [
745 [
746 'selector' => '[data-fluent-cart-checkout-page-cart-items-wrapper]',
747 'content' => $cartSummaryInner,
748 'type' => 'replace'
749 ]
750 ]
751 ]);
752 }
753
754 protected function getTermsByProductIds($products)
755 {
756 $formattedTerms = null;
757
758 if ($formattedTerms === null) {
759 $terms = App::make('db')->table('term_relationships')
760 ->whereIn('object_id', $products)
761 ->get();
762
763 $formattedTerms = [];
764
765 foreach ($terms as $term) {
766 if (!isset($formattedTerms[$term->object_id])) {
767 $formattedTerms[$term->object_id] = [];
768 }
769 $formattedTerms[$term->object_id][] = $term->term_taxonomy_id;
770 }
771 }
772
773 return $formattedTerms;
774 }
775
776 protected function validateEuVatNumber($countryCode, $vatNumber)
777 {
778 try {
779 $wsdl = "https://ec.europa.eu/taxation_customs/vies/checkVatService.wsdl";
780
781 if (!class_exists('\\SoapClient')) {
782 throw new \Exception(__('SOAP is not available on the server.', 'fluent-cart'));
783 }
784
785 $client = new \SoapClient($wsdl, [
786 'exceptions' => true,
787 'trace' => true,
788 ]);
789
790 $params = [
791 'countryCode' => $countryCode,
792 'vatNumber' => preg_replace('/[^A-Za-z0-9]/', '', $vatNumber)
793 ];
794
795 $result = $client->checkVat($params);
796
797 if (empty($result->valid)) {
798 throw new \Exception(
799 sprintf(
800 /* translators: %s is the country code */
801 __('Invalid VAT number for country %s!', 'fluent-cart'),
802 $countryCode
803 )
804 );
805 }
806
807 $taxData = [
808 'country' => $result->countryCode,
809 'vat_number' => $result->vatNumber,
810 'valid' => (bool)$result->valid,
811 'name' => $result->name,
812 'address' => $result->address,
813 ];
814
815 return $taxData;
816
817 } catch (\SoapFault $e) {
818 return new \WP_Error('soap_fault', $e->getMessage());
819 } catch (\Exception $e) {
820 return new \WP_Error('invalid', $e->getMessage());
821 }
822 }
823
824 public static function isTaxEnabled()
825 {
826 $taxSettings = get_option('fluent_cart_tax_configuration_settings', []);
827 return Arr::get($taxSettings, 'enable_tax', 'no') === 'yes';
828 }
829
830 public static function euVatCountyOptions()
831 {
832 $eu_vat_countries = [
833 ['label' => 'Austria', 'value' => 'AT'],
834 ['label' => 'Belgium', 'value' => 'BE'],
835 ['label' => 'Bulgaria', 'value' => 'BG'],
836 ['label' => 'Croatia', 'value' => 'HR'],
837 ['label' => 'Cyprus', 'value' => 'CY'],
838 ['label' => 'Czech Republic', 'value' => 'CZ'],
839 ['label' => 'Denmark', 'value' => 'DK'],
840 ['label' => 'Estonia', 'value' => 'EE'],
841 ['label' => 'Finland', 'value' => 'FI'],
842 ['label' => 'France', 'value' => 'FR'],
843 ['label' => 'Germany', 'value' => 'DE'],
844 ['label' => 'Greece', 'value' => 'GR'],
845 ['label' => 'Hungary', 'value' => 'HU'],
846 ['label' => 'Ireland', 'value' => 'IE'],
847 ['label' => 'Italy', 'value' => 'IT'],
848 ['label' => 'Latvia', 'value' => 'LV'],
849 ['label' => 'Lithuania', 'value' => 'LT'],
850 ['label' => 'Luxembourg', 'value' => 'LU'],
851 ['label' => 'Malta', 'value' => 'MT'],
852 ['label' => 'Netherlands', 'value' => 'NL'],
853 ['label' => 'Poland', 'value' => 'PL'],
854 ['label' => 'Portugal', 'value' => 'PT'],
855 ['label' => 'Romania', 'value' => 'RO'],
856 ['label' => 'Slovakia', 'value' => 'SK'],
857 ['label' => 'Slovenia', 'value' => 'SI'],
858 ['label' => 'Spain', 'value' => 'ES'],
859 ['label' => 'Sweden', 'value' => 'SE'],
860 ];
861 return $eu_vat_countries;
862 }
863
864 public static function taxTitleLists(): array
865 {
866 return apply_filters('fluent_cart/tax/country_tax_titles', [
867 'AU' => __('ABN', 'fluent-cart'), // Australia
868 'NZ' => __('GST', 'fluent-cart'), // New Zealand
869 'IN' => __('GST', 'fluent-cart'), // India
870 'SG' => __('GST', 'fluent-cart'), // Singapore
871 'MY' => __('SST', 'fluent-cart'), // Malaysia
872 'CA' => __('GST / HST / PST / QST', 'fluent-cart'), // Canada
873 'GB' => __('VAT', 'fluent-cart'), // United Kingdom
874 'EU' => __('VAT', 'fluent-cart'), // European Union
875 'FR' => __('VAT', 'fluent-cart'), // France
876 'DE' => __('VAT', 'fluent-cart'), // Germany
877 'NL' => __('VAT', 'fluent-cart'), // Netherlands
878 'ES' => __('VAT', 'fluent-cart'), // Spain
879 'IT' => __('VAT', 'fluent-cart'), // Italy
880 'IE' => __('VAT', 'fluent-cart'), // Ireland
881 'US' => __('EIN / Sales Tax', 'fluent-cart'), // United States
882 'ZA' => __('VAT', 'fluent-cart'), // South Africa
883 'NG' => __('TIN / VAT', 'fluent-cart'), // Nigeria
884 'AE' => __('TRN / VAT', 'fluent-cart'), // United Arab Emirates
885 'SA' => __('VAT', 'fluent-cart'), // Saudi Arabia
886 'QA' => __('VAT', 'fluent-cart'), // Qatar
887 'JP' => __('Consumption Tax (CTN)', 'fluent-cart'), // Japan
888 'CN' => __('VAT', 'fluent-cart'), // China
889 'HK' => __('BRN', 'fluent-cart'), // Hong Kong
890 'PH' => __('TIN / VAT', 'fluent-cart'), // Philippines
891 'ID' => __('NPWP / PPN', 'fluent-cart'), // Indonesia
892 'TH' => __('VAT', 'fluent-cart'), // Thailand
893 'VN' => __('MST / VAT', 'fluent-cart'), // Vietnam
894 'BD' => __('BIN / VAT', 'fluent-cart'), // Bangladesh
895 'PK' => __('NTN / STRN', 'fluent-cart'), // Pakistan
896 'LK' => __('VAT', 'fluent-cart'), // Sri Lanka
897 'NP' => __('PAN / VAT', 'fluent-cart'), // Nepal
898 'BR' => __('CNPJ / CPF', 'fluent-cart'), // Brazil
899 'AR' => __('CUIT', 'fluent-cart'), // Argentina
900 'MX' => __('RFC / IVA', 'fluent-cart'), // Mexico
901 'CL' => __('RUT / IVA', 'fluent-cart'), // Chile
902 'PE' => __('RUC / IGV', 'fluent-cart'), // Peru
903 'RU' => __('INN / VAT', 'fluent-cart'), // Russia
904 'TR' => __('VKN / VAT', 'fluent-cart'), // Turkey
905 'CH' => __('MWST / TVA / IVA', 'fluent-cart'), // Switzerland
906 'NO' => __('VAT', 'fluent-cart'), // Norway
907 'IS' => __('VSK', 'fluent-cart'), // Iceland
908 'IL' => __('VAT', 'fluent-cart'), // Israel
909 'SE' => __('VAT', 'fluent-cart'), // Sweden
910 ]);
911
912 }
913
914 public static function getCountryTaxTitle($countryCode = '')
915 {
916 $countryTaxTitles = self::taxTitleLists();
917 if (isset($countryTaxTitles[$countryCode])) {
918 return $countryTaxTitles[$countryCode];
919 }
920 return __('VAT', 'fluent-cart');
921 }
922
923 }
924