2026-07-21 12:36:59 +03:30
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
|
|
|
|
|
use App\Models\Customer;
|
|
|
|
|
use App\Models\PargarSyncLog;
|
2026-07-21 14:13:26 +03:30
|
|
|
use Illuminate\Support\Facades\Cache;
|
2026-07-21 12:36:59 +03:30
|
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
|
use Rooyesh\Wallet\interfaces\TransactionInterface;
|
|
|
|
|
use Rooyesh\Wallet\interfaces\WalletInterface;
|
|
|
|
|
|
|
|
|
|
/**
|
2026-07-21 14:33:16 +03:30
|
|
|
* همگامسازی کیف با پرگار — فقط با دکمهٔ ادمین.
|
2026-07-21 14:13:26 +03:30
|
|
|
*
|
2026-07-21 14:33:16 +03:30
|
|
|
* قواعد سخت:
|
|
|
|
|
* 1) webhook اسناد (/documents) هرگز کیف را عوض نمیکند — فقط لاگ.
|
|
|
|
|
* 2) دکمه: اول اسنادِ اعمالنشدهٔ همان مشتری، بعد مانده.
|
|
|
|
|
* 3) هر سطر سند حداکثر یکبار (اثرانگشت).
|
|
|
|
|
* 4) هر لاگ دسته حداکثر یکبار پردازش میشود (wallet_batch_applied).
|
2026-07-21 12:36:59 +03:30
|
|
|
*/
|
|
|
|
|
class PargarInboundWalletService
|
|
|
|
|
{
|
|
|
|
|
public const TX_TYPE_PARGAR = 22;
|
|
|
|
|
|
2026-07-21 14:23:26 +03:30
|
|
|
public const KILL_SWITCH_CACHE_KEY = 'pargar_wallet_apply_disabled';
|
2026-07-21 14:13:26 +03:30
|
|
|
|
2026-07-21 14:33:16 +03:30
|
|
|
/** سقف تعداد دستهٔ سند در هر کلیک (جلوگیری از timeout) */
|
|
|
|
|
private const MAX_BATCHES_PER_SYNC = 30;
|
|
|
|
|
|
|
|
|
|
/** @var array<string, true> */
|
|
|
|
|
private array $seenFingerprints = [];
|
|
|
|
|
|
2026-07-21 12:36:59 +03:30
|
|
|
public function __construct(
|
|
|
|
|
private readonly WalletBroadcastService $walletBroadcast
|
|
|
|
|
) {
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 14:23:26 +03:30
|
|
|
public static function isApplyFrozen(): bool
|
|
|
|
|
{
|
2026-07-21 14:33:16 +03:30
|
|
|
return (bool) Cache::get(self::KILL_SWITCH_CACHE_KEY, false);
|
2026-07-21 14:23:26 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static function freezeApplies(bool $frozen = true): void
|
|
|
|
|
{
|
|
|
|
|
Cache::forever(self::KILL_SWITCH_CACHE_KEY, $frozen);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 12:36:59 +03:30
|
|
|
/**
|
2026-07-21 14:33:16 +03:30
|
|
|
* دکمهٔ ادمین:
|
|
|
|
|
* ۱) اسناد لاگشدهٔ اعمالنشده برای همین مشتری
|
|
|
|
|
* ۲) سپس همترازی مانده (mande)
|
|
|
|
|
*
|
2026-07-21 14:13:26 +03:30
|
|
|
* @return array{
|
2026-07-21 14:33:16 +03:30
|
|
|
* documents: array{applied: int, skipped: int, batches: int, errors: list<string>},
|
2026-07-21 14:13:26 +03:30
|
|
|
* mande: array{applied: bool, gold_delta?: float, rial_delta?: float, message: string}
|
|
|
|
|
* }
|
2026-07-21 12:49:08 +03:30
|
|
|
*/
|
2026-07-21 14:13:26 +03:30
|
|
|
public function syncWalletFromPargar(Customer $customer, bool $forceMande = false): array
|
2026-07-21 12:49:08 +03:30
|
|
|
{
|
2026-07-21 14:33:16 +03:30
|
|
|
unset($forceMande); // مانده در دکمه همیشه force میشود
|
|
|
|
|
|
|
|
|
|
if (self::isApplyFrozen()) {
|
2026-07-21 14:23:26 +03:30
|
|
|
return [
|
2026-07-21 14:33:16 +03:30
|
|
|
'documents' => ['applied' => 0, 'skipped' => 0, 'batches' => 0, 'errors' => []],
|
2026-07-21 14:23:26 +03:30
|
|
|
'mande' => [
|
|
|
|
|
'applied' => false,
|
2026-07-21 14:33:16 +03:30
|
|
|
'message' => 'اعمال کیف یخزده است — php artisan pargar:wallet-freeze off',
|
2026-07-21 14:23:26 +03:30
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 14:33:16 +03:30
|
|
|
$lock = Cache::lock('pargar-wallet-sync:'.$customer->id, 120);
|
2026-07-21 14:13:26 +03:30
|
|
|
if (! $lock->get()) {
|
|
|
|
|
return [
|
2026-07-21 14:33:16 +03:30
|
|
|
'documents' => ['applied' => 0, 'skipped' => 0, 'batches' => 0, 'errors' => ['همگامسازی در حال اجراست']],
|
|
|
|
|
'mande' => ['applied' => false, 'message' => 'همگامسازی دیگری برای این مشتری در حال اجراست'],
|
2026-07-21 14:13:26 +03:30
|
|
|
];
|
2026-07-21 12:49:08 +03:30
|
|
|
}
|
|
|
|
|
|
2026-07-21 14:13:26 +03:30
|
|
|
try {
|
2026-07-21 14:33:16 +03:30
|
|
|
// ۱) اسناد لاگشده که هنوز روی کیف نرفتهاند
|
|
|
|
|
$documents = $this->applyPendingDocumentBatches($customer);
|
|
|
|
|
|
|
|
|
|
// ۲) همیشه مانده را با آخرین لاگ mande همتراز کن (حتی اگر سندی نبود)
|
|
|
|
|
// force تا بعد از اعمال اسناد، موجودی متورم به ماندهٔ واقعی پرگار برگردد
|
|
|
|
|
$mande = $this->applyLatestMandeDifference($customer, force: true);
|
|
|
|
|
|
|
|
|
|
if ($documents['applied'] > 0 || ($mande['applied'] ?? false)) {
|
|
|
|
|
$this->walletBroadcast->broadcastForCustomer(
|
|
|
|
|
(int) $customer->id,
|
|
|
|
|
(int) ($customer->type ?? 2)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 14:13:26 +03:30
|
|
|
return [
|
2026-07-21 14:33:16 +03:30
|
|
|
'documents' => $documents,
|
|
|
|
|
'mande' => $mande,
|
2026-07-21 14:13:26 +03:30
|
|
|
];
|
|
|
|
|
} finally {
|
|
|
|
|
$lock->release();
|
|
|
|
|
}
|
2026-07-21 12:49:08 +03:30
|
|
|
}
|
|
|
|
|
|
2026-07-21 14:33:16 +03:30
|
|
|
/**
|
|
|
|
|
* فقط لاگهای دستهٔ ورودی که هنوز روی کیف اعمال نشدهاند.
|
|
|
|
|
*
|
|
|
|
|
* @return array{applied: int, skipped: int, batches: int, errors: list<string>}
|
|
|
|
|
*/
|
|
|
|
|
public function applyPendingDocumentBatches(Customer $customer): array
|
|
|
|
|
{
|
|
|
|
|
$result = [
|
|
|
|
|
'applied' => 0,
|
|
|
|
|
'skipped' => 0,
|
|
|
|
|
'batches' => 0,
|
|
|
|
|
'errors' => [],
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
$batches = PargarSyncLog::query()
|
|
|
|
|
->where('type', PargarSyncService::TYPE_DOCUMENT)
|
|
|
|
|
->where('customer_id', $customer->id)
|
|
|
|
|
->orderBy('id')
|
|
|
|
|
->limit(200)
|
|
|
|
|
->get();
|
|
|
|
|
|
|
|
|
|
foreach ($batches as $batch) {
|
|
|
|
|
if ($result['batches'] >= self::MAX_BATCHES_PER_SYNC) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$data = json_decode((string) $batch->data, true);
|
|
|
|
|
if (! is_array($data)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// فقط دستههای ورودی پرگار (آرایه transactions) — نه رکورد تکسند اعمالشده
|
|
|
|
|
if (empty($data['transactions']) || ! is_array($data['transactions'])) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (! empty($data['wallet_batch_applied']) || ! empty($data['wallet_applied'])) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$result['batches']++;
|
|
|
|
|
$accountId = trim((string) ($data['accountId'] ?? $customer->pargar_account_id ?? ''));
|
|
|
|
|
|
|
|
|
|
foreach ($data['transactions'] as $tx) {
|
|
|
|
|
if (! is_array($tx)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$outcome = $this->applyOneTransaction($customer, $accountId, $tx);
|
|
|
|
|
if ($outcome === 'applied') {
|
|
|
|
|
$result['applied']++;
|
|
|
|
|
} elseif ($outcome === 'skipped') {
|
|
|
|
|
$result['skipped']++;
|
|
|
|
|
} else {
|
|
|
|
|
$result['errors'][] = $outcome;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// حتی اگر همه skip شدند، دسته را مصرفشده علامت بزن تا دوباره نیاید
|
|
|
|
|
$data['wallet_batch_applied'] = true;
|
|
|
|
|
$data['wallet_batch_applied_at'] = now()->toDateTimeString();
|
|
|
|
|
$batch->data = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
|
|
|
|
if (is_string($batch->desc) && ! str_contains($batch->desc, '[اعمالشده]')) {
|
|
|
|
|
$batch->desc = '[اعمالشده] '.$batch->desc;
|
|
|
|
|
}
|
|
|
|
|
$batch->save();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param array<string, mixed> $tx
|
|
|
|
|
* @return 'applied'|'skipped'|string error
|
|
|
|
|
*/
|
|
|
|
|
private function applyOneTransaction(Customer $customer, string $accountId, array $tx): string
|
|
|
|
|
{
|
|
|
|
|
$fingerprint = $this->transactionFingerprint($accountId, $tx);
|
|
|
|
|
if ($fingerprint === '' || $this->isFingerprintUsed($fingerprint)) {
|
|
|
|
|
return 'skipped';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->seenFingerprints[$fingerprint] = true;
|
|
|
|
|
|
|
|
|
|
$goldDelta = $this->parseNumber($tx['besGold'] ?? 0) - $this->parseNumber($tx['bedGold'] ?? 0);
|
|
|
|
|
$rialDelta = $this->parseNumber($tx['besRial'] ?? 0) - $this->parseNumber($tx['bedRial'] ?? 0);
|
|
|
|
|
|
|
|
|
|
if (abs($goldDelta) < 0.01 && abs($rialDelta) < 1) {
|
|
|
|
|
$this->rememberFingerprint($customer, $fingerprint, $accountId, $tx, false, 'zero');
|
|
|
|
|
|
|
|
|
|
return 'skipped';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($this->isEchoOfRecentOutbound((int) $customer->id, $goldDelta, $rialDelta)) {
|
|
|
|
|
$this->rememberFingerprint($customer, $fingerprint, $accountId, $tx, false, 'echo');
|
|
|
|
|
|
|
|
|
|
return 'skipped';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$this->writeWalletDelta($customer, $tx, $goldDelta, $rialDelta);
|
|
|
|
|
$this->rememberFingerprint($customer, $fingerprint, $accountId, $tx, true, null, $goldDelta, $rialDelta);
|
|
|
|
|
|
|
|
|
|
return 'applied';
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
unset($this->seenFingerprints[$fingerprint]);
|
|
|
|
|
Log::warning('Pargar applyOneTransaction failed', [
|
|
|
|
|
'customer_id' => $customer->id,
|
|
|
|
|
'fingerprint' => $fingerprint,
|
|
|
|
|
'error' => $e->getMessage(),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return $e->getMessage();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 12:49:08 +03:30
|
|
|
/**
|
|
|
|
|
* @return array{applied: bool, gold_delta?: float, rial_delta?: float, message: string}
|
|
|
|
|
*/
|
2026-07-21 14:13:26 +03:30
|
|
|
public function applyLatestMandeDifference(Customer $customer, bool $force = false): array
|
2026-07-21 12:49:08 +03:30
|
|
|
{
|
2026-07-21 13:25:18 +03:30
|
|
|
if ($this->hasPendingOutboundWalletSync((int) $customer->id)) {
|
|
|
|
|
return [
|
|
|
|
|
'applied' => false,
|
2026-07-21 14:33:16 +03:30
|
|
|
'message' => 'سند ارسالی از پنل هنوز تأیید نشده — مانده اعمال نشد تا شارژ برنگردد',
|
2026-07-21 13:25:18 +03:30
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 12:49:08 +03:30
|
|
|
$log = PargarSyncLog::query()
|
|
|
|
|
->where('customer_id', $customer->id)
|
|
|
|
|
->where('type', PargarSyncService::TYPE_MANDE)
|
|
|
|
|
->orderByDesc('id')
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
if ($log === null) {
|
2026-07-21 14:33:16 +03:30
|
|
|
return ['applied' => false, 'message' => 'لاگ ماندهٔ پرگار برای این مشتری نیست'];
|
2026-07-21 12:49:08 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$data = json_decode((string) $log->data, true) ?: [];
|
2026-07-21 14:13:26 +03:30
|
|
|
if (! $force && ! empty($data['wallet_applied_from_mande'])) {
|
2026-07-21 14:33:16 +03:30
|
|
|
return ['applied' => false, 'message' => 'مانده قبلاً همتراز شده است'];
|
2026-07-21 12:49:08 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$goldInv = $this->parseNumber($data['GoldInv'] ?? 0);
|
|
|
|
|
$rialInv = $this->parseNumber($data['RialInv'] ?? 0);
|
|
|
|
|
|
|
|
|
|
$ownerType = (int) ($customer->type ?? 2);
|
|
|
|
|
$walletInterface = new WalletInterface((int) $customer->id, $ownerType);
|
|
|
|
|
$wallet = $walletInterface->wallet;
|
|
|
|
|
|
|
|
|
|
$goldDiff = $goldInv - (float) ($wallet->gold ?? 0);
|
|
|
|
|
$rialDiff = $rialInv - (float) ($wallet->cash ?? 0);
|
|
|
|
|
|
|
|
|
|
if (abs($goldDiff) < 0.01 && abs($rialDiff) < 1) {
|
|
|
|
|
$data['wallet_applied_from_mande'] = true;
|
|
|
|
|
$log->data = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
|
|
|
|
$log->save();
|
|
|
|
|
|
2026-07-21 14:33:16 +03:30
|
|
|
return ['applied' => false, 'message' => 'مانده با پرگار یکسان است'];
|
2026-07-21 12:49:08 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$input = [
|
|
|
|
|
'title' => 'تطبیق ماندهٔ پرگار',
|
2026-07-21 14:13:26 +03:30
|
|
|
'note' => 'mande-log:'.$log->id,
|
2026-07-21 12:49:08 +03:30
|
|
|
'expire_at' => now(),
|
|
|
|
|
'active' => true,
|
|
|
|
|
];
|
|
|
|
|
if (abs($goldDiff) >= 0.01) {
|
|
|
|
|
$input['gold'] = abs($goldDiff);
|
|
|
|
|
}
|
|
|
|
|
if (abs($rialDiff) >= 1) {
|
|
|
|
|
$input['cash'] = abs($rialDiff);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
new TransactionInterface(
|
|
|
|
|
$walletInterface,
|
|
|
|
|
$input,
|
|
|
|
|
self::TX_TYPE_PARGAR,
|
|
|
|
|
$goldDiff > 0,
|
|
|
|
|
$rialDiff > 0
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$data['wallet_applied_from_mande'] = true;
|
|
|
|
|
$data['applied_gold_delta'] = $goldDiff;
|
|
|
|
|
$data['applied_rial_delta'] = $rialDiff;
|
2026-07-21 14:13:26 +03:30
|
|
|
$data['applied_at'] = now()->toDateTimeString();
|
2026-07-21 12:49:08 +03:30
|
|
|
$log->data = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
|
|
|
|
$log->desc = 'اعمال ماندهٔ پرگار روی کیف اپ';
|
|
|
|
|
$log->save();
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'applied' => true,
|
|
|
|
|
'gold_delta' => $goldDiff,
|
|
|
|
|
'rial_delta' => $rialDiff,
|
2026-07-21 14:33:16 +03:30
|
|
|
'message' => 'مانده با پرگار همتراز شد',
|
2026-07-21 12:49:08 +03:30
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 14:33:16 +03:30
|
|
|
/**
|
|
|
|
|
* @param array<string, mixed> $tx
|
|
|
|
|
*/
|
|
|
|
|
public function transactionFingerprint(string $accountId, array $tx): string
|
|
|
|
|
{
|
|
|
|
|
// بدون اتکا به docId یکتای پرگار — محتوایی
|
|
|
|
|
$parts = [
|
|
|
|
|
$accountId,
|
|
|
|
|
trim((string) ($tx['date'] ?? '')),
|
|
|
|
|
trim((string) ($tx['docType'] ?? '')),
|
|
|
|
|
trim((string) ($tx['desc'] ?? '')),
|
|
|
|
|
$this->normalizeNumberString($tx['bedGold'] ?? 0),
|
|
|
|
|
$this->normalizeNumberString($tx['besGold'] ?? 0),
|
|
|
|
|
$this->normalizeNumberString($tx['bedRial'] ?? 0),
|
|
|
|
|
$this->normalizeNumberString($tx['besRial'] ?? 0),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return 'fp:'.hash('sha256', implode('|', $parts));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function isFingerprintUsed(string $fingerprint): bool
|
|
|
|
|
{
|
|
|
|
|
if ($fingerprint === '' || isset($this->seenFingerprints[$fingerprint])) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return PargarSyncLog::query()
|
|
|
|
|
->where('type', PargarSyncService::TYPE_DOCUMENT)
|
|
|
|
|
->where('pargar_ref', $fingerprint)
|
|
|
|
|
->exists();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param array<string, mixed> $tx
|
|
|
|
|
*/
|
|
|
|
|
private function rememberFingerprint(
|
|
|
|
|
Customer $customer,
|
|
|
|
|
string $fingerprint,
|
|
|
|
|
string $accountId,
|
|
|
|
|
array $tx,
|
|
|
|
|
bool $applied,
|
|
|
|
|
?string $skipReason,
|
|
|
|
|
float $goldDelta = 0,
|
|
|
|
|
float $rialDelta = 0
|
|
|
|
|
): void {
|
|
|
|
|
if (PargarSyncLog::query()
|
|
|
|
|
->where('type', PargarSyncService::TYPE_DOCUMENT)
|
|
|
|
|
->where('pargar_ref', $fingerprint)
|
|
|
|
|
->exists()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PargarSyncLog::create([
|
|
|
|
|
'customer_id' => $customer->id,
|
|
|
|
|
'amount' => abs($rialDelta) >= 1 ? abs($rialDelta) : abs($goldDelta),
|
|
|
|
|
'desc' => $applied
|
|
|
|
|
? 'اعمال سند پرگار'
|
|
|
|
|
: ('رد سند پرگار'.($skipReason ? " ({$skipReason})" : '')),
|
|
|
|
|
'type' => PargarSyncService::TYPE_DOCUMENT,
|
|
|
|
|
'sync_status' => PargarSyncService::STATUS_SUCCESS,
|
|
|
|
|
'pargar_ref' => $fingerprint,
|
|
|
|
|
'data' => json_encode([
|
|
|
|
|
'accountId' => $accountId,
|
|
|
|
|
'transaction' => $tx,
|
|
|
|
|
'wallet_applied' => $applied,
|
|
|
|
|
'fingerprint' => $fingerprint,
|
|
|
|
|
'skip_reason' => $skipReason,
|
|
|
|
|
'gold_delta' => $goldDelta,
|
|
|
|
|
'rial_delta' => $rialDelta,
|
|
|
|
|
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param array<string, mixed> $tx
|
|
|
|
|
*/
|
|
|
|
|
private function writeWalletDelta(Customer $customer, array $tx, float $goldDelta, float $rialDelta): void
|
|
|
|
|
{
|
|
|
|
|
$ownerType = (int) ($customer->type ?? 2);
|
|
|
|
|
$walletInterface = new WalletInterface((int) $customer->id, $ownerType);
|
|
|
|
|
|
|
|
|
|
$desc = trim((string) ($tx['desc'] ?? ''));
|
|
|
|
|
$docType = trim((string) ($tx['docType'] ?? ''));
|
|
|
|
|
$title = $desc !== '' ? $desc : ($docType !== '' ? $docType : 'سند پرگار');
|
|
|
|
|
|
|
|
|
|
$input = [
|
|
|
|
|
'title' => mb_substr($title, 0, 180),
|
|
|
|
|
'note' => trim((string) ($tx['docId'] ?? '')),
|
|
|
|
|
'expire_at' => now(),
|
|
|
|
|
'active' => true,
|
|
|
|
|
];
|
|
|
|
|
if (abs($goldDelta) >= 0.01) {
|
|
|
|
|
$input['gold'] = abs($goldDelta);
|
|
|
|
|
}
|
|
|
|
|
if (abs($rialDelta) >= 1) {
|
|
|
|
|
$input['cash'] = abs($rialDelta);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
new TransactionInterface(
|
|
|
|
|
$walletInterface,
|
|
|
|
|
$input,
|
|
|
|
|
self::TX_TYPE_PARGAR,
|
|
|
|
|
$goldDelta > 0,
|
|
|
|
|
$rialDelta > 0
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function isEchoOfRecentOutbound(int $customerId, float $goldDelta, float $rialDelta): bool
|
|
|
|
|
{
|
|
|
|
|
$logs = PargarSyncLog::query()
|
|
|
|
|
->where('customer_id', $customerId)
|
|
|
|
|
->whereIn('type', [PargarSyncService::TYPE_WALLET_CASH, PargarSyncService::TYPE_WALLET_GOLD])
|
|
|
|
|
->where('created_at', '>=', now()->subDays(3))
|
|
|
|
|
->whereIn('sync_status', [PargarSyncService::STATUS_PENDING, PargarSyncService::STATUS_SUCCESS])
|
|
|
|
|
->get();
|
|
|
|
|
|
|
|
|
|
foreach ($logs as $log) {
|
|
|
|
|
$payload = json_decode((string) $log->data, true) ?: [];
|
|
|
|
|
$signed = (float) ($payload['signed_amount'] ?? 0);
|
|
|
|
|
$kind = (string) ($payload['kind'] ?? '');
|
|
|
|
|
|
|
|
|
|
if ($kind === 'gold' && abs($goldDelta) >= 0.01 && abs(abs($signed) - abs($goldDelta)) < 0.01) {
|
|
|
|
|
if (($signed > 0 && $goldDelta > 0) || ($signed < 0 && $goldDelta < 0)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if ($kind === 'cash' && abs($rialDelta) >= 1 && abs(abs($signed) - abs($rialDelta)) < 1) {
|
|
|
|
|
if (($signed > 0 && $rialDelta > 0) || ($signed < 0 && $rialDelta < 0)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 13:25:18 +03:30
|
|
|
private function hasPendingOutboundWalletSync(int $customerId): bool
|
|
|
|
|
{
|
|
|
|
|
return PargarSyncLog::query()
|
|
|
|
|
->where('customer_id', $customerId)
|
|
|
|
|
->whereIn('type', [PargarSyncService::TYPE_WALLET_CASH, PargarSyncService::TYPE_WALLET_GOLD])
|
|
|
|
|
->where('sync_status', PargarSyncService::STATUS_PENDING)
|
|
|
|
|
->exists();
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 14:33:16 +03:30
|
|
|
private function normalizeNumberString(mixed $value): string
|
|
|
|
|
{
|
|
|
|
|
return (string) $this->parseNumber($value);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 12:36:59 +03:30
|
|
|
private function parseNumber(mixed $value): float
|
|
|
|
|
{
|
|
|
|
|
if ($value === null || $value === '') {
|
|
|
|
|
return 0.0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (float) str_replace('/', '.', (string) $value);
|
|
|
|
|
}
|
|
|
|
|
}
|