ifnex/04_Laravel/app/Services/PriceCalculatorService.php
Kazem Alghasi f5917c34c2 feat(logic): integrate discount code application and wallet services
Implement the core logic for applying discount codes during price
calculation and introduce the WalletService. This includes updating
the PriceCalculatorService to validate and apply discounts,
incrementing usage counts, and adding a Filament resource for
managing discount codes via the admin panel.

Additionally, refactor TrackingDataImport to use OnEachRow for better
memory management during large Excel imports.
2026-08-05 19:16:09 +03:30

199 lines
7.8 KiB
PHP

<?php
namespace App\Services;
use App\Enums\ShipmentType;
use App\Models\Country;
use App\Models\DiscountCode;
use App\Models\SystemSetting;
use InvalidArgumentException;
class PriceCalculatorService
{
public function calculate(array $data): array
{
$type = ShipmentType::from($data['type']);
$direction = $data['direction'] === 'Outbound' ? 'export' : 'import';
$chargeableWeight = max($data['weight'], $data['volumetric_weight']);
$country = Country::where('iso_code', $data['country_iso'])->firstOrFail();
$zone = match (true) {
$type === ShipmentType::Parcel && $direction === 'export' => $country->export_zone_parcel,
$type === ShipmentType::Parcel && $direction === 'import' => $country->import_zone_parcel,
$type !== ShipmentType::Parcel && $direction === 'export' => $country->export_zone_doc,
$type !== ShipmentType::Parcel && $direction === 'import' => $country->import_zone_doc,
};
$rate = $this->lookupRate($direction, $type, $chargeableWeight, $zone);
$profitMargin = (float) SystemSetting::get('profit_margin', 1.25);
$aedToIrr = (float) SystemSetting::get('aed_to_irr', 455000);
$vatRate = (float) SystemSetting::get('vat_rate', 0.09);
$packingCostDefault = (float) SystemSetting::get('packing_cost_default', 100000);
$netDirham = $rate * $profitMargin;
$netRial = $netDirham * $aedToIrr;
$extraService = (float) ($data['extra_service'] ?? 0);
$packingCost = (float) ($data['packing_cost'] ?? $packingCostDefault);
$domesticPickup = (float) ($data['domestic_pickup'] ?? 0);
$domesticDelivery = (float) ($data['domestic_delivery'] ?? 0);
$warehousingCost = (float) ($data['warehousing_cost'] ?? 0);
// متغیر $discount را از آرایه داده‌ها دریافت می‌کنیم (اگر وجود داشته باشد)
// اما در اینجا ما از کد تخفیف استفاده می‌کنیم، پس این خط را حذف یا اصلاح می‌کنیم
// $discount = (float) ($data['discount'] ?? 0);
// محاسبه قیمت اولیه قبل از تخفیف کد
$subtotal = $netRial + $extraService + $packingCost + $domesticPickup + $domesticDelivery + $warehousingCost;
// اعمال کد تخفیف (در صورت وجود)
$discountCode = $data['discount_code'] ?? null;
$discountResult = $this->applyDiscount($subtotal, $discountCode);
if ($discountResult['success']) {
$finalPrice = $discountResult['final_price'];
// افزایش شمارنده استفاده از کد تخفیف
$discount = DiscountCode::where('code', $discountResult['discount_code'])->first();
if ($discount) {
$discount->increment('used_count');
}
} else {
$finalPrice = $subtotal;
}
$totalFee = $finalPrice * (1 + $vatRate);
$vatAmount = $totalFee - $finalPrice;
return [
'base_price' => $rate,
'net_dirham' => $netDirham,
'net_rial' => $netRial,
'extra_service' => $extraService,
'packing_cost' => $packingCost,
'domestic_pickup' => $domesticPickup,
'domestic_delivery' => $domesticDelivery,
'warehousing_cost' => $warehousingCost,
'discount_applied' => $discountResult['success'],
'discount_amount' => $discountResult['discount_amount'],
'discount_message' => $discountResult['message'],
'vat_amount' => $vatAmount,
'total_fee' => $totalFee,
'zone' => $zone,
'chargeable_weight' => $chargeableWeight,
];
}
/**
* اعمال کد تخفیف روی قیمت نهایی
*
* @param float $totalPrice قیمت کل قبل از تخفیف
* @param string|null $code کد تخفیف وارد شده توسط کاربر
* @return array ['success' => bool, 'message' => string, 'discount_amount' => float, 'final_price' => float]
*/
public function applyDiscount(float $totalPrice, ?string $code): array
{
if (empty($code)) {
return [
'success' => false,
'message' => 'کد تخفیف وارد نشده است.',
'discount_amount' => 0,
'final_price' => $totalPrice,
];
}
$discount = DiscountCode::where('code', $code)->first();
// 1. بررسی وجود کد
if (!$discount) {
return [
'success' => false,
'message' => 'کد تخفیف نامعتبر است.',
'discount_amount' => 0,
'final_price' => $totalPrice,
];
}
// 2. بررسی فعال بودن کد
if (!$discount->is_active) {
return [
'success' => false,
'message' => 'این کد تخفیف غیرفعال شده است.',
'discount_amount' => 0,
'final_price' => $totalPrice,
];
}
// 3. بررسی تاریخ انقضا
if ($discount->expires_at && $discount->expires_at->isPast()) {
return [
'success' => false,
'message' => 'این کد تخفیف منقضی شده است.',
'discount_amount' => 0,
'final_price' => $totalPrice,
];
}
// 4. بررسی حداقل مبلغ سفارش
if ($discount->min_order_amount > 0 && $totalPrice < $discount->min_order_amount) {
return [
'success' => false,
'message' => "حداقل مبلغ سفارش برای استفاده از این کد " . number_format($discount->min_order_amount) . " ریال است.",
'discount_amount' => 0,
'final_price' => $totalPrice,
];
}
// 5. بررسی محدودیت استفاده
if ($discount->usage_limit && $discount->used_count >= $discount->usage_limit) {
return [
'success' => false,
'message' => 'ظرفیت استفاده از این کد تخفیف به پایان رسیده است.',
'discount_amount' => 0,
'final_price' => $totalPrice,
];
}
// محاسبه مبلغ تخفیف
$discountAmount = 0;
if ($discount->type === 'percentage') {
$discountAmount = ($totalPrice * $discount->value) / 100;
} elseif ($discount->type === 'fixed') {
$discountAmount = $discount->value;
}
// اطمینان از اینکه تخفیف بیشتر از مبلغ کل نشود
$discountAmount = min($discountAmount, $totalPrice);
$finalPrice = $totalPrice - $discountAmount;
return [
'success' => true,
'message' => 'کد تخفیف با موفقیت اعمال شد.',
'discount_amount' => $discountAmount,
'final_price' => $finalPrice,
'discount_code' => $discount->code, // برای ذخیره در سفارش
];
}
private function lookupRate(string $direction, ShipmentType $type, float $weight, int $zone): float
{
$zoneColumn = 'zone_' . $zone;
$rate = \App\Models\ShippingRate::query()
->where('direction', $direction)
->where('type', $type->value)
->where('weight', '<=', $weight)
->where($zoneColumn, '>', 0)
->orderByDesc('weight')
->value($zoneColumn);
if ($rate === null) {
throw new InvalidArgumentException('No rate found for the given parameters.');
}
return (float) $rate;
}
}