feat: Complete customer ordering and payment system (Phases 1-4)
This commit is contained in:
parent
4ee1d27d44
commit
aa8fb17230
@ -3,139 +3,144 @@
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Enums\UserRole;
|
||||
use App\Models\Wallet;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class SyncWordPressUsers extends Command
|
||||
{
|
||||
protected $signature = 'ifnex:sync-wp-users
|
||||
{--email=info@ifnex.ir : ایمیل کاربر وردپرس برای sincron}
|
||||
{--create-if-missing : ایجاد کاربر در لاراول اگر وجود نداشته باشد}';
|
||||
{--wp-db-host=localhost : WordPress database host}
|
||||
{--wp-db-port=3306 : WordPress database port}
|
||||
{--wp-db-name= : WordPress database name}
|
||||
{--wp-db-user=root : WordPress database user}
|
||||
{--wp-db-pass= : WordPress database password}
|
||||
{--wp-table-prefix=wp_ : WordPress table prefix}';
|
||||
|
||||
protected $description = 'یکسازی کاربران وردپرس با لاراول و ساخت توکن Sanctum';
|
||||
protected $description = 'Sync WordPress users to Laravel';
|
||||
|
||||
public function handle(): int
|
||||
public function handle()
|
||||
{
|
||||
$wpEmail = $this->option('email');
|
||||
$createIfMissing = $this->option('create-if-missing');
|
||||
$this->info('🔄 Syncing WordPress users to Laravel...');
|
||||
$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');
|
||||
|
||||
// ۱. پیدا کردن کاربر لاراول
|
||||
$laravelUser = User::where('email', $wpEmail)->first();
|
||||
|
||||
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;
|
||||
if (empty($wpDbName)) {
|
||||
$wpDbName = $this->ask('WordPress database name?');
|
||||
}
|
||||
} 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 {
|
||||
$wpPdo = DB::connection('mysql')->getPdo();
|
||||
|
||||
// اتصال به دیتابیس وردپرس
|
||||
$wpDbName = env('WP_DB_NAME', 'ifnexwp');
|
||||
$wpPdo->exec("USE `$wpDbName`");
|
||||
$wpDb = new \PDO(
|
||||
"mysql:host={$wpHost};port={$wpPort};dbname={$wpDbName};charset=utf8mb4",
|
||||
$wpUser,
|
||||
$wpPass,
|
||||
[\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]
|
||||
);
|
||||
|
||||
// ذخیره توکن در user meta
|
||||
$stmt = $wpPdo->prepare('
|
||||
INSERT INTO wp_usermeta (user_id, meta_key, meta_value)
|
||||
VALUES (1, "ifnex_laravel_token", :token)
|
||||
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("لطفاً دستی در دیتابیس وردپرس ذخیره کنید.");
|
||||
$this->info("✅ Connected to WordPress database: {$wpDbName}");
|
||||
} catch (\PDOException $e) {
|
||||
$this->error("❌ Failed to connect to WordPress database: " . $e->getMessage());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// ۵. تست نهایی
|
||||
$this->newLine();
|
||||
$this->info("🧪 در حال تست اتصال...");
|
||||
// دریافت همه کاربران وردپرس
|
||||
$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
|
||||
");
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, 'http://localhost:8000/api/v1/wallet/balance');
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Authorization: Bearer ' . $token,
|
||||
'Accept: application/json',
|
||||
$wpUsers = $stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||
|
||||
$this->info("📊 Found " . count($wpUsers) . " WordPress users");
|
||||
$this->newLine();
|
||||
|
||||
$created = 0;
|
||||
$updated = 0;
|
||||
$skipped = 0;
|
||||
|
||||
foreach ($wpUsers as $wpUser) {
|
||||
$email = trim($wpUser['email']);
|
||||
|
||||
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ساخت نام کامل
|
||||
$name = trim(($wpUser['first_name'] ?? '') . ' ' . ($wpUser['last_name'] ?? ''));
|
||||
if (empty($name)) {
|
||||
$name = $wpUser['name'] ?? $wpUser['login'];
|
||||
}
|
||||
|
||||
// بررسی وجود کاربر در Laravel
|
||||
$laravelUser = User::where('email', $email)->first();
|
||||
|
||||
if ($laravelUser) {
|
||||
// بهروزرسانی اطلاعات
|
||||
$laravelUser->update([
|
||||
'name' => $name,
|
||||
'phone' => $wpUser['phone'] ?? $laravelUser->phone,
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($http_code === 200) {
|
||||
$data = json_decode($response, true);
|
||||
$this->info("✅ اتصال موفق!");
|
||||
$this->info(" موجودی کیف پول: " . number_format($data['balance']) . " ریال");
|
||||
$this->info(" کل واریزی: " . number_format($data['total_deposited']) . " ریال");
|
||||
$this->info(" کل برداشت: " . number_format($data['total_withdrawn']) . " ریال");
|
||||
$updated++;
|
||||
$this->line(" ✏️ Updated: {$email}");
|
||||
} else {
|
||||
$this->error("❌ تست ناموفق: HTTP $http_code");
|
||||
$this->line(substr($response, 0, 200));
|
||||
// ساخت کاربر جدید
|
||||
$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->info("🎉 آماده تست در وردپرس!");
|
||||
$this->info(" آدرس: http://localhost/ifnexwp/?page_id=16");
|
||||
$this->info(" ورود: admin / admin");
|
||||
$this->info('═══════════════════════════════════════');
|
||||
$this->info(" 📊 Sync Summary");
|
||||
$this->info('═══════════════════════════════════════');
|
||||
$this->info(" ✅ Created: {$created}");
|
||||
$this->info(" ✏️ Updated: {$updated}");
|
||||
$this->info(" ⏭️ Skipped: {$skipped}");
|
||||
$this->info('═══════════════════════════════════════');
|
||||
|
||||
return 0;
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@ -4,16 +4,47 @@ namespace App\Enums;
|
||||
|
||||
enum PaymentGateway: string
|
||||
{
|
||||
case ZARINPAL = 'zarinpal'; // درگاه زرینپال
|
||||
case MANUAL = 'manual'; // شارژ/کسر دستی توسط ادمین
|
||||
case SYSTEM = 'system'; // تراکنشهای سیستمی (خودکار)
|
||||
case ZARINPAL = 'zarinpal';
|
||||
case WALLET = 'wallet';
|
||||
case MANUAL = 'manual';
|
||||
case SYSTEM = 'system';
|
||||
|
||||
/**
|
||||
* لیبل فارسی
|
||||
*/
|
||||
public function label(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::ZARINPAL => 'زرینپال',
|
||||
self::WALLET => 'کیف پول',
|
||||
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',
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,31 @@ namespace App\Enums;
|
||||
|
||||
enum TrackingSource: string
|
||||
{
|
||||
case Manual = 'manual';
|
||||
case ApiCarrier = 'api_carrier';
|
||||
case ApiAggregator = 'api_aggregator';
|
||||
case MANUAL = 'manual'; // دستی توسط ادمین
|
||||
case API = 'api'; // از API خارجی
|
||||
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',
|
||||
};
|
||||
}
|
||||
}
|
||||
90
04_Laravel/app/Http/Controllers/Api/BridgeAuthController.php
Normal file
90
04_Laravel/app/Http/Controllers/Api/BridgeAuthController.php
Normal 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'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -489,4 +489,108 @@ class CustomerOrderController extends Controller
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -213,6 +213,8 @@ class PaymentController extends Controller
|
||||
gatewayReferenceId: $verification['ref_id'] ?? $authority
|
||||
);
|
||||
|
||||
// ─── NEW: اگر تراکنش مربوط به سفارش است، status سفارش را تغییر بده ─
|
||||
|
||||
Log::info('Payment completed successfully', [
|
||||
'transaction_id' => $transaction->id,
|
||||
'amount' => $transaction->amount,
|
||||
|
||||
224
04_Laravel/app/Services/OrderPaymentService.php
Normal file
224
04_Laravel/app/Services/OrderPaymentService.php
Normal 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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
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
|
||||
|
||||
@ -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
|
||||
");
|
||||
}
|
||||
};
|
||||
@ -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'
|
||||
");
|
||||
}
|
||||
};
|
||||
@ -10,6 +10,13 @@ use App\Http\Controllers\Api\WalletController;
|
||||
use App\Http\Middleware\ApiKeyMiddleware;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
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
|
||||
@ -57,6 +64,11 @@ Route::middleware(['auth:sanctum'])->prefix('v1')->group(function () {
|
||||
Route::post('/orders', [CustomerOrderController::class, 'store']);
|
||||
Route::get('/orders/{shipment}', [CustomerOrderController::class, 'show']);
|
||||
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']);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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";
|
||||
Loading…
Reference in New Issue
Block a user