2026-07-05 16:43:34 +03:30
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
|
|
|
|
|
use App\Models\Customer;
|
|
|
|
|
use App\Models\Order;
|
|
|
|
|
use App\Models\PargarSyncLog;
|
2026-07-20 15:52:42 +03:30
|
|
|
use App\Models\Product;
|
2026-07-26 14:15:26 +03:30
|
|
|
use Illuminate\Database\QueryException;
|
2026-07-26 21:17:57 +03:30
|
|
|
use Illuminate\Support\Facades\DB;
|
2026-07-05 16:43:34 +03:30
|
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
|
|
|
|
|
|
class PargarSyncService
|
|
|
|
|
{
|
|
|
|
|
public const STATUS_PENDING = 'pending';
|
|
|
|
|
public const STATUS_SUCCESS = 'success';
|
|
|
|
|
public const STATUS_FAILED = 'failed';
|
|
|
|
|
|
2026-07-20 15:52:42 +03:30
|
|
|
public const TYPE_CUSTOMER = 'customer';
|
|
|
|
|
public const TYPE_ORDER = 'order';
|
|
|
|
|
public const TYPE_MANDE = 'mande';
|
|
|
|
|
public const TYPE_DOCUMENT = 'document';
|
|
|
|
|
public const TYPE_WALLET_CASH = 'wallet_cash';
|
|
|
|
|
public const TYPE_WALLET_GOLD = 'wallet_gold';
|
|
|
|
|
|
|
|
|
|
public const PAYMENT_CASH = 'cash';
|
|
|
|
|
public const PAYMENT_MAIN_GOLD = 'main_gold';
|
|
|
|
|
public const PAYMENT_OTHER_PRODUCT = 'other_product';
|
|
|
|
|
|
2026-08-18 17:57:22 +03:30
|
|
|
/** فقط صفر اعشاری/نویز شناور رد شود — سند ریز (۰٫۰۰۴ گرم جایزه) باید برود */
|
|
|
|
|
public const WALLET_DOC_EPSILON = 1.0e-12;
|
|
|
|
|
|
|
|
|
|
public static function isWalletDocAmount(float $amount): bool
|
|
|
|
|
{
|
|
|
|
|
return abs($amount) >= self::WALLET_DOC_EPSILON;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-05 16:43:34 +03:30
|
|
|
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(PargarSyncLog $log, ?string $ref = null, mixed $response = null, ?array $payload = null): void
|
|
|
|
|
{
|
|
|
|
|
$update = [
|
|
|
|
|
'pargar_ref' => $ref,
|
|
|
|
|
'res' => is_array($response)
|
|
|
|
|
? json_encode($response, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
|
|
|
|
|
: (is_string($response) ? $response : null),
|
|
|
|
|
'sync_status' => self::STATUS_SUCCESS,
|
|
|
|
|
'sync_log' => null,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if ($payload !== null) {
|
|
|
|
|
$update['data'] = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-25 12:02:58 +03:30
|
|
|
// لحظهٔ ثبت سند در پرگار: ماندهٔ قدیمیتر از این لحظه دیگر معتبر نیست
|
|
|
|
|
if (PargarSyncLog::hasConfirmedAtColumn()) {
|
|
|
|
|
$update['confirmed_at'] = now();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-05 16:43:34 +03:30
|
|
|
PargarSyncLog::where('id', $log->id)->update($update);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function markFailed(PargarSyncLog $log, mixed $response = null, ?string $extra = null): void
|
|
|
|
|
{
|
|
|
|
|
PargarSyncLog::where('id', $log->id)->update([
|
|
|
|
|
'res' => is_array($response)
|
|
|
|
|
? json_encode($response, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
|
|
|
|
|
: (is_string($response) ? $response : null),
|
|
|
|
|
'sync_status' => self::STATUS_FAILED,
|
|
|
|
|
'sync_log' => $this->buildLog($response, $extra),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function isFailed(PargarSyncLog $log): bool
|
|
|
|
|
{
|
|
|
|
|
return ($log->sync_status ?? null) === self::STATUS_FAILED
|
|
|
|
|
|| (empty($log->pargar_ref) && ($log->sync_status ?? null) !== self::STATUS_SUCCESS);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function registerOnCreate(Customer $customer): void
|
|
|
|
|
{
|
2026-07-19 15:53:05 +03:30
|
|
|
if ($customer->hasValidPargarAccountId() || ! $this->isSyncEnabled()) {
|
2026-07-05 16:43:34 +03:30
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$customerId = (int) $customer->id;
|
|
|
|
|
|
|
|
|
|
dispatch(function () use ($customerId) {
|
|
|
|
|
$service = app(self::class);
|
|
|
|
|
if (! $service->isSyncEnabled()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$customer = Customer::find($customerId);
|
2026-07-19 15:53:05 +03:30
|
|
|
if ($customer === null || $customer->hasValidPargarAccountId()) {
|
2026-07-05 16:43:34 +03:30
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$service->queueCustomerExport($customer);
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
Log::warning('Pargar auto-queue on customer create failed', [
|
|
|
|
|
'customer_id' => $customerId,
|
|
|
|
|
'error' => $e->getMessage(),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
})->afterResponse();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function queueCustomerExport(Customer $customer): PargarSyncLog
|
|
|
|
|
{
|
|
|
|
|
$existing = PargarSyncLog::query()
|
|
|
|
|
->where('customer_id', $customer->id)
|
2026-07-20 15:52:42 +03:30
|
|
|
->where('type', self::TYPE_CUSTOMER)
|
2026-07-05 16:43:34 +03:30
|
|
|
->where('sync_status', self::STATUS_PENDING)
|
|
|
|
|
->latest('id')
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
if ($existing) {
|
|
|
|
|
return $existing;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return PargarSyncLog::create([
|
|
|
|
|
'customer_id' => $customer->id,
|
|
|
|
|
'desc' => 'در انتظار دریافت توسط پرگار',
|
2026-07-20 15:52:42 +03:30
|
|
|
'type' => self::TYPE_CUSTOMER,
|
2026-07-05 16:43:34 +03:30
|
|
|
'sync_status' => self::STATUS_PENDING,
|
|
|
|
|
'data' => json_encode(['handler' => 'exportCustomer'], JSON_UNESCAPED_UNICODE),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function registerCustomer(Customer $customer): array
|
|
|
|
|
{
|
|
|
|
|
$customer->refresh();
|
|
|
|
|
|
2026-07-19 15:53:05 +03:30
|
|
|
if ($customer->hasValidPargarAccountId()) {
|
2026-07-05 16:43:34 +03:30
|
|
|
return [
|
|
|
|
|
'ok' => true,
|
|
|
|
|
'message' => 'مشتری قبلاً در پرگار ثبت شده است',
|
|
|
|
|
'pargar_account_id' => $customer->pargar_account_id,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->queueCustomerExport($customer);
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'ok' => true,
|
|
|
|
|
'message' => 'مشتری در صف دریافت پرگار قرار گرفت',
|
|
|
|
|
'pargar_account_id' => null,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-01 14:04:01 +03:30
|
|
|
/**
|
|
|
|
|
* سقف تاریخی ستون amount وقتی هنوز DOUBLE(8,2) است (~۹۹۹٬۹۹۹٫۹۹).
|
|
|
|
|
* مبلغ واقعی همیشه در data.signed_amount نگه داشته میشود.
|
|
|
|
|
*/
|
|
|
|
|
private const AMOUNT_COLUMN_SAFE_MAX = 999999.99;
|
|
|
|
|
|
2026-07-20 15:52:42 +03:30
|
|
|
/**
|
|
|
|
|
* صف سند کیف برای سینک مانده پرگار با اپ.
|
|
|
|
|
*
|
2026-08-01 14:04:01 +03:30
|
|
|
* سند pending باید حتماً ساخته شود؛ اگر ستون amount مبلغ بزرگ را نپذیرد،
|
|
|
|
|
* بدون amount ثبت میشود تا حرکت کیف بیسندی نماند و مانده قفل نشود.
|
|
|
|
|
*
|
2026-07-20 15:52:42 +03:30
|
|
|
* @param 'cash'|'gold' $kind
|
|
|
|
|
* @param array{withdraw_id?:int|null,note?:string|null,product_id?:int|null,source?:string|null} $meta
|
|
|
|
|
*/
|
|
|
|
|
public function queueWalletDocument(
|
|
|
|
|
Customer $customer,
|
|
|
|
|
string $kind,
|
|
|
|
|
float $signedAmount,
|
|
|
|
|
array $meta = []
|
|
|
|
|
): ?PargarSyncLog {
|
|
|
|
|
if (! $this->isSyncEnabled() || ! $this->hasSyncTable()) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-18 17:57:22 +03:30
|
|
|
if (! self::isWalletDocAmount($signedAmount)) {
|
2026-07-20 15:52:42 +03:30
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($kind === 'gold') {
|
|
|
|
|
$productId = (int) ($meta['product_id'] ?? 0);
|
|
|
|
|
if ($productId > 0 && ! app(ConfigService::class)->isMainWalletProduct($productId)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
} elseif ($kind !== 'cash') {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$withdrawId = isset($meta['withdraw_id']) ? (int) $meta['withdraw_id'] : null;
|
|
|
|
|
$type = $kind === 'cash' ? self::TYPE_WALLET_CASH : self::TYPE_WALLET_GOLD;
|
|
|
|
|
|
|
|
|
|
if ($withdrawId) {
|
|
|
|
|
$existing = PargarSyncLog::query()
|
|
|
|
|
->where('withdraw_id', $withdrawId)
|
|
|
|
|
->where('type', $type)
|
|
|
|
|
->latest('id')
|
|
|
|
|
->first();
|
|
|
|
|
if ($existing) {
|
|
|
|
|
return $existing;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$isDeposit = $signedAmount > 0;
|
2026-08-03 11:51:16 +03:30
|
|
|
$note = isset($meta['note']) ? trim((string) $meta['note']) : '';
|
|
|
|
|
$docDesc = \App\Http\Controllers\Controller::accountingWalletDesc($isDeposit, $note !== '' ? $note : null);
|
|
|
|
|
$desc = $docDesc . ' — ارسال به پرگار';
|
2026-07-20 15:52:42 +03:30
|
|
|
|
2026-08-01 14:04:01 +03:30
|
|
|
$absAmount = abs($signedAmount);
|
|
|
|
|
// اگر هنوز مایگریشن widen اجرا نشده، مبلغ بزرگ را از همان اول در ستون نگذار
|
|
|
|
|
// تا insert نترکد؛ خروجی/مانده از signed_amount میخوانند.
|
|
|
|
|
$columnAmount = $absAmount <= self::AMOUNT_COLUMN_SAFE_MAX ? $absAmount : null;
|
|
|
|
|
|
2026-07-26 14:15:26 +03:30
|
|
|
$attributes = [
|
2026-07-20 15:52:42 +03:30
|
|
|
'customer_id' => $customer->id,
|
2026-08-01 14:04:01 +03:30
|
|
|
'amount' => $columnAmount,
|
2026-07-20 15:52:42 +03:30
|
|
|
'desc' => $desc,
|
|
|
|
|
'withdraw_id' => $withdrawId,
|
|
|
|
|
'date' => now(),
|
|
|
|
|
'type' => $type,
|
|
|
|
|
'sync_status' => self::STATUS_PENDING,
|
|
|
|
|
'data' => json_encode([
|
|
|
|
|
'handler' => 'exportWallet',
|
|
|
|
|
'kind' => $kind,
|
|
|
|
|
'signed_amount' => $signedAmount,
|
|
|
|
|
'note' => $meta['note'] ?? null,
|
|
|
|
|
'product_id' => $meta['product_id'] ?? null,
|
|
|
|
|
'source' => $meta['source'] ?? null,
|
|
|
|
|
], JSON_UNESCAPED_UNICODE),
|
2026-07-26 14:15:26 +03:30
|
|
|
];
|
|
|
|
|
|
2026-08-01 14:04:01 +03:30
|
|
|
if ($columnAmount === null) {
|
|
|
|
|
$attributes['sync_log'] = 'amount بزرگتر از سقف ستون بود؛ مبلغ در data.signed_amount است';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $this->createWalletSyncLogSafely($attributes, $absAmount);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* ساخت لاگ سند کیف بدون امکان گمشدن بهخاطر سقف ستون amount.
|
|
|
|
|
*
|
|
|
|
|
* @param array<string, mixed> $attributes
|
|
|
|
|
*/
|
|
|
|
|
private function createWalletSyncLogSafely(array $attributes, float $absAmount): PargarSyncLog
|
|
|
|
|
{
|
2026-07-26 14:15:26 +03:30
|
|
|
try {
|
2026-08-01 14:04:01 +03:30
|
|
|
// اگر ستون widen شده، مبلغ کامل را هم در amount بنویس (نمایش ادمین)
|
|
|
|
|
if (($attributes['amount'] ?? null) === null && $absAmount > self::AMOUNT_COLUMN_SAFE_MAX) {
|
|
|
|
|
$withAmount = $attributes;
|
|
|
|
|
$withAmount['amount'] = $absAmount;
|
|
|
|
|
unset($withAmount['sync_log']);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
return PargarSyncLog::create($withAmount);
|
|
|
|
|
} catch (QueryException $e) {
|
|
|
|
|
report($e);
|
|
|
|
|
$attributes['sync_log'] = mb_substr(
|
|
|
|
|
'amount ثبت نشد: ' . $e->getMessage(),
|
|
|
|
|
0,
|
|
|
|
|
500
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-26 14:15:26 +03:30
|
|
|
return PargarSyncLog::create($attributes);
|
|
|
|
|
} catch (QueryException $e) {
|
|
|
|
|
report($e);
|
|
|
|
|
|
|
|
|
|
$attributes['amount'] = null;
|
|
|
|
|
$attributes['sync_log'] = mb_substr('amount ثبت نشد: ' . $e->getMessage(), 0, 500);
|
|
|
|
|
|
2026-08-01 14:04:01 +03:30
|
|
|
try {
|
|
|
|
|
return PargarSyncLog::create($attributes);
|
|
|
|
|
} catch (\Throwable $e2) {
|
|
|
|
|
report($e2);
|
|
|
|
|
|
|
|
|
|
return $this->insertWalletSyncLogRaw($attributes, $e2);
|
|
|
|
|
}
|
2026-07-26 14:15:26 +03:30
|
|
|
}
|
2026-07-20 15:52:42 +03:30
|
|
|
}
|
|
|
|
|
|
2026-08-01 14:04:01 +03:30
|
|
|
/**
|
|
|
|
|
* آخرین پناه: insert خام تا سند pending حتماً در DB بماند.
|
|
|
|
|
*
|
|
|
|
|
* @param array<string, mixed> $attributes
|
|
|
|
|
*/
|
|
|
|
|
private function insertWalletSyncLogRaw(array $attributes, \Throwable $previous): PargarSyncLog
|
|
|
|
|
{
|
|
|
|
|
$now = now();
|
|
|
|
|
$row = [
|
|
|
|
|
'customer_id' => $attributes['customer_id'] ?? null,
|
|
|
|
|
'amount' => null,
|
|
|
|
|
'desc' => $attributes['desc'] ?? null,
|
|
|
|
|
'order_id' => $attributes['order_id'] ?? null,
|
|
|
|
|
'withdraw_id' => $attributes['withdraw_id'] ?? null,
|
|
|
|
|
'date' => $attributes['date'] ?? $now,
|
|
|
|
|
'type' => $attributes['type'] ?? null,
|
|
|
|
|
'sync_status' => $attributes['sync_status'] ?? self::STATUS_PENDING,
|
|
|
|
|
'sync_log' => mb_substr(
|
|
|
|
|
(string) ($attributes['sync_log'] ?? '') . ' | raw insert: ' . $previous->getMessage(),
|
|
|
|
|
0,
|
|
|
|
|
500
|
|
|
|
|
),
|
|
|
|
|
'data' => $attributes['data'] ?? null,
|
|
|
|
|
'created_at' => $now,
|
|
|
|
|
'updated_at' => $now,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$id = DB::table('pargar_sync_logs')->insertGetId($row);
|
|
|
|
|
$log = PargarSyncLog::find($id);
|
|
|
|
|
if ($log !== null) {
|
|
|
|
|
Log::warning('Pargar wallet document saved via raw insert', [
|
|
|
|
|
'pargar_sync_log_id' => $id,
|
|
|
|
|
'withdraw_id' => $row['withdraw_id'],
|
|
|
|
|
'customer_id' => $row['customer_id'],
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return $log;
|
|
|
|
|
}
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
report($e);
|
|
|
|
|
Log::critical('Pargar wallet document queue failed completely', [
|
|
|
|
|
'customer_id' => $row['customer_id'],
|
|
|
|
|
'withdraw_id' => $row['withdraw_id'],
|
|
|
|
|
'type' => $row['type'],
|
|
|
|
|
'error' => $e->getMessage(),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
throw $e;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw new \RuntimeException(
|
|
|
|
|
'ساخت سند کیف پرگار ناموفق بود (withdraw_id=' . ($row['withdraw_id'] ?? 'null') . ')'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* صفکردن سند برای واریز/برداشتهایی که لاگ پرگار ندارند.
|
|
|
|
|
*
|
|
|
|
|
* @param iterable<int, object{withdraw_id?:mixed,cash?:mixed,note?:mixed}> $rows
|
|
|
|
|
* @return int تعداد سند تازهساخته یا موجود
|
|
|
|
|
*/
|
|
|
|
|
public function queueMissingCashDocuments(Customer $customer, iterable $rows, string $source = 'auto_missing'): int
|
|
|
|
|
{
|
|
|
|
|
if (! $this->isSyncEnabled() || ! $this->hasSyncTable()) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$queued = 0;
|
|
|
|
|
foreach ($rows as $row) {
|
|
|
|
|
$withdrawId = (int) ($row->withdraw_id ?? 0);
|
|
|
|
|
$cash = (float) ($row->cash ?? 0);
|
2026-08-18 17:57:22 +03:30
|
|
|
if ($withdrawId <= 0 || ! self::isWalletDocAmount($cash)) {
|
2026-08-01 14:04:01 +03:30
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$log = $this->queueWalletDocument($customer, 'cash', $cash, [
|
|
|
|
|
'withdraw_id' => $withdrawId,
|
|
|
|
|
'note' => $row->note ?? null,
|
|
|
|
|
'source' => $source,
|
|
|
|
|
]);
|
|
|
|
|
if ($log !== null) {
|
|
|
|
|
$queued++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $queued;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-18 17:57:22 +03:30
|
|
|
/**
|
|
|
|
|
* صفکردن سند طلا برای حرکتهایی که لاگ پرگار ندارند (جایزه ریز و …).
|
|
|
|
|
*
|
|
|
|
|
* @param iterable<int, object{withdraw_id?:mixed,gold?:mixed,note?:mixed}> $rows
|
|
|
|
|
*/
|
|
|
|
|
public function queueMissingGoldDocuments(Customer $customer, iterable $rows, string $source = 'auto_missing_gold'): int
|
|
|
|
|
{
|
|
|
|
|
if (! $this->isSyncEnabled() || ! $this->hasSyncTable()) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$queued = 0;
|
|
|
|
|
foreach ($rows as $row) {
|
|
|
|
|
$withdrawId = (int) ($row->withdraw_id ?? 0);
|
|
|
|
|
$gold = (float) ($row->gold ?? 0);
|
|
|
|
|
if ($withdrawId <= 0 || ! self::isWalletDocAmount($gold)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$log = $this->queueWalletDocument($customer, 'gold', $gold, [
|
|
|
|
|
'withdraw_id' => $withdrawId,
|
|
|
|
|
'note' => $row->note ?? null,
|
|
|
|
|
'source' => $source,
|
|
|
|
|
]);
|
|
|
|
|
if ($log !== null) {
|
|
|
|
|
$queued++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $queued;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 15:52:42 +03:30
|
|
|
/**
|
|
|
|
|
* @param array{
|
|
|
|
|
* payment_kind?: string,
|
|
|
|
|
* payment_product_id?: int|null,
|
|
|
|
|
* product_id?: int|null,
|
|
|
|
|
* number?: float|null
|
|
|
|
|
* } $context
|
|
|
|
|
*/
|
|
|
|
|
public function recordTrade(?Order $order, bool $fromCart = false, array $context = []): ?PargarSyncLog
|
2026-07-05 16:43:34 +03:30
|
|
|
{
|
|
|
|
|
if (! $order || ! $order->id || ! $this->isSyncEnabled()) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$existing = PargarSyncLog::where('order_id', $order->id)->first();
|
|
|
|
|
if ($existing) {
|
|
|
|
|
return $existing;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$direction = (int) ($order->direction ?? 0);
|
2026-07-18 16:38:46 +03:30
|
|
|
$desc = \App\Http\Controllers\Controller::accountingShopTradeDesc($direction, $fromCart);
|
2026-07-05 16:43:34 +03:30
|
|
|
|
2026-07-20 15:52:42 +03:30
|
|
|
$paymentKind = $context['payment_kind']
|
|
|
|
|
?? $this->inferPaymentKindFromContext($context);
|
|
|
|
|
|
|
|
|
|
$payload = [
|
|
|
|
|
'handler' => 'exportOrder',
|
|
|
|
|
'direction' => $direction,
|
|
|
|
|
'price' => $order->price,
|
|
|
|
|
'total_price' => $order->total_price,
|
|
|
|
|
'payment_kind' => $paymentKind,
|
|
|
|
|
'payment_product_id' => $context['payment_product_id'] ?? null,
|
|
|
|
|
'product_id' => $context['product_id'] ?? $order->product_id ?? null,
|
|
|
|
|
'number' => $context['number'] ?? null,
|
|
|
|
|
'payment_gold' => $context['payment_gold'] ?? null,
|
|
|
|
|
'from_cart' => $fromCart,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// تهاتر بین دو محصول غیر اصلی — روی ریال/طلای پرگار اثر ندارد
|
|
|
|
|
if ($paymentKind === self::PAYMENT_OTHER_PRODUCT) {
|
|
|
|
|
$order->pargar_confirmed_at = now();
|
|
|
|
|
$order->save();
|
|
|
|
|
|
|
|
|
|
return PargarSyncLog::create([
|
|
|
|
|
'customer_id' => $order->customer_id,
|
|
|
|
|
'amount' => $order->amount,
|
|
|
|
|
'desc' => $desc . ' (تهاتر محصول — بدون سند ریال/طلای پرگار)',
|
|
|
|
|
'order_id' => $order->id,
|
|
|
|
|
'date' => $order->pay_at ?? $order->created_at,
|
|
|
|
|
'type' => self::TYPE_ORDER,
|
|
|
|
|
'sync_status' => self::STATUS_SUCCESS,
|
|
|
|
|
'pargar_ref' => 'skip:barter',
|
|
|
|
|
'data' => json_encode($payload, JSON_UNESCAPED_UNICODE),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-05 16:43:34 +03:30
|
|
|
return PargarSyncLog::create([
|
|
|
|
|
'customer_id' => $order->customer_id,
|
|
|
|
|
'amount' => $order->amount,
|
|
|
|
|
'desc' => $desc,
|
|
|
|
|
'order_id' => $order->id,
|
|
|
|
|
'date' => $order->pay_at ?? $order->created_at,
|
2026-07-20 15:52:42 +03:30
|
|
|
'type' => self::TYPE_ORDER,
|
2026-07-05 16:43:34 +03:30
|
|
|
'sync_status' => self::STATUS_PENDING,
|
2026-07-20 15:52:42 +03:30
|
|
|
'data' => json_encode($payload, JSON_UNESCAPED_UNICODE),
|
2026-07-05 16:43:34 +03:30
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 15:52:42 +03:30
|
|
|
/**
|
|
|
|
|
* @param array{payment_kind?: string, payment_product_id?: int|null} $context
|
|
|
|
|
*/
|
|
|
|
|
public function inferPaymentKindFromContext(array $context): string
|
|
|
|
|
{
|
|
|
|
|
if (! empty($context['payment_kind'])) {
|
|
|
|
|
return (string) $context['payment_kind'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$paymentProductId = (int) ($context['payment_product_id'] ?? 0);
|
|
|
|
|
if ($paymentProductId <= 0) {
|
|
|
|
|
return self::PAYMENT_CASH;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return app(ConfigService::class)->isMainWalletProduct($paymentProductId)
|
|
|
|
|
? self::PAYMENT_MAIN_GOLD
|
|
|
|
|
: self::PAYMENT_OTHER_PRODUCT;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-05 16:43:34 +03:30
|
|
|
public function confirmOrders(array $orderIds): array
|
|
|
|
|
{
|
|
|
|
|
$confirmed = 0;
|
2026-07-22 01:05:00 +03:30
|
|
|
$customerIds = [];
|
2026-07-05 16:43:34 +03:30
|
|
|
|
|
|
|
|
foreach ($orderIds as $orderId) {
|
2026-07-20 15:52:42 +03:30
|
|
|
$id = trim((string) $orderId);
|
|
|
|
|
if ($id === '') {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (str_starts_with(strtoupper($id), 'W-')) {
|
|
|
|
|
$withdrawId = (int) substr($id, 2);
|
|
|
|
|
if ($withdrawId <= 0) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$log = PargarSyncLog::query()
|
|
|
|
|
->where('withdraw_id', $withdrawId)
|
|
|
|
|
->whereIn('type', [self::TYPE_WALLET_CASH, self::TYPE_WALLET_GOLD])
|
|
|
|
|
->where('sync_status', self::STATUS_PENDING)
|
|
|
|
|
->latest('id')
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
if ($log) {
|
|
|
|
|
$this->markSuccess($log, $id, ['OrderId' => $id]);
|
2026-07-22 01:05:00 +03:30
|
|
|
if ($log->customer_id) {
|
|
|
|
|
$customerIds[(int) $log->customer_id] = true;
|
|
|
|
|
}
|
2026-07-20 15:52:42 +03:30
|
|
|
$confirmed++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (str_starts_with(strtoupper($id), 'WLOG-')) {
|
|
|
|
|
$logId = (int) substr($id, 5);
|
|
|
|
|
if ($logId <= 0) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$log = PargarSyncLog::query()
|
|
|
|
|
->where('id', $logId)
|
|
|
|
|
->whereIn('type', [self::TYPE_WALLET_CASH, self::TYPE_WALLET_GOLD])
|
|
|
|
|
->where('sync_status', self::STATUS_PENDING)
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
if ($log) {
|
|
|
|
|
$this->markSuccess($log, $id, ['OrderId' => $id]);
|
2026-07-22 01:05:00 +03:30
|
|
|
if ($log->customer_id) {
|
|
|
|
|
$customerIds[(int) $log->customer_id] = true;
|
|
|
|
|
}
|
2026-07-20 15:52:42 +03:30
|
|
|
$confirmed++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$order = Order::find($id);
|
2026-07-05 16:43:34 +03:30
|
|
|
if (! $order) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$order->pargar_confirmed_at = now();
|
|
|
|
|
$order->save();
|
|
|
|
|
|
|
|
|
|
$log = PargarSyncLog::where('order_id', $order->id)->latest('id')->first();
|
|
|
|
|
if ($log) {
|
|
|
|
|
$this->markSuccess($log, (string) $order->id, ['OrderId' => (string) $order->id]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-22 01:05:00 +03:30
|
|
|
if ($order->customer_id) {
|
|
|
|
|
$customerIds[(int) $order->customer_id] = true;
|
|
|
|
|
} elseif ($log?->customer_id) {
|
|
|
|
|
$customerIds[(int) $log->customer_id] = true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-05 16:43:34 +03:30
|
|
|
$confirmed++;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-22 01:05:00 +03:30
|
|
|
return [
|
|
|
|
|
'confirmed' => $confirmed,
|
|
|
|
|
'customer_ids' => array_keys($customerIds),
|
|
|
|
|
];
|
2026-07-05 16:43:34 +03:30
|
|
|
}
|
|
|
|
|
|
2026-07-26 21:17:57 +03:30
|
|
|
/**
|
|
|
|
|
* شناسهای که سند با آن در GET /pargar/orders بیرون میرود.
|
|
|
|
|
*/
|
|
|
|
|
public function outboundExportId(PargarSyncLog $log): string
|
|
|
|
|
{
|
|
|
|
|
if ($log->type === self::TYPE_ORDER) {
|
|
|
|
|
return (string) ($log->order_id ?? $log->id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$withdrawId = (int) ($log->withdraw_id ?? 0);
|
|
|
|
|
|
|
|
|
|
return $withdrawId > 0 ? 'W-'.$withdrawId : 'WLOG-'.$log->id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* اسنادی که در نوبت GET /pargar/orders هستند و پرگار هنوز تأییدشان نکرده است.
|
|
|
|
|
*
|
|
|
|
|
* تا وقتی اینها معلقاند، ماندهٔ پرگارِ همان مشتری روی کیف اعمال نمیشود؛ پس باید
|
|
|
|
|
* در پنل دیده شوند، نه اینکه با شکایت مشتری کشف شوند.
|
|
|
|
|
*
|
|
|
|
|
* @return array{
|
|
|
|
|
* count: int,
|
|
|
|
|
* customers: int,
|
|
|
|
|
* waited_minutes: int,
|
|
|
|
|
* oldest_at: string|null,
|
|
|
|
|
* last_confirmed_at: string|null,
|
|
|
|
|
* threshold_minutes: int,
|
|
|
|
|
* rows: list<array<string, mixed>>
|
|
|
|
|
* }
|
|
|
|
|
*/
|
|
|
|
|
public function stuckOutboundDocuments(int $olderThanMinutes = 15, int $limit = 50): array
|
|
|
|
|
{
|
|
|
|
|
$threshold = now()->subMinutes(max(0, $olderThanMinutes));
|
|
|
|
|
|
|
|
|
|
$logs = PargarSyncLog::with('customer')
|
|
|
|
|
->whereIn('type', [self::TYPE_ORDER, self::TYPE_WALLET_CASH, self::TYPE_WALLET_GOLD])
|
|
|
|
|
->where('sync_status', self::STATUS_PENDING)
|
|
|
|
|
->where('created_at', '<=', $threshold)
|
|
|
|
|
->where(function ($q) {
|
|
|
|
|
// سند کیف تا وقتی pending است در هر fetch فرستاده میشود؛ سفارش فقط
|
|
|
|
|
// وقتی هنوز در نوبت خروجی است (sts=2 و تأییدنشده).
|
|
|
|
|
$q->whereIn('type', [self::TYPE_WALLET_CASH, self::TYPE_WALLET_GOLD])
|
|
|
|
|
->orWhereExists(function ($sub) {
|
|
|
|
|
$sub->select(DB::raw(1))
|
|
|
|
|
->from('orders')
|
|
|
|
|
->whereColumn('orders.id', 'pargar_sync_logs.order_id')
|
|
|
|
|
->where('orders.sts', 2)
|
|
|
|
|
->whereNull('orders.pargar_confirmed_at');
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
->orderBy('id')
|
|
|
|
|
->get();
|
|
|
|
|
|
|
|
|
|
$oldest = $logs->first()?->created_at;
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'count' => $logs->count(),
|
|
|
|
|
'customers' => $logs->pluck('customer_id')->filter()->unique()->count(),
|
|
|
|
|
'waited_minutes' => $oldest ? (int) $oldest->diffInMinutes(now()) : 0,
|
|
|
|
|
'oldest_at' => $oldest?->toDateTimeString(),
|
|
|
|
|
'last_confirmed_at' => $this->lastOutboundConfirmedAt(),
|
|
|
|
|
'threshold_minutes' => max(0, $olderThanMinutes),
|
|
|
|
|
'rows' => $logs->take($limit)->map(fn (PargarSyncLog $log) => [
|
|
|
|
|
'id' => (int) $log->id,
|
|
|
|
|
'export_id' => $this->outboundExportId($log),
|
|
|
|
|
'type' => (string) $log->type,
|
|
|
|
|
'amount' => $log->amount === null ? null : (float) $log->amount,
|
|
|
|
|
'desc' => (string) ($log->desc ?? ''),
|
|
|
|
|
'order_id' => $log->order_id ? (int) $log->order_id : null,
|
|
|
|
|
'withdraw_id' => $log->withdraw_id ? (int) $log->withdraw_id : null,
|
|
|
|
|
'customer_id' => $log->customer_id ? (int) $log->customer_id : null,
|
|
|
|
|
'customer_name' => $log->customer?->name,
|
|
|
|
|
'customer_mobile' => $log->customer?->mobile,
|
|
|
|
|
'pargar_account_id' => $log->customer?->pargar_account_id,
|
|
|
|
|
'created_at' => $log->created_at?->toDateTimeString(),
|
|
|
|
|
'waited_minutes' => $log->created_at ? (int) $log->created_at->diffInMinutes(now()) : 0,
|
|
|
|
|
])->values()->all(),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** آخرین باری که پرگار سندی از پنل را تأیید کرده است */
|
|
|
|
|
private function lastOutboundConfirmedAt(): ?string
|
|
|
|
|
{
|
|
|
|
|
$query = PargarSyncLog::query()
|
|
|
|
|
->whereIn('type', [self::TYPE_ORDER, self::TYPE_WALLET_CASH, self::TYPE_WALLET_GOLD])
|
|
|
|
|
->where('sync_status', self::STATUS_SUCCESS);
|
|
|
|
|
|
|
|
|
|
$confirmedAt = PargarSyncLog::hasConfirmedAtColumn()
|
|
|
|
|
? $query->clone()->max('confirmed_at')
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
return $confirmedAt ?: $query->max('updated_at');
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-05 16:43:34 +03:30
|
|
|
public function retry(PargarSyncLog $log): array
|
|
|
|
|
{
|
|
|
|
|
if (! $this->isFailed($log) && ! empty($log->pargar_ref)) {
|
|
|
|
|
return ['ok' => false, 'message' => 'این سند قبلاً در پرگار ثبت شده است'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$payload = json_decode((string) $log->data, true) ?: [];
|
|
|
|
|
$handler = $payload['handler'] ?? $this->inferHandler($log);
|
|
|
|
|
|
|
|
|
|
if ($handler === 'exportOrder' && $log->order_id) {
|
|
|
|
|
Order::where('id', $log->order_id)->update(['pargar_confirmed_at' => null]);
|
|
|
|
|
PargarSyncLog::where('id', $log->id)->update([
|
|
|
|
|
'sync_status' => self::STATUS_PENDING,
|
|
|
|
|
'sync_log' => null,
|
|
|
|
|
'pargar_ref' => null,
|
|
|
|
|
]);
|
|
|
|
|
$log->refresh();
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'ok' => true,
|
|
|
|
|
'message' => 'سفارش دوباره در صف ارسال به پرگار قرار گرفت',
|
|
|
|
|
'pargar' => $log,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($handler === 'exportCustomer' && $log->customer_id) {
|
|
|
|
|
PargarSyncLog::where('id', $log->id)->update([
|
|
|
|
|
'sync_status' => self::STATUS_PENDING,
|
|
|
|
|
'sync_log' => null,
|
|
|
|
|
'pargar_ref' => null,
|
|
|
|
|
]);
|
|
|
|
|
$log->refresh();
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'ok' => true,
|
|
|
|
|
'message' => 'مشتری دوباره در صف دریافت پرگار قرار گرفت',
|
|
|
|
|
'pargar' => $log,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 15:52:42 +03:30
|
|
|
if ($handler === 'exportWallet') {
|
|
|
|
|
PargarSyncLog::where('id', $log->id)->update([
|
|
|
|
|
'sync_status' => self::STATUS_PENDING,
|
|
|
|
|
'sync_log' => null,
|
|
|
|
|
'pargar_ref' => null,
|
|
|
|
|
]);
|
|
|
|
|
$log->refresh();
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'ok' => true,
|
|
|
|
|
'message' => 'سند کیف دوباره در صف ارسال به پرگار قرار گرفت',
|
|
|
|
|
'pargar' => $log,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-05 16:43:34 +03:30
|
|
|
return [
|
|
|
|
|
'ok' => false,
|
|
|
|
|
'message' => $log->sync_log ?: 'ارسال مجدد به پرگار ناموفق بود',
|
|
|
|
|
'pargar' => $log,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function inferHandler(PargarSyncLog $log): string
|
|
|
|
|
{
|
|
|
|
|
if ($log->order_id) {
|
|
|
|
|
return 'exportOrder';
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 15:52:42 +03:30
|
|
|
if ($log->type === self::TYPE_CUSTOMER) {
|
2026-07-05 16:43:34 +03:30
|
|
|
return 'exportCustomer';
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 15:52:42 +03:30
|
|
|
if (in_array($log->type, [self::TYPE_WALLET_CASH, self::TYPE_WALLET_GOLD], true)) {
|
|
|
|
|
return 'exportWallet';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($log->type === self::TYPE_MANDE) {
|
2026-07-05 16:43:34 +03:30
|
|
|
return 'importMande';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 'importDocument';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function isSyncEnabled(): bool
|
|
|
|
|
{
|
|
|
|
|
$config = app(ConfigService::class);
|
|
|
|
|
|
|
|
|
|
return $config->isPargarAccounting()
|
|
|
|
|
&& trim($config->getString('pargar_username', '')) !== ''
|
|
|
|
|
&& trim($config->getString('pargar_password', '')) !== '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function hasSyncTable(): bool
|
|
|
|
|
{
|
|
|
|
|
return Schema::hasTable('pargar_sync_logs');
|
|
|
|
|
}
|
2026-07-20 15:52:42 +03:30
|
|
|
|
|
|
|
|
public function pargarItemIdRial(): int
|
|
|
|
|
{
|
|
|
|
|
$raw = trim(app(ConfigService::class)->getString('pargar_item_id_rial', '0'));
|
|
|
|
|
|
|
|
|
|
return max(0, (int) $raw);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function pargarItemIdGold(): int
|
|
|
|
|
{
|
|
|
|
|
$raw = trim(app(ConfigService::class)->getString('pargar_item_id_gold', '11'));
|
|
|
|
|
$id = (int) $raw;
|
|
|
|
|
|
|
|
|
|
return $id > 0 ? $id : 11;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function mainWalletGoldItemName(): string
|
|
|
|
|
{
|
|
|
|
|
$product = app(ConfigService::class)->mainWalletRepresentativeProduct();
|
|
|
|
|
$title = trim((string) ($product?->title ?? ''));
|
|
|
|
|
|
|
|
|
|
return $title !== '' ? $title : 'آبشده';
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 12:36:59 +03:30
|
|
|
/**
|
|
|
|
|
* خلاصهٔ اسناد ورودی از پرگار برای نمایش در جدول ادمین.
|
|
|
|
|
*
|
|
|
|
|
* @param array<string, mixed> $data
|
|
|
|
|
* @return array{
|
|
|
|
|
* desc: string,
|
|
|
|
|
* amount: float|null,
|
|
|
|
|
* amount_unit: 'gold'|'rial'|null,
|
|
|
|
|
* tx_count: int,
|
|
|
|
|
* account_name: string|null
|
|
|
|
|
* }
|
|
|
|
|
*/
|
|
|
|
|
public function summarizeInboundDocument(array $data): array
|
|
|
|
|
{
|
|
|
|
|
$accountName = trim((string) ($data['accountName'] ?? ''));
|
|
|
|
|
$transactions = is_array($data['transactions'] ?? null) ? $data['transactions'] : [];
|
|
|
|
|
|
|
|
|
|
$txLines = [];
|
|
|
|
|
$maxGold = 0.0;
|
|
|
|
|
$maxRial = 0.0;
|
|
|
|
|
$txCount = 0;
|
|
|
|
|
|
|
|
|
|
foreach ($transactions as $tx) {
|
|
|
|
|
if (! is_array($tx)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$txCount++;
|
|
|
|
|
$desc = trim((string) ($tx['desc'] ?? ''));
|
|
|
|
|
$docType = trim((string) ($tx['docType'] ?? ''));
|
|
|
|
|
$line = $desc !== '' ? $desc : ($docType !== '' ? $docType : null);
|
|
|
|
|
if ($line !== null) {
|
|
|
|
|
$txLines[] = $line;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$maxGold = max(
|
|
|
|
|
$maxGold,
|
|
|
|
|
abs($this->parsePargarNumber($tx['bedGold'] ?? 0)),
|
|
|
|
|
abs($this->parsePargarNumber($tx['besGold'] ?? 0))
|
|
|
|
|
);
|
|
|
|
|
$maxRial = max(
|
|
|
|
|
$maxRial,
|
|
|
|
|
abs($this->parsePargarNumber($tx['bedRial'] ?? 0)),
|
|
|
|
|
abs($this->parsePargarNumber($tx['besRial'] ?? 0))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($txLines !== []) {
|
|
|
|
|
$unique = array_values(array_unique($txLines));
|
|
|
|
|
$desc = implode(' | ', array_slice($unique, 0, 3));
|
|
|
|
|
if ($txCount > count(array_slice($unique, 0, 3))) {
|
|
|
|
|
$desc .= ' (+' . ($txCount - min(3, count($unique))) . ' سند دیگر)';
|
|
|
|
|
}
|
|
|
|
|
} elseif ($accountName !== '') {
|
|
|
|
|
$desc = "اسناد {$accountName} از پرگار";
|
|
|
|
|
} else {
|
|
|
|
|
$desc = $txCount > 0 ? "دریافت {$txCount} سند از پرگار" : 'دریافت اسناد از پرگار';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$amount = null;
|
|
|
|
|
$amountUnit = null;
|
2026-08-18 17:57:22 +03:30
|
|
|
if ($maxGold >= self::WALLET_DOC_EPSILON) {
|
2026-07-21 12:36:59 +03:30
|
|
|
$amount = $maxGold;
|
|
|
|
|
$amountUnit = 'gold';
|
2026-08-18 17:57:22 +03:30
|
|
|
} elseif ($maxRial >= self::WALLET_DOC_EPSILON) {
|
2026-07-21 12:36:59 +03:30
|
|
|
$amount = $maxRial;
|
|
|
|
|
$amountUnit = 'rial';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'desc' => $desc,
|
|
|
|
|
'amount' => $amount,
|
|
|
|
|
'amount_unit' => $amountUnit,
|
|
|
|
|
'tx_count' => $txCount,
|
|
|
|
|
'account_name' => $accountName !== '' ? $accountName : null,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function parsePargarNumber(mixed $value): float
|
|
|
|
|
{
|
|
|
|
|
if ($value === null || $value === '') {
|
|
|
|
|
return 0.0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (float) str_replace('/', '.', (string) $value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 15:52:42 +03:30
|
|
|
public function resolveOrderProduct(?Order $order, ?array $payload = null): ?Product
|
|
|
|
|
{
|
|
|
|
|
$productId = (int) ($payload['product_id'] ?? $order?->product_id ?? 0);
|
|
|
|
|
if ($productId <= 0) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Product::find($productId);
|
|
|
|
|
}
|
2026-07-05 16:43:34 +03:30
|
|
|
}
|