2026-07-23 20:24:58 +03:30
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
|
|
|
|
|
use App\Http\Controllers\Admin\ZarinController;
|
|
|
|
|
use App\Models\Customer;
|
2026-09-05 17:01:39 +03:30
|
|
|
use App\Models\Withdraw;
|
2026-07-23 20:24:58 +03:30
|
|
|
use App\Models\Zarin;
|
|
|
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
|
|
|
|
|
|
class ZarinSyncService
|
|
|
|
|
{
|
|
|
|
|
public const STATUS_PENDING = 'pending';
|
|
|
|
|
public const STATUS_SUCCESS = 'success';
|
|
|
|
|
public const STATUS_FAILED = 'failed';
|
|
|
|
|
|
|
|
|
|
public function responseOk(mixed $response): bool
|
|
|
|
|
{
|
|
|
|
|
if (! is_array($response)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($this->hasNumericIdKey($response)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$data = $response['data'] ?? null;
|
|
|
|
|
if (! is_array($data)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$row = array_is_list($data) ? ($data[0] ?? null) : $data;
|
|
|
|
|
|
|
|
|
|
return is_array($row) && $this->hasNumericIdKey($row);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function hasNumericIdKey(array $row): bool
|
|
|
|
|
{
|
2026-09-01 16:51:16 +03:30
|
|
|
foreach (['idFactor', 'idSanad', 'pkRadifSanad', 'idHesab', 'OK', 'shomareSanad'] as $key) {
|
2026-07-23 20:24:58 +03:30
|
|
|
if (! array_key_exists($key, $row)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
$value = $row[$key];
|
|
|
|
|
if ($value === null || $value === '' || $value === false) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (is_numeric($value) || (is_string($value) && trim($value) !== '')) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function buildLog(mixed $response = null, ?string $extra = null): string
|
|
|
|
|
{
|
|
|
|
|
$parts = array_filter([
|
|
|
|
|
$extra,
|
|
|
|
|
is_array($response) ? json_encode($response, JSON_UNESCAPED_UNICODE) : (is_string($response) ? $response : null),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return implode("\n", $parts);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function markSuccess(Zarin $row, array $response, ?array $payload = null): void
|
|
|
|
|
{
|
|
|
|
|
$flat = $this->flattenResponse($response);
|
|
|
|
|
|
|
|
|
|
$zarinId = $flat['OK']
|
|
|
|
|
?? $flat['idFactor']
|
|
|
|
|
?? $flat['idSanad']
|
|
|
|
|
?? $flat['pkRadifSanad']
|
2026-09-01 16:51:16 +03:30
|
|
|
?? $flat['shomareSanad']
|
2026-07-23 20:24:58 +03:30
|
|
|
?? $flat['idHesab']
|
|
|
|
|
?? null;
|
|
|
|
|
|
|
|
|
|
$shFactor = $flat['Sh_factor']
|
|
|
|
|
?? $flat['pkRadifSanad']
|
|
|
|
|
?? $flat['idRadifSanad']
|
|
|
|
|
?? null;
|
|
|
|
|
|
|
|
|
|
$update = [
|
|
|
|
|
'sh_factor' => $shFactor !== null ? (string) $shFactor : null,
|
|
|
|
|
'zarin_id' => $zarinId !== null ? (string) $zarinId : null,
|
|
|
|
|
'factor_number' => isset($flat['Factor_Number']) ? (string) $flat['Factor_Number'] : (isset($flat['idFactor']) ? (string) $flat['idFactor'] : null),
|
|
|
|
|
'res' => json_encode($response, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
|
|
|
|
|
'sync_status' => self::STATUS_SUCCESS,
|
|
|
|
|
'sync_log' => null,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if ($payload !== null) {
|
|
|
|
|
$update['data'] = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($this->hasSyncColumns()) {
|
|
|
|
|
Zarin::where('id', $row->id)->update($update);
|
|
|
|
|
} else {
|
|
|
|
|
unset($update['sync_status'], $update['sync_log']);
|
|
|
|
|
Zarin::where('id', $row->id)->update($update);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function markFailed(Zarin $row, mixed $response = null, ?string $extra = null): void
|
|
|
|
|
{
|
|
|
|
|
$update = [
|
|
|
|
|
'res' => is_array($response)
|
|
|
|
|
? json_encode($response, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
|
|
|
|
|
: (is_string($response) ? $response : null),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if ($this->hasSyncColumns()) {
|
|
|
|
|
$update['sync_status'] = self::STATUS_FAILED;
|
|
|
|
|
$update['sync_log'] = $this->buildLog($response, $extra);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Zarin::where('id', $row->id)->update($update);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function isFailed(Zarin $row): bool
|
|
|
|
|
{
|
|
|
|
|
if ($this->hasSyncColumns()) {
|
|
|
|
|
return ($row->sync_status ?? null) === self::STATUS_FAILED
|
|
|
|
|
|| (empty($row->zarin_id) && ($row->sync_status ?? null) !== self::STATUS_SUCCESS);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return empty($row->zarin_id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function registerOnCreate(Customer $customer): void
|
|
|
|
|
{
|
|
|
|
|
if (! empty($customer->moshtari_code) || ! $this->isSyncEnabled()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$customerId = (int) $customer->id;
|
|
|
|
|
|
|
|
|
|
dispatch(function () use ($customerId) {
|
|
|
|
|
$service = app(ZarinSyncService::class);
|
|
|
|
|
$customer = Customer::find($customerId);
|
|
|
|
|
if ($customer === null || ! empty($customer->moshtari_code) || ! $service->isSyncEnabled()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$result = $service->registerCustomer($customer);
|
|
|
|
|
if (! ($result['ok'] ?? false)) {
|
|
|
|
|
Log::warning('Zarin auto-register on customer create failed', [
|
|
|
|
|
'customer_id' => $customerId,
|
|
|
|
|
'message' => $result['message'] ?? null,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
Log::warning('Zarin auto-register on customer create threw', [
|
|
|
|
|
'customer_id' => $customerId,
|
|
|
|
|
'error' => $e->getMessage(),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
})->afterResponse();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function registerCustomer(Customer $customer): array
|
|
|
|
|
{
|
|
|
|
|
$customer->refresh();
|
|
|
|
|
|
|
|
|
|
if (! empty($customer->moshtari_code)) {
|
|
|
|
|
return [
|
|
|
|
|
'ok' => true,
|
|
|
|
|
'message' => 'مشتری قبلاً در زرین ثبت شده است',
|
|
|
|
|
'moshtari_code' => $customer->moshtari_code,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$controller = app(ZarinController::class);
|
|
|
|
|
$result = $controller->createCustomerInZarin($customer->id);
|
|
|
|
|
$customer->refresh();
|
|
|
|
|
|
2026-07-28 14:15:51 +03:30
|
|
|
// createCustomerInZarin در موفقیت idHesab برمیگرداند (عدد یا رشتهٔ عددی مثل "426")
|
|
|
|
|
// و در شکست پیام فارسی/خطا — قبلاً is_string همهٔ موفقیتهای رشتهای را خطا میگرفت.
|
|
|
|
|
if (! empty($customer->moshtari_code) || is_numeric($result)) {
|
|
|
|
|
$code = $customer->moshtari_code ?: $result;
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'ok' => true,
|
|
|
|
|
'message' => 'مشتری با موفقیت در زرین ثبت شد',
|
|
|
|
|
'moshtari_code' => $code,
|
|
|
|
|
];
|
2026-07-23 20:24:58 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [
|
2026-07-28 14:15:51 +03:30
|
|
|
'ok' => false,
|
|
|
|
|
'message' => is_string($result) && $result !== ''
|
|
|
|
|
? $result
|
|
|
|
|
: 'ثبت مشتری در زرین ناموفق بود',
|
2026-07-23 20:24:58 +03:30
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function retry(Zarin $row): array
|
|
|
|
|
{
|
|
|
|
|
if (! $this->isFailed($row) && ! empty($row->zarin_id)) {
|
|
|
|
|
return ['ok' => false, 'message' => 'این سند قبلاً در زرین ثبت شده است'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$payload = json_decode((string) $row->data, true) ?: [];
|
|
|
|
|
$handler = $payload['handler'] ?? $this->inferHandler($row);
|
|
|
|
|
$controller = app(ZarinController::class);
|
|
|
|
|
|
|
|
|
|
$result = match ($handler) {
|
|
|
|
|
'wallet' => $controller->wallet(array_merge($payload, ['_retry_zarin_id' => $row->id]), $row),
|
|
|
|
|
'walletGold' => $controller->walletGold(array_merge($payload, ['_retry_zarin_id' => $row->id]), $row),
|
|
|
|
|
'storeTrade' => $controller->storeTrade(array_merge($payload, ['_retry_zarin_id' => $row->id]), $row),
|
|
|
|
|
'goldReward' => $controller->goldReward(array_merge($payload, ['_retry_zarin_id' => $row->id]), $row),
|
|
|
|
|
default => false,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
$row->refresh();
|
|
|
|
|
|
|
|
|
|
if ($result !== false && ! empty($row->zarin_id)) {
|
|
|
|
|
return ['ok' => true, 'message' => 'سند با موفقیت در زرین ثبت شد', 'zarin' => $row];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'ok' => false,
|
|
|
|
|
'message' => $row->sync_log ?: 'ارسال مجدد به زرین ناموفق بود',
|
|
|
|
|
'zarin' => $row,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function inferHandler(Zarin $row): string
|
|
|
|
|
{
|
|
|
|
|
if ($row->order_id) {
|
|
|
|
|
return 'storeTrade';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($row->type === 'ريال') {
|
|
|
|
|
return 'wallet';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$desc = (string) $row->desc;
|
|
|
|
|
|
2026-07-28 18:55:59 +03:30
|
|
|
if (str_contains($desc, 'شارژ') || str_contains($desc, 'دشارژ') || str_contains($desc, 'برداشت از کیف') || str_contains($desc, 'شارژ کیف')) {
|
|
|
|
|
return 'walletGold';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// نوع تعدادی ذخیرهشده روی ردیف sync
|
|
|
|
|
$type = (string) $row->type;
|
|
|
|
|
if (in_array($type, ['سکه', 'شمش', 'کارساخته'], true)) {
|
2026-07-23 20:24:58 +03:30
|
|
|
return 'walletGold';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 'goldReward';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function isSyncEnabled(): bool
|
|
|
|
|
{
|
|
|
|
|
$config = app(ConfigService::class);
|
|
|
|
|
|
|
|
|
|
return $config->isZarinAccounting()
|
|
|
|
|
&& trim($config->getString('zarin_api', '')) !== ''
|
|
|
|
|
&& trim($config->getString('zarin_token', '')) !== '';
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-27 16:16:20 +03:30
|
|
|
public function unsyncedRowsQuery()
|
|
|
|
|
{
|
|
|
|
|
return Zarin::query()->where(function ($q) {
|
|
|
|
|
$q->where(function ($inner) {
|
|
|
|
|
$inner->whereNull('zarin_id')->orWhere('zarin_id', '');
|
|
|
|
|
})->orWhere('sync_status', self::STATUS_FAILED);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return array{failed_rows:int,missing_withdraws:int,missing_orders:int,total:int}
|
|
|
|
|
*/
|
|
|
|
|
public function pendingSummary(?int $customerId = null): array
|
|
|
|
|
{
|
|
|
|
|
$failedRows = (int) $this->unsyncedRowsQuery()
|
|
|
|
|
->when($customerId, fn ($q) => $q->where('customer_id', $customerId))
|
|
|
|
|
->count();
|
|
|
|
|
$missingWithdraws = count($this->missingWithdrawIds(200, $customerId));
|
|
|
|
|
$missingOrders = count($this->missingOrderIds(200, $customerId));
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'failed_rows' => $failedRows,
|
|
|
|
|
'missing_withdraws' => $missingWithdraws,
|
|
|
|
|
'missing_orders' => $missingOrders,
|
|
|
|
|
'total' => $failedRows + $missingWithdraws + $missingOrders,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return list<array<string, mixed>>
|
|
|
|
|
*/
|
|
|
|
|
public function pendingDocs(?int $customerId = null, int $limit = 200): array
|
|
|
|
|
{
|
|
|
|
|
$limit = max(1, min(500, $limit));
|
|
|
|
|
$rows = [];
|
|
|
|
|
|
|
|
|
|
$local = $this->unsyncedRowsQuery()
|
|
|
|
|
->with('customer')
|
|
|
|
|
->when($customerId, fn ($q) => $q->where('customer_id', $customerId))
|
|
|
|
|
->orderByDesc('id')
|
|
|
|
|
->limit($limit)
|
|
|
|
|
->get();
|
|
|
|
|
|
|
|
|
|
foreach ($local as $row) {
|
|
|
|
|
$arr = $row->toArray();
|
|
|
|
|
$arr['issue_kind'] = 'failed';
|
|
|
|
|
$arr['sync_target'] = ['type' => 'row', 'id' => (int) $row->id];
|
|
|
|
|
$rows[] = $arr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$remaining = $limit - count($rows);
|
|
|
|
|
if ($remaining > 0) {
|
|
|
|
|
$withdrawIds = $this->missingWithdrawIds($remaining, $customerId);
|
|
|
|
|
if ($withdrawIds !== []) {
|
|
|
|
|
$withdraws = \App\Models\Withdraw::query()
|
|
|
|
|
->with('customer')
|
|
|
|
|
->whereIn('id', $withdrawIds)
|
|
|
|
|
->orderByDesc('id')
|
|
|
|
|
->get();
|
|
|
|
|
foreach ($withdraws as $w) {
|
|
|
|
|
$cash = (float) ($w->cash ?? 0);
|
|
|
|
|
$gold = (float) ($w->gold ?? 0);
|
|
|
|
|
$isGold = abs($gold) > 1e-9;
|
|
|
|
|
$rows[] = [
|
|
|
|
|
'id' => null,
|
|
|
|
|
'customer_id' => $w->customer_id,
|
|
|
|
|
'customer' => $w->customer,
|
|
|
|
|
'amount' => $isGold ? abs($gold) : abs($cash),
|
|
|
|
|
'type' => $isGold ? 'طلا' : 'ريال',
|
|
|
|
|
'desc' => 'شارژ/برداشت بدون سند محلی #'.$w->id.($w->note ? ' — '.$w->note : ''),
|
|
|
|
|
'withdraw_id' => (int) $w->id,
|
|
|
|
|
'order_id' => null,
|
|
|
|
|
'sync_status' => 'missing',
|
|
|
|
|
'sync_log' => 'سند در زرین ساخته نشده (جاافتاده)',
|
|
|
|
|
'zarin_id' => null,
|
|
|
|
|
'created_at' => optional($w->created_at)?->toDateTimeString(),
|
|
|
|
|
'issue_kind' => 'missing',
|
|
|
|
|
'sync_target' => ['type' => 'withdraw', 'id' => (int) $w->id],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$remaining = $limit - count($rows);
|
|
|
|
|
if ($remaining > 0) {
|
|
|
|
|
$orderIds = $this->missingOrderIds($remaining, $customerId);
|
|
|
|
|
if ($orderIds !== []) {
|
|
|
|
|
$orders = \App\Models\Order::query()
|
|
|
|
|
->with(['customer', 'product'])
|
|
|
|
|
->whereIn('id', $orderIds)
|
|
|
|
|
->orderByDesc('id')
|
|
|
|
|
->get();
|
|
|
|
|
foreach ($orders as $order) {
|
|
|
|
|
$direction = (int) ($order->direction ?? 0);
|
|
|
|
|
$dirLabel = $direction === 1 ? 'خرید' : ($direction === 2 ? 'فروش' : 'سفارش');
|
|
|
|
|
$productTitle = $order->product->title ?? 'محصول';
|
|
|
|
|
$rows[] = [
|
|
|
|
|
'id' => null,
|
|
|
|
|
'customer_id' => $order->customer_id,
|
|
|
|
|
'customer' => $order->customer,
|
|
|
|
|
'amount' => (float) ($order->amount ?? 0),
|
|
|
|
|
'type' => 'طلا',
|
|
|
|
|
'desc' => $dirLabel.' '.$productTitle.' بدون سند محلی #'.$order->id,
|
|
|
|
|
'withdraw_id' => null,
|
|
|
|
|
'order_id' => (int) $order->id,
|
|
|
|
|
'sync_status' => 'missing',
|
|
|
|
|
'sync_log' => 'سند در زرین ساخته نشده (جاافتاده)',
|
|
|
|
|
'zarin_id' => null,
|
|
|
|
|
'created_at' => optional($order->pay_at ?? $order->created_at)?->toDateTimeString(),
|
|
|
|
|
'issue_kind' => 'missing',
|
|
|
|
|
'sync_target' => ['type' => 'order', 'id' => (int) $order->id],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
usort($rows, function ($a, $b) {
|
|
|
|
|
return strcmp((string) ($b['created_at'] ?? ''), (string) ($a['created_at'] ?? ''));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return $rows;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return array{ok:bool,message:string}
|
|
|
|
|
*/
|
|
|
|
|
public function syncOne(string $type, int $id): array
|
|
|
|
|
{
|
|
|
|
|
if (! $this->isSyncEnabled()) {
|
|
|
|
|
return ['ok' => false, 'message' => 'همگامسازی زرین فعال نیست یا تنظیمات ناقص است'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$controller = app(ZarinController::class);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if ($type === 'row') {
|
|
|
|
|
$row = Zarin::with('customer')->findOrFail($id);
|
|
|
|
|
|
|
|
|
|
return $this->retry($row);
|
|
|
|
|
}
|
|
|
|
|
if ($type === 'withdraw') {
|
|
|
|
|
$ok = $this->backfillWithdraw($controller, $id);
|
|
|
|
|
|
|
|
|
|
return ['ok' => $ok, 'message' => $ok ? 'سند شارژ/برداشت ثبت شد' : 'ارسال شارژ/برداشت ناموفق بود'];
|
|
|
|
|
}
|
|
|
|
|
if ($type === 'order') {
|
|
|
|
|
$ok = $this->backfillOrder($controller, $id);
|
|
|
|
|
|
|
|
|
|
return ['ok' => $ok, 'message' => $ok ? 'سند سفارش ثبت شد' : 'ارسال سفارش ناموفق بود'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ['ok' => false, 'message' => 'نوع نامعتبر است'];
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
report($e);
|
|
|
|
|
|
|
|
|
|
return ['ok' => false, 'message' => $e->getMessage()];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return array{ok:bool,retried:int,backfilled_withdraws:int,backfilled_orders:int,failed:int,errors:list<string>,summary:array}
|
|
|
|
|
*/
|
|
|
|
|
public function syncAllPending(int $limit = 80, ?int $customerId = null): array
|
|
|
|
|
{
|
|
|
|
|
if (! $this->isSyncEnabled()) {
|
|
|
|
|
return [
|
|
|
|
|
'ok' => false,
|
|
|
|
|
'retried' => 0,
|
|
|
|
|
'backfilled_withdraws' => 0,
|
|
|
|
|
'backfilled_orders' => 0,
|
|
|
|
|
'failed' => 0,
|
|
|
|
|
'errors' => ['همگامسازی زرین فعال نیست یا تنظیمات ناقص است'],
|
|
|
|
|
'summary' => $this->pendingSummary($customerId),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$retried = 0;
|
|
|
|
|
$backfilledWithdraws = 0;
|
|
|
|
|
$backfilledOrders = 0;
|
|
|
|
|
$failed = 0;
|
|
|
|
|
$errors = [];
|
|
|
|
|
$controller = app(ZarinController::class);
|
|
|
|
|
$used = 0;
|
|
|
|
|
|
|
|
|
|
$rows = $this->unsyncedRowsQuery()
|
|
|
|
|
->when($customerId, fn ($q) => $q->where('customer_id', $customerId))
|
|
|
|
|
->orderBy('id')
|
|
|
|
|
->limit($limit)
|
|
|
|
|
->get();
|
|
|
|
|
foreach ($rows as $row) {
|
|
|
|
|
$used++;
|
|
|
|
|
try {
|
|
|
|
|
$result = $this->retry($row);
|
|
|
|
|
if ($result['ok'] ?? false) {
|
|
|
|
|
$retried++;
|
|
|
|
|
} else {
|
|
|
|
|
$failed++;
|
|
|
|
|
$errors[] = 'سند #'.$row->id.': '.($result['message'] ?? 'ناموفق');
|
|
|
|
|
}
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
$failed++;
|
|
|
|
|
$errors[] = 'سند #'.$row->id.': '.$e->getMessage();
|
|
|
|
|
report($e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ($this->missingWithdrawIds(max(0, $limit - $used), $customerId) as $withdrawId) {
|
|
|
|
|
$used++;
|
|
|
|
|
try {
|
|
|
|
|
$ok = $this->backfillWithdraw($controller, (int) $withdrawId);
|
|
|
|
|
if ($ok) {
|
|
|
|
|
$backfilledWithdraws++;
|
|
|
|
|
} else {
|
|
|
|
|
$failed++;
|
|
|
|
|
$errors[] = 'شارژ/برداشت #'.$withdrawId.': ارسال به زرین ناموفق';
|
|
|
|
|
}
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
$failed++;
|
|
|
|
|
$errors[] = 'شارژ/برداشت #'.$withdrawId.': '.$e->getMessage();
|
|
|
|
|
report($e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ($this->missingOrderIds(max(0, $limit - $used), $customerId) as $orderId) {
|
|
|
|
|
$used++;
|
|
|
|
|
try {
|
|
|
|
|
$ok = $this->backfillOrder($controller, (int) $orderId);
|
|
|
|
|
if ($ok) {
|
|
|
|
|
$backfilledOrders++;
|
|
|
|
|
} else {
|
|
|
|
|
$failed++;
|
|
|
|
|
$errors[] = 'سفارش #'.$orderId.': ارسال به زرین ناموفق';
|
|
|
|
|
}
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
$failed++;
|
|
|
|
|
$errors[] = 'سفارش #'.$orderId.': '.$e->getMessage();
|
|
|
|
|
report($e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'ok' => $failed === 0,
|
|
|
|
|
'retried' => $retried,
|
|
|
|
|
'backfilled_withdraws' => $backfilledWithdraws,
|
|
|
|
|
'backfilled_orders' => $backfilledOrders,
|
|
|
|
|
'failed' => $failed,
|
|
|
|
|
'errors' => array_slice($errors, 0, 20),
|
|
|
|
|
'summary' => $this->pendingSummary($customerId),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @return list<int> */
|
|
|
|
|
private function missingWithdrawIds(int $limit, ?int $customerId = null): array
|
|
|
|
|
{
|
|
|
|
|
if ($limit <= 0 || ! Schema::hasTable('withdraws')) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$linked = Zarin::query()
|
|
|
|
|
->whereNotNull('withdraw_id')
|
|
|
|
|
->when($customerId, fn ($q) => $q->where('customer_id', $customerId))
|
|
|
|
|
->pluck('withdraw_id')
|
|
|
|
|
->all();
|
|
|
|
|
|
|
|
|
|
return \App\Models\Withdraw::query()
|
|
|
|
|
->where('sts', 4)
|
|
|
|
|
->where(function ($q) {
|
|
|
|
|
$q->where('cash', '!=', 0)->orWhere('gold', '!=', 0);
|
|
|
|
|
})
|
|
|
|
|
->where(function ($q) {
|
|
|
|
|
$q->whereNull('note')
|
|
|
|
|
->orWhere('note', 'not like', 'fix:intval-gold-loss%');
|
|
|
|
|
})
|
|
|
|
|
->when($customerId, fn ($q) => $q->where('customer_id', $customerId))
|
|
|
|
|
->when($linked !== [], fn ($q) => $q->whereNotIn('id', $linked))
|
|
|
|
|
->orderBy('id')
|
|
|
|
|
->limit($limit)
|
|
|
|
|
->pluck('id')
|
|
|
|
|
->map(fn ($id) => (int) $id)
|
|
|
|
|
->all();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @return list<int> */
|
|
|
|
|
private function missingOrderIds(int $limit, ?int $customerId = null): array
|
|
|
|
|
{
|
|
|
|
|
if ($limit <= 0 || ! Schema::hasTable('orders')) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$linked = Zarin::query()
|
|
|
|
|
->whereNotNull('order_id')
|
|
|
|
|
->when($customerId, fn ($q) => $q->where('customer_id', $customerId))
|
|
|
|
|
->pluck('order_id')
|
|
|
|
|
->all();
|
|
|
|
|
|
|
|
|
|
return \App\Models\Order::query()
|
|
|
|
|
->where('sts', 2)
|
|
|
|
|
->where('payment_sts', 4)
|
|
|
|
|
->whereIn('direction', [1, 2])
|
|
|
|
|
->when($customerId, fn ($q) => $q->where('customer_id', $customerId))
|
|
|
|
|
->when($linked !== [], fn ($q) => $q->whereNotIn('id', $linked))
|
|
|
|
|
->orderBy('id')
|
|
|
|
|
->limit($limit)
|
|
|
|
|
->pluck('id')
|
|
|
|
|
->map(fn ($id) => (int) $id)
|
|
|
|
|
->all();
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-05 17:01:39 +03:30
|
|
|
/**
|
|
|
|
|
* درخواست برداشت مشتری cash را مثبت ذخیره میکند (type=2) — علامت مبلغ برای جهت سند کافی نیست.
|
|
|
|
|
* 0 = واریز، 1 = برداشت
|
|
|
|
|
*/
|
|
|
|
|
public function rialWalletDirectionFromWithdraw(Withdraw $withdraw): int
|
|
|
|
|
{
|
|
|
|
|
if ((int) ($withdraw->type ?? 0) === 2) {
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
$note = (string) ($withdraw->note ?? '');
|
|
|
|
|
if ($note !== '' && str_contains($note, 'برداشت')) {
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ((float) ($withdraw->cash ?? 0)) >= 0 ? 0 : 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** شرح محلی سند ریالی: «برداشت وجه» برداشت است؛ «واریز وجه» واریز. */
|
|
|
|
|
public function rialWalletDirectionFromDesc(string $desc): int
|
|
|
|
|
{
|
|
|
|
|
return str_contains($desc, 'برداشت') ? 1 : 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-27 16:16:20 +03:30
|
|
|
private function backfillWithdraw(ZarinController $controller, int $withdrawId): bool
|
|
|
|
|
{
|
|
|
|
|
$withdraw = \App\Models\Withdraw::query()->find($withdrawId);
|
|
|
|
|
if (! $withdraw || ! $withdraw->customer_id) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$cash = (float) ($withdraw->cash ?? 0);
|
|
|
|
|
$gold = (float) ($withdraw->gold ?? 0);
|
|
|
|
|
|
|
|
|
|
if (abs($cash) > 1e-9) {
|
|
|
|
|
$result = $controller->wallet([
|
|
|
|
|
'customer_id' => (int) $withdraw->customer_id,
|
|
|
|
|
'value' => abs($cash),
|
2026-09-05 17:01:39 +03:30
|
|
|
'direction' => $this->rialWalletDirectionFromWithdraw($withdraw),
|
2026-08-27 16:16:20 +03:30
|
|
|
'withdraw_id' => $withdrawId,
|
|
|
|
|
'note' => $withdraw->note,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return $result !== false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (abs($gold) > 1e-9) {
|
|
|
|
|
$result = $controller->walletGold([
|
|
|
|
|
'customer_id' => (int) $withdraw->customer_id,
|
|
|
|
|
'value' => abs($gold),
|
|
|
|
|
'direction' => $gold >= 0 ? 1 : 0,
|
|
|
|
|
'withdraw_id' => $withdrawId,
|
|
|
|
|
'note' => $withdraw->note,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return $result !== false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function backfillOrder(ZarinController $controller, int $orderId): bool
|
|
|
|
|
{
|
|
|
|
|
$order = \App\Models\Order::query()->with('product')->find($orderId);
|
|
|
|
|
if (! $order || ! $order->customer_id) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$product = $order->product;
|
|
|
|
|
$payload = [
|
|
|
|
|
'customer_id' => (int) $order->customer_id,
|
|
|
|
|
'amount' => (float) $order->amount,
|
|
|
|
|
'direction' => (int) $order->direction,
|
|
|
|
|
'price' => (float) $order->price,
|
|
|
|
|
'total_price' => (float) $order->total_price,
|
|
|
|
|
'pay_at' => $order->pay_at ?? $order->created_at,
|
|
|
|
|
'product_id' => $order->product_id ? (int) $order->product_id : null,
|
|
|
|
|
'product' => $product ? [
|
|
|
|
|
'id' => (int) $product->id,
|
|
|
|
|
'title' => $product->title,
|
|
|
|
|
'group_type' => $product->group_type,
|
|
|
|
|
'metal_id' => $product->metal_id,
|
|
|
|
|
'amount' => $product->amount,
|
|
|
|
|
'unit' => $product->unit,
|
|
|
|
|
'trade_by_quantity' => $product->trade_by_quantity,
|
|
|
|
|
] : ['metal_id' => 1],
|
|
|
|
|
'order' => ['id' => $orderId, 'product_id' => $order->product_id],
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
$result = $controller->storeTrade($payload);
|
|
|
|
|
|
|
|
|
|
return $result !== false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-23 20:24:58 +03:30
|
|
|
private function flattenResponse(array $response): array
|
|
|
|
|
{
|
|
|
|
|
$data = $response['data'] ?? null;
|
|
|
|
|
if (is_array($data)) {
|
|
|
|
|
$row = array_is_list($data) ? ($data[0] ?? []) : $data;
|
|
|
|
|
if (is_array($row)) {
|
|
|
|
|
return array_merge($response, $row);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $response;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function hasSyncColumns(): bool
|
|
|
|
|
{
|
|
|
|
|
return Schema::hasTable('zarin') && Schema::hasColumn('zarin', 'sync_status');
|
|
|
|
|
}
|
|
|
|
|
}
|