Files
boursecu_server/app/Http/Controllers/Admin/ZarinController.php
T
mohmdsdgandCursor f6118689e1 fix(zarin): post rial deposits without gold conversion and restore shop-side trade korf
Wallet docs were sending mazaneh as fi so Zarin turned cash deposits into خرید فی rows; customer buy/sell used the inverted korf mapping.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-31 18:34:40 +03:30

2629 lines
92 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Controllers\Site\ReportController;
use App\Models\Customer;
use App\Models\Order;
use App\Models\Product;
use App\Models\Zarin;
use App\Services\AccountingProductDocMapper;
use App\Services\ConfigService;
use App\Services\ZarinInboundWalletService;
use App\Services\ZarinSyncService;
use Carbon\Carbon;
use Illuminate\Contracts\Cache\LockTimeoutException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
class ZarinController extends Controller
{
protected function sync(): ZarinSyncService
{
return app(ZarinSyncService::class);
}
private function lastPriceData(): array
{
$response = app(ReportController::class)->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<int, 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);
$maps = $this->panelAccountingMaps();
$walletService = app(ZarinInboundWalletService::class);
$zarinOnlyList = [];
$phoneMatches = [];
$phoneConflicts = [];
$withPhone = 0;
$withoutPhone = 0;
foreach ($zarinCustomers as $item) {
if (!is_array($item)) {
continue;
}
if (count($debugSample) < 3) {
$debugSample[] = $item;
}
$code = $this->extractZarinHesabId($item) ?? ($item['idHesab'] ?? $item['Code'] ?? $item['code'] ?? $item['id'] ?? null);
$codeKey = $this->accountingCodeKey($code);
$phone = $this->extractZarinPhone($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' => $codeKey !== null ? (int) $codeKey : null,
'name' => $this->extractZarinName($item),
'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);
$zarinOnlyList[] = [
'code' => (int) $codeKey,
'name' => $this->extractZarinName($item),
'tel' => $phone,
'gid' => $item['GID'] ?? null,
'group' => $this->extractZarinGroup($item),
'has_phone' => $hasPhone,
'gold' => $mande['gold'] ?? null,
'cash' => $mande['cash'] ?? null,
];
}
$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_with_phone_count' => $include ? ($withPhone ?? 0) : null,
'zarin_without_phone_count' => $include ? ($withoutPhone ?? 0) : null,
'zarin_importable_count' => $include
? count(array_filter($zarinOnlyList ?? [], fn ($r) => ! empty($r['has_phone'])))
: null,
'phone_match_count' => $include ? count($phoneMatches ?? []) : null,
'phone_conflict_count' => $include ? count($phoneConflicts ?? []) : null,
'phone_matches' => $phoneMatches ?? [],
'phone_conflicts' => $phoneConflicts ?? [],
'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 '';
}
/** کد حساب زرین از پاسخ addhesab / listhesabha — OK و Code هم مثل idHesab معتبرند */
private function extractZarinHesabId(array $row): ?int
{
foreach (['idHesab', 'idhesab', 'Code', 'code', 'OK', 'id'] as $key) {
if (! array_key_exists($key, $row)) {
continue;
}
$value = $row[$key];
if ($value === null || $value === '' || $value === false || $value === true) {
continue;
}
if (is_numeric($value) && (int) $value > 0) {
return (int) $value;
}
}
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<string, Customer>, by_mobile: array<string, Customer>}
*/
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];
}
/** حساب از قبل ساخته‌شده در زرین — با موبایل، تا addhesab دوم نسازیم */
private function findExistingZarinHesabId(?string $mobile): ?int
{
$want = Customer::normalizeMobile($mobile ?? '');
if ($want === null) {
return null;
}
$fetched = $this->fetchAllZarinCustomers();
$rows = $fetched['rows'] ?? [];
if ($rows === []) {
return null;
}
$groupFilter = $this->zarinCustomerGroupName();
$sameGroup = null;
$any = null;
foreach ($rows as $item) {
if (! is_array($item)) {
continue;
}
$phone = $this->extractZarinPhone($item);
$got = Customer::normalizeMobile($phone);
if ($got === null || $got !== $want) {
continue;
}
$id = $this->extractZarinHesabId($item);
if ($id === null) {
continue;
}
$g = $this->extractZarinGroup($item);
if ($groupFilter !== '' && $g !== '' && $this->zarinGroupNamesMatch($g, $groupFilter)) {
$sameGroup = $id;
break;
}
$any ??= $id;
}
return $sameGroup ?? $any;
}
private function bindCustomerZarinHesab(Customer $customer, int $idHesab): int
{
$customer->moshtari_code = $idHesab;
$customer->save();
return $idHesab;
}
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 = [];
/** @var list<Customer> $importedForWallet */
$importedForWallet = [];
foreach ($validated['customers'] as $item) {
$mobile = Customer::normalizeMobile($item['tel'] ?? '') ?? '';
$fullName = trim($item['name']);
$code = (int) $item['code'];
if ($fullName === '') {
$fullName = 'مشتری زرین ' . $code;
}
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 = new Customer();
$customer->fname = $fullName;
$customer->lname = '';
$customer->name = $fullName;
$customer->mobile = $mobile;
$customer->active = true;
$customer->moshtari_code = $item['code'];
$customer->referral_id = $referralId;
$customer->type = 2;
$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(ZarinInboundWalletService::class)->syncAfterImport($importedForWallet);
} catch (\Throwable $e) {
Log::warning('Zarin import wallet sync batch failed', [
'count' => count($importedForWallet),
'error' => $e->getMessage(),
]);
}
}
return response()->json([
'ok' => true,
'success' => $success,
'failed' => $failed,
'errors' => $errors,
]);
}
public function linkMatchingPhones(): \Illuminate\Http\JsonResponse
{
$fetched = $this->fetchAllZarinCustomers();
$rows = $fetched['rows'] ?? [];
$groupFilter = $this->zarinCustomerGroupName();
if ($groupFilter !== '' && $rows !== []) {
$rows = array_values(array_filter($rows, function ($item) use ($groupFilter) {
if (! is_array($item)) {
return false;
}
$g = $this->extractZarinGroup($item);
return $g !== '' && $this->zarinGroupNamesMatch($g, $groupFilter);
}));
}
$maps = $this->panelAccountingMaps();
$linked = [];
$conflicts = [];
$toSync = [];
foreach ($rows as $item) {
if (! is_array($item)) {
continue;
}
$code = $this->extractZarinHesabId($item);
$codeKey = $this->accountingCodeKey($code);
if ($codeKey === null) {
continue;
}
$phone = $this->extractZarinPhone($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' => $this->extractZarinName($item),
'tel' => $phone,
'panel_id' => (int) $panel->id,
'panel_name' => $panel->name,
'error' => 'این کد زرین روی مشتری دیگری در پنل است',
];
continue;
}
if ($panelCodeKey === $codeKey) {
continue;
}
if ($panelCodeKey) {
$conflicts[] = [
'code' => (int) $codeKey,
'name' => $this->extractZarinName($item),
'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(ZarinInboundWalletService::class)->syncAfterImport($toSync);
} catch (\Throwable $e) {
Log::warning('Zarin 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)->isZarinAccounting()) {
return response()->json(['ok' => false, 'message' => 'حسابداری زرین فعال نیست'], 422);
}
$customer = Customer::findOrFail($customerId);
$code = (int) $request->input('code', $customer->moshtari_code);
$result = app(ZarinInboundWalletService::class)->previewForCustomer($customer, $code);
return response()->json([
'ok' => (bool) ($result['ok'] ?? false),
'needs_decision' => (bool) ($result['needs_decision'] ?? false),
'message' => $result['message'] ?? '',
'result' => $result,
]);
}
public function syncCustomerWallet(Request $request, $customerId)
{
if (! app(ConfigService::class)->isZarinAccounting()) {
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);
}
$overwrite = $request->boolean('overwrite');
$keep = $request->boolean('keep');
$alwaysAsk = $request->boolean('always_ask');
$askIfConflict = $request->boolean('ask_if_conflict', ! $overwrite && ! $keep && ! $alwaysAsk);
$result = app(ZarinInboundWalletService::class)->syncWalletFromZarin(
$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,
]);
}
public function syncAllWallets(Request $request)
{
if (! app(ConfigService::class)->isZarinAccounting()) {
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(ZarinInboundWalletService::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->syncWalletFromZarin(
$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['zarin_gold'] ?? $result['tahesab_gold'] ?? null,
'tahesab_cash' => $result['zarin_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,
]);
}
/**
* حذف مشتریانِ خرابِ واردشده از زرین (نام خالی) برای پاک‌سازی 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;
}
/**
* جهت سند معامله در زرین — از دید فروشگاه (نه مشتری).
* korf=k در عمل «خرید فی» می‌سازد: طلا بد + ریال بس (خرید فروشگاه از مشتری).
* خرید مشتری = فروش فروشگاه → f / parDar −1 : طلا بس + ریال بد
* فروش مشتری = خرید فروشگاه → k / parDar +1 : طلا بد + ریال بس
*
* @return array{korf: string, parDar: int}
*/
private function zarinShopSidesForCustomerTrade(bool $customerBuys): array
{
return $customerBuys
? ['korf' => 'f', 'parDar' => -1]
: ['korf' => 'k', 'parDar' => 1];
}
/** شرح سند ریالی زرین — همیشه با واریز/برداشت شروع شود تا «خرید فی» دیده نشود */
private function zarinRialWalletSharh(bool $isDeposit, ?string $note): string
{
$base = $isDeposit ? 'واریز' : 'برداشت';
$normalized = self::accountingWalletDesc($isDeposit, $note);
if ($normalized === '' || $normalized === $base) {
return $base;
}
if (str_starts_with($normalized, $base)) {
return $normalized;
}
return $base . ' — ' . $normalized;
}
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->hasValidMoshtariCode()) {
$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;
$sides = $this->zarinShopSidesForCustomerTrade($isBuy);
$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,
$sides['parDar'],
$fi,
$sharh,
$this->zarinUid($uidKind, $uidSource ?? $zarin->id)
);
} else {
$result = $this->postZarinWeightDoc(
$zarin,
$data,
$accountCustomer,
$vazn,
$sides['korf'],
(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->hasValidMoshtariCode()) {
$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 = $this->zarinRialWalletSharh($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)]);
}
$isWithdraw = (int) ($data['direction'] ?? 0) === 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' => '',
'hesab' => $customer->moshtari_code,
'sharh' => $sharh,
'ayar' => '',
'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->hasValidMoshtariCode()) {
$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->hasValidMoshtariCode()) {
$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->hasValidMoshtariCode()) {
return $customer->moshtari_code;
}
$lock = Cache::lock('zarin:register-customer:' . $customer->id, 90);
try {
$lock->block(25);
} catch (LockTimeoutException $e) {
$customer->refresh();
if ($customer->hasValidMoshtariCode()) {
return $customer->moshtari_code;
}
return 'ثبت مشتری در زرین در حال انجام است';
}
try {
$customer->refresh();
if ($customer->hasValidMoshtariCode()) {
return $customer->moshtari_code;
}
$existingId = $this->findExistingZarinHesabId($customer->mobile);
if ($existingId !== null) {
return $this->bindCustomerZarinHesab($customer, $existingId);
}
$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 = $this->extractZarinHesabId($data);
if ($idHesab !== null) {
return $this->bindCustomerZarinHesab($customer, $idHesab);
}
$existingId = $this->findExistingZarinHesabId($customer->mobile);
if ($existingId !== null) {
return $this->bindCustomerZarinHesab($customer, $existingId);
}
$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,
]);
}
}