fix pargar bugs 6
This commit is contained in:
@@ -0,0 +1,123 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Customer;
|
||||||
|
use App\Services\PargarInboundWalletService;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Rooyesh\Wallet\interfaces\WalletInterface;
|
||||||
|
use Rooyesh\Wallet\models\Transaction;
|
||||||
|
|
||||||
|
class PargarWalletFreezeCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'pargar:wallet-freeze
|
||||||
|
{action=on : on|off|status}
|
||||||
|
{--customer= : شناسه مشتری برای گزارش تراکنشهای type=22}
|
||||||
|
{--set-cash= : تنظیم مستقیم cash کیف اصلی}
|
||||||
|
{--set-gold= : تنظیم مستقیم gold کیف اصلی}';
|
||||||
|
|
||||||
|
protected $description = 'یخزدن اضطراری اعمال کیف از پرگار + اصلاح دستی موجودی';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$action = strtolower((string) $this->argument('action'));
|
||||||
|
|
||||||
|
if ($action === 'on') {
|
||||||
|
PargarInboundWalletService::freezeApplies(true);
|
||||||
|
$this->warn('اعمال کیف از پرگار یخزده شد (KILL SWITCH ON)');
|
||||||
|
} elseif ($action === 'off') {
|
||||||
|
PargarInboundWalletService::freezeApplies(false);
|
||||||
|
$this->info('اعمال کیف از پرگار آزاد شد (KILL SWITCH OFF)');
|
||||||
|
} elseif ($action === 'status') {
|
||||||
|
$frozen = PargarInboundWalletService::isApplyFrozen();
|
||||||
|
$this->line($frozen ? 'وضعیت: یخزده (ON)' : 'وضعیت: آزاد (OFF)');
|
||||||
|
} else {
|
||||||
|
$this->error('action باید on|off|status باشد');
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$customerId = (int) ($this->option('customer') ?: 0);
|
||||||
|
if ($customerId > 0) {
|
||||||
|
$this->reportCustomer($customerId);
|
||||||
|
$this->maybeSetBalances($customerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->newLine();
|
||||||
|
$this->warn('اگر هنوز موجودی بالا میرود: php-fpm / octane / queue را حتماً ریاستارت کنید.');
|
||||||
|
$this->line('درخواستهای قدیمیِ قبل از دیپلوی با کد قبلی هنوز ممکن است در حال اجرا باشند.');
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function reportCustomer(int $customerId): void
|
||||||
|
{
|
||||||
|
$customer = Customer::find($customerId);
|
||||||
|
if (! $customer) {
|
||||||
|
$this->error("مشتری {$customerId} یافت نشد");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$wallet = (new WalletInterface($customerId, (int) ($customer->type ?? 2)))->wallet;
|
||||||
|
$this->info("مشتری {$customerId} — {$customer->name}");
|
||||||
|
$this->line(' cash='.((float) ($wallet->cash ?? 0)));
|
||||||
|
$this->line(' gold='.((float) ($wallet->gold ?? 0)));
|
||||||
|
|
||||||
|
$recent = Transaction::query()
|
||||||
|
->where('customer_id', $customerId)
|
||||||
|
->where('type', PargarInboundWalletService::TX_TYPE_PARGAR)
|
||||||
|
->orderByDesc('id')
|
||||||
|
->limit(15)
|
||||||
|
->get(['id', 'cash', 'gold', 'title', 'note', 'created_at']);
|
||||||
|
|
||||||
|
$this->line('آخرین تراکنشهای type=22 (پرگار):');
|
||||||
|
foreach ($recent as $row) {
|
||||||
|
$this->line(sprintf(
|
||||||
|
' #%d cash=%s gold=%s at=%s title=%s',
|
||||||
|
$row->id,
|
||||||
|
$row->cash,
|
||||||
|
$row->gold,
|
||||||
|
$row->created_at,
|
||||||
|
$row->title
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$countLastMinute = Transaction::query()
|
||||||
|
->where('customer_id', $customerId)
|
||||||
|
->where('type', PargarInboundWalletService::TX_TYPE_PARGAR)
|
||||||
|
->where('created_at', '>=', now()->subMinute())
|
||||||
|
->count();
|
||||||
|
$this->line("تراکنش type=22 در ۱ دقیقهٔ اخیر: {$countLastMinute}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private function maybeSetBalances(int $customerId): void
|
||||||
|
{
|
||||||
|
$setCash = $this->option('set-cash');
|
||||||
|
$setGold = $this->option('set-gold');
|
||||||
|
if ($setCash === null && $setGold === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $this->confirm("موجودی مشتری {$customerId} مستقیم در DB عوض شود؟", false)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$customer = Customer::findOrFail($customerId);
|
||||||
|
$wallet = (new WalletInterface($customerId, (int) ($customer->type ?? 2)))->wallet;
|
||||||
|
if (! $wallet) {
|
||||||
|
$this->error('کیف یافت نشد');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table($wallet->getTable())->where('id', $wallet->id)->update(array_filter([
|
||||||
|
'cash' => $setCash !== null ? (float) $setCash : null,
|
||||||
|
'gold' => $setGold !== null ? (float) $setGold : null,
|
||||||
|
'updated_at' => now(),
|
||||||
|
], fn ($v) => $v !== null));
|
||||||
|
|
||||||
|
$this->info('موجودی بهروز شد. صفحه را رفرش کنید.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -240,13 +240,14 @@ class PargarInboundController extends Controller
|
|||||||
? Customer::where('pargar_account_id', $accountId)->first()
|
? Customer::where('pargar_account_id', $accountId)->first()
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// فقط لاگ — اعمال خودکار روی کیف خاموش است (جلوگیری از شارژ چندباره وقتی پرگار مکرر میفرستد)
|
// فقط لاگ — اعمال خودکار روی کیف کاملاً خاموش است
|
||||||
|
// اگر هنوز موجودی بالا میرود = worker/php قدیمی هنوز زنده است → ریاستارت الزامی
|
||||||
PargarSyncLog::create([
|
PargarSyncLog::create([
|
||||||
'customer_id' => $customer?->id,
|
'customer_id' => $customer?->id,
|
||||||
'amount' => $summary['amount'],
|
'amount' => $summary['amount'],
|
||||||
'desc' => $customer
|
'desc' => $customer
|
||||||
? ($summary['desc'] !== '' ? $summary['desc'] : 'دریافت اسناد از پرگار')
|
? ('[no-apply] '.($summary['desc'] !== '' ? $summary['desc'] : 'دریافت اسناد از پرگار'))
|
||||||
: 'دریافت اسناد از پرگار — مشتری یافت نشد',
|
: '[no-apply] دریافت اسناد از پرگار — مشتری یافت نشد',
|
||||||
'type' => PargarSyncService::TYPE_DOCUMENT,
|
'type' => PargarSyncService::TYPE_DOCUMENT,
|
||||||
'sync_status' => PargarSyncService::STATUS_SUCCESS,
|
'sync_status' => PargarSyncService::STATUS_SUCCESS,
|
||||||
'pargar_ref' => $accountId !== '' ? $accountId : null,
|
'pargar_ref' => $accountId !== '' ? $accountId : null,
|
||||||
@@ -254,15 +255,23 @@ class PargarInboundController extends Controller
|
|||||||
'_summary' => $summary,
|
'_summary' => $summary,
|
||||||
'wallet_applied' => false,
|
'wallet_applied' => false,
|
||||||
'auto_apply_disabled' => true,
|
'auto_apply_disabled' => true,
|
||||||
|
'code_build' => 'no-apply-v2',
|
||||||
]), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
|
]), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
\Illuminate\Support\Facades\Log::info('Pargar documents stored without wallet apply', [
|
||||||
|
'account_id' => $accountId,
|
||||||
|
'customer_id' => $customer?->id,
|
||||||
|
'tx_count' => is_array($data['transactions'] ?? null) ? count($data['transactions']) : 0,
|
||||||
|
]);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'message' => $customer
|
'message' => 'اسناد ثبت شد بدون تغییر کیف (no-apply-v2)',
|
||||||
? 'اسناد مشتری دریافت و ثبت شد (بدون تغییر کیف)'
|
'data' => [
|
||||||
: 'اسناد دریافت شد ولی مشتری با این کد حساب یافت نشد',
|
'wallet_applied' => false,
|
||||||
'data' => null,
|
'build' => 'no-apply-v2',
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,6 +293,7 @@ class PargarInboundController extends Controller
|
|||||||
$rialInv = isset($data['RialInv']) ? (float) str_replace('/', '.', (string) $data['RialInv']) : 0;
|
$rialInv = isset($data['RialInv']) ? (float) str_replace('/', '.', (string) $data['RialInv']) : 0;
|
||||||
|
|
||||||
// فقط مقایسه برای لاگ — اپ source of truth است؛ کیف را از مانده پرگار عوض نکن
|
// فقط مقایسه برای لاگ — اپ source of truth است؛ کیف را از مانده پرگار عوض نکن
|
||||||
|
// اضطراری: هیچوقت از mande کیف را تغییر نده (حتی اگر کد قدیمی جایی مانده باشد)
|
||||||
$ownerType = (int) ($customer->type ?? 2);
|
$ownerType = (int) ($customer->type ?? 2);
|
||||||
$walletInterface = new WalletInterface($customer->id, $ownerType);
|
$walletInterface = new WalletInterface($customer->id, $ownerType);
|
||||||
$wallet = $walletInterface->wallet;
|
$wallet = $walletInterface->wallet;
|
||||||
|
|||||||
@@ -12,15 +12,15 @@ use Rooyesh\Wallet\interfaces\WalletInterface;
|
|||||||
/**
|
/**
|
||||||
* اعمال اسناد/ماندهٔ ورودی پرگار روی کیف اپ.
|
* اعمال اسناد/ماندهٔ ورودی پرگار روی کیف اپ.
|
||||||
*
|
*
|
||||||
* دکمهٔ ادمین فقط مانده (mande) را یکبار تنظیم میکند.
|
* اضطراری: اعمال اسناد روی کیف بهطور کامل خاموش است تا از شارژ چندباره جلوگیری شود.
|
||||||
* اسناد جدید از webhook با اثرانگشت یکتا اعمال میشوند — بدون replay انبوه لاگهای قدیمی.
|
* دکمهٔ ادمین فقط مانده را (در صورت فعال بودن) یکبار تنظیم میکند.
|
||||||
*/
|
*/
|
||||||
class PargarInboundWalletService
|
class PargarInboundWalletService
|
||||||
{
|
{
|
||||||
public const TX_TYPE_PARGAR = 22;
|
public const TX_TYPE_PARGAR = 22;
|
||||||
|
|
||||||
/** @var array<string, true> */
|
/** کلید کش برای قطع اضطراری هرگونه اعمال کیف از پرگار */
|
||||||
private array $seenFingerprints = [];
|
public const KILL_SWITCH_CACHE_KEY = 'pargar_wallet_apply_disabled';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly PargarSyncService $sync,
|
private readonly PargarSyncService $sync,
|
||||||
@@ -28,6 +28,22 @@ class PargarInboundWalletService
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function isApplyFrozen(): bool
|
||||||
|
{
|
||||||
|
// پیشفرض: یخزده — مگر صریحاً false شده باشد
|
||||||
|
$cached = Cache::get(self::KILL_SWITCH_CACHE_KEY);
|
||||||
|
if ($cached === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (bool) $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function freezeApplies(bool $frozen = true): void
|
||||||
|
{
|
||||||
|
Cache::forever(self::KILL_SWITCH_CACHE_KEY, $frozen);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $data
|
* @param array<string, mixed> $data
|
||||||
* @return array{applied: int, skipped: int, errors: list<string>, logs: list<int>}
|
* @return array{applied: int, skipped: int, errors: list<string>, logs: list<int>}
|
||||||
@@ -35,115 +51,21 @@ class PargarInboundWalletService
|
|||||||
public function applyDocuments(Customer $customer, array $data, bool $broadcast = true): array
|
public function applyDocuments(Customer $customer, array $data, bool $broadcast = true): array
|
||||||
{
|
{
|
||||||
$transactions = is_array($data['transactions'] ?? null) ? $data['transactions'] : [];
|
$transactions = is_array($data['transactions'] ?? null) ? $data['transactions'] : [];
|
||||||
$accountId = trim((string) ($data['accountId'] ?? ''));
|
|
||||||
|
|
||||||
$result = [
|
Log::warning('Pargar applyDocuments blocked (frozen)', [
|
||||||
|
'customer_id' => $customer->id,
|
||||||
|
'tx_count' => count($transactions),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
'applied' => 0,
|
'applied' => 0,
|
||||||
'skipped' => 0,
|
'skipped' => count($transactions),
|
||||||
'errors' => [],
|
'errors' => ['اعمال اسناد پرگار روی کیف موقتاً غیرفعال است'],
|
||||||
'logs' => [],
|
'logs' => [],
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($transactions === []) {
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
$lock = Cache::lock('pargar-wallet-apply:'.$customer->id, 60);
|
|
||||||
if (! $lock->get()) {
|
|
||||||
$result['errors'][] = 'همگامسازی دیگری برای این مشتری در حال اجراست';
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$walletApplied = false;
|
|
||||||
|
|
||||||
foreach ($transactions as $tx) {
|
|
||||||
if (! is_array($tx)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$fingerprint = $this->transactionFingerprint($accountId, $tx);
|
|
||||||
if ($fingerprint === '' || $this->isFingerprintApplied($fingerprint)) {
|
|
||||||
$result['skipped']++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// علامت در حافظه تا در همین درخواست دوباره نخورد
|
|
||||||
$this->seenFingerprints[$fingerprint] = true;
|
|
||||||
|
|
||||||
$goldDelta = $this->transactionGoldDelta($tx);
|
|
||||||
$rialDelta = $this->transactionRialDelta($tx);
|
|
||||||
|
|
||||||
if (abs($goldDelta) < 0.01 && abs($rialDelta) < 1) {
|
|
||||||
$this->recordProcessedFingerprint($customer, $fingerprint, $accountId, $tx, false, 'zero_amount');
|
|
||||||
$result['skipped']++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->isEchoOfRecentOutbound((int) $customer->id, $goldDelta, $rialDelta)) {
|
|
||||||
$this->recordProcessedFingerprint($customer, $fingerprint, $accountId, $tx, false, 'echo_outbound');
|
|
||||||
$result['skipped']++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$this->applyTransactionDelta($customer, $tx, $goldDelta, $rialDelta);
|
|
||||||
$walletApplied = true;
|
|
||||||
|
|
||||||
$summary = $this->sync->summarizeInboundDocument([
|
|
||||||
'accountId' => $accountId,
|
|
||||||
'accountName' => $data['accountName'] ?? null,
|
|
||||||
'transactions' => [$tx],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$log = PargarSyncLog::create([
|
|
||||||
'customer_id' => $customer->id,
|
|
||||||
'amount' => $summary['amount'],
|
|
||||||
'desc' => $summary['desc'] !== '' ? $summary['desc'] : 'اعمال سند پرگار',
|
|
||||||
'type' => PargarSyncService::TYPE_DOCUMENT,
|
|
||||||
'sync_status' => PargarSyncService::STATUS_SUCCESS,
|
|
||||||
'pargar_ref' => $fingerprint,
|
|
||||||
'data' => json_encode([
|
|
||||||
'accountId' => $accountId,
|
|
||||||
'accountName' => $data['accountName'] ?? null,
|
|
||||||
'transaction' => $tx,
|
|
||||||
'wallet_applied' => true,
|
|
||||||
'fingerprint' => $fingerprint,
|
|
||||||
'gold_delta' => $goldDelta,
|
|
||||||
'rial_delta' => $rialDelta,
|
|
||||||
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$result['applied']++;
|
|
||||||
$result['logs'][] = (int) $log->id;
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
unset($this->seenFingerprints[$fingerprint]);
|
|
||||||
Log::warning('Pargar inbound document apply failed', [
|
|
||||||
'customer_id' => $customer->id,
|
|
||||||
'fingerprint' => $fingerprint,
|
|
||||||
'error' => $e->getMessage(),
|
|
||||||
]);
|
|
||||||
$result['errors'][] = 'سند: '.$e->getMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($walletApplied && $broadcast) {
|
|
||||||
$this->walletBroadcast->broadcastForCustomer(
|
|
||||||
(int) $customer->id,
|
|
||||||
(int) ($customer->type ?? 2)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
$lock->release();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* دکمهٔ ادمین: فقط اختلاف ماندهٔ پرگار را یکبار اعمال میکند (نه replay اسناد).
|
|
||||||
*
|
|
||||||
* @return array{
|
* @return array{
|
||||||
* documents: array{applied: int, skipped: int, errors: list<string>, logs: list<int>},
|
* documents: array{applied: int, skipped: int, errors: list<string>, logs: list<int>},
|
||||||
* mande: array{applied: bool, gold_delta?: float, rial_delta?: float, message: string}
|
* mande: array{applied: bool, gold_delta?: float, rial_delta?: float, message: string}
|
||||||
@@ -151,6 +73,16 @@ class PargarInboundWalletService
|
|||||||
*/
|
*/
|
||||||
public function syncWalletFromPargar(Customer $customer, bool $forceMande = false): array
|
public function syncWalletFromPargar(Customer $customer, bool $forceMande = false): array
|
||||||
{
|
{
|
||||||
|
if (self::isApplyFrozen() && ! $forceMande) {
|
||||||
|
return [
|
||||||
|
'documents' => ['applied' => 0, 'skipped' => 0, 'errors' => [], 'logs' => []],
|
||||||
|
'mande' => [
|
||||||
|
'applied' => false,
|
||||||
|
'message' => 'اعمال کیف از پرگار یخزده است — ابتدا با دستور pargar:wallet-freeze off آزاد کنید',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
$lock = Cache::lock('pargar-wallet-sync:'.$customer->id, 90);
|
$lock = Cache::lock('pargar-wallet-sync:'.$customer->id, 90);
|
||||||
if (! $lock->get()) {
|
if (! $lock->get()) {
|
||||||
return [
|
return [
|
||||||
@@ -160,7 +92,6 @@ class PargarInboundWalletService
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// عمداً replay اسناد قدیمی نداریم — باعث شارژ چندباره میشد
|
|
||||||
return [
|
return [
|
||||||
'documents' => ['applied' => 0, 'skipped' => 0, 'errors' => [], 'logs' => []],
|
'documents' => ['applied' => 0, 'skipped' => 0, 'errors' => [], 'logs' => []],
|
||||||
'mande' => $this->applyLatestMandeDifference($customer, force: $forceMande),
|
'mande' => $this->applyLatestMandeDifference($customer, force: $forceMande),
|
||||||
@@ -175,6 +106,13 @@ class PargarInboundWalletService
|
|||||||
*/
|
*/
|
||||||
public function applyLatestMandeDifference(Customer $customer, bool $force = false): array
|
public function applyLatestMandeDifference(Customer $customer, bool $force = false): array
|
||||||
{
|
{
|
||||||
|
if (self::isApplyFrozen() && ! $force) {
|
||||||
|
return [
|
||||||
|
'applied' => false,
|
||||||
|
'message' => 'اعمال کیف از پرگار یخزده است',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->hasPendingOutboundWalletSync((int) $customer->id)) {
|
if ($this->hasPendingOutboundWalletSync((int) $customer->id)) {
|
||||||
return [
|
return [
|
||||||
'applied' => false,
|
'applied' => false,
|
||||||
@@ -254,165 +192,6 @@ class PargarInboundWalletService
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @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));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isFingerprintApplied(string $fingerprint): bool
|
|
||||||
{
|
|
||||||
if ($fingerprint === '' || isset($this->seenFingerprints[$fingerprint])) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return PargarSyncLog::query()
|
|
||||||
->where('type', PargarSyncService::TYPE_DOCUMENT)
|
|
||||||
->where('pargar_ref', $fingerprint)
|
|
||||||
->where('sync_status', PargarSyncService::STATUS_SUCCESS)
|
|
||||||
->exists();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $tx
|
|
||||||
*/
|
|
||||||
private function recordProcessedFingerprint(
|
|
||||||
Customer $customer,
|
|
||||||
string $fingerprint,
|
|
||||||
string $accountId,
|
|
||||||
array $tx,
|
|
||||||
bool $walletApplied,
|
|
||||||
string $reason
|
|
||||||
): void {
|
|
||||||
if ($this->isFingerprintApplied($fingerprint) && ! isset($this->seenFingerprints[$fingerprint])) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// اگر فقط در حافظه است و هنوز در DB نیست، برای skipها هم رکورد بساز تا تکرار نشود
|
|
||||||
if (PargarSyncLog::query()
|
|
||||||
->where('type', PargarSyncService::TYPE_DOCUMENT)
|
|
||||||
->where('pargar_ref', $fingerprint)
|
|
||||||
->exists()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
PargarSyncLog::create([
|
|
||||||
'customer_id' => $customer->id,
|
|
||||||
'desc' => $walletApplied
|
|
||||||
? 'اعمال سند پرگار'
|
|
||||||
: ($reason === 'echo_outbound'
|
|
||||||
? 'سند پرگار — قبلاً از پنل ارسال شده (بدون اعمال مجدد)'
|
|
||||||
: 'سند پرگار — رد شده'),
|
|
||||||
'type' => PargarSyncService::TYPE_DOCUMENT,
|
|
||||||
'sync_status' => PargarSyncService::STATUS_SUCCESS,
|
|
||||||
'pargar_ref' => $fingerprint,
|
|
||||||
'data' => json_encode([
|
|
||||||
'accountId' => $accountId,
|
|
||||||
'transaction' => $tx,
|
|
||||||
'wallet_applied' => $walletApplied,
|
|
||||||
'fingerprint' => $fingerprint,
|
|
||||||
'skip_reason' => $walletApplied ? null : $reason,
|
|
||||||
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $tx
|
|
||||||
*/
|
|
||||||
private function applyTransactionDelta(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' => $title,
|
|
||||||
'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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $tx
|
|
||||||
*/
|
|
||||||
private function transactionGoldDelta(array $tx): float
|
|
||||||
{
|
|
||||||
return $this->parseNumber($tx['besGold'] ?? 0) - $this->parseNumber($tx['bedGold'] ?? 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $tx
|
|
||||||
*/
|
|
||||||
private function transactionRialDelta(array $tx): float
|
|
||||||
{
|
|
||||||
return $this->parseNumber($tx['besRial'] ?? 0) - $this->parseNumber($tx['bedRial'] ?? 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function isEchoOfRecentOutbound(int $customerId, float $goldDelta, float $rialDelta): bool
|
|
||||||
{
|
|
||||||
$since = now()->subDays(3);
|
|
||||||
|
|
||||||
$logs = PargarSyncLog::query()
|
|
||||||
->where('customer_id', $customerId)
|
|
||||||
->whereIn('type', [PargarSyncService::TYPE_WALLET_CASH, PargarSyncService::TYPE_WALLET_GOLD])
|
|
||||||
->where('created_at', '>=', $since)
|
|
||||||
->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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function hasPendingOutboundWalletSync(int $customerId): bool
|
private function hasPendingOutboundWalletSync(int $customerId): bool
|
||||||
{
|
{
|
||||||
return PargarSyncLog::query()
|
return PargarSyncLog::query()
|
||||||
@@ -422,11 +201,6 @@ class PargarInboundWalletService
|
|||||||
->exists();
|
->exists();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function normalizeNumberString(mixed $value): string
|
|
||||||
{
|
|
||||||
return (string) $this->parseNumber($value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function parseNumber(mixed $value): float
|
private function parseNumber(mixed $value): float
|
||||||
{
|
{
|
||||||
if ($value === null || $value === '') {
|
if ($value === null || $value === '') {
|
||||||
|
|||||||
Reference in New Issue
Block a user