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 / Helpers / CartHelper.php

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

833 lines 32.2 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 $totalShippingAmount = 0;
482
483 // Preload all class-specific methods in a single query
484 $classMethodsMap = [];
485 $classIdsWithMethods = array_filter(array_unique(array_column($groups, 'class_id')));
486 if ($classIdsWithMethods && $country) {
487 $allClassMethods = ShippingMethod::query()
488 ->whereHas('zone', function ($q) use ($country, $classIdsWithMethods) {
489 $q->where(function ($zq) use ($country) {
490 $zq->whereIn('region', [$country, 'all'])
491 ->orWhere('region', 'selection');
492 })
493 ->whereIn('shipping_class_id', $classIdsWithMethods);
494 })
495 ->where('is_enabled', 1)
496 ->orderBy('amount', 'DESC')
497 ->with('zone')
498 ->get()
499 ->filter(function ($method) use ($country) {
500 if (!$method->zone || $method->zone->region !== 'selection') {
501 return true;
502 }
503 return $method->zone->appliesToCountry($country);
504 });
505
506 // Group by shipping_class_id
507 foreach ($allClassMethods as $method) {
508 $classId = $method->zone->shipping_class_id ?? null;
509 if ($classId) {
510 $classMethodsMap[$classId][] = $method;
511 }
512 }
513 }
514
515 // Preload all variations for weight calculation (avoids N+1 per group)
516 $allVariationIds = [];
517 foreach ($groups as $group) {
518 foreach ($group['items'] as $item) {
519 $vId = Arr::get($item, 'object_id', Arr::get($item, 'variation_id'));
520 if ($vId) $allVariationIds[] = $vId;
521 }
522 }
523 $allVariationIds = array_unique(array_filter($allVariationIds));
524 $allVariationsMap = !empty($allVariationIds)
525 ? ProductVariation::query()->whereIn('id', $allVariationIds)->get()->keyBy('id')
526 : new \FluentCart\Framework\Support\Collection();
527
528 foreach ($groups as $groupKey => &$group) {
529 $classId = $group['class_id'];
530 $groupItems = $group['items'];
531
532 // Find the applicable method for this group (from preloaded map)
533 $groupMethod = $selectedMethod;
534 if ($classId && isset($classMethodsMap[$classId])) {
535 $classMethods = $classMethodsMap[$classId];
536 $matched = false;
537 foreach ($classMethods as $m) {
538 if ($m->id == $shippingMethodId) {
539 $groupMethod = $m;
540 $matched = true;
541 break;
542 }
543 }
544 if (!$matched) {
545 $groupMethod = $classMethods[0];
546 }
547 }
548
549 // Calculate group totals
550 $groupTotalPrice = 0;
551 $groupTotalQuantity = 0;
552 $groupMaxClassCharge = 0;
553 $groupTotalClassCharge = 0;
554
555 foreach ($groupItems as &$gItem) {
556 $quantity = Arr::get($gItem, 'quantity', 1);
557 $groupTotalQuantity += $quantity;
558 $groupTotalPrice += ($quantity * Arr::get($gItem, 'unit_price', 0)) - Arr::get($gItem, 'discount_total', 0);
559
560 // Calculate class surcharge per item
561 $itemClassCharge = 0;
562 if ($classId && $shippingClasses->has($classId)) {
563 $sc = $shippingClasses->get($classId);
564 $factor = $sc->per_item ? $quantity : 1;
565 if ($sc->type === 'percentage') {
566 $itemClassCharge = ($sc->cost / 100) * Arr::get($gItem, 'unit_price', 0) * $factor;
567 } else {
568 $itemClassCharge = Helper::toCent($sc->cost) * $factor;
569 }
570 }
571
572 $gItem['shipping_charge'] = $itemClassCharge;
573 $groupTotalClassCharge += $itemClassCharge;
574 $groupMaxClassCharge = max($groupMaxClassCharge, $itemClassCharge);
575 }
576 unset($gItem);
577
578 // Calculate method-level amount for this group
579 $settings = Arr::wrap($groupMethod->settings);
580 $configureRate = Arr::get($settings, 'configure_rate', 'per_order');
581 $classAggregation = Arr::get($settings, 'class_aggregation', 'sum_all');
582
583 if ($groupMethod->type === 'free_shipping') {
584 $methodAmount = 0;
585 } elseif ($configureRate === 'per_order') {
586 $methodAmount = $groupMethod->amount * 100;
587 } elseif ($configureRate === 'per_price') {
588 $methodAmount = $groupTotalPrice * ($groupMethod->amount / 100);
589 } elseif ($configureRate === 'per_weight') {
590 $storeWeightUnit = Helper::shopConfig('weight_unit') ?: 'kg';
591 $totalWeight = 0;
592
593 foreach ($groupItems as $gItem) {
594 $varId = Arr::get($gItem, 'object_id', Arr::get($gItem, 'variation_id'));
595 $variation = $varId ? $allVariationsMap->get($varId) : null;
596 if ($variation) {
597 $otherInfo = $variation->other_info ?: [];
598 $productWeight = floatval(Arr::get($otherInfo, 'weight', 0));
599 $productWeightUnit = Arr::get($otherInfo, 'weight_unit', $storeWeightUnit);
600 $convertedProductWeight = Helper::convertWeight($productWeight, $productWeightUnit, $storeWeightUnit);
601
602 $packageSlug = Arr::get($otherInfo, 'package_slug', '');
603 $package = Helper::getPackageBySlug($packageSlug);
604 $packageWeight = 0;
605 if ($package) {
606 $packageWeightUnit = Arr::get($package, 'weight_unit', $storeWeightUnit);
607 $packageWeight = Helper::convertWeight(
608 floatval(Arr::get($package, 'weight', 0)),
609 $packageWeightUnit,
610 $storeWeightUnit
611 );
612 }
613
614 $totalWeight += ($convertedProductWeight + $packageWeight) * Arr::get($gItem, 'quantity', 1);
615 }
616 }
617 $weightTiers = Arr::get($settings, 'weight_tiers', []);
618 $methodAmount = 0;
619 foreach ($weightTiers as $tier) {
620 $min = floatval(Arr::get($tier, 'min', 0));
621 $max = floatval(Arr::get($tier, 'max', 0));
622 if ($totalWeight >= $min && ($max <= 0 || $totalWeight <= $max)) {
623 $methodAmount = Helper::toCent(floatval(Arr::get($tier, 'cost', 0)));
624 break;
625 }
626 }
627 } else {
628 // per_item
629 $methodAmount = $groupMethod->amount * $groupTotalQuantity * 100;
630 }
631
632 // Add class aggregation
633 if ($classAggregation === 'highest_class') {
634 $methodAmount += $groupMaxClassCharge;
635 } else {
636 $methodAmount += $groupTotalClassCharge;
637 }
638
639 // Distribute method-level portion across group items proportionally
640 $methodOnlyAmount = $methodAmount - $groupTotalClassCharge;
641 if ($methodOnlyAmount < 0) {
642 $methodOnlyAmount = 0;
643 }
644
645 $totalLineTotal = 0;
646 foreach ($groupItems as $gItem) {
647 $totalLineTotal += Arr::get($gItem, 'line_total', 0);
648 }
649
650 $distributed = 0;
651 $itemCount = count($groupItems);
652 foreach ($groupItems as $idx => &$gItem) {
653 if ($totalLineTotal > 0) {
654 $share = (Arr::get($gItem, 'line_total', 0) / $totalLineTotal) * $methodOnlyAmount;
655 } else {
656 $share = $itemCount > 0 ? ($methodOnlyAmount / $itemCount) : 0;
657 }
658 $share = round($share, 2);
659 $gItem['itemwise_shipping_charge'] = ceil($share) + Arr::get($gItem, 'shipping_charge', 0);
660 $distributed += $share;
661 }
662 unset($gItem);
663
664 $diff = round($methodOnlyAmount - $distributed, 2);
665 if ($diff != 0 && !empty($groupItems)) {
666 $lastIdx = array_key_last($groupItems);
667 $groupItems[$lastIdx]['itemwise_shipping_charge'] += ceil($diff);
668 }
669
670 $group['items'] = $groupItems;
671 $group['amount'] = $methodAmount;
672 $totalShippingAmount += $methodAmount;
673 }
674 unset($group);
675
676 // Merge group items back into cartItems
677 foreach ($groups as $group) {
678 foreach ($group['keys'] as $i => $key) {
679 if (isset($group['items'][$i])) {
680 $cartItems[$key]['shipping_charge'] = Arr::get($group['items'][$i], 'shipping_charge', 0);
681 $cartItems[$key]['itemwise_shipping_charge'] = Arr::get($group['items'][$i], 'itemwise_shipping_charge', 0);
682 }
683 }
684 }
685
686 if ($returnType === 'items') {
687 return [
688 'items' => $cartItems,
689 'shipping_amount' => $totalShippingAmount
690 ];
691 }
692
693 return $totalShippingAmount;
694 }
695
696 public static function resetShippingCharge()
697 {
698 $cart = CartHelper::getCart();
699 $items = $cart->cart_data;
700 foreach ($items as $key => $item) {
701 $items[$key]['shipping_charge'] = 0;
702 $items[$key]['itemwise_shipping_charge'] = 0;
703 }
704 $cart->cart_data = $items;
705
706 $cart->checkout_data = array_merge($cart->checkout_data, [
707 'shipping_data' => [
708 'shipping_method_id' => null,
709 'shipping_charge' => 0
710 ]
711 ]);
712
713 $cart->save();
714
715 do_action('fluent_cart/checkout/shipping_data_changed', [
716 'cart' => $cart
717 ]);
718 }
719
720 public static function generateCartFromVariation(ProductVariation $variation, $quantity = 1): Cart
721 {
722 $cart = new Cart();
723 $cart->cart_data = [
724 static::generateCartItemFromVariation($variation, $quantity)
725 ];
726
727 $cart = static::addCommonCartData($cart);
728 return $cart;
729 }
730
731 public static function addCommonCartData(Cart $cart)
732 {
733 if (is_user_logged_in()) {
734 $wpUser = wp_get_current_user();
735 $cart->user_id = get_current_user_id();
736 $customer = Customer::query()->where('email', wp_get_current_user()->user_email)->first();
737 if ($customer) {
738 $cart->customer_id = $customer->id;
739 }
740 $cart->email = $wpUser->user_email;
741 $cart->first_name = $wpUser->first_name;
742 $cart->last_name = $wpUser->last_name;
743 $cart->ip_address = AddressHelper::getIpAddress();
744 $cart->user_agent = AddressHelper::getUserAgent();
745 }
746
747 return $cart;
748 }
749
750 public static function generateCartFromCustomVariation(array $variation, $quantity = 1): Cart
751 {
752 $cart = new Cart();
753 $cart->cart_data = [
754 static::generateCartItemCustomItem($variation, $quantity)
755 ];
756 return $cart;
757 }
758
759 /**
760 * Normalize custom item fields to standard cart variation format.
761 *
762 * NOTE:
763 * - Custom items may originate from external sources (filters, adjustments, migrations)
764 * - Some sources provide `id`, others only provide `item_id`
765 * - For cart consistency, `id` is required and will fall back to `item_id` when missing
766 * - This method intentionally mutates the provided object to normalize field names.
767 * The variation object is treated as a transient data structure and is not reused
768 * elsewhere after normalization.
769 *
770 * @param object $variation
771 * @return object
772 */
773 public static function normalizeCustomFields(object $variation): object
774 {
775 // Map custom fields to native fields only if they exist
776 $variation->id = $variation->id ?? $variation->item_id;
777 $variation->item_price = $variation->item_price
778 ?? $variation->unit_price
779 ?? $variation->price
780 ?? 0;
781 $variation->variation_title = $variation->title ?? ($variation->variation_title ?? '');
782
783
784 // Fallbacks
785 $variation->post_id = $variation->post_id ?? 0;
786 $variation->object_id = $variation->object_id ?? $variation->id;
787 $variation->unit_price = $variation->unit_price
788 ?? $variation->item_price
789 ?? $variation->price
790 ?? 0;
791
792 // Payment & fulfillment
793 $variation->payment_type = sanitize_text_field($variation->payment_type ?? 'onetime');
794 $variation->fulfillment_type = sanitize_text_field($variation->fulfillment_type ?? 'digital');
795
796 // Other info
797 if (!empty($variation->other_info) && is_array($variation->other_info)) {
798 $variation->other_info = $variation->other_info;
799 } else {
800 $variation->other_info = [];
801 }
802
803 // Add custom flags
804 $variation->other_info['is_custom'] = $variation->is_custom ?? false;
805 $variation->other_info['view_url'] = $variation->view_url ?? '';
806
807 return $variation;
808 }
809
810
811 /**
812 * @param ProductVariation $variation
813 * @param int|string $updatedQuantity
814 * @return bool
815 */
816 public static function shouldAddItemToCart(ProductVariation $variation, $updatedQuantity): bool
817 {
818 if ($variation->manage_stock == 0) {
819 return true;
820 }
821 return $updatedQuantity <= $variation->available;
822 }
823
824 public static function doingInstantCheckout()
825 {
826 $variationId = App::request()->get(Helper::INSTANT_CHECKOUT_URL_PARAM);
827 if (empty($variationId)) {
828 return false;
829 }
830 return $variationId;
831 }
832 }
833