isPargarAccounting()) { return response()->json([ 'success' => false, 'data' => null, 'message' => 'Pargar integration is disabled', ], 503); } [$username, $password] = $this->pargarLoginCredentials($request); $config = app(ConfigService::class); $expectedUser = $this->normalizeCredential($config->getString('pargar_username')); $expectedPass = $this->normalizeCredential($config->getString('pargar_password')); if ($username !== $expectedUser || $password !== $expectedPass || $expectedUser === '') { return response()->json([ 'success' => false, 'data' => null, 'message' => 'Invalid credentials!', ]); } $token = PargarAuth::issueToken($username); return response()->json([ 'success' => true, 'data' => ['token' => $token], 'message' => 'You are logged in successfully.', ]); } public function upsertCustomers(Request $request) { $payload = $this->pargarPayload($request); $customers = $payload['Customers'] ?? []; if (! is_array($customers) || $customers === []) { return response()->json([ 'success' => false, 'message' => 'Customers list is required', 'data' => null, ], 422); } $messages = []; foreach ($customers as $row) { if (! is_array($row)) { continue; } $accountId = trim((string) ($row['AccountId'] ?? '')); $fullName = trim((string) ($row['FullName'] ?? '')); $mobile = Customer::normalizeMobile((string) ($row['CellPhone'] ?? '')); if ($accountId === '' || $mobile === null) { $messages[] = 'ردیف نامعتبر: AccountId یا CellPhone خالی است'; continue; } $byAccount = Customer::whereNull('deleted_at') ->where('pargar_account_id', $accountId) ->first(); if ($byAccount && Customer::normalizeMobile($byAccount->mobile) !== $mobile) { $messages[] = "کد حساب {$accountId} قبلاً برای مشتری دیگری ثبت شده است"; continue; } $customer = Customer::findActiveByMobile($mobile); if ($customer) { $currentAccount = (string) ($customer->pargar_account_id ?? ''); if ($currentAccount === $accountId) { $messages[] = 'کد حساب مشتری وجود دارد'; continue; } if ($currentAccount !== '' && $currentAccount !== $accountId) { $messages[] = "شماره {$mobile} قبلاً به حساب پرگار دیگری متصل است"; continue; } $customer->pargar_account_id = $accountId; $this->applyPargarCustomerName($customer, $fullName); $customer->save(); app(PargarInboundWalletService::class)->queueWalletSyncAfterResponse((int) $customer->id); $messages[] = "کد حسابداری مشتری {$fullName} ویرایش شد"; continue; } if ($byAccount) { if (Customer::isMobileTaken($mobile, (int) $byAccount->id)) { $messages[] = "شماره {$mobile} تکراری است"; continue; } if ($fullName !== '') { $this->applyPargarCustomerName($byAccount, $fullName); } $byAccount->mobile = $mobile; if (empty($byAccount->password)) { $byAccount->password = $mobile; } $byAccount->save(); app(PargarInboundWalletService::class)->queueWalletSyncAfterResponse((int) $byAccount->id); $messages[] = "مشتری با کد حسابداری {$accountId} به‌روزرسانی شد"; continue; } try { $customer = new Customer(); $this->applyPargarCustomerName($customer, $fullName !== '' ? $fullName : 'مشتری'); $customer->mobile = $mobile; $customer->pargar_account_id = $accountId; $customer->active = true; $customer->type = 2; $customer->password = $mobile; $customer->referral_id = $this->uniqueReferralId(); $customer->save(); app(PargarInboundWalletService::class)->queueWalletSyncAfterResponse((int) $customer->id); $messages[] = "مشتری با کد حسابداری {$accountId} ایجاد شد"; } catch (\Throwable $e) { $messages[] = "خطا برای {$mobile}: " . ( $e instanceof UnprocessableEntityHttpException ? $e->getMessage() : 'ثبت مشتری ناموفق بود' ); } } return response()->json([ 'success' => true, 'message' => implode(' | ', $messages), 'data' => null, ]); } public function listOrders(Request $request) { $documents = collect(); $orders = Order::query() ->where('sts', 2) ->whereNull('pargar_confirmed_at') ->limit(100) ->get(); foreach ($orders as $order) { $payload = $this->orderExportPayload($order); if ($payload === null) { continue; } $documents->push($payload); } $walletLogs = PargarSyncLog::query() ->whereIn('type', [PargarSyncService::TYPE_WALLET_CASH, PargarSyncService::TYPE_WALLET_GOLD]) ->where('sync_status', PargarSyncService::STATUS_PENDING) ->orderBy('id') ->limit(100) ->get(); foreach ($walletLogs as $log) { $payload = $this->walletExportPayload($log); if ($payload !== null) { $documents->push($payload); } } return response()->json([ 'state' => 'true', 'msg' => '', 'data' => [ 'documents' => $documents->values(), 'duplicated' => 'false', 'result' => 'OK', ], ]); } public function confirmOrders(Request $request) { $payload = $this->pargarPayload($request); $orderIds = $payload['OrderIds'] ?? []; if (! is_array($orderIds)) { return response()->json([ 'success' => false, 'message' => 'OrderIds must be an array', 'data' => null, ], 422); } $result = $this->sync()->confirmOrders($orderIds); // سند در پرگار ثبت شد → ماندهٔ معطل‌مانده را همگام کن foreach ($result['customer_ids'] ?? [] as $customerId) { $this->inboundWallet()->queueWalletSyncAfterResponse((int) $customerId); } return response()->json([ 'success' => true, 'message' => 'تاییدیه اسناد با موفقیت دریافت شد', 'data' => null, ]); } protected function inboundWallet(): PargarInboundWalletService { return app(PargarInboundWalletService::class); } public function storeDocuments(Request $request) { $data = $this->pargarPayload($request); $accountId = trim((string) ($data['accountId'] ?? '')); $summary = $this->sync()->summarizeInboundDocument($data); $customer = $accountId !== '' ? Customer::where('pargar_account_id', $accountId)->first() : null; // فقط لاگ — replay تاریخچه روی کیف خاموش است؛ بعد از ذخیره، مانده به‌صورت خودکار هم‌تراز می‌شود PargarSyncLog::create([ 'customer_id' => $customer?->id, 'amount' => $summary['amount'], 'desc' => $customer ? ($summary['desc'] !== '' ? $summary['desc'] : 'دریافت اسناد از پرگار') : 'دریافت اسناد از پرگار — مشتری یافت نشد', 'type' => PargarSyncService::TYPE_DOCUMENT, 'sync_status' => PargarSyncService::STATUS_SUCCESS, 'pargar_ref' => $accountId !== '' ? $accountId : null, 'data' => json_encode(array_merge($data, [ '_summary' => $summary, 'wallet_applied' => false, 'code_build' => 'docs-then-mande-auto', ]), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), ]); \Illuminate\Support\Facades\Log::info('Pargar documents stored; queue auto mande sync', [ 'account_id' => $accountId, 'customer_id' => $customer?->id, 'tx_count' => is_array($data['transactions'] ?? null) ? count($data['transactions']) : 0, ]); // به‌محض ورود اسناد → ack + اعمال آخرین مانده (با تأخیر کوتاه تا webhook مانده قفل را بگیرد) if ($customer) { $this->inboundWallet()->queueWalletSyncAfterResponse((int) $customer->id, delaySeconds: 2); } return response()->json([ 'success' => true, 'message' => 'اسناد ثبت شد؛ همگام‌سازی مانده در صف است', 'data' => [ 'wallet_applied' => false, 'auto_mande_queued' => (bool) $customer, 'build' => 'docs-then-mande-auto', ], ]); } public function storeMande(Request $request) { $data = $this->pargarPayload($request); $accountId = trim((string) ($data['AccountId'] ?? '')); $customer = Customer::where('pargar_account_id', $accountId)->first(); $goldInv = isset($data['GoldInv']) ? (float) str_replace('/', '.', (string) $data['GoldInv']) : 0; $rialInv = isset($data['RialInv']) ? (float) str_replace('/', '.', (string) $data['RialInv']) : 0; if (! $customer) { PargarSyncLog::create([ 'customer_id' => null, 'desc' => 'مانده پرگار — مشتری هنوز در پنل نیست', 'type' => PargarSyncService::TYPE_MANDE, 'amount' => $goldInv, 'sync_status' => PargarSyncService::STATUS_SUCCESS, 'pargar_ref' => $accountId, 'data' => json_encode(array_merge($data, [ 'orphan_mande' => true, 'wallet_adjusted' => false, ]), JSON_UNESCAPED_UNICODE), ]); return response()->json([ 'success' => true, 'message' => 'مانده ذخیره شد؛ پس از ثبت مشتری اعمال می‌شود', 'data' => null, ]); } $ownerType = (int) ($customer->type ?? 2); $walletInterface = new WalletInterface($customer->id, $ownerType); $wallet = $walletInterface->wallet; // GoldInv پرگار با جمع آزاد+قلک محصولات کیف اصلی مقایسه می‌شود (انتقال قلک سند نمی‌زند) $appGold = app(WalletBalanceService::class) ->accountingVisibleMainWalletGold((int) $customer->id, $ownerType); $appCash = (float) ($wallet->cash ?? 0); $goldDiff = $goldInv - $appGold; $rialDiff = $rialInv - $appCash; $hasDiff = PargarSyncService::isWalletDocAmount($goldDiff) || abs($rialDiff) >= 1; $pendingOutbound = $this->inboundWallet()->hasPendingOutboundToPargar($customer); $desc = $pendingOutbound ? 'مانده دریافت شد — منتظر ثبت سند پنل در پرگار' : ($hasDiff ? 'اختلاف مانده پرگار با اپ — در حال اعمال روی کیف' : 'دریافت مانده از پرگار — بدون اختلاف'); PargarSyncLog::create([ 'customer_id' => $customer->id, 'desc' => $desc, 'type' => PargarSyncService::TYPE_MANDE, 'amount' => $goldInv, 'sync_status' => PargarSyncService::STATUS_SUCCESS, 'pargar_ref' => $accountId, 'data' => json_encode(array_merge($data, [ 'app_gold' => $appGold, 'app_cash' => $appCash, 'gold_diff' => $goldDiff, 'rial_diff' => $rialDiff, 'wallet_adjusted' => false, 'awaiting_outbound_confirm' => $pendingOutbound, ]), JSON_UNESCAPED_UNICODE), ]); // بدون سند pending → همین‌جا اعمال؛ اگر قفل/خطا بود صف با تأخیر تکرار می‌کند // (اسناد و مانده اغلب پشت‌سرهم می‌آیند و قفل هم‌زمان شایع است) $appliedNow = false; if ($pendingOutbound) { $this->inboundWallet()->queueWalletSyncAfterResponse((int) $customer->id, delaySeconds: 2); } else { try { $syncResult = $this->inboundWallet()->syncWalletFromPargar($customer, forceMande: false, auto: true); $appliedNow = (bool) ($syncResult['mande']['applied'] ?? false); } catch (\Throwable $e) { report($e); } if (! $appliedNow) { $this->inboundWallet()->queueWalletSyncAfterResponse((int) $customer->id, delaySeconds: 3); } } return response()->json([ 'success' => true, 'message' => $pendingOutbound ? 'مانده ذخیره شد؛ بعد از ثبت سند در پرگار خودکار سینک می‌شود' : ($appliedNow ? 'مانده دریافت و روی کیف اپ اعمال شد' : 'مانده دریافت شد؛ همگام‌سازی خودکار در صف است'), 'data' => [ 'awaiting_outbound_confirm' => $pendingOutbound, 'wallet_applied' => $appliedNow, 'auto_sync_queued' => ! $appliedNow, ], ]); } public function listNewCustomers(Request $request) { $customers = Customer::query() ->whereNull('deleted_at') ->where(function ($q) { $q->whereNull('pargar_account_id') ->orWhere('pargar_account_id', '') ->orWhere('pargar_account_id', '0') ->orWhere('pargar_account_id', '-1'); }) ->limit(100) ->get(['id', 'fname', 'lname', 'name', 'mobile']); if ($customers->isEmpty()) { return response()->json([ 'success' => true, 'message' => 'مشتری جدید وجود ندارد', 'data' => null, ]); } $rows = $customers->map(function (Customer $customer) { $fname = trim((string) ($customer->fname ?? '')); $lname = trim((string) ($customer->lname ?? '')); if ($fname === '' && $lname === '') { $nameParts = preg_split('/\s+/', trim((string) ($customer->name ?? '')), 2) ?: ['', '']; $fname = trim((string) ($nameParts[0] ?? '')); $lname = trim((string) ($nameParts[1] ?? '')); } return [ 'id' => (string) $customer->id, 'name' => $fname, 'family' => $lname, 'mobile' => (string) $customer->mobile, ]; }); return response()->json([ 'state' => 'true', 'msg' => '', 'customer' => $rows, ]); } public function confirmCustomers(Request $request) { $payload = $this->pargarPayload($request); $rows = $payload['customers'] ?? []; if (! is_array($rows)) { return response()->json([ 'success' => false, 'message' => 'customers must be an array', 'data' => null, ], 422); } foreach ($rows as $row) { if (! is_array($row)) { continue; } $customerId = $row['customer_id'] ?? $row['id_customer'] ?? null; $accountId = $row['accounting_id'] ?? null; if ($customerId === null || $accountId === null) { continue; } $normalizedAccountId = trim((string) $accountId); if ($normalizedAccountId === '' || in_array($normalizedAccountId, ['0', '-1'], true)) { continue; } $customer = Customer::find($customerId); if ($customer) { $customer->pargar_account_id = $normalizedAccountId; $customer->save(); $log = PargarSyncLog::query() ->where('customer_id', $customer->id) ->where('type', 'customer') ->where('sync_status', PargarSyncService::STATUS_PENDING) ->latest('id') ->first(); if ($log) { $this->sync()->markSuccess($log, $normalizedAccountId); } app(PargarInboundWalletService::class)->queueWalletSyncAfterResponse((int) $customer->id); } } return response()->json([ 'success' => true, 'message' => 'تاییدیه ثبت مشتری با موفقیت دریافت شد', 'data' => null, ]); } /** * @return array{0: string, 1: string} */ private function pargarLoginCredentials(Request $request): array { $payload = $this->pargarPayload($request); return [ $this->normalizeCredential((string) ($payload['username'] ?? '')), $this->normalizeCredential((string) ($payload['password'] ?? '')), ]; } /** * @return array */ private function pargarPayload(Request $request): array { $payload = $request->all(); if ($payload !== []) { return $payload; } $decoded = json_decode((string) $request->getContent(), true); return is_array($decoded) ? $decoded : []; } private function normalizeCredential(string $value): string { $persian = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹']; $arabic = ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩']; $latin = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; return trim(str_replace(array_merge($persian, $arabic), array_merge($latin, $latin), $value)); } private function applyPargarCustomerName(Customer $customer, string $fullName): void { $fullName = trim($fullName); if ($fullName === '') { return; } [$fname, $lname] = $this->splitPargarFullName($fullName); $customer->fname = $fname; $customer->lname = $lname; // mutator مدل Customer مقدار name را از fname + lname می‌سازد $customer->name = trim("$fname $lname"); } /** * @return array{0: string, 1: string} */ private function splitPargarFullName(string $fullName): array { $parts = preg_split('/\s+/u', trim($fullName), 2) ?: ['', '']; return [ trim((string) ($parts[0] ?? '')), trim((string) ($parts[1] ?? '')), ]; } private function uniqueReferralId(): string { do { $referral = substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyz'), 0, 4); } while (Customer::where('referral_id', $referral)->exists()); return $referral; } private function pargarItemName(Order $order): string { return $this->isCartOrder($order) ? 'سبد خرید' : 'آبشده'; } private function isCartOrder(Order $order): bool { if (Schema::hasTable('cart') && Schema::hasColumn('cart', 'order_id')) { return Cart::where('order_id', $order->id)->exists(); } return PargarSyncLog::where('order_id', $order->id) ->whereIn('desc', ['سبد خرید', 'فروش سبد به مشتری']) ->exists(); } /** * @return array|null */ private function orderExportPayload(Order $order): ?array { $sync = $this->sync(); $log = PargarSyncLog::where('order_id', $order->id)->latest('id')->first(); $payload = $log ? (json_decode((string) $log->data, true) ?: []) : []; $paymentKind = (string) ($payload['payment_kind'] ?? PargarSyncService::PAYMENT_CASH); if ($paymentKind === PargarSyncService::PAYMENT_OTHER_PRODUCT) { return null; } if ($log && is_string($log->pargar_ref) && str_starts_with($log->pargar_ref, 'skip:')) { return null; } $customer = Customer::find((int) ($order->customer_id ?? 0)); $isCustomerBuy = (int) ($order->direction ?? 1) === 1; $customerDirection = (int) ($order->direction ?? 0); $fromCart = (bool) ($payload['from_cart'] ?? $this->isCartOrder($order)); $product = $sync->resolveOrderProduct($order, $payload); $number = (float) ($payload['number'] ?? 0); $amount = abs((float) ($order->amount ?? 0)); $totalPrice = (float) ($order->total_price ?? $order->payment_amount ?? 0); $unitPrice = (float) ($order->price ?? 0); $isCountBased = $product ? self::isCountBasedProduct(self::productUnitPayload($product)) : false; // پرگار: Count>0 → سند تعدادی (وزن را نادیده می‌گیرد) // آبشده/وزنی مثل آذین: Count=0 و Weight=گرم $count = 0; $weight = $amount; if ($isCountBased) { $count = $number > 0 ? max(1, (int) round($number)) : 1; $weight = $amount; } $itemId = $sync->pargarItemIdGold(); $itemName = $this->pargarItemName($order); if ($product && trim((string) $product->title) !== '') { $itemName = trim((string) $product->title); } $tradedProductId = (int) ($payload['product_id'] ?? $order->product_id ?? 0); // فقط محصولات main_wallet_products روی GoldInv آبشده اثر دارند. // group_type=آبشده به‌تنهایی کافی نیست (مثلاً آبشده سرمایه‌گذاری / قلک). if ($tradedProductId > 0) { $tradesMainGold = app(ConfigService::class)->isMainWalletProduct($tradedProductId); } else { $tradesMainGold = $product && self::isMeltedTradeGroup($product->group_type ?? null); } // اثر مانده از روش پرداخت؛ وزن طلا فقط وقتی کالای معامله به کیف اصلی مربوط است if ($paymentKind === PargarSyncService::PAYMENT_CASH) { if ($tradesMainGold) { // خرید/فروش آبشده کیف‌اصلی با نقدی: وزن + مبلغ — حتماً تعدادی نباشد $itemId = $sync->pargarItemIdGold(); $count = 0; $weight = $amount; } else { // محصول غیر اصلی با پرداخت نقدی: فقط ریال (بدون وزن طلای پرگار) $rialItemId = $sync->pargarItemIdRial(); $itemId = $rialItemId > 0 ? $rialItemId : $sync->pargarItemIdGold(); $weight = 0; $count = 0; } } elseif ($paymentKind === PargarSyncService::PAYMENT_MAIN_GOLD) { $itemId = $sync->pargarItemIdGold(); $itemName = $sync->mainWalletGoldItemName(); $paymentGold = abs((float) ($payload['payment_gold'] ?? $amount)); $weight = $paymentGold; $count = 0; $totalPrice = 0; } return [ 'OrderId' => (string) $order->id, 'OrderType' => $isCustomerBuy ? -1 : 1, 'PriceType' => 1, 'CustomerAccountId' => (int) ($customer->pargar_account_id ?? 0), 'ItemId' => $itemId, 'ItemName' => $itemName, 'OrderDate' => $order->pay_at ? Jalalian::fromDateTime($order->pay_at)->format('Y/m/d-H:i:s') : Jalalian::fromDateTime($order->created_at)->format('Y/m/d-H:i:s'), 'Fee' => $unitPrice, 'Count' => $count, 'Weight' => $weight, 'Price' => $totalPrice, 'Carat' => 750, 'Description' => self::accountingShopTradeDesc($customerDirection, $fromCart), ]; } /** * @return array|null */ private function walletExportPayload(PargarSyncLog $log): ?array { $sync = $this->sync(); $payload = json_decode((string) $log->data, true) ?: []; $kind = (string) ($payload['kind'] ?? ($log->type === PargarSyncService::TYPE_WALLET_GOLD ? 'gold' : 'cash')); $signed = (float) ($payload['signed_amount'] ?? $log->amount ?? 0); if (! PargarSyncService::isWalletDocAmount($signed)) { $signed = (float) ($log->amount ?? 0); // اگر علامت نبود، از desc حدس بزن — پیش‌فرض واریز مثبت if (! isset($payload['signed_amount']) && is_string($log->desc) && str_contains($log->desc, 'برداشت')) { $signed = -abs($signed); } elseif (! isset($payload['signed_amount'])) { $signed = abs($signed); } } $customer = Customer::find((int) ($log->customer_id ?? 0)); $withdrawId = (int) ($log->withdraw_id ?? 0); $orderId = $withdrawId > 0 ? 'W-' . $withdrawId : 'WLOG-' . $log->id; $isDeposit = $signed >= 0; $note = trim((string) ($payload['note'] ?? '')); $docDesc = self::accountingWalletDesc($isDeposit, $note !== '' ? $note : null); // قرارداد پرگار (همان چیزی که سند سفارش هم با آن ساخته می‌شود): // OrderType = 1 → سند «خرید» فروشگاه: وزن بدهکار، ریال بستانکار // OrderType = -1 → سند «فروش» فروشگاه: وزن بستانکار، ریال بدهکار // مانده در پرگار = بستانکار − بدهکار، پس برای اینکه واریز در حساب مشتری مثبت بنشیند: // واریز طلای کیف اصلی → باید وزن بستانکار شود → OrderType = -1 // واریز ریالی → باید ریال بستانکار شود → OrderType = 1 // برچسب UI پرگار ممکن است همچنان «خرید/فروش پولی» بسازد؛ شرح ارسالی همیشه واریز/برداشت/پاداش است. $orderType = $kind === 'gold' ? ($isDeposit ? -1 : 1) : ($isDeposit ? 1 : -1); if ($kind === 'gold') { return [ 'OrderId' => $orderId, 'OrderType' => $orderType, 'PriceType' => 1, 'CustomerAccountId' => (int) ($customer->pargar_account_id ?? 0), 'ItemId' => $sync->pargarItemIdGold(), 'ItemName' => $sync->mainWalletGoldItemName(), 'OrderDate' => Jalalian::fromDateTime($log->date ?? $log->created_at)->format('Y/m/d-H:i:s'), 'Fee' => 0, 'Count' => 0, 'Weight' => abs($signed), 'Price' => 0, 'Carat' => 750, 'Description' => $docDesc, ]; } $rialItemId = $sync->pargarItemIdRial(); return [ 'OrderId' => $orderId, 'OrderType' => $orderType, 'PriceType' => 1, 'CustomerAccountId' => (int) ($customer->pargar_account_id ?? 0), 'ItemId' => $rialItemId > 0 ? $rialItemId : $sync->pargarItemIdGold(), 'ItemName' => $docDesc, 'OrderDate' => Jalalian::fromDateTime($log->date ?? $log->created_at)->format('Y/m/d-H:i:s'), 'Fee' => 0, 'Count' => 0, 'Weight' => 0, 'Price' => abs($signed), 'Carat' => 750, 'Description' => $docDesc, ]; } }