lastPrice(request()); if (is_array($response)) { return $response; } if (is_object($response) && method_exists($response, 'getData')) { return (array) $response->getData(true); } return (array) $response; } private function zarinConfigError() { return response()->json([ 'message' => 'تنظیمات زرین کامل نیست. zarin_api یا zarin_token تنظیم نشده است.' ], 422); } private function zarinAccountingUrl() { $api = trim((string) $this->Config('zarin_api')); if (empty($api)) { return null; } return rtrim($api, '/') . '/ZarrinService/accounting'; } private function zarinToken() { $token = trim((string) $this->Config('zarin_token')); return empty($token) ? null : $token; } private function zarinRequest(array $params, int $timeout = 15) { $url = $this->zarinAccountingUrl(); $token = $this->zarinToken(); if (!$url || !$token) { return null; } $params['token'] = $token; return Http::withoutVerifying() ->timeout($timeout) ->get($url, $params); } /** * decode پاسخ HTTP زرین — خطاهای متنی (مثل ERROR: radif is empty) را هم برمی‌گرداند. * * @return array{0: ?array, 1: ?string} [json, error] */ private function zarinDecodeResponse($http): array { if (!$http) { return [null, 'تنظیمات zarin_api یا zarin_token خالی است.']; } $body = (string) $http->body(); $json = $http->json(); if (!is_array($json) && $body !== '') { $decoded = json_decode($body, true); if (is_array($decoded)) { $json = $decoded; } } if (!$http->successful()) { $detail = null; if (is_array($json)) { $detail = $json['detail'] ?? $json['msg'] ?? $json['message'] ?? $json['error'] ?? null; } if ($detail === null || $detail === '') { $detail = trim($body) !== '' ? mb_substr(trim($body), 0, 300) : null; } $error = 'HTTP ' . $http->status() . ($detail ? (': ' . $detail) : ''); return [is_array($json) ? $json : null, $error]; } if (!is_array($json)) { return [null, 'پاسخ زرین JSON نیست (طول ' . strlen($body) . ')']; } return [$json, null]; } private function zarinDefaultNoeKar() { $value = (int) $this->Config('zarin_noe_kar'); return $value > 0 ? $value : 16; } /** * نوع کار برای سند تعدادی (sabtSanadTedadi). * روی زرین aygin: 16=گرمی (رد می‌شود)، 17=سکه-تعدادی، 18=اجرت تعدادی. */ private function zarinCountNoeKar(): int { $tedadi = (int) $this->Config('zarin_noe_kar_tedadi'); if (in_array($tedadi, [17, 18], true)) { return $tedadi; } $configured = $this->zarinDefaultNoeKar(); if (in_array($configured, [17, 18], true)) { return $configured; } // 16 و سایر مقادیر گرمی/نامرتبط برای sabtSanadTedadi قابل استفاده نیستند return 17; } /** fkNoeهایی که sabtSanadTedadi قبول می‌کند */ private function zarinCountFkNoes(): array { return ['17', '18']; } private function zarinDefaultGoroheKar() { $value = (int) $this->Config('zarin_gorohe_kar'); return $value > 0 ? $value : 52; } /** نام گروه حساب مشتری در زرین — تنظیم zarin_name_g؛ خالی = گروه ۶ مشتری نقدی */ private function zarinCustomerGroupName(): string { $nameG = trim((string) $this->Config('zarin_name_g')); return $nameG !== '' ? $nameG : 'مشتری نقدی (خریدار)'; } /** آیا درخواست لیست زرین را خواسته؟ (query/body/header؛ مقاوم به QUERY_STRING خالی) */ private function requestWantsZarinList(Request $request): bool { $header = $request->header('X-Include-Zarin', $request->header('Include-Zarin')); if ($header !== null && $header !== '') { if (filter_var($header, FILTER_VALIDATE_BOOLEAN) || in_array($header, [1, '1', 'yes', 'on'], true)) { return true; } } foreach (['include_zarin', 'include_tahesab'] as $key) { $raw = $request->query($key, $request->input($key, null)); if ($raw === null && !$request->query->has($key) && !$request->exists($key)) { continue; } // ?include_zarin بدون مقدار if ($raw === null || $raw === '' || $raw === []) { return true; } if ($request->boolean($key)) { return true; } if (in_array($raw, [1, '1', true, 'true', 'yes', 'on', 'TRUE', 'Yes'], true)) { return true; } } return false; } /** مقایسه نام گروه زرین (ی/ک، پرانتز/آندرلاین، فاصله) */ private function zarinGroupNamesMatch(string $a, string $b): bool { $norm = function (string $s): string { $s = $this->normalizeZarinName($s); $s = str_replace(['(', ')', '_', '-', '–', '—'], ' ', $s); return trim(preg_replace('/\s+/u', ' ', $s) ?? $s); }; $na = $norm($a); $nb = $norm($b); if ($na === '' || $nb === '') { return false; } return $na === $nb || $this->containsText($na, $nb) || $this->containsText($nb, $na); } /** * پیشوند عددی uid — وقتی چند پنل یک توکن زرین دارند باید متفاوت باشد * تا uid تکراری و پاسخ Cache قاطی نشود (فقط ارقام، طبق داکیومنت زرین). */ private function zarinUid(string $kindPrefix, $localId): string { $panel = preg_replace('/\D+/', '', (string) $this->Config('zarin_uid_prefix')) ?: ''; $local = preg_replace('/\D+/', '', (string) $localId) ?: '0'; return $panel . $kindPrefix . $local; } private function isListArray(array $array) { if (function_exists('array_is_list')) { return array_is_list($array); } return array_keys($array) === range(0, count($array) - 1); } private function containsText($haystack, $needle) { $haystack = (string) $haystack; $needle = (string) $needle; if ($needle === '') { return false; } return mb_strpos($haystack, $needle) !== false; } private function normalizeZarinName($value) { $value = trim((string) $value); $value = str_replace(['ي', 'ك', '‌'], ['ی', 'ک', ' '], $value); $value = preg_replace('/\s+/u', ' ', $value); return mb_strtolower($value); } private function zarinRowsFromBody($body) { if (!is_array($body)) { return []; } $data = $body['data'] ?? $body['Data'] ?? []; if (!is_array($data)) { return []; } return $this->isListArray($data) ? $data : [$data]; } private function extractResponseRow(?array $body): array { if (!is_array($body)) { return []; } if (isset($body['data']) && is_array($body['data'])) { $data = $this->isListArray($body['data']) ? ($body['data'][0] ?? []) : $body['data']; if (is_array($data)) { return array_merge($body, $data); } } return $body; } /** * پیدا کردن کار/کالای محصول در زرین. * اولویت: listKarSakhte (کاتالوگ کار ساخته) → infosakhte → infokarbarcodi. */ private function findZarinKarForProduct(Product $product, $goroheKar = null, bool $exactOnly = false) { $goroheKar = $goroheKar ?: $this->zarinDefaultGoroheKar(); $productName = $this->normalizeZarinName($product->title); $fromList = $this->findZarinKarInListKarSakhte($productName, $exactOnly, true); if ($fromList) { return $fromList; } $fromBarcode = $this->findZarinKarInInfokarbarcodi($productName, $goroheKar, $exactOnly); if ($fromBarcode) { return $fromBarcode; } return $this->findZarinKarInInfosakhte($productName, $goroheKar, $exactOnly); } /** * کاتالوگ کارهای ساخته (لیست ۴۷ زرین) — id همین idKar سند تعدادی است. * برای سند تعدادی فقط fkNoe سکه-تعدادی/اجرت تعدادی (۱۷/۱۸) قبول می‌شود؛ گرمی(۱۶) رد. */ private function findZarinKarInListKarSakhte(string $productName, bool $exactOnly = false, bool $countDocOnly = true): ?array { $response = $this->zarinRequest([ 'kind' => 'listKarSakhte', ], 30); if (!$response || !$response->successful()) { return null; } $rows = $this->zarinRowsFromBody($response->json()); if (empty($rows)) { return null; } $allowedFk = $this->zarinCountFkNoes(); $exact = null; $fuzzy = null; foreach ($rows as $row) { if (!is_array($row)) { continue; } $fkNoe = (string) ($row['fkNoe'] ?? $row['fknoe'] ?? ''); if ($countDocOnly && $fkNoe !== '' && !in_array($fkNoe, $allowedFk, true)) { continue; } $name = $row['namFelezsakhthe'] ?? $row['nam_kar_sakhte'] ?? $row['nameKar'] ?? $row['nam'] ?? $row['name'] ?? ''; $rowName = $this->normalizeZarinName($name); $idKar = $this->extractUsableZarinKarId($row); if (!$idKar || $rowName === '') { continue; } if ($rowName === $productName) { $exact = [ 'idKar' => $idKar, 'row' => $row, 'match' => 'exact', 'source' => 'listKarSakhte', 'fkNoe' => $fkNoe, ]; break; } if ( !$exactOnly && ( $this->containsText($rowName, $productName) || $this->containsText($productName, $rowName) ) ) { $fuzzy ??= [ 'idKar' => $idKar, 'row' => $row, 'match' => 'fuzzy', 'source' => 'listKarSakhte', 'fkNoe' => $fkNoe, ]; } } return $exact ?? ($exactOnly ? null : $fuzzy); } private function matchZarinKarRow(string $productName, array $row, bool $exactOnly = false): ?array { $name = $row['nameKar'] ?? $row['nam_kar_sakhte'] ?? $row['nam'] ?? $row['name'] ?? $row['namKar'] ?? $row['title'] ?? ''; $rowName = $this->normalizeZarinName($name); $idKar = $this->extractUsableZarinKarId($row); if (!$idKar || $rowName === '') { return null; } if ($rowName === $productName) { return [ 'idKar' => $idKar, 'row' => $row, 'match' => 'exact', ]; } if ($exactOnly) { return null; } if ( $this->containsText($rowName, $productName) || $this->containsText($productName, $rowName) ) { return [ 'idKar' => $idKar, 'row' => $row, 'match' => 'fuzzy', ]; } return null; } private function findZarinKarInInfokarbarcodi(string $productName, $goroheKar, bool $exactOnly = false): ?array { $exact = null; $fuzzy = null; for ($page = 1; $page <= 10; $page++) { $response = $this->zarinRequest([ 'kind' => 'infokarbarcodi', 'id' => $goroheKar, 'vazneat' => 0, 'pageNum' => $page, 'detailCount' => 250, ]); // روی بعضی سرورهای زرین جدول barcodetala نیست → 400؛ برو سراغ infosakhte if (!$response || !$response->successful()) { return null; } $rows = $this->zarinRowsFromBody($response->json()); if (empty($rows)) { break; } foreach ($rows as $row) { if (!is_array($row)) { continue; } $hit = $this->matchZarinKarRow($productName, $row, $exactOnly); if (!$hit) { continue; } if (($hit['match'] ?? '') === 'exact') { $exact = $hit; break 2; } $fuzzy ??= $hit; } if (count($rows) < 250) { break; } } return $exact ?? ($exactOnly ? null : $fuzzy); } /** لیست کارهای ساخته — فیلد nam_kar_sakhte / id (تست‌شده روی API زرین) */ private function findZarinKarInInfosakhte(string $productName, $goroheKar, bool $exactOnly = false): ?array { $response = $this->zarinRequest([ 'kind' => 'infosakhte', ], 30); if (!$response || !$response->successful()) { return null; } $rows = $this->zarinRowsFromBody($response->json()); if (empty($rows)) { return null; } $noeKar = (string) $this->zarinDefaultNoeKar(); $gorohe = (string) $goroheKar; $exactPreferred = null; $exactFallback = null; $fuzzyPreferred = null; $fuzzyFallback = null; foreach ($rows as $row) { if (!is_array($row)) { continue; } $hit = $this->matchZarinKarRow($productName, $row, $exactOnly); if (!$hit) { continue; } $idGp = (string) ($row['idGpFelez'] ?? $row['idgpfelez'] ?? $row['gorooheKar'] ?? ''); $inGroup = $idGp !== '' && ($idGp === $gorohe || $idGp === $noeKar); $isExact = ($hit['match'] ?? '') === 'exact'; if ($isExact && $inGroup) { $exactPreferred = $hit; break; } if ($isExact) { $exactFallback ??= $hit; } elseif ($inGroup) { $fuzzyPreferred ??= $hit; } else { $fuzzyFallback ??= $hit; } } if ($exactOnly) { return $exactPreferred ?? $exactFallback; } return $exactPreferred ?? $exactFallback ?? $fuzzyPreferred ?? $fuzzyFallback; } private function getZarinKarById($idKar) { if (empty($idKar) || !is_numeric($idKar)) { return null; } $response = $this->zarinRequest([ 'kind' => 'infosakhte', 'id' => (int) $idKar, ]); if (!$response || !$response->successful()) { return null; } $body = $response->json(); $rows = $this->zarinRowsFromBody($body); foreach ($rows as $row) { if (!is_array($row)) { continue; } // infosakhte: کد کار = id (نه pkFelez) $rowId = $row['idKar'] ?? $row['idkar'] ?? $row['IDKar'] ?? $row['id'] ?? null; if ((string) $rowId === (string) $idKar) { return $row; } } return null; } /** * کد کار قابل استفاده برای sabtSanadTedadi. * infosakhte → فیلد id؛ addFelezsakhteModel → idKar. * pkFelez هرگز idKar نیست. */ private function extractUsableZarinKarId(array $row) { foreach ([ 'idKar', 'idkar', 'IDKar', 'id', 'zarrinId', 'zarinId', 'code', 'Code', ] as $key) { if (isset($row[$key]) && is_numeric($row[$key]) && (int) $row[$key] > 0) { return (int) $row[$key]; } } return null; } /** پاسخ addFelezsakhteModel: {"state":true,"msg":"...","idKar":3130} */ private function extractIdKarFromCreateResponse($body): ?int { if (!is_array($body)) { return null; } foreach (['idKar', 'idkar', 'IDKar'] as $key) { if (isset($body[$key]) && is_numeric($body[$key]) && (int) $body[$key] > 0) { return (int) $body[$key]; } } $data = $body['data'] ?? null; if (is_array($data)) { if (isset($data[0]) && is_array($data[0])) { $fromRow = $this->extractUsableZarinKarId($data[0]); if ($fromRow) { return $fromRow; } } $fromData = $this->extractUsableZarinKarId($data); if ($fromData) { return $fromData; } } return null; } private function createZarinKarForProduct(Product $product) { $params = [ 'kind' => 'addFelezsakhteModel', 'nameKar' => $product->title, 'noeKar' => $this->zarinCountNoeKar(), 'gorooheKar' => $this->zarinDefaultGoroheKar(), ]; $response = $this->zarinRequest($params); if (!$response) { return [ 'ok' => false, 'message' => 'تنظیمات زرین کامل نیست. zarin_api یا zarin_token تنظیم نشده است.', 'status_code' => 422, 'sent_params' => $params, ]; } if (!$response->successful()) { return [ 'ok' => false, 'message' => 'ساخت کد کار در زرین ناموفق بود.', 'status_code' => 502, 'status' => $response->status(), 'body' => $response->body(), 'json' => $response->json(), 'sent_params' => $params, ]; } $body = $response->json(); if (is_array($body) && array_key_exists('state', $body) && filter_var($body['state'], FILTER_VALIDATE_BOOLEAN) === false) { // ممکن است کالا از قبل باشد و idKar در پیام آمده باشد $createdIdKar = $this->extractIdKarFromCreateResponse($body); if ($createdIdKar) { $product->finance_p_name = (string) $createdIdKar; $product->save(); return [ 'ok' => true, 'source' => 'create_response_idKar_despite_state', 'idKar' => $createdIdKar, 'product' => $product->fresh(), 'zarin_response' => $body, 'sent_params_success' => $params, ]; } return [ 'ok' => false, 'message' => (string) ($body['detail'] ?? $body['msg'] ?? $body['message'] ?? 'ساخت کد کار در زرین ناموفق بود.'), 'status_code' => 422, 'zarin_response' => $body, 'sent_params' => $params, ]; } $createdIdKar = $this->extractIdKarFromCreateResponse(is_array($body) ? $body : null); // اولویت با idKar برگشتی از ساخت — جستجوی فازی نام می‌تواند کالای اشتباه بچسباند if ($createdIdKar) { $product->finance_p_name = (string) $createdIdKar; $product->save(); return [ 'ok' => true, 'source' => 'created_idKar', 'idKar' => $createdIdKar, 'product' => $product->fresh(), 'zarin_row' => $this->getZarinKarById($createdIdKar), 'zarin_response' => $body, 'sent_params_success' => $params, ]; } $foundAfterCreate = $this->findZarinKarForProduct($product, $this->zarinDefaultGoroheKar()); if ($foundAfterCreate && !empty($foundAfterCreate['idKar']) && is_numeric($foundAfterCreate['idKar'])) { $product->finance_p_name = (string) $foundAfterCreate['idKar']; $product->save(); return [ 'ok' => true, 'source' => 'created_and_found_by_name', 'idKar' => (int) $foundAfterCreate['idKar'], 'product' => $product->fresh(), 'zarin_row' => $foundAfterCreate['row'], 'zarin_response' => $body, 'sent_params_success' => $params, ]; } return [ 'ok' => false, 'message' => 'زرین کالا را ساخت، اما idKar قابل استفاده برنگشت.', 'status_code' => 422, 'zarin_response' => $body, 'sent_params' => $params, ]; } private function ensureZarinKarForProduct(Product $product, $forceCreate = false) { $currentIdKar = $product->finance_p_name; $productName = $this->normalizeZarinName($product->title); // بعد از «کد کار اشتباه»: کالای تازه بساز؛ اگر ساخت نشد، exact از کاتالوگ if ($forceCreate) { $this->clearProductZarinKar($product); $product->refresh(); $created = $this->createZarinKarForProduct($product); if (!empty($created['ok']) && !empty($created['idKar'])) { return $created; } $found = $this->findZarinKarForProduct($product, $this->zarinDefaultGoroheKar(), true); if ($found && !empty($found['idKar']) && is_numeric($found['idKar'])) { $product->finance_p_name = (string) $found['idKar']; $product->save(); return [ 'ok' => true, 'source' => 'found_exact_after_create_failed', 'idKar' => (int) $found['idKar'], 'product' => $product->fresh(), 'zarin_row' => $found['row'], 'create_error' => $created, ]; } return $created; } if (!empty($currentIdKar) && is_numeric($currentIdKar)) { $existingRow = $this->getZarinKarById($currentIdKar); if ($existingRow) { $rowName = $this->normalizeZarinName( $existingRow['nameKar'] ?? $existingRow['nam_kar_sakhte'] ?? $existingRow['nam'] ?? $existingRow['name'] ?? $existingRow['namKar'] ?? $existingRow['title'] ?? '' ); $idGp = (string) ($existingRow['idGp'] ?? $existingRow['idgp'] ?? $existingRow['fkNoe'] ?? ''); $wrongType = $idGp !== '' && !in_array($idGp, $this->zarinCountFkNoes(), true); // اگر idKar موجود مال کالای دیگری است یا نوع گرمی است (نه تعدادی) قبول نکن if ( $wrongType || ($rowName !== '' && $productName !== '' && $rowName !== $productName) ) { $this->clearProductZarinKar($product); $product->refresh(); } else { $usableIdKar = $this->extractUsableZarinKarId($existingRow) ?: (int) $currentIdKar; if ((string) $product->finance_p_name !== (string) $usableIdKar) { $product->finance_p_name = (string) $usableIdKar; $product->save(); } return [ 'ok' => true, 'source' => 'existing_verified', 'idKar' => (int) $usableIdKar, 'product' => $product->fresh(), 'zarin_row' => $existingRow, ]; } } else { $this->clearProductZarinKar($product); $product->refresh(); } } // اول فقط exact؛ اگر نبود fuzzy از listKarSakhte/infosakhte $found = $this->findZarinKarForProduct($product, $this->zarinDefaultGoroheKar(), true) ?? $this->findZarinKarForProduct($product, $this->zarinDefaultGoroheKar(), false); if ($found && !empty($found['idKar']) && is_numeric($found['idKar'])) { $product->finance_p_name = (string) $found['idKar']; $product->save(); return [ 'ok' => true, 'source' => 'found_by_name', 'idKar' => (int) $found['idKar'], 'product' => $product->fresh(), 'zarin_row' => $found['row'], 'match' => $found['match'] ?? null, 'list_source' => $found['source'] ?? null, ]; } return $this->createZarinKarForProduct($product); } private function clearProductZarinKar(Product $product): void { if ($product->finance_p_name === null || $product->finance_p_name === '') { return; } $product->finance_p_name = null; $product->save(); } private function isInvalidZarinKarError($response, ?string $httpError = null): bool { $haystack = ''; if (is_string($httpError)) { $haystack .= $httpError; } if (is_array($response)) { $haystack .= ' ' . json_encode($response, JSON_UNESCAPED_UNICODE); } elseif (is_string($response)) { $haystack .= ' ' . $response; } return str_contains($haystack, 'کد کار اشتباه'); } public function table(Request $request) { $view = (string) $request->input('view', 'all'); $query = Zarin::with('customer')->orderByDesc('id'); if ($view === 'ok') { $query->whereNotNull('zarin_id')->where('zarin_id', '!=', ''); } elseif ($view === 'failed') { $query->where(function ($q) { $q->where(function ($inner) { $inner->whereNull('zarin_id')->orWhere('zarin_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) { $zarin = Zarin::with('customer')->findOrFail($id); return response()->json($zarin); } /** * لیست حساب‌های زرین — فقط listhesabha (Get100Account لیست نیست و ۴۰۰ می‌دهد). * * @return array{rows: array, error: ?string, http_status: ?int} */ private function fetchAllZarinCustomers(): array { if (!$this->zarinAccountingUrl() || !$this->zarinToken()) { return [ 'rows' => [], 'error' => 'تنظیمات zarin_api یا zarin_token خالی است.', 'http_status' => null, ]; } try { // listhesabha کامل است (~۳۰۰ ردیف / چند ثانیه)؛ timeout بالاتر از درخواست‌های کوتاه $response = $this->zarinRequest([ 'kind' => 'listhesabha', ], 45); if (!$response) { return [ 'rows' => [], 'error' => 'درخواست به زرین ساخته نشد (آدرس/توکن).', 'http_status' => null, ]; } $status = $response->status(); $body = $response->body(); if (!$response->successful()) { $snippet = mb_substr(trim((string) $body), 0, 180); $msg = "listhesabha ناموفق (HTTP {$status})"; if ($snippet !== '') { $msg .= ': ' . $snippet; } return [ 'rows' => [], 'error' => $msg, 'http_status' => $status, ]; } $json = $response->json(); if (!is_array($json)) { return [ 'rows' => [], 'error' => 'پاسخ listhesabha JSON معتبر نیست. طول بدنه: ' . strlen((string) $body), 'http_status' => $status, ]; } $rows = $this->zarinRowsFromBody($json); if (empty($rows)) { return [ 'rows' => [], 'error' => 'listhesabha خالی بود یا کلید data پیدا نشد. کلیدها: ' . implode(',', array_keys($json)), 'http_status' => $status, ]; } return [ 'rows' => $rows, 'error' => null, 'http_status' => $status, ]; } catch (\Exception $e) { Log::warning('zarin listhesabha failed', ['message' => $e->getMessage()]); return [ 'rows' => [], 'error' => 'خطا در ارتباط با زرین: ' . $e->getMessage(), 'http_status' => null, ]; } } /** نام حساب از پاسخ listhesabha (فیلد nam، نه name) */ private function extractZarinName(array $item): string { $nam = trim((string) ($item['nam'] ?? $item['Nam'] ?? '')); if ($nam !== '') { // "تستی * هوم" → "تستی هوم" $nam = preg_replace('/\s*\*\s*/u', ' ', $nam) ?? $nam; return trim(preg_replace('/\s+/u', ' ', $nam) ?? $nam); } $name = trim((string) ($item['name'] ?? $item['Name'] ?? '')); $family = trim((string) ($item['family'] ?? $item['Family'] ?? '')); $full = trim($name . ' ' . $family); return $full !== '' ? $full : $name; } private function extractZarinGroup(array $item): string { return trim((string) ( $item['gorooh'] ?? $item['GoroupName'] ?? $item['group'] ?? $item['Group'] ?? '' )); } public function customerSyncStatus(Request $request): \Illuminate\Http\JsonResponse { return $this->buildCustomerSyncStatus($request, $this->requestWantsZarinList($request)); } /** مسیر بدون query — وقتی nginx QUERY_STRING را نمی‌فرستد */ public function customerSyncStatusWithZarin(Request $request): \Illuminate\Http\JsonResponse { return $this->buildCustomerSyncStatus($request, true); } private function buildCustomerSyncStatus(Request $request, bool $include): \Illuminate\Http\JsonResponse { $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); $zarinTotal = null; $zarinOnlyCount = null; $zarinFetchError = null; $zarinHttpStatus = null; $zarinGroupFilter = $this->zarinCustomerGroupName(); $zarinOnlyResult = ['data' => [], 'total' => 0, 'current_page' => 1, 'last_page' => 1]; $debugSample = []; if ($include) { $fetched = $this->fetchAllZarinCustomers(); $zarinCustomers = $fetched['rows']; $zarinFetchError = $fetched['error']; $zarinHttpStatus = $fetched['http_status']; // فقط گروه تنظیم‌شده (گروه ۶ / zarin_name_g) — نه همهٔ حساب‌های زرین if ($zarinGroupFilter !== '' && !empty($zarinCustomers)) { $zarinCustomers = array_values(array_filter($zarinCustomers, function ($item) use ($zarinGroupFilter) { if (!is_array($item)) { return false; } $g = $this->extractZarinGroup($item); return $g !== '' && $this->zarinGroupNamesMatch($g, $zarinGroupFilter); })); } $zarinTotal = count($zarinCustomers); $panelMobiles = Customer::whereNull('deleted_at') ->whereNotNull('mobile') ->pluck('mobile') ->mapWithKeys(fn ($m) => [ltrim((string) $m, '0') => true]) ->toArray(); $panelCodes = Customer::whereNull('deleted_at') ->whereNotNull('moshtari_code') ->pluck('moshtari_code') ->mapWithKeys(fn ($c) => [(string) $c => true]) ->toArray(); $zarinOnlyList = []; foreach ($zarinCustomers as $item) { if (!is_array($item)) { continue; } if (count($debugSample) < 3) { $debugSample[] = $item; } $code = $item['idHesab'] ?? $item['Code'] ?? $item['code'] ?? $item['id'] ?? null; if ($code !== null && isset($panelCodes[(string) $code])) { continue; } $phone = $this->extractZarinPhone($item); $tel = ltrim($phone, '0'); if ($tel && isset($panelMobiles[$tel])) { continue; } $zarinOnlyList[] = [ 'code' => $code, 'name' => $this->extractZarinName($item), 'tel' => $phone, 'gid' => $item['GID'] ?? null, 'group' => $this->extractZarinGroup($item), ]; } $zarinOnlyCount = count($zarinOnlyList); $zarinOnlyResult = [ 'data' => array_values($zarinOnlyList), 'total' => $zarinOnlyCount, 'current_page' => 1, 'last_page' => 1, ]; } $payload = [ 'panel_total' => $panelTotal, 'panel_registered' => $panelRegistered, 'panel_only' => $panelOnly, 'zarin_total' => $zarinTotal, 'zarin_only_count' => $zarinOnlyCount, 'zarin_fetch_error' => $zarinFetchError, 'zarin_http_status' => $zarinHttpStatus, 'zarin_group_filter' => $zarinGroupFilter, 'include_zarin' => $include, 'include_zarin_param' => $request->query('include_zarin', $request->input('include_zarin')), 'panel_only_list' => $panelOnlyList, 'zarin_only_list' => $zarinOnlyResult, ]; if ($include && ($request->boolean('raw') || $request->input('raw'))) { $payload['zarin_debug_sample'] = $debugSample; } return response()->json($payload)->header('Cache-Control', 'no-store, private'); } private function extractZarinPhone(array $item): string { $candidates = ['mobile', 'Mobile', 'tell', 'Tell', 'tel', 'Tel', 'Phone', 'CellPhone', 'Cell', 'MobileNo', 'Telephone', 'Mobile1']; foreach ($candidates as $key) { if (isset($item[$key]) && trim((string) $item[$key]) !== '') { return trim((string) $item[$key]); } } 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 ''; } public function syncPanelToZarin(): \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 importFromZarin(Request $request): \Illuminate\Http\JsonResponse { $validated = $request->validate([ 'customers' => 'required|array', 'customers.*.code' => 'required|integer', 'customers.*.name' => 'nullable|string', 'customers.*.tel' => 'nullable|string', ]); $success = 0; $failed = 0; $errors = []; foreach ($validated['customers'] as $item) { $mobile = Customer::normalizeMobile($item['tel'] ?? '') ?? ''; $fullName = trim($item['name']); $code = (int) $item['code']; if ($fullName === '') { $fullName = 'مشتری زرین ' . $code; } // موبایل برای خیلی از حساب‌های زرین خالی است (مثل تستی هوم) — مانع import نیست $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(); } $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(); $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 = new Customer(); $customer->fname = $fullName; $customer->lname = ''; $customer->name = $fullName; $customer->mobile = $mobile !== '' ? $mobile : null; $customer->active = true; $customer->moshtari_code = $item['code']; $customer->referral_id = $referralId; $customer->type = 2; if ($mobile !== '') { $customer->password = $mobile; } $customer->save(); $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]; } } return response()->json([ 'ok' => true, 'success' => $success, 'failed' => $failed, 'errors' => $errors, ]); } /** * حذف مشتریانِ خرابِ واردشده از زرین (نام خالی) برای پاک‌سازی import ناقص. * با dry_run=1 فقط تعداد و نمونه را نشان می‌دهد و چیزی حذف نمی‌کند. */ public function rollbackImport(Request $request): \Illuminate\Http\JsonResponse { $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) { $zarin = Zarin::with('customer')->findOrFail($id); $result = $this->sync()->retry($zarin); return response()->json($result, ($result['ok'] ?? false) ? 200 : 422); } public function pendingSummary(Request $request) { $customerId = $request->filled('customer_id') ? (int) $request->input('customer_id') : null; return response()->json($this->sync()->pendingSummary($customerId)); } 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); } private function resolveProductId(array $data, ?Zarin $existing = null): ?int { if (!empty($data['product']['id'])) { return (int) $data['product']['id']; } if (!empty($data['product_id'])) { return (int) $data['product_id']; } $orderId = $data['order']['id'] ?? ($existing->order_id ?? null); if ($orderId) { $order = Order::find($orderId); if ($order && !empty($order->product_id)) { return (int) $order->product_id; } } return null; } public function storeTrade($data, ?Zarin $existing = null) { if (empty($data['customer_id']) && $existing && $existing->order_id) { $order = Order::find($existing->order_id); if ($order) { $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' => [ 'id' => $order->product_id, 'metal_id' => $existing->type === 'طلا' ? 1 : 2, ], '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) { if ($customer && ($customer->moshtari_code === null)) { $this->createCustomerInZarin($customer->id); $customer = Customer::where('id', $data['customer_id'])->first(); } if (!$customer || empty($customer->moshtari_code)) { return false; } } elseif (! $customer) { return false; } $accountCustomer = $customer; if ($isColleagueLeg) { $accountCustomer = clone $customer; $accountCustomer->moshtari_code = $accountOverride; } $rawAmount = (float) $data['amount']; $count = $mapper->accountingQty($product, $rawAmount, $data); $vazn = $mapper->accountingWeight($product, $rawAmount, $data); $isBuy = (int) ($data['direction'] ?? 0) === 1; $orderId = is_object($data['order'] ?? null) ? ($data['order']->id ?? null) : ($data['order']['id'] ?? ($existing->order_id ?? null)); $title = $mapper->productTitle($product); $action = $isBuy ? 'خرید' : 'فروش'; $sharh = trim((string) ($data['sharh_override'] ?? '')); if ($sharh === '') { $sharh = trim($title !== '' ? "{$action} {$title}" : "{$action} {$mapper->kindLabel($mapper->resolveKind($product))}"); } $syncAmount = $mapper->isCountDoc($product) ? $count : $vazn; $uidSource = $orderId ?? null; if (!empty($data['line'])) { $uidSource = ($orderId ?? '0') . 'L' . (int) $data['line']; } if ($isColleagueLeg) { $uidSource = ($uidSource ?? '0') . 'C'; } $data['handler'] = 'storeTrade'; if ($existing && empty($existing->data)) { $existing->update(['data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)]); } $zarin = $existing ?? Zarin::create([ 'customer_id' => $customer->id, 'desc' => $sharh, 'amount' => $syncAmount, 'order_id' => $orderId, 'date' => $data['pay_at'] ?? Carbon::now(), 'type' => $mapper->zarinSyncTypeLabel($product), 'created_at' => Carbon::now(), 'data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), ]); if ($existing) { $existing->update([ 'type' => $mapper->zarinSyncTypeLabel($product), 'desc' => $sharh, 'amount' => $syncAmount, ]); } $uidKind = $isColleagueLeg ? '11' : '10'; try { if ($mapper->isCountDoc($product)) { $fi = (float) ($data['price'] ?? 0); if ($count > 0) { if (!empty($data['total_price'])) { $fi = ((float) $data['total_price']) / $count; } elseif ($fi > 0 && abs($vazn - $count) > 0.0001) { $fi = $fi * ($vazn / $count); } } $result = $this->postZarinCountDoc( $zarin, $data, $accountCustomer, $product, $count, $isBuy ? -1 : 1, $fi, $sharh, $this->zarinUid($uidKind, $uidSource ?? $zarin->id) ); } else { $result = $this->postZarinWeightDoc( $zarin, $data, $accountCustomer, $vazn, $isBuy ? 'f' : 'k', (float) ($data['price'] ?? 0), $sharh, $mapper->jensFelez($product) ?? 0, $this->zarinUid($uidKind, $uidSource ?? $zarin->id) ); } } catch (\Exception $e) { $this->sync()->markFailed($zarin, $e->getMessage()); return $e->getMessage(); } return $result; } // direction: 0 = daryaft az moshtari, 1 = pardakht be moshtari public function wallet($data, ?Zarin $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 = Customer::where('id', $data['customer_id'])->first(); if ($customer && ($customer->moshtari_code === null)) { $this->createCustomerInZarin($customer->id); $customer = Customer::where('id', $data['customer_id'])->first(); } if (!$customer || empty($customer->moshtari_code)) { return false; } $data['handler'] = 'wallet'; $isDeposit = (int) ($data['direction'] ?? 0) === 0; $sharh = self::accountingWalletDesc($isDeposit, $data['note'] ?? null); $zarin = $existing ?? Zarin::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)]); } $prices = $this->lastPriceData(); $isWithdraw = (int) ($data['direction'] ?? 0) === 1; $mazaneh = $isWithdraw ? (int) ($prices['sell'] ?? 0) : (int) ($prices['buy'] ?? 0); if ($mazaneh <= 0) { $mazaneh = 1; } $uidSource = $data['withdraw_id'] ?? $zarin->id; try { $http = $this->zarinRequest([ 'kind' => 'pooli', 'radif' => 1, 'korf' => $isWithdraw ? 'f' : 'k', 'poolorvazn' => 'pool', 'meghdar' => $data['value'], 'fi' => $mazaneh, 'hesab' => $customer->moshtari_code, 'sharh' => $sharh, 'ayar' => '750', 'movaghat' => '', 'idFactor' => '', 'uid' => $this->zarinUid('20', $uidSource), ]); if (!$http) { $this->sync()->markFailed($zarin, null, 'تنظیمات زرین کامل نیست'); return 'تنظیمات زرین کامل نیست. zarin_api یا zarin_token تنظیم نشده است.'; } [$response, $httpError] = $this->zarinDecodeResponse($http); if ($httpError) { $this->sync()->markFailed($zarin, $response ?? $httpError, $httpError); return false; } Log::info('Zarin wallet response', [ 'data' => $data, 'response' => $response ?? null, ]); } catch (\Exception $e) { $this->sync()->markFailed($zarin, $e->getMessage()); return $e->getMessage(); } if ($this->sync()->responseOk(is_array($response) ? $response : [])) { $this->sync()->markSuccess($zarin, is_array($response) ? $response : [], $data); return $data; } $this->sync()->markFailed($zarin, $response, 'ثبت سند ریالی در زرین ناموفق بود'); return false; } /** * پاداش معرفی — همیشه روی حساب معرف (نه کاربر تازه‌ثبت‌نام‌شده). */ 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, ]); } /** * پاداش/هدیه طلایی — سند وزنی با korf=f (هدیه به مشتری). */ public function goldReward($data, ?Zarin $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(); if (!$customer) { return false; } if ($customer->moshtari_code === null) { $this->createCustomerInZarin($customer->id); $customer = Customer::where('id', $data['customer_id'])->first(); } if (!$customer || empty($customer->moshtari_code)) { return false; } $amount = abs((float) $data['value']); $desc = !empty($data['desc']) ? (string) $data['desc'] : 'پاداش'; $data['handler'] = 'goldReward'; $zarin = $existing ?? Zarin::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)]); } $prices = $this->lastPriceData(); $mazaneh = (int) ($prices['buy'] ?? 0); if ($mazaneh <= 0) { $mazaneh = 1; } try { $http = $this->zarinRequest([ 'kind' => 'pooli', 'radif' => 1, 'korf' => 'f', 'poolorvazn' => 'vazn', 'meghdar' => $amount, 'fi' => $mazaneh, 'hesab' => $customer->moshtari_code, 'sharh' => $desc, 'ayar' => '750', 'movaghat' => '', 'idFactor' => '', 'uid' => $this->zarinUid('50', $zarin->id), ]); if (!$http) { $this->sync()->markFailed($zarin, null, 'تنظیمات زرین کامل نیست'); return 'تنظیمات زرین کامل نیست. zarin_api یا zarin_token تنظیم نشده است.'; } [$response, $httpError] = $this->zarinDecodeResponse($http); if ($httpError) { $this->sync()->markFailed($zarin, $response ?? $httpError, $httpError); return false; } } catch (\Exception $e) { $this->sync()->markFailed($zarin, $e->getMessage()); return $e->getMessage(); } if ($this->sync()->responseOk(is_array($response) ? $response : [])) { $this->sync()->markSuccess($zarin, is_array($response) ? $response : [], $data); return $data; } $this->sync()->markFailed($zarin, $response, 'ثبت پاداش طلایی در زرین ناموفق بود'); return false; } // direction: 1 = واریز، 0 = برداشت public function walletGold($data, ?Zarin $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(); if ($customer && ($customer->moshtari_code === null)) { $this->createCustomerInZarin($customer->id); $customer = Customer::where('id', $data['customer_id'])->first(); } if (!$customer || empty($customer->moshtari_code)) { return false; } $rawValue = (float) $data['value']; $count = $mapper->accountingQty($product, $rawValue, $data); $vazn = $mapper->accountingWeight($product, $rawValue, $data); $isDeposit = (int) ($data['direction'] ?? 1) === 1; $title = $mapper->productTitle($product); $sharh = self::accountingWalletDesc( $isDeposit, !empty($data['note']) ? (string) $data['note'] : (!empty($data['desc']) ? (string) $data['desc'] : null) ); $data['note'] = $sharh; $data['handler'] = 'walletGold'; $syncAmount = $mapper->isCountDoc($product) ? $count : $vazn; $zarin = $existing ?? Zarin::create([ 'customer_id' => $customer->id, 'desc' => $sharh, 'amount' => $syncAmount, 'date' => Carbon::now(), 'type' => $mapper->zarinSyncTypeLabel($product), 'created_at' => Carbon::now(), 'withdraw_id' => $data['withdraw_id'] ?? null, 'data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), ]); if ($existing) { $existing->update([ 'type' => $mapper->zarinSyncTypeLabel($product), 'desc' => $sharh, 'amount' => $syncAmount, ]); } if ($existing && empty($existing->data)) { $existing->update(['data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)]); } $prices = $this->lastPriceData(); $mazaneh = $isDeposit ? (int) ($prices['buy'] ?? 0) : (int) ($prices['sell'] ?? 0); if ($mazaneh <= 0) { $mazaneh = 1; } $uidSource = $data['withdraw_id'] ?? $zarin->id; try { if ($mapper->isCountDoc($product)) { $unitPrice = $mazaneh; if ($product && (float) ($product->amount ?? 0) > 0) { $unitPrice = (int) round($mazaneh * (float) $product->amount); } if ($unitPrice <= 0) { $unitPrice = $mazaneh; } return $this->postZarinCountDoc( $zarin, $data, $customer, $product, $count, $isDeposit ? -1 : 1, (float) $unitPrice, $sharh, $this->zarinUid('21', $uidSource) ); } return $this->postZarinWeightDoc( $zarin, $data, $customer, $vazn, $isDeposit ? 'f' : 'k', (float) $mazaneh, $sharh, $mapper->jensFelez($product) ?? 0, $this->zarinUid('21', $uidSource) ); } catch (\Exception $e) { $this->sync()->markFailed($zarin, $e->getMessage()); return $e->getMessage(); } } /** * سند تعدادی زرین (سکه / شمش / گوی / مکعب). * در زرین همه با sabtSanadTedadi می‌روند؛ تفکیک فقط با idKar جداگانهٔ هر محصول است. */ private function postZarinCountDoc( Zarin $zarin, array $data, Customer $customer, $product, float $tedad, int $parDar, float $fi, string $sharh, string $uid ) { if (!$product instanceof Product || empty($product->id)) { $this->sync()->markFailed($zarin, null, 'محصول برای سند تعدادی زرین مشخص نیست'); return false; } if ($fi <= 0) { $fi = 1; } $retriedInvalidKar = false; while (true) { $kar = $this->ensureZarinKarForProduct($product, $retriedInvalidKar); if (empty($kar['ok']) || empty($kar['idKar'])) { $this->sync()->markFailed($zarin, $kar ?? null, $kar['message'] ?? 'کد کار زرین برای محصول یافت نشد'); return false; } $idKar = (int) $kar['idKar']; $http = $this->zarinRequest([ 'kind' => 'sabtSanadTedadi', 'idHesab' => $customer->moshtari_code, 'idKar' => $idKar, 'fi' => $fi, 'tedad' => $tedad, 'parDar' => $parDar, 'sharh' => $sharh, 'uid' => $uid, ]); if (!$http) { $this->sync()->markFailed($zarin, null, 'تنظیمات زرین کامل نیست'); return 'تنظیمات زرین کامل نیست. zarin_api یا zarin_token تنظیم نشده است.'; } [$response, $httpError] = $this->zarinDecodeResponse($http); $diag = 'idKar=' . $idKar . ' noeKar=' . $this->zarinCountNoeKar() . ' goroheKar=' . $this->zarinDefaultGoroheKar() . ' product=' . ($product->title ?? '') . ' source=' . ($kar['source'] ?? ''); if ( !$retriedInvalidKar && $this->isInvalidZarinKarError($response, $httpError) ) { Log::warning('zarin sabtSanadTedadi invalid idKar, recreating', [ 'diag' => $diag, 'response' => $response, 'httpError' => $httpError, ]); $this->clearProductZarinKar($product); $product->refresh(); $retriedInvalidKar = true; continue; } if ($httpError) { $this->sync()->markFailed( $zarin, $response ?? $httpError, $httpError . ' | ' . $diag ); return false; } $flat = $this->extractResponseRow(is_array($response) ? $response : null); if ($this->sync()->responseOk(is_array($response) ? $response : []) || $this->sync()->responseOk($flat)) { $this->sync()->markSuccess($zarin, is_array($response) ? $response : $flat, $data); return $data; } if ( !$retriedInvalidKar && $this->isInvalidZarinKarError($response) ) { Log::warning('zarin sabtSanadTedadi invalid idKar (HTTP 200), recreating', [ 'diag' => $diag, 'response' => $response, ]); $this->clearProductZarinKar($product); $product->refresh(); $retriedInvalidKar = true; continue; } $this->sync()->markFailed( $zarin, $response, 'ثبت سند تعدادی در زرین ناموفق بود | ' . $diag ); return false; } } /** * سند وزنی زرین (آبشده طلا/نقره). */ private function postZarinWeightDoc( Zarin $zarin, array $data, Customer $customer, float $meghdar, string $korf, float $fi, string $sharh, int $jensFelez, string $uid ) { if ($fi <= 0) { $fi = 1; } $http = $this->zarinRequest([ 'kind' => 'pooli', 'radif' => 1, 'korf' => $korf, 'poolorvazn' => 'vazn', 'meghdar' => $meghdar, 'fi' => $fi, 'hesab' => $customer->moshtari_code, 'sharh' => $sharh, 'ayar' => $jensFelez === 2 ? '' : '750', 'movaghat' => '', 'idFactor' => '', 'uid' => $uid, ]); if (!$http) { $this->sync()->markFailed($zarin, null, 'تنظیمات زرین کامل نیست'); return 'تنظیمات زرین کامل نیست. zarin_api یا zarin_token تنظیم نشده است.'; } [$response, $httpError] = $this->zarinDecodeResponse($http); if ($httpError) { $this->sync()->markFailed($zarin, $response ?? $httpError, $httpError); return false; } $flat = $this->extractResponseRow(is_array($response) ? $response : null); if ($this->sync()->responseOk(is_array($response) ? $response : []) || $this->sync()->responseOk($flat)) { $this->sync()->markSuccess($zarin, is_array($response) ? $response : $flat, $data); return $data; } $this->sync()->markFailed($zarin, $response, 'ثبت سند وزنی در زرین ناموفق بود'); return false; } public function createCustomerInZarin($customer_id) { $customer = Customer::findOrFail($customer_id); if ($customer->moshtari_code != null) { return $customer->moshtari_code; } $lock = Cache::lock('zarin:register-customer:' . $customer->id, 30); if (!$lock->get()) { $customer->refresh(); if ($customer->moshtari_code != null) { return $customer->moshtari_code; } return 'ثبت مشتری در زرین در حال انجام است'; } try { $customer->refresh(); if ($customer->moshtari_code != null) { return $customer->moshtari_code; } $name = $customer->fname ?: ($customer->name ?? ('مشتری ' . $customer->id)); $family = $customer->lname ?: ''; $mobile = $customer->mobile ?? ''; $params = [ 'kind' => 'addhesab', 'name' => $name, 'family' => $family, 'tel' => $mobile, 'mobile' => $mobile, 'address' => trim(($customer->province ?? '') . ' ' . ($customer->city ?? '')) ?: 'ثبت نشده', 'codePosti' => '0000000000', 'nameG' => $this->zarinCustomerGroupName(), 'gender' => in_array($customer->gender, ['اقای', 'آقای'], true) ? 'm' : ($customer->gender == 'خانم' ? 'f' : ''), 'uid' => $this->zarinUid('40', $customer->id), ]; try { $http = $this->zarinRequest($params); if (!$http) { return 'تنظیمات زرین کامل نیست. zarin_api یا zarin_token تنظیم نشده است.'; } [$response, $httpError] = $this->zarinDecodeResponse($http); if ($httpError) { return $httpError; } $data = $this->extractResponseRow($response); $idHesab = $data['idHesab'] ?? null; if (!empty($idHesab)) { $customer->moshtari_code = $idHesab; $customer->save(); // int برگردان تا با پیام خطای string اشتباه نشود return is_numeric($idHesab) ? (0 + $idHesab) : $idHesab; } $detail = $data['msg'] ?? $data['message'] ?? $data['detail'] ?? null; return $detail ? (string) $detail : 'zarin api error!!!'; } catch (\Exception $e) { return $e->getMessage(); } } finally { $lock->release(); } } public function healthCheck() { $http = $this->zarinRequest([ 'kind' => 'version', ]); [$json, $error] = $this->zarinDecodeResponse($http); if ($error) { return response()->json([ 'ok' => false, 'message' => $error, 'body' => $json, ], 422); } return response()->json([ 'ok' => true, 'version' => $json['version'] ?? null, 'message' => $json['message'] ?? 'OK', 'raw' => $json, ]); } }