From ebfeb24e60c20f0d8aaae0ef060a04f3ec94023a Mon Sep 17 00:00:00 2001 From: adonis Date: Tue, 21 Jul 2026 12:36:59 +0330 Subject: [PATCH] fix pargar bugs --- .../Pargar/PargarInboundController.php | 37 ++- app/Services/PargarInboundWalletService.php | 260 ++++++++++++++++++ app/Services/PargarSyncService.php | 87 ++++++ database/sql/deploy_settings_const.sql | 6 +- 4 files changed, 381 insertions(+), 9 deletions(-) create mode 100644 app/Services/PargarInboundWalletService.php diff --git a/app/Http/Controllers/Pargar/PargarInboundController.php b/app/Http/Controllers/Pargar/PargarInboundController.php index 4f69870..41a7fc6 100644 --- a/app/Http/Controllers/Pargar/PargarInboundController.php +++ b/app/Http/Controllers/Pargar/PargarInboundController.php @@ -9,6 +9,7 @@ use App\Models\Customer; use App\Models\Order; use App\Models\PargarSyncLog; use App\Services\ConfigService; +use App\Services\PargarInboundWalletService; use App\Services\PargarSyncService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Schema; @@ -224,28 +225,52 @@ class PargarInboundController extends Controller ]); } + protected function inboundWallet(): PargarInboundWalletService + { + return app(PargarInboundWalletService::class); + } + public function storeDocuments(Request $request) { $data = $this->pargarPayload($request); $accountId = trim((string) ($data['accountId'] ?? '')); - $transactions = $data['transactions'] ?? []; $customer = $accountId !== '' ? Customer::where('pargar_account_id', $accountId)->first() : null; + if ($customer) { + $applyResult = $this->inboundWallet()->applyDocuments($customer, $data); + + return response()->json([ + 'success' => true, + 'message' => sprintf( + 'اسناد مشتری دریافت شد — اعمال: %d، رد شده: %d', + $applyResult['applied'], + $applyResult['skipped'] + ), + 'data' => $applyResult, + ]); + } + + // مشتری ناشناس — فقط لاگ برای بررسی ادمین + $summary = $this->sync()->summarizeInboundDocument($data); PargarSyncLog::create([ - 'customer_id' => $customer?->id, - 'desc' => 'دریافت اسناد از پرگار', + 'customer_id' => null, + 'amount' => $summary['amount'], + 'desc' => 'دریافت اسناد از پرگار — مشتری یافت نشد', 'type' => PargarSyncService::TYPE_DOCUMENT, 'sync_status' => PargarSyncService::STATUS_SUCCESS, - 'pargar_ref' => $accountId, - 'data' => json_encode($data, JSON_UNESCAPED_UNICODE), + 'pargar_ref' => $accountId !== '' ? $accountId : null, + 'data' => json_encode(array_merge($data, [ + '_summary' => $summary, + 'wallet_applied' => false, + ]), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), ]); return response()->json([ 'success' => true, - 'message' => 'اسناد مشتری با موفقیت دریافت شد', + 'message' => 'اسناد دریافت شد ولی مشتری با این کد حساب یافت نشد', 'data' => null, ]); } diff --git a/app/Services/PargarInboundWalletService.php b/app/Services/PargarInboundWalletService.php new file mode 100644 index 0000000..f215cc6 --- /dev/null +++ b/app/Services/PargarInboundWalletService.php @@ -0,0 +1,260 @@ + $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; + } + + public function isDocumentApplied(string $docId): bool + { + if ($docId === '') { + return false; + } + + return PargarSyncLog::query() + ->where('type', PargarSyncService::TYPE_DOCUMENT) + ->where('pargar_ref', $docId) + ->where('sync_status', PargarSyncService::STATUS_SUCCESS) + ->exists(); + } + + /** + * @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 parseNumber(mixed $value): float + { + if ($value === null || $value === '') { + return 0.0; + } + + return (float) str_replace('/', '.', (string) $value); + } +} diff --git a/app/Services/PargarSyncService.php b/app/Services/PargarSyncService.php index 048c5ea..4e49894 100644 --- a/app/Services/PargarSyncService.php +++ b/app/Services/PargarSyncService.php @@ -490,6 +490,93 @@ class PargarSyncService return $title !== '' ? $title : 'آبشده'; } + /** + * خلاصهٔ اسناد ورودی از پرگار برای نمایش در جدول ادمین. + * + * @param array $data + * @return array{ + * desc: string, + * amount: float|null, + * amount_unit: 'gold'|'rial'|null, + * tx_count: int, + * account_name: string|null + * } + */ + public function summarizeInboundDocument(array $data): array + { + $accountName = trim((string) ($data['accountName'] ?? '')); + $transactions = is_array($data['transactions'] ?? null) ? $data['transactions'] : []; + + $txLines = []; + $maxGold = 0.0; + $maxRial = 0.0; + $txCount = 0; + + foreach ($transactions as $tx) { + if (! is_array($tx)) { + continue; + } + + $txCount++; + $desc = trim((string) ($tx['desc'] ?? '')); + $docType = trim((string) ($tx['docType'] ?? '')); + $line = $desc !== '' ? $desc : ($docType !== '' ? $docType : null); + if ($line !== null) { + $txLines[] = $line; + } + + $maxGold = max( + $maxGold, + abs($this->parsePargarNumber($tx['bedGold'] ?? 0)), + abs($this->parsePargarNumber($tx['besGold'] ?? 0)) + ); + $maxRial = max( + $maxRial, + abs($this->parsePargarNumber($tx['bedRial'] ?? 0)), + abs($this->parsePargarNumber($tx['besRial'] ?? 0)) + ); + } + + if ($txLines !== []) { + $unique = array_values(array_unique($txLines)); + $desc = implode(' | ', array_slice($unique, 0, 3)); + if ($txCount > count(array_slice($unique, 0, 3))) { + $desc .= ' (+' . ($txCount - min(3, count($unique))) . ' سند دیگر)'; + } + } elseif ($accountName !== '') { + $desc = "اسناد {$accountName} از پرگار"; + } else { + $desc = $txCount > 0 ? "دریافت {$txCount} سند از پرگار" : 'دریافت اسناد از پرگار'; + } + + $amount = null; + $amountUnit = null; + if ($maxGold >= 0.01) { + $amount = $maxGold; + $amountUnit = 'gold'; + } elseif ($maxRial >= 1) { + $amount = $maxRial; + $amountUnit = 'rial'; + } + + return [ + 'desc' => $desc, + 'amount' => $amount, + 'amount_unit' => $amountUnit, + 'tx_count' => $txCount, + 'account_name' => $accountName !== '' ? $accountName : null, + ]; + } + + private function parsePargarNumber(mixed $value): float + { + if ($value === null || $value === '') { + return 0.0; + } + + return (float) str_replace('/', '.', (string) $value); + } + public function resolveOrderProduct(?Order $order, ?array $payload = null): ?Product { $productId = (int) ($payload['product_id'] ?? $order?->product_id ?? 0); diff --git a/database/sql/deploy_settings_const.sql b/database/sql/deploy_settings_const.sql index 3954e1c..1b68756 100644 --- a/database/sql/deploy_settings_const.sql +++ b/database/sql/deploy_settings_const.sql @@ -1010,7 +1010,7 @@ SELECT 'main_wallet_products', '13', 'محصولات متصل به کیف اصل WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'main_wallet_products' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) -SELECT 'require_zero_main_gold_before_other_buy', 'true', 'الزام صفر بودن کیف اصلی قبل از خرید محصول دیگر', 0, 1, +SELECT 'require_zero_main_gold_before_other_buy', 'true', 'الزام صفر بودن کیف اصلی قبل از خرید محصول دیگر', 1, 1, 'روشن: اگر مانده طلای کیف اصلی صفر یا زیر صفر باشد (بدهی)، مشتری ابتدا باید آبشده/مانده کیف اصلی را صفر کند و بعد می‌تواند محصول دیگر بخرد. خاموش: بدون این محدودیت. معامله خودِ محصولات کیف اصلی همیشه مجاز است.', 'switch' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'require_zero_main_gold_before_other_buy' LIMIT 1); @@ -1366,7 +1366,7 @@ UPDATE `settings` SET `can_edit` = 0 WHERE `key` = 'main_wallet_products'; UPDATE `settings` SET `title_fa` = 'الزام صفر بودن کیف اصلی قبل از خرید محصول دیگر', `type` = 'switch', - `can_edit` = 0, + `can_edit` = 1, `description` = 'روشن: اگر مانده طلای کیف اصلی صفر یا زیر صفر باشد (بدهی)، مشتری ابتدا باید آبشده/مانده کیف اصلی را صفر کند و بعد می‌تواند محصول دیگر بخرد. خاموش: بدون این محدودیت. معامله خودِ محصولات کیف اصلی همیشه مجاز است.' WHERE `key` = 'require_zero_main_gold_before_other_buy'; UPDATE `settings` SET `title_fa` = 'الزام صفر بودن کیف اصلی قبل از خرید محصول دیگر' WHERE `key` = 'require_zero_main_gold_before_other_buy'; @@ -1580,7 +1580,6 @@ UPDATE `settings` SET `can_edit` = 0 WHERE `key` IN ( 'sms_panel', 'logo_login', 'logo_register', 'title_pwa', 'title_fav', 'main_product', 'main_wallet_products', 'metal_type', 'main_metal', - 'require_zero_main_gold_before_other_buy', 'montify_name', 'montzar_base_url', 'montzar_closed', 'montzar_diff', 'montzar_last_product_log', 'montzar_live_price', 'montzar_mazaneh', @@ -1608,6 +1607,7 @@ UPDATE `settings` SET `can_edit` = 1 WHERE `key` IN ( 'name_fa', 'auto_active_customer', 'balance_report_exclude_customer_id', 'buy_fee', 'charge_notif', 'close_price', + 'require_zero_main_gold_before_other_buy', 'sms_notif_order_admin', 'sms_notif_order_customer', 'sms_notif_withdraw_admin', 'sms_notif_withdraw_otp', 'sms_notif_login_otp', 'sms_notif_gateway_charge_admin', 'sms_notif_register_customer', 'sms_notif_register_admin',