PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.8.0
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.8.0
1.8.0 1.7.1 1.7.0 1.6.6 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 All 52 releases
← All changes | app/Modules/Tax/TaxModule.php +140 -7 1.6.2 → 1.8.0 View file →
@@ -155,12 +155,21 @@
155 155
156 156 add_action('fluent_cart/checkout/prepare_other_data', [$this, 'prepareOtherData'], 10, 1);
157 157
158 158 add_action('fluent_cart/product/after_price', function ($data) {
159 - if (Arr::get($data, 'scope') === 'price_range') {
159 + $variant = Arr::get($data, 'variant', null);
160 +
161 + // scope 'price_range' covers two cases: a single confirmed price
162 + // for a simple product (variant is set — resolve its suffix
163 + // normally) and the non-simple price summary (min/max, or a
164 + // single value when every variant is priced the same — variant
165 + // is always null there, deliberately skipped since no single
166 + // tax_inclusion applies to a multi-variant summary; see
167 + // PR #1767/#2271).
168 + if (Arr::get($data, 'scope') === 'price_range' && !$variant) {
160 169 return;
161 170 }
162 - $variant = Arr::get($data, 'variant', null);
171 +
163 172 $priceSuffix = $this->resolvePriceSuffix($variant);
164 173 $variantInclusion = $variant ? Arr::get($variant->other_info ?: [], 'tax_inclusion', '') : '';
165 174 $taxInclusion = $variantInclusion ?: Arr::get($this->taxSettings, 'tax_inclusion', '');
166 175
@@ -280,9 +289,9 @@
280 289
281 290 $fillData = $this->calculateCartTax([
282 291 'cart_data' => $cart->cart_data,
283 292 'checkout_data' => $checkoutData
284 - ]);
293 + ], $cart);
285 294
286 295 $cart->fill($fillData);
287 296 $cart->save();
288 297 }
@@ -334,9 +343,9 @@
334 343 'shipping_country' => Arr::get($data, 'prev_data.shipping_country', Arr::get($fillData, 'checkout_data.form_data.shipping_country', '')),
335 344 ]
336 345 );
337 346
338 - return $this->calculateCartTax($fillData);
347 + return $this->calculateCartTax($fillData, $cart);
339 348 }
340 349
341 350 public function maybeInvalidateVatValidationForCountryChange($checkoutData, $previousFormData = [])
342 351 {
@@ -446,9 +455,15 @@
446 455 $settings = $this->getSettings();
447 456 return Arr::get($settings, 'enable_tax', 'no') === 'yes';
448 457 }
449 458
450 - public function calculateCartTax($fillData)
459 + /**
460 + * @param array $fillData ['cart_data' => array, 'checkout_data' => array]
461 + * @param Cart|null $cart Pass it while cart_data holds no rule discounts yet, so tax
462 + * is worked out on the price after dynamic pricing rules.
463 + * @return array
464 + */
465 + public function calculateCartTax($fillData, $cart = null)
451 466 {
452 467 $lineItems = Arr::get($fillData, 'cart_data', []);
453 468 $checkoutData = Arr::get($fillData, 'checkout_data', []);
454 469
@@ -537,8 +552,13 @@
537 552 }
538 553 }
539 554 }
540 555
556 + $unpricedLineItems = $lineItems;
557 + if ($cart instanceof Cart) {
558 + $lineItems = $this->applyPricingRules($lineItems, $checkoutData, $cart);
559 + }
560 +
541 561 $country = '';
542 562 $state = '';
543 563 $postCode = '';
544 564
@@ -788,8 +808,12 @@
788 808 $checkoutData['tax_data']['signup_fee_tax'] = $signupFeeTaxTotal;
789 809 $checkoutData['tax_data']['tax_lines'] = $taxLines;
790 810 $fillData['checkout_data'] = $checkoutData;
791 811
812 + if ($cart instanceof Cart) {
813 + $productLines = $this->restoreUnpricedValues($productLines, $unpricedLineItems);
814 + }
815 +
792 816 // Only product lines go back into cart_data
793 817 $fillData['cart_data'] = $productLines;
794 818
795 819 if (isset($fillData['hook_changes'])) {
@@ -798,8 +822,117 @@
798 822
799 823 return $fillData;
800 824 }
801 825
826 + /**
827 + * The lines as the checkout prices them: rule discounts on the items and on each
828 + * line's shipping share. Uses the same hooks as the summary and placeOrder(), so
829 + * tax is charged on the amount the customer pays.
830 + *
831 + * @param array $lineItems
832 + * @param array $checkoutData
833 + * @param Cart $cart
834 + * @return array
835 + */
836 + private function applyPricingRules(array $lineItems, array $checkoutData, Cart $cart): array
837 + {
838 + // A copy, so rule conditions read the address being patched without touching the cart.
839 + $pricedCart = clone $cart;
840 + $pricedCart->cart_data = $lineItems;
841 + $pricedCart->checkout_data = $checkoutData;
842 +
843 + $priced = apply_filters('fluent_cart/cart/item_dynamic_discount', $lineItems, [
844 + 'cart' => $pricedCart,
845 + ]);
846 + $lineItems = is_array($priced) ? $priced : $lineItems;
847 +
848 + $shipping = 0;
849 + foreach ($lineItems as $lineItem) {
850 + $shipping += (int) Arr::get($lineItem, 'shipping_charge', 0)
851 + + (int) Arr::get($lineItem, 'itemwise_shipping_charge', 0);
852 + }
853 +
854 + if ($shipping <= 0) {
855 + return $lineItems;
856 + }
857 +
858 + $discounted = (int) apply_filters('fluent_cart/checkout/order_shipping_charge', $shipping, [
859 + 'cart' => $pricedCart,
860 + ]);
861 + $discounted = max(0, min($discounted, $shipping));
862 +
863 + if ($discounted === $shipping) {
864 + return $lineItems;
865 + }
866 +
867 + // Floor each share, then give the leftover cents to the largest remainders, so the
868 + // shares add up to exactly the discounted charge and none goes below zero.
869 + $left = $discounted;
870 + $remainders = [];
871 + foreach ($lineItems as $key => $lineItem) {
872 + foreach (['shipping_charge', 'itemwise_shipping_charge'] as $field) {
873 + $share = (int) Arr::get($lineItem, $field, 0);
874 + if ($share <= 0) {
875 + continue;
876 + }
877 + $scaled = intdiv($share * $discounted, $shipping);
878 + $lineItems[$key][$field] = $scaled;
879 + $left -= $scaled;
880 + $remainders[] = [$key, $field, ($share * $discounted) % $shipping];
881 + }
882 + }
883 +
884 + usort($remainders, function ($first, $second) {
885 + return $second[2] <=> $first[2];
886 + });
887 +
888 + foreach ($remainders as $remainder) {
889 + if ($left <= 0) {
890 + break;
891 + }
892 + $lineItems[$remainder[0]][$remainder[1]]++;
893 + $left--;
894 + }
895 +
896 + return $lineItems;
897 + }
898 +
899 + /**
900 + * Put back what applyPricingRules() changed, keeping the tax worked out on it.
901 + * cart_data must stay free of rule discounts: the summary applies them on every
902 + * render, and the order writes them in at before_order_processing.
903 + *
904 + * @param array $productLines Taxed lines, in the order of $unpricedLineItems.
905 + * @param array $unpricedLineItems
906 + * @return array
907 + */
908 + private function restoreUnpricedValues(array $productLines, array $unpricedLineItems): array
909 + {
910 + $unpricedLineItems = array_values($unpricedLineItems);
911 +
912 + foreach ($productLines as $index => $line) {
913 + if (!isset($unpricedLineItems[$index])) {
914 + continue;
915 + }
916 + $unpriced = $unpricedLineItems[$index];
917 +
918 + foreach (['manual_discount', 'discount_total', 'shipping_charge', 'itemwise_shipping_charge'] as $field) {
919 + if (array_key_exists($field, $unpriced)) {
920 + $line[$field] = $unpriced[$field];
921 + } else {
922 + unset($line[$field]);
923 + }
924 + }
925 +
926 + // From the taxed subtotal: dynamic reverse charge may have lowered it.
927 + $line['line_total'] = max(0, (int) Arr::get($line, 'subtotal', 0) - (int) Arr::get($line, 'discount_total', 0));
928 +
929 + $productLines[$index] = $line;
930 + }
931 +
932 + return $productLines;
933 + }
934 +
802 935 protected function shouldApplyReverseCharge($checkoutData, $taxApplicableCountry, $lineItems = [])
803 936 {
804 937 if (!Arr::get($checkoutData, 'tax_data.valid', false)) {
805 938 return false;
@@ -1299,9 +1432,9 @@
1299 1432 $cart->checkout_data = $checkoutData;
1300 1433 $fillData = $this->calculateCartTax([
1301 1434 'cart_data' => $cart->cart_data,
1302 1435 'checkout_data' => $cart->checkout_data,
1303 - ]);
1436 + ], $cart);
1304 1437 $cart->fill($fillData);
1305 1438 $cart->save();
1306 1439
1307 1440 ob_start();
@@ -1371,9 +1504,9 @@
1371 1504 $cart->checkout_data = $checkoutData;
1372 1505 $fillData = $this->calculateCartTax([
1373 1506 'cart_data' => $cart->cart_data,
1374 1507 'checkout_data' => $cart->checkout_data,
1375 - ]);
1508 + ], $cart);
1376 1509 $cart->fill($fillData);
1377 1510 $cart->save();
1378 1511
1379 1512 ob_start();