Files
boursecu_server/app/Http/Controllers/Admin/ZarinController.php
T
2026-07-23 20:24:58 +03:30

1332 lines
45 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)
{
$url = $this->zarinAccountingUrl();
$token = $this->zarinToken();
if (!$url || !$token) {
return null;
}
$params['token'] = $token;
return Http::withoutVerifying()
->timeout(15)
->get($url, $params);
}
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;
}
/** نام گروه حساب مشتری در زرین — برای جداسازی چند پنل روی یک سرور زرین */
private function zarinCustomerGroupName(): string
{
$nameG = trim((string) $this->Config('zarin_name_g'));
if ($nameG !== '') {
return $nameG;
}
return trim((string) $this->Config('name_fa'));
}
/**
* پیشوند عددی 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;
}
private function findZarinKarForProduct(Product $product, $goroheKar = null)
{
$goroheKar = $goroheKar ?: $this->zarinDefaultGoroheKar();
$productName = $this->normalizeZarinName($product->title);
for ($page = 1; $page <= 10; $page++) {
$params = [
'kind' => 'infokarbarcodi',
'id' => $goroheKar,
'vazneat' => 0,
'pageNum' => $page,
'detailCount' => 250,
];
$response = $this->zarinRequest($params);
if (!$response || !$response->successful()) {
return null;
}
$body = $response->json();
$rows = $this->zarinRowsFromBody($body);
if (empty($rows)) {
break;
}
foreach ($rows as $row) {
if (!is_array($row)) {
continue;
}
$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) {
continue;
}
if (
$rowName === $productName ||
$this->containsText($rowName, $productName) ||
$this->containsText($productName, $rowName)
) {
return [
'idKar' => $idKar,
'row' => $row,
];
}
}
if (count($rows) < 250) {
break;
}
}
return null;
}
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 قابل استفاده برای sabtSanadTedadi از لیست 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);
}
/** همه مشتریان زرین را از API می‌گیرد و برمی‌گرداند */
private function fetchAllZarinCustomers(): array
{
try {
$response = $this->zarinRequest([
'kind' => 'Get100Account',
]);
if ($response && $response->successful()) {
$rows = $this->zarinRowsFromBody($response->json());
if (!empty($rows)) {
return $rows;
}
}
$response = $this->zarinRequest([
'kind' => 'listhesabha',
]);
if ($response && $response->successful()) {
return $this->zarinRowsFromBody($response->json());
}
return [];
} catch (\Exception $e) {
return [];
}
}
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;
$zarinOnlyResult = ['data' => [], 'total' => 0, 'current_page' => 1, 'last_page' => 1];
$debugSample = [];
$include = $request->boolean('include_zarin') || $request->boolean('include_tahesab');
if ($include) {
$zarinCustomers = $this->fetchAllZarinCustomers();
$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;
}
$name = $item['name'] ?? $item['Name'] ?? '';
$family = $item['family'] ?? $item['Family'] ?? '';
$fullName = trim($name . ' ' . $family);
$zarinOnlyList[] = [
'code' => $code,
'name' => $fullName !== '' ? $fullName : ($item['Name'] ?? ''),
'tel' => $phone,
'gid' => $item['GID'] ?? null,
'group' => $item['GoroupName'] ?? ($item['group'] ?? ''),
];
}
$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,
'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', 'tel', 'Tel', 'Tell', '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 = $http->json();
if (!is_array($response)) {
$response = json_decode($http->body(), true);
}
} 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 = $http->json();
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 = $http->json();
} 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 = $http->json();
} 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 = $http->json();
$data = is_array($response) ? $this->extractResponseRow($response) : [];
$idHesab = $data['idHesab'] ?? null;
if (!empty($idHesab)) {
$customer->moshtari_code = $idHesab;
$customer->save();
return $idHesab;
}
} catch (\Exception $e) {
return $e->getMessage();
}
return 'zarin api error!!!';
} finally {
$lock->release();
}
}
public function healthCheck()
{
$http = $this->zarinRequest([
'kind' => 'version',
]);
if (!$http) {
return $this->zarinConfigError();
}
return $http->json() ?? $http->body();
}
}