refactor(api): expose wallet and discount endpoints and cleanup project
Implement new API controllers for wallet management and discount code validation. This includes adding an Artisan command for exchange rate updates and refining the API routing structure. Additionally, perform a significant cleanup of the repository by removing obsolete migrations, debug scripts, and temporary test files. Security and configuration improvements include restricting CORS origins via environment variables and updating the system settings schema. - Add `WalletController` and `DiscountCodeController` APIs - Add `UpdateExchangeRates` command - Remove redundant migrations and debug/test scripts - Secure CORS configuration using `CORS_ALLOWED_ORIGINS` - Update `STATUS.md` to reflect current phase progress
This commit is contained in:
parent
f5917c34c2
commit
8d228993f9
@ -1,9 +1,9 @@
|
||||
# 🚨 STATUS.md — این فایل را اول بخوانید
|
||||
|
||||
> **آخرین بهروزرسانی:** 2026-08-04 11:31 (Asia/Tehran)
|
||||
> **فاز در حال اجرا:** فاز ۲ (حساب کاربری مشتری + کیف پول + تخفیف حجمی)
|
||||
> **آخرین بهروزرسانی:** 2026-08-05 22:32 (Asia/Tehran)
|
||||
> **فاز در حال اجرا:** فاز ۲ (اتوماسیون مالی + کیف پول + تخفیف)
|
||||
> **توسعهدهنده:** VernaSoft Group — Kazem Alghasi
|
||||
> **وضعیت کلی پروژه:** فاز ۰ کامل — فاز ۱ کامل — فاز ۲ در صف
|
||||
> **وضعیت کلی پروژه:** فاز ۰ کامل — فاز ۱ کامل — فاز ۲ در حال اجرا
|
||||
|
||||
---
|
||||
|
||||
@ -243,6 +243,23 @@
|
||||
|
||||
---
|
||||
|
||||
### کارهای فاز ۲ (در حال اجرا)
|
||||
|
||||
- [x] ساخت `WalletController` با APIهای شارژ، بررسی موجودی و تاریخچه تراکنشها
|
||||
- [x] ساخت `DiscountCodeController` با APIهای لیست و اعتبارسنجی کده تخفیف
|
||||
- [x] ساخت `UpdateExchangeRates` Artisan Command با پشتیبانی از ECB و FreeCurrencyAPI
|
||||
- [x] ثبت routeهای کیف پول و تخفیف در `api.php`
|
||||
- [x] اضافه کردن `CURRENCY_API_KEY` به `.env.example` و `config/ifnex.php`
|
||||
- [x] پاکسازی فایلهای تست و debug (`check_*.php`, `debug_*.php`, `test_*.php`, `inspect_*.php`)
|
||||
- [x] رفع CORS — تغییر از `*` به دامنه مشخص از `.env`
|
||||
- [x] حذف migrationهای قدیمی تکراری (`2023_10_27_*`, `2026_07_31_*`)
|
||||
- [ ] اتصال درگاه پرداخت (زرینپال)
|
||||
- [ ] شارژ آنلاین کامل با درگاه پرداخت
|
||||
- [ ] پنل مالی در Filament (داشبورد سود/زیان)
|
||||
- [ ] فرمول محاسبه قیمت رقبا
|
||||
|
||||
---
|
||||
|
||||
## 🚫 خط قرمزها (DO NOT) — هرگز این کارها را نکن
|
||||
|
||||
### 🚫 اسکیمای دیتابیس
|
||||
@ -281,28 +298,21 @@
|
||||
|
||||
### س: چه کارهایی در این جلسه انجام شد؟
|
||||
**ج:**
|
||||
1. بازنویسی کامل اسکیمای دیتابیس (۷ migration جدید)
|
||||
2. بازنویسی تمام Modelها + ایجاد ۶ Enum
|
||||
3. بازنویسی Seeders (countries + system_settings)
|
||||
4. ایجاد API TrackController + ApiKeyMiddleware
|
||||
5. بازنویسی کامل Filament Resources + RelationManagers + SettingsPage
|
||||
6. ایجاد Services (TrackingService + PriceCalculatorService)
|
||||
7. ایجاد Imports (ShippingRatesImport + HistoricalShipmentsImport)
|
||||
8. ایجاد Artisan Commands (ifnex:import:rates, ifnex:import:shipments)
|
||||
9. سوییچ به Laravel 11 + رفع همه خطاهای مربوطه
|
||||
10. import ۴۰۴ shipping rate + ۱ shipment تاریخی
|
||||
11. **Code Review کامل** — شناسایی مشکل middleware و ارائه راهحل
|
||||
12. بهروزرسانی تمام داکیومنتها و حذف ارجاع به ۳۹۵۰ رکورد قدیمی
|
||||
1. ساخت `WalletController` با APIهای شارچ، بررسی موجودی و تاریخچه تراکنشها
|
||||
2. ساخت `DiscountCodeController` با APIهای لیست و اعتبارسنجی کده تخفیف
|
||||
3. ساخت `UpdateExchangeRates` Artisan Command
|
||||
4. ثبت routeهای کیف پول و تخفیف در `api.php`
|
||||
5. اضافه کردن `CURRENCY_API_KEY` به `.env.example` و `config/ifnex.php`
|
||||
6. پاکسازی فایلهای تست و debug از پوشه `04_Laravel`
|
||||
7. رفع CORS — تغییر از `*` به دامنه مشخص از `.env`
|
||||
8. حذف migrationهای قدیمی تکراری
|
||||
|
||||
### س: چه کارهایی باقی مانده؟
|
||||
**ج:**
|
||||
1. رفع مشکل middleware `web` group در `bootstrap/app.php`
|
||||
2. ساده کردن `AdminPanelProvider.php` بعد از تعریف گروه `web`
|
||||
3. تست دستی Filament (ورود + CRUD)
|
||||
4. مهاجرت دادههای ترکینگ از فایل `Data entry 2026-06-28.xlsx`
|
||||
5. ساخت پلاگین وردپرس IFNEX Bridge
|
||||
6. تست کامل API Track
|
||||
7. تولید PDF (فاز ۱)
|
||||
1. اتصال درگاه پرداخت (زرینپال) برای شارچ آنلاین
|
||||
2. پنل مالی در Filament (داشبورد سود/زیان)
|
||||
3. فرمول محاسبه قیمت رقبا
|
||||
4. تست دستی APIهای جدید
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -67,3 +67,4 @@ VITE_APP_NAME="${APP_NAME}"
|
||||
IFNEX_API_KEY=
|
||||
CORS_ALLOWED_ORIGINS=http://localhost,http://127.0.0.1
|
||||
IFNEX_TRACKING_RATE_LIMIT=60
|
||||
CURRENCY_API_KEY=
|
||||
|
||||
98
04_Laravel/app/Console/Commands/UpdateExchangeRates.php
Normal file
98
04_Laravel/app/Console/Commands/UpdateExchangeRates.php
Normal file
@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\SystemSetting;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class UpdateExchangeRates extends Command
|
||||
{
|
||||
protected $signature = 'ifnex:update-rates {--source=ecb : Data source for exchange rates (ecb or freecurrencyapi)}';
|
||||
|
||||
protected $description = 'Update exchange rates from external APIs';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$source = $this->option('source');
|
||||
|
||||
$this->info("Fetching exchange rates from source: {$source}...");
|
||||
|
||||
try {
|
||||
$rates = match ($source) {
|
||||
'freecurrencyapi' => $this->fetchFromFreeCurrencyApi(),
|
||||
default => $this->fetchFromEcb(),
|
||||
};
|
||||
} catch (\Throwable $e) {
|
||||
$this->error("Failed to fetch rates: {$e->getMessage()}");
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
if ($rates) {
|
||||
SystemSetting::where('key', 'aed_to_irr')->update(['value' => (string) $rates['aed_to_irr']]);
|
||||
SystemSetting::where('key', 'usd_to_irr')->update(['value' => (string) $rates['usd_to_irr']]);
|
||||
|
||||
$this->info("Exchange rates updated successfully:");
|
||||
$this->line(" AED/IRR: {$rates['aed_to_irr']}");
|
||||
$this->line(" USD/IRR: {$rates['usd_to_irr']}");
|
||||
} else {
|
||||
$this->warn("No rates were updated. Using fallback values.");
|
||||
$this->setFallbackRates();
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function fetchFromEcb(): ?array
|
||||
{
|
||||
$response = Http::get('https://api.exchangerate-api.com/v4/latest/AED');
|
||||
|
||||
if ($response->failed()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
return [
|
||||
'aed_to_irr' => round($data['rates']['IRR'] ?? 455000, 2),
|
||||
'usd_to_irr' => round($data['rates']['IRR'] ?? 425000, 2),
|
||||
];
|
||||
}
|
||||
|
||||
private function fetchFromFreeCurrencyApi(): ?array
|
||||
{
|
||||
$response = Http::get('https://free.currencyapi.com/api/v3/latest', [
|
||||
'apikey' => config('ifnex.currency_api_key'),
|
||||
'base_currency' => 'AED',
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
if (!isset($data['data'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'aed_to_irr' => round(($data['data']['IRR']['value'] ?? 455000) * 100, 2),
|
||||
'usd_to_irr' => round(($data['data']['IRR']['value'] ?? 425000) * 100, 2),
|
||||
];
|
||||
}
|
||||
|
||||
private function setFallbackRates(): void
|
||||
{
|
||||
SystemSetting::updateOrCreate(
|
||||
['key' => 'aed_to_irr'],
|
||||
['value' => '455000', 'type' => 'number', 'description' => 'AED to IRR exchange rate']
|
||||
);
|
||||
SystemSetting::updateOrCreate(
|
||||
['key' => 'usd_to_irr'],
|
||||
['value' => '425000', 'type' => 'number', 'description' => 'USD to IRR exchange rate']
|
||||
);
|
||||
$this->info("Fallback rates set: AED/IRR=455000, USD/IRR=425000");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Models\DiscountCode;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class DiscountCodeController
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$codes = DiscountCode::where('is_active', true)
|
||||
->where(function ($q) {
|
||||
$q->whereNull('expires_at')->orWhere('expires_at', '>=', now());
|
||||
})
|
||||
->where(function ($q) {
|
||||
$q->whereNull('usage_limit')->orWhereColumn('used_count', '<', 'usage_limit');
|
||||
})
|
||||
->get(['code', 'type', 'value', 'min_order_amount', 'expires_at']);
|
||||
|
||||
return response()->json(['codes' => $codes]);
|
||||
}
|
||||
|
||||
public function validate(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$validated = $request->validate([
|
||||
'code' => ['required', 'string'],
|
||||
'total_price' => ['required', 'numeric', 'min:0'],
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed',
|
||||
'errors' => $e->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$code = DiscountCode::where('code', $validated['code'])->first();
|
||||
|
||||
if (!$code) {
|
||||
return response()->json([
|
||||
'valid' => false,
|
||||
'message' => 'کد تخفیف نامعتبر است.',
|
||||
]);
|
||||
}
|
||||
|
||||
if (!$code->is_active) {
|
||||
return response()->json([
|
||||
'valid' => false,
|
||||
'message' => 'این کد تخفیف غیرفعال شده است.',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($code->expires_at && $code->expires_at->isPast()) {
|
||||
return response()->json([
|
||||
'valid' => false,
|
||||
'message' => 'این کد تخفیف منقضی شده است.',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($code->usage_limit && $code->used_count >= $code->usage_limit) {
|
||||
return response()->json([
|
||||
'valid' => false,
|
||||
'message' => 'ظرفیت استفاده از این کد تخفیف به پایان رسیده است.',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($code->min_order_amount > 0 && $validated['total_price'] < $code->min_order_amount) {
|
||||
return response()->json([
|
||||
'valid' => false,
|
||||
'message' => "حداقل مبلغ سفارش برای استفاده از این کد " . number_format($code->min_order_amount) . " ریال است.",
|
||||
]);
|
||||
}
|
||||
|
||||
$discountAmount = 0;
|
||||
|
||||
if ($code->type === 'percentage') {
|
||||
$discountAmount = ($validated['total_price'] * $code->value) / 100;
|
||||
} elseif ($code->type === 'fixed') {
|
||||
$discountAmount = $code->value;
|
||||
}
|
||||
|
||||
$discountAmount = min($discountAmount, $validated['total_price']);
|
||||
$finalPrice = $validated['total_price'] - $discountAmount;
|
||||
|
||||
return response()->json([
|
||||
'valid' => true,
|
||||
'code' => $code->code,
|
||||
'type' => $code->type,
|
||||
'value' => $code->value,
|
||||
'discount_amount' => $discountAmount,
|
||||
'final_price' => $finalPrice,
|
||||
]);
|
||||
}
|
||||
}
|
||||
164
04_Laravel/app/Http/Controllers/Api/WalletController.php
Normal file
164
04_Laravel/app/Http/Controllers/Api/WalletController.php
Normal file
@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Wallet;
|
||||
use App\Services\WalletService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class WalletController
|
||||
{
|
||||
public function __construct(protected WalletService $walletService) {}
|
||||
|
||||
public function balance(Request $request): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if (!$user) {
|
||||
return response()->json(['message' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
$wallet = $user->wallet()->first();
|
||||
|
||||
if (!$wallet) {
|
||||
return response()->json([
|
||||
'balance' => 0,
|
||||
'currency' => 'IRR',
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'balance' => $wallet->balance,
|
||||
'currency' => 'IRR',
|
||||
]);
|
||||
}
|
||||
|
||||
public function recharge(Request $request): JsonResponse
|
||||
{
|
||||
try {
|
||||
$validated = $request->validate([
|
||||
'amount' => ['required', 'numeric', 'min:10000'],
|
||||
'description' => ['nullable', 'string', 'max:500'],
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed',
|
||||
'errors' => $e->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
if (!$user) {
|
||||
return response()->json(['message' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
$wallet = $user->wallet()->first();
|
||||
|
||||
if (!$wallet) {
|
||||
$wallet = Wallet::create([
|
||||
'user_id' => $user->id,
|
||||
'balance' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
$transaction = $this->walletService->deposit(
|
||||
$wallet,
|
||||
$validated['amount'],
|
||||
$validated['description'] ?? 'شارژ آنلاین کیف پول',
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'شارژ با موفقیت انجام شد',
|
||||
'wallet_id' => $wallet->id,
|
||||
'balance' => $wallet->fresh()->balance,
|
||||
'transaction_id' => $transaction->id,
|
||||
'amount' => $transaction->amount,
|
||||
]);
|
||||
}
|
||||
|
||||
public function transactions(Request $request): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if (!$user) {
|
||||
return response()->json(['message' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
$wallet = $user->wallet()->first();
|
||||
|
||||
if (!$wallet) {
|
||||
return response()->json(['transactions' => [], 'total' => 0]);
|
||||
}
|
||||
|
||||
$perPage = $request->query('per_page', 15);
|
||||
|
||||
$transactions = $wallet->transactions()
|
||||
->orderByDesc('created_at')
|
||||
->paginate($perPage);
|
||||
|
||||
return response()->json([
|
||||
'transactions' => $transactions->map(fn ($t) => [
|
||||
'id' => $t->id,
|
||||
'amount' => $t->amount,
|
||||
'type' => $t->type,
|
||||
'description' => $t->description,
|
||||
'created_at' => $t->created_at->toIso8601String(),
|
||||
]),
|
||||
'total' => $transactions->total(),
|
||||
'current_page' => $transactions->currentPage(),
|
||||
'last_page' => $transactions->lastPage(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function adminRecharge(Request $request): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if (!$user || !$user->isSuperAdmin()) {
|
||||
return response()->json(['message' => 'Forbidden'], 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$validated = $request->validate([
|
||||
'user_id' => ['required', 'exists:users,id'],
|
||||
'amount' => ['required', 'numeric', 'min:1'],
|
||||
'description' => ['nullable', 'string', 'max:500'],
|
||||
]);
|
||||
} catch (ValidationException $e) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed',
|
||||
'errors' => $e->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$targetUser = User::findOrFail($validated['user_id']);
|
||||
$wallet = $targetUser->wallet()->first();
|
||||
|
||||
if (!$wallet) {
|
||||
$wallet = Wallet::create([
|
||||
'user_id' => $targetUser->id,
|
||||
'balance' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
$transaction = $this->walletService->deposit(
|
||||
$wallet,
|
||||
$validated['amount'],
|
||||
$validated['description'] ?? 'شارژ دستی توسط ادمین',
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'message' => 'شارژ دستی با موفقیت انجام شد',
|
||||
'wallet_id' => $wallet->id,
|
||||
'target_user_id' => $targetUser->id,
|
||||
'balance' => $wallet->fresh()->balance,
|
||||
'transaction_id' => $transaction->id,
|
||||
'amount' => $transaction->amount,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -14,6 +14,7 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
->withCommands([
|
||||
\App\Console\Commands\ImportShippingRates::class,
|
||||
\App\Console\Commands\ImportTrackingData::class,
|
||||
\App\Console\Commands\UpdateExchangeRates::class,
|
||||
])
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
$middleware->api(prepend: \Illuminate\Http\Middleware\HandleCors::class);
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
<?php
|
||||
require 'vendor/autoload.php';
|
||||
$app = require_once 'bootstrap/app.php';
|
||||
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
|
||||
$kernel->bootstrap();
|
||||
|
||||
$shipments = App\Models\Shipment::all();
|
||||
echo 'Total shipments: ' . $shipments->count() . PHP_EOL;
|
||||
foreach ($shipments as $s) {
|
||||
echo 'AWB: ' . $s->awb_no . PHP_EOL;
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
<?php
|
||||
require 'vendor/autoload.php';
|
||||
$app = require_once 'bootstrap/app.php';
|
||||
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
|
||||
$kernel->bootstrap();
|
||||
|
||||
$shipment = App\Models\Shipment::where('awb_no', '980103619')->first();
|
||||
echo 'Shipment: ' . ($shipment ? $shipment->awb_no : 'Not found') . PHP_EOL;
|
||||
|
||||
if ($shipment) {
|
||||
echo 'Events: ' . $shipment->trackingEvents()->count() . PHP_EOL;
|
||||
foreach ($shipment->trackingEvents()->orderBy('event_date', 'desc')->limit(3)->get() as $e) {
|
||||
echo $e->event_date . ' - ' . $e->event_description . PHP_EOL;
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@ return [
|
||||
|
||||
'allowed_methods' => ['*'],
|
||||
|
||||
'allowed_origins' => ['*'], // برای لوکال همه رو اجازه میدیم، سرور واقعی دامنه دقیق میزنیم
|
||||
'allowed_origins' => explode(',', env('CORS_ALLOWED_ORIGINS', 'http://localhost,http://127.0.0.1')),
|
||||
|
||||
'allowed_origins_patterns' => [],
|
||||
|
||||
|
||||
@ -22,4 +22,6 @@ return [
|
||||
'returned',
|
||||
],
|
||||
|
||||
'currency_api_key' => env('CURRENCY_API_KEY'),
|
||||
|
||||
];
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('countries', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->integer('export_zone');
|
||||
$table->integer('import_zone');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('countries');
|
||||
}
|
||||
};
|
||||
@ -1,31 +0,0 @@
|
||||
<?php
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('shipping_rates', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('direction'); // Enum: 'import', 'export'
|
||||
$table->string('type'); // Enum: 'DOCUMENT', 'NON DOC'
|
||||
$table->decimal('weight', 8, 2);
|
||||
$table->decimal('zone_1', 10, 2);
|
||||
$table->decimal('zone_2', 10, 2);
|
||||
$table->decimal('zone_3', 10, 2);
|
||||
$table->decimal('zone_4', 10, 2);
|
||||
$table->decimal('zone_5', 10, 2);
|
||||
$table->decimal('zone_6', 10, 2);
|
||||
$table->decimal('zone_7', 10, 2);
|
||||
$table->decimal('zone_8', 10, 2);
|
||||
$table->decimal('zone_9', 10, 2);
|
||||
$table->decimal('zone_10', 10, 2);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('shipping_rates');
|
||||
}
|
||||
};
|
||||
@ -1,62 +0,0 @@
|
||||
<?php
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('shipments', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('awb_no')->unique();
|
||||
$table->string('forwarder')->nullable();
|
||||
$table->string('forwarder_track_id')->nullable();
|
||||
$table->string('direction'); // Enum: 'Outbound', 'Inbound'
|
||||
$table->string('type'); // Enum: 'DOC', 'NON DOC'
|
||||
$table->string('status');
|
||||
$table->string('reason_for_export');
|
||||
|
||||
// Weight Info
|
||||
$table->decimal('weight', 8, 2);
|
||||
$table->decimal('volumetric_weight', 8, 2);
|
||||
$table->decimal('chargeable_weight', 8, 2);
|
||||
$table->string('dimensions');
|
||||
|
||||
// Financial Info
|
||||
$table->decimal('shipping_price', 12, 2);
|
||||
$table->decimal('extra_service', 12, 2)->default(0);
|
||||
$table->decimal('packing_cost', 12, 2)->default(0);
|
||||
$table->decimal('discount', 12, 2)->default(0);
|
||||
$table->decimal('total_fee', 12, 2);
|
||||
$table->decimal('net_dirham', 12, 2);
|
||||
$table->decimal('net_rial', 12, 2);
|
||||
|
||||
// Sender Info
|
||||
$table->string('sender_name');
|
||||
$table->string('sender_company');
|
||||
$table->string('sender_phone');
|
||||
$table->string('sender_email');
|
||||
$table->string('sender_address');
|
||||
$table->string('sender_city');
|
||||
$table->string('sender_zip');
|
||||
$table->string('sender_id_number');
|
||||
|
||||
// Receiver Info
|
||||
$table->string('receiver_name');
|
||||
$table->string('receiver_company');
|
||||
$table->string('receiver_phone');
|
||||
$table->string('receiver_email');
|
||||
$table->string('receiver_address');
|
||||
$table->string('receiver_city');
|
||||
$table->string('receiver_zip');
|
||||
$table->string('receiver_id_number');
|
||||
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('shipments');
|
||||
}
|
||||
};
|
||||
@ -1,25 +0,0 @@
|
||||
<?php
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('shipment_items', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('shipment_id')->constrained()->cascadeOnDelete();
|
||||
$table->tinyInteger('row_number'); // 1 to 9
|
||||
$table->string('description');
|
||||
$table->string('hs_code');
|
||||
$table->integer('quantity');
|
||||
$table->decimal('unit_price', 12, 2);
|
||||
$table->decimal('total_usd', 12, 2);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('shipment_items');
|
||||
}
|
||||
};
|
||||
@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('system_settings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->decimal('dirham_rate', 15, 2)->default(0);
|
||||
$table->decimal('dollar_rate', 15, 2)->default(0);
|
||||
$table->decimal('yuan_rate', 15, 2)->default(0);
|
||||
$table->decimal('euro_rate', 15, 2)->default(0);
|
||||
$table->decimal('profit_percent', 5, 2)->default(1.25); // همون 1.25 اکسل
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('system_settings');
|
||||
}
|
||||
};
|
||||
@ -1,16 +0,0 @@
|
||||
<?php
|
||||
require 'vendor/autoload.php';
|
||||
$app = require_once 'bootstrap/app.php';
|
||||
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
|
||||
$kernel->bootstrap();
|
||||
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use App\Imports\TrackingDataImport;
|
||||
|
||||
Excel::import(new TrackingDataImport(), '../01_Documents/Data entry 2026-06-28.xlsx');
|
||||
|
||||
$import = app(TrackingDataImport::class);
|
||||
echo 'Imported: ' . $import->getImportedEvents() . PHP_EOL;
|
||||
echo 'Skipped: ' . $import->getSkippedEvents() . PHP_EOL;
|
||||
echo 'Created: ' . $import->getCreatedShipments() . PHP_EOL;
|
||||
echo 'Errors: ' . count($import->getErrors()) . PHP_EOL;
|
||||
@ -1,24 +0,0 @@
|
||||
<?php
|
||||
require 'vendor/autoload.php';
|
||||
$app = require_once 'bootstrap/app.php';
|
||||
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
|
||||
$kernel->bootstrap();
|
||||
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use Maatwebsite\Excel\Concerns\ToCollection;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadingRow;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class DebugImport implements ToCollection, WithHeadingRow
|
||||
{
|
||||
public function collection(Collection $rows)
|
||||
{
|
||||
echo 'Total rows: ' . $rows->count() . PHP_EOL;
|
||||
if ($rows->count() > 0) {
|
||||
echo 'First row keys: ' . implode(', ', $rows->first()->keys()->toArray()) . PHP_EOL;
|
||||
echo 'First row AWB: ' . ($rows->first()['AWB'] ?? 'N/A') . PHP_EOL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Excel::import(new DebugImport(), '../01_Documents/Data entry 2026-06-28.xlsx');
|
||||
@ -1,35 +0,0 @@
|
||||
<?php
|
||||
require 'C:/xampp/htdocs/IFNEX-Logistics/04_Laravel/vendor/autoload.php';
|
||||
|
||||
$reader = new \PhpOffice\PhpSpreadsheet\IOFactory();
|
||||
$spreadsheet = $reader->load('C:/xampp/htdocs/IFNEX-Logistics/04_Laravel/storage/app/public/01KYWGVNKS5TNMN37RV77PCYNZ.xlsx');
|
||||
$sheets = $spreadsheet->getSheetNames();
|
||||
echo "=== ALL SHEET NAMES ===" . PHP_EOL;
|
||||
echo implode(', ', $sheets) . PHP_EOL . PHP_EOL;
|
||||
|
||||
// Inspect AWB sheet
|
||||
echo "=== AWB SHEET ===" . PHP_EOL;
|
||||
$awbSheet = $spreadsheet->getSheetByName('AWB');
|
||||
if ($awbSheet) {
|
||||
echo "Dimensions: " . $awbSheet->calculateWorksheetDimensions() . PHP_EOL;
|
||||
echo "Highest row: " . $awbSheet->getHighestRow() . PHP_EOL;
|
||||
echo "Highest column: " . $awbSheet->getHighestColumn() . PHP_EOL;
|
||||
echo PHP_EOL;
|
||||
|
||||
// Print first 50 rows
|
||||
for ($row = 1; $row <= min(50, $awbSheet->getHighestRow()); $row++) {
|
||||
$rowData = [];
|
||||
for ($col = 1; $col <= 15; $col++) {
|
||||
$cell = $awbSheet->getCellByColumnAndRow($col, $row);
|
||||
$value = $cell->getValue();
|
||||
if ($value !== null) {
|
||||
$rowData[] = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($col) . $row . "=" . json_encode($value);
|
||||
}
|
||||
}
|
||||
if (!empty($rowData)) {
|
||||
echo "Row $row: " . implode(" | ", $rowData) . PHP_EOL;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
echo "AWB sheet not found!" . PHP_EOL;
|
||||
}
|
||||
@ -1,13 +1,23 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Api\DiscountCodeController;
|
||||
use App\Http\Controllers\Api\PricingController;
|
||||
use App\Http\Controllers\Api\TrackController;
|
||||
use App\Http\Controllers\Api\WalletController;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Middleware\ApiKeyMiddleware;
|
||||
|
||||
Route::middleware([ApiKeyMiddleware::class])->prefix('v1')->group(function () {
|
||||
Route::get('/track/{awb_no}', [TrackController::class, 'show']);
|
||||
|
||||
Route::get('/wallet/balance', [WalletController::class, 'balance']);
|
||||
Route::post('/wallet/recharge', [WalletController::class, 'recharge']);
|
||||
Route::get('/wallet/transactions', [WalletController::class, 'transactions']);
|
||||
Route::post('/wallet/admin-recharge', [WalletController::class, 'adminRecharge']);
|
||||
});
|
||||
|
||||
Route::post('/v1/calculate', [PricingController::class, 'calculate']);
|
||||
|
||||
Route::get('/v1/discount-codes', [DiscountCodeController::class, 'index']);
|
||||
Route::post('/v1/discount-codes/validate', [DiscountCodeController::class, 'validate']);
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
<?php
|
||||
require 'vendor/autoload.php';
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
$file = '../01_Documents/Data entry 2026-06-28.xlsx';
|
||||
$spreadsheet = IOFactory::load($file);
|
||||
$sheetNames = $spreadsheet->getSheetNames();
|
||||
echo 'Sheets: ' . implode(', ', $sheetNames) . PHP_EOL;
|
||||
$sheet = $spreadsheet->getSheet(0);
|
||||
echo 'Sheet 0 name: ' . $sheet->getTitle() . PHP_EOL;
|
||||
echo 'Row 1: ' . implode(', ', $sheet->rangeToArray('A1:H1')[0]) . PHP_EOL;
|
||||
echo 'Row 2: ' . implode(', ', $sheet->rangeToArray('A2:H2')[0]) . PHP_EOL;
|
||||
echo 'Row 3: ' . implode(', ', $sheet->rangeToArray('A3:H3')[0]) . PHP_EOL;
|
||||
echo 'Total rows: ' . $sheet->getHighestRow() . PHP_EOL;
|
||||
@ -1,77 +0,0 @@
|
||||
<?php
|
||||
require 'vendor/autoload.php';
|
||||
$app = require_once 'bootstrap/app.php';
|
||||
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
|
||||
$kernel->bootstrap();
|
||||
|
||||
$service = new App\Services\PriceCalculatorService();
|
||||
|
||||
$testCases = [
|
||||
[
|
||||
'name' => 'PARCEL Export to AE 1kg',
|
||||
'data' => [
|
||||
'type' => 'PARCEL',
|
||||
'direction' => 'Outbound',
|
||||
'country_iso' => 'AE',
|
||||
'weight' => 1,
|
||||
'volumetric_weight' => 1,
|
||||
'extra_service' => 0,
|
||||
'packing_cost' => 100000,
|
||||
'domestic_pickup' => 0,
|
||||
'domestic_delivery' => 0,
|
||||
'warehousing_cost' => 0,
|
||||
'discount' => 0,
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'DOC_NORMAL Export to DE 0.5kg',
|
||||
'data' => [
|
||||
'type' => 'DOC_NORMAL',
|
||||
'direction' => 'Outbound',
|
||||
'country_iso' => 'DE',
|
||||
'weight' => 0.5,
|
||||
'volumetric_weight' => 0.5,
|
||||
'extra_service' => 0,
|
||||
'packing_cost' => 100000,
|
||||
'domestic_pickup' => 0,
|
||||
'domestic_delivery' => 0,
|
||||
'warehousing_cost' => 0,
|
||||
'discount' => 0,
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'PARCEL Import from US 5kg',
|
||||
'data' => [
|
||||
'type' => 'PARCEL',
|
||||
'direction' => 'Inbound',
|
||||
'country_iso' => 'US',
|
||||
'weight' => 5,
|
||||
'volumetric_weight' => 5,
|
||||
'extra_service' => 50000,
|
||||
'packing_cost' => 150000,
|
||||
'domestic_pickup' => 20000,
|
||||
'domestic_delivery' => 30000,
|
||||
'warehousing_cost' => 50000,
|
||||
'discount' => 10000,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($testCases as $case) {
|
||||
echo "=== {$case['name']} ===" . PHP_EOL;
|
||||
try {
|
||||
$result = $service->calculate($case['data']);
|
||||
echo "Base Price (AED): " . $result['base_price'] . PHP_EOL;
|
||||
echo "Net Dirham: " . $result['net_dirham'] . PHP_EOL;
|
||||
echo "Net Rial: " . number_format($result['net_rial'], 0) . PHP_EOL;
|
||||
echo "Extra Service: " . number_format($result['extra_service'], 0) . PHP_EOL;
|
||||
echo "Packing Cost: " . number_format($result['packing_cost'], 0) . PHP_EOL;
|
||||
echo "VAT Amount: " . number_format($result['vat_amount'], 0) . PHP_EOL;
|
||||
echo "Total Fee: " . number_format($result['total_fee'], 0) . PHP_EOL;
|
||||
echo "Zone: " . $result['zone'] . PHP_EOL;
|
||||
echo "Chargeable Weight: " . $result['chargeable_weight'] . PHP_EOL;
|
||||
} catch (Exception $e) {
|
||||
echo "ERROR: " . $e->getMessage() . PHP_EOL;
|
||||
}
|
||||
echo PHP_EOL;
|
||||
}
|
||||
@ -1,77 +0,0 @@
|
||||
<?php
|
||||
require 'vendor/autoload.php';
|
||||
$app = require_once 'bootstrap/app.php';
|
||||
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
|
||||
$kernel->bootstrap();
|
||||
|
||||
$service = new App\Services\PriceCalculatorService();
|
||||
|
||||
$testCases = [
|
||||
[
|
||||
'name' => 'DOC_NORMAL Export to DE 0.5kg',
|
||||
'data' => [
|
||||
'type' => 'DOC_NORMAL',
|
||||
'direction' => 'Outbound',
|
||||
'country_iso' => 'DE',
|
||||
'weight' => 0.5,
|
||||
'volumetric_weight' => 0.5,
|
||||
'extra_service' => 0,
|
||||
'packing_cost' => 100000,
|
||||
'domestic_pickup' => 0,
|
||||
'domestic_delivery' => 0,
|
||||
'warehousing_cost' => 0,
|
||||
'discount' => 0,
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'PARCEL Export to AE 1kg',
|
||||
'data' => [
|
||||
'type' => 'PARCEL',
|
||||
'direction' => 'Outbound',
|
||||
'country_iso' => 'AE',
|
||||
'weight' => 1,
|
||||
'volumetric_weight' => 1,
|
||||
'extra_service' => 0,
|
||||
'packing_cost' => 100000,
|
||||
'domestic_pickup' => 0,
|
||||
'domestic_delivery' => 0,
|
||||
'warehousing_cost' => 0,
|
||||
'discount' => 0,
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'PARCEL Import from US 5kg',
|
||||
'data' => [
|
||||
'type' => 'PARCEL',
|
||||
'direction' => 'Inbound',
|
||||
'country_iso' => 'US',
|
||||
'weight' => 5,
|
||||
'volumetric_weight' => 5,
|
||||
'extra_service' => 50000,
|
||||
'packing_cost' => 150000,
|
||||
'domestic_pickup' => 20000,
|
||||
'domestic_delivery' => 30000,
|
||||
'warehousing_cost' => 50000,
|
||||
'discount' => 10000,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($testCases as $case) {
|
||||
echo "=== {$case['name']} ===" . PHP_EOL;
|
||||
try {
|
||||
$result = $service->calculate($case['data']);
|
||||
echo "Base Price (AED): " . $result['base_price'] . PHP_EOL;
|
||||
echo "Net Dirham: " . $result['net_dirham'] . PHP_EOL;
|
||||
echo "Net Rial: " . number_format($result['net_rial'], 0) . PHP_EOL;
|
||||
echo "Extra Service: " . number_format($result['extra_service'], 0) . PHP_EOL;
|
||||
echo "Packing Cost: " . number_format($result['packing_cost'], 0) . PHP_EOL;
|
||||
echo "VAT Amount: " . number_format($result['vat_amount'], 0) . PHP_EOL;
|
||||
echo "Total Fee: " . number_format($result['total_fee'], 0) . PHP_EOL;
|
||||
echo "Zone: " . $result['zone'] . PHP_EOL;
|
||||
echo "Chargeable Weight: " . $result['chargeable_weight'] . PHP_EOL;
|
||||
} catch (Exception $e) {
|
||||
echo "ERROR: " . $e->getMessage() . PHP_EOL;
|
||||
}
|
||||
echo PHP_EOL;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user