fix some bugs 3
This commit is contained in:
@@ -274,20 +274,22 @@ class PargarController extends Controller
|
||||
|
||||
$force = $request->boolean('force');
|
||||
$result = app(PargarInboundWalletService::class)->syncWalletFromPargar($customer, $force);
|
||||
$appliedDocs = (int) ($result['documents']['applied'] ?? 0);
|
||||
$acknowledged = (int) ($result['documents']['acknowledged'] ?? 0);
|
||||
$skippedDocs = (int) ($result['documents']['skipped'] ?? 0);
|
||||
$batches = (int) ($result['documents']['batches'] ?? 0);
|
||||
$appliedMande = (bool) ($result['mande']['applied'] ?? false);
|
||||
$mandeMessage = (string) ($result['mande']['message'] ?? '');
|
||||
$ok = $appliedDocs > 0 || $appliedMande;
|
||||
$docErrors = $result['documents']['errors'] ?? [];
|
||||
$ok = $appliedMande || $acknowledged > 0;
|
||||
|
||||
if ($appliedDocs > 0 && $appliedMande) {
|
||||
$message = "{$appliedDocs} سند اعمال شد و مانده همتراز شد";
|
||||
} elseif ($appliedDocs > 0) {
|
||||
$message = "{$appliedDocs} سند اعمال شد"
|
||||
.($mandeMessage !== '' ? " — {$mandeMessage}" : '');
|
||||
if ($appliedMande && $acknowledged > 0) {
|
||||
$message = "مانده همتراز شد ({$acknowledged} دسته سند مصرف شد)";
|
||||
} elseif ($appliedMande) {
|
||||
$message = 'مانده با پرگار همتراز شد';
|
||||
} elseif ($acknowledged > 0) {
|
||||
$message = $mandeMessage !== ''
|
||||
? "{$acknowledged} دسته سند مصرف شد — {$mandeMessage}"
|
||||
: "{$acknowledged} دسته سند مصرف شد";
|
||||
} elseif ($batches > 0 && $skippedDocs > 0) {
|
||||
$message = $mandeMessage !== ''
|
||||
? $mandeMessage
|
||||
@@ -298,6 +300,10 @@ class PargarController extends Controller
|
||||
: 'سند یا ماندهٔ جدیدی برای اعمال نبود';
|
||||
}
|
||||
|
||||
if (is_array($docErrors) && $docErrors !== []) {
|
||||
$message .= ' — '.implode('؛ ', array_slice($docErrors, 0, 2));
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'ok' => $ok,
|
||||
'message' => $message,
|
||||
|
||||
@@ -14,9 +14,9 @@ use Rooyesh\Wallet\interfaces\WalletInterface;
|
||||
*
|
||||
* قواعد سخت:
|
||||
* 1) webhook اسناد (/documents) هرگز کیف را عوض نمیکند — فقط لاگ.
|
||||
* 2) دکمه: اول اسنادِ اعمالنشدهٔ همان مشتری، بعد مانده.
|
||||
* 3) هر سطر سند حداکثر یکبار (اثرانگشت).
|
||||
* 4) هر لاگ دسته حداکثر یکبار پردازش میشود (wallet_batch_applied).
|
||||
* 2) دکمه: دستههای سند را فقط «مصرفشده» علامت میزند (بدون دلتا روی کیف)،
|
||||
* سپس مانده (mande) را بهعنوان منبع حقیقت روی کیف مینشاند.
|
||||
* 3) اعمال تکتک سطرهای تاریخچهٔ پرگار ممنوع است — باعث تورم/تایماوت میشود.
|
||||
*/
|
||||
class PargarInboundWalletService
|
||||
{
|
||||
@@ -24,11 +24,8 @@ class PargarInboundWalletService
|
||||
|
||||
public const KILL_SWITCH_CACHE_KEY = 'pargar_wallet_apply_disabled';
|
||||
|
||||
/** سقف تعداد دستهٔ سند در هر کلیک (جلوگیری از timeout) */
|
||||
private const MAX_BATCHES_PER_SYNC = 30;
|
||||
|
||||
/** @var array<string, true> */
|
||||
private array $seenFingerprints = [];
|
||||
/** سقف تعداد دسته برای علامتزدن در هر کلیک */
|
||||
private const MAX_BATCHES_PER_SYNC = 50;
|
||||
|
||||
public function __construct(
|
||||
private readonly WalletBroadcastService $walletBroadcast
|
||||
@@ -47,11 +44,11 @@ class PargarInboundWalletService
|
||||
|
||||
/**
|
||||
* دکمهٔ ادمین:
|
||||
* ۱) اسناد لاگشدهٔ اعمالنشده برای همین مشتری
|
||||
* ۲) سپس همترازی مانده (mande)
|
||||
* ۱) علامت مصرف دستههای سند (بدون نوشتن روی کیف)
|
||||
* ۲) همترازی مانده با آخرین لاگ mande
|
||||
*
|
||||
* @return array{
|
||||
* documents: array{applied: int, skipped: int, batches: int, errors: list<string>},
|
||||
* documents: array{applied: int, skipped: int, batches: int, acknowledged: int, errors: list<string>},
|
||||
* mande: array{applied: bool, gold_delta?: float, rial_delta?: float, message: string}
|
||||
* }
|
||||
*/
|
||||
@@ -61,7 +58,13 @@ class PargarInboundWalletService
|
||||
|
||||
if (self::isApplyFrozen()) {
|
||||
return [
|
||||
'documents' => ['applied' => 0, 'skipped' => 0, 'batches' => 0, 'errors' => []],
|
||||
'documents' => [
|
||||
'applied' => 0,
|
||||
'skipped' => 0,
|
||||
'batches' => 0,
|
||||
'acknowledged' => 0,
|
||||
'errors' => [],
|
||||
],
|
||||
'mande' => [
|
||||
'applied' => false,
|
||||
'message' => 'اعمال کیف یخزده است — php artisan pargar:wallet-freeze off',
|
||||
@@ -69,23 +72,28 @@ class PargarInboundWalletService
|
||||
];
|
||||
}
|
||||
|
||||
$lock = Cache::lock('pargar-wallet-sync:'.$customer->id, 120);
|
||||
$lock = Cache::lock('pargar-wallet-sync:'.$customer->id, 60);
|
||||
if (! $lock->get()) {
|
||||
return [
|
||||
'documents' => ['applied' => 0, 'skipped' => 0, 'batches' => 0, 'errors' => ['همگامسازی در حال اجراست']],
|
||||
'documents' => [
|
||||
'applied' => 0,
|
||||
'skipped' => 0,
|
||||
'batches' => 0,
|
||||
'acknowledged' => 0,
|
||||
'errors' => ['همگامسازی در حال اجراست'],
|
||||
],
|
||||
'mande' => ['applied' => false, 'message' => 'همگامسازی دیگری برای این مشتری در حال اجراست'],
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
// ۱) اسناد لاگشده که هنوز روی کیف نرفتهاند
|
||||
$documents = $this->applyPendingDocumentBatches($customer);
|
||||
// ۱) تاریخچهٔ اسناد را روی کیف replay نکن — فقط مصرفشده علامت بزن
|
||||
$documents = $this->acknowledgePendingDocumentBatches($customer);
|
||||
|
||||
// ۲) همیشه مانده را با آخرین لاگ mande همتراز کن (حتی اگر سندی نبود)
|
||||
// force تا بعد از اعمال اسناد، موجودی متورم به ماندهٔ واقعی پرگار برگردد
|
||||
// ۲) منبع حقیقت موجودی = ماندهٔ پرگار
|
||||
$mande = $this->applyLatestMandeDifference($customer, force: true);
|
||||
|
||||
if ($documents['applied'] > 0 || ($mande['applied'] ?? false)) {
|
||||
if (($documents['acknowledged'] ?? 0) > 0 || ($mande['applied'] ?? false)) {
|
||||
$this->walletBroadcast->broadcastForCustomer(
|
||||
(int) $customer->id,
|
||||
(int) ($customer->type ?? 2)
|
||||
@@ -102,16 +110,18 @@ class PargarInboundWalletService
|
||||
}
|
||||
|
||||
/**
|
||||
* فقط لاگهای دستهٔ ورودی که هنوز روی کیف اعمال نشدهاند.
|
||||
* دستههای ورودی پرگار را بدون تغییر کیف، مصرفشده علامت میزند.
|
||||
* (اعمال تکتک bed/bes تاریخچه = تورم کیف و 504)
|
||||
*
|
||||
* @return array{applied: int, skipped: int, batches: int, errors: list<string>}
|
||||
* @return array{applied: int, skipped: int, batches: int, acknowledged: int, errors: list<string>}
|
||||
*/
|
||||
public function applyPendingDocumentBatches(Customer $customer): array
|
||||
public function acknowledgePendingDocumentBatches(Customer $customer): array
|
||||
{
|
||||
$result = [
|
||||
'applied' => 0,
|
||||
'skipped' => 0,
|
||||
'batches' => 0,
|
||||
'acknowledged' => 0,
|
||||
'errors' => [],
|
||||
];
|
||||
|
||||
@@ -132,7 +142,6 @@ class PargarInboundWalletService
|
||||
continue;
|
||||
}
|
||||
|
||||
// فقط دستههای ورودی پرگار (آرایه transactions) — نه رکورد تکسند اعمالشده
|
||||
if (empty($data['transactions']) || ! is_array($data['transactions'])) {
|
||||
continue;
|
||||
}
|
||||
@@ -142,30 +151,17 @@ class PargarInboundWalletService
|
||||
}
|
||||
|
||||
$result['batches']++;
|
||||
$accountId = trim((string) ($data['accountId'] ?? $customer->pargar_account_id ?? ''));
|
||||
$txCount = count($data['transactions']);
|
||||
$result['skipped'] += $txCount;
|
||||
|
||||
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 شدند، دسته را مصرفشده علامت بزن تا دوباره نیاید
|
||||
// خطای ذخیره نباید کل sync را بشکند (mande باید بعداً اجرا شود)
|
||||
try {
|
||||
$data['wallet_batch_ack_only'] = true;
|
||||
$data['wallet_batch_ack_reason'] = 'mande_is_source_of_truth';
|
||||
$this->markDocumentBatchApplied($batch, $data);
|
||||
$result['acknowledged']++;
|
||||
} catch (\Throwable $e) {
|
||||
$result['errors'][] = 'علامت دسته #'.$batch->id.' ناموفق: '.$e->getMessage();
|
||||
Log::error('Pargar markDocumentBatchApplied failed', [
|
||||
Log::error('Pargar acknowledgeDocumentBatch failed', [
|
||||
'log_id' => $batch->id,
|
||||
'customer_id' => $customer->id,
|
||||
'error' => $e->getMessage(),
|
||||
@@ -177,7 +173,24 @@ class PargarInboundWalletService
|
||||
}
|
||||
|
||||
/**
|
||||
* علامت مصرف دسته — بدون Pretty-Print؛ در صورت بزرگ بودن payload فشردهسازی میکند.
|
||||
* @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 فشرده مینویسد.
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
@@ -190,6 +203,14 @@ class PargarInboundWalletService
|
||||
$batch->desc = mb_substr('[اعمالشده] '.$batch->desc, 0, 250);
|
||||
}
|
||||
|
||||
// ack-only: همیشه stub فشرده — نیازی به نگهداشتن صدها سطر تاریخچه نیست
|
||||
if (! empty($data['wallet_batch_ack_only'])) {
|
||||
$batch->data = $this->encodeAppliedBatchCompact($data);
|
||||
$batch->save();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$encoded = json_encode($data, JSON_UNESCAPED_UNICODE);
|
||||
if ($encoded === false) {
|
||||
throw new \RuntimeException('json_encode failed for pargar batch #'.$batch->id);
|
||||
@@ -200,7 +221,6 @@ class PargarInboundWalletService
|
||||
try {
|
||||
$batch->save();
|
||||
} catch (\Throwable $e) {
|
||||
// ستون هنوز TEXT باشد یا payload خیلی بزرگ — stub کوچک تا دسته دوباره نیاید
|
||||
$batch->data = $this->encodeAppliedBatchCompact($data);
|
||||
$batch->save();
|
||||
}
|
||||
@@ -220,61 +240,18 @@ class PargarInboundWalletService
|
||||
'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(),
|
||||
'wallet_batch_ack_only' => true,
|
||||
'payload_compacted' => true,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{applied: bool, gold_delta?: float, rial_delta?: float, message: string}
|
||||
*/
|
||||
public function applyLatestMandeDifference(Customer $customer, bool $force = false): array
|
||||
{
|
||||
if ($this->hasPendingOutboundWalletSync((int) $customer->id)) {
|
||||
// دکمهٔ ادمین (force): حتی با سند pending خروجی، مانده را همتراز کن تا کیف متورم اصلاح شود
|
||||
if (! $force && $this->hasPendingOutboundWalletSync((int) $customer->id)) {
|
||||
return [
|
||||
'applied' => false,
|
||||
'message' => 'سند ارسالی از پنل هنوز تأیید نشده — مانده اعمال نشد تا شارژ برنگردد',
|
||||
@@ -351,142 +328,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));
|
||||
}
|
||||
|
||||
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),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
private function hasPendingOutboundWalletSync(int $customerId): bool
|
||||
{
|
||||
return PargarSyncLog::query()
|
||||
@@ -496,11 +337,6 @@ class PargarInboundWalletService
|
||||
->exists();
|
||||
}
|
||||
|
||||
private function normalizeNumberString(mixed $value): string
|
||||
{
|
||||
return (string) $this->parseNumber($value);
|
||||
}
|
||||
|
||||
private function parseNumber(mixed $value): float
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
|
||||
Reference in New Issue
Block a user