diff --git a/app/Http/Controllers/Admin/ZarinController.php b/app/Http/Controllers/Admin/ZarinController.php index 4f2b83f..f9ec2ff 100644 --- a/app/Http/Controllers/Admin/ZarinController.php +++ b/app/Http/Controllers/Admin/ZarinController.php @@ -274,20 +274,20 @@ class ZarinController extends Controller * پیدا کردن کار/کالای محصول در زرین. * اول infokarbarcodi (اگر روی آن سرور فعال باشد)؛ وگرنه infosakhte (لیست کامل). */ - private function findZarinKarForProduct(Product $product, $goroheKar = null) + private function findZarinKarForProduct(Product $product, $goroheKar = null, bool $exactOnly = false) { $goroheKar = $goroheKar ?: $this->zarinDefaultGoroheKar(); $productName = $this->normalizeZarinName($product->title); - $fromBarcode = $this->findZarinKarInInfokarbarcodi($productName, $goroheKar); + $fromBarcode = $this->findZarinKarInInfokarbarcodi($productName, $goroheKar, $exactOnly); if ($fromBarcode) { return $fromBarcode; } - return $this->findZarinKarInInfosakhte($productName, $goroheKar); + return $this->findZarinKarInInfosakhte($productName, $goroheKar, $exactOnly); } - private function matchZarinKarRow(string $productName, array $row): ?array + private function matchZarinKarRow(string $productName, array $row, bool $exactOnly = false): ?array { $name = $row['nameKar'] ?? $row['nam_kar_sakhte'] @@ -300,26 +300,41 @@ class ZarinController extends Controller $rowName = $this->normalizeZarinName($name); $idKar = $this->extractUsableZarinKarId($row); - if (!$idKar) { + if (!$idKar || $rowName === '') { + return null; + } + + if ($rowName === $productName) { + return [ + 'idKar' => $idKar, + 'row' => $row, + 'match' => 'exact', + ]; + } + + if ($exactOnly) { return null; } if ( - $rowName === $productName || $this->containsText($rowName, $productName) || $this->containsText($productName, $rowName) ) { return [ 'idKar' => $idKar, 'row' => $row, + 'match' => 'fuzzy', ]; } return null; } - private function findZarinKarInInfokarbarcodi(string $productName, $goroheKar): ?array + 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', @@ -343,10 +358,15 @@ class ZarinController extends Controller if (!is_array($row)) { continue; } - $hit = $this->matchZarinKarRow($productName, $row); - if ($hit) { - return $hit; + $hit = $this->matchZarinKarRow($productName, $row, $exactOnly); + if (!$hit) { + continue; } + if (($hit['match'] ?? '') === 'exact') { + $exact = $hit; + break 2; + } + $fuzzy ??= $hit; } if (count($rows) < 250) { @@ -354,11 +374,11 @@ class ZarinController extends Controller } } - return null; + return $exact ?? ($exactOnly ? null : $fuzzy); } /** لیست کارهای ساخته — فیلد nam_kar_sakhte / id (تست‌شده روی API زرین) */ - private function findZarinKarInInfosakhte(string $productName, $goroheKar): ?array + private function findZarinKarInInfosakhte(string $productName, $goroheKar, bool $exactOnly = false): ?array { $response = $this->zarinRequest([ 'kind' => 'infosakhte', @@ -375,29 +395,42 @@ class ZarinController extends Controller $noeKar = (string) $this->zarinDefaultNoeKar(); $gorohe = (string) $goroheKar; - $preferred = null; - $fallback = null; + $exactPreferred = null; + $exactFallback = null; + $fuzzyPreferred = null; + $fuzzyFallback = null; foreach ($rows as $row) { if (!is_array($row)) { continue; } - $hit = $this->matchZarinKarRow($productName, $row); + $hit = $this->matchZarinKarRow($productName, $row, $exactOnly); if (!$hit) { continue; } - $idGp = (string) ($row['idGp'] ?? $row['idgp'] ?? ''); - if ($idGp !== '' && ($idGp === $gorohe || $idGp === $noeKar)) { - $preferred = $hit; + $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 ($fallback === null) { - $fallback = $hit; + if ($isExact) { + $exactFallback ??= $hit; + } elseif ($inGroup) { + $fuzzyPreferred ??= $hit; + } else { + $fuzzyFallback ??= $hit; } } - return $preferred ?? $fallback; + if ($exactOnly) { + return $exactPreferred ?? $exactFallback; + } + + return $exactPreferred ?? $exactFallback ?? $fuzzyPreferred ?? $fuzzyFallback; } private function getZarinKarById($idKar) @@ -423,14 +456,11 @@ class ZarinController extends Controller continue; } - $rowId = $row['pkFelez'] - ?? $row['pkfelez'] - ?? $row['PKFelez'] - ?? $row['id'] - ?? $row['idKar'] + // infosakhte: کد کار = id (نه pkFelez) + $rowId = $row['idKar'] ?? $row['idkar'] - ?? $row['zarrinId'] - ?? $row['zarinId'] + ?? $row['IDKar'] + ?? $row['id'] ?? null; if ((string) $rowId === (string) $idKar) { return $row; @@ -440,19 +470,24 @@ class ZarinController extends Controller return null; } + /** + * کد کار قابل استفاده برای sabtSanadTedadi. + * infosakhte → فیلد id؛ addFelezsakhteModel → idKar. + * pkFelez هرگز idKar نیست. + */ private function extractUsableZarinKarId(array $row) { foreach ([ - 'zarrinId', - 'zarinId', 'idKar', 'idkar', 'IDKar', 'id', + 'zarrinId', + 'zarinId', 'code', 'Code', ] as $key) { - if (isset($row[$key]) && is_numeric($row[$key])) { + if (isset($row[$key]) && is_numeric($row[$key]) && (int) $row[$key] > 0) { return (int) $row[$key]; } } @@ -460,6 +495,36 @@ class ZarinController extends Controller 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 = [ @@ -493,11 +558,23 @@ class ZarinController extends Controller } $body = $response->json(); + $createdIdKar = $this->extractIdKarFromCreateResponse(is_array($body) ? $body : null); - $createdPkFelez = $body['data'][0]['pkFelez'] - ?? $body['data']['pkFelez'] - ?? $body['pkFelez'] - ?? 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()); @@ -512,16 +589,14 @@ class ZarinController extends Controller 'product' => $product->fresh(), 'zarin_row' => $foundAfterCreate['row'], 'zarin_response' => $body, - 'created_pkFelez' => $createdPkFelez, 'sent_params_success' => $params, ]; } return [ 'ok' => false, - 'message' => 'زرین کالا را ساخت، اما idKar قابل استفاده از infosakhte/infokarbarcodi پیدا نشد.', + 'message' => 'زرین کالا را ساخت، اما idKar قابل استفاده برنگشت.', 'status_code' => 422, - 'created_pkFelez' => $createdPkFelez, 'zarin_response' => $body, 'sent_params' => $params, ]; @@ -530,48 +605,93 @@ class ZarinController extends Controller private function ensureZarinKarForProduct(Product $product, $forceCreate = false) { $currentIdKar = $product->finance_p_name; + $productName = $this->normalizeZarinName($product->title); if (!$forceCreate && !empty($currentIdKar) && is_numeric($currentIdKar)) { $existingRow = $this->getZarinKarById($currentIdKar); if ($existingRow) { - $usableIdKar = $this->extractUsableZarinKarId($existingRow) ?: (int) $currentIdKar; + $rowName = $this->normalizeZarinName( + $existingRow['nameKar'] + ?? $existingRow['nam_kar_sakhte'] + ?? $existingRow['nam'] + ?? $existingRow['name'] + ?? $existingRow['namKar'] + ?? $existingRow['title'] + ?? '' + ); + // اگر idKar موجود مال کالای دیگری است (مثلاً شمش برای گوی) قبول نکن + if ($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(); + 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, + ]; } - - return [ - 'ok' => true, - 'source' => 'existing_verified', - 'idKar' => (int) $usableIdKar, - 'product' => $product->fresh(), - 'zarin_row' => $existingRow, - ]; + } else { + // finance_p_name کهنه / اشتباه — پاک کن تا دوباره پیدا یا ساخته شود + $this->clearProductZarinKar($product); + $product->refresh(); } } - if (!$forceCreate) { - $found = $this->findZarinKarForProduct($product, $this->zarinDefaultGoroheKar()); + // بعد از خطای «کد کار اشتباه»: فقط تطبیق دقیق نام، نه فازی + $exactOnly = (bool) $forceCreate; + $found = $this->findZarinKarForProduct($product, $this->zarinDefaultGoroheKar(), $exactOnly); - if ($found && !empty($found['idKar']) && is_numeric($found['idKar'])) { - $product->finance_p_name = (string) $found['idKar']; - $product->save(); + 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'], - ]; - } + return [ + 'ok' => true, + 'source' => $exactOnly ? 'found_exact_after_invalid' : 'found_by_name', + 'idKar' => (int) $found['idKar'], + 'product' => $product->fresh(), + 'zarin_row' => $found['row'], + 'match' => $found['match'] ?? 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) { $query = Zarin::with('customer')->orderByDesc('id'); @@ -1141,10 +1261,17 @@ class ZarinController extends Controller 'amount' => $syncAmount, 'order_id' => $orderId, 'date' => $data['pay_at'] ?? Carbon::now(), - 'type' => $mapper->metalLabel($product), + '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, + ]); + } try { if ($mapper->isCountDoc($product)) { @@ -1465,11 +1592,18 @@ class ZarinController extends Controller 'desc' => $sharh, 'amount' => $syncAmount, 'date' => Carbon::now(), - 'type' => $mapper->metalLabel($product), + '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)]); @@ -1525,6 +1659,7 @@ class ZarinController extends Controller /** * سند تعدادی زرین (سکه / شمش / گوی / مکعب). + * در زرین همه با sabtSanadTedadi می‌روند؛ تفکیک فقط با idKar جداگانهٔ هر محصول است. */ private function postZarinCountDoc( Zarin $zarin, @@ -1542,46 +1677,71 @@ class ZarinController extends Controller return false; } - $kar = $this->ensureZarinKarForProduct($product); - if (empty($kar['ok']) || empty($kar['idKar'])) { - $this->sync()->markFailed($zarin, $kar ?? null, $kar['message'] ?? 'کد کار زرین برای محصول یافت نشد'); - return false; - } - if ($fi <= 0) { $fi = 1; } - $http = $this->zarinRequest([ - 'kind' => 'sabtSanadTedadi', - 'idHesab' => $customer->moshtari_code, - 'idKar' => (int) $kar['idKar'], - 'fi' => $fi, - 'tedad' => $tedad, - 'parDar' => $parDar, - 'sharh' => $sharh, - 'uid' => $uid, - ]); + $retriedInvalidKar = false; - if (!$http) { - $this->sync()->markFailed($zarin, null, 'تنظیمات زرین کامل نیست'); - return 'تنظیمات زرین کامل نیست. zarin_api یا zarin_token تنظیم نشده است.'; - } + while (true) { + $kar = $this->ensureZarinKarForProduct($product, $retriedInvalidKar); + if (empty($kar['ok']) || empty($kar['idKar'])) { + $this->sync()->markFailed($zarin, $kar ?? null, $kar['message'] ?? 'کد کار زرین برای محصول یافت نشد'); + return false; + } - [$response, $httpError] = $this->zarinDecodeResponse($http); - if ($httpError) { - $this->sync()->markFailed($zarin, $response ?? $httpError, $httpError); + $http = $this->zarinRequest([ + 'kind' => 'sabtSanadTedadi', + 'idHesab' => $customer->moshtari_code, + 'idKar' => (int) $kar['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); + + if ( + !$retriedInvalidKar + && $this->isInvalidZarinKarError($response, $httpError) + ) { + $this->clearProductZarinKar($product); + $product->refresh(); + $retriedInvalidKar = true; + continue; + } + + 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; + } + + if ( + !$retriedInvalidKar + && $this->isInvalidZarinKarError($response) + ) { + $this->clearProductZarinKar($product); + $product->refresh(); + $retriedInvalidKar = true; + continue; + } + + $this->sync()->markFailed($zarin, $response, 'ثبت سند تعدادی در زرین ناموفق بود'); 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; } /** diff --git a/app/Services/AccountingProductDocMapper.php b/app/Services/AccountingProductDocMapper.php index 9831f59..8ce5e51 100644 --- a/app/Services/AccountingProductDocMapper.php +++ b/app/Services/AccountingProductDocMapper.php @@ -194,6 +194,20 @@ class AccountingProductDocMapper return 'طلا'; } + /** + * برچسب ستون type جدول sync زرین: تعدادی → سکه/شمش/کارساخته؛ وزنی → طلا/نقره. + * + * @param Product|array|null $product + */ + public function zarinSyncTypeLabel($product): string + { + if ($this->isCountDoc($product)) { + return $this->kindLabel($this->resolveKind($product)); + } + + return $this->metalLabel($product); + } + /** * Resolve Product model from storeTrade/walletGold payload. */