PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.3.26
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.3.26
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 / Helpers / CartHelper.php

CartHelper.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.3.26, at app/Helpers/CartHelper.php

812 lines 31.3 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\Helpers;
4
5 use FluentCart\Api\CurrencySettings;
6 use FluentCart\Api\Resource\FrontendResource\CartResource;
7 use FluentCart\App\App;
8 use FluentCart\App\Models\Cart;
9 use FluentCart\App\Models\Customer;
10 use FluentCart\App\Models\Product;
11 use FluentCart\App\Models\ProductVariation;
12 use FluentCart\App\Models\ShippingClass;
13 use FluentCart\App\Models\ShippingMethod;
14 use FluentCart\App\Services\CheckoutService;
15 use FluentCart\App\Services\URL;
16 use FluentCart\Framework\Support\Arr;
17
18 class CartHelper
19 {
20 public static function getCart($hash = null, $create = false)
21 {
22 return CartResource::get([
23 'hash' => $hash ?? App::request()->get(Helper::INSTANT_CHECKOUT_URL_PARAM),
24 'create' => $create
25 ]);
26 }
27
28 public static function generateCartItemFromVariation(ProductVariation $variation, $quantity = 1): array
29 {
30 $mediaUrl = $variation->thumbnail ?: $variation->product->thumbnail;
31
32 // $shippingCharge = static::calculateShippingCharge($variation, $quantity);
33
34 $itemPrice = apply_filters('fluent_cart/cart/item_price', $variation->item_price, [
35 'variation' => $variation,
36 'quantity' => $quantity,
37 ]);
38
39 // Ensure filtered price is a valid non-negative integer (cents)
40 $itemPrice = max(0, (int)$itemPrice);
41
42 $subtotal = $itemPrice * $quantity;
43
44 //Need to test and check this toArray Issue
45 $data = wp_parse_args([
46 'quantity' => $quantity,
47 'price' => $itemPrice,
48 'unit_price' => $itemPrice,
49 'line_total' => $itemPrice * $quantity,
50 'subtotal' => $subtotal,
51 'discount_total' => 0,
52 'tax_total' => 0,
53 'line_total_formatted' => CurrencySettings::getFormattedPrice($subtotal),
54 'object_id' => $variation->id,
55 'title' => $variation->variation_title,
56 'post_title' => $variation->product->post_title,
57 'coupon_discount' => 0,
58 'cost' => $variation->item_cost ?? 0,
59 'featured_media' => $mediaUrl,
60 'view_url' => URL::appendQueryParams(
61 $variation->product->view_url,
62 [
63 'selected' => $variation->id
64 ]
65 ),
66 'variation_type' => $variation['product_detail']['variation_type'],
67 'is_custom' => false,
68 ], $variation->toArray());
69
70 $cartItem = Arr::only($data, [
71 'id',
72 'object_id',
73 'post_id',
74 'quantity',
75 'post_title',
76 'title',
77 'price',
78 'unit_price',
79 'coupon_discount',
80 'fulfillment_type',
81 'featured_media',
82 'other_info',
83 'cost',
84 'view_url',
85 'line_total_formatted',
86 'line_total',
87 'subtotal',
88 'total',
89 'variation_type',
90 'is_custom'
91 ]);
92
93 // $cartItem['shipping_charge'] = $shippingCharge;
94
95 return $cartItem;
96 }
97
98 public static function generateCartItemCustomItem(array $variation, $quantity = 1): array
99 {
100 //Need to test and check this toArray Issue
101 $data = wp_parse_args(
102 [
103 'quantity' => $quantity,
104 'price' => Arr::get($variation, 'item_price'),
105 'unit_price' => Arr::get($variation, 'item_price'),
106 'object_id' => Arr::get($variation, 'id'),
107 'tax_amount' => Arr::get($variation, 'tax_amount', 0),
108 'title' => Arr::get($variation, 'variation_title'),
109 'post_title' => Arr::get($variation, 'post_title'),
110 'cost' => Arr::get($variation, 'item_cost', 0),
111 'featured_media' => Arr::get($variation, 'featured_media'),
112 'view_url' => Arr::get($variation, 'view_url'),
113 'variation_type' => Arr::get($variation, 'variation_type'),
114 'is_custom' => Arr::get($variation, 'is_custom', false),
115 ],
116 $variation
117 );
118
119 $manualDiscount = Arr::get($data, 'manual_discount', 0);
120 $couponDiscount = Arr::get($data, 'coupon_discount', 0);
121 $discountTotal = $manualDiscount + $couponDiscount;
122 $itemPrice = Arr::get($data, 'item_price', 0);
123 if (!is_numeric($itemPrice)) {
124 $itemPrice = 0;
125 }
126 $subtotal = $itemPrice * Arr::get($data, 'quantity');
127
128 $data['subtotal'] = $subtotal;
129 $data['manual_discount'] = $manualDiscount;
130 $data['coupon_discount'] = $couponDiscount;
131 $data['discount_total'] = $discountTotal;
132 $data['line_total'] = $subtotal - $discountTotal;
133
134 $cartItem = Arr::only($data, [
135 'id',
136 'object_id',
137 'post_id',
138 'quantity',
139 'post_title',
140 'title',
141 'price',
142 'unit_price',
143 'manual_discount',
144 'coupon_discount',
145 'discount_total',
146 'fulfillment_type',
147 'featured_media',
148 'other_info',
149 'cost',
150 'view_url',
151 'line_total',
152 'subtotal',
153 'total',
154 'variation_type',
155 'is_custom'
156 ]);
157
158 return $cartItem;
159 }
160
161 public static function calculateShippingCharge(ProductVariation $variation, int $quantity = 1)
162 {
163
164 if ($variation->fulfillment_type !== 'physical') {
165 return 0;
166 }
167
168 $shippingClass = $variation->shippingClass;
169
170 if (!$shippingClass) {
171 return 0;
172 }
173
174 $factor = empty($shippingClass->per_item) ? 1 : $quantity;
175 if ($shippingClass->type === 'percentage') {
176 return ($shippingClass->cost / 100) * $variation->item_price * $factor;
177 }
178
179 return ($shippingClass->cost * 100) * $factor;
180 }
181
182 public static function calculateShippingMethodCharge(ShippingMethod $method, ?array $items = null, $returnType = 'amount')
183 {
184 static $onceCalculated = false;
185 static $onceDistributed = false;
186 static $totalItemPrice = 0;
187 static $totalQuantity = 0;
188 static $physicalItems = [];
189 static $isAllDigital = false;
190 static $maxShippingCharge = 0;
191 static $totalShippingCharge = 0;
192 static $lastMethodId = null;
193 $isUsingCart = false;
194
195 // Reset statics when called with a different method to prevent stale state
196 if ($lastMethodId !== $method->id) {
197 $onceCalculated = false;
198 $onceDistributed = false;
199 $totalItemPrice = 0;
200 $totalQuantity = 0;
201 $physicalItems = [];
202 $isAllDigital = false;
203 $maxShippingCharge = 0;
204 $totalShippingCharge = 0;
205 $lastMethodId = $method->id;
206 }
207
208 if ($items === null) {
209 $isUsingCart = true;
210 $items = static::getCart()->cart_data ?? [];
211 }
212
213 if ($method->type === 'free_shipping') {
214 if ($returnType === 'items') {
215 if ($items === null) {
216 $items = static::getCart()->cart_data ?? [];
217 }
218 foreach ($items as $key => $item) {
219 $items[$key]['shipping_charge'] = 0;
220 $items[$key]['itemwise_shipping_charge'] = 0;
221 }
222 return [
223 'items' => $items,
224 'shipping_amount' => 0
225 ];
226 }
227 return 0;
228 }
229
230 $totalItemWiseShippingCharge = 0;
231
232 $cartCheckoutService = new CheckoutService($items);
233 $isAllDigital = $cartCheckoutService->isAllDigital();
234 $physicalItems = $cartCheckoutService->physicalItems;
235
236 if (!$onceCalculated) {
237 $onceCalculated = true;
238 $productIds = array_unique(array_column($physicalItems, 'post_id'));
239 $products = Product::query()->whereIn('ID', $productIds)
240 ->with(['detail'])
241 ->get()
242 ->keyBy('ID');
243
244 $shippingClassIds = $products->pluck('detail.other_info.shipping_class')->filter(function ($item) {
245 return !empty($item);
246 })->toArray();
247
248 $shippingClasses = ShippingClass::query()->whereIn('id', $shippingClassIds)->get()->keyBy('id');
249
250 foreach ($physicalItems as $key => &$item) {
251 $totalQuantity += Arr::get($item, 'quantity');
252 $totalItemPrice += (Arr::get($item, 'quantity') * Arr::get($item, 'unit_price')) - Arr::get($item, 'discount_total');
253 $itemShippingCharge = 0;
254
255 $product = $products->get(Arr::get($item, 'post_id'));
256
257
258 if (isset($product->detail->other_info['shipping_class'])) {
259 // shipping_class is null or not defined
260 $shippingClass = $shippingClasses->get(
261 $product->detail->other_info['shipping_class']
262 );
263
264 if ($shippingClass) {
265 $perItem = $shippingClass->per_item;
266 $factor = empty($perItem) ? 1 : Arr::get($item, 'quantity');
267 if ($shippingClass->type === 'percentage') {
268 $itemShippingCharge = ($shippingClass->cost / 100) * Arr::get($item, 'unit_price') * $factor;
269 } else {
270 $itemShippingCharge = Helper::toCent($shippingClass->cost) * $factor;
271 }
272 }
273 }
274 $item['shipping_charge'] = $itemShippingCharge;
275 $totalShippingCharge += $itemShippingCharge;
276
277 $items[$key] = $item;
278 $maxShippingCharge = max($maxShippingCharge, $itemShippingCharge);
279 }
280
281 $totalItemWiseShippingCharge = $totalShippingCharge;
282 }
283
284 if ($isAllDigital) {
285 return 0;
286 }
287
288 $settings = Arr::wrap($method->settings);
289 $configureRate = Arr::get($settings, 'configure_rate', 'per_order');
290 $classAggregation = Arr::get($settings, 'class_aggregation', 'sum_all');
291
292 if ($configureRate === 'per_order') {
293 $shippingMethodAmount = $method->amount * 100;
294 } else if ($configureRate === 'per_price') {
295 $shippingMethodAmount = $totalItemPrice * ($method->amount / 100);
296 } else if ($configureRate === 'per_weight') {
297 // Sum (product weight + package weight) * quantity for all physical items
298 $storeWeightUnit = Helper::shopConfig('weight_unit') ?: 'kg';
299 $totalWeight = 0;
300
301 // Batch-load all variations to avoid N+1
302 $variationIds = array_filter(array_map(function ($item) {
303 return Arr::get($item, 'object_id', Arr::get($item, 'variation_id'));
304 }, $physicalItems));
305 $variationsMap = $variationIds ? ProductVariation::query()->whereIn('id', $variationIds)->get()->keyBy('id') : new \FluentCart\Framework\Support\Collection();
306
307 foreach ($physicalItems as $item) {
308 $variationId = Arr::get($item, 'object_id', Arr::get($item, 'variation_id'));
309 if ($variationId) {
310 $variation = $variationsMap->get($variationId);
311 if ($variation) {
312 $otherInfo = $variation->other_info ?: [];
313 $productWeight = floatval(Arr::get($otherInfo, 'weight', 0));
314 $productWeightUnit = Arr::get($otherInfo, 'weight_unit', $storeWeightUnit);
315
316 // Convert product weight to store unit
317 $convertedProductWeight = Helper::convertWeight($productWeight, $productWeightUnit, $storeWeightUnit);
318
319 // Add package weight
320 $packageSlug = Arr::get($otherInfo, 'package_slug', '');
321 $package = Helper::getPackageBySlug($packageSlug);
322 $packageWeight = 0;
323 if ($package) {
324 $packageWeightUnit = Arr::get($package, 'weight_unit', $storeWeightUnit);
325 $packageWeight = Helper::convertWeight(
326 floatval(Arr::get($package, 'weight', 0)),
327 $packageWeightUnit,
328 $storeWeightUnit
329 );
330 }
331
332 $totalWeight += ($convertedProductWeight + $packageWeight) * Arr::get($item, 'quantity', 1);
333 }
334 }
335 }
336
337 // Look up matching tier from weight_tiers
338 $weightTiers = Arr::get($settings, 'weight_tiers', []);
339 $shippingMethodAmount = 0;
340 foreach ($weightTiers as $tier) {
341 $min = floatval(Arr::get($tier, 'min', 0));
342 $max = floatval(Arr::get($tier, 'max', 0));
343 $cost = floatval(Arr::get($tier, 'cost', 0));
344
345 if ($totalWeight >= $min && ($max <= 0 || $totalWeight <= $max)) {
346 $shippingMethodAmount = Helper::toCent($cost);
347 break;
348 }
349 }
350 } else {
351 $shippingMethodAmount = $method->amount * $totalQuantity * 100;
352 }
353
354 if ($classAggregation === 'highest_class') {
355 $shippingMethodAmount += $maxShippingCharge;
356 } else {
357 $shippingMethodAmount += $totalShippingCharge;
358 }
359
360 $remainingShippingMethodAmount = ($shippingMethodAmount - $totalItemWiseShippingCharge);
361
362 if (!$onceDistributed) {
363 $onceDistributed = true;
364 $totalLineTotal = array_sum(array_column($physicalItems, 'line_total'));
365 $distributed = 0;
366 $totalRemain = $remainingShippingMethodAmount;
367 $itemCount = count($physicalItems);
368
369 if ($totalLineTotal > 0) {
370 foreach ($physicalItems as $key => &$item) {
371 $share = ($item['line_total'] / $totalLineTotal) * $remainingShippingMethodAmount;
372 $share = round($share, 2);
373 $items[$key]['itemwise_shipping_charge'] = ceil($share);
374 $distributed += $share;
375 }
376 } else {
377 $equalShare = round($remainingShippingMethodAmount / $itemCount, 2);
378 foreach ($physicalItems as $key => &$item) {
379 $items[$key]['itemwise_shipping_charge'] = ceil($equalShare);
380 $distributed += $equalShare;
381 }
382 }
383
384 $diff = round($totalRemain - $distributed, 2);
385 if ($diff != 0) {
386 $lastIndex = array_key_last($physicalItems);
387 $items[$lastIndex]['itemwise_shipping_charge'] = ceil($diff);
388 }
389 }
390
391 if ($isUsingCart) {
392 $cart = CartHelper::getCart();
393 $cart->cart_data = $items;
394 $cart->save();
395
396 do_action('fluent_cart/checkout/shipping_data_changed', [
397 'cart' => $cart
398 ]);
399 }
400
401 if ($returnType === 'items') {
402 return [
403 'items' => $items,
404 'shipping_amount' => $shippingMethodAmount
405 ];
406 }
407
408 return $shippingMethodAmount;
409 }
410
411 /**
412 * Calculate shipping charges using the profile-based approach.
413 * Groups cart items by shipping class, finds applicable methods per profile,
414 * falls back to General zones when no class-specific zones exist.
415 *
416 * @param int $shippingMethodId The selected shipping method ID
417 * @param array $cartItems Cart items
418 * @param string $country Country code
419 * @param string|null $state State code
420 * @param string $returnType 'amount' or 'items'
421 * @return int|array
422 */
423 public static function calculateShippingByProfile($shippingMethodId, $cartItems, $country, $state = null, $returnType = 'amount')
424 {
425 $cartCheckoutService = new CheckoutService($cartItems);
426
427 if ($cartCheckoutService->isAllDigital()) {
428 if ($returnType === 'items') {
429 foreach ($cartItems as $key => $item) {
430 $cartItems[$key]['shipping_charge'] = 0;
431 $cartItems[$key]['itemwise_shipping_charge'] = 0;
432 }
433 return ['items' => $cartItems, 'shipping_amount' => 0];
434 }
435 return 0;
436 }
437
438 $physicalItems = $cartCheckoutService->physicalItems;
439
440 // Load products with details
441 $productIds = array_unique(array_column($physicalItems, 'post_id'));
442 $products = Product::query()->whereIn('ID', $productIds)
443 ->with(['detail'])
444 ->get()
445 ->keyBy('ID');
446
447 // Group physical items by shipping_class_id (null = General group)
448 $groups = [];
449 foreach ($physicalItems as $key => $item) {
450 $product = $products->get(Arr::get($item, 'post_id'));
451 $classId = null;
452 if ($product && isset($product->detail->other_info['shipping_class'])) {
453 $classId = $product->detail->other_info['shipping_class'] ?: null;
454 }
455 $groupKey = $classId ?: 'general';
456 if (!isset($groups[$groupKey])) {
457 $groups[$groupKey] = [
458 'class_id' => $classId,
459 'items' => [],
460 'keys' => []
461 ];
462 }
463 $groups[$groupKey]['items'][] = $item;
464 $groups[$groupKey]['keys'][] = $key;
465 }
466
467 // Load shipping classes for surcharge calculation
468 $classIds = array_filter(array_column($groups, 'class_id'));
469 $shippingClasses = !empty($classIds)
470 ? ShippingClass::query()->whereIn('id', $classIds)->get()->keyBy('id')
471 : new \FluentCart\Framework\Support\Collection();
472
473 $selectedMethod = ShippingMethod::find($shippingMethodId);
474 if (!$selectedMethod) {
475 if ($returnType === 'items') {
476 return ['items' => $cartItems, 'shipping_amount' => 0];
477 }
478 return 0;
479 }
480
481 // Early return for free_shipping — no base rate, no class surcharges
482 if ($selectedMethod->type === 'free_shipping') {
483 if ($returnType === 'items') {
484 foreach ($cartItems as $key => $item) {
485 $cartItems[$key]['shipping_charge'] = 0;
486 $cartItems[$key]['itemwise_shipping_charge'] = 0;
487 }
488 return ['items' => $cartItems, 'shipping_amount' => 0];
489 }
490 return 0;
491 }
492
493 $totalShippingAmount = 0;
494
495 // Preload all variations for weight calculation (avoids N+1 per group)
496 $allVariationIds = [];
497 foreach ($groups as $group) {
498 foreach ($group['items'] as $item) {
499 $vId = Arr::get($item, 'object_id', Arr::get($item, 'variation_id'));
500 if ($vId) $allVariationIds[] = $vId;
501 }
502 }
503 $allVariationIds = array_unique(array_filter($allVariationIds));
504 $allVariationsMap = !empty($allVariationIds)
505 ? ProductVariation::query()->whereIn('id', $allVariationIds)->get()->keyBy('id')
506 : new \FluentCart\Framework\Support\Collection();
507
508 // Compute cart-wide totals BEFORE the group loop (for per_order/per_price/per_weight base rate)
509 $cartTotalPrice = 0;
510 $cartTotalQuantity = 0;
511 foreach ($physicalItems as $item) {
512 $quantity = Arr::get($item, 'quantity', 1);
513 $cartTotalQuantity += $quantity;
514 $cartTotalPrice += ($quantity * Arr::get($item, 'unit_price', 0)) - Arr::get($item, 'discount_total', 0);
515 }
516
517 // Calculate the method base rate ONCE using cart-wide totals
518 $settings = Arr::wrap($selectedMethod->settings);
519 $configureRate = Arr::get($settings, 'configure_rate', 'per_order');
520 $classAggregation = Arr::get($settings, 'class_aggregation', 'sum_all');
521
522 if ($configureRate === 'per_order') {
523 $methodBaseRate = Helper::toCent($selectedMethod->amount);
524 } elseif ($configureRate === 'per_price') {
525 $methodBaseRate = $cartTotalPrice * ($selectedMethod->amount / 100);
526 } elseif ($configureRate === 'per_weight') {
527 $storeWeightUnit = Helper::shopConfig('weight_unit') ?: 'kg';
528 $totalWeight = 0;
529
530 foreach ($physicalItems as $item) {
531 $varId = Arr::get($item, 'object_id', Arr::get($item, 'variation_id'));
532 $variation = $varId ? $allVariationsMap->get($varId) : null;
533 if ($variation) {
534 $otherInfo = $variation->other_info ?: [];
535 $productWeight = floatval(Arr::get($otherInfo, 'weight', 0));
536 $productWeightUnit = Arr::get($otherInfo, 'weight_unit', $storeWeightUnit);
537 $convertedProductWeight = Helper::convertWeight($productWeight, $productWeightUnit, $storeWeightUnit);
538
539 $packageSlug = Arr::get($otherInfo, 'package_slug', '');
540 $package = Helper::getPackageBySlug($packageSlug);
541 $packageWeight = 0;
542 if ($package) {
543 $packageWeightUnit = Arr::get($package, 'weight_unit', $storeWeightUnit);
544 $packageWeight = Helper::convertWeight(
545 floatval(Arr::get($package, 'weight', 0)),
546 $packageWeightUnit,
547 $storeWeightUnit
548 );
549 }
550
551 $totalWeight += ($convertedProductWeight + $packageWeight) * Arr::get($item, 'quantity', 1);
552 }
553 }
554
555 $weightTiers = Arr::get($settings, 'weight_tiers', []);
556 $methodBaseRate = 0;
557 foreach ($weightTiers as $tier) {
558 $min = floatval(Arr::get($tier, 'min', 0));
559 $max = floatval(Arr::get($tier, 'max', 0));
560 if ($totalWeight >= $min && ($max <= 0 || $totalWeight <= $max)) {
561 $methodBaseRate = Helper::toCent(floatval(Arr::get($tier, 'cost', 0)));
562 break;
563 }
564 }
565 } else {
566 // per_item
567 $methodBaseRate = Helper::toCent($selectedMethod->amount) * $cartTotalQuantity;
568 }
569
570 // Accumulate class surcharges across all groups
571 $allGroupsClassCharge = 0;
572 $allGroupsMaxClassCharge = 0;
573
574 foreach ($groups as $groupKey => &$group) {
575 $classId = $group['class_id'];
576 $groupItems = $group['items'];
577
578 // Calculate class surcharges for this group
579 $groupTotalClassCharge = 0;
580 $groupMaxClassCharge = 0;
581
582 foreach ($groupItems as &$gItem) {
583 $quantity = Arr::get($gItem, 'quantity', 1);
584
585 // Calculate class surcharge per item
586 $itemClassCharge = 0;
587 if ($classId && $shippingClasses->has($classId)) {
588 $sc = $shippingClasses->get($classId);
589 $factor = $sc->per_item ? $quantity : 1;
590 if ($sc->type === 'percentage') {
591 $itemClassCharge = ($sc->cost / 100) * Arr::get($gItem, 'unit_price', 0) * $factor;
592 } else {
593 $itemClassCharge = Helper::toCent($sc->cost) * $factor;
594 }
595 }
596
597 $gItem['shipping_charge'] = $itemClassCharge;
598 $groupTotalClassCharge += $itemClassCharge;
599 $groupMaxClassCharge = max($groupMaxClassCharge, $itemClassCharge);
600 }
601 unset($gItem);
602
603 $allGroupsClassCharge += $groupTotalClassCharge;
604 $allGroupsMaxClassCharge = max($allGroupsMaxClassCharge, $groupMaxClassCharge);
605
606 $group['items'] = $groupItems;
607 $group['class_charge'] = $groupTotalClassCharge;
608 }
609 unset($group);
610
611 // Compute total: base rate (once) + class surcharges
612 if ($classAggregation === 'highest_class') {
613 $totalShippingAmount = $methodBaseRate + $allGroupsMaxClassCharge;
614 } else {
615 $totalShippingAmount = $methodBaseRate + $allGroupsClassCharge;
616 }
617
618 // Distribute the total shipping amount across all physical items proportionally
619 $totalLineTotal = 0;
620 foreach ($physicalItems as $item) {
621 $totalLineTotal += Arr::get($item, 'line_total', 0);
622 }
623
624 $methodOnlyAmount = $methodBaseRate;
625 $distributed = 0;
626 $itemCount = count($physicalItems);
627 foreach ($groups as $groupKey => &$group) {
628 $groupItems = $group['items'];
629 foreach ($groupItems as $idx => &$gItem) {
630 if ($totalLineTotal > 0) {
631 $share = (Arr::get($gItem, 'line_total', 0) / $totalLineTotal) * $methodOnlyAmount;
632 } else {
633 $share = $itemCount > 0 ? ($methodOnlyAmount / $itemCount) : 0;
634 }
635 $share = round($share, 2);
636 $gItem['itemwise_shipping_charge'] = ceil($share) + Arr::get($gItem, 'shipping_charge', 0);
637 $distributed += $share;
638 }
639 unset($gItem);
640 $group['items'] = $groupItems;
641 $group['amount'] = $group['class_charge'];
642 }
643 unset($group);
644
645 // Correct rounding difference on last physical item
646 $diff = round($methodOnlyAmount - $distributed, 2);
647 if ($diff != 0) {
648 $lastGroupKey = array_key_last($groups);
649 if ($lastGroupKey !== null && !empty($groups[$lastGroupKey]['items'])) {
650 $lastItemIdx = array_key_last($groups[$lastGroupKey]['items']);
651 $groups[$lastGroupKey]['items'][$lastItemIdx]['itemwise_shipping_charge'] += ceil($diff);
652 }
653 }
654
655 // Merge group items back into cartItems
656 foreach ($groups as $group) {
657 foreach ($group['keys'] as $i => $key) {
658 if (isset($group['items'][$i])) {
659 $cartItems[$key]['shipping_charge'] = Arr::get($group['items'][$i], 'shipping_charge', 0);
660 $cartItems[$key]['itemwise_shipping_charge'] = Arr::get($group['items'][$i], 'itemwise_shipping_charge', 0);
661 }
662 }
663 }
664
665 if ($returnType === 'items') {
666 return [
667 'items' => $cartItems,
668 'shipping_amount' => $totalShippingAmount
669 ];
670 }
671
672 return $totalShippingAmount;
673 }
674
675 public static function resetShippingCharge()
676 {
677 $cart = CartHelper::getCart();
678 $items = $cart->cart_data;
679 foreach ($items as $key => $item) {
680 $items[$key]['shipping_charge'] = 0;
681 $items[$key]['itemwise_shipping_charge'] = 0;
682 }
683 $cart->cart_data = $items;
684
685 $cart->checkout_data = array_merge($cart->checkout_data, [
686 'shipping_data' => [
687 'shipping_method_id' => null,
688 'shipping_charge' => 0
689 ]
690 ]);
691
692 $cart->save();
693
694 do_action('fluent_cart/checkout/shipping_data_changed', [
695 'cart' => $cart
696 ]);
697 }
698
699 public static function generateCartFromVariation(ProductVariation $variation, $quantity = 1): Cart
700 {
701 $cart = new Cart();
702 $cart->cart_data = [
703 static::generateCartItemFromVariation($variation, $quantity)
704 ];
705
706 $cart = static::addCommonCartData($cart);
707 return $cart;
708 }
709
710 public static function addCommonCartData(Cart $cart)
711 {
712 if (is_user_logged_in()) {
713 $wpUser = wp_get_current_user();
714 $cart->user_id = get_current_user_id();
715 $customer = Customer::query()->where('email', wp_get_current_user()->user_email)->first();
716 if ($customer) {
717 $cart->customer_id = $customer->id;
718 }
719 $cart->email = $wpUser->user_email;
720 $cart->first_name = $wpUser->first_name;
721 $cart->last_name = $wpUser->last_name;
722 $cart->ip_address = AddressHelper::getIpAddress();
723 $cart->user_agent = AddressHelper::getUserAgent();
724 }
725
726 return $cart;
727 }
728
729 public static function generateCartFromCustomVariation(array $variation, $quantity = 1): Cart
730 {
731 $cart = new Cart();
732 $cart->cart_data = [
733 static::generateCartItemCustomItem($variation, $quantity)
734 ];
735 return $cart;
736 }
737
738 /**
739 * Normalize custom item fields to standard cart variation format.
740 *
741 * NOTE:
742 * - Custom items may originate from external sources (filters, adjustments, migrations)
743 * - Some sources provide `id`, others only provide `item_id`
744 * - For cart consistency, `id` is required and will fall back to `item_id` when missing
745 * - This method intentionally mutates the provided object to normalize field names.
746 * The variation object is treated as a transient data structure and is not reused
747 * elsewhere after normalization.
748 *
749 * @param object $variation
750 * @return object
751 */
752 public static function normalizeCustomFields(object $variation): object
753 {
754 // Map custom fields to native fields only if they exist
755 $variation->id = $variation->id ?? $variation->item_id;
756 $variation->item_price = $variation->item_price
757 ?? $variation->unit_price
758 ?? $variation->price
759 ?? 0;
760 $variation->variation_title = $variation->title ?? ($variation->variation_title ?? '');
761
762
763 // Fallbacks
764 $variation->post_id = $variation->post_id ?? 0;
765 $variation->object_id = $variation->object_id ?? $variation->id;
766 $variation->unit_price = $variation->unit_price
767 ?? $variation->item_price
768 ?? $variation->price
769 ?? 0;
770
771 // Payment & fulfillment
772 $variation->payment_type = sanitize_text_field($variation->payment_type ?? 'onetime');
773 $variation->fulfillment_type = sanitize_text_field($variation->fulfillment_type ?? 'digital');
774
775 // Other info
776 if (!empty($variation->other_info) && is_array($variation->other_info)) {
777 $variation->other_info = $variation->other_info;
778 } else {
779 $variation->other_info = [];
780 }
781
782 // Add custom flags
783 $variation->other_info['is_custom'] = $variation->is_custom ?? false;
784 $variation->other_info['view_url'] = $variation->view_url ?? '';
785
786 return $variation;
787 }
788
789
790 /**
791 * @param ProductVariation $variation
792 * @param int|string $updatedQuantity
793 * @return bool
794 */
795 public static function shouldAddItemToCart(ProductVariation $variation, $updatedQuantity): bool
796 {
797 if ($variation->manage_stock == 0) {
798 return true;
799 }
800 return $updatedQuantity <= $variation->available;
801 }
802
803 public static function doingInstantCheckout()
804 {
805 $variationId = App::request()->get(Helper::INSTANT_CHECKOUT_URL_PARAM);
806 if (empty($variationId)) {
807 return false;
808 }
809 return $variationId;
810 }
811 }
812