Files
boursecu_server/app/Http/Controllers/Admin/ZarinController.php
T
2026-07-28 14:02:23 +03:30

1542 lines
52 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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;
}
private function zarinRequest(array $params, int $timeout = 15)
{
$url = $this->zarinAccountingUrl();
$token = $this->zarinToken();
if (!$url || !$token) {
return null;
}
$params['token'] = $token;
return Http::withoutVerifying()
->timeout($timeout)
->get($url, $params);
}
/**
* decode پاسخ HTTP زرین — خطاهای متنی (مثل ERROR: radif is empty) را هم برمی‌گرداند.
*
* @return array{0: ?array, 1: ?string} [json, error]
*/
private function zarinDecodeResponse($http): array
{
if (!$http) {
return [null, 'تنظیمات zarin_api یا zarin_token خالی است.'];
}
$body = (string) $http->body();
$json = $http->json();
if (!is_array($json) && $body !== '') {
$decoded = json_decode($body, true);
if (is_array($decoded)) {
$json = $decoded;
}
}
if (!$http->successful()) {
$detail = null;
if (is_array($json)) {
$detail = $json['detail'] ?? $json['msg'] ?? $json['message'] ?? $json['error'] ?? null;
}
if ($detail === null || $detail === '') {
$detail = trim($body) !== '' ? mb_substr(trim($body), 0, 300) : null;
}
$error = 'HTTP ' . $http->status() . ($detail ? (': ' . $detail) : '');
return [is_array($json) ? $json : null, $error];
}
if (!is_array($json)) {
return [null, 'پاسخ زرین JSON نیست (طول ' . strlen($body) . ')'];
}
return [$json, null];
}
private function zarinDefaultNoeKar()
{
$value = (int) $this->Config('zarin_noe_kar');
return $value > 0 ? $value : 16;
}
private function zarinDefaultGoroheKar()
{
$value = (int) $this->Config('zarin_gorohe_kar');
return $value > 0 ? $value : 52;
}
/** نام گروه حساب مشتری در زرین — فقط از تنظیم zarin_name_g */
private function zarinCustomerGroupName(): string
{
return trim((string) $this->Config('zarin_name_g'));
}
/**
* پیشوند عددی 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;
}
/**
* پیدا کردن کار/کالای محصول در زرین.
* اول infokarbarcodi (اگر روی آن سرور فعال باشد)؛ وگرنه infosakhte (لیست کامل).
*/
private function findZarinKarForProduct(Product $product, $goroheKar = null)
{
$goroheKar = $goroheKar ?: $this->zarinDefaultGoroheKar();
$productName = $this->normalizeZarinName($product->title);
$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
{
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);
if ($hit) {
return $hit;
}
}
if (count($rows) < 250) {
break;
}
}
return null;
}
/** لیست کارهای ساخته — فیلد 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;
}
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,
'message' => 'زرین کالا را ساخت، اما idKar قابل استفاده از infosakhte/infokarbarcodi پیدا نشد.',
'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);
}
/**
* لیست حساب‌های زرین — فقط 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
{
$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;
$zarinOnlyResult = ['data' => [], 'total' => 0, 'current_page' => 1, 'last_page' => 1];
$debugSample = [];
$include = $request->boolean('include_zarin') || $request->boolean('include_tahesab');
if ($include) {
$fetched = $this->fetchAllZarinCustomers();
$zarinCustomers = $fetched['rows'];
$zarinFetchError = $fetched['error'];
$zarinHttpStatus = $fetched['http_status'];
$zarinTotal = count($zarinCustomers);
$panelMobiles = Customer::whereNull('deleted_at')
->whereNotNull('mobile')
->pluck('mobile')
->mapWithKeys(fn ($m) => [ltrim((string) $m, '0') => true])
->toArray();
$panelCodes = Customer::whereNull('deleted_at')
->whereNotNull('moshtari_code')
->pluck('moshtari_code')
->mapWithKeys(fn ($c) => [(string) $c => true])
->toArray();
$zarinOnlyList = [];
foreach ($zarinCustomers as $item) {
if (!is_array($item)) {
continue;
}
if (count($debugSample) < 3) {
$debugSample[] = $item;
}
$code = $item['idHesab'] ?? $item['Code'] ?? $item['code'] ?? $item['id'] ?? null;
if ($code !== null && isset($panelCodes[(string) $code])) {
continue;
}
$phone = $this->extractZarinPhone($item);
$tel = ltrim($phone, '0');
if ($tel && isset($panelMobiles[$tel])) {
continue;
}
$zarinOnlyList[] = [
'code' => $code,
'name' => $this->extractZarinName($item),
'tel' => $phone,
'gid' => $item['GID'] ?? null,
'group' => $this->extractZarinGroup($item),
];
}
$zarinOnlyCount = count($zarinOnlyList);
$zarinOnlyResult = [
'data' => array_values($zarinOnlyList),
'total' => $zarinOnlyCount,
'current_page' => 1,
'last_page' => 1,
];
}
$payload = [
'panel_total' => $panelTotal,
'panel_registered' => $panelRegistered,
'panel_only' => $panelOnly,
'zarin_total' => $zarinTotal,
'zarin_only_count' => $zarinOnlyCount,
'zarin_fetch_error' => $zarinFetchError,
'zarin_http_status' => $zarinHttpStatus,
'panel_only_list' => $panelOnlyList,
'zarin_only_list' => $zarinOnlyResult,
];
if ($include && $request->boolean('raw')) {
$payload['zarin_debug_sample'] = $debugSample;
}
return response()->json($payload);
}
private function extractZarinPhone(array $item): string
{
$candidates = ['mobile', 'Mobile', 'tell', 'Tell', 'tel', 'Tel', 'Phone', 'CellPhone', 'Cell', 'MobileNo', 'Telephone', 'Mobile1'];
foreach ($candidates as $key) {
if (isset($item[$key]) && trim((string) $item[$key]) !== '') {
return trim((string) $item[$key]);
}
}
foreach ($item as $k => $v) {
if ($v === null || trim((string) $v) === '') {
continue;
}
$lk = strtolower((string) $k);
if (str_contains($lk, 'mobile') || str_contains($lk, 'tel')
|| str_contains($lk, 'phone') || str_contains($lk, 'cell')) {
return trim((string) $v);
}
}
return '';
}
public function syncPanelToZarin(): \Illuminate\Http\JsonResponse
{
$customers = Customer::whereNull('deleted_at')
->whereNull('moshtari_code')
->select('id', 'name', 'mobile')
->get();
$success = 0;
$failed = 0;
$errors = [];
foreach ($customers as $customer) {
$result = $this->sync()->registerCustomer($customer);
if ($result['ok'] ?? false) {
$success++;
} else {
$failed++;
$errors[] = ['name' => $customer->name, 'mobile' => $customer->mobile, 'error' => $result['message'] ?? ''];
}
}
return response()->json([
'ok' => true,
'success' => $success,
'failed' => $failed,
'errors' => $errors,
]);
}
public function importFromZarin(Request $request): \Illuminate\Http\JsonResponse
{
$validated = $request->validate([
'customers' => 'required|array',
'customers.*.code' => 'required|integer',
'customers.*.name' => 'required|string',
'customers.*.tel' => 'nullable|string',
]);
$success = 0;
$failed = 0;
$errors = [];
foreach ($validated['customers'] as $item) {
$mobile = Customer::normalizeMobile($item['tel'] ?? '') ?? '';
$fullName = trim($item['name']);
$code = (int) $item['code'];
if ($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();
}
$success++;
continue;
}
if ($mobile !== '') {
$byMobile = Customer::findActiveByMobile($mobile);
if ($byMobile) {
if (empty($byMobile->moshtari_code)) {
$byMobile->moshtari_code = $code;
if ($fullName !== '') {
$byMobile->fname = $fullName;
$byMobile->lname = '';
$byMobile->name = $fullName;
}
if (empty($byMobile->password)) {
$byMobile->password = $mobile;
}
$byMobile->save();
$success++;
continue;
}
$failed++;
$errors[] = [
'name' => $item['name'],
'code' => $item['code'],
'error' => 'شماره موبایل تکراری است',
];
continue;
}
}
$characters = '0123456789abcdefghijklmnopqrstuvwxyz';
do {
$referralId = '';
for ($i = 0; $i < 4; $i++) {
$referralId .= $characters[random_int(0, strlen($characters) - 1)];
}
} while (Customer::where('referral_id', $referralId)->exists());
try {
$customer = new Customer();
$customer->fname = $fullName;
$customer->lname = '';
$customer->name = $fullName;
$customer->mobile = $mobile;
$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 تنظیم نشده است.';
}
[$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();
}
$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 تنظیم نشده است.';
}
[$response, $httpError] = $this->zarinDecodeResponse($http);
if ($httpError) {
$this->sync()->markFailed($zarin, $response ?? $httpError, $httpError);
return false;
}
Log::info('Zarin wallet response', [
'data' => $data,
'response' => $response ?? null,
]);
} catch (\Exception $e) {
$this->sync()->markFailed($zarin, $e->getMessage());
return $e->getMessage();
}
if ($this->sync()->responseOk(is_array($response) ? $response : [])) {
$this->sync()->markSuccess($zarin, is_array($response) ? $response : [], $data);
return $data;
}
$this->sync()->markFailed($zarin, $response, 'ثبت سند ریالی در زرین ناموفق بود');
return false;
}
/**
* پاداش معرفی — همیشه روی حساب معرف (نه کاربر تازه‌ثبت‌نام‌شده).
*/
public function referrerGoldReward(Customer $invitedCustomer, float $gold, string $desc = 'پاداش بابت معرفی')
{
$referrer = $invitedCustomer->referrer();
if (!$referrer) {
return false;
}
return $this->goldReward([
'customer_id' => $referrer->id,
'value' => $gold,
'desc' => $desc,
]);
}
/**
* پاداش/هدیه طلایی — سند وزنی با korf=f (هدیه به مشتری).
*/
public function goldReward($data, ?Zarin $existing = null)
{
if (empty($data['customer_id']) && $existing) {
$data = array_merge($data, [
'customer_id' => $existing->customer_id,
'value' => $existing->amount,
'desc' => $existing->desc,
]);
}
if (!isset($data['customer_id'], $data['value'])) {
return request()->expectsJson()
? response()->json(['error' => 'Data missing: customer_id or value'], 422)
: false;
}
$customer = Customer::where('id', $data['customer_id'])->first();
if (!$customer) {
return false;
}
if ($customer->moshtari_code === null) {
$this->createCustomerInZarin($customer->id);
$customer = Customer::where('id', $data['customer_id'])->first();
}
if (!$customer || empty($customer->moshtari_code)) {
return false;
}
$amount = abs((float) $data['value']);
$desc = !empty($data['desc']) ? (string) $data['desc'] : 'پاداش';
$data['handler'] = 'goldReward';
$zarin = $existing ?? Zarin::create([
'customer_id' => $customer->id,
'desc' => $desc,
'amount' => $amount,
'date' => Carbon::now(),
'type' => 'طلا',
'created_at' => Carbon::now(),
'data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
]);
if ($existing && empty($existing->data)) {
$existing->update(['data' => json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)]);
}
$prices = $this->lastPriceData();
$mazaneh = (int) ($prices['buy'] ?? 0);
if ($mazaneh <= 0) {
$mazaneh = 1;
}
try {
$http = $this->zarinRequest([
'kind' => 'pooli',
'radif' => 1,
'korf' => 'f',
'poolorvazn' => 'vazn',
'meghdar' => $amount,
'fi' => $mazaneh,
'hesab' => $customer->moshtari_code,
'sharh' => $desc,
'ayar' => '750',
'movaghat' => '',
'idFactor' => '',
'uid' => $this->zarinUid('50', $zarin->id),
]);
if (!$http) {
$this->sync()->markFailed($zarin, null, 'تنظیمات زرین کامل نیست');
return 'تنظیمات زرین کامل نیست. zarin_api یا zarin_token تنظیم نشده است.';
}
[$response, $httpError] = $this->zarinDecodeResponse($http);
if ($httpError) {
$this->sync()->markFailed($zarin, $response ?? $httpError, $httpError);
return false;
}
} catch (\Exception $e) {
$this->sync()->markFailed($zarin, $e->getMessage());
return $e->getMessage();
}
if ($this->sync()->responseOk(is_array($response) ? $response : [])) {
$this->sync()->markSuccess($zarin, is_array($response) ? $response : [], $data);
return $data;
}
$this->sync()->markFailed($zarin, $response, 'ثبت پاداش طلایی در زرین ناموفق بود');
return false;
}
// direction: 1 = واریز طلا (خرید)، 0 = برداشت طلا (فروش)
public function walletGold($data, ?Zarin $existing = null)
{
if (empty($data['customer_id']) && $existing) {
$data = array_merge($data, [
'customer_id' => $existing->customer_id,
'value' => $existing->amount,
'direction' => str_contains((string) $existing->desc, 'برداشت') ? 0 : 1,
'withdraw_id' => $existing->withdraw_id,
]);
}
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 تنظیم نشده است.';
}
[$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;
}
public function createCustomerInZarin($customer_id)
{
$customer = Customer::findOrFail($customer_id);
if ($customer->moshtari_code != null) {
return $customer->moshtari_code;
}
$lock = Cache::lock('zarin:register-customer:' . $customer->id, 30);
if (!$lock->get()) {
$customer->refresh();
if ($customer->moshtari_code != null) {
return $customer->moshtari_code;
}
return 'ثبت مشتری در زرین در حال انجام است';
}
try {
$customer->refresh();
if ($customer->moshtari_code != null) {
return $customer->moshtari_code;
}
$name = $customer->fname ?: ($customer->name ?? ('مشتری ' . $customer->id));
$family = $customer->lname ?: '';
$mobile = $customer->mobile ?? '';
$params = [
'kind' => 'addhesab',
'name' => $name,
'family' => $family,
'tel' => $mobile,
'mobile' => $mobile,
'address' => trim(($customer->province ?? '') . ' ' . ($customer->city ?? '')) ?: 'ثبت نشده',
'codePosti' => '0000000000',
'nameG' => $this->zarinCustomerGroupName(),
'gender' => in_array($customer->gender, ['اقای', 'آقای'], true) ? 'm' : ($customer->gender == 'خانم' ? 'f' : ''),
'uid' => $this->zarinUid('40', $customer->id),
];
try {
$http = $this->zarinRequest($params);
if (!$http) {
return 'تنظیمات زرین کامل نیست. zarin_api یا zarin_token تنظیم نشده است.';
}
[$response, $httpError] = $this->zarinDecodeResponse($http);
if ($httpError) {
return $httpError;
}
$data = $this->extractResponseRow($response);
$idHesab = $data['idHesab'] ?? null;
if (!empty($idHesab)) {
$customer->moshtari_code = $idHesab;
$customer->save();
return $idHesab;
}
$detail = $data['msg'] ?? $data['message'] ?? $data['detail'] ?? null;
return $detail ? (string) $detail : 'zarin api error!!!';
} catch (\Exception $e) {
return $e->getMessage();
}
} finally {
$lock->release();
}
}
public function healthCheck()
{
$http = $this->zarinRequest([
'kind' => 'version',
]);
[$json, $error] = $this->zarinDecodeResponse($http);
if ($error) {
return response()->json([
'ok' => false,
'message' => $error,
'body' => $json,
], 422);
}
return response()->json([
'ok' => true,
'version' => $json['version'] ?? null,
'message' => $json['message'] ?? 'OK',
'raw' => $json,
]);
}
}