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 +129 -5 1.7.0 → 1.8.0 View file →
@@ -289,9 +289,9 @@
289 289
290 290 $fillData = $this->calculateCartTax([
291 291 'cart_data' => $cart->cart_data,
292 292 'checkout_data' => $checkoutData
293 - ]);
293 + ], $cart);
294 294
295 295 $cart->fill($fillData);
296 296 $cart->save();
297 297 }
@@ -343,9 +343,9 @@
343 343 'shipping_country' => Arr::get($data, 'prev_data.shipping_country', Arr::get($fillData, 'checkout_data.form_data.shipping_country', '')),
344 344 ]
345 345 );
346 346
347 - return $this->calculateCartTax($fillData);
347 + return $this->calculateCartTax($fillData, $cart);
348 348 }
349 349
350 350 public function maybeInvalidateVatValidationForCountryChange($checkoutData, $previousFormData = [])
351 351 {
@@ -455,9 +455,15 @@
455 455 $settings = $this->getSettings();
456 456 return Arr::get($settings, 'enable_tax', 'no') === 'yes';
457 457 }
458 458
459 - 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)
460 466 {
461 467 $lineItems = Arr::get($fillData, 'cart_data', []);
462 468 $checkoutData = Arr::get($fillData, 'checkout_data', []);
463 469
@@ -546,8 +552,13 @@
546 552 }
547 553 }
548 554 }
549 555
556 + $unpricedLineItems = $lineItems;
557 + if ($cart instanceof Cart) {
558 + $lineItems = $this->applyPricingRules($lineItems, $checkoutData, $cart);
559 + }
560 +
550 561 $country = '';
551 562 $state = '';
552 563 $postCode = '';
553 564
@@ -797,8 +808,12 @@
797 808 $checkoutData['tax_data']['signup_fee_tax'] = $signupFeeTaxTotal;
798 809 $checkoutData['tax_data']['tax_lines'] = $taxLines;
799 810 $fillData['checkout_data'] = $checkoutData;
800 811
812 + if ($cart instanceof Cart) {
813 + $productLines = $this->restoreUnpricedValues($productLines, $unpricedLineItems);
814 + }
815 +
801 816 // Only product lines go back into cart_data
802 817 $fillData['cart_data'] = $productLines;
803 818
804 819 if (isset($fillData['hook_changes'])) {
@@ -807,8 +822,117 @@
807 822
808 823 return $fillData;
809 824 }
810 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 +
811 935 protected function shouldApplyReverseCharge($checkoutData, $taxApplicableCountry, $lineItems = [])
812 936 {
813 937 if (!Arr::get($checkoutData, 'tax_data.valid', false)) {
814 938 return false;
@@ -1308,9 +1432,9 @@
1308 1432 $cart->checkout_data = $checkoutData;
1309 1433 $fillData = $this->calculateCartTax([
1310 1434 'cart_data' => $cart->cart_data,
1311 1435 'checkout_data' => $cart->checkout_data,
1312 - ]);
1436 + ], $cart);
1313 1437 $cart->fill($fillData);
1314 1438 $cart->save();
1315 1439
1316 1440 ob_start();
@@ -1380,9 +1504,9 @@
1380 1504 $cart->checkout_data = $checkoutData;
1381 1505 $fillData = $this->calculateCartTax([
1382 1506 'cart_data' => $cart->cart_data,
1383 1507 'checkout_data' => $cart->checkout_data,
1384 - ]);
1508 + ], $cart);
1385 1509 $cart->fill($fillData);
1386 1510 $cart->save();
1387 1511
1388 1512 ob_start();