$data * @return array{applied: int, skipped: int, errors: list, logs: list} */ public function applyDocuments(Customer $customer, array $data): array { $transactions = is_array($data['transactions'] ?? null) ? $data['transactions'] : []; $accountId = trim((string) ($data['accountId'] ?? '')); $result = [ 'applied' => 0, 'skipped' => 0, 'errors' => [], 'logs' => [], ]; if ($transactions === []) { return $result; } $walletApplied = false; foreach ($transactions as $tx) { if (! is_array($tx)) { continue; } $docId = trim((string) ($tx['docId'] ?? '')); if ($docId === '') { $result['skipped']++; continue; } if ($this->isDocumentApplied($docId)) { $result['skipped']++; continue; } $goldDelta = $this->transactionGoldDelta($tx); $rialDelta = $this->transactionRialDelta($tx); if (abs($goldDelta) < 0.01 && abs($rialDelta) < 1) { $result['skipped']++; continue; } if ($this->isEchoOfRecentOutbound((int) $customer->id, $goldDelta, $rialDelta)) { $this->recordSkippedDocument($customer, $docId, $accountId, $tx, 'echo_outbound'); $result['skipped']++; continue; } try { $this->applyTransactionDelta($customer, $tx, $goldDelta, $rialDelta); $walletApplied = true; $summary = $this->sync->summarizeInboundDocument([ 'accountId' => $accountId, 'accountName' => $data['accountName'] ?? null, 'transactions' => [$tx], ]); $log = PargarSyncLog::create([ 'customer_id' => $customer->id, 'amount' => $summary['amount'], 'desc' => $summary['desc'] !== '' ? $summary['desc'] : 'اعمال سند پرگار', 'type' => PargarSyncService::TYPE_DOCUMENT, 'sync_status' => PargarSyncService::STATUS_SUCCESS, 'pargar_ref' => $docId, 'data' => json_encode([ 'accountId' => $accountId, 'accountName' => $data['accountName'] ?? null, 'transaction' => $tx, 'wallet_applied' => true, 'gold_delta' => $goldDelta, 'rial_delta' => $rialDelta, ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), ]); $result['applied']++; $result['logs'][] = (int) $log->id; } catch (\Throwable $e) { Log::warning('Pargar inbound document apply failed', [ 'customer_id' => $customer->id, 'doc_id' => $docId, 'error' => $e->getMessage(), ]); $result['errors'][] = "سند {$docId}: " . $e->getMessage(); } } if ($walletApplied) { $this->walletBroadcast->broadcastForCustomer( (int) $customer->id, (int) ($customer->type ?? 2) ); } return $result; } /** * اسناد ذخیره‌شدهٔ قبلی (قبل از deploy) را از لاگ دوباره اعمال می‌کند. * * @return array{applied: int, skipped: int, errors: list, logs: list} */ public function replayStoredDocuments(Customer $customer): array { $accountId = trim((string) ($customer->pargar_account_id ?? '')); $logs = PargarSyncLog::query() ->where('type', PargarSyncService::TYPE_DOCUMENT) ->where(function ($q) use ($customer, $accountId) { $q->where('customer_id', $customer->id); if ($accountId !== '') { $q->orWhere('pargar_ref', $accountId); } }) ->orderBy('id') ->get(); $total = [ 'applied' => 0, 'skipped' => 0, 'errors' => [], 'logs' => [], ]; foreach ($logs as $log) { $data = json_decode((string) $log->data, true); if (! is_array($data) || empty($data['transactions']) || ! is_array($data['transactions'])) { continue; } $batch = $this->applyDocuments($customer, $data); $total['applied'] += $batch['applied']; $total['skipped'] += $batch['skipped']; $total['errors'] = array_merge($total['errors'], $batch['errors']); $total['logs'] = array_merge($total['logs'], $batch['logs']); } return $total; } /** * آخرین ماندهٔ پرگار را (در صورت اختلاف) یک‌بار روی کیف اعمال می‌کند — فقط با درخواست ادمین. * * @return array{applied: bool, gold_delta?: float, rial_delta?: float, message: string} */ public function applyLatestMandeDifference(Customer $customer): array { if ($this->hasPendingOutboundWalletSync((int) $customer->id)) { return [ 'applied' => false, 'message' => 'سند ارسالی از پنل به پرگار هنوز تأیید نشده — برای جلوگیری از برگشت شارژ، ماندهٔ پرگار اعمال نشد', ]; } $log = PargarSyncLog::query() ->where('customer_id', $customer->id) ->where('type', PargarSyncService::TYPE_MANDE) ->orderByDesc('id') ->first(); if ($log === null) { return ['applied' => false, 'message' => 'لاگ ماندهٔ پرگار برای این مشتری یافت نشد']; } $data = json_decode((string) $log->data, true) ?: []; if (! empty($data['wallet_applied_from_mande'])) { return ['applied' => false, 'message' => 'ماندهٔ پرگار قبلاً روی کیف اعمال شده است']; } $goldInv = $this->parseNumber($data['GoldInv'] ?? 0); $rialInv = $this->parseNumber($data['RialInv'] ?? 0); $ownerType = (int) ($customer->type ?? 2); $walletInterface = new WalletInterface((int) $customer->id, $ownerType); $wallet = $walletInterface->wallet; $goldDiff = $goldInv - (float) ($wallet->gold ?? 0); $rialDiff = $rialInv - (float) ($wallet->cash ?? 0); if (abs($goldDiff) < 0.01 && abs($rialDiff) < 1) { $data['wallet_applied_from_mande'] = true; $log->data = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); $log->save(); return ['applied' => false, 'message' => 'اختلافی بین ماندهٔ پرگار و کیف اپ نیست']; } $input = [ 'title' => 'تطبیق ماندهٔ پرگار', 'note' => 'mande-log:' . $log->id, 'expire_at' => now(), 'active' => true, ]; if (abs($goldDiff) >= 0.01) { $input['gold'] = abs($goldDiff); } if (abs($rialDiff) >= 1) { $input['cash'] = abs($rialDiff); } new TransactionInterface( $walletInterface, $input, self::TX_TYPE_PARGAR, $goldDiff > 0, $rialDiff > 0 ); $data['wallet_applied_from_mande'] = true; $data['applied_gold_delta'] = $goldDiff; $data['applied_rial_delta'] = $rialDiff; $log->data = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); $log->desc = 'اعمال ماندهٔ پرگار روی کیف اپ'; $log->save(); $this->walletBroadcast->broadcastForCustomer((int) $customer->id, $ownerType); return [ 'applied' => true, 'gold_delta' => $goldDiff, 'rial_delta' => $rialDiff, 'message' => 'ماندهٔ پرگار روی کیف اعمال شد', ]; } /** * @return array{ * documents: array{applied: int, skipped: int, errors: list, logs: list}, * mande: array{applied: bool, gold_delta?: float, rial_delta?: float, message: string} * } */ public function syncWalletFromPargar(Customer $customer): array { return [ 'documents' => $this->replayStoredDocuments($customer), 'mande' => $this->applyLatestMandeDifference($customer), ]; } public function isDocumentApplied(string $docId): bool { if ($docId === '') { return false; } $logs = PargarSyncLog::query() ->where('type', PargarSyncService::TYPE_DOCUMENT) ->where('pargar_ref', $docId) ->where('sync_status', PargarSyncService::STATUS_SUCCESS) ->get(['data']); foreach ($logs as $log) { $data = json_decode((string) $log->data, true); if (($data['wallet_applied'] ?? false) === true) { return true; } } return false; } /** * @param array $tx */ private function applyTransactionDelta(Customer $customer, array $tx, float $goldDelta, float $rialDelta): void { $ownerType = (int) ($customer->type ?? 2); $walletInterface = new WalletInterface((int) $customer->id, $ownerType); $desc = trim((string) ($tx['desc'] ?? '')); $docType = trim((string) ($tx['docType'] ?? '')); $title = $desc !== '' ? $desc : ($docType !== '' ? $docType : 'سند پرگار'); $input = [ 'title' => $title, 'note' => trim((string) ($tx['docId'] ?? '')), 'expire_at' => now(), 'active' => true, ]; if (abs($goldDelta) >= 0.01) { $input['gold'] = abs($goldDelta); } if (abs($rialDelta) >= 1) { $input['cash'] = abs($rialDelta); } new TransactionInterface( $walletInterface, $input, self::TX_TYPE_PARGAR, $goldDelta > 0, $rialDelta > 0 ); } /** * @param array $tx */ private function transactionGoldDelta(array $tx): float { $bes = $this->parseNumber($tx['besGold'] ?? 0); $bed = $this->parseNumber($tx['bedGold'] ?? 0); return $bes - $bed; } /** * @param array $tx */ private function transactionRialDelta(array $tx): float { $bes = $this->parseNumber($tx['besRial'] ?? 0); $bed = $this->parseNumber($tx['bedRial'] ?? 0); return $bes - $bed; } private function isEchoOfRecentOutbound(int $customerId, float $goldDelta, float $rialDelta): bool { $since = now()->subDays(3); $logs = PargarSyncLog::query() ->where('customer_id', $customerId) ->whereIn('type', [PargarSyncService::TYPE_WALLET_CASH, PargarSyncService::TYPE_WALLET_GOLD]) ->where('created_at', '>=', $since) ->whereIn('sync_status', [PargarSyncService::STATUS_PENDING, PargarSyncService::STATUS_SUCCESS]) ->get(); foreach ($logs as $log) { $payload = json_decode((string) $log->data, true) ?: []; $signed = (float) ($payload['signed_amount'] ?? 0); $kind = (string) ($payload['kind'] ?? ''); if ($kind === 'gold' && abs($goldDelta) >= 0.01 && abs(abs($signed) - abs($goldDelta)) < 0.01) { if (($signed > 0 && $goldDelta > 0) || ($signed < 0 && $goldDelta < 0)) { return true; } } if ($kind === 'cash' && abs($rialDelta) >= 1 && abs(abs($signed) - abs($rialDelta)) < 1) { if (($signed > 0 && $rialDelta > 0) || ($signed < 0 && $rialDelta < 0)) { return true; } } } return false; } /** * @param array $tx */ private function recordSkippedDocument( Customer $customer, string $docId, string $accountId, array $tx, string $reason ): void { PargarSyncLog::create([ 'customer_id' => $customer->id, 'desc' => 'سند پرگار — قبلاً از پنل ارسال شده (بدون اعمال مجدد)', 'type' => PargarSyncService::TYPE_DOCUMENT, 'sync_status' => PargarSyncService::STATUS_SUCCESS, 'pargar_ref' => $docId, 'data' => json_encode([ 'accountId' => $accountId, 'transaction' => $tx, 'wallet_applied' => false, 'skip_reason' => $reason, ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), ]); } private function hasPendingOutboundWalletSync(int $customerId): bool { return PargarSyncLog::query() ->where('customer_id', $customerId) ->whereIn('type', [PargarSyncService::TYPE_WALLET_CASH, PargarSyncService::TYPE_WALLET_GOLD]) ->where('sync_status', PargarSyncService::STATUS_PENDING) ->exists(); } private function parseNumber(mixed $value): float { if ($value === null || $value === '') { return 0.0; } return (float) str_replace('/', '.', (string) $value); } }