| 1 |
<?php |
| 2 |
|
| 3 |
namespace FluentCart\App\Services\Coupon\Concerns; |
| 4 |
|
| 5 |
use FluentCart\Api\CurrencySettings; |
| 6 |
use FluentCart\Api\Resource\FrontendResource\CartResource; |
| 7 |
use FluentCart\App\App; |
| 8 |
use FluentCart\App\Helpers\Helper; |
| 9 |
use FluentCart\App\Models\Coupon; |
| 10 |
use FluentCart\Framework\Support\Arr; |
| 11 |
use FluentCart\Framework\Support\Collection; |
| 12 |
|
| 13 |
trait CanCalculateLineTotal |
| 14 |
{ |
| 15 |
protected array $discountData = []; |
| 16 |
|
| 17 |
public function calculateLineItems() |
| 18 |
{ |
| 19 |
$lineItems = $this->prepareLineItems($this->lineItems); |
| 20 |
|
| 21 |
$this->discountData = []; |
| 22 |
// $firstTime determines whether to use the original price or the discounted price as the current price for calculations |
| 23 |
|
| 24 |
foreach ($this->applicableCoupons as $coupon) { |
| 25 |
$validated = $this->validate($coupon->code); |
| 26 |
if (is_wp_error($validated)) { |
| 27 |
$this->couponErrors->put($coupon->code, $validated); |
| 28 |
continue; |
| 29 |
} |
| 30 |
|
| 31 |
$this->applyCouponToAllItems($coupon, $lineItems); |
| 32 |
$this->previouslyAppliedCouponCodes[] = $coupon->code; |
| 33 |
$this->previouslyAppliedCoupons->put($coupon->code, $coupon); |
| 34 |
} |
| 35 |
|
| 36 |
$this->formatDiscountNumbers(); |
| 37 |
$this->calculatedLineItems = $lineItems; |
| 38 |
|
| 39 |
} |
| 40 |
|
| 41 |
protected function prepareLineItems(array $lineItems = []): array |
| 42 |
{ |
| 43 |
foreach ($lineItems as $index => $lineItem) { |
| 44 |
$lineItems[$index]['price'] = $lineItem['unit_price']; |
| 45 |
$lineItems[$index]['discount_total'] = 0; |
| 46 |
} |
| 47 |
|
| 48 |
return $lineItems; |
| 49 |
} |
| 50 |
|
| 51 |
protected function formatDiscountNumbers() |
| 52 |
{ |
| 53 |
foreach ($this->discountData as $code => $discount) { |
| 54 |
$formattedTitle = $code; |
| 55 |
|
| 56 |
if ($discount['type'] === 'percentage') { |
| 57 |
$formattedTitle .= ' (' . $discount['actual_amount'] . '%)'; |
| 58 |
} |
| 59 |
|
| 60 |
$this->discountData[$code]['formatted_discount'] = CurrencySettings::getPriceHtml($discount['discount']); |
| 61 |
$this->discountData[$code]['actual_formatted_discount'] = CurrencySettings::getPriceHtml( |
| 62 |
$discount['discount'] |
| 63 |
); |
| 64 |
$this->discountData[$code]['formatted_title'] = $formattedTitle; |
| 65 |
|
| 66 |
} |
| 67 |
} |
| 68 |
|
| 69 |
public function getDiscountData(): array |
| 70 |
{ |
| 71 |
return $this->discountData; |
| 72 |
} |
| 73 |
|
| 74 |
private function applyCouponToAllItems(Coupon $coupon, array &$lineItems) |
| 75 |
{ |
| 76 |
$this->ensureCouponExistInDiscountData($coupon); |
| 77 |
|
| 78 |
$filteredItems = Collection::make($lineItems)->filter(function ($item) use ($coupon) { |
| 79 |
return $this->isApplicableToProduct($coupon, $item['post_id'], $item['id']); |
| 80 |
}); |
| 81 |
|
| 82 |
$totalNetPrice = $filteredItems->reduce(function ($carry, $item) { |
| 83 |
return $carry + (($item['unit_price'] * $item['quantity']) - $item['discount_total']); |
| 84 |
}, 0); |
| 85 |
|
| 86 |
$applicableAmount = $this->calculateApplicableAmount($coupon, $totalNetPrice); |
| 87 |
|
| 88 |
$lineItems = Collection::make($lineItems)->keyBy('id')->toArray(); |
| 89 |
|
| 90 |
|
| 91 |
$discountAmount = $this->distributeDiscount($filteredItems, $lineItems, $applicableAmount); |
| 92 |
|
| 93 |
$this->discountData[$coupon->code]['discount'] += round($discountAmount, 2); |
| 94 |
$this->discountData[$coupon->code]['unit_amount'] = round($discountAmount, 2); |
| 95 |
$this->discountData[$coupon->code]['actual_quantity'] = $filteredItems->sum('quantity'); |
| 96 |
|
| 97 |
} |
| 98 |
|
| 99 |
private function distributeDiscount(Collection $filteredItems, array &$lineItems, float &$remainingDiscount, int $depth = 0): float |
| 100 |
{ |
| 101 |
$totalApplied = 0; |
| 102 |
$totalNetPrice = $filteredItems->reduce(function ($carry, $item) use ($lineItems) { |
| 103 |
$id = $item['id']; |
| 104 |
$line = Arr::get($lineItems, $id); |
| 105 |
|
| 106 |
if (empty($line)) { |
| 107 |
return $carry; |
| 108 |
} |
| 109 |
|
| 110 |
return $carry + (($line['unit_price'] * $line['quantity']) - $line['discount_total']); |
| 111 |
}, 0); |
| 112 |
|
| 113 |
if ($totalNetPrice <= 0 || $remainingDiscount <= 0.01 || $depth > 10) { |
| 114 |
return 0; |
| 115 |
} |
| 116 |
|
| 117 |
foreach ($filteredItems as $item) { |
| 118 |
$id = $item['id']; |
| 119 |
if (!isset($lineItems[$id])) continue; |
| 120 |
|
| 121 |
$lineItem = &$lineItems[$id]; |
| 122 |
$itemNetTotal = ($lineItem['unit_price'] * $lineItem['quantity']) - $lineItem['discount_total']; |
| 123 |
if ($itemNetTotal <= 0) { |
| 124 |
continue; |
| 125 |
} |
| 126 |
|
| 127 |
$itemShare = ($itemNetTotal / $totalNetPrice) * $remainingDiscount; |
| 128 |
$itemShare = round($itemShare, 2); |
| 129 |
|
| 130 |
$applied = min($itemShare, $itemNetTotal, $remainingDiscount); |
| 131 |
$lineItem['discount_total'] += $applied; |
| 132 |
$remainingDiscount -= $applied; |
| 133 |
$totalApplied += $applied; |
| 134 |
|
| 135 |
if ($remainingDiscount <= 0.01) { |
| 136 |
break; |
| 137 |
} |
| 138 |
} |
| 139 |
|
| 140 |
// Recursive call if leftover remains and there's still room |
| 141 |
if ($remainingDiscount > 0.01) { |
| 142 |
$totalApplied += $this->distributeDiscount($filteredItems, $lineItems, $remainingDiscount, $depth + 1); |
| 143 |
} |
| 144 |
|
| 145 |
return $totalApplied; |
| 146 |
} |
| 147 |
|
| 148 |
|
| 149 |
public function calculateApplicableAmount(Coupon $coupon, $totalPrice) |
| 150 |
{ |
| 151 |
$amount = $coupon->amount; |
| 152 |
|
| 153 |
$cart = CartResource::get([ |
| 154 |
'hash' => App::request()->get(Helper::INSTANT_CHECKOUT_URL_PARAM) |
| 155 |
]); |
| 156 |
|
| 157 |
if ($cart) { |
| 158 |
$upgradeDiscount = Arr::get($cart, 'checkout_data.manual_discount.amount', 0); |
| 159 |
$totalPrice = $totalPrice - $upgradeDiscount; |
| 160 |
if ($totalPrice < 0) { |
| 161 |
$totalPrice = 0; |
| 162 |
} |
| 163 |
} |
| 164 |
|
| 165 |
if ($coupon->type === 'percentage') { |
| 166 |
$amount = ($amount / 100) * $totalPrice; |
| 167 |
} |
| 168 |
$maxAmount = Arr::get($coupon->conditions, 'max_discount_amount', 0); |
| 169 |
if (!empty($maxAmount) && is_numeric($maxAmount)) { |
| 170 |
return min($totalPrice, $amount, $maxAmount); |
| 171 |
} |
| 172 |
|
| 173 |
return min($totalPrice, $amount); |
| 174 |
} |
| 175 |
|
| 176 |
} |
| 177 |
|