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.
This commit is contained in:
parent
6aa18f76a8
commit
f5917c34c2
64
04_Laravel/app/Filament/Resources/DiscountCodeResource.php
Normal file
64
04_Laravel/app/Filament/Resources/DiscountCodeResource.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\DiscountCodeResource\Pages;
|
||||
use App\Filament\Resources\DiscountCodeResource\RelationManagers;
|
||||
use App\Models\DiscountCode;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
|
||||
class DiscountCodeResource extends Resource
|
||||
{
|
||||
protected static ?string $model = DiscountCode::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
//
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
//
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\EditAction::make(),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DeleteBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListDiscountCodes::route('/'),
|
||||
'create' => Pages\CreateDiscountCode::route('/create'),
|
||||
'edit' => Pages\EditDiscountCode::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\DiscountCodeResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DiscountCodeResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateDiscountCode extends CreateRecord
|
||||
{
|
||||
protected static string $resource = DiscountCodeResource::class;
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\DiscountCodeResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DiscountCodeResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditDiscountCode extends EditRecord
|
||||
{
|
||||
protected static string $resource = DiscountCodeResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\DiscountCodeResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DiscountCodeResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListDiscountCodes extends ListRecords
|
||||
{
|
||||
protected static string $resource = DiscountCodeResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -6,92 +6,83 @@ use App\Models\Shipment;
|
||||
use App\Models\ShipmentTrackingEvent;
|
||||
use App\Models\Country;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Maatwebsite\Excel\Concerns\ToCollection;
|
||||
use Maatwebsite\Excel\Concerns\OnEachRow;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadingRow;
|
||||
use Illuminate\Support\Collection;
|
||||
use Maatwebsite\Excel\Row;
|
||||
|
||||
class TrackingDataImport implements ToCollection, WithHeadingRow
|
||||
class TrackingDataImport implements OnEachRow, WithHeadingRow
|
||||
{
|
||||
protected $importedEvents = 0;
|
||||
protected $skippedEvents = 0;
|
||||
protected $errors = [];
|
||||
protected $createdShipments = 0;
|
||||
|
||||
public function collection(Collection $rows)
|
||||
public function onRow(Row $row)
|
||||
{
|
||||
Log::info('Tracking import started', ['total_rows' => $rows->count()]);
|
||||
$rowIndex = $row->getIndex();
|
||||
$rowArray = $row->toArray();
|
||||
|
||||
if ($rows->count() === 0) {
|
||||
Log::warning('No rows found');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// استخراج و نرمالسازی دادهها از ردیف اکسل
|
||||
$awbNo = $this->normalizeAwb($rowArray['awb'] ?? null);
|
||||
$eventDate = $this->normalizeDate($rowArray['date'] ?? null);
|
||||
$eventTime = $this->normalizeTime($rowArray['time'] ?? null);
|
||||
$eventDescription = $this->normalizeDescription($rowArray['state'] ?? null);
|
||||
$location = $this->normalizeLocation($rowArray['country'] ?? null);
|
||||
$deliveryStatus = $this->normalizeDeliveryStatus($rowArray['last_state'] ?? null);
|
||||
|
||||
$firstRow = $rows->first();
|
||||
Log::info('First row keys', ['keys' => $firstRow->keys()->toArray()]);
|
||||
Log::info('First row awb', ['awb' => $firstRow['awb'] ?? 'N/A']);
|
||||
|
||||
foreach ($rows as $index => $row) {
|
||||
try {
|
||||
$awbNo = $this->normalizeAwb($row['awb'] ?? null);
|
||||
$eventDate = $this->normalizeDate($row['date'] ?? null);
|
||||
$eventTime = $this->normalizeTime($row['time'] ?? null);
|
||||
$eventDescription = $this->normalizeDescription($row['state'] ?? null);
|
||||
$location = $this->normalizeLocation($row['country'] ?? null);
|
||||
$deliveryStatus = $this->normalizeDeliveryStatus($row['last_state'] ?? null);
|
||||
|
||||
if (!$awbNo || !$eventDate || !$eventDescription) {
|
||||
$this->skippedEvents++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$shipment = Shipment::where('awb_no', $awbNo)->first();
|
||||
|
||||
if (!$shipment) {
|
||||
$shipment = $this->createShipmentFromTrackingRow($row, $awbNo, $location);
|
||||
$this->createdShipments++;
|
||||
}
|
||||
|
||||
ShipmentTrackingEvent::create([
|
||||
'shipment_id' => $shipment->id,
|
||||
'event_date' => $eventDate,
|
||||
'event_time' => $eventTime,
|
||||
'location' => $location,
|
||||
'event_description' => $eventDescription,
|
||||
'delivery_status' => $deliveryStatus,
|
||||
'source' => 'manual',
|
||||
]);
|
||||
|
||||
$this->importedEvents++;
|
||||
} catch (\Throwable $e) {
|
||||
// بررسی اعتبار دادههای ضروری
|
||||
if (!$awbNo || !$eventDate || !$eventDescription) {
|
||||
$this->skippedEvents++;
|
||||
$this->errors[] = "Row {$index}: " . $e->getMessage();
|
||||
Log::error('Tracking import error', ['row' => $index, 'error' => $e->getMessage()]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Log::info('Tracking import completed', [
|
||||
'imported' => $this->importedEvents,
|
||||
'skipped' => $this->skippedEvents,
|
||||
'created' => $this->createdShipments,
|
||||
'errors' => count($this->errors),
|
||||
]);
|
||||
// جستجوی مرسوله بر اساس AWB
|
||||
$shipment = Shipment::where('awb_no', $awbNo)->first();
|
||||
|
||||
// اگر مرسوله وجود نداشت، آن را ایجاد کن
|
||||
if (!$shipment) {
|
||||
$shipment = $this->createShipmentFromTrackingRow($rowArray, $awbNo, $location);
|
||||
$this->createdShipments++;
|
||||
}
|
||||
|
||||
// ثبت رویداد ترکینگ
|
||||
ShipmentTrackingEvent::create([
|
||||
'shipment_id' => $shipment->id,
|
||||
'event_date' => $eventDate,
|
||||
'event_time' => $eventTime,
|
||||
'location' => $location,
|
||||
'event_description' => $eventDescription,
|
||||
'delivery_status' => $deliveryStatus,
|
||||
'source' => 'manual',
|
||||
]);
|
||||
|
||||
$this->importedEvents++;
|
||||
} catch (\Throwable $e) {
|
||||
$this->skippedEvents++;
|
||||
$this->errors[] = "Row {$rowIndex}: " . $e->getMessage();
|
||||
Log::error('Tracking import error', ['row' => $rowIndex, 'error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
protected function createShipmentFromTrackingRow(array $row, string $awbNo, ?string $location): Shipment
|
||||
{
|
||||
// استخراج نام کشور از لوکیشن
|
||||
$countryName = $location ? explode(' - ', $location)[0] : null;
|
||||
$country = Country::where('name', 'like', "%{$countryName}%")->first();
|
||||
|
||||
// تعیین وضعیت مرسوله بر اساس آخرین وضعیت
|
||||
$lastState = strtolower($row['last_state'] ?? '');
|
||||
|
||||
$status = 'processed';
|
||||
|
||||
if (str_contains($lastState, 'delivered')) $status = 'delivered';
|
||||
elseif (str_contains($lastState, 'in transit')) $status = 'in_transit';
|
||||
elseif (str_contains($lastState, 'picked')) $status = 'picked_up';
|
||||
elseif (str_contains($lastState, 'out for delivery')) $status = 'out_for_delivery';
|
||||
elseif (str_contains($lastState, 'failed')) $status = 'failed';
|
||||
|
||||
$shipment = Shipment::create([
|
||||
// ایجاد مرسوله جدید با مقادیر پیشفرض
|
||||
return Shipment::create([
|
||||
'awb_no' => $awbNo,
|
||||
'direction' => 'export',
|
||||
'type' => 'PARCEL',
|
||||
@ -115,29 +106,9 @@ class TrackingDataImport implements ToCollection, WithHeadingRow
|
||||
'sender_name' => 'Unknown',
|
||||
'receiver_name' => 'Unknown',
|
||||
]);
|
||||
|
||||
return $shipment;
|
||||
}
|
||||
|
||||
public function getImportedEvents(): int
|
||||
{
|
||||
return $this->importedEvents;
|
||||
}
|
||||
|
||||
public function getSkippedEvents(): int
|
||||
{
|
||||
return $this->skippedEvents;
|
||||
}
|
||||
|
||||
public function getErrors(): array
|
||||
{
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
public function getCreatedShipments(): int
|
||||
{
|
||||
return $this->createdShipments;
|
||||
}
|
||||
// --- متدهای کمکی برای نرمالسازی دادهها ---
|
||||
|
||||
protected function normalizeAwb($value): ?string
|
||||
{
|
||||
@ -221,4 +192,26 @@ class TrackingDataImport implements ToCollection, WithHeadingRow
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- متدهای دریافت آمار ---
|
||||
|
||||
public function getImportedEvents(): int
|
||||
{
|
||||
return $this->importedEvents;
|
||||
}
|
||||
|
||||
public function getSkippedEvents(): int
|
||||
{
|
||||
return $this->skippedEvents;
|
||||
}
|
||||
|
||||
public function getErrors(): array
|
||||
{
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
public function getCreatedShipments(): int
|
||||
{
|
||||
return $this->createdShipments;
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,8 +7,14 @@ use Illuminate\Database\Eloquent\Model;
|
||||
class DiscountCode extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'code', 'type', 'value', 'min_order_amount',
|
||||
'usage_limit', 'used_count', 'expires_at', 'is_active'
|
||||
'code',
|
||||
'type',
|
||||
'value',
|
||||
'min_order_amount',
|
||||
'usage_limit',
|
||||
'used_count',
|
||||
'expires_at',
|
||||
'is_active'
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
|
||||
@ -4,7 +4,7 @@ namespace App\Services;
|
||||
|
||||
use App\Enums\ShipmentType;
|
||||
use App\Models\Country;
|
||||
use App\Models\Shipment;
|
||||
use App\Models\DiscountCode;
|
||||
use App\Models\SystemSetting;
|
||||
use InvalidArgumentException;
|
||||
|
||||
@ -40,11 +40,31 @@ class PriceCalculatorService
|
||||
$domesticPickup = (float) ($data['domestic_pickup'] ?? 0);
|
||||
$domesticDelivery = (float) ($data['domestic_delivery'] ?? 0);
|
||||
$warehousingCost = (float) ($data['warehousing_cost'] ?? 0);
|
||||
$discount = (float) ($data['discount'] ?? 0);
|
||||
|
||||
// متغیر $discount را از آرایه دادهها دریافت میکنیم (اگر وجود داشته باشد)
|
||||
// اما در اینجا ما از کد تخفیف استفاده میکنیم، پس این خط را حذف یا اصلاح میکنیم
|
||||
// $discount = (float) ($data['discount'] ?? 0);
|
||||
|
||||
$subtotal = $netRial + $extraService + $packingCost + $domesticPickup + $domesticDelivery + $warehousingCost - $discount;
|
||||
$totalFee = $subtotal * (1 + $vatRate);
|
||||
$vatAmount = $totalFee - $subtotal;
|
||||
// محاسبه قیمت اولیه قبل از تخفیف کد
|
||||
$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,
|
||||
@ -55,7 +75,9 @@ class PriceCalculatorService
|
||||
'domestic_pickup' => $domesticPickup,
|
||||
'domestic_delivery' => $domesticDelivery,
|
||||
'warehousing_cost' => $warehousingCost,
|
||||
'discount' => $discount,
|
||||
'discount_applied' => $discountResult['success'],
|
||||
'discount_amount' => $discountResult['discount_amount'],
|
||||
'discount_message' => $discountResult['message'],
|
||||
'vat_amount' => $vatAmount,
|
||||
'total_fee' => $totalFee,
|
||||
'zone' => $zone,
|
||||
@ -63,6 +85,98 @@ class PriceCalculatorService
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* اعمال کد تخفیف روی قیمت نهایی
|
||||
*
|
||||
* @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;
|
||||
|
||||
53
04_Laravel/app/Services/WalletService.php
Normal file
53
04_Laravel/app/Services/WalletService.php
Normal file
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Wallet;
|
||||
use App\Models\WalletTransaction;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class WalletService
|
||||
{
|
||||
/**
|
||||
* واریز وجه به کیف پول
|
||||
*/
|
||||
public function deposit(Wallet $wallet, float $amount, string $description = null, $transactionable = null): WalletTransaction
|
||||
{
|
||||
return DB::transaction(function () use ($wallet, $amount, $description, $transactionable) {
|
||||
$wallet->increment('balance', $amount);
|
||||
|
||||
return $this->createTransaction($wallet, $amount, 'deposit', $description, $transactionable);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* برداشت وجه از کیف پول
|
||||
*/
|
||||
public function withdraw(Wallet $wallet, float $amount, string $description = null, $transactionable = null): WalletTransaction
|
||||
{
|
||||
if ($wallet->balance < $amount) {
|
||||
throw new \Exception('موجودی کیف پول کافی نیست.');
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($wallet, $amount, $description, $transactionable) {
|
||||
$wallet->decrement('balance', $amount);
|
||||
|
||||
// مبلغ برداشت به صورت منفی ثبت میشود
|
||||
return $this->createTransaction($wallet, -$amount, 'withdraw', $description, $transactionable);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ثبت تراکنش در دیتابیس
|
||||
*/
|
||||
private function createTransaction(Wallet $wallet, float $amount, string $type, ?string $description, $transactionable): WalletTransaction
|
||||
{
|
||||
return WalletTransaction::create([
|
||||
'wallet_id' => $wallet->id,
|
||||
'amount' => $amount,
|
||||
'type' => $type,
|
||||
'description' => $description,
|
||||
'transactionable_type' => $transactionable ? get_class($transactionable) : null,
|
||||
'transactionable_id' => $transactionable?->id,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user