Refactor the pricing calculation service and shipping rate import logic to improve consistency and reliability. This includes updating direction naming conventions, enhancing model casting, and adding comprehensive test suites. - Update `ShippingRatesImport` to use 'outbound' and 'inbound' instead of 'export' and 'import' - Refactor `PriceCalculatorService` to use a more modular calculation structure - Update `ShippingRate` model to use explicit property casting for zones - Add `HasFactory` trait to `Country` and `ShippingRate` models - Add new database factories for `Country` and `ShippingRate` - Implement new feature tests for API endpoints and service logic - Add new service tests for `PriceCalculatorService`
258 lines
10 KiB
PHP
258 lines
10 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
|
|
{
|
|
/**
|
|
* محاسبه قیمت نهایی حمل و نقل بر اساس پارامترهای ورودی
|
|
*
|
|
* @param array $data آرایهای شامل اطلاعات محاسبه قیمت:
|
|
* - 'type' (string): نوع سرویس (مقدار Enum ShipmentType)
|
|
* - 'direction' (string): جهت ارسال ('Outbound' یا 'Inbound')
|
|
* - 'weight' (float): وزن واقعی بسته
|
|
* - 'volumetric_weight' (float): وزن حجمی بسته
|
|
* - 'country_iso' (string): کد ISO کشور مقصد
|
|
* - 'extra_service' (float, optional): هزینه خدمات اضافی
|
|
* - 'packing_cost' (float, optional): هزینه بستهبندی
|
|
* - 'domestic_pickup' (float, optional): هزینه دریافت داخلی
|
|
* - 'domestic_delivery' (float, optional): هزینه تحویل داخلی
|
|
* - 'warehousing_cost' (float, optional): هزینه انبارداری
|
|
* - 'discount_code' (string, optional): کد تخفیف
|
|
*
|
|
* @return array آرایهای شامل جزئیات قیمت نهایی
|
|
*
|
|
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException اگر کشور یافت نشود
|
|
* @throws InvalidArgumentException اگر نرخی برای پارامترهای داده شده یافت نشود
|
|
*/
|
|
public function calculate(array $data): array
|
|
{
|
|
// 1. استخراج و نرمالسازی دادههای ورودی
|
|
$type = ShipmentType::from($data['type']);
|
|
$direction = $data['direction'] === 'Outbound' ? 'export' : 'import';
|
|
$chargeableWeight = max($data['weight'], $data['volumetric_weight']);
|
|
|
|
// 2. یافتن کشور و زون مربوطه
|
|
$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,
|
|
};
|
|
|
|
// 3. یافتن نرخ پایه
|
|
$baseRate = $this->lookupRate($direction, $type, $chargeableWeight, $zone);
|
|
|
|
// 4. محاسبات مالی (تبدیل ارز، سود، مالیات)
|
|
$financials = $this->calculateFinancials($baseRate);
|
|
|
|
// 5. جمعآوری هزینههای جانبی
|
|
$extraCosts = $this->calculateExtraCosts($data);
|
|
|
|
// 6. محاسبه قیمت قبل از تخفیف
|
|
$subtotal = $financials['net_rial'] + array_sum($extraCosts);
|
|
|
|
// 7. اعمال کد تخفیف
|
|
$discountResult = $this->applyDiscount($subtotal, $data['discount_code'] ?? null);
|
|
|
|
if ($discountResult['success']) {
|
|
// افزایش شمارنده استفاده از کد تخفیف
|
|
DiscountCode::where('code', $discountResult['discount_code'])->increment('used_count');
|
|
}
|
|
|
|
// 8. محاسبه مالیات و قیمت نهایی
|
|
$vatRate = (float) SystemSetting::get('vat_rate', 0.09);
|
|
$finalPrice = $discountResult['final_price'];
|
|
$vatAmount = $finalPrice * $vatRate;
|
|
$totalFee = $finalPrice + $vatAmount;
|
|
|
|
// 9. بازگرداندن نتیجه نهایی
|
|
return [
|
|
'base_price' => $baseRate,
|
|
'net_dirham' => $financials['net_dirham'],
|
|
'net_rial' => $financials['net_rial'],
|
|
'extra_service' => $extraCosts['extra_service'],
|
|
'packing_cost' => $extraCosts['packing_cost'],
|
|
'domestic_pickup' => $extraCosts['domestic_pickup'],
|
|
'domestic_delivery' => $extraCosts['domestic_delivery'],
|
|
'warehousing_cost' => $extraCosts['warehousing_cost'],
|
|
'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 $baseRate نرخ پایه به درهم
|
|
* @return array ['net_dirham' => float, 'net_rial' => float]
|
|
*/
|
|
private function calculateFinancials(float $baseRate): array
|
|
{
|
|
$profitMargin = (float) SystemSetting::get('profit_margin', 1.25);
|
|
$aedToIrr = (float) SystemSetting::get('aed_to_irr', 455000);
|
|
|
|
$netDirham = $baseRate * $profitMargin;
|
|
$netRial = $netDirham * $aedToIrr;
|
|
|
|
return [
|
|
'net_dirham' => $netDirham,
|
|
'net_rial' => $netRial,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* محاسبه مجموع هزینههای جانبی
|
|
*
|
|
* @param array $data دادههای ورودی
|
|
* @return array آرایهای شامل هزینههای جانبی
|
|
*/
|
|
private function calculateExtraCosts(array $data): array
|
|
{
|
|
$packingCostDefault = (float) SystemSetting::get('packing_cost_default', 100000);
|
|
|
|
return [
|
|
'extra_service' => (float) ($data['extra_service'] ?? 0),
|
|
'packing_cost' => (float) ($data['packing_cost'] ?? $packingCostDefault),
|
|
'domestic_pickup' => (float) ($data['domestic_pickup'] ?? 0),
|
|
'domestic_delivery' => (float) ($data['domestic_delivery'] ?? 0),
|
|
'warehousing_cost' => (float) ($data['warehousing_cost'] ?? 0),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* اعمال کد تخفیف روی قیمت نهایی
|
|
*
|
|
* @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, // برای ذخیره در سفارش
|
|
];
|
|
}
|
|
|
|
/**
|
|
* جستجوی نرخ حمل و نقل در دیتابیس
|
|
*
|
|
* @param string $direction جهت ارسال (export/import)
|
|
* @param ShipmentType $type نوع سرویس
|
|
* @param float $weight وزن قابل پرداخت
|
|
* @param int $zone شماره زون
|
|
* @return float نرخ پیدا شده
|
|
*
|
|
* @throws InvalidArgumentException اگر نرخی یافت نشود
|
|
*/
|
|
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;
|
|
}
|
|
}
|