lastPrice(request()); if (is_array($response)) { return $response; } if (is_object($response) && method_exists($response, 'getData')) { return (array) $response->getData(true); } return (array) $response; } public function table(Request $request) { app(TahesabSyncService::class)->ensureLocalTable(); if (! Schema::hasTable('tahesab')) { return response()->json([]); } $view = (string) $request->input('view', 'all'); $query = Tahesab::with('customer')->orderByDesc('id'); if ($view === 'ok') { $query->whereNotNull('tahesab_id')->where('tahesab_id', '!=', ''); } elseif ($view === 'failed') { $query->where(function ($q) { $q->where(function ($inner) { $inner->whereNull('tahesab_id')->orWhere('tahesab_id', ''); })->orWhere('sync_status', 'failed'); }); } \App\DB\Te\TableEngine::create($query, $request) ->initSearch(['desc', 'type']) ->select(['*']) ->export($query); return response()->json($query); } public function show($id) { $tahesab = Tahesab::with('customer')->findOrFail($id); return response()->json($tahesab); } /** همه مشتریان ته‌حساب را از API می‌گیرد و برمی‌گرداند (Code => [Code,Name,Tel]) */ private function fetchAllTahesabCustomers(): array { try { $response = Http::withoutVerifying()->timeout(30)->withHeaders([ 'DBName' => $this->Config('tahesab_db'), 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $this->Config('tahesab_token'), ])->post($this->tahesabApiUrl(), [ 'DoListMoshtari' => [1, 999999], ])->json(); if (! is_array($response)) return []; return $this->tahesabCustomerRows($response); } catch (\Exception $e) { return []; } } public function customerSyncStatus(Request $request): \Illuminate\Http\JsonResponse { app(TahesabDocSyncService::class)->tickIfStale(); // مشتریان پنل $panelTotal = Customer::whereNull('deleted_at')->count(); $panelRegistered = Customer::whereNull('deleted_at')->whereNotNull('moshtari_code')->count(); $panelOnly = $panelTotal - $panelRegistered; $panelOnlyQuery = Customer::whereNull('deleted_at')->whereNull('moshtari_code') ->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); // مشتریان ته‌حساب — فقط وقتی include_tahesab=true ارسال شده (چون کند است) $tahesabTotal = null; $tahesabOnlyCount = null; $tahesabOnlyResult = ['data' => [], 'total' => 0, 'current_page' => 1, 'last_page' => 1]; $tahesabWithPhoneCount = null; $tahesabWithoutPhoneCount = null; $tahesabImportableCount = null; $phoneMatchCount = null; $phoneConflictCount = null; $phoneMatches = []; $phoneConflicts = []; if ($request->boolean('include_tahesab')) { $tahesabCustomers = $this->fetchAllTahesabCustomers(); $tahesabTotal = count($tahesabCustomers); $walletService = app(TahesabInboundWalletService::class); $maps = $this->panelAccountingMaps(); $tahesabOnlyList = []; $debugSample = []; $withPhone = 0; $withoutPhone = 0; foreach ($tahesabCustomers as $item) { if (! is_array($item)) continue; if (count($debugSample) < 3) $debugSample[] = $item; $code = $this->extractTahesabCode($item); $codeKey = $this->accountingCodeKey($code); $phone = $this->extractTahesabPhone($item); $hasPhone = Customer::normalizeMobile($phone) !== null; if ($hasPhone) { $withPhone++; } else { $withoutPhone++; } if ($codeKey !== null && isset($maps['by_code'][$codeKey])) { continue; } $mobileKey = Customer::normalizeMobile($phone); if ($mobileKey && isset($maps['by_mobile'][$mobileKey])) { $panel = $maps['by_mobile'][$mobileKey]; $panelCodeKey = $this->accountingCodeKey($panel->moshtari_code); $row = [ 'code' => $code !== null ? (int) $code : null, 'name' => trim((string) ($item['Name'] ?? $item['name'] ?? '')), 'tel' => $phone, 'panel_id' => (int) $panel->id, 'panel_name' => $panel->name, 'panel_code' => $panelCodeKey, ]; if ($panelCodeKey && $panelCodeKey !== $codeKey) { $phoneConflicts[] = $row; } else { $phoneMatches[] = $row; } continue; } if ($codeKey === null) { continue; } $mande = $walletService->mandeFromAccountingRow($item); $tahesabOnlyList[] = [ 'code' => $code !== null ? (int) $code : null, 'name' => trim((string) ($item['Name'] ?? $item['name'] ?? '')), 'tel' => $phone, 'gid' => $item['GID'] ?? $item['gid'] ?? null, 'group' => $item['GoroupName'] ?? $item['group'] ?? '', 'has_phone' => $hasPhone, 'gold' => $mande['gold'] ?? null, 'cash' => $mande['cash'] ?? null, ]; } $tahesabOnlyCount = count($tahesabOnlyList); $tahesabWithPhoneCount = $withPhone; $tahesabWithoutPhoneCount = $withoutPhone; $tahesabImportableCount = count(array_filter($tahesabOnlyList, fn ($r) => ! empty($r['has_phone']))); $phoneMatchCount = count($phoneMatches); $phoneConflictCount = count($phoneConflicts); $tahesabOnlyResult = [ 'data' => array_values($tahesabOnlyList), 'total' => $tahesabOnlyCount, 'current_page' => 1, 'last_page' => 1, ]; } $payload = [ 'panel_total' => $panelTotal, 'panel_registered' => $panelRegistered, 'panel_only' => $panelOnly, 'tahesab_total' => $tahesabTotal, 'tahesab_only_count' => $tahesabOnlyCount, 'tahesab_with_phone_count' => $tahesabWithPhoneCount, 'tahesab_without_phone_count' => $tahesabWithoutPhoneCount, 'tahesab_importable_count' => $tahesabImportableCount, 'phone_match_count' => $phoneMatchCount, 'phone_conflict_count' => $phoneConflictCount, 'phone_matches' => $phoneMatches, 'phone_conflicts' => $phoneConflicts, 'panel_only_list' => $panelOnlyList, 'tahesab_only_list' => $tahesabOnlyResult, 'doc_sync' => app(TahesabDocSyncService::class)->snapshot(), ]; // برای تشخیص نام دقیق فیلدها: ?include_tahesab=1&raw=1 if ($request->boolean('include_tahesab') && $request->boolean('raw')) { $payload['tahesab_debug_sample'] = $debugSample ?? []; } return response()->json($payload); } /** شماره تماس را از هر کلید محتمل در رکورد مشتری ته‌حساب استخراج می‌کند */ private function extractTahesabPhone(array $item): string { $candidates = ['Mobile', 'CellPhone', 'Cell', 'Tel', 'Tell', 'Phone', 'MobileNo', 'Telephone', 'Mobile1']; foreach ($candidates as $key) { if (isset($item[$key]) && trim((string) $item[$key]) !== '') { return trim((string) $item[$key]); } } // جستجوی عمومی: هر کلیدی که شامل mobile/tel/phone/cell باشد و مقدار داشته باشد foreach ($item as $k => $v) { if ($v === null || trim((string) $v) === '') continue; $lk = strtolower((string) $k); if (str_contains($lk, 'mobile') || str_contains($lk, 'tel') || str_contains($lk, 'phone') || str_contains($lk, 'cell')) { return trim((string) $v); } } return ''; } /** * @param array $response * @return list> */ private function tahesabCustomerRows(array $response): array { if ($response === []) { return []; } if (isset($response[0]) && is_array($response[0])) { return array_values(array_filter($response, 'is_array')); } foreach (['Moshtari', 'ListMoshtari', 'data', 'Data'] as $key) { if (isset($response[$key]) && is_array($response[$key])) { return array_values(array_filter($response[$key], 'is_array')); } } return array_values(array_filter($response, 'is_array')); } /** @param array $item */ private function extractTahesabCode(array $item): mixed { foreach (['Code', 'code', 'MoshtariCode', 'moshtari_code'] as $key) { if (isset($item[$key]) && $item[$key] !== '' && $item[$key] !== null) { return $item[$key]; } } return null; } private function accountingCodeKey(mixed $code): ?string { if ($code === null || $code === '' || $code === false) { return null; } $s = trim((string) $code); if ($s === '' || $s === '0' || $s === '-') { return null; } if (is_numeric($s)) { $n = (int) $s; return $n > 0 ? (string) $n : null; } return $s; } /** * @return array{by_code: array, by_mobile: array} */ private function panelAccountingMaps(): array { $byCode = []; $byMobile = []; $rows = Customer::whereNull('deleted_at')->get(['id', 'name', 'mobile', 'moshtari_code', 'type']); foreach ($rows as $row) { $codeKey = $this->accountingCodeKey($row->moshtari_code); if ($codeKey !== null) { $byCode[$codeKey] = $row; } $mobile = Customer::normalizeMobile((string) ($row->mobile ?? '')); if ($mobile !== null) { $byMobile[$mobile] = $row; } } return ['by_code' => $byCode, 'by_mobile' => $byMobile]; } /** * مشتریان ته‌حساب که در پنل نیستند و شماره دارند — برای ورود تکی‌تکی سمت سرور. * * @return list */ public function collectImportableTahesabCustomers(): array { $tahesabCustomers = $this->fetchAllTahesabCustomers(); $maps = $this->panelAccountingMaps(); $out = []; foreach ($tahesabCustomers as $item) { if (! is_array($item)) { continue; } $code = $this->extractTahesabCode($item); $codeKey = $this->accountingCodeKey($code); $phone = $this->extractTahesabPhone($item); if (Customer::normalizeMobile($phone) === null) { continue; } if ($codeKey !== null && isset($maps['by_code'][$codeKey])) { continue; } $mobileKey = Customer::normalizeMobile($phone); if ($mobileKey && isset($maps['by_mobile'][$mobileKey])) { continue; } if ($codeKey === null) { continue; } $out[] = [ 'code' => (int) $code, 'name' => trim((string) ($item['Name'] ?? $item['name'] ?? '')), 'tel' => $phone, ]; } return $out; } /** لینک کد ته‌حساب روی مشتری پنل وقتی شماره یکسان است */ public function linkMatchingPhones(): \Illuminate\Http\JsonResponse { $tahesabCustomers = $this->fetchAllTahesabCustomers(); $maps = $this->panelAccountingMaps(); $linked = []; $conflicts = []; $toSync = []; foreach ($tahesabCustomers as $item) { if (! is_array($item)) { continue; } $code = $this->extractTahesabCode($item); $codeKey = $this->accountingCodeKey($code); if ($codeKey === null) { continue; } $phone = $this->extractTahesabPhone($item); $mobileKey = Customer::normalizeMobile($phone); if ($mobileKey === null || ! isset($maps['by_mobile'][$mobileKey])) { continue; } $panel = $maps['by_mobile'][$mobileKey]; $panelCodeKey = $this->accountingCodeKey($panel->moshtari_code); if (isset($maps['by_code'][$codeKey]) && (int) $maps['by_code'][$codeKey]->id !== (int) $panel->id) { $conflicts[] = [ 'code' => (int) $codeKey, 'name' => trim((string) ($item['Name'] ?? '')), 'tel' => $phone, 'panel_id' => (int) $panel->id, 'panel_name' => $panel->name, 'error' => 'این کد ته‌حساب روی مشتری دیگری در پنل است', ]; continue; } if ($panelCodeKey === $codeKey) { continue; } if ($panelCodeKey) { $conflicts[] = [ 'code' => (int) $codeKey, 'name' => trim((string) ($item['Name'] ?? '')), 'tel' => $phone, 'panel_id' => (int) $panel->id, 'panel_name' => $panel->name, 'panel_code' => $panelCodeKey, 'error' => 'مشتری پنل کد دیگری دارد — دستی تصمیم بگیرید', ]; continue; } $panel->moshtari_code = (int) $codeKey; $panel->save(); $maps['by_code'][$codeKey] = $panel; $toSync[] = $panel; $linked[] = [ 'code' => (int) $codeKey, 'name' => $panel->name, 'tel' => $phone, 'panel_id' => (int) $panel->id, ]; } if ($toSync !== []) { try { app(TahesabInboundWalletService::class)->syncAfterImport($toSync); } catch (\Throwable $e) { \Log::warning('Tahesab link-by-phone wallet sync failed', ['error' => $e->getMessage()]); } } return response()->json([ 'ok' => true, 'linked' => count($linked), 'conflicts' => count($conflicts), 'linked_rows' => $linked, 'conflict_rows' => $conflicts, 'message' => count($linked) ? (count($linked).' کد روی مشتری پنل با شماره یکسان نشست') : (count($conflicts) ? 'کد یکسانی برای لینک خودکار نبود' : 'شماره یکسانی برای لینک پیدا نشد'), ]); } public function previewCustomerWallet(Request $request, $customerId): \Illuminate\Http\JsonResponse { if (! app(ConfigService::class)->isTahesabAccounting()) { return response()->json(['ok' => false, 'message' => 'حسابداری ته‌حساب فعال نیست'], 422); } $customer = Customer::findOrFail($customerId); $code = (int) $request->input('code', $customer->moshtari_code); $result = app(TahesabInboundWalletService::class)->previewForCustomer($customer, $code); $needsDecision = (bool) ($result['needs_decision'] ?? false); return response()->json([ 'ok' => (bool) ($result['ok'] ?? false), 'needs_decision' => $needsDecision, 'message' => $result['message'] ?? '', 'result' => $result, ]); } public function mandeByCodes(Request $request): \Illuminate\Http\JsonResponse { $codes = $request->input('codes', []); if (! is_array($codes) || $codes === []) { return response()->json(['ok' => true, 'mandes' => []]); } if (count($codes) > 50) { return response()->json(['ok' => false, 'message' => 'حداکثر ۵۰ کد در هر درخواست'], 422); } $mandes = app(TahesabInboundWalletService::class)->fetchMandeByCodes($codes); $out = []; foreach ($mandes as $code => $row) { $out[(string) $code] = [ 'gold' => $row['gold'] ?? 0, 'cash' => $row['cash'] ?? 0, ]; } return response()->json(['ok' => true, 'mandes' => $out]); } public function syncPanelToTahesab(): \Illuminate\Http\JsonResponse { $customers = Customer::whereNull('deleted_at') ->whereNull('moshtari_code') ->select('id', 'name', 'mobile') ->get(); $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 importFromTahesab(Request $request): \Illuminate\Http\JsonResponse { $validated = $request->validate([ 'customers' => 'required|array', 'customers.*.code' => 'required|integer', 'customers.*.name' => 'required|string', 'customers.*.tel' => 'nullable|string', ]); $success = 0; $failed = 0; $errors = []; /** @var list $importedForWallet */ $importedForWallet = []; foreach ($validated['customers'] as $item) { $mobile = Customer::normalizeMobile($item['tel'] ?? '') ?? ''; $fullName = trim($item['name']); $code = (int) $item['code']; // بدون موبایل ردیف placeholder نساز if ($mobile === '') { $failed++; $errors[] = [ 'name' => $item['name'], 'code' => $item['code'], 'error' => 'شماره موبایل خالی است — وارد نشد', ]; continue; } $existing = Customer::whereNull('deleted_at')->where('moshtari_code', $code)->first(); if ($existing) { $changed = false; if (($existing->name === null || $existing->name === '') && $fullName !== '') { $existing->fname = $fullName; $existing->lname = (string) ($existing->lname ?? ''); $existing->name = $fullName; $changed = true; } if ($mobile !== '' && ($existing->mobile === null || $existing->mobile === '')) { if (Customer::isMobileTaken($mobile, (int) $existing->id)) { $failed++; $errors[] = [ 'name' => $item['name'], 'code' => $item['code'], 'error' => 'شماره موبایل تکراری است', ]; continue; } $existing->mobile = $mobile; if (empty($existing->password)) { $existing->password = $mobile; } $changed = true; } if ($changed) { $existing->save(); } $importedForWallet[] = $existing; $success++; continue; } if ($mobile !== '') { $byMobile = Customer::findActiveByMobile($mobile); if ($byMobile) { if (empty($byMobile->moshtari_code)) { $byMobile->moshtari_code = $code; if ($fullName !== '') { $byMobile->fname = $fullName; $byMobile->lname = ''; $byMobile->name = $fullName; } if (empty($byMobile->password)) { $byMobile->password = $mobile; } $byMobile->save(); $importedForWallet[] = $byMobile; $success++; continue; } $failed++; $errors[] = [ 'name' => $item['name'], 'code' => $item['code'], 'error' => 'شماره موبایل تکراری است', ]; continue; } } // کد معرف یکتا $characters = '0123456789abcdefghijklmnopqrstuvwxyz'; do { $referralId = ''; for ($i = 0; $i < 4; $i++) { $referralId .= $characters[random_int(0, strlen($characters) - 1)]; } } while (Customer::where('referral_id', $referralId)->exists()); try { // توجه: مدل Customer یک mutator روی name دارد که name را از fname+lname می‌سازد // و مقدار مستقیم name را نادیده می‌گیرد. پس باید fname را قبل از name ست کنیم. $customer = new Customer(); $customer->fname = $fullName; $customer->lname = ''; $customer->name = $fullName; // mutator: name = fname (+ lname) $customer->mobile = $mobile; $customer->active = true; $customer->moshtari_code = $item['code']; $customer->referral_id = $referralId; $customer->type = 2; // رمز عبور = شماره موبایل (فقط وقتی موبایل موجود است) if ($mobile !== '') { $customer->password = $mobile; } $customer->save(); $importedForWallet[] = $customer; $success++; } catch (\Throwable $e) { $failed++; $msg = $e->getMessage(); if ($e instanceof UnprocessableEntityHttpException) { $msg = $e->getMessage(); } elseif ($e instanceof \Illuminate\Database\QueryException && (int) ($e->errorInfo[1] ?? 0) === 1062) { $msg = 'شماره موبایل تکراری است'; } $errors[] = ['name' => $item['name'], 'code' => $item['code'], 'error' => $msg]; } } // مانده طلا/ریال ته‌حساب را روی کیف مشتریان واردشده بنشان if ($importedForWallet !== []) { try { app(TahesabInboundWalletService::class)->syncAfterImport($importedForWallet); } catch (\Throwable $e) { \Log::warning('Tahesab import wallet sync batch failed', [ 'count' => count($importedForWallet), 'error' => $e->getMessage(), ]); } } return response()->json([ 'ok' => true, 'success' => $success, 'failed' => $failed, 'errors' => $errors, ]); } /** * حذف مشتریانِ خرابِ واردشده از ته‌حساب (نام خالی) برای پاک‌سازی import ناقص. * با dry_run=1 فقط تعداد و نمونه را نشان می‌دهد و چیزی حذف نمی‌کند. */ public function rollbackImport(Request $request): \Illuminate\Http\JsonResponse { // مشتریانِ ساخته‌شده توسط import که name آن‌ها خالی مانده (به‌خاطر mutator) $baseQuery = fn () => Customer::whereNotNull('moshtari_code') ->where(function ($q) { $q->whereNull('name')->orWhere('name', ''); }); $count = $baseQuery()->count(); $sample = $baseQuery() ->select('id', 'fname', 'name', 'mobile', 'moshtari_code', '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) { $tahesab = Tahesab::with('customer')->findOrFail($id); $result = $this->sync()->retry($tahesab); return response()->json($result, ($result['ok'] ?? false) ? 200 : 422); } public function pendingSummary(Request $request) { $docs = app(TahesabDocSyncService::class); $docs->tickIfStale(); $customerId = $request->filled('customer_id') ? (int) $request->input('customer_id') : null; return response()->json($this->sync()->pendingSummary($customerId)); } public function startDocSync() { $result = app(TahesabDocSyncService::class)->start(); return response()->json($result, ($result['ok'] ?? false) ? 200 : 422); } public function stopDocSync() { $result = app(TahesabDocSyncService::class)->stop(); return response()->json($result); } public function resetLocalDocSync() { $result = $this->sync()->resetLocalSyncedDocs(); return response()->json($result, ($result['ok'] ?? false) ? 200 : 422); } public function startCustomerOutbound() { $result = app(TahesabDocSyncService::class)->startCustomerOutbound(); return response()->json($result, ($result['ok'] ?? false) ? 200 : 422); } public function startCustomerInbound() { $result = app(TahesabDocSyncService::class)->startCustomerInbound(); return response()->json($result, ($result['ok'] ?? false) ? 200 : 422); } public function pendingDocs(Request $request) { $customerId = $request->filled('customer_id') ? (int) $request->input('customer_id') : null; $limit = max(1, min(500, (int) $request->input('limit', 200))); return response()->json([ 'rows' => $this->sync()->pendingDocs($customerId, $limit), 'summary' => $this->sync()->pendingSummary($customerId), ]); } public function syncOne(Request $request) { $type = (string) $request->input('type', ''); $id = (int) $request->input('id', 0); if ($id <= 0 || ! in_array($type, ['row', 'withdraw', 'order'], true)) { return response()->json(['ok' => false, 'message' => 'پارامتر نامعتبر'], 422); } $result = $this->sync()->syncOne($type, $id); return response()->json($result, ($result['ok'] ?? false) ? 200 : 422); } public function syncAllPending(Request $request) { $limit = max(1, min(200, (int) $request->input('limit', 80))); $customerId = $request->filled('customer_id') ? (int) $request->input('customer_id') : null; $result = $this->sync()->syncAllPending($limit, $customerId); 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); } /** * همگام‌سازی مانده یک مشتری با ته‌حساب. * body: overwrite=1 | keep=1 | push_to_tahesab=1 | ask_if_conflict=1 */ public function syncCustomerWallet(Request $request, $customerId) { if (! app(ConfigService::class)->isTahesabAccounting()) { return response()->json([ 'ok' => false, 'message' => 'حسابداری ته‌حساب فعال نیست', ], 422); } $customer = Customer::findOrFail($customerId); if ((int) ($customer->moshtari_code ?? 0) <= 0) { return response()->json([ 'ok' => false, 'message' => 'این مشتری کد ته‌حساب ندارد', ], 422); } $walletService = app(TahesabInboundWalletService::class); if ($request->boolean('push_to_tahesab')) { $result = $walletService->pushWalletToTahesab($customer); $applied = (bool) ($result['applied'] ?? false); return response()->json([ 'ok' => $applied, 'needs_decision' => false, 'message' => $result['message'] ?? '', 'result' => $result, ], $applied ? 200 : 422); } $overwrite = $request->boolean('overwrite'); $keep = $request->boolean('keep'); $alwaysAsk = $request->boolean('always_ask'); $askIfConflict = $request->boolean('ask_if_conflict', ! $overwrite && ! $keep && ! $alwaysAsk); $result = $walletService->syncWalletFromTahesab( $customer, null, [ 'overwrite' => $overwrite, 'keep' => $keep, 'ask_if_conflict' => $askIfConflict, 'always_ask' => $alwaysAsk, ] ); $needsDecision = (bool) ($result['needs_decision'] ?? false); $applied = (bool) ($result['applied'] ?? false); $skipped = (bool) ($result['skipped'] ?? false); $same = ! $applied && ! $needsDecision && ! $skipped && str_contains((string) ($result['message'] ?? ''), 'یکسان'); return response()->json([ 'ok' => $applied || $skipped || $same, 'needs_decision' => $needsDecision, 'message' => $result['message'] ?? '', 'result' => $result, ]); } /** * همگام‌سازی مانده همه مشتریان دارای کد ته‌حساب — تکه‌تکه. * body: overwrite=1 برای اعمال اجباری اختلاف‌ها؛ بدون آن فقط کیف خالی یا یکسان پردازش می‌شود و تداخل‌ها در needs_decision می‌آیند. */ public function syncAllWallets(Request $request) { if (! app(ConfigService::class)->isTahesabAccounting()) { 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)); $overwrite = $request->boolean('overwrite'); $base = Customer::query() ->whereNull('deleted_at') ->whereNotNull('moshtari_code') ->where('moshtari_code', '!=', '') ->where('moshtari_code', '!=', '0'); $total = (clone $base)->count(); $customers = (clone $base) ->orderBy('id') ->offset($offset) ->limit($limit) ->get(['id', 'name', 'mobile', 'moshtari_code', 'type']); $walletService = app(TahesabInboundWalletService::class); $codes = $customers->pluck('moshtari_code')->all(); $mandes = $walletService->fetchMandeByCodes($codes); $success = 0; $changed = 0; $needsDecision = 0; $failed = 0; $errors = []; $decisions = []; foreach ($customers as $customer) { try { $code = (string) ((int) $customer->moshtari_code); $result = $walletService->syncWalletFromTahesab( $customer, $mandes[$code] ?? null, [ 'overwrite' => $overwrite, 'ask_if_conflict' => ! $overwrite, ] ); if ($result['needs_decision'] ?? false) { $needsDecision++; $decisions[] = [ 'id' => $customer->id, 'name' => $customer->name, 'mobile' => $customer->mobile, 'app_gold' => $result['app_gold'] ?? null, 'app_cash' => $result['app_cash'] ?? null, 'tahesab_gold' => $result['tahesab_gold'] ?? null, 'tahesab_cash' => $result['tahesab_cash'] ?? null, 'message' => $result['message'] ?? '', ]; continue; } $success++; if ($result['applied'] ?? false) { $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, 'needs_decision' => $needsDecision, 'failed' => $failed, 'errors' => $errors, 'decisions' => $decisions, ]); } /** * لیست مشتریان لینک‌شده با مقایسهٔ کیف و ماندهٔ ته‌حساب. */ public function walletLinkedList(Request $request): \Illuminate\Http\JsonResponse { if (! app(ConfigService::class)->isTahesabAccounting()) { return response()->json([ 'ok' => false, 'message' => 'حسابداری ته‌حساب فعال نیست', ], 422); } $search = trim((string) $request->input('search', '')); $page = max(1, (int) $request->input('page', 1)); $perPage = max(1, min(50, (int) $request->input('per_page', 20))); $base = Customer::query() ->whereNull('deleted_at') ->whereNotNull('moshtari_code') ->where('moshtari_code', '!=', '') ->where('moshtari_code', '!=', '0'); if ($search !== '') { $base->where(function ($q) use ($search) { $q->where('name', 'like', '%'.$search.'%') ->orWhere('mobile', 'like', '%'.$search.'%') ->orWhere('moshtari_code', 'like', '%'.$search.'%'); }); } $total = (clone $base)->count(); $customers = (clone $base) ->orderBy('id') ->offset(($page - 1) * $perPage) ->limit($perPage) ->get(['id', 'name', 'mobile', 'moshtari_code', 'type']); $rows = app(TahesabInboundWalletService::class)->compareCustomers($customers); return response()->json([ 'ok' => true, 'total' => $total, 'page' => $page, 'per_page' => $perPage, 'last_page' => max(1, (int) ceil($total / max(1, $perPage))), 'rows' => $rows, ]); } private function tahesabApiUrl(): string { $host = trim((string) $this->Config('tahesab_host')); $port = trim((string) $this->Config('tahesab_port')); if ($host !== '' && $port !== '') { $host = rtrim(preg_replace('#^https?://#i', '', $host), '/'); return 'httpS://' . $host . ':' . $port . '/'; } $legacy = trim((string) env('TAHESAB', '')); return $legacy !== '' ? $legacy : ''; } public function storeTrade($data, ?Tahesab $existing = null) { // اگر payload خالی است اما Tahesab موجود و order_id دارد، از Order بازسازی کن if (empty($data['customer_id']) && $existing && $existing->order_id) { $order = \App\Models\Order::find($existing->order_id); if ($order) { $productPayload = ['metal_id' => $existing->type === 'طلا' ? 1 : 2]; if ($order->product_id) { $productPayload['id'] = (int) $order->product_id; } $data = array_merge($data, [ 'customer_id' => $existing->customer_id, 'amount' => $existing->amount, 'direction' => $order->direction, 'price' => $order->price, 'total_price' => $order->total_price, 'pay_at' => $existing->date, 'product' => $productPayload, 'product_id' => $order->product_id ? (int) $order->product_id : null, 'order' => ['id' => $existing->order_id, 'product_id' => $order->product_id], ]); } } if (!isset($data['customer_id']) || !isset($data['amount'])) { return request()->expectsJson() ? response()->json(['error' => 'Data missing: customer_id or amount'], 422) : false; } $mapper = app(AccountingProductDocMapper::class); $product = $mapper->resolveProductFromPayload($data); if ($product && $mapper->shouldSkip($product)) { return false; } $customer = Customer::where('id', $data['customer_id'])->first(); $accountOverride = trim((string) ($data['accounting_account_code'] ?? '')); $isColleagueLeg = ! empty($data['colleague_leg']) && $accountOverride !== ''; if (! $isColleagueLeg) { $ensured = $this->ensureCustomerHasTahesabCode($customer); if (! ($ensured['ok'] ?? false)) { return $ensured['message'] ?? 'مشتری کد ته‌حساب ندارد'; } $customer = $ensured['customer']; } if (! $customer) { return 'مشتری سند پیدا نشد'; } $hesabCode = $isColleagueLeg ? (int) $accountOverride : (int) ($customer->moshtari_code ?? 0); if ($hesabCode <= 0) { return 'مشتری کد ته‌حساب ندارد'; } $kind = $mapper->resolveKind($product); $rawAmount = (float) $data['amount']; $count = $mapper->accountingQty($product, $rawAmount, $data); $vazn = $mapper->accountingWeight($product, $rawAmount, $data); $jens = $mapper->jensFelez($product) ?? 0; $title = $mapper->productTitle($product); $isBuy = (int) ($data['direction'] ?? 0) === 1; $price = (float) ($data['price'] ?? 0); $mablaghKol = $isBuy ? (int) round($vazn * $price) : (int) ($data['total_price'] ?? round($vazn * $price)); // سند تعدادی: مظنه = فی هر عدد (نه لزوماً قیمت هر گرم) $mazaneh = (int) $price; if ($mapper->isCountDoc($product) && $count > 0) { if (!empty($data['total_price'])) { $mazaneh = (int) round(((float) $data['total_price']) / $count); } elseif ($price > 0 && abs($vazn - $count) > 0.0001) { // price احتمالاً هر گرم است $unitW = $vazn / $count; $mazaneh = (int) round($price * $unitW); } if ($mazaneh <= 0 && $mablaghKol > 0) { $mazaneh = (int) round($mablaghKol / $count); } } $date = Verta::instance(Carbon::parse($data['pay_at'] ?? now()))->format('Y-m-d'); [$year, $month, $day] = explode('-', $date); $action = $isBuy ? 'خرید' : 'فروش'; $kindFa = $mapper->kindLabel($kind); $sharh = trim((string) ($data['sharh_override'] ?? '')); if ($sharh === '') { $sharh = trim($title !== '' ? "{$action} {$title}" : "{$action} {$kindFa}"); } $sharh = $this->sharhWithCustomer($sharh, $customer); $orderId = is_object($data['order'] ?? null) ? ($data['order']->id ?? null) : ($data['order']['id'] ?? null); $data['handler'] = 'storeTrade'; if ($existing && empty($existing->data)) { $existing->update(['data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)]); } $tahesab = $existing ?? Tahesab::create([ 'customer_id' => $customer->id, 'desc' => $sharh, 'amount' => $mapper->isCountDoc($product) ? $count : $vazn, 'order_id' => $orderId, 'date' => $data['pay_at'] ?? Carbon::now(), 'type' => $mapper->syncTypeLabel($product), 'created_at' => Carbon::now(), 'data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), ]); if ($mapper->isCountDoc($product) && $title === '' && ! $isColleagueLeg) { $this->sync()->markFailed($tahesab, null, 'نام محصول برای سند تعدادی ته‌حساب خالی است'); return false; } $body = $this->buildBuySaleBody( $kind, $hesabCode, (int) $year, (int) $month, (int) $day, $vazn, $count, $title, $isBuy ? 1 : 0, $mazaneh, $mablaghKol, $sharh, $jens, (string) Verta::instance($data['pay_at'] ?? now()) ); try { $response = $this->postTahesab($body); } catch (\Exception $e) { $this->sync()->markFailed($tahesab, $e->getMessage()); return $e->getMessage(); } if ($this->sync()->responseOk($response)) { $this->sync()->markSuccess($tahesab, $response, $data); return $data; } $this->sync()->markFailed($tahesab, $response, 'ثبت سند معامله در ته‌حساب ناموفق بود'); return false; } // directon: 0 = daryaft az moshtari // directon: 1 = pardakht be moshtari public function wallet($data, ?Tahesab $existing = null) { if (empty($data['customer_id']) && $existing) { $data = array_merge($data, [ 'customer_id' => $existing->customer_id, 'value' => $existing->amount, 'direction' => str_contains((string) $existing->desc, 'برداشت') ? 1 : 0, 'withdraw_id' => $existing->withdraw_id, ]); } if (!isset($data['customer_id']) || !isset($data['value'])) { return request()->expectsJson() ? response()->json(['error' => 'Data missing: customer_id or value'], 422) : false; } // $customer = auth()->user(); // if (!$customer) { // throw new UnprocessableEntityHttpException("Auth needed"); // } $customer = Customer::where('id', $data['customer_id'])->first(); $ensured = $this->ensureCustomerHasTahesabCode($customer); if (! ($ensured['ok'] ?? false)) { return $ensured['message'] ?? 'مشتری کد ته‌حساب ندارد'; } $customer = $ensured['customer']; $data['handler'] = 'wallet'; $isDeposit = (int) ($data['direction'] ?? 0) === 0; $sharh = $this->sharhWithCustomer( self::accountingWalletDesc($isDeposit, $data['note'] ?? null), $customer ); $tahesab = $existing ?? Tahesab::create([ "customer_id" => $customer->id, "desc" => $sharh, "amount" => $data['value'], "type" => "ريال", "date" => Carbon::now(), "created_at" => Carbon::now(), "withdraw_id" => $data['withdraw_id'] ?? null, "data" => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), ]); if ($existing && empty($existing->data)) { $existing->update(['data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)]); } $dateSource = ($existing !== null && !empty($existing->date)) ? Carbon::parse($existing->date) : Carbon::now(); $date = Verta::instance($dateSource)->format('Y-m-d'); [$year, $month, $day] = explode('-', $date); try { $response = Http::withoutVerifying()->timeout(10)->withHeaders([ 'DBName' => $this->Config('tahesab_db'), 'Content-Type' => 'application/json', 'Authorization' => "Bearer " . $this->Config('tahesab_token'), ])->post($this->tahesabApiUrl(), [ "DoNewSanadVKHBank" => [ 1,//Sabte_Kol_Or_Movaghat_1_0 $customer->moshtari_code,//Moshtari_Code -1,//Factor_Number 1,//Radif_Number (int)$year,//Shamsi_Year (int)$month,//Shamsi_Month (int)$day,//Shamsi_Day (int)$data['direction'],//IsDayaftAzMoshtariOrPardakhtBeMoshtari_0_1 (int)$data['value'],//Mablagh $sharh,//Sharh (string)$this->Config('tahesab_bank_account'), //Name_Bank "$tahesab->id",//Code_Rahgiri -1,//Factor_Code -1, //MultiRadif ] ])->json(); \Log::info('Tahesab wallet response', [ 'data' => $data, 'response' => $response ?? null, ]); } catch (\Exception $e) { $this->sync()->markFailed($tahesab, $e->getMessage()); return $e->getMessage(); } if ($this->sync()->responseOk($response)) { $this->sync()->markSuccess($tahesab, $response, $data); return $data; } $this->sync()->markFailed($tahesab, $response, 'ثبت سند ریالی در ته‌حساب ناموفق بود'); return false; // if($data['bank'] == true) { // try { // $response = Http::withoutVerifying()->timeout(10)->withHeaders([ // 'DBName' => $this->ConfigService('tahesab_db'), // 'Content-Type' => 'application/json', // 'Authorization' => "Bearer " . $this->ConfigService('tahesab_token'), // ])->post($this->tahesabApiUrl(), [ // "DoNewSanadVKHBank" => [ // 1,//Sabte_Kol_Or_Movaghat_1_0 // $customer->moshtari_code,//Moshtari_Code // -1,//Factor_Number // 1,//Radif_Number // (int)$year,//Shamsi_Year // (int)$month,//Shamsi_Month // (int)$day,//Shamsi_Day // (int)$data['direction'],//IsDayaftAzMoshtariOrPardakhtBeMoshtari_0_1 // (int)$data['value'],//Mablagh // $data['direction'] == 0 ? "شارژ حساب" : "برداشت از حساب",//Sharh // (string)$this->ConfigService('tahesab_bank_account'), //Name_Bank // "$tahesab->id",//Code_Rahgiri // -1,//Factor_Code // -1, // //MultiRadif // ] // ])->json(); // } catch (\Exception $e) { // return $e->getMessage(); // } // if ($response['OK']) { // Tahesab::where('id', $tahesab->id)->update([ // "sh_factor" => $response['Sh_factor'], // "tahesab_id" => $response['OK'], // "tah_id" => $response['ID'], // "factor_number" => $response['Factor_Number'], // "data" => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), // "res" => json_encode($response, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), // ]); // // return $data; // } // }else{ // try { // $response = Http::withoutVerifying()->timeout(10)->withHeaders([ // 'DBName' => $this->ConfigService('tahesab_db'), // 'Content-Type' => 'application/json', // 'Authorization' => "Bearer " . $this->ConfigService('tahesab_token'), // ])->post($this->tahesabApiUrl(), [ // "DoNewSanadTalabBedehi" => [ // 1,//Sabte_Kol_Or_Movaghat_1_0, // $customer->moshtari_code,//Moshtari_Code // -1,//Factor_Number // 1,//Radif_Number // (int)$year,//Shamsi_Year // (int)$month,//Shamsi_Month // (int)$day,//Shamsi_Day // $data['value'] < 0 ? 0 : 1,//IsBedehiOrTalab_0_1, // (int)abs($data['value']),//Mablagh // $data['value'] < 0 ? "بدهکار" : "بستانکار",//Sharh // $data['note'],//"Baabat", // 0,//Vazn, // -1,//Factor_Code // -1,//HavalehBe_Mcode, // null,//MultiRadif // 0,//Jens_Felez, // "ArzName" // ] // ])->json(); // } catch (\Exception $e) { // return $e->getMessage(); // } // if ($response['OK']) { // Tahesab::where('id', $tahesab->id)->update([ // "sh_factor" => $response['Sh_factor'], // "tahesab_id" => $response['OK'], // "data" => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), // "res" => json_encode($response, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), // ]); // // return $data; // } // } } /** * پاداش معرفی — همیشه روی حساب معرف (نه کاربر تازه‌ثبت‌نام‌شده). */ public function referrerGoldReward(Customer $invitedCustomer, float $gold, string $desc = 'پاداش معرفی') { $referrer = $invitedCustomer->referrer(); if (!$referrer) { return false; } return $this->goldReward([ 'customer_id' => $referrer->id, 'value' => $gold, 'desc' => $desc, ]); } /** * پاداش/هدیه طلایی — سند بدهی وزنی (مثل ثبت دستی «بدهی / بابت معرفی» در ته‌حساب). */ public function goldReward($data, ?Tahesab $existing = null) { if (empty($data['customer_id']) && $existing) { $data = array_merge($data, [ 'customer_id' => $existing->customer_id, 'value' => $existing->amount, 'desc' => $existing->desc, ]); } if (!isset($data['customer_id'], $data['value'])) { return request()->expectsJson() ? response()->json(['error' => 'Data missing: customer_id or value'], 422) : false; } $customer = Customer::where('id', $data['customer_id'])->first(); $ensured = $this->ensureCustomerHasTahesabCode($customer); if (! ($ensured['ok'] ?? false)) { return $ensured['message'] ?? 'مشتری کد ته‌حساب ندارد'; } $customer = $ensured['customer']; $amount = abs((float) $data['value']); $desc = $this->sharhWithCustomer( ! empty($data['desc']) ? (string) $data['desc'] : 'پاداش', $customer ); $data['handler'] = 'goldReward'; $tahesab = $existing ?? Tahesab::create([ 'customer_id' => $customer->id, 'desc' => $desc, 'amount' => $amount, 'date' => Carbon::now(), 'type' => 'طلا', 'created_at' => Carbon::now(), 'data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), ]); if ($existing && empty($existing->data)) { $existing->update(['data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)]); } $dateSource = ($existing !== null && !empty($existing->date)) ? Carbon::parse($existing->date) : Carbon::now(); $date = Verta::instance($dateSource)->format('Y-m-d'); [$year, $month, $day] = explode('-', $date); try { $response = Http::withoutVerifying()->timeout(10)->withHeaders([ 'DBName' => $this->Config('tahesab_db'), 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $this->Config('tahesab_token'), ])->post($this->tahesabApiUrl(), [ 'DoNewSanadTalabBedehi' => [ 1, // Sabte_Kol (int) $customer->moshtari_code, -1, // Factor_Number 1, // Radif_Number (int) $year, (int) $month, (int) $day, 0, // IsBedehiOrTalab — بدهی (فروشگاه به مشتری طلا بدهکار است) 0, // MablaghKol — بدون مبلغ ریالی $desc, $desc, // Baabat (float) $amount, -1, // Factor_Code -1, // HavalehBe_Mcode -1, // MultiRadif 0, // Jens_Felez — طلا 'IRR', ], ])->json(); } catch (\Exception $e) { $this->sync()->markFailed($tahesab, $e->getMessage()); return $e->getMessage(); } if ($this->sync()->responseOk($response)) { $this->sync()->markSuccess($tahesab, $response, $data); return $data; } $this->sync()->markFailed($tahesab, $response, 'ثبت پاداش طلایی در ته‌حساب ناموفق بود'); return false; } // direction: 1 = واریز طلا (ورود)، 0 = برداشت طلا (خروج) — VKH نه BuySale public function walletGold($data, ?Tahesab $existing = null) { if (empty($data['customer_id']) && $existing) { $data = array_merge($data, [ 'customer_id' => $existing->customer_id, 'value' => $existing->amount, 'direction' => str_contains((string) $existing->desc, 'برداشت') ? 0 : 1, 'withdraw_id' => $existing->withdraw_id, ]); $payload = json_decode((string) ($existing->data ?? ''), true); if (is_array($payload)) { if (!empty($payload['product_id'])) { $data['product_id'] = (int) $payload['product_id']; } if (!empty($payload['product']) && is_array($payload['product'])) { $data['product'] = $payload['product']; } } } if (!isset($data['customer_id'], $data['value'])) { return request()->expectsJson() ? response()->json(['error' => 'Data missing: customer_id or value'], 422) : false; } $mapper = app(AccountingProductDocMapper::class); $product = $mapper->resolveProductFromPayload($data); if ($product && $mapper->shouldSkip($product)) { return false; } $customer = Customer::where('id', $data['customer_id'])->first(); $ensured = $this->ensureCustomerHasTahesabCode($customer); if (! ($ensured['ok'] ?? false)) { return $ensured['message'] ?? 'مشتری کد ته‌حساب ندارد'; } $customer = $ensured['customer']; $kind = $mapper->resolveKind($product); $rawValue = (float) $data['value']; $count = $mapper->accountingQty($product, $rawValue, $data); $vazn = $mapper->accountingWeight($product, $rawValue, $data); $jens = $mapper->jensFelez($product) ?? 0; $title = $mapper->productTitle($product); $isDeposit = (int) ($data['direction'] ?? 1) === 1; $isVorood = $isDeposit ? 1 : 0; $sharh = $this->sharhWithCustomer( self::accountingWalletDesc( $isDeposit, ! empty($data['note']) ? (string) $data['note'] : (! empty($data['desc']) ? (string) $data['desc'] : null) ), $customer ); $data['note'] = $sharh; $data['handler'] = 'walletGold'; $tahesab = $existing ?? Tahesab::create([ 'customer_id' => $customer->id, 'desc' => $sharh, 'amount' => $mapper->isCountDoc($product) ? $count : $vazn, 'date' => Carbon::now(), 'type' => $mapper->syncTypeLabel($product), 'created_at' => Carbon::now(), 'withdraw_id' => $data['withdraw_id'] ?? null, 'data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), ]); if ($existing && empty($existing->data)) { $existing->update(['data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)]); } if ($mapper->isCountDoc($product) && $title === '') { $this->sync()->markFailed($tahesab, null, 'نام محصول برای سند تعدادی ته‌حساب خالی است'); return false; } $dateSource = ($existing !== null && !empty($existing->date)) ? Carbon::parse($existing->date) : Carbon::now(); $date = Verta::instance($dateSource)->format('Y-m-d'); [$year, $month, $day] = explode('-', $date); $body = $this->buildVkhBody( $kind, (int) $customer->moshtari_code, (int) $year, (int) $month, (int) $day, $vazn, $count, $title, $isVorood, $sharh, $jens ); try { $response = $this->postTahesab($body); } catch (\Exception $e) { $this->sync()->markFailed($tahesab, $e->getMessage()); return $e->getMessage(); } if ($this->sync()->responseOk($response)) { $this->sync()->markSuccess($tahesab, $response, $data); return $data; } $this->sync()->markFailed($tahesab, $response, 'ثبت سند طلایی کیف پول در ته‌حساب ناموفق بود'); return false; } private function postTahesab(array $body): array { $response = Http::withoutVerifying()->timeout(10)->withHeaders([ 'DBName' => $this->Config('tahesab_db'), 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $this->Config('tahesab_token'), ])->post($this->tahesabApiUrl(), $body)->json(); return is_array($response) ? $response : []; } /** * سند خرید/فروش — معامله. */ private function buildBuySaleBody( string $kind, int $moshtariCode, int $year, int $month, int $day, float $vazn, float $count, string $name, int $buyOrSale, int $mazaneh, int $mablaghKol, string $sharh, int $jens, string $zamanTasvie ): array { $ayar = $jens === 0 ? 750 : ''; if ($kind === AccountingProductDocMapper::KIND_SEKEH) { return [ 'DoNewSanadBuySaleSEKEH' => [ 1, $moshtariCode, -1, 1, $year, $month, $day, (float) $vazn, $ayar, (float) $count, $name, $buyOrSale, $mazaneh, $mablaghKol, $sharh, -1, -1, -1, $jens, '', ], ]; } if ($kind === AccountingProductDocMapper::KIND_SHEMSH) { return [ 'DoNewSanadBuySaleShemsh' => [ 1, $moshtariCode, -1, 1, $year, $month, $day, (float) $vazn, $ayar, (float) $count, $name, $buyOrSale, $mazaneh, $mablaghKol, $sharh, -1, -1, -1, $jens, '', ], ]; } if ($kind === AccountingProductDocMapper::KIND_KAR) { return [ 'DoNewSanadBuySaleKar' => [ 1, $moshtariCode, -1, 1, $year, $month, $day, (float) $vazn, $ayar ?: 750, (float) $count, $name, $buyOrSale, $mazaneh, 1, $mablaghKol, 0, 0, 0, 0, 0, 0, 0, '', 0, $sharh, -1, -1, -1, $jens, '', ], ]; } // وزن / آبشده — همان payload قبلی با Jens_Felez و Sharh دقیق‌تر return [ 'DoNewSanadBuySaleGOLD' => [ 1, $moshtariCode, -1, 1, $year, $month, $day, (float) $vazn, $ayar, 0, '', $buyOrSale, $mazaneh, 1, 1, $mablaghKol, $sharh, -1, -1, -1, $jens, $zamanTasvie, ], ]; } /** * سند ورود/خروج — شارژ، دشارژ، برداشت فیزیکی. */ private function buildVkhBody( string $kind, int $moshtariCode, int $year, int $month, int $day, float $vazn, float $count, string $name, int $isVorood, string $sharh, int $jens ): array { $ayar = $jens === 0 ? 750 : ''; if ($kind === AccountingProductDocMapper::KIND_SEKEH) { return [ 'DoNewSanadVKHSEKEH' => [ 1, $moshtariCode, -1, 1, $year, $month, $day, (float) $vazn, $ayar, (float) $count, $name, $isVorood, $sharh, -1, -1, -1, $jens, ], ]; } if ($kind === AccountingProductDocMapper::KIND_SHEMSH) { return [ 'DoNewSanadVKHShemsh' => [ 1, $moshtariCode, -1, 1, $year, $month, $day, (float) $vazn, $ayar, (float) $count, $name, $isVorood, $sharh, -1, -1, -1, $jens, ], ]; } if ($kind === AccountingProductDocMapper::KIND_KAR) { return [ 'DoNewSanadVKHKar' => [ 1, $moshtariCode, -1, 1, $year, $month, $day, (float) $vazn, $ayar ?: 750, (float) $count, $name, $isVorood, 0, 0, 0, 0, 0, 0, '', 0, 0, $sharh, -1, -1, -1, $jens, ], ]; } return [ 'DoNewSanadVKHGOLD' => [ 1, $moshtariCode, -1, 1, $year, $month, $day, (float) $vazn, $ayar ?: 750, '', '', $isVorood, 1, $sharh, -1, -1, -1, $jens, 0, ], ]; } private function ensureCustomerHasTahesabCode(?Customer $customer): array { if (! $customer) { return ['ok' => false, 'message' => 'مشتری سند پیدا نشد']; } if ((int) ($customer->moshtari_code ?? 0) > 0) { return ['ok' => true, 'customer' => $customer]; } $reg = $this->createCustomerInTahesab($customer->id); $customer->refresh(); if ((int) ($customer->moshtari_code ?? 0) > 0) { return ['ok' => true, 'customer' => $customer]; } $message = is_string($reg) && $reg !== '' ? $reg : 'ثبت مشتری در ته‌حساب ناموفق بود'; return ['ok' => false, 'message' => $message]; } private function sharhWithCustomer(string $sharh, Customer $customer): string { $name = trim((string) ($customer->name ?? '')); if ($name === '' || str_contains($sharh, $name)) { return $sharh; } return trim($sharh.' — '.$name); } public function createCustomerInTahesab($customer_id) { $customer = Customer::findOrFail($customer_id); $response = null; if ((int) ($customer->moshtari_code ?? 0) <= 0) { $lockKey = 'tahesab:register-customer:'.$customer->id; if (! Cache::add($lockKey, 1, 30)) { $customer->refresh(); if ((int) ($customer->moshtari_code ?? 0) > 0) { return $customer->moshtari_code; } return 'ثبت مشتری در ته‌حساب در حال انجام است'; } try { $customer->refresh(); if ((int) ($customer->moshtari_code ?? 0) > 0) { return $customer->moshtari_code; } try { $response = Http::withoutVerifying()->timeout(10)->withHeaders([ 'DBName' => $this->Config('tahesab_db'), 'Content-Type' => 'application/json', 'Authorization' => "Bearer " . $this->Config('tahesab_token'), ])->post($this->tahesabApiUrl(), [ "DoNewMoshtari" => [ (string)$customer->name, $this->Config('name_fa'), (string)$customer->mobile, "", "", "", "", "", -1, 0 ] ])->json(); if ($this->sync()->responseOk($response)) { $customer->moshtari_code = $response['OK']; $customer->save(); return $response['OK']; } } catch (\Exception $e) { return $e->getMessage(); } return is_array($response) ? ($this->sync()->buildLog($response, 'ثبت مشتری در ته‌حساب ناموفق بود') ?: 'ثبت مشتری در ته‌حساب ناموفق بود') : 'ثبت مشتری در ته‌حساب ناموفق بود'; } finally { Cache::forget($lockKey); } } $birth_date = ''; if ($customer->birth_date !== null) { $birth_date = Verta::instance(Carbon::parse($customer->birth_date))->format('Y-m-d'); } try { $response = Http::withoutVerifying()->timeout(10)->withHeaders([ 'DBName' => $this->Config('tahesab_db'), 'Content-Type' => 'application/json', 'Authorization' => "Bearer " . $this->Config('tahesab_token'), ])->post($this->tahesabApiUrl(), [ "DoEditMoshtari" => [ $customer->name, $this->Config('name_fa'), $customer->mobile, "", $customer->n_code ?? "", $birth_date, "", "", (int)$customer->moshtari_code, 0 ] ])->json(); } catch (\Exception $e) { return $e->getMessage(); } if (! $this->sync()->responseOk($response)) { return 'tahesab api error!!!'; } return true; } public function healthCheck() { $tahesab = Http::withoutVerifying()->withHeaders([ 'Content-Type' => 'application/json', 'DBName' => $this->Config('tahesab_db'), 'Authorization' => "Bearer " . $this->Config('tahesab_token'), ])->post($this->tahesabApiUrl(), [ "CheckHealth" => [] ])->json(); return $tahesab; // if($tahesab['Api_Status'] === "OK" && $tahesab['DB_Status'] === "OK"){ // return true; // }else{ // return false; // } } public function sanadList() { $tahesab = Http::withoutVerifying()->withHeaders([ 'Content-Type' => 'application/json', 'DBName' => $this->Config('tahesab_db'), 'Authorization' => "Bearer " . $this->Config('tahesab_token'), ])->post($this->tahesabApiUrl(), [ "DoListAsnad" => [ 10, /*Count_Last,*/ "", /*Moshtari_Code,*/ "", /*"AzTarikhShamsi"*/ (string)Verta::instance(Carbon::now()->format('Y-m-d')), /*"TaTarikhShamsi"*/ "", /*"FilterNoSanad"*/ 0, /*Jens_Felez*/ ] ])->json(); return $tahesab; } }