From 7dd53046389267fe5acfa276a3a8c39296a4e435 Mon Sep 17 00:00:00 2001 From: safavimohadeseh78 Date: Thu, 5 Nov 2026 17:15:38 +0330 Subject: [PATCH] feat: shayegan inbound charges workflow and copper kilogram unit enforcement --- .../Controllers/Admin/CustomerController.php | 18 + .../Controllers/Admin/ShayeganController.php | 133 ++++++- app/Http/Controllers/Controller.php | 8 +- app/Models/Shayegan.php | 7 + app/Models/ShayeganInboundCharge.php | 59 +++ app/Services/AssetSummaryService.php | 4 +- app/Services/ShayeganInboundChargeService.php | 367 ++++++++++++++++++ app/Services/ShayeganSyncService.php | 78 +++- ..._create_shayegan_inbound_charges_table.php | 39 ++ ...02_add_shayegan_webhook_secret_setting.php | 41 ++ ...et_boursecu_trade_weight_unit_kilogram.php | 43 ++ routes/admin.php | 5 + routes/api.php | 3 + 13 files changed, 787 insertions(+), 18 deletions(-) create mode 100644 app/Models/ShayeganInboundCharge.php create mode 100644 app/Services/ShayeganInboundChargeService.php create mode 100644 database/migrations/2026_11_05_000001_create_shayegan_inbound_charges_table.php create mode 100644 database/migrations/2026_11_05_000002_add_shayegan_webhook_secret_setting.php create mode 100644 database/migrations/2026_11_05_000003_set_boursecu_trade_weight_unit_kilogram.php diff --git a/app/Http/Controllers/Admin/CustomerController.php b/app/Http/Controllers/Admin/CustomerController.php index 2d610a6..bddd963 100644 --- a/app/Http/Controllers/Admin/CustomerController.php +++ b/app/Http/Controllers/Admin/CustomerController.php @@ -16,6 +16,7 @@ use App\Services\BalanceReportService; use App\Services\ConfigService; use App\Services\CustomerPresenceService; use App\Services\ReferralGiftService; +use App\Services\ShayeganSyncService; use App\Services\WalletBalanceService; use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; @@ -198,6 +199,23 @@ class CustomerController extends SuperController $updated = $data === [] ? false : $customer->update($data); + // همگام‌سازی با شایگان در صورت تغییر دستی کد حسابداری + if (array_key_exists('moshtari_code', $data) && app(ConfigService::class)->isShayeganAccounting()) { + $newCode = $data['moshtari_code']; + if ($newCode !== null && $newCode !== '') { + try { + $shayeganSync = app(ShayeganSyncService::class); + $shayeganSync->registerCustomer($customer, $newCode); + } catch (\Throwable $e) { + \Illuminate\Support\Facades\Log::warning('shayegan.manual_code_sync_failed', [ + 'customer_id' => $customer->id, + 'code' => $newCode, + 'error' => $e->getMessage(), + ]); + } + } + } + return response()->json([ 'g' => $goldUpdated, 'u' => $updated, diff --git a/app/Http/Controllers/Admin/ShayeganController.php b/app/Http/Controllers/Admin/ShayeganController.php index 46503ad..70cfa53 100644 --- a/app/Http/Controllers/Admin/ShayeganController.php +++ b/app/Http/Controllers/Admin/ShayeganController.php @@ -4,7 +4,10 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use App\Models\Shayegan; +use App\Models\ShayeganInboundCharge; +use App\Services\ConfigService; use App\Services\ShayeganClient; +use App\Services\ShayeganInboundChargeService; use App\Services\ShayeganSyncService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Schema; @@ -88,7 +91,7 @@ class ShayeganController extends Controller ]); } - $query = Shayegan::query()->orderByDesc('id'); + $query = Shayegan::with('customer')->orderByDesc('id'); \App\DB\Te\TableEngine::create($query, $request) ->initSearch(['desc', 'type', 'shayegan_number', 'sync_status']) ->select(['*']) @@ -99,7 +102,7 @@ class ShayeganController extends Controller public function show($id) { - $row = Shayegan::findOrFail($id); + $row = Shayegan::with('customer')->findOrFail($id); return response()->json($row); } @@ -163,6 +166,132 @@ class ShayeganController extends Controller return response()->json(array_merge(['ok' => false, 'message' => $row->sync_log, 'row' => $row], $exchange), 422); } + /** + * وب‌هوک دریافت شارژ حساب مشتری از پنل حسابداری شایگان + */ + public function inboundWebhook(Request $request, ShayeganInboundChargeService $inboundService) + { + $secret = app(ConfigService::class)->getString('shayegan_webhook_secret', ''); + $key = $request->header('X-Shayegan-Key') ?: $request->input('secret'); + + if ($secret !== '' && $key !== $secret) { + return response()->json([ + 'ok' => false, + 'message' => 'کلید امنیتی وب‌هوک شایگان نامعتبر است (X-Shayegan-Key).', + ], 403); + } + + $res = $inboundService->registerCharge($request->all(), 'webhook'); + + return response()->json($res, ($res['ok'] ?? false) ? 200 : 422); + } + + /** + * دریافت لیست شارژهای در انتظار تایید برای نمایش اعلان در پنل ادمین + */ + public function pendingCharges() + { + if (! Schema::hasTable('shayegan_inbound_charges')) { + return response()->json(['ok' => true, 'charges' => []]); + } + + $charges = ShayeganInboundCharge::with('customer') + ->where('status', 'pending') + ->orderByDesc('id') + ->limit(20) + ->get(); + + return response()->json([ + 'ok' => true, + 'charges' => $charges, + ]); + } + + /** + * جدول شارژهای دریافتی از شایگان + */ + public function chargesTable(Request $request) + { + if (! Schema::hasTable('shayegan_inbound_charges')) { + return response()->json([ + 'rows' => [], + 'query' => [ + 'limit' => (int) $request->input('limit', 20), + 'page' => 1, + 'total' => 0, + 'filters' => $request->input('filters', []), + 'search' => $request->input('search'), + ], + ]); + } + + $query = ShayeganInboundCharge::with(['customer', 'approver'])->orderByDesc('id'); + + if ($request->filled('status')) { + $query->where('status', $request->input('status')); + } + + if ($request->filled('search')) { + $s = trim((string) $request->input('search')); + $query->where(function ($q) use ($s) { + $q->where('shayegan_number', 'like', "%{$s}%") + ->orWhere('customer_tafsil', 'like', "%{$s}%") + ->orWhereHas('customer', function ($cq) use ($s) { + $cq->where('name', 'like', "%{$s}%") + ->orWhere('mobile', 'like', "%{$s}%") + ->orWhere('n_code', 'like', "%{$s}%"); + }); + }); + } + + $limit = max(1, (int) $request->input('limit', 20)); + $page = max(1, (int) $request->input('page', 1)); + $total = $query->count(); + $rows = $query->skip(($page - 1) * $limit)->take($limit)->get(); + + return response()->json([ + 'rows' => $rows, + 'query' => [ + 'page' => $page, + 'limit' => $limit, + 'total' => $total, + ], + ]); + } + + /** + * تایید شارژ و افزایش کیف پول مشتری در بورس مس + */ + public function approveCharge(Request $request, $id, ShayeganInboundChargeService $inboundService) + { + $charge = ShayeganInboundCharge::with('customer')->findOrFail($id); + $res = $inboundService->approveCharge($charge, auth()->user()); + + return response()->json($res, ($res['ok'] ?? false) ? 200 : 422); + } + + /** + * رد درخواست شارژ + */ + public function rejectCharge(Request $request, $id, ShayeganInboundChargeService $inboundService) + { + $charge = ShayeganInboundCharge::findOrFail($id); + $reason = $request->input('reason'); + $res = $inboundService->rejectCharge($charge, auth()->user(), $reason); + + return response()->json($res, ($res['ok'] ?? false) ? 200 : 422); + } + + /** + * استعلام اسناد جدید از شایگان + */ + public function syncChargesFromShayegan(ShayeganInboundChargeService $inboundService) + { + $res = $inboundService->pollFromShayegan(); + + return response()->json($res, ($res['ok'] ?? false) ? 200 : 422); + } + /** * @param mixed $body * @return mixed diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 98132da..1cc33d4 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -500,7 +500,7 @@ class Controller extends BaseController return self::isMainWalletProduct($productId) ? null : $productId; } - /** واحد وزن معاملات فلز — از ستینگ unit؛ پیش‌فرض گرم */ + /** واحد وزن معاملات فلز — در بورس مس پیش‌فرض کیلوگرم است */ public static function tradeWeightUnit(): string { return self::resolveTradeWeightUnit( @@ -512,11 +512,11 @@ class Controller extends BaseController { $unit = trim((string) $value); - if ($unit === 'کیلوگرم' || $unit === 'kilogram' || $unit === 'kg') { - return 'کیلوگرم'; + if ($unit === 'گرم' || $unit === 'gram' || $unit === 'g') { + return 'گرم'; } - return 'گرم'; + return 'کیلوگرم'; } public static function weightToGrams(float $value, ?string $fromUnit = null): float diff --git a/app/Models/Shayegan.php b/app/Models/Shayegan.php index 51a0d50..ca4d7e2 100644 --- a/app/Models/Shayegan.php +++ b/app/Models/Shayegan.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; class Shayegan extends Model { @@ -14,4 +15,10 @@ class Shayegan extends Model 'date' => 'datetime', 'amount' => 'float', ]; + + public function customer(): BelongsTo + { + return $this->belongsTo(Customer::class); + } } + diff --git a/app/Models/ShayeganInboundCharge.php b/app/Models/ShayeganInboundCharge.php new file mode 100644 index 0000000..ca4d103 --- /dev/null +++ b/app/Models/ShayeganInboundCharge.php @@ -0,0 +1,59 @@ + 'float', + 'approved_at' => 'datetime', + 'rejected_at' => 'datetime', + 'raw_data' => 'array', + ]; + + public function customer() + { + return $this->belongsTo(Customer::class, 'customer_id'); + } + + public function approver() + { + return $this->belongsTo(User::class, 'approved_by'); + } + + public function rejecter() + { + return $this->belongsTo(User::class, 'rejected_by'); + } + + public function withdraw() + { + return $this->belongsTo(Withdraw::class, 'withdraw_id'); + } +} + diff --git a/app/Services/AssetSummaryService.php b/app/Services/AssetSummaryService.php index 1ee7693..e48bb89 100644 --- a/app/Services/AssetSummaryService.php +++ b/app/Services/AssetSummaryService.php @@ -717,6 +717,8 @@ class AssetSummaryService $meltedWeight = round(Controller::gramsToTradeUnit($meltedGrams), 3); $totalWeight = round($productWeight + $meltedWeight, 3); + $metalUnit = ((int) $metalId === 3 || $weightUnit === 'کیلوگرم') ? 'کیلوگرم' : $weightUnit; + $totals[] = [ 'metal_id' => (int) $metalId, 'metal_label' => self::METAL_LABELS[$metalId] ?? 'فلز', @@ -724,7 +726,7 @@ class AssetSummaryService 'product_weight' => $productWeight, 'melted_weight' => $meltedWeight, 'total_weight' => $totalWeight, - 'unit' => $weightUnit, + 'unit' => $metalUnit, ]; } diff --git a/app/Services/ShayeganInboundChargeService.php b/app/Services/ShayeganInboundChargeService.php new file mode 100644 index 0000000..8083c8d --- /dev/null +++ b/app/Services/ShayeganInboundChargeService.php @@ -0,0 +1,367 @@ +config = $config; + $this->client = $client; + $this->walletBroadcast = $walletBroadcast; + } + + /** + * پیدا کردن مشتری با اولویت: شناسه مستقیم، کد تفصیلی، موبایل، کد ملی + */ + public function resolveCustomer(array $params): ?Customer + { + // 1. با شناسه مشتری در بورس مس + if (! empty($params['customer_id'])) { + $customer = Customer::find((int) $params['customer_id']); + if ($customer) { + return $customer; + } + } + + // 2. با کد تفصیلی شایگان (moshtari_code) + $tafsil = trim((string) ($params['customer_tafsil'] ?? $params['moshtari_code'] ?? $params['job_number'] ?? '')); + if ($tafsil !== '') { + $customer = Customer::where('moshtari_code', $tafsil)->first(); + if ($customer) { + return $customer; + } + + // بررسی فرمول احتمالی کد کل + کد کاربر ۷ رقمی + $kol = trim($this->config->getString('shayegan_kol_customer', '121')); + if ($kol !== '' && str_starts_with($tafsil, $kol)) { + $subId = (int) substr($tafsil, strlen($kol)); + if ($subId > 0) { + $customer = Customer::find($subId); + if ($customer) { + return $customer; + } + } + } + } + + // 3. با شماره موبایل + $mobile = trim((string) ($params['mobile'] ?? '')); + if ($mobile !== '') { + $customer = Customer::findActiveByMobile($mobile); + if ($customer) { + return $customer; + } + } + + // 4. با کد ملی + $nCode = trim((string) ($params['n_code'] ?? $params['national_code'] ?? $params['mellicode'] ?? '')); + if ($nCode !== '') { + $customer = Customer::where('n_code', $nCode)->first(); + if ($customer) { + return $customer; + } + } + + return null; + } + + /** + * ثبت درخواست شارژ ورودی از شایگان در وضعیت pending + */ + public function registerCharge(array $data, string $source = 'webhook'): array + { + $customer = $this->resolveCustomer($data); + if (! $customer) { + return [ + 'ok' => false, + 'message' => 'مشتری با مشخصات ارسالی (کد مشتری، کد تفصیلی، موبایل یا کد ملی) یافت نشد.', + ]; + } + + $amount = (float) ($data['amount'] ?? $data['value'] ?? $data['RialBes'] ?? $data['AmountBes'] ?? 0); + if ($amount <= 0) { + return [ + 'ok' => false, + 'message' => 'مبلغ شارژ باید مقداری مثبت و بزرگتر از صفر باشد.', + ]; + } + + $shayeganNumber = trim((string) ($data['shayegan_number'] ?? $data['Number'] ?? $data['doc_number'] ?? '')); + $tafsil = trim((string) ($data['customer_tafsil'] ?? $data['moshtari_code'] ?? $customer->moshtari_code ?? '')); + $description = trim((string) ($data['description'] ?? $data['desc'] ?? $data['note'] ?? 'شارژ از طریق پنل حسابداری شایگان')); + + // جلوگیری از ثبت تکراری اگر قبلاً سند با همین شماره برای این مشتری ثبت شده است + if ($shayeganNumber !== '') { + $existing = ShayeganInboundCharge::where('shayegan_number', $shayeganNumber) + ->where('customer_id', $customer->id) + ->first(); + if ($existing) { + return [ + 'ok' => true, + 'message' => 'این سند شایگان قبلاً ثبت شده است.', + 'charge' => $existing->load('customer'), + 'duplicate' => true, + ]; + } + } + + $charge = ShayeganInboundCharge::create([ + 'customer_id' => $customer->id, + 'customer_tafsil' => $tafsil ?: null, + 'shayegan_number' => $shayeganNumber ?: null, + 'amount' => $amount, + 'description' => $description, + 'source' => $source, + 'status' => 'pending', + 'raw_data' => $data, + ]); + + Log::info('shayegan.inbound_charge_registered', [ + 'charge_id' => $charge->id, + 'customer_id' => $customer->id, + 'amount' => $amount, + 'shayegan_number' => $shayeganNumber, + 'source' => $source, + ]); + + return [ + 'ok' => true, + 'message' => "حساب شخص {$customer->name} از طریق پنل حسابداری مقدار " . number_format($amount) . " ریال شارژ شد.", + 'charge' => $charge->load('customer'), + ]; + } + + /** + * تایید شارژ و افزایش کیف پول ریالی مشتری در بورس مس + */ + public function approveCharge(ShayeganInboundCharge $charge, ?User $adminUser = null): array + { + return DB::transaction(function () use ($charge, $adminUser) { + $charge = ShayeganInboundCharge::where('id', $charge->id)->lockForUpdate()->firstOrFail(); + + if ($charge->status === 'approved') { + return [ + 'ok' => true, + 'message' => 'این شارژ قبلاً تایید شده است.', + 'charge' => $charge->load(['customer', 'approver']), + ]; + } + + if ($charge->status !== 'pending') { + throw new UnprocessableEntityHttpException('وضعیت این شارژ برای تایید معتبر نیست (' . $charge->status . ').'); + } + + $customer = $charge->customer; + if (! $customer) { + throw new UnprocessableEntityHttpException('مشتری مرتبط با این شارژ یافت نشد.'); + } + + $adminId = $adminUser ? (int) $adminUser->id : 1; + $note = $charge->description ?: ('شارژ تایید شده از پنل حسابداری شایگان' . ($charge->shayegan_number ? " (سند {$charge->shayegan_number})" : '')); + + // 1. ثبت Withdraw (واریز ریالی با sts = 4) + $withdraw = Withdraw::create([ + 'user_id' => $adminId, + 'customer_id' => $customer->id, + 'cash' => $charge->amount, + 'note' => $note, + 'sts' => 4, + ]); + + // 2. افزایش موجودی در کیف پول (Rooyesh Wallet) + $walletInterface = new WalletInterface((int) $customer->id, (int) $customer->type); + new TransactionInterface($walletInterface, [ + 'gold' => 0, + 'cash' => $charge->amount, + 'expire_at' => now(), + 'note' => $note, + 'withdraw_id' => $withdraw->id, + 'active' => true, + ], 3, true, true, 0); // type 3 = deposit, deposit = true + + // 3. ثبت لاگ تراکنش ادمین + $lastTx = Transaction::where('withdraw_id', $withdraw->id)->latest('id')->first(); + TransactionsAdmin::create([ + 'customer_id' => $customer->id, + 'customer_to_id' => $customer->id, + 'user_id' => $adminId, + 'wallet' => null, + 'transaction_id' => $lastTx?->id, + 'cash' => $charge->amount, + 'note' => $note, + 'type' => 1, + 'gold' => 0, + 'balance_cash' => 0, + ]); + + // 4. به‌روزرسانی وضعیت رکورد شارژ شایگان + $charge->status = 'approved'; + $charge->approved_by = $adminId; + $charge->approved_at = now(); + $charge->withdraw_id = $withdraw->id; + $charge->transaction_id = $lastTx?->id; + $charge->save(); + + // 5. برودکست ریل‌تایم موجودی کیف پول کاربر + try { + $this->walletBroadcast->broadcastForCustomer((int) $customer->id, (int) $customer->type); + } catch (\Throwable $e) { + Log::warning('shayegan.wallet_broadcast_failed', ['error' => $e->getMessage()]); + } + + // 6. تریگر اتوماسیون + try { + \App\Services\Automation\AutomationEvents::fire('wallet.rial_charge', (int) $customer->id); + } catch (\Throwable $e) { + Log::warning('shayegan.automation_failed', ['error' => $e->getMessage()]); + } + + Log::info('shayegan.inbound_charge_approved', [ + 'charge_id' => $charge->id, + 'customer_id' => $customer->id, + 'amount' => $charge->amount, + 'withdraw_id' => $withdraw->id, + 'admin_id' => $adminId, + ]); + + return [ + 'ok' => true, + 'message' => "کیف پول شخص {$customer->name} به مبلغ " . number_format($charge->amount) . " ریال با موفقیت شارژ شد.", + 'charge' => $charge->load(['customer', 'approver']), + ]; + }); + } + + /** + * رد درخواست شارژ + */ + public function rejectCharge(ShayeganInboundCharge $charge, ?User $adminUser = null, ?string $reason = null): array + { + if ($charge->status === 'approved') { + throw new UnprocessableEntityHttpException('شارژ تایید شده را نمی‌توان رد کرد.'); + } + + $adminId = $adminUser ? (int) $adminUser->id : 1; + $charge->status = 'rejected'; + $charge->rejected_by = $adminId; + $charge->rejected_at = now(); + $charge->reject_reason = $reason; + $charge->save(); + + Log::info('shayegan.inbound_charge_rejected', [ + 'charge_id' => $charge->id, + 'customer_id' => $charge->customer_id, + 'admin_id' => $adminId, + 'reason' => $reason, + ]); + + return [ + 'ok' => true, + 'message' => 'درخواست شارژ رد شد.', + 'charge' => $charge->load(['customer', 'rejecter']), + ]; + } + + /** + * استعلام اسناد جدید از وب‌سرویس شایگان (Sync / Polling) + */ + public function pollFromShayegan(): array + { + if (! $this->client->isConfigured()) { + return [ + 'ok' => false, + 'message' => 'تنظیمات اتصال وب‌سرویس شایگان در سیستم پیکربندی نشده است.', + 'new_count' => 0, + ]; + } + + try { + // استعلام از وب‌سرویس شایگان (مانند Document/Get با فیلتر بستانکاری تفصیلی مشتریان) + $res = $this->client->get('Document/Get', [], 0, 50); + if (empty($res['ok']) || empty($res['body']['Result']) || ! is_array($res['body']['Result'])) { + return [ + 'ok' => true, + 'message' => 'سند جدیدی در استعلام شایگان یافت نشد.', + 'new_count' => 0, + ]; + } + + $count = 0; + foreach ($res['body']['Result'] as $doc) { + $docNumber = (string) ($doc['Number'] ?? $doc['DocNumber'] ?? ''); + if ($docNumber === '') { + continue; + } + + $rows = $doc['Body'] ?? []; + if (! is_array($rows)) { + continue; + } + + foreach ($rows as $row) { + $amountBes = (float) ($row['AmountBes'] ?? $row['RialBes'] ?? 0); + $accountNumber = trim((string) ($row['AccountNumber'] ?? '')); + if ($amountBes <= 0 || $accountNumber === '') { + continue; + } + + // جستجوی مشتری با شماره تفصیلی + $customer = Customer::where('moshtari_code', $accountNumber)->first(); + if (! $customer) { + continue; + } + + // آیا قبلا این شماره سند ثبت شده؟ + $already = ShayeganInboundCharge::where('shayegan_number', $docNumber) + ->where('customer_id', $customer->id) + ->exists(); + + if (! $already) { + $this->registerCharge([ + 'customer_id' => $customer->id, + 'customer_tafsil' => $accountNumber, + 'shayegan_number' => $docNumber, + 'amount' => $amountBes, + 'description' => (string) ($row['DocRowDesc'] ?? $doc['DocDescription'] ?? 'استعلام خودکار از شایگان'), + ], 'sync'); + $count++; + } + } + } + + return [ + 'ok' => true, + 'message' => $count > 0 ? "تعداد {$count} شارژ جدید از شایگان واکشی شد." : 'سند شارژ جدیدی یافت نشد.', + 'new_count' => $count, + ]; + } catch (\Throwable $e) { + Log::warning('shayegan.poll_failed', ['error' => $e->getMessage()]); + return [ + 'ok' => false, + 'message' => 'خطا در ارتباط با وب‌سرویس شایگان: ' . $e->getMessage(), + 'new_count' => 0, + ]; + } + } +} + diff --git a/app/Services/ShayeganSyncService.php b/app/Services/ShayeganSyncService.php index b735005..b873196 100644 --- a/app/Services/ShayeganSyncService.php +++ b/app/Services/ShayeganSyncService.php @@ -94,11 +94,16 @@ class ShayeganSyncService /** * @return array{ok:bool,message:string,moshtari_code?:string} */ - public function registerCustomer(Customer $customer): array + public function registerCustomer(Customer $customer, ?string $customJobNumber = null): array { $customer->refresh(); - if (! empty($customer->moshtari_code)) { + $customJobNumber = $customJobNumber !== null ? trim($customJobNumber) : null; + if ($customJobNumber === '') { + $customJobNumber = null; + } + + if ($customJobNumber === null && ! empty($customer->moshtari_code)) { return [ 'ok' => true, 'message' => 'مشتری قبلاً در شایگان ثبت شده است', @@ -119,8 +124,8 @@ class ShayeganSyncService $name = 'مشتری ' . $customer->id; } - // تفصیلی شناور: کل ثابت + کد کاربری ۷رقمی. مشتری ۱ → 1210000001 - $requestedAccountNumber = $this->customerTafsilNumber((int) $customer->id); + // تفصیلی شناور: اگر کد دستی ارسال شده باشد همان کد استفاده می‌شود، در غیر این صورت فرمول پیش‌فرض + $requestedAccountNumber = $customJobNumber ?? $this->customerTafsilNumber((int) $customer->id); $guid = (string) Str::uuid(); $put = [[ @@ -138,9 +143,22 @@ class ShayeganSyncService $result = $this->client->put('Job/Put', $put); $accountNumber = trim((string) ($result['number'] ?? '')); + if ($accountNumber === '' && $customJobNumber !== null && ! empty($result['ok'])) { + $accountNumber = $customJobNumber; + } $exchange = $this->client->debugExchange($result); if (! empty($result['timeout'])) { + $this->logCustomerRegistrationDoc( + $customer, + $requestedAccountNumber, + $guid, + $put, + $result, + self::STATUS_TIMEOUT, + (string) ($result['message'] ?? 'timeout') + ); + return array_merge([ 'ok' => false, 'message' => (string) ($result['message'] ?? 'timeout'), @@ -148,15 +166,56 @@ class ShayeganSyncService } if (empty($result['ok']) || $accountNumber === '') { + $errorMsg = (string) ($result['message'] ?? 'ثبت حساب مشتری در شایگان ناموفق بود'); + $this->logCustomerRegistrationDoc( + $customer, + $requestedAccountNumber, + $guid, + $put, + $result, + self::STATUS_FAILED, + $errorMsg + ); + return array_merge([ 'ok' => false, - 'message' => (string) ($result['message'] ?? 'ثبت حساب مشتری در شایگان ناموفق بود'), + 'message' => $errorMsg, ], $exchange); } $customer->moshtari_code = $accountNumber; $customer->save(); + $this->logCustomerRegistrationDoc( + $customer, + $accountNumber, + $guid, + $put, + $result, + self::STATUS_SUCCESS, + null + ); + + return array_merge([ + 'ok' => true, + 'message' => 'تفصیلی شناور مشتری در شایگان ایجاد شد', + 'moshtari_code' => $accountNumber, + ], $exchange); + } + + /** + * @param array $put + * @param array $result + */ + private function logCustomerRegistrationDoc( + Customer $customer, + string $accountNumber, + string $guid, + array $put, + array $result, + string $status, + ?string $logMessage = null + ): Shayegan { $row = new Shayegan(); $row->customer_id = (int) $customer->id; $row->order_id = null; @@ -170,7 +229,8 @@ class ShayeganSyncService $row->request_guid = $guid; $row->shayegan_number = $accountNumber; $row->shayegan_guid = $result['guid'] ?? null; - $row->sync_status = self::STATUS_SUCCESS; + $row->sync_status = $status; + $row->sync_log = $logMessage; $row->data = json_encode($this->client->redactPayload([ 'Config' => $this->client->buildConfig(), 'PutObject' => $put, @@ -178,11 +238,7 @@ class ShayeganSyncService $row->res = json_encode($result['body'] ?? [], JSON_UNESCAPED_UNICODE); $row->save(); - return array_merge([ - 'ok' => true, - 'message' => 'تفصیلی شناور مشتری در شایگان ایجاد شد', - 'moshtari_code' => $accountNumber, - ], $exchange); + return $row; } /** diff --git a/database/migrations/2026_11_05_000001_create_shayegan_inbound_charges_table.php b/database/migrations/2026_11_05_000001_create_shayegan_inbound_charges_table.php new file mode 100644 index 0000000..ae260dd --- /dev/null +++ b/database/migrations/2026_11_05_000001_create_shayegan_inbound_charges_table.php @@ -0,0 +1,39 @@ +id(); + $table->unsignedBigInteger('customer_id')->nullable()->index(); + $table->string('customer_tafsil', 64)->nullable()->index(); + $table->string('shayegan_number', 64)->nullable()->index(); + $table->decimal('amount', 18, 4); + $table->text('description')->nullable(); + $table->string('source', 32)->default('webhook')->index(); + $table->string('status', 20)->default('pending')->index(); // pending, approved, rejected + $table->longText('raw_data')->nullable(); + $table->unsignedBigInteger('approved_by')->nullable()->index(); + $table->timestamp('approved_at')->nullable(); + $table->unsignedBigInteger('rejected_by')->nullable()->index(); + $table->timestamp('rejected_at')->nullable(); + $table->string('reject_reason', 255)->nullable(); + $table->unsignedBigInteger('withdraw_id')->nullable()->index(); + $table->unsignedBigInteger('transaction_id')->nullable()->index(); + $table->timestamps(); + }); + } + } + + public function down(): void + { + Schema::dropIfExists('shayegan_inbound_charges'); + } +}; + diff --git a/database/migrations/2026_11_05_000002_add_shayegan_webhook_secret_setting.php b/database/migrations/2026_11_05_000002_add_shayegan_webhook_secret_setting.php new file mode 100644 index 0000000..ef6d5c7 --- /dev/null +++ b/database/migrations/2026_11_05_000002_add_shayegan_webhook_secret_setting.php @@ -0,0 +1,41 @@ +where('key', 'shayegan_webhook_secret')->exists(); + if (! $exists) { + $hasMenu = Schema::hasColumn('settings', 'menu'); + $row = [ + 'key' => 'shayegan_webhook_secret', + 'value' => 'shayegan_sec_' . bin2hex(random_bytes(12)), + 'title_fa' => 'کلید امنیتی وب‌هوک شایگان (X-Shayegan-Key)', + 'description' => 'کلید محرمانه جهت احراز هویت درخواست‌های وب‌هوک شارژ از شایگان به بورس مس.', + 'can_edit' => 0, + 'is_active' => 1, + 'type' => 'password', + ]; + if ($hasMenu) { + $row['menu'] = 'accounting_shayegan'; + } + DB::table('settings')->insert($row); + } + } + + public function down(): void + { + if (Schema::hasTable('settings')) { + DB::table('settings')->where('key', 'shayegan_webhook_secret')->delete(); + } + } +}; + diff --git a/database/migrations/2026_11_05_000003_set_boursecu_trade_weight_unit_kilogram.php b/database/migrations/2026_11_05_000003_set_boursecu_trade_weight_unit_kilogram.php new file mode 100644 index 0000000..1546e80 --- /dev/null +++ b/database/migrations/2026_11_05_000003_set_boursecu_trade_weight_unit_kilogram.php @@ -0,0 +1,43 @@ +where('key', 'unit')->first(); + if ($row) { + $val = trim((string) ($row->value ?? '')); + if ($val === '' || $val === 'گرم') { + DB::table('settings')->where('key', 'unit')->update([ + 'value' => 'کیلوگرم', + 'updated_at' => now(), + ]); + } + } else { + DB::table('settings')->insert([ + 'key' => 'unit', + 'value' => 'کیلوگرم', + 'title_fa' => 'واحد وزن معاملات', + 'description' => 'واحد اصلی وزن در بورس مس (کیلوگرم).', + 'can_edit' => 0, + 'is_active' => 1, + 'type' => 'text', + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + } + + public function down(): void + { + } +}; + diff --git a/routes/admin.php b/routes/admin.php index 2e9f368..5edda57 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -302,6 +302,11 @@ Route::middleware(['auth:user', 'admin.page'])->group(function (){ Route::get('shayegan/{id}', [ShayeganController::class, 'show']); Route::post('shayegan/{id}/retry', [ShayeganController::class, 'retrySanad']); Route::post('customer/{id}/shayegan/register', [ShayeganController::class, 'registerCustomer']); + Route::get('shayegan/charges/pending', [ShayeganController::class, 'pendingCharges']); + Route::post('shayegan/charges/table', [ShayeganController::class, 'chargesTable']); + Route::post('shayegan/charges/{id}/approve', [ShayeganController::class, 'approveCharge']); + Route::post('shayegan/charges/{id}/reject', [ShayeganController::class, 'rejectCharge']); + Route::post('shayegan/charges/sync', [ShayeganController::class, 'syncChargesFromShayegan']); Route::post('pargar/table', [PargarController::class, 'table']); Route::get('pargar/customer-sync/status', [PargarController::class, 'customerSyncStatus']); diff --git a/routes/api.php b/routes/api.php index 0bc18f9..be47416 100644 --- a/routes/api.php +++ b/routes/api.php @@ -71,3 +71,6 @@ if (app(ConfigService::class)->isStore()) { Route::post('/cart/remove', [CartController::class, 'removeItem']); Route::get('categories', [CategoryController::class, 'clientIndex']); } + +// وب‌هوک دریافت شارژ مشتری از شایگان +Route::post('/shayegan/webhook/inbound-charge', [\App\Http\Controllers\Admin\ShayeganController::class, 'inboundWebhook']);