Files
boursecu_server/app/Services/PargarInboundWalletService.php
T

688 lines
27 KiB
PHP
Raw Normal View History

2026-07-21 12:36:59 +03:30
<?php
namespace App\Services;
use App\Models\Customer;
2026-07-26 19:18:16 +03:30
use App\Models\Order;
2026-07-21 12:36:59 +03:30
use App\Models\PargarSyncLog;
2026-07-25 12:02:58 +03:30
use Illuminate\Support\Carbon;
2026-07-21 14:13:26 +03:30
use Illuminate\Support\Facades\Cache;
2026-07-26 14:15:26 +03:30
use Illuminate\Support\Facades\DB;
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-26 14:15:26 +03:30
use Rooyesh\Wallet\models\Transaction;
2026-07-21 12:36:59 +03:30
/**
2026-07-22 01:05:00 +03:30
* همگام‌سازی کیف با پرگار.
2026-07-21 14:13:26 +03:30
*
2026-07-21 14:33:16 +03:30
* قواعد سخت:
2026-07-22 01:05:00 +03:30
* 1) webhook اسناد inbound فقط لاگ می‌کند؛ کیف را با replay تاریخچه عوض نمی‌کند.
2026-07-25 12:02:58 +03:30
* 2) دامنهٔ مانده: RialInv = کیف ریالی، GoldInv = طلای کیف اصلی (main_wallet_products).
* محصولات خارج از کیف اصلی هرگز از مانده اثر نمی‌گیرند.
* 3) ماندهٔ کهنه دور ریخته می‌شود، نه معوق: اگر سند ما هنوز pending باشد یا snapshot
* پیش از ثبت سند گرفته شده باشد، اعمال نمی‌شود و منتظر ماندهٔ بعدی می‌مانیم.
2026-07-27 15:44:21 +03:30
* 4) ماندهٔ تازهٔ پرگار (از جمله صفر/تغییر دستی حسابدار) بلافاصله اعمال می‌شود —
* دیگر منتظر دو snapshot یکسان یا بلوک صفر نمی‌مانیم.
2026-07-26 14:15:26 +03:30
* 5) اگر واریز/برداشت ریالی‌ای اصلاً سند پرگار نداشته باشد، مانده اعمال نمی‌شود؛
* پرگار از آن پول خبر ندارد و اعمال مانده موجودی مشتری را پاک می‌کند.
2026-07-27 15:44:21 +03:30
* 6) دکمهٔ ادمین با force می‌تواند گیت‌های سند pending را رد کند (ابزار اصلاح دستی).
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-26 14:15:26 +03:30
/** واریز و برداشت ریالی — دو نوعی که باید سند ریالی پرگار داشته باشند */
public const CASH_DOCUMENT_TX_TYPES = [3, 4];
2026-07-21 21:38:48 +03:30
/** سقف تعداد دسته برای علامت‌زدن در هر کلیک */
private const MAX_BATCHES_PER_SYNC = 50;
2026-07-21 14:33:16 +03:30
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-22 01:05:00 +03:30
* آیا سندی از پنل در صف ثبت داخل پرگار است؟ (کیف یا سفارش)
* در این حالت مانده را اعمال نمی‌کنیم تا بعد از ثبت سند، ماندهٔ درست بیاید.
*/
public function hasPendingOutboundToPargar(Customer $customer): bool
{
2026-07-26 19:18:16 +03:30
return $this->blockingOutboundLog($customer) !== null;
}
/**
* سند پنلی که واقعاً منتظر ثبت در پرگار است.
*
* لاگ pending که دیگر قابل ارسال نیست (سفارش حذف شده یا از حالت «موفق» درآمده)
* هیچ‌وقت تأیید نمی‌شود و اگر بشمریمش، مانده برای همیشه قفل می‌ماند؛ پس آن را
* failed می‌کنیم تا از صف بیرون برود.
*/
public function blockingOutboundLog(Customer $customer): ?PargarSyncLog
{
$logs = PargarSyncLog::query()
2026-07-22 01:05:00 +03:30
->where('customer_id', $customer->id)
->whereIn('type', [
PargarSyncService::TYPE_WALLET_CASH,
PargarSyncService::TYPE_WALLET_GOLD,
PargarSyncService::TYPE_ORDER,
])
->where('sync_status', PargarSyncService::STATUS_PENDING)
2026-07-26 19:18:16 +03:30
->orderBy('id')
->get();
foreach ($logs as $log) {
if ($this->isDeliverableOutbound($log)) {
return $log;
}
$this->retireDeadOutbound($log);
}
return null;
}
/**
* آیا این لاگ در نوبت ارسال به پرگار (GET /pargar/orders) قرار می‌گیرد؟
*/
private function isDeliverableOutbound(PargarSyncLog $log): bool
{
if ($log->type !== PargarSyncService::TYPE_ORDER) {
// سند کیف تا وقتی pending است در هر بار fetch پرگار فرستاده می‌شود
return true;
}
$orderId = (int) ($log->order_id ?? 0);
if ($orderId <= 0) {
return false;
}
return Order::query()
->where('id', $orderId)
->where('sts', 2)
->whereNull('pargar_confirmed_at')
2026-07-22 01:05:00 +03:30
->exists();
}
2026-07-26 19:18:16 +03:30
private function retireDeadOutbound(PargarSyncLog $log): void
{
app(PargarSyncService::class)->markFailed(
$log,
null,
'سفارش این سند دیگر در نوبت ارسال به پرگار نیست (حذف یا تغییر وضعیت) — از صف خارج شد'
);
Log::warning('Pargar outbound log retired', [
'log_id' => $log->id,
'customer_id' => $log->customer_id,
'order_id' => $log->order_id,
]);
}
2026-07-22 01:05:00 +03:30
/**
* دکمهٔ ادمین / همگام‌سازی خودکار:
* ۱) علامت مصرف دسته‌های سند inbound (بدون نوشتن روی کیف)
2026-07-21 21:38:48 +03:30
* ۲) هم‌ترازی مانده با آخرین لاگ mande
2026-07-21 14:33:16 +03:30
*
2026-07-25 12:02:58 +03:30
* @param bool $forceMande اصلاح دستی ادمین: گیت‌های اعتبارسنجی مانده رد شوند
* @param bool $auto مسیر خودکار (webhook/صف) — همیشه با گیت کامل
2026-07-21 14:13:26 +03:30
* @return array{
2026-07-21 21:38:48 +03:30
* documents: array{applied: int, skipped: int, batches: int, acknowledged: 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-22 01:05:00 +03:30
public function syncWalletFromPargar(Customer $customer, bool $forceMande = false, bool $auto = false): array
2026-07-21 12:49:08 +03:30
{
2026-07-21 14:33:16 +03:30
if (self::isApplyFrozen()) {
2026-07-21 14:23:26 +03:30
return [
2026-07-21 21:38:48 +03:30
'documents' => [
'applied' => 0,
'skipped' => 0,
'batches' => 0,
'acknowledged' => 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 21:38:48 +03:30
$lock = Cache::lock('pargar-wallet-sync:'.$customer->id, 60);
2026-07-21 14:13:26 +03:30
if (! $lock->get()) {
return [
2026-07-21 21:38:48 +03:30
'documents' => [
'applied' => 0,
'skipped' => 0,
'batches' => 0,
'acknowledged' => 0,
'errors' => ['همگام‌سازی در حال اجراست'],
],
2026-07-21 14:33:16 +03:30
'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-22 01:05:00 +03:30
$this->attachOrphanDocumentLogs($customer);
$this->attachOrphanMandeLogs($customer);
// ۱) تاریخچهٔ اسناد inbound را روی کیف replay نکن — فقط مصرف‌شده علامت بزن
2026-07-21 21:38:48 +03:30
$documents = $this->acknowledgePendingDocumentBatches($customer);
2026-07-21 14:33:16 +03:30
2026-07-25 12:02:58 +03:30
// ۲) منبع حقیقت موجودی = ماندهٔ پرگار، اما فقط ماندهٔ معتبر
2026-07-22 01:05:00 +03:30
$mande = $this->applyLatestMandeDifference(
$customer,
2026-07-25 12:02:58 +03:30
force: ! $auto && $forceMande
2026-07-22 01:05:00 +03:30
);
2026-07-21 14:33:16 +03:30
2026-07-21 21:38:48 +03:30
if (($documents['acknowledged'] ?? 0) > 0 || ($mande['applied'] ?? false)) {
2026-07-21 14:33:16 +03:30
$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
/**
2026-07-21 21:38:48 +03:30
* دسته‌های ورودی پرگار را بدون تغییر کیف، مصرف‌شده علامت می‌زند.
* (اعمال تک‌تک bed/bes تاریخچه = تورم کیف و 504)
2026-07-21 14:33:16 +03:30
*
2026-07-21 21:38:48 +03:30
* @return array{applied: int, skipped: int, batches: int, acknowledged: int, errors: list<string>}
2026-07-21 14:33:16 +03:30
*/
2026-07-21 21:38:48 +03:30
public function acknowledgePendingDocumentBatches(Customer $customer): array
2026-07-21 14:33:16 +03:30
{
$result = [
'applied' => 0,
'skipped' => 0,
'batches' => 0,
2026-07-21 21:38:48 +03:30
'acknowledged' => 0,
2026-07-21 14:33:16 +03:30
'errors' => [],
];
2026-07-26 19:18:16 +03:30
// فقط شناسهٔ دسته‌های علامت‌نخورده — payload دسته‌های تأییدشده می‌تواند مگابایتی باشد
// و کشیدن دوبارهٔ آن‌ها در هر کلیک همان کندی/۵۰۴ است.
$batchIds = PargarSyncLog::query()
2026-07-21 14:33:16 +03:30
->where('type', PargarSyncService::TYPE_DOCUMENT)
->where('customer_id', $customer->id)
2026-07-26 19:18:16 +03:30
->whereNotNull('data')
->where('data', 'not like', '%wallet_batch_applied%')
->where('data', 'not like', '%"wallet_applied": true%')
->where('data', 'not like', '%"wallet_applied":true%')
2026-07-21 14:33:16 +03:30
->orderBy('id')
2026-07-26 19:18:16 +03:30
->limit(self::MAX_BATCHES_PER_SYNC)
->pluck('id');
2026-07-21 14:33:16 +03:30
2026-07-26 19:18:16 +03:30
foreach ($batchIds as $batchId) {
2026-07-21 14:33:16 +03:30
if ($result['batches'] >= self::MAX_BATCHES_PER_SYNC) {
break;
}
2026-07-26 19:18:16 +03:30
$batch = PargarSyncLog::find($batchId);
if ($batch === null) {
continue;
}
2026-07-21 14:33:16 +03:30
$data = json_decode((string) $batch->data, true);
if (! is_array($data)) {
continue;
}
if (empty($data['transactions']) || ! is_array($data['transactions'])) {
continue;
}
if (! empty($data['wallet_batch_applied']) || ! empty($data['wallet_applied'])) {
continue;
}
$result['batches']++;
2026-07-21 21:38:48 +03:30
$txCount = count($data['transactions']);
$result['skipped'] += $txCount;
2026-07-21 14:33:16 +03:30
2026-07-21 21:31:16 +03:30
try {
2026-07-21 21:38:48 +03:30
$data['wallet_batch_ack_only'] = true;
$data['wallet_batch_ack_reason'] = 'mande_is_source_of_truth';
2026-07-21 21:31:16 +03:30
$this->markDocumentBatchApplied($batch, $data);
2026-07-21 21:38:48 +03:30
$result['acknowledged']++;
2026-07-21 21:31:16 +03:30
} catch (\Throwable $e) {
$result['errors'][] = 'علامت دسته #'.$batch->id.' ناموفق: '.$e->getMessage();
2026-07-21 21:38:48 +03:30
Log::error('Pargar acknowledgeDocumentBatch failed', [
2026-07-21 21:31:16 +03:30
'log_id' => $batch->id,
'customer_id' => $customer->id,
'error' => $e->getMessage(),
]);
2026-07-21 14:33:16 +03:30
}
}
return $result;
}
2026-07-21 21:31:16 +03:30
/**
2026-07-21 21:38:48 +03:30
* @deprecated از sync دکمه حذف شد — فقط برای سازگاری احتمالی تست/کال‌های قدیمی
*
* @return array{applied: int, skipped: int, batches: int, errors: list<string>}
*/
public function applyPendingDocumentBatches(Customer $customer): array
{
$ack = $this->acknowledgePendingDocumentBatches($customer);
return [
'applied' => 0,
'skipped' => (int) ($ack['skipped'] ?? 0),
'batches' => (int) ($ack['batches'] ?? 0),
'errors' => $ack['errors'] ?? [],
];
}
/**
* علامت مصرف دسته — بدون Pretty-Print؛ در صورت شکست ذخیره، stub فشرده می‌نویسد.
2026-07-21 21:31:16 +03:30
*
* @param array<string, mixed> $data
*/
private function markDocumentBatchApplied(PargarSyncLog $batch, array $data): void
{
$data['wallet_batch_applied'] = true;
$data['wallet_batch_applied_at'] = now()->toDateTimeString();
if (is_string($batch->desc) && ! str_contains($batch->desc, '[اعمال‌شده]')) {
$batch->desc = mb_substr('[اعمال‌شده] '.$batch->desc, 0, 250);
}
2026-07-21 21:38:48 +03:30
// ack-only: همیشه stub فشرده — نیازی به نگه‌داشتن صدها سطر تاریخچه نیست
if (! empty($data['wallet_batch_ack_only'])) {
$batch->data = $this->encodeAppliedBatchCompact($data);
$batch->save();
return;
}
2026-07-21 21:31:16 +03:30
$encoded = json_encode($data, JSON_UNESCAPED_UNICODE);
if ($encoded === false) {
throw new \RuntimeException('json_encode failed for pargar batch #'.$batch->id);
}
$batch->data = $encoded;
try {
$batch->save();
} catch (\Throwable $e) {
$batch->data = $this->encodeAppliedBatchCompact($data);
$batch->save();
}
}
/**
* @param array<string, mixed> $data
*/
private function encodeAppliedBatchCompact(array $data): string
{
$transactions = is_array($data['transactions'] ?? null) ? $data['transactions'] : [];
return (string) json_encode([
'accountName' => $data['accountName'] ?? null,
'accountId' => $data['accountId'] ?? null,
'transactions_count' => count($transactions),
'transactions_fp' => hash('sha256', (string) json_encode($transactions, JSON_UNESCAPED_UNICODE)),
'wallet_batch_applied' => true,
'wallet_batch_applied_at' => $data['wallet_batch_applied_at'] ?? now()->toDateTimeString(),
2026-07-21 21:38:48 +03:30
'wallet_batch_ack_only' => true,
2026-07-21 21:31:16 +03:30
'payload_compacted' => true,
], JSON_UNESCAPED_UNICODE);
}
2026-07-21 12:49:08 +03:30
/**
2026-07-25 12:02:58 +03:30
* ماندهٔ پرگار روی کیف اپ:
* RialInv → کیف ریالی، GoldInv → طلای کیف اصلی (main_wallet_products).
* محصولات خارج از کیف اصلی (قلک سرمایه‌گذاری، سکه و …) هیچ سهمی در مانده ندارند و
* هرگز از آن اثر نمی‌گیرند.
*
* @param bool $force اعمال دستی ادمین — گیت‌های اعتبارسنجی نادیده گرفته می‌شوند
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
{
$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
}
2026-07-25 12:02:58 +03:30
if (! $force) {
$block = $this->mandeBlockReason($customer, $log, $data);
if ($block !== null) {
$this->markMandeSkipped($log, $data, $block['reason']);
return ['applied' => false, 'message' => $block['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;
2026-07-25 12:02:58 +03:30
$mainGold = app(WalletBalanceService::class)
->effectiveMainWalletGold((int) $customer->id, $ownerType);
2026-07-21 22:23:09 +03:30
$goldDiff = $goldInv - $mainGold;
2026-07-21 12:49:08 +03:30
$rialDiff = $rialInv - (float) ($wallet->cash ?? 0);
if (abs($goldDiff) < 0.01 && abs($rialDiff) < 1) {
$data['wallet_applied_from_mande'] = true;
2026-07-21 21:31:16 +03:30
$log->data = json_encode($data, JSON_UNESCAPED_UNICODE);
2026-07-21 12:49:08 +03:30
$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);
}
2026-07-26 14:15:26 +03:30
// مانده پرگار می‌تواند منفی باشد و کیف باید همان را نشان دهد؛ کف موجودی اینجا اعمال نمی‌شود
2026-07-21 12:49:08 +03:30
new TransactionInterface(
$walletInterface,
$input,
self::TX_TYPE_PARGAR,
$goldDiff > 0,
2026-07-26 14:15:26 +03:30
$rialDiff > 0,
0,
[],
true
2026-07-21 12:49:08 +03:30
);
$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-25 12:02:58 +03:30
$data['applied_forced'] = $force;
2026-07-21 22:23:09 +03:30
$data['main_gold_before'] = $mainGold;
2026-07-25 12:02:58 +03:30
unset($data['mande_skip_reason'], $data['mande_skipped_at']);
2026-07-21 21:31:16 +03:30
$log->data = json_encode($data, JSON_UNESCAPED_UNICODE);
2026-07-21 12:49:08 +03:30
$log->desc = 'اعمال ماندهٔ پرگار روی کیف اپ';
$log->save();
2026-07-25 12:02:58 +03:30
Log::info('Pargar mande applied to wallet', [
'customer_id' => (int) $customer->id,
'mande_log_id' => (int) $log->id,
'gold_delta' => $goldDiff,
'rial_delta' => $rialDiff,
'forced' => $force,
]);
2026-07-21 12:49:08 +03:30
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 22:23:09 +03:30
/**
2026-07-27 15:44:21 +03:30
* ماندهٔ پرگار منبع حقیقت است — از جمله تغییر دستی حسابدار در پنل پرگار.
* فقط وقتی نگه می‌داریم که هنوز سندِ ساخته‌شدهٔ خودمان در پرگار ثبت نشده باشد؛
* وگرنه موجودی مشتری به قبل از آن سند برمی‌گردد («وزن/ریالم کم شد»).
*
* دیگر منتظر «دو snapshot یکسان» یا بلوک صفر نمی‌مانیم: حسابدار ممکن است عمداً
* مانده را صفر/عوض کند و باید همان لحظه روی اپ بنشیند.
2026-07-25 12:02:58 +03:30
*
* @param array<string, mixed> $data
* @return array{reason: string, message: string}|null null یعنی معتبر
2026-07-21 22:23:09 +03:30
*/
2026-07-25 12:02:58 +03:30
private function mandeBlockReason(Customer $customer, PargarSyncLog $log, array $data): ?array
2026-07-21 22:23:09 +03:30
{
2026-07-26 19:18:16 +03:30
$blocking = $this->blockingOutboundLog($customer);
if ($blocking !== null) {
2026-07-25 12:02:58 +03:30
return [
'reason' => 'pending_outbound',
2026-07-26 19:18:16 +03:30
'message' => 'سند پنل شمارهٔ ' . $blocking->id
. ' (' . $this->outboundLogLabel($blocking) . ') هنوز در پرگار ثبت نشده — مانده اعمال نمی‌شود',
2026-07-25 12:02:58 +03:30
];
2026-07-21 22:23:09 +03:30
}
2026-07-25 12:02:58 +03:30
$lastConfirmedAt = $this->lastOutboundConfirmedAt($customer);
$takenAt = $log->created_at;
if ($lastConfirmedAt !== null && ($takenAt === null || $takenAt->lte($lastConfirmedAt))) {
return [
'reason' => 'taken_before_document',
'message' => 'این مانده پیش از ثبت آخرین سند گرفته شده — نادیده گرفته شد',
];
}
2026-07-26 14:15:26 +03:30
$missing = $this->unexportedCashMovement($customer, $lastConfirmedAt);
if ($missing !== null) {
return [
'reason' => 'unexported_wallet_movement',
'message' => 'حرکت ریالی '
. number_format($missing['amount'])
. ' ریال هنوز سندی در پرگار ندارد — تا ثبت سند، مانده اعمال نمی‌شود',
];
}
2026-07-25 12:02:58 +03:30
return null;
}
2026-07-26 19:18:16 +03:30
private function outboundLogLabel(PargarSyncLog $log): string
{
return match ($log->type) {
PargarSyncService::TYPE_ORDER => 'سفارش ' . $log->order_id,
PargarSyncService::TYPE_WALLET_GOLD => 'شارژ طلای کیف',
PargarSyncService::TYPE_WALLET_CASH => 'سند ریالی کیف',
default => (string) $log->type,
};
}
2026-07-26 14:15:26 +03:30
/**
* واریز/برداشت ریالی که سند پرگارش ساخته نشده است.
*
* تا وقتی پرگار از این حرکت خبر ندارد، ماندهٔ آن هم شامل این پول نیست؛ اعمال آن
* موجودی مشتری را پاک می‌کند. پس مانده را نگه می‌داریم تا سند ساخته و ثبت شود.
*
* @return array{amount: float, withdraw_ids: list<int>}|null
*/
private function unexportedCashMovement(Customer $customer, ?Carbon $lastConfirmedAt): ?array
{
$since = $this->cashMovementScanStart($customer, $lastConfirmedAt);
if ($since === null) {
return null;
}
$rows = Transaction::query()
->where('customer_id', $customer->id)
->whereIn('type', self::CASH_DOCUMENT_TX_TYPES)
->whereNotNull('withdraw_id')
->where('created_at', '>', $since)
->where(function ($q) {
$q->where('cash', '>=', 1)->orWhere('cash', '<=', -1);
})
->whereNotExists(function ($q) {
$q->select(DB::raw(1))
->from('pargar_sync_logs')
->whereColumn('pargar_sync_logs.withdraw_id', 'transactions.withdraw_id')
->where('pargar_sync_logs.type', PargarSyncService::TYPE_WALLET_CASH);
})
->get(['withdraw_id', 'cash']);
if ($rows->isEmpty()) {
return null;
}
return [
'amount' => (float) $rows->sum(fn ($row) => abs((float) $row->cash)),
'withdraw_ids' => $rows->pluck('withdraw_id')->map(fn ($id) => (int) $id)->all(),
];
}
/**
* از کجا دنبال سند جاافتاده بگردیم: بعد از آخرین سند ثبت‌شده، و اگر نبود از
* شروع اتصال همین مشتری به پرگار. تراکنش‌های قبل از اتصال سند ندارند و نباید
* سینک را برای همیشه قفل کنند.
*/
private function cashMovementScanStart(Customer $customer, ?Carbon $lastConfirmedAt): ?Carbon
{
$firstLogAt = PargarSyncLog::query()
->where('customer_id', $customer->id)
->min('created_at');
$integrationStart = $firstLogAt !== null ? Carbon::parse($firstLogAt) : null;
if ($lastConfirmedAt !== null && $integrationStart !== null) {
return $lastConfirmedAt->greaterThan($integrationStart) ? $lastConfirmedAt : $integrationStart;
}
return $lastConfirmedAt ?? $integrationStart;
}
2026-07-25 12:02:58 +03:30
/**
* آخرین لحظه‌ای که پرگار یکی از اسناد ما را ثبت کرده است.
*/
private function lastOutboundConfirmedAt(Customer $customer): ?Carbon
{
if (! PargarSyncLog::hasConfirmedAtColumn()) {
return null;
}
$value = PargarSyncLog::query()
->where('customer_id', $customer->id)
->whereIn('type', [
PargarSyncService::TYPE_WALLET_CASH,
PargarSyncService::TYPE_WALLET_GOLD,
PargarSyncService::TYPE_ORDER,
])
->whereNotNull('confirmed_at')
->max('confirmed_at');
return $value !== null ? Carbon::parse($value) : null;
}
/**
* @param array<string, mixed> $data
*/
private function markMandeSkipped(PargarSyncLog $log, array $data, string $reason): void
{
if (($data['mande_skip_reason'] ?? null) === $reason) {
return;
}
$data['mande_skip_reason'] = $reason;
$data['mande_skipped_at'] = now()->toDateTimeString();
$log->data = json_encode($data, JSON_UNESCAPED_UNICODE);
$log->save();
2026-07-21 22:23:09 +03:30
}
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);
}
2026-07-22 00:31:38 +03:30
/**
2026-07-22 01:05:00 +03:30
* همگام‌سازی خودکار در پس‌زمینه — بعد از مانده / تأیید سند در پرگار / import.
2026-07-22 00:31:38 +03:30
*/
public function queueWalletSyncAfterResponse(int $customerId): void
{
dispatch(function () use ($customerId) {
$customer = Customer::find($customerId);
if ($customer === null || ! $customer->hasValidPargarAccountId()) {
return;
}
try {
2026-07-22 01:05:00 +03:30
$this->syncWalletFromPargar($customer, forceMande: false, auto: true);
2026-07-22 00:31:38 +03:30
} catch (\Throwable $e) {
Log::warning('Pargar auto wallet sync failed', [
'customer_id' => $customerId,
'error' => $e->getMessage(),
]);
}
})->afterResponse();
}
/**
* لاگ‌های مانده‌ای که قبل از ساخت/لینک مشتری با pargar_ref آمده‌اند را به مشتری وصل می‌کند.
*/
public function attachOrphanMandeLogs(Customer $customer): int
{
if (! $customer->hasValidPargarAccountId()) {
return 0;
}
$accountId = trim((string) $customer->pargar_account_id);
return PargarSyncLog::query()
->where('type', PargarSyncService::TYPE_MANDE)
->where('pargar_ref', $accountId)
->where(function ($q) {
$q->whereNull('customer_id')->orWhere('customer_id', 0);
})
->update(['customer_id' => $customer->id]);
}
2026-07-22 01:05:00 +03:30
/**
* اسناد یتیم (قبل از لینک مشتری) را به customer_id وصل می‌کند.
*/
public function attachOrphanDocumentLogs(Customer $customer): int
{
if (! $customer->hasValidPargarAccountId()) {
return 0;
}
$accountId = trim((string) $customer->pargar_account_id);
return PargarSyncLog::query()
->where('type', PargarSyncService::TYPE_DOCUMENT)
->where('pargar_ref', $accountId)
->where(function ($q) {
$q->whereNull('customer_id')->orWhere('customer_id', 0);
})
->update(['customer_id' => $customer->id]);
}
2026-07-21 12:36:59 +03:30
}