fix some bugs from accounting 4

This commit is contained in:
2026-07-28 18:40:06 +03:30
parent d81cea3d4f
commit 5122d7e2b7
2 changed files with 271 additions and 97 deletions
+257 -97
View File
@@ -274,20 +274,20 @@ class ZarinController extends Controller
* پیدا کردن کار/کالای محصول در زرین. * پیدا کردن کار/کالای محصول در زرین.
* اول infokarbarcodi (اگر روی آن سرور فعال باشد)؛ وگرنه infosakhte (لیست کامل). * اول infokarbarcodi (اگر روی آن سرور فعال باشد)؛ وگرنه infosakhte (لیست کامل).
*/ */
private function findZarinKarForProduct(Product $product, $goroheKar = null) private function findZarinKarForProduct(Product $product, $goroheKar = null, bool $exactOnly = false)
{ {
$goroheKar = $goroheKar ?: $this->zarinDefaultGoroheKar(); $goroheKar = $goroheKar ?: $this->zarinDefaultGoroheKar();
$productName = $this->normalizeZarinName($product->title); $productName = $this->normalizeZarinName($product->title);
$fromBarcode = $this->findZarinKarInInfokarbarcodi($productName, $goroheKar); $fromBarcode = $this->findZarinKarInInfokarbarcodi($productName, $goroheKar, $exactOnly);
if ($fromBarcode) { if ($fromBarcode) {
return $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'] $name = $row['nameKar']
?? $row['nam_kar_sakhte'] ?? $row['nam_kar_sakhte']
@@ -300,26 +300,41 @@ class ZarinController extends Controller
$rowName = $this->normalizeZarinName($name); $rowName = $this->normalizeZarinName($name);
$idKar = $this->extractUsableZarinKarId($row); $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; return null;
} }
if ( if (
$rowName === $productName ||
$this->containsText($rowName, $productName) || $this->containsText($rowName, $productName) ||
$this->containsText($productName, $rowName) $this->containsText($productName, $rowName)
) { ) {
return [ return [
'idKar' => $idKar, 'idKar' => $idKar,
'row' => $row, 'row' => $row,
'match' => 'fuzzy',
]; ];
} }
return null; 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++) { for ($page = 1; $page <= 10; $page++) {
$response = $this->zarinRequest([ $response = $this->zarinRequest([
'kind' => 'infokarbarcodi', 'kind' => 'infokarbarcodi',
@@ -343,10 +358,15 @@ class ZarinController extends Controller
if (!is_array($row)) { if (!is_array($row)) {
continue; continue;
} }
$hit = $this->matchZarinKarRow($productName, $row); $hit = $this->matchZarinKarRow($productName, $row, $exactOnly);
if ($hit) { if (!$hit) {
return $hit; continue;
} }
if (($hit['match'] ?? '') === 'exact') {
$exact = $hit;
break 2;
}
$fuzzy ??= $hit;
} }
if (count($rows) < 250) { if (count($rows) < 250) {
@@ -354,11 +374,11 @@ class ZarinController extends Controller
} }
} }
return null; return $exact ?? ($exactOnly ? null : $fuzzy);
} }
/** لیست کارهای ساخته — فیلد nam_kar_sakhte / id (تست‌شده روی API زرین) */ /** لیست کارهای ساخته — فیلد 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([ $response = $this->zarinRequest([
'kind' => 'infosakhte', 'kind' => 'infosakhte',
@@ -375,29 +395,42 @@ class ZarinController extends Controller
$noeKar = (string) $this->zarinDefaultNoeKar(); $noeKar = (string) $this->zarinDefaultNoeKar();
$gorohe = (string) $goroheKar; $gorohe = (string) $goroheKar;
$preferred = null; $exactPreferred = null;
$fallback = null; $exactFallback = null;
$fuzzyPreferred = null;
$fuzzyFallback = null;
foreach ($rows as $row) { foreach ($rows as $row) {
if (!is_array($row)) { if (!is_array($row)) {
continue; continue;
} }
$hit = $this->matchZarinKarRow($productName, $row); $hit = $this->matchZarinKarRow($productName, $row, $exactOnly);
if (!$hit) { if (!$hit) {
continue; continue;
} }
$idGp = (string) ($row['idGp'] ?? $row['idgp'] ?? ''); $idGp = (string) ($row['idGpFelez'] ?? $row['idgpfelez'] ?? $row['gorooheKar'] ?? '');
if ($idGp !== '' && ($idGp === $gorohe || $idGp === $noeKar)) { $inGroup = $idGp !== '' && ($idGp === $gorohe || $idGp === $noeKar);
$preferred = $hit; $isExact = ($hit['match'] ?? '') === 'exact';
if ($isExact && $inGroup) {
$exactPreferred = $hit;
break; break;
} }
if ($fallback === null) { if ($isExact) {
$fallback = $hit; $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) private function getZarinKarById($idKar)
@@ -423,14 +456,11 @@ class ZarinController extends Controller
continue; continue;
} }
$rowId = $row['pkFelez'] // infosakhte: کد کار = id (نه pkFelez)
?? $row['pkfelez'] $rowId = $row['idKar']
?? $row['PKFelez']
?? $row['id']
?? $row['idKar']
?? $row['idkar'] ?? $row['idkar']
?? $row['zarrinId'] ?? $row['IDKar']
?? $row['zarinId'] ?? $row['id']
?? null; ?? null;
if ((string) $rowId === (string) $idKar) { if ((string) $rowId === (string) $idKar) {
return $row; return $row;
@@ -440,19 +470,24 @@ class ZarinController extends Controller
return null; return null;
} }
/**
* کد کار قابل استفاده برای sabtSanadTedadi.
* infosakhte → فیلد id؛ addFelezsakhteModel → idKar.
* pkFelez هرگز idKar نیست.
*/
private function extractUsableZarinKarId(array $row) private function extractUsableZarinKarId(array $row)
{ {
foreach ([ foreach ([
'zarrinId',
'zarinId',
'idKar', 'idKar',
'idkar', 'idkar',
'IDKar', 'IDKar',
'id', 'id',
'zarrinId',
'zarinId',
'code', 'code',
'Code', 'Code',
] as $key) { ] 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]; return (int) $row[$key];
} }
} }
@@ -460,6 +495,36 @@ class ZarinController extends Controller
return null; 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) private function createZarinKarForProduct(Product $product)
{ {
$params = [ $params = [
@@ -493,11 +558,23 @@ class ZarinController extends Controller
} }
$body = $response->json(); $body = $response->json();
$createdIdKar = $this->extractIdKarFromCreateResponse(is_array($body) ? $body : null);
$createdPkFelez = $body['data'][0]['pkFelez'] // اولویت با idKar برگشتی از ساخت — جستجوی فازی نام می‌تواند کالای اشتباه بچسباند
?? $body['data']['pkFelez'] if ($createdIdKar) {
?? $body['pkFelez'] $product->finance_p_name = (string) $createdIdKar;
?? null; $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()); $foundAfterCreate = $this->findZarinKarForProduct($product, $this->zarinDefaultGoroheKar());
@@ -512,16 +589,14 @@ class ZarinController extends Controller
'product' => $product->fresh(), 'product' => $product->fresh(),
'zarin_row' => $foundAfterCreate['row'], 'zarin_row' => $foundAfterCreate['row'],
'zarin_response' => $body, 'zarin_response' => $body,
'created_pkFelez' => $createdPkFelez,
'sent_params_success' => $params, 'sent_params_success' => $params,
]; ];
} }
return [ return [
'ok' => false, 'ok' => false,
'message' => 'زرین کالا را ساخت، اما idKar قابل استفاده از infosakhte/infokarbarcodi پیدا نشد.', 'message' => 'زرین کالا را ساخت، اما idKar قابل استفاده برنگشت.',
'status_code' => 422, 'status_code' => 422,
'created_pkFelez' => $createdPkFelez,
'zarin_response' => $body, 'zarin_response' => $body,
'sent_params' => $params, 'sent_params' => $params,
]; ];
@@ -530,48 +605,93 @@ class ZarinController extends Controller
private function ensureZarinKarForProduct(Product $product, $forceCreate = false) private function ensureZarinKarForProduct(Product $product, $forceCreate = false)
{ {
$currentIdKar = $product->finance_p_name; $currentIdKar = $product->finance_p_name;
$productName = $this->normalizeZarinName($product->title);
if (!$forceCreate && !empty($currentIdKar) && is_numeric($currentIdKar)) { if (!$forceCreate && !empty($currentIdKar) && is_numeric($currentIdKar)) {
$existingRow = $this->getZarinKarById($currentIdKar); $existingRow = $this->getZarinKarById($currentIdKar);
if ($existingRow) { 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) { if ((string) $product->finance_p_name !== (string) $usableIdKar) {
$product->finance_p_name = (string) $usableIdKar; $product->finance_p_name = (string) $usableIdKar;
$product->save(); $product->save();
}
return [
'ok' => true,
'source' => 'existing_verified',
'idKar' => (int) $usableIdKar,
'product' => $product->fresh(),
'zarin_row' => $existingRow,
];
} }
} else {
return [ // finance_p_name کهنه / اشتباه — پاک کن تا دوباره پیدا یا ساخته شود
'ok' => true, $this->clearProductZarinKar($product);
'source' => 'existing_verified', $product->refresh();
'idKar' => (int) $usableIdKar,
'product' => $product->fresh(),
'zarin_row' => $existingRow,
];
} }
} }
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'])) { if ($found && !empty($found['idKar']) && is_numeric($found['idKar'])) {
$product->finance_p_name = (string) $found['idKar']; $product->finance_p_name = (string) $found['idKar'];
$product->save(); $product->save();
return [ return [
'ok' => true, 'ok' => true,
'source' => 'found_by_name', 'source' => $exactOnly ? 'found_exact_after_invalid' : 'found_by_name',
'idKar' => (int) $found['idKar'], 'idKar' => (int) $found['idKar'],
'product' => $product->fresh(), 'product' => $product->fresh(),
'zarin_row' => $found['row'], 'zarin_row' => $found['row'],
]; 'match' => $found['match'] ?? null,
} ];
} }
return $this->createZarinKarForProduct($product); 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) public function table(Request $request)
{ {
$query = Zarin::with('customer')->orderByDesc('id'); $query = Zarin::with('customer')->orderByDesc('id');
@@ -1141,10 +1261,17 @@ class ZarinController extends Controller
'amount' => $syncAmount, 'amount' => $syncAmount,
'order_id' => $orderId, 'order_id' => $orderId,
'date' => $data['pay_at'] ?? Carbon::now(), 'date' => $data['pay_at'] ?? Carbon::now(),
'type' => $mapper->metalLabel($product), 'type' => $mapper->zarinSyncTypeLabel($product),
'created_at' => Carbon::now(), 'created_at' => Carbon::now(),
'data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), 'data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
]); ]);
if ($existing) {
$existing->update([
'type' => $mapper->zarinSyncTypeLabel($product),
'desc' => $sharh,
'amount' => $syncAmount,
]);
}
try { try {
if ($mapper->isCountDoc($product)) { if ($mapper->isCountDoc($product)) {
@@ -1465,11 +1592,18 @@ class ZarinController extends Controller
'desc' => $sharh, 'desc' => $sharh,
'amount' => $syncAmount, 'amount' => $syncAmount,
'date' => Carbon::now(), 'date' => Carbon::now(),
'type' => $mapper->metalLabel($product), 'type' => $mapper->zarinSyncTypeLabel($product),
'created_at' => Carbon::now(), 'created_at' => Carbon::now(),
'withdraw_id' => $data['withdraw_id'] ?? null, 'withdraw_id' => $data['withdraw_id'] ?? null,
'data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), '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)) { if ($existing && empty($existing->data)) {
$existing->update(['data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)]); $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( private function postZarinCountDoc(
Zarin $zarin, Zarin $zarin,
@@ -1542,46 +1677,71 @@ class ZarinController extends Controller
return false; 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) { if ($fi <= 0) {
$fi = 1; $fi = 1;
} }
$http = $this->zarinRequest([ $retriedInvalidKar = false;
'kind' => 'sabtSanadTedadi',
'idHesab' => $customer->moshtari_code,
'idKar' => (int) $kar['idKar'],
'fi' => $fi,
'tedad' => $tedad,
'parDar' => $parDar,
'sharh' => $sharh,
'uid' => $uid,
]);
if (!$http) { while (true) {
$this->sync()->markFailed($zarin, null, 'تنظیمات زرین کامل نیست'); $kar = $this->ensureZarinKarForProduct($product, $retriedInvalidKar);
return 'تنظیمات زرین کامل نیست. zarin_api یا zarin_token تنظیم نشده است.'; if (empty($kar['ok']) || empty($kar['idKar'])) {
} $this->sync()->markFailed($zarin, $kar ?? null, $kar['message'] ?? 'کد کار زرین برای محصول یافت نشد');
return false;
}
[$response, $httpError] = $this->zarinDecodeResponse($http); $http = $this->zarinRequest([
if ($httpError) { 'kind' => 'sabtSanadTedadi',
$this->sync()->markFailed($zarin, $response ?? $httpError, $httpError); '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; 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;
} }
/** /**
@@ -194,6 +194,20 @@ class AccountingProductDocMapper
return 'طلا'; 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. * Resolve Product model from storeTrade/walletGold payload.
*/ */