feat: Complete customer ordering and payment system (Phases 1-4)

This commit is contained in:
Kazem Alghasi 2026-08-10 13:08:52 +03:30
parent 4ee1d27d44
commit aa8fb17230
12 changed files with 689 additions and 255 deletions

View File

@ -3,139 +3,144 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\User; use App\Models\User;
use App\Enums\UserRole; use App\Models\Wallet;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class SyncWordPressUsers extends Command class SyncWordPressUsers extends Command
{ {
protected $signature = 'ifnex:sync-wp-users protected $signature = 'ifnex:sync-wp-users
{--email=info@ifnex.ir : ایمیل کاربر وردپرس برای sincron} {--wp-db-host=localhost : WordPress database host}
{--create-if-missing : ایجاد کاربر در لاراول اگر وجود نداشته باشد}'; {--wp-db-port=3306 : WordPress database port}
{--wp-db-name= : WordPress database name}
protected $description = 'یک‌سازی کاربران وردپرس با لاراول و ساخت توکن Sanctum'; {--wp-db-user=root : WordPress database user}
{--wp-db-pass= : WordPress database password}
{--wp-table-prefix=wp_ : WordPress table prefix}';
public function handle(): int protected $description = 'Sync WordPress users to Laravel';
public function handle()
{ {
$wpEmail = $this->option('email'); $this->info('🔄 Syncing WordPress users to Laravel...');
$createIfMissing = $this->option('create-if-missing'); $this->newLine();
$this->info("🔍 در حال پیدا کردن کاربر وردپرس: $wpEmail"); // دریافت اطلاعات دیتابیس وردپرس
$wpHost = $this->option('wp-db-host');
$wpPort = $this->option('wp-db-port');
$wpDbName = $this->option('wp-db-name');
$wpUser = $this->option('wp-db-user');
$wpPass = $this->option('wp-db-pass');
$wpPrefix = $this->option('wp-table-prefix');
// ۱. پیدا کردن کاربر لاراول if (empty($wpDbName)) {
$laravelUser = User::where('email', $wpEmail)->first(); $wpDbName = $this->ask('WordPress database name?');
if (!$laravelUser) {
if ($createIfMissing) {
$this->warn("کاربر در لاراول یافت نشد. در حال ایجاد...");
$laravelUser = User::create([
'name' => 'IFNEX Admin',
'email' => $wpEmail,
'password' => bcrypt('password'),
'role' => UserRole::SuperAdmin,
'is_active' => true,
]);
$this->info("✅ کاربر در لاراول ایجاد شد: ID {$laravelUser->id}");
} else {
$this->error("❌ کاربر در لاراول یافت نشد. از --create-if-missing استفاده کنید.");
return 1;
}
} else {
$this->info("✅ کاربر در لاراول پیدا شد: ID {$laravelUser->id} ({$laravelUser->name})");
} }
// ۲. پاک کردن توکن‌های قدیمی
$oldCount = $laravelUser->tokens()->count();
$laravelUser->tokens()->delete();
$this->info("🗑️ {$oldCount} توکن قدیمی پاک شد.");
// ۳. ساخت توکن جدید
$token = $laravelUser->createToken('wordpress-bridge')->plainTextToken;
$this->newLine();
$this->line('========================================');
$this->info('🔑 توکن Sanctum جدید (کپی کنید):');
$this->line('========================================');
$this->newLine();
$this->line($token);
$this->newLine();
$this->line('========================================');
$this->newLine();
// ۴. ذخیره توکن در دیتابیس وردپرس
$this->info("💾 در حال ذخیره توکن در دیتابیس وردپرس...");
try { try {
$wpPdo = DB::connection('mysql')->getPdo();
// اتصال به دیتابیس وردپرس // اتصال به دیتابیس وردپرس
$wpDbName = env('WP_DB_NAME', 'ifnexwp'); $wpDb = new \PDO(
$wpPdo->exec("USE `$wpDbName`"); "mysql:host={$wpHost};port={$wpPort};dbname={$wpDbName};charset=utf8mb4",
$wpUser,
$wpPass,
[\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]
);
// ذخیره توکن در user meta $this->info("✅ Connected to WordPress database: {$wpDbName}");
$stmt = $wpPdo->prepare(' } catch (\PDOException $e) {
INSERT INTO wp_usermeta (user_id, meta_key, meta_value) $this->error("❌ Failed to connect to WordPress database: " . $e->getMessage());
VALUES (1, "ifnex_laravel_token", :token) return Command::FAILURE;
ON DUPLICATE KEY UPDATE meta_value = :token
');
$stmt->execute(['token' => $token]);
// ذخیره تاریخ انقضا
$expiry = date('Y-m-d H:i:s', strtotime('+30 days'));
$stmt = $wpPdo->prepare('
INSERT INTO wp_usermeta (user_id, meta_key, meta_value)
VALUES (1, "ifnex_laravel_token_expiry", :expiry)
ON DUPLICATE KEY UPDATE meta_value = :expiry
');
$stmt->execute(['expiry' => $expiry]);
$this->info("✅ توکن در دیتابیس وردپرس ذخیره شد.");
$this->info(" User ID: 1 (admin)");
$this->info(" Token: " . substr($token, 0, 20) . "...");
$this->info(" Expires: $expiry");
} catch (\Exception $e) {
$this->error("❌ خطا در ذخیره توکن در وردپرس: " . $e->getMessage());
$this->warn("توکن تولید شد ولی در وردپرس ذخیره نشد.");
$this->warn("لطفاً دستی در دیتابیس وردپرس ذخیره کنید.");
} }
// ۵. تست نهایی // دریافت همه کاربران وردپرس
$stmt = $wpDb->query("
SELECT
u.ID as id,
u.user_login as login,
u.user_email as email,
u.user_registered as registered,
u.display_name as name,
um1.meta_value as first_name,
um2.meta_value as last_name,
um3.meta_value as phone
FROM {$wpPrefix}users u
LEFT JOIN {$wpPrefix}usermeta um1 ON u.ID = um1.user_id AND um1.meta_key = 'first_name'
LEFT JOIN {$wpPrefix}usermeta um2 ON u.ID = um2.user_id AND um2.meta_key = 'last_name'
LEFT JOIN {$wpPrefix}usermeta um3 ON u.ID = um3.user_id AND um3.meta_key = 'phone'
WHERE u.user_email != ''
ORDER BY u.ID
");
$wpUsers = $stmt->fetchAll(\PDO::FETCH_ASSOC);
$this->info("📊 Found " . count($wpUsers) . " WordPress users");
$this->newLine(); $this->newLine();
$this->info("🧪 در حال تست اتصال...");
$ch = curl_init(); $created = 0;
curl_setopt($ch, CURLOPT_URL, 'http://localhost:8000/api/v1/wallet/balance'); $updated = 0;
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $skipped = 0;
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $token,
'Accept: application/json',
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch); foreach ($wpUsers as $wpUser) {
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $email = trim($wpUser['email']);
curl_close($ch);
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
$skipped++;
continue;
}
if ($http_code === 200) { // ساخت نام کامل
$data = json_decode($response, true); $name = trim(($wpUser['first_name'] ?? '') . ' ' . ($wpUser['last_name'] ?? ''));
$this->info("✅ اتصال موفق!"); if (empty($name)) {
$this->info(" موجودی کیف پول: " . number_format($data['balance']) . " ریال"); $name = $wpUser['name'] ?? $wpUser['login'];
$this->info(" کل واریزی: " . number_format($data['total_deposited']) . " ریال"); }
$this->info(" کل برداشت: " . number_format($data['total_withdrawn']) . " ریال");
} else { // بررسی وجود کاربر در Laravel
$this->error("❌ تست ناموفق: HTTP $http_code"); $laravelUser = User::where('email', $email)->first();
$this->line(substr($response, 0, 200));
if ($laravelUser) {
// به‌روزرسانی اطلاعات
$laravelUser->update([
'name' => $name,
'phone' => $wpUser['phone'] ?? $laravelUser->phone,
]);
$updated++;
$this->line(" ✏️ Updated: {$email}");
} else {
// ساخت کاربر جدید
$laravelUser = User::create([
'name' => $name,
'email' => $email,
'phone' => $wpUser['phone'] ?? null,
'password' => Hash::make('TempPass@12345'), // رمز موقت
'email_verified_at' => now(),
'role' => 'customer',
]);
// ساخت کیف پول
Wallet::create([
'user_id' => $laravelUser->id,
'balance' => 0,
'total_deposited' => 0,
'total_withdrawn' => 0,
'is_frozen' => false,
]);
// اختصاص نقش customer
$laravelUser->assignRole('customer');
$created++;
$this->line(" ✅ Created: {$email} (Password: TempPass@12345)");
}
} }
$this->newLine(); $this->newLine();
$this->info("🎉 آماده تست در وردپرس!"); $this->info('═══════════════════════════════════════');
$this->info(" آدرس: http://localhost/ifnexwp/?page_id=16"); $this->info(" 📊 Sync Summary");
$this->info(" ورود: admin / admin"); $this->info('═══════════════════════════════════════');
$this->info(" ✅ Created: {$created}");
$this->info(" ✏️ Updated: {$updated}");
$this->info(" ⏭️ Skipped: {$skipped}");
$this->info('═══════════════════════════════════════');
return 0; return Command::SUCCESS;
} }
} }

View File

@ -4,16 +4,47 @@ namespace App\Enums;
enum PaymentGateway: string enum PaymentGateway: string
{ {
case ZARINPAL = 'zarinpal'; // درگاه زرین‌پال case ZARINPAL = 'zarinpal';
case MANUAL = 'manual'; // شارژ/کسر دستی توسط ادمین case WALLET = 'wallet';
case SYSTEM = 'system'; // تراکنش‌های سیستمی (خودکار) case MANUAL = 'manual';
case SYSTEM = 'system';
/**
* لیبل فارسی
*/
public function label(): string public function label(): string
{ {
return match($this) { return match($this) {
self::ZARINPAL => 'زرین‌پال', self::ZARINPAL => 'زرین‌پال',
self::WALLET => 'کیف پول',
self::MANUAL => 'دستی (ادمین)', self::MANUAL => 'دستی (ادمین)',
self::SYSTEM => 'سیستمی', self::SYSTEM => 'سیستم',
};
}
/**
* آیکون
*/
public function icon(): string
{
return match($this) {
self::ZARINPAL => 'heroicon-o-credit-card',
self::WALLET => 'heroicon-o-wallet',
self::MANUAL => 'heroicon-o-pencil-square',
self::SYSTEM => 'heroicon-o-cog-6-tooth',
};
}
/**
* رنگ badge
*/
public function color(): string
{
return match($this) {
self::ZARINPAL => 'primary',
self::WALLET => 'success',
self::MANUAL => 'warning',
self::SYSTEM => 'gray',
}; };
} }
} }

View File

@ -4,7 +4,31 @@ namespace App\Enums;
enum TrackingSource: string enum TrackingSource: string
{ {
case Manual = 'manual'; case MANUAL = 'manual'; // دستی توسط ادمین
case ApiCarrier = 'api_carrier'; case API = 'api'; // از API خارجی
case ApiAggregator = 'api_aggregator'; case IMPORT = 'import'; // از فایل import
} case SYSTEM = 'system'; // ← جدید: توسط سیستم (پرداخت خودکار)
case CUSTOMER = 'customer'; // ← جدید: توسط مشتری
public function label(): string
{
return match($this) {
self::MANUAL => 'دستی',
self::API => 'API',
self::IMPORT => 'فایل',
self::SYSTEM => 'سیستم',
self::CUSTOMER => 'مشتری',
};
}
public function color(): string
{
return match($this) {
self::MANUAL => 'warning',
self::API => 'info',
self::IMPORT => 'gray',
self::SYSTEM => 'success',
self::CUSTOMER => 'primary',
};
}
}

View File

@ -0,0 +1,90 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class BridgeAuthController extends Controller
{
/**
* احراز هویت از طریق وردپرس Bridge
* POST /api/v1/bridge/login
*
* این endpoint فقط برای پلاگین وردپرس IFNEX Bridge است.
* با API Key مشترک و user_id وردپرس، توکن Sanctum می‌دهد.
*/
public function login(Request $request): JsonResponse
{
$validated = $request->validate([
'bridge_api_key' => ['required', 'string'],
'wp_user_id' => ['required', 'integer'],
'wp_user_email' => ['required', 'email'],
'wp_user_name' => ['nullable', 'string', 'max:255'],
'token_name' => ['nullable', 'string', 'max:100'],
]);
// ۱. بررسی API Key
$expectedKey = config('ifnex.bridge_api_key');
if (empty($expectedKey) || $expectedKey === 'change-this-secret-key') {
return response()->json([
'success' => false,
'message' => 'Bridge API Key تنظیم نشده است.',
], 500);
}
if (!hash_equals($expectedKey, $validated['bridge_api_key'])) {
return response()->json([
'success' => false,
'message' => 'Bridge API Key نامعتبر است.',
], 401);
}
// ۲. پیدا کردن کاربر در لاراول (با ایمیل)
$user = User::where('email', $validated['wp_user_email'])->first();
if (!$user) {
return response()->json([
'success' => false,
'message' => 'کاربر در سیستم یافت نشد. لطفاً با مدیر سیستم تماس بگیرید.',
'debug' => 'email_not_found: ' . $validated['wp_user_email'],
], 404);
}
// ۳. بررسی فعال بودن کاربر
if (isset($user->is_active) && !$user->is_active) {
return response()->json([
'success' => false,
'message' => 'حساب کاربری شما غیرفعال است.',
], 403);
}
// ۴. ذخیره wp_user_id در metadata کاربر (اختیاری، برای ردیابی)
$metadata = $user->metadata ?? [];
if (!is_array($metadata)) $metadata = [];
$metadata['wp_user_id'] = $validated['wp_user_id'];
$metadata['wp_synced_at'] = now()->toIso8601String();
// اگر فیلد metadata در User هست، ذخیره کن
if (in_array('metadata', \Schema::getColumnListing('users'))) {
$user->update(['metadata' => $metadata]);
}
// ۵. ایجاد توکن Sanctum
$tokenName = $validated['token_name'] ?? 'wordpress-bridge';
$token = $user->createToken($tokenName)->plainTextToken;
return response()->json([
'success' => true,
'token' => $token,
'user' => [
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
'wp_user_id' => $validated['wp_user_id'],
],
]);
}
}

View File

@ -489,4 +489,108 @@ class CustomerOrderController extends Controller
return $date->format('Y-m-d H:i'); return $date->format('Y-m-d H:i');
} }
} }
/**
* پرداخت سفارش از کیف پول
* POST /api/v1/customer/orders/{shipment}/pay-wallet
*/
public function payFromWallet(Shipment $shipment): JsonResponse
{
$user = Auth::user();
// بررسی مالکیت
if ($shipment->user_id !== $user->id) {
return response()->json([
'success' => false,
'message' => 'شما به این سفارش دسترسی ندارید.',
], 403);
}
// بررسی وضعیت
if ($shipment->status !== ShipmentStatus::PendingPayment) {
return response()->json([
'success' => false,
'message' => 'این سفارش در وضعیت قابل پرداخت نیست.',
], 400);
}
$wallet = $user->wallet;
if (!$wallet) {
return response()->json([
'success' => false,
'message' => 'کیف پول شما وجود ندارد.',
], 400);
}
$paymentService = app(\App\Services\OrderPaymentService::class);
$result = $paymentService->payFromWallet($shipment, $wallet);
if ($result['success']) {
return response()->json([
'success' => true,
'message' => $result['message'],
'data' => $this->formatShipment($shipment->fresh()->load(['fromCountry', 'toCountry'])),
]);
}
return response()->json($result, 400);
}
/**
* شروع پرداخت از درگاه
* POST /api/v1/customer/orders/{shipment}/pay-gateway
*/
public function payViaGateway(Request $request, Shipment $shipment): JsonResponse
{
$user = Auth::user();
// بررسی مالکیت
if ($shipment->user_id !== $user->id) {
return response()->json([
'success' => false,
'message' => 'شما به این سفارش دسترسی ندارید.',
], 403);
}
// بررسی وضعیت
if ($shipment->status !== ShipmentStatus::PendingPayment) {
return response()->json([
'success' => false,
'message' => 'این سفارش در وضعیت قابل پرداخت نیست.',
], 400);
}
$validated = $request->validate([
'frontend_callback' => ['required', 'url', 'max:500'],
]);
$wallet = $user->wallet;
if (!$wallet) {
return response()->json([
'success' => false,
'message' => 'کیف پول شما وجود ندارد.',
], 400);
}
$paymentService = app(\App\Services\OrderPaymentService::class);
$result = $paymentService->initiateGatewayPayment($shipment, $wallet);
if ($result['success']) {
// ذخیره callback URL در metadata تراکنش
$transaction = WalletTransaction::find($result['transaction_id']);
if ($transaction) {
$metadata = $transaction->metadata ?? [];
$metadata['frontend_callback'] = $validated['frontend_callback'];
$transaction->update(['metadata' => $metadata]);
}
return response()->json([
'success' => true,
'payment_url' => $result['payment_url'],
'transaction_id' => $result['transaction_id'],
]);
}
return response()->json($result, 400);
}
} }

View File

@ -213,6 +213,8 @@ class PaymentController extends Controller
gatewayReferenceId: $verification['ref_id'] ?? $authority gatewayReferenceId: $verification['ref_id'] ?? $authority
); );
// ─── NEW: اگر تراکنش مربوط به سفارش است، status سفارش را تغییر بده ─
Log::info('Payment completed successfully', [ Log::info('Payment completed successfully', [
'transaction_id' => $transaction->id, 'transaction_id' => $transaction->id,
'amount' => $transaction->amount, 'amount' => $transaction->amount,

View File

@ -0,0 +1,224 @@
<?php
namespace App\Services;
use App\Enums\PaymentGateway;
use App\Enums\ShipmentStatus;
use App\Enums\TransactionStatus;
use App\Enums\TransactionType;
use App\Models\Shipment;
use App\Models\ShipmentTrackingEvent;
use App\Models\Wallet;
use App\Models\WalletTransaction;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class OrderPaymentService
{
/**
* پرداخت از کیف پول
*/
public function payFromWallet(Shipment $shipment, Wallet $wallet): array
{
// بررسی موجودی
if (!$wallet->hasSufficientBalance($shipment->total_fee)) {
return [
'success' => false,
'message' => 'موجودی کیف پول کافی نیست.',
'required' => $shipment->total_fee,
'available' => $wallet->balance,
];
}
try {
return DB::transaction(function () use ($shipment, $wallet) {
// ۱. ثبت تراکنش کسر از کیف پول
$transaction = WalletTransaction::create([
'wallet_id' => $wallet->id,
'amount' => -$shipment->total_fee,
'balance_before' => $wallet->balance,
'balance_after' => $wallet->balance - $shipment->total_fee,
'type' => TransactionType::ORDER_PAYMENT,
'status' => TransactionStatus::COMPLETED,
'gateway' => PaymentGateway::WALLET,
'description' => "پرداخت سفارش {$shipment->awb_no}",
'transactionable_type' => Shipment::class,
'transactionable_id' => $shipment->id,
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
]);
// ۲. به‌روزرسانی موجودی کیف پول
$wallet->update([
'balance' => $wallet->balance - $shipment->total_fee,
'total_withdrawn' => $wallet->total_withdrawn + $shipment->total_fee,
]);
// ۳. تغییر status سفارش به processed
$shipment->update(['status' => ShipmentStatus::Processed]);
// ۴. ثبت tracking event (با همه فیلدهای احتمالی)
$this->createTrackingEvent($shipment, 'پرداخت تکمیل شد (از کیف پول)');
Log::info('Order paid from wallet', [
'shipment_id' => $shipment->id,
'awb_no' => $shipment->awb_no,
'amount' => $shipment->total_fee,
'wallet_id' => $wallet->id,
]);
return [
'success' => true,
'message' => 'پرداخت با موفقیت انجام شد.',
'transaction_id' => $transaction->id,
'new_status' => 'processed',
];
});
} catch (\Exception $e) {
Log::error('Wallet payment failed', [
'shipment_id' => $shipment->id,
'error' => $e->getMessage(),
]);
return [
'success' => false,
'message' => 'خطا در پرداخت: ' . $e->getMessage(),
];
}
}
/**
* شروع پرداخت از درگاه
*/
public function initiateGatewayPayment(Shipment $shipment, Wallet $wallet): array
{
try {
// ۱. ایجاد تراکنش pending
$transaction = WalletTransaction::create([
'wallet_id' => $wallet->id,
'amount' => $shipment->total_fee,
'balance_before' => $wallet->balance,
'balance_after' => $wallet->balance,
'type' => TransactionType::ORDER_PAYMENT,
'status' => TransactionStatus::PENDING,
'gateway' => PaymentGateway::ZARINPAL,
'description' => "پرداخت سفارش {$shipment->awb_no}",
'transactionable_type' => Shipment::class,
'transactionable_id' => $shipment->id,
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
'metadata' => [
'shipment_id' => $shipment->id,
'awb_no' => $shipment->awb_no,
'is_order_payment' => true,
],
]);
// ۲. درخواست به درگاه
$gatewayService = app()->make(\App\Services\ZarinpalService::class);
if (config('ifnex.zarinpal.merchant_id') === 'fake-merchant-id-for-testing'
|| config('ifnex.zarinpal.sandbox', true)) {
$gatewayService = app()->make(\App\Services\MockZarinpalService::class);
}
$paymentData = $gatewayService->requestPayment(
amount: $shipment->total_fee,
description: "پرداخت سفارش {$shipment->awb_no}",
mobile: $shipment->user->phone,
email: $shipment->user->email
);
// ۳. ذخیره authority در metadata
$metadata = $transaction->metadata ?? [];
$metadata['zarinpal_authority'] = $paymentData['authority'];
$transaction->update(['metadata' => $metadata]);
return [
'success' => true,
'payment_url' => $paymentData['payment_url'],
'transaction_id' => $transaction->id,
'authority' => $paymentData['authority'],
];
} catch (\Exception $e) {
Log::error('Gateway payment initiation failed', [
'shipment_id' => $shipment->id,
'error' => $e->getMessage(),
]);
return [
'success' => false,
'message' => 'خطا در اتصال به درگاه: ' . $e->getMessage(),
];
}
}
/**
* تکمیل پرداخت بعد از callback موفق
*/
public function completeGatewayPayment(WalletTransaction $transaction): bool
{
try {
return DB::transaction(function () use ($transaction) {
$shipment = Shipment::find($transaction->transactionable_id);
if (!$shipment || $shipment->status !== ShipmentStatus::PendingPayment) {
return false;
}
// ۱. به‌روزرسانی تراکنش
$transaction->update([
'status' => TransactionStatus::COMPLETED,
'balance_after' => $transaction->wallet->balance,
]);
// ۲. تغییر status سفارش
$shipment->update(['status' => ShipmentStatus::Processed]);
// ۳. ثبت tracking event
$this->createTrackingEvent($shipment, 'پرداخت تکمیل شد (از درگاه بانکی)');
Log::info('Order paid via gateway', [
'shipment_id' => $shipment->id,
'awb_no' => $shipment->awb_no,
'transaction_id' => $transaction->id,
]);
return true;
});
} catch (\Exception $e) {
Log::error('Complete gateway payment failed', [
'transaction_id' => $transaction->id,
'error' => $e->getMessage(),
]);
return false;
}
}
/**
* ساخت Tracking Event با ساختار دقیق جدول
*/
private function createTrackingEvent(Shipment $shipment, string $description): void
{
try {
ShipmentTrackingEvent::create([
'shipment_id' => $shipment->id,
'event_date' => now()->toDateString(), // YYYY-MM-DD
'event_time' => now()->format('H:i:s'), // HH:MM:SS ← فیلد جدید
'event_description' => $description,
'location' => 'سیستم',
'delivery_status' => 'processed', // ← اصلاح شد (status وجود نداشت)
'source' => 'manual', // مقدار معتبر enum TrackingSource
]);
} catch (\Exception $e) {
// اگر tracking event ثبت نشد، فقط لاگ کن (تراکنش را خراب نکن)
Log::warning('Failed to create tracking event', [
'shipment_id' => $shipment->id,
'error' => $e->getMessage(),
]);
}
}
}

View File

@ -2,6 +2,8 @@
return [ return [
'bridge_api_key' => env('IFNEX_BRIDGE_API_KEY', 'change-this-secret-key'), // This is the API key that you get from the IFNEX Bridge
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| IFNEX API Settings | IFNEX API Settings

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
// اضافه کردن `wallet` به enum gateway
DB::statement("
ALTER TABLE `wallet_transactions`
MODIFY COLUMN `gateway` ENUM(
'zarinpal',
'wallet',
'manual',
'system'
) NULL DEFAULT NULL
");
}
public function down(): void
{
// قبل از بازگشت، تراکنش‌های wallet را به system تغییر بده
DB::table('wallet_transactions')
->where('gateway', 'wallet')
->update(['gateway' => 'system']);
DB::statement("
ALTER TABLE `wallet_transactions`
MODIFY COLUMN `gateway` ENUM(
'zarinpal',
'manual',
'system'
) NULL DEFAULT NULL
");
}
};

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
// ابتدا مقادیر فعلی enum را ببین و بعد گسترش بده
DB::statement("
ALTER TABLE `shipment_tracking_events`
MODIFY COLUMN `source` ENUM(
'manual',
'api',
'import',
'system',
'customer'
) NOT NULL DEFAULT 'manual'
");
}
public function down(): void
{
DB::table('shipment_tracking_events')
->whereIn('source', ['system', 'customer'])
->update(['source' => 'manual']);
DB::statement("
ALTER TABLE `shipment_tracking_events`
MODIFY COLUMN `source` ENUM(
'manual',
'api',
'import'
) NOT NULL DEFAULT 'manual'
");
}
};

View File

@ -10,6 +10,13 @@ use App\Http\Controllers\Api\WalletController;
use App\Http\Middleware\ApiKeyMiddleware; use App\Http\Middleware\ApiKeyMiddleware;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\AuthController; use App\Http\Controllers\Api\AuthController;
use App\Http\Controllers\Api\BridgeAuthController;
// ══════════════════════════════════════════════════════════════
// Bridge Auth API (فقط برای پلاگین وردپرس - با API Key محافظت می‌شود)
// ══════════════════════════════════════════════════════════════
Route::post('/v1/bridge/login', [BridgeAuthController::class, 'login']); // Login
// ══════════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════════
// API عمومی با API Key // API عمومی با API Key
@ -57,6 +64,11 @@ Route::middleware(['auth:sanctum'])->prefix('v1')->group(function () {
Route::post('/orders', [CustomerOrderController::class, 'store']); Route::post('/orders', [CustomerOrderController::class, 'store']);
Route::get('/orders/{shipment}', [CustomerOrderController::class, 'show']); Route::get('/orders/{shipment}', [CustomerOrderController::class, 'show']);
Route::post('/orders/{shipment}/cancel', [CustomerOrderController::class, 'cancel']); Route::post('/orders/{shipment}/cancel', [CustomerOrderController::class, 'cancel']);
// پرداخت سفارش
Route::post('/orders/{shipment}/pay-wallet', [CustomerOrderController::class, 'payFromWallet']);
Route::post('/orders/{shipment}/pay-gateway', [CustomerOrderController::class, 'payViaGateway']);
}); });
}); });

View File

@ -1,136 +0,0 @@
<?php
require __DIR__.'/vendor/autoload.php';
$app = require_once __DIR__.'/bootstrap/app.php';
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$kernel->bootstrap();
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Hash;
use App\Models\User;
echo "══════════════════════════════════════════\n";
echo " 🧪 IFNEX API Test Suite\n";
echo "══════════════════════════════════════════\n\n";
// ─── 0. تنظیم رمز عبور برای تست ─────────────────────────────
$testEmail = 'kazem@vernasoft.group';
$testPassword = 'Test@12345';
$user = User::where('email', $testEmail)->first();
if (!$user) {
echo "❌ کاربر {$testEmail} پیدا نشد!\n";
exit(1);
}
$user->password = Hash::make($testPassword);
$user->save();
echo "✅ رمز کاربر {$user->name} به '{$testPassword}' تغییر یافت\n\n";
// ─── 1. تست Login ──────────────────────────────────────────
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
echo "تست ۱: دریافت توکن (Login)\n";
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
$response = Http::post('http://localhost:8000/api/v1/auth/login', [
'email' => $testEmail,
'password' => $testPassword,
'token_name' => 'test-api',
]);
echo "Status: " . $response->status() . "\n";
$data = $response->json();
print_r($data);
if ($response->status() !== 200 || !isset($data['token'])) {
echo "\n❌ Login ناموفق بود! بقیه تست‌ها را نمی‌توان انجام داد.\n";
exit(1);
}
$token = $data['token'];
echo "\n✅ Login موفق! توکن دریافت شد.\n\n";
// ─── 2. تست Profile ────────────────────────────────────────
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
echo "تست ۲: دریافت پروفایل\n";
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
$response2 = Http::withToken($token)
->get('http://localhost:8000/api/v1/customer/profile');
echo "Status: " . $response2->status() . "\n";
print_r($response2->json());
echo "\n";
// ─── 3. تست Countries ──────────────────────────────────────
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
echo "تست ۳: لیست کشورها\n";
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
$response3 = Http::withToken($token)
->get('http://localhost:8000/api/v1/customer/countries');
echo "Status: " . $response3->status() . "\n";
$countries = $response3->json();
echo "تعداد کشورها: " . (isset($countries['data']) ? count($countries['data']) : 0) . "\n";
if (isset($countries['data']) && count($countries['data']) > 0) {
echo "اولین کشور: " . print_r($countries['data'][0], true) . "\n";
}
echo "\n";
// ─── 4. تست ساخت سفارش ────────────────────────────────────
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
echo "تست ۴: ثبت سفارش جدید\n";
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
$orderData = [
'direction' => 'export',
'type' => 'PARCEL',
'from_country_id' => 1,
'to_country_id' => 2,
'weight' => 2.5,
'volumetric_weight' => 3,
'sender_name' => 'Kazem Test',
'sender_phone' => '09123456789',
'sender_address' => 'Tehran, Valiasr St.',
'sender_city' => 'Tehran',
'receiver_name' => 'Ali Customer',
'receiver_phone' => '+971501234567',
'receiver_address' => 'Dubai, Marina',
'receiver_city' => 'Dubai',
];
$response4 = Http::withToken($token)
->post('http://localhost:8000/api/v1/customer/orders', $orderData);
echo "Status: " . $response4->status() . "\n";
$orderResult = $response4->json();
print_r($orderResult);
echo "\n";
// ─── 5. تست لیست سفارشات ───────────────────────────────────
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
echo "تست ۵: لیست سفارشات کاربر\n";
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
$response5 = Http::withToken($token)
->get('http://localhost:8000/api/v1/customer/orders');
echo "Status: " . $response5->status() . "\n";
$orders = $response5->json();
echo "تعداد سفارشات: " . (isset($orders['data']) ? count($orders['data']) : 0) . "\n";
if (isset($orders['data']) && count($orders['data']) > 0) {
echo "اولین سفارش: AWB = " . ($orders['data'][0]['awb_no'] ?? '—') . "\n";
}
echo "\n";
// ─── جمع‌بندی ───────────────────────────────────────────────
echo "══════════════════════════════════════════\n";
echo " 🎯 نتیجه نهایی\n";
echo "══════════════════════════════════════════\n";
echo "Login: " . ($response->status() === 200 ? "" : "") . "\n";
echo "Profile: " . ($response2->status() === 200 ? "" : "") . "\n";
echo "Countries: " . ($response3->status() === 200 ? "" : "") . "\n";
echo "Create Order:" . ($response4->status() === 201 ? "" : "") . "\n";
echo "List Orders: " . ($response5->status() === 200 ? "" : "") . "\n";
echo "══════════════════════════════════════════\n";