orderByDesc('id'); \App\DB\Te\TableEngine::create($query, $request) ->initSearch(['desc', 'type']) ->select(['*']) ->export($query); return response()->json($query); } public function show($id) { return response()->json(PargarSyncLog::with('customer')->findOrFail($id)); } public function customerSyncStatus(Request $request) { $panelTotal = Customer::whereNull('deleted_at')->count(); $panelRegistered = Customer::whereNull('deleted_at')->whereNotNull('pargar_account_id')->count(); $panelOnly = $panelTotal - $panelRegistered; $panelOnlyQuery = Customer::whereNull('deleted_at') ->whereNull('pargar_account_id') ->select('id', 'name', 'mobile', 'created_at'); if ($search = $request->input('search')) { $panelOnlyQuery->where(function ($q) use ($search) { $q->where('name', 'like', "%{$search}%") ->orWhere('mobile', 'like', "%{$search}%"); }); } $panelOnlyList = $panelOnlyQuery->orderBy('id')->paginate(20); $inboundOnlyQuery = PargarSyncLog::query() ->where('type', 'customer') ->where('sync_status', PargarSyncService::STATUS_SUCCESS) ->whereNotNull('pargar_ref') ->latest('id'); $inboundOnlyCount = (clone $inboundOnlyQuery)->count(); $inboundSample = $inboundOnlyQuery->limit(100)->get(['id', 'customer_id', 'pargar_ref', 'created_at']); return response()->json([ 'panel_total' => $panelTotal, 'panel_registered' => $panelRegistered, 'panel_only' => $panelOnly, 'pargar_total' => $panelRegistered + $inboundOnlyCount, 'pargar_only_count' => max(0, $inboundOnlyCount), 'panel_only_list' => $panelOnlyList, 'pargar_only_list' => [ 'data' => $inboundSample->map(fn ($row) => [ 'code' => $row->pargar_ref, 'name' => $row->customer?->name ?? '', 'tel' => $row->customer?->mobile ?? '', 'gid' => null, 'group' => '', ])->values(), 'total' => $inboundOnlyCount, 'current_page' => 1, 'last_page' => 1, ], ]); } public function syncPanelToPargar() { $customers = Customer::whereNull('deleted_at') ->whereNull('pargar_account_id') ->get(['id', 'name', 'mobile']); $success = 0; $failed = 0; $errors = []; foreach ($customers as $customer) { $result = $this->sync()->registerCustomer($customer); if ($result['ok'] ?? false) { $success++; } else { $failed++; $errors[] = [ 'name' => $customer->name, 'mobile' => $customer->mobile, 'error' => $result['message'] ?? '', ]; } } return response()->json([ 'ok' => true, 'success' => $success, 'failed' => $failed, 'errors' => $errors, ]); } public function importFromPargar(Request $request) { $validated = $request->validate([ 'customers' => 'required|array', 'customers.*.code' => 'required', 'customers.*.name' => 'required|string', 'customers.*.tel' => 'nullable|string', ]); $success = 0; $failed = 0; $errors = []; foreach ($validated['customers'] as $item) { $mobile = Customer::normalizeMobile($item['tel'] ?? '') ?? ''; $fullName = trim($item['name']); $accountId = (string) $item['code']; $existing = Customer::whereNull('deleted_at') ->where('pargar_account_id', $accountId) ->first(); if ($existing) { $success++; continue; } if ($mobile !== '') { $byMobile = Customer::findActiveByMobile($mobile); if ($byMobile) { $currentAccount = (string) ($byMobile->pargar_account_id ?? ''); if ($currentAccount !== '' && $currentAccount !== $accountId) { $failed++; $errors[] = [ 'name' => $item['name'], 'code' => $item['code'], 'error' => 'شماره موبایل تکراری است', ]; continue; } $byMobile->pargar_account_id = $accountId; if ($fullName !== '') { $byMobile->fname = $fullName; $byMobile->name = $fullName; } $byMobile->save(); $success++; continue; } } $byAccount = Customer::whereNull('deleted_at') ->where('pargar_account_id', $accountId) ->first(); if ($byAccount) { $failed++; $errors[] = [ 'name' => $item['name'], 'code' => $item['code'], 'error' => 'کد حساب پرگار قبلاً ثبت شده است', ]; continue; } try { $customer = new Customer(); $customer->fname = $fullName; $customer->lname = ''; $customer->name = $fullName; $customer->mobile = $mobile; $customer->active = true; $customer->pargar_account_id = $accountId; $customer->referral_id = $this->uniqueReferralId(); $customer->type = 2; if ($mobile !== '') { $customer->password = $mobile; } $customer->save(); $success++; } catch (\Throwable $e) { $failed++; $errors[] = ['name' => $item['name'], 'code' => $item['code'], 'error' => $e->getMessage()]; } } return response()->json([ 'ok' => true, 'success' => $success, 'failed' => $failed, 'errors' => $errors, ]); } public function rollbackImport(Request $request) { $baseQuery = fn () => Customer::whereNotNull('pargar_account_id') ->where(function ($q) { $q->whereNull('name')->orWhere('name', ''); }); $count = $baseQuery()->count(); $sample = $baseQuery() ->select('id', 'fname', 'name', 'mobile', 'pargar_account_id', 'created_at') ->orderByDesc('id') ->limit(20) ->get(); if ($request->boolean('dry_run')) { return response()->json([ 'dry_run' => true, 'count' => $count, 'sample' => $sample, ]); } $deleted = $baseQuery()->forceDelete(); return response()->json([ 'ok' => true, 'deleted' => $deleted, ]); } public function retrySanad($id) { $log = PargarSyncLog::with('customer')->findOrFail($id); $result = $this->sync()->retry($log); return response()->json($result, ($result['ok'] ?? false) ? 200 : 422); } public function registerCustomer($customerId) { $customer = Customer::findOrFail($customerId); $result = $this->sync()->registerCustomer($customer); return response()->json($result, ($result['ok'] ?? false) ? 200 : 422); } public function syncCustomerWallet(Request $request, $customerId) { if (! app(ConfigService::class)->isPargarAccounting()) { return response()->json([ 'ok' => false, 'message' => 'حسابداری پرگار فعال نیست', ], 422); } $customer = Customer::findOrFail($customerId); if (trim((string) ($customer->pargar_account_id ?? '')) === '') { return response()->json([ 'ok' => false, 'message' => 'این مشتری کد حساب پرگار ندارد', ], 422); } $force = $request->boolean('force'); $result = app(PargarInboundWalletService::class)->syncWalletFromPargar($customer, $force); $summary = $this->summarizeWalletSyncResult($result); return response()->json([ 'ok' => $summary['ok'], 'message' => $summary['message'], 'result' => $result, ]); } /** * همگام‌سازی ماندهٔ کیف برای همهٔ مشتریان دارای کد پرگار — تکه‌تکه (offset/limit). */ public function syncAllWallets(Request $request) { if (! app(ConfigService::class)->isPargarAccounting()) { return response()->json([ 'ok' => false, 'message' => 'حسابداری پرگار فعال نیست', ], 422); } $limit = max(1, min(50, (int) $request->input('limit', 20))); $offset = max(0, (int) $request->input('offset', 0)); $force = $request->boolean('force'); $base = Customer::query() ->whereNull('deleted_at') ->whereNotNull('pargar_account_id') ->where('pargar_account_id', '!=', ''); $total = (clone $base)->count(); $customers = (clone $base) ->orderBy('id') ->offset($offset) ->limit($limit) ->get(['id', 'name', 'mobile', 'pargar_account_id', 'type']); $walletService = app(PargarInboundWalletService::class); $success = 0; $changed = 0; $failed = 0; $errors = []; foreach ($customers as $customer) { try { $result = $walletService->syncWalletFromPargar($customer, $force); $summary = $this->summarizeWalletSyncResult($result); $docErrors = $result['documents']['errors'] ?? []; $hardFail = is_array($docErrors) && $docErrors !== [] && ! ($result['mande']['applied'] ?? false); if ($hardFail) { $failed++; $errors[] = [ 'id' => $customer->id, 'name' => $customer->name, 'mobile' => $customer->mobile, 'error' => $summary['message'], ]; continue; } $success++; if ($summary['ok']) { $changed++; } } catch (\Throwable $e) { $failed++; $errors[] = [ 'id' => $customer->id, 'name' => $customer->name, 'mobile' => $customer->mobile, 'error' => $e->getMessage() !== '' ? $e->getMessage() : 'خطای ناشناخته', ]; } } $processed = $customers->count(); $nextOffset = $offset + $processed; return response()->json([ 'ok' => true, 'message' => $total === 0 ? 'مشتری دارای کد پرگار یافت نشد' : "دستهٔ {$offset} تا {$nextOffset} از {$total}", 'total' => $total, 'offset' => $offset, 'processed' => $processed, 'next_offset' => $nextOffset, 'done' => $nextOffset >= $total, 'success' => $success, 'changed' => $changed, 'failed' => $failed, 'errors' => $errors, ]); } /** * @param array{ * documents: array{applied: int, skipped: int, batches: int, acknowledged: int, errors: list}, * mande: array{applied: bool, gold_delta?: float, rial_delta?: float, message: string} * } $result * @return array{ok: bool, message: string} */ private function summarizeWalletSyncResult(array $result): array { $acknowledged = (int) ($result['documents']['acknowledged'] ?? 0); $skippedDocs = (int) ($result['documents']['skipped'] ?? 0); $batches = (int) ($result['documents']['batches'] ?? 0); $appliedMande = (bool) ($result['mande']['applied'] ?? false); $mandeMessage = (string) ($result['mande']['message'] ?? ''); $docErrors = $result['documents']['errors'] ?? []; $ok = $appliedMande || $acknowledged > 0; if ($appliedMande && $acknowledged > 0) { $message = "مانده هم‌تراز شد ({$acknowledged} دسته سند مصرف شد)"; } elseif ($appliedMande) { $message = 'مانده با پرگار هم‌تراز شد'; } elseif ($acknowledged > 0) { $message = $mandeMessage !== '' ? "{$acknowledged} دسته سند مصرف شد — {$mandeMessage}" : "{$acknowledged} دسته سند مصرف شد"; } elseif ($batches > 0 && $skippedDocs > 0) { $message = $mandeMessage !== '' ? $mandeMessage : 'اسناد تکراری بودند؛ تغییری لازم نبود'; } else { $message = $mandeMessage !== '' ? $mandeMessage : 'سند یا ماندهٔ جدیدی برای اعمال نبود'; } if (is_array($docErrors) && $docErrors !== []) { $message .= ' — '.implode('؛ ', array_slice($docErrors, 0, 2)); } return [ 'ok' => $ok, 'message' => $message, ]; } private function uniqueReferralId(): string { do { $referral = substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyz'), 0, 4); } while (Customer::where('referral_id', $referral)->exists()); return $referral; } }