Files
boursecu_server/app/Http/Controllers/Admin/ZarinController.php
T

2051 lines
70 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\ZarinSyncService;
use Carbon\Carbon;
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;
}
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);
if ($fromList) {
return $fromList;
}
$fromBarcode = $this->findZarinKarInInfokarbarcodi($productName, $goroheKar, $exactOnly);
if ($fromBarcode) {
return $fromBarcode;
}
return $this->findZarinKarInInfosakhte($productName, $goroheKar, $exactOnly);
}
/**
* کاتالوگ کارهای ساخته (لیست ۴۷ زرین) — id همین idKar سند تعدادی است.
*/
private function findZarinKarInListKarSakhte(string $productName, bool $exactOnly = false): ?array
{
$response = $this->zarinRequest([
'kind' => 'listKarSakhte',
], 30);
if (!$response || !$response->successful()) {
return null;
}
$rows = $this->zarinRowsFromBody($response->json());
if (empty($rows)) {
return null;
}
$exact = null;
$fuzzy = null;
foreach ($rows as $row) {
if (!is_array($row)) {
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',
];
break;
}
if (
!$exactOnly
&& (
$this->containsText($rowName, $productName)
|| $this->containsText($productName, $rowName)
)
) {
$fuzzy ??= [
'idKar' => $idKar,
'row' => $row,
'match' => 'fuzzy',
'source' => 'listKarSakhte',
];
}
}
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->zarinDefaultNoeKar(),
'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']
?? ''
);
// اگر 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();
}
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)
{
$query = Zarin::with('customer')->orderByDesc('id');
\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);
$panelMobiles = Customer::whereNull('deleted_at')
->whereNotNull('mobile')
->pluck('mobile')
->mapWithKeys(fn ($m) => [ltrim((string) $m, '0') => true])
->toArray();
$panelCodes = Customer::whereNull('deleted_at')
->whereNotNull('moshtari_code')
->pluck('moshtari_code')
->mapWithKeys(fn ($c) => [(string) $c => true])
->toArray();
$zarinOnlyList = [];
foreach ($zarinCustomers as $item) {
if (!is_array($item)) {
continue;
}
if (count($debugSample) < 3) {
$debugSample[] = $item;
}
$code = $item['idHesab'] ?? $item['Code'] ?? $item['code'] ?? $item['id'] ?? null;
if ($code !== null && isset($panelCodes[(string) $code])) {
continue;
}
$phone = $this->extractZarinPhone($item);
$tel = ltrim($phone, '0');
if ($tel && isset($panelMobiles[$tel])) {
continue;
}
$zarinOnlyList[] = [
'code' => $code,
'name' => $this->extractZarinName($item),
'tel' => $phone,
'gid' => $item['GID'] ?? null,
'group' => $this->extractZarinGroup($item),
];
}
$zarinOnlyCount = count($zarinOnlyList);
$zarinOnlyResult = [
'data' => array_values($zarinOnlyList),
'total' => $zarinOnlyCount,
'current_page' => 1,
'last_page' => 1,
];
}
$payload = [
'panel_total' => $panelTotal,
'panel_registered' => $panelRegistered,
'panel_only' => $panelOnly,
'zarin_total' => $zarinTotal,
'zarin_only_count' => $zarinOnlyCount,
'zarin_fetch_error' => $zarinFetchError,
'zarin_http_status' => $zarinHttpStatus,
'zarin_group_filter' => $zarinGroupFilter,
'include_zarin' => $include,
'include_zarin_param' => $request->query('include_zarin', $request->input('include_zarin')),
'panel_only_list' => $panelOnlyList,
'zarin_only_list' => $zarinOnlyResult,
];
if ($include && ($request->boolean('raw') || $request->input('raw'))) {
$payload['zarin_debug_sample'] = $debugSample;
}
return response()->json($payload)->header('Cache-Control', 'no-store, private');
}
private function extractZarinPhone(array $item): string
{
$candidates = ['mobile', 'Mobile', 'tell', 'Tell', 'tel', 'Tel', 'Phone', 'CellPhone', 'Cell', 'MobileNo', 'Telephone', 'Mobile1'];
foreach ($candidates as $key) {
if (isset($item[$key]) && trim((string) $item[$key]) !== '') {
return trim((string) $item[$key]);
}
}
foreach ($item as $k => $v) {
if ($v === null || trim((string) $v) === '') {
continue;
}
$lk = strtolower((string) $k);
if (str_contains($lk, 'mobile') || str_contains($lk, 'tel')
|| str_contains($lk, 'phone') || str_contains($lk, 'cell')) {
return trim((string) $v);
}
}
return '';
}
public function syncPanelToZarin(): \Illuminate\Http\JsonResponse
{
$customers = Customer::whereNull('deleted_at')
->whereNull('moshtari_code')
->select('id', 'name', 'mobile')
->get();
$success = 0;
$failed = 0;
$errors = [];
foreach ($customers as $customer) {
$result = $this->sync()->registerCustomer($customer);
if ($result['ok'] ?? false) {
$success++;
} else {
$failed++;
$errors[] = ['name' => $customer->name, 'mobile' => $customer->mobile, 'error' => $result['message'] ?? ''];
}
}
return response()->json([
'ok' => true,
'success' => $success,
'failed' => $failed,
'errors' => $errors,
]);
}
public function importFromZarin(Request $request): \Illuminate\Http\JsonResponse
{
$validated = $request->validate([
'customers' => 'required|array',
'customers.*.code' => 'required|integer',
'customers.*.name' => 'nullable|string',
'customers.*.tel' => 'nullable|string',
]);
$success = 0;
$failed = 0;
$errors = [];
foreach ($validated['customers'] as $item) {
$mobile = Customer::normalizeMobile($item['tel'] ?? '') ?? '';
$fullName = trim($item['name']);
$code = (int) $item['code'];
if ($fullName === '') {
$fullName = 'مشتری زرین ' . $code;
}
// موبایل برای خیلی از حساب‌های زرین خالی است (مثل تستی هوم) — مانع import نیست
$existing = Customer::whereNull('deleted_at')->where('moshtari_code', $code)->first();
if ($existing) {
$changed = false;
if (($existing->name === null || $existing->name === '') && $fullName !== '') {
$existing->fname = $fullName;
$existing->lname = (string) ($existing->lname ?? '');
$existing->name = $fullName;
$changed = true;
}
if ($mobile !== '' && ($existing->mobile === null || $existing->mobile === '')) {
if (Customer::isMobileTaken($mobile, (int) $existing->id)) {
$failed++;
$errors[] = [
'name' => $item['name'],
'code' => $item['code'],
'error' => 'شماره موبایل تکراری است',
];
continue;
}
$existing->mobile = $mobile;
if (empty($existing->password)) {
$existing->password = $mobile;
}
$changed = true;
}
if ($changed) {
$existing->save();
}
$success++;
continue;
}
if ($mobile !== '') {
$byMobile = Customer::findActiveByMobile($mobile);
if ($byMobile) {
if (empty($byMobile->moshtari_code)) {
$byMobile->moshtari_code = $code;
if ($fullName !== '') {
$byMobile->fname = $fullName;
$byMobile->lname = '';
$byMobile->name = $fullName;
}
if (empty($byMobile->password)) {
$byMobile->password = $mobile;
}
$byMobile->save();
$success++;
continue;
}
$failed++;
$errors[] = [
'name' => $item['name'],
'code' => $item['code'],
'error' => 'شماره موبایل تکراری است',
];
continue;
}
}
$characters = '0123456789abcdefghijklmnopqrstuvwxyz';
do {
$referralId = '';
for ($i = 0; $i < 4; $i++) {
$referralId .= $characters[random_int(0, strlen($characters) - 1)];
}
} while (Customer::where('referral_id', $referralId)->exists());
try {
$customer = new Customer();
$customer->fname = $fullName;
$customer->lname = '';
$customer->name = $fullName;
$customer->mobile = $mobile !== '' ? $mobile : null;
$customer->active = true;
$customer->moshtari_code = $item['code'];
$customer->referral_id = $referralId;
$customer->type = 2;
if ($mobile !== '') {
$customer->password = $mobile;
}
$customer->save();
$success++;
} catch (\Throwable $e) {
$failed++;
$msg = $e->getMessage();
if ($e instanceof UnprocessableEntityHttpException) {
$msg = $e->getMessage();
} elseif ($e instanceof \Illuminate\Database\QueryException && (int) ($e->errorInfo[1] ?? 0) === 1062) {
$msg = 'شماره موبایل تکراری است';
}
$errors[] = ['name' => $item['name'], 'code' => $item['code'], 'error' => $msg];
}
}
return response()->json([
'ok' => true,
'success' => $success,
'failed' => $failed,
'errors' => $errors,
]);
}
/**
* حذف مشتریانِ خرابِ واردشده از زرین (نام خالی) برای پاک‌سازی import ناقص.
* با dry_run=1 فقط تعداد و نمونه را نشان می‌دهد و چیزی حذف نمی‌کند.
*/
public function rollbackImport(Request $request): \Illuminate\Http\JsonResponse
{
$baseQuery = fn () => Customer::whereNotNull('moshtari_code')
->where(function ($q) {
$q->whereNull('name')->orWhere('name', '');
});
$count = $baseQuery()->count();
$sample = $baseQuery()
->select('id', 'fname', 'name', 'mobile', 'moshtari_code', 'created_at')
->orderByDesc('id')
->limit(20)
->get();
if ($request->boolean('dry_run')) {
return response()->json([
'dry_run' => true,
'count' => $count,
'sample' => $sample,
]);
}
$deleted = $baseQuery()->forceDelete();
return response()->json([
'ok' => true,
'deleted' => $deleted,
]);
}
public function retrySanad($id)
{
$zarin = Zarin::with('customer')->findOrFail($id);
$result = $this->sync()->retry($zarin);
return response()->json($result, ($result['ok'] ?? false) ? 200 : 422);
}
public function registerCustomer($customerId)
{
$customer = Customer::findOrFail($customerId);
$result = $this->sync()->registerCustomer($customer);
return response()->json($result, ($result['ok'] ?? false) ? 200 : 422);
}
private function resolveProductId(array $data, ?Zarin $existing = null): ?int
{
if (!empty($data['product']['id'])) {
return (int) $data['product']['id'];
}
if (!empty($data['product_id'])) {
return (int) $data['product_id'];
}
$orderId = $data['order']['id'] ?? ($existing->order_id ?? null);
if ($orderId) {
$order = Order::find($orderId);
if ($order && !empty($order->product_id)) {
return (int) $order->product_id;
}
}
return null;
}
public function storeTrade($data, ?Zarin $existing = null)
{
if (empty($data['customer_id']) && $existing && $existing->order_id) {
$order = Order::find($existing->order_id);
if ($order) {
$data = array_merge($data, [
'customer_id' => $existing->customer_id,
'amount' => $existing->amount,
'direction' => $order->direction,
'price' => $order->price,
'total_price' => $order->total_price,
'pay_at' => $existing->date,
'product' => [
'id' => $order->product_id,
'metal_id' => $existing->type === 'طلا' ? 1 : 2,
],
'product_id' => $order->product_id ? (int) $order->product_id : null,
'order' => ['id' => $existing->order_id, 'product_id' => $order->product_id],
]);
}
}
if (!isset($data['customer_id']) || !isset($data['amount'])) {
return request()->expectsJson()
? response()->json(['error' => 'Data missing: customer_id or amount'], 422)
: false;
}
$mapper = app(AccountingProductDocMapper::class);
$product = $mapper->resolveProductFromPayload($data);
if ($product && $mapper->shouldSkip($product)) {
return false;
}
$customer = Customer::where('id', $data['customer_id'])->first();
if ($customer && ($customer->moshtari_code === null)) {
$this->createCustomerInZarin($customer->id);
$customer = Customer::where('id', $data['customer_id'])->first();
}
if (!$customer || empty($customer->moshtari_code)) {
return false;
}
$rawAmount = (float) $data['amount'];
$count = $mapper->accountingQty($product, $rawAmount, $data);
$vazn = $mapper->accountingWeight($product, $rawAmount, $data);
$isBuy = (int) ($data['direction'] ?? 0) === 1;
$orderId = $data['order']['id'] ?? ($existing->order_id ?? null);
$title = $mapper->productTitle($product);
$action = $isBuy ? 'خرید' : 'فروش';
$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'];
}
$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,
]);
}
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,
$customer,
$product,
$count,
$isBuy ? -1 : 1,
$fi,
$sharh,
$this->zarinUid('10', $uidSource ?? $zarin->id)
);
} else {
$result = $this->postZarinWeightDoc(
$zarin,
$data,
$customer,
$vazn,
$isBuy ? 'f' : 'k',
(float) ($data['price'] ?? 0),
$sharh,
$mapper->jensFelez($product) ?? 0,
$this->zarinUid('10', $uidSource ?? $zarin->id)
);
}
} catch (\Exception $e) {
$this->sync()->markFailed($zarin, $e->getMessage());
return $e->getMessage();
}
return $result;
}
// direction: 0 = daryaft az moshtari, 1 = pardakht be moshtari
public function wallet($data, ?Zarin $existing = null)
{
if (empty($data['customer_id']) && $existing) {
$data = array_merge($data, [
'customer_id' => $existing->customer_id,
'value' => $existing->amount,
'direction' => str_contains((string) $existing->desc, 'برداشت') ? 1 : 0,
'withdraw_id' => $existing->withdraw_id,
]);
}
if (!isset($data['customer_id']) || !isset($data['value'])) {
return request()->expectsJson()
? response()->json(['error' => 'Data missing: customer_id or value'], 422)
: false;
}
$customer = Customer::where('id', $data['customer_id'])->first();
if ($customer && ($customer->moshtari_code === null)) {
$this->createCustomerInZarin($customer->id);
$customer = Customer::where('id', $data['customer_id'])->first();
}
if (!$customer || empty($customer->moshtari_code)) {
return false;
}
$data['handler'] = 'wallet';
$zarin = $existing ?? Zarin::create([
'customer_id' => $customer->id,
'desc' => $data['direction'] == 0 ? 'شارژ حساب' : 'برداشت از حساب',
'amount' => $data['value'],
'type' => 'ريال',
'date' => Carbon::now(),
'created_at' => Carbon::now(),
'withdraw_id' => $data['withdraw_id'] ?? null,
'data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
]);
if ($existing && empty($existing->data)) {
$existing->update(['data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)]);
}
$prices = $this->lastPriceData();
$isWithdraw = (int) ($data['direction'] ?? 0) === 1;
$mazaneh = $isWithdraw ? (int) ($prices['sell'] ?? 0) : (int) ($prices['buy'] ?? 0);
if ($mazaneh <= 0) {
$mazaneh = 1;
}
$uidSource = $data['withdraw_id'] ?? $zarin->id;
try {
$http = $this->zarinRequest([
'kind' => 'pooli',
'radif' => 1,
'korf' => $isWithdraw ? 'f' : 'k',
'poolorvazn' => 'pool',
'meghdar' => $data['value'],
'fi' => $mazaneh,
'hesab' => $customer->moshtari_code,
'sharh' => $data['direction'] == 0 ? 'شارژ حساب' : 'برداشت از حساب',
'ayar' => '750',
'movaghat' => '',
'idFactor' => '',
'uid' => $this->zarinUid('20', $uidSource),
]);
if (!$http) {
$this->sync()->markFailed($zarin, null, 'تنظیمات زرین کامل نیست');
return 'تنظیمات زرین کامل نیست. zarin_api یا zarin_token تنظیم نشده است.';
}
[$response, $httpError] = $this->zarinDecodeResponse($http);
if ($httpError) {
$this->sync()->markFailed($zarin, $response ?? $httpError, $httpError);
return false;
}
Log::info('Zarin wallet response', [
'data' => $data,
'response' => $response ?? null,
]);
} catch (\Exception $e) {
$this->sync()->markFailed($zarin, $e->getMessage());
return $e->getMessage();
}
if ($this->sync()->responseOk(is_array($response) ? $response : [])) {
$this->sync()->markSuccess($zarin, is_array($response) ? $response : [], $data);
return $data;
}
$this->sync()->markFailed($zarin, $response, 'ثبت سند ریالی در زرین ناموفق بود');
return false;
}
/**
* پاداش معرفی — همیشه روی حساب معرف (نه کاربر تازه‌ثبت‌نام‌شده).
*/
public function referrerGoldReward(Customer $invitedCustomer, float $gold, string $desc = 'پاداش بابت معرفی')
{
$referrer = $invitedCustomer->referrer();
if (!$referrer) {
return false;
}
return $this->goldReward([
'customer_id' => $referrer->id,
'value' => $gold,
'desc' => $desc,
]);
}
/**
* پاداش/هدیه طلایی — سند وزنی با korf=f (هدیه به مشتری).
*/
public function goldReward($data, ?Zarin $existing = null)
{
if (empty($data['customer_id']) && $existing) {
$data = array_merge($data, [
'customer_id' => $existing->customer_id,
'value' => $existing->amount,
'desc' => $existing->desc,
]);
}
if (!isset($data['customer_id'], $data['value'])) {
return request()->expectsJson()
? response()->json(['error' => 'Data missing: customer_id or value'], 422)
: false;
}
$customer = Customer::where('id', $data['customer_id'])->first();
if (!$customer) {
return false;
}
if ($customer->moshtari_code === null) {
$this->createCustomerInZarin($customer->id);
$customer = Customer::where('id', $data['customer_id'])->first();
}
if (!$customer || empty($customer->moshtari_code)) {
return false;
}
$amount = abs((float) $data['value']);
$desc = !empty($data['desc']) ? (string) $data['desc'] : 'پاداش';
$data['handler'] = 'goldReward';
$zarin = $existing ?? Zarin::create([
'customer_id' => $customer->id,
'desc' => $desc,
'amount' => $amount,
'date' => Carbon::now(),
'type' => 'طلا',
'created_at' => Carbon::now(),
'data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
]);
if ($existing && empty($existing->data)) {
$existing->update(['data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)]);
}
$prices = $this->lastPriceData();
$mazaneh = (int) ($prices['buy'] ?? 0);
if ($mazaneh <= 0) {
$mazaneh = 1;
}
try {
$http = $this->zarinRequest([
'kind' => 'pooli',
'radif' => 1,
'korf' => 'f',
'poolorvazn' => 'vazn',
'meghdar' => $amount,
'fi' => $mazaneh,
'hesab' => $customer->moshtari_code,
'sharh' => $desc,
'ayar' => '750',
'movaghat' => '',
'idFactor' => '',
'uid' => $this->zarinUid('50', $zarin->id),
]);
if (!$http) {
$this->sync()->markFailed($zarin, null, 'تنظیمات زرین کامل نیست');
return 'تنظیمات زرین کامل نیست. zarin_api یا zarin_token تنظیم نشده است.';
}
[$response, $httpError] = $this->zarinDecodeResponse($http);
if ($httpError) {
$this->sync()->markFailed($zarin, $response ?? $httpError, $httpError);
return false;
}
} catch (\Exception $e) {
$this->sync()->markFailed($zarin, $e->getMessage());
return $e->getMessage();
}
if ($this->sync()->responseOk(is_array($response) ? $response : [])) {
$this->sync()->markSuccess($zarin, is_array($response) ? $response : [], $data);
return $data;
}
$this->sync()->markFailed($zarin, $response, 'ثبت پاداش طلایی در زرین ناموفق بود');
return false;
}
// direction: 1 = واریز، 0 = برداشت
public function walletGold($data, ?Zarin $existing = null)
{
if (empty($data['customer_id']) && $existing) {
$data = array_merge($data, [
'customer_id' => $existing->customer_id,
'value' => $existing->amount,
'direction' => str_contains((string) $existing->desc, 'برداشت') ? 0 : 1,
'withdraw_id' => $existing->withdraw_id,
]);
$payload = json_decode((string) ($existing->data ?? ''), true);
if (is_array($payload)) {
if (!empty($payload['product_id'])) {
$data['product_id'] = (int) $payload['product_id'];
}
if (!empty($payload['product']) && is_array($payload['product'])) {
$data['product'] = $payload['product'];
}
}
}
if (!isset($data['customer_id'], $data['value'])) {
return request()->expectsJson()
? response()->json(['error' => 'Data missing: customer_id or value'], 422)
: false;
}
$mapper = app(AccountingProductDocMapper::class);
$product = $mapper->resolveProductFromPayload($data);
if ($product && $mapper->shouldSkip($product)) {
return false;
}
$customer = Customer::where('id', $data['customer_id'])->first();
if ($customer && ($customer->moshtari_code === null)) {
$this->createCustomerInZarin($customer->id);
$customer = Customer::where('id', $data['customer_id'])->first();
}
if (!$customer || empty($customer->moshtari_code)) {
return false;
}
$rawValue = (float) $data['value'];
$count = $mapper->accountingQty($product, $rawValue, $data);
$vazn = $mapper->accountingWeight($product, $rawValue, $data);
$isDeposit = (int) ($data['direction'] ?? 1) === 1;
$title = $mapper->productTitle($product);
$action = $isDeposit ? 'شارژ' : 'دشارژ';
$sharh = !empty($data['note'])
? (string) $data['note']
: (!empty($data['desc'])
? (string) $data['desc']
: trim($title !== '' ? "{$action} {$title}" : "{$action} {$mapper->kindLabel($mapper->resolveKind($product))}"));
$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->zarinDefaultNoeKar()
. ' goroheKar=' . $this->zarinDefaultGoroheKar()
. ' product=' . ($product->title ?? '')
. ' source=' . ($kar['source'] ?? '');
if (
!$retriedInvalidKar
&& $this->isInvalidZarinKarError($response, $httpError)
) {
Log::warning('zarin sabtSanadTedadi invalid idKar, recreating', [
'diag' => $diag,
'response' => $response,
'httpError' => $httpError,
]);
$this->clearProductZarinKar($product);
$product->refresh();
$retriedInvalidKar = true;
continue;
}
if ($httpError) {
$this->sync()->markFailed(
$zarin,
$response ?? $httpError,
$httpError . ' | ' . $diag
);
return false;
}
$flat = $this->extractResponseRow(is_array($response) ? $response : null);
if ($this->sync()->responseOk(is_array($response) ? $response : []) || $this->sync()->responseOk($flat)) {
$this->sync()->markSuccess($zarin, is_array($response) ? $response : $flat, $data);
return $data;
}
if (
!$retriedInvalidKar
&& $this->isInvalidZarinKarError($response)
) {
Log::warning('zarin sabtSanadTedadi invalid idKar (HTTP 200), recreating', [
'diag' => $diag,
'response' => $response,
]);
$this->clearProductZarinKar($product);
$product->refresh();
$retriedInvalidKar = true;
continue;
}
$this->sync()->markFailed(
$zarin,
$response,
'ثبت سند تعدادی در زرین ناموفق بود | ' . $diag
);
return false;
}
}
/**
* سند وزنی زرین (آبشده طلا/نقره).
*/
private function postZarinWeightDoc(
Zarin $zarin,
array $data,
Customer $customer,
float $meghdar,
string $korf,
float $fi,
string $sharh,
int $jensFelez,
string $uid
) {
if ($fi <= 0) {
$fi = 1;
}
$http = $this->zarinRequest([
'kind' => 'pooli',
'radif' => 1,
'korf' => $korf,
'poolorvazn' => 'vazn',
'meghdar' => $meghdar,
'fi' => $fi,
'hesab' => $customer->moshtari_code,
'sharh' => $sharh,
'ayar' => $jensFelez === 2 ? '' : '750',
'movaghat' => '',
'idFactor' => '',
'uid' => $uid,
]);
if (!$http) {
$this->sync()->markFailed($zarin, null, 'تنظیمات زرین کامل نیست');
return 'تنظیمات زرین کامل نیست. zarin_api یا zarin_token تنظیم نشده است.';
}
[$response, $httpError] = $this->zarinDecodeResponse($http);
if ($httpError) {
$this->sync()->markFailed($zarin, $response ?? $httpError, $httpError);
return false;
}
$flat = $this->extractResponseRow(is_array($response) ? $response : null);
if ($this->sync()->responseOk(is_array($response) ? $response : []) || $this->sync()->responseOk($flat)) {
$this->sync()->markSuccess($zarin, is_array($response) ? $response : $flat, $data);
return $data;
}
$this->sync()->markFailed($zarin, $response, 'ثبت سند وزنی در زرین ناموفق بود');
return false;
}
public function createCustomerInZarin($customer_id)
{
$customer = Customer::findOrFail($customer_id);
if ($customer->moshtari_code != null) {
return $customer->moshtari_code;
}
$lock = Cache::lock('zarin:register-customer:' . $customer->id, 30);
if (!$lock->get()) {
$customer->refresh();
if ($customer->moshtari_code != null) {
return $customer->moshtari_code;
}
return 'ثبت مشتری در زرین در حال انجام است';
}
try {
$customer->refresh();
if ($customer->moshtari_code != null) {
return $customer->moshtari_code;
}
$name = $customer->fname ?: ($customer->name ?? ('مشتری ' . $customer->id));
$family = $customer->lname ?: '';
$mobile = $customer->mobile ?? '';
$params = [
'kind' => 'addhesab',
'name' => $name,
'family' => $family,
'tel' => $mobile,
'mobile' => $mobile,
'address' => trim(($customer->province ?? '') . ' ' . ($customer->city ?? '')) ?: 'ثبت نشده',
'codePosti' => '0000000000',
'nameG' => $this->zarinCustomerGroupName(),
'gender' => in_array($customer->gender, ['اقای', 'آقای'], true) ? 'm' : ($customer->gender == 'خانم' ? 'f' : ''),
'uid' => $this->zarinUid('40', $customer->id),
];
try {
$http = $this->zarinRequest($params);
if (!$http) {
return 'تنظیمات زرین کامل نیست. zarin_api یا zarin_token تنظیم نشده است.';
}
[$response, $httpError] = $this->zarinDecodeResponse($http);
if ($httpError) {
return $httpError;
}
$data = $this->extractResponseRow($response);
$idHesab = $data['idHesab'] ?? null;
if (!empty($idHesab)) {
$customer->moshtari_code = $idHesab;
$customer->save();
// int برگردان تا با پیام خطای string اشتباه نشود
return is_numeric($idHesab) ? (0 + $idHesab) : $idHesab;
}
$detail = $data['msg'] ?? $data['message'] ?? $data['detail'] ?? null;
return $detail ? (string) $detail : 'zarin api error!!!';
} catch (\Exception $e) {
return $e->getMessage();
}
} finally {
$lock->release();
}
}
public function healthCheck()
{
$http = $this->zarinRequest([
'kind' => 'version',
]);
[$json, $error] = $this->zarinDecodeResponse($http);
if ($error) {
return response()->json([
'ok' => false,
'message' => $error,
'body' => $json,
], 422);
}
return response()->json([
'ok' => true,
'version' => $json['version'] ?? null,
'message' => $json['message'] ?? 'OK',
'raw' => $json,
]);
}
}