2606 lines
91 KiB
PHP
2606 lines
91 KiB
PHP
<?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;
|
||
}
|
||
|
||
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;
|
||
// زرین korf/parDar از دید حساب مشتری است، نه فروشگاه:
|
||
// خرید مشتری → k / +1 : طلا بستانکار (سوت اضافه) + ریال بدهکار (پول کم)
|
||
// فروش مشتری → f / −1 : طلا بدهکار + ریال بستانکار
|
||
$orderId = is_object($data['order'] ?? null)
|
||
? ($data['order']->id ?? null)
|
||
: ($data['order']['id'] ?? ($existing->order_id ?? null));
|
||
$title = $mapper->productTitle($product);
|
||
$action = $isBuy ? 'خرید' : 'فروش';
|
||
$sharh = trim((string) ($data['sharh_override'] ?? ''));
|
||
if ($sharh === '') {
|
||
$sharh = trim($title !== '' ? "{$action} {$title}" : "{$action} {$mapper->kindLabel($mapper->resolveKind($product))}");
|
||
}
|
||
$syncAmount = $mapper->isCountDoc($product) ? $count : $vazn;
|
||
$uidSource = $orderId ?? null;
|
||
if (!empty($data['line'])) {
|
||
$uidSource = ($orderId ?? '0') . 'L' . (int) $data['line'];
|
||
}
|
||
if ($isColleagueLeg) {
|
||
$uidSource = ($uidSource ?? '0') . 'C';
|
||
}
|
||
|
||
$data['handler'] = 'storeTrade';
|
||
if ($existing && empty($existing->data)) {
|
||
$existing->update(['data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)]);
|
||
}
|
||
$zarin = $existing ?? Zarin::create([
|
||
'customer_id' => $customer->id,
|
||
'desc' => $sharh,
|
||
'amount' => $syncAmount,
|
||
'order_id' => $orderId,
|
||
'date' => $data['pay_at'] ?? Carbon::now(),
|
||
'type' => $mapper->zarinSyncTypeLabel($product),
|
||
'created_at' => Carbon::now(),
|
||
'data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
|
||
]);
|
||
if ($existing) {
|
||
$existing->update([
|
||
'type' => $mapper->zarinSyncTypeLabel($product),
|
||
'desc' => $sharh,
|
||
'amount' => $syncAmount,
|
||
]);
|
||
}
|
||
|
||
$uidKind = $isColleagueLeg ? '11' : '10';
|
||
|
||
try {
|
||
if ($mapper->isCountDoc($product)) {
|
||
$fi = (float) ($data['price'] ?? 0);
|
||
if ($count > 0) {
|
||
if (!empty($data['total_price'])) {
|
||
$fi = ((float) $data['total_price']) / $count;
|
||
} elseif ($fi > 0 && abs($vazn - $count) > 0.0001) {
|
||
$fi = $fi * ($vazn / $count);
|
||
}
|
||
}
|
||
$result = $this->postZarinCountDoc(
|
||
$zarin,
|
||
$data,
|
||
$accountCustomer,
|
||
$product,
|
||
$count,
|
||
$isBuy ? 1 : -1,
|
||
$fi,
|
||
$sharh,
|
||
$this->zarinUid($uidKind, $uidSource ?? $zarin->id)
|
||
);
|
||
} else {
|
||
$result = $this->postZarinWeightDoc(
|
||
$zarin,
|
||
$data,
|
||
$accountCustomer,
|
||
$vazn,
|
||
$isBuy ? 'k' : 'f',
|
||
(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 = self::accountingWalletDesc($isDeposit, $data['note'] ?? null);
|
||
$zarin = $existing ?? Zarin::create([
|
||
'customer_id' => $customer->id,
|
||
'desc' => $sharh,
|
||
'amount' => $data['value'],
|
||
'type' => 'ريال',
|
||
'date' => Carbon::now(),
|
||
'created_at' => Carbon::now(),
|
||
'withdraw_id' => $data['withdraw_id'] ?? null,
|
||
'data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
|
||
]);
|
||
|
||
if ($existing && empty($existing->data)) {
|
||
$existing->update(['data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)]);
|
||
}
|
||
|
||
$prices = $this->lastPriceData();
|
||
$isWithdraw = (int) ($data['direction'] ?? 0) === 1;
|
||
$mazaneh = $isWithdraw ? (int) ($prices['sell'] ?? 0) : (int) ($prices['buy'] ?? 0);
|
||
if ($mazaneh <= 0) {
|
||
$mazaneh = 1;
|
||
}
|
||
|
||
$uidSource = $data['withdraw_id'] ?? $zarin->id;
|
||
|
||
try {
|
||
$http = $this->zarinRequest([
|
||
'kind' => 'pooli',
|
||
'radif' => 1,
|
||
'korf' => $isWithdraw ? 'f' : 'k',
|
||
'poolorvazn' => 'pool',
|
||
'meghdar' => $data['value'],
|
||
'fi' => $mazaneh,
|
||
'hesab' => $customer->moshtari_code,
|
||
'sharh' => $sharh,
|
||
'ayar' => '750',
|
||
'movaghat' => '',
|
||
'idFactor' => '',
|
||
'uid' => $this->zarinUid('20', $uidSource),
|
||
]);
|
||
|
||
if (!$http) {
|
||
$this->sync()->markFailed($zarin, null, 'تنظیمات زرین کامل نیست');
|
||
return 'تنظیمات زرین کامل نیست. zarin_api یا zarin_token تنظیم نشده است.';
|
||
}
|
||
|
||
[$response, $httpError] = $this->zarinDecodeResponse($http);
|
||
if ($httpError) {
|
||
$this->sync()->markFailed($zarin, $response ?? $httpError, $httpError);
|
||
return false;
|
||
}
|
||
|
||
Log::info('Zarin wallet response', [
|
||
'data' => $data,
|
||
'response' => $response ?? null,
|
||
]);
|
||
} catch (\Exception $e) {
|
||
$this->sync()->markFailed($zarin, $e->getMessage());
|
||
return $e->getMessage();
|
||
}
|
||
|
||
if ($this->sync()->responseOk(is_array($response) ? $response : [])) {
|
||
$this->sync()->markSuccess($zarin, is_array($response) ? $response : [], $data);
|
||
return $data;
|
||
}
|
||
|
||
$this->sync()->markFailed($zarin, $response, 'ثبت سند ریالی در زرین ناموفق بود');
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* پاداش معرفی — همیشه روی حساب معرف (نه کاربر تازهثبتنامشده).
|
||
*/
|
||
public function referrerGoldReward(Customer $invitedCustomer, float $gold, string $desc = 'پاداش معرفی')
|
||
{
|
||
$referrer = $invitedCustomer->referrer();
|
||
if (!$referrer) {
|
||
return false;
|
||
}
|
||
|
||
return $this->goldReward([
|
||
'customer_id' => $referrer->id,
|
||
'value' => $gold,
|
||
'desc' => $desc,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* پاداش/هدیه طلایی — سند وزنی با korf=f (هدیه به مشتری).
|
||
*/
|
||
public function goldReward($data, ?Zarin $existing = null)
|
||
{
|
||
if (empty($data['customer_id']) && $existing) {
|
||
$data = array_merge($data, [
|
||
'customer_id' => $existing->customer_id,
|
||
'value' => $existing->amount,
|
||
'desc' => $existing->desc,
|
||
]);
|
||
}
|
||
|
||
if (!isset($data['customer_id'], $data['value'])) {
|
||
return request()->expectsJson()
|
||
? response()->json(['error' => 'Data missing: customer_id or value'], 422)
|
||
: false;
|
||
}
|
||
|
||
$customer = Customer::where('id', $data['customer_id'])->first();
|
||
if (!$customer) {
|
||
return false;
|
||
}
|
||
|
||
if (! $customer->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,
|
||
]);
|
||
}
|
||
}
|