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

1602 lines
55 KiB
PHP
Raw Normal View History

2026-07-23 20:24:58 +03:30
<?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\ConfigService;
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;
}
2026-07-28 14:02:23 +03:30
private function zarinRequest(array $params, int $timeout = 15)
2026-07-23 20:24:58 +03:30
{
$url = $this->zarinAccountingUrl();
$token = $this->zarinToken();
if (!$url || !$token) {
return null;
}
$params['token'] = $token;
return Http::withoutVerifying()
2026-07-28 14:02:23 +03:30
->timeout($timeout)
2026-07-23 20:24:58 +03:30
->get($url, $params);
}
2026-07-28 14:02:23 +03:30
/**
* 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];
}
2026-07-23 20:24:58 +03:30
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;
}
2026-07-28 14:49:55 +03:30
/** نام گروه حساب مشتری در زرین — تنظیم zarin_name_g؛ خالی = گروه ۶ مشتری نقدی */
2026-07-23 20:24:58 +03:30
private function zarinCustomerGroupName(): string
{
2026-07-28 14:49:55 +03:30
$nameG = trim((string) $this->Config('zarin_name_g'));
return $nameG !== '' ? $nameG : 'مشتری نقدی (خریدار)';
}
/** آیا درخواست لیست زرین را خواسته؟ (query/body؛ مقاوم به boolean عجیب) */
private function requestWantsZarinList(Request $request): bool
{
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);
2026-07-23 20:24:58 +03:30
}
/**
* پیشوند عددی 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;
}
2026-07-28 14:02:23 +03:30
/**
* پیدا کردن کار/کالای محصول در زرین.
* اول infokarbarcodi (اگر روی آن سرور فعال باشد)؛ وگرنه infosakhte (لیست کامل).
*/
2026-07-23 20:24:58 +03:30
private function findZarinKarForProduct(Product $product, $goroheKar = null)
{
$goroheKar = $goroheKar ?: $this->zarinDefaultGoroheKar();
$productName = $this->normalizeZarinName($product->title);
2026-07-28 14:02:23 +03:30
$fromBarcode = $this->findZarinKarInInfokarbarcodi($productName, $goroheKar);
if ($fromBarcode) {
return $fromBarcode;
}
return $this->findZarinKarInInfosakhte($productName, $goroheKar);
}
private function matchZarinKarRow(string $productName, array $row): ?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) {
return null;
}
if (
$rowName === $productName ||
$this->containsText($rowName, $productName) ||
$this->containsText($productName, $rowName)
) {
return [
'idKar' => $idKar,
'row' => $row,
];
}
return null;
}
private function findZarinKarInInfokarbarcodi(string $productName, $goroheKar): ?array
{
2026-07-23 20:24:58 +03:30
for ($page = 1; $page <= 10; $page++) {
2026-07-28 14:02:23 +03:30
$response = $this->zarinRequest([
2026-07-23 20:24:58 +03:30
'kind' => 'infokarbarcodi',
'id' => $goroheKar,
'vazneat' => 0,
'pageNum' => $page,
'detailCount' => 250,
2026-07-28 14:02:23 +03:30
]);
2026-07-23 20:24:58 +03:30
2026-07-28 14:02:23 +03:30
// روی بعضی سرورهای زرین جدول barcodetala نیست → 400؛ برو سراغ infosakhte
2026-07-23 20:24:58 +03:30
if (!$response || !$response->successful()) {
return null;
}
2026-07-28 14:02:23 +03:30
$rows = $this->zarinRowsFromBody($response->json());
2026-07-23 20:24:58 +03:30
if (empty($rows)) {
break;
}
foreach ($rows as $row) {
if (!is_array($row)) {
continue;
}
2026-07-28 14:02:23 +03:30
$hit = $this->matchZarinKarRow($productName, $row);
if ($hit) {
return $hit;
2026-07-23 20:24:58 +03:30
}
}
if (count($rows) < 250) {
break;
}
}
return null;
}
2026-07-28 14:02:23 +03:30
/** لیست کارهای ساخته — فیلد nam_kar_sakhte / id (تست‌شده روی API زرین) */
private function findZarinKarInInfosakhte(string $productName, $goroheKar): ?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;
$preferred = null;
$fallback = null;
foreach ($rows as $row) {
if (!is_array($row)) {
continue;
}
$hit = $this->matchZarinKarRow($productName, $row);
if (!$hit) {
continue;
}
$idGp = (string) ($row['idGp'] ?? $row['idgp'] ?? '');
if ($idGp !== '' && ($idGp === $gorohe || $idGp === $noeKar)) {
$preferred = $hit;
break;
}
if ($fallback === null) {
$fallback = $hit;
}
}
return $preferred ?? $fallback;
}
2026-07-23 20:24:58 +03:30
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;
}
$rowId = $row['pkFelez']
?? $row['pkfelez']
?? $row['PKFelez']
?? $row['id']
?? $row['idKar']
?? $row['idkar']
?? $row['zarrinId']
?? $row['zarinId']
?? null;
if ((string) $rowId === (string) $idKar) {
return $row;
}
}
return null;
}
private function extractUsableZarinKarId(array $row)
{
foreach ([
'zarrinId',
'zarinId',
'idKar',
'idkar',
'IDKar',
'id',
'code',
'Code',
] as $key) {
if (isset($row[$key]) && is_numeric($row[$key])) {
return (int) $row[$key];
}
}
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();
$createdPkFelez = $body['data'][0]['pkFelez']
?? $body['data']['pkFelez']
?? $body['pkFelez']
?? null;
$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,
'created_pkFelez' => $createdPkFelez,
'sent_params_success' => $params,
];
}
return [
'ok' => false,
2026-07-28 14:02:23 +03:30
'message' => 'زرین کالا را ساخت، اما idKar قابل استفاده از infosakhte/infokarbarcodi پیدا نشد.',
2026-07-23 20:24:58 +03:30
'status_code' => 422,
'created_pkFelez' => $createdPkFelez,
'zarin_response' => $body,
'sent_params' => $params,
];
}
private function ensureZarinKarForProduct(Product $product, $forceCreate = false)
{
$currentIdKar = $product->finance_p_name;
if (!$forceCreate && !empty($currentIdKar) && is_numeric($currentIdKar)) {
$existingRow = $this->getZarinKarById($currentIdKar);
if ($existingRow) {
$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,
];
}
}
if (!$forceCreate) {
$found = $this->findZarinKarForProduct($product, $this->zarinDefaultGoroheKar());
if ($found && !empty($found['idKar']) && is_numeric($found['idKar'])) {
$product->finance_p_name = (string) $found['idKar'];
$product->save();
return [
'ok' => true,
'source' => 'found_by_name',
'idKar' => (int) $found['idKar'],
'product' => $product->fresh(),
'zarin_row' => $found['row'],
];
}
}
return $this->createZarinKarForProduct($product);
}
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);
}
2026-07-28 14:02:23 +03:30
/**
* لیست حساب‌های زرین — فقط listhesabha (Get100Account لیست نیست و ۴۰۰ می‌دهد).
*
* @return array{rows: array<int, array>, error: ?string, http_status: ?int}
*/
2026-07-23 20:24:58 +03:30
private function fetchAllZarinCustomers(): array
{
2026-07-28 14:02:23 +03:30
if (!$this->zarinAccountingUrl() || !$this->zarinToken()) {
return [
'rows' => [],
'error' => 'تنظیمات zarin_api یا zarin_token خالی است.',
'http_status' => null,
];
}
2026-07-23 20:24:58 +03:30
try {
2026-07-28 14:02:23 +03:30
// listhesabha کامل است (~۳۰۰ ردیف / چند ثانیه)؛ timeout بالاتر از درخواست‌های کوتاه
2026-07-23 20:24:58 +03:30
$response = $this->zarinRequest([
'kind' => 'listhesabha',
2026-07-28 14:02:23 +03:30
], 45);
2026-07-23 20:24:58 +03:30
2026-07-28 14:02:23 +03:30
if (!$response) {
return [
'rows' => [],
'error' => 'درخواست به زرین ساخته نشد (آدرس/توکن).',
'http_status' => null,
];
2026-07-23 20:24:58 +03:30
}
2026-07-28 14:02:23 +03:30
$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,
];
2026-07-23 20:24:58 +03:30
} catch (\Exception $e) {
2026-07-28 14:02:23 +03:30
Log::warning('zarin listhesabha failed', ['message' => $e->getMessage()]);
return [
'rows' => [],
'error' => 'خطا در ارتباط با زرین: ' . $e->getMessage(),
'http_status' => null,
];
2026-07-23 20:24:58 +03:30
}
}
2026-07-28 14:02:23 +03:30
/** نام حساب از پاسخ 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']
?? ''
));
}
2026-07-23 20:24:58 +03:30
public function customerSyncStatus(Request $request): \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;
2026-07-28 14:02:23 +03:30
$zarinFetchError = null;
$zarinHttpStatus = null;
2026-07-28 14:49:55 +03:30
$zarinGroupFilter = $this->zarinCustomerGroupName();
2026-07-23 20:24:58 +03:30
$zarinOnlyResult = ['data' => [], 'total' => 0, 'current_page' => 1, 'last_page' => 1];
$debugSample = [];
2026-07-28 14:49:55 +03:30
$include = $this->requestWantsZarinList($request);
2026-07-23 20:24:58 +03:30
if ($include) {
2026-07-28 14:02:23 +03:30
$fetched = $this->fetchAllZarinCustomers();
$zarinCustomers = $fetched['rows'];
$zarinFetchError = $fetched['error'];
$zarinHttpStatus = $fetched['http_status'];
2026-07-28 14:49:55 +03:30
// فقط گروه تنظیم‌شده (گروه ۶ / 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);
}));
}
2026-07-23 20:24:58 +03:30
$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,
2026-07-28 14:02:23 +03:30
'name' => $this->extractZarinName($item),
2026-07-23 20:24:58 +03:30
'tel' => $phone,
'gid' => $item['GID'] ?? null,
2026-07-28 14:02:23 +03:30
'group' => $this->extractZarinGroup($item),
2026-07-23 20:24:58 +03:30
];
}
$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,
2026-07-28 14:02:23 +03:30
'zarin_fetch_error' => $zarinFetchError,
'zarin_http_status' => $zarinHttpStatus,
2026-07-28 14:49:55 +03:30
'zarin_group_filter' => $zarinGroupFilter,
'include_zarin' => $include,
'include_zarin_param' => $request->query('include_zarin', $request->input('include_zarin')),
2026-07-23 20:24:58 +03:30
'panel_only_list' => $panelOnlyList,
'zarin_only_list' => $zarinOnlyResult,
];
if ($include && $request->boolean('raw')) {
$payload['zarin_debug_sample'] = $debugSample;
}
2026-07-28 14:49:55 +03:30
return response()->json($payload)->header('Cache-Control', 'no-store, private');
2026-07-23 20:24:58 +03:30
}
private function extractZarinPhone(array $item): string
{
2026-07-28 14:02:23 +03:30
$candidates = ['mobile', 'Mobile', 'tell', 'Tell', 'tel', 'Tel', 'Phone', 'CellPhone', 'Cell', 'MobileNo', 'Telephone', 'Mobile1'];
2026-07-23 20:24:58 +03:30
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',
2026-07-28 14:28:28 +03:30
'customers.*.name' => 'nullable|string',
2026-07-23 20:24:58 +03:30
'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'];
2026-07-28 14:28:28 +03:30
if ($fullName === '') {
$fullName = 'مشتری زرین ' . $code;
2026-07-23 20:24:58 +03:30
}
2026-07-28 14:28:28 +03:30
// موبایل برای خیلی از حساب‌های زرین خالی است (مثل تستی هوم) — مانع import نیست
2026-07-23 20:24:58 +03:30
$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;
2026-07-28 14:28:28 +03:30
$customer->mobile = $mobile !== '' ? $mobile : null;
2026-07-23 20:24:58 +03:30
$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,
],
'order' => ['id' => $existing->order_id],
]);
}
}
if (!isset($data['customer_id']) || !isset($data['amount'])) {
return request()->expectsJson()
? response()->json(['error' => 'Data missing: customer_id or amount'], 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;
}
$amount = $data['amount'];
$orderId = $data['order']['id'] ?? ($existing->order_id ?? null);
$metalId = (int) ($data['product']['metal_id'] ?? 1);
$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' => $data['direction'] == 1 ? 'خرید آبشده' : 'فروش آبشده',
'amount' => $amount,
'order_id' => $orderId,
'date' => $data['pay_at'] ?? Carbon::now(),
'type' => $metalId == 1 ? 'طلا' : 'نقره',
'created_at' => Carbon::now(),
'data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
]);
$isStore = app(ConfigService::class)->isStore();
$productId = $this->resolveProductId($data, $existing);
try {
if ($isStore && $productId) {
$product = Product::find($productId);
if (!$product) {
$this->sync()->markFailed($zarin, null, 'محصول برای ثبت سند زرین پیدا نشد');
return false;
}
$karResult = $this->ensureZarinKarForProduct($product);
if (empty($karResult['ok'])) {
$this->sync()->markFailed($zarin, $karResult, 'کد کار زرین برای محصول آماده نشد');
return false;
}
$params = [
'kind' => 'sabtSanadTedadi',
'parDar' => ((int) ($data['direction'] ?? 0) === 1) ? -1 : 1,
'idHesab' => $customer->moshtari_code,
'idKar' => (int) $karResult['idKar'],
'fi' => $data['price'] ?? 0,
'tedad' => $amount,
'sharh' => $data['direction'] == 1 ? 'خرید' : 'فروش',
'uid' => $this->zarinUid('30', $orderId),
];
} else {
$params = [
'kind' => 'pooli',
'radif' => 1,
'korf' => ((int) ($data['direction'] ?? 0) === 1) ? 'f' : 'k',
'poolorvazn' => 'vazn',
'meghdar' => $amount,
'fi' => $data['price'] ?? 0,
'hesab' => $customer->moshtari_code,
'sharh' => $data['direction'] == 1 ? 'خرید آبشده' : 'فروش آبشده',
'ayar' => '750',
'movaghat' => '',
'idFactor' => '',
'uid' => $this->zarinUid('10', $orderId),
];
}
$http = $this->zarinRequest($params);
if (!$http) {
$this->sync()->markFailed($zarin, null, 'تنظیمات زرین کامل نیست');
return 'تنظیمات زرین کامل نیست. zarin_api یا zarin_token تنظیم نشده است.';
}
2026-07-28 14:02:23 +03:30
[$response, $httpError] = $this->zarinDecodeResponse($http);
if ($httpError) {
$this->sync()->markFailed($zarin, $response ?? $httpError, $httpError);
return false;
2026-07-23 20:24:58 +03:30
}
} catch (\Exception $e) {
$this->sync()->markFailed($zarin, $e->getMessage());
return $e->getMessage();
}
$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;
}
// 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 تنظیم نشده است.';
}
2026-07-28 14:02:23 +03:30
[$response, $httpError] = $this->zarinDecodeResponse($http);
if ($httpError) {
$this->sync()->markFailed($zarin, $response ?? $httpError, $httpError);
return false;
}
2026-07-23 20:24:58 +03:30
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 تنظیم نشده است.';
}
2026-07-28 14:02:23 +03:30
[$response, $httpError] = $this->zarinDecodeResponse($http);
if ($httpError) {
$this->sync()->markFailed($zarin, $response ?? $httpError, $httpError);
return false;
}
2026-07-23 20:24:58 +03:30
} 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,
]);
}
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 && ($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($data['value']);
$isDeposit = (int) ($data['direction'] ?? 1) === 1;
$data['handler'] = 'walletGold';
$zarin = $existing ?? Zarin::create([
'customer_id' => $customer->id,
'desc' => $isDeposit ? 'شارژ کیف پول آبشده' : 'برداشت از کیف پول آبشده',
'amount' => $amount,
'date' => Carbon::now(),
'type' => 'طلا',
'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();
$mazaneh = $isDeposit ? (int) ($prices['buy'] ?? 0) : (int) ($prices['sell'] ?? 0);
if ($mazaneh <= 0) {
$mazaneh = 1;
}
$sharh = !empty($data['note'])
? $data['note']
: (!empty($data['desc']) ? $data['desc'] : ($isDeposit ? 'خرید آبشده' : 'فروش آبشده'));
$data['note'] = $sharh;
$uidSource = $data['withdraw_id'] ?? $zarin->id;
try {
$http = $this->zarinRequest([
'kind' => 'pooli',
'radif' => 1,
'korf' => $isDeposit ? 'f' : 'k',
'poolorvazn' => 'vazn',
'meghdar' => $amount,
'fi' => $mazaneh,
'hesab' => $customer->moshtari_code,
'sharh' => $sharh,
'ayar' => '750',
'movaghat' => '',
'idFactor' => '',
'uid' => $this->zarinUid('21', $uidSource),
]);
if (!$http) {
$this->sync()->markFailed($zarin, null, 'تنظیمات زرین کامل نیست');
return 'تنظیمات زرین کامل نیست. zarin_api یا zarin_token تنظیم نشده است.';
}
2026-07-28 14:02:23 +03:30
[$response, $httpError] = $this->zarinDecodeResponse($http);
if ($httpError) {
$this->sync()->markFailed($zarin, $response ?? $httpError, $httpError);
return false;
}
2026-07-23 20:24:58 +03:30
} 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 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 تنظیم نشده است.';
}
2026-07-28 14:02:23 +03:30
[$response, $httpError] = $this->zarinDecodeResponse($http);
if ($httpError) {
return $httpError;
}
2026-07-23 20:24:58 +03:30
2026-07-28 14:02:23 +03:30
$data = $this->extractResponseRow($response);
2026-07-23 20:24:58 +03:30
$idHesab = $data['idHesab'] ?? null;
if (!empty($idHesab)) {
$customer->moshtari_code = $idHesab;
$customer->save();
2026-07-28 14:15:51 +03:30
// int برگردان تا با پیام خطای string اشتباه نشود
return is_numeric($idHesab) ? (0 + $idHesab) : $idHesab;
2026-07-23 20:24:58 +03:30
}
2026-07-28 14:02:23 +03:30
$detail = $data['msg'] ?? $data['message'] ?? $data['detail'] ?? null;
return $detail ? (string) $detail : 'zarin api error!!!';
2026-07-23 20:24:58 +03:30
} catch (\Exception $e) {
return $e->getMessage();
}
} finally {
$lock->release();
}
}
public function healthCheck()
{
$http = $this->zarinRequest([
'kind' => 'version',
]);
2026-07-28 14:02:23 +03:30
[$json, $error] = $this->zarinDecodeResponse($http);
if ($error) {
return response()->json([
'ok' => false,
'message' => $error,
'body' => $json,
], 422);
2026-07-23 20:24:58 +03:30
}
2026-07-28 14:02:23 +03:30
return response()->json([
'ok' => true,
'version' => $json['version'] ?? null,
'message' => $json['message'] ?? 'OK',
'raw' => $json,
]);
2026-07-23 20:24:58 +03:30
}
}