fix pargar and login

This commit is contained in:
2026-08-01 14:04:01 +03:30
parent 6e75b9632b
commit 87f0e2beed
9 changed files with 248 additions and 38 deletions
@@ -73,13 +73,9 @@ class PargarRequeueWalletDocumentsCommand extends Command
$status = 'بدون کد پرگار — رد شد';
$skipped++;
} elseif ($execute) {
$log = $sync->queueWalletDocument($customer, 'cash', (float) $row->cash, [
'withdraw_id' => (int) $row->withdraw_id,
'note' => $row->note,
'source' => 'requeue_missing',
]);
$status = $log !== null ? "ثبت شد (لاگ {$log->id})" : 'ثبت نشد';
$log !== null ? $queued++ : $skipped++;
$queuedCount = $sync->queueMissingCashDocuments($customer, [$row], 'requeue_missing');
$status = $queuedCount > 0 ? 'ثبت/موجود شد' : 'ثبت نشد';
$queuedCount > 0 ? $queued++ : $skipped++;
}
$table[] = [
+6
View File
@@ -29,6 +29,12 @@ class Kernel extends ConsoleKernel
$schedule->command('prices:prune')
->dailyAt('00:00')
->name('prune_price_snapshots');
// ترمیم اسناد ریالی جاافتادهٔ پرگار (واریز/برداشت بدون لاگ) تا مانده قفل نماند
$schedule->command('pargar:requeue-wallet-documents --execute --days=120')
->hourly()
->name('pargar_requeue_wallet_docs')
->withoutOverlapping(50);
}
protected function commands()
+26 -11
View File
@@ -33,17 +33,29 @@ class MontzarService
public function montzarLogin(): string
{
$response = Http::timeout(10)
->acceptJson()
->post($this->endpoint('/server/auth/login'), [
'mobile' => $this->config()->Config('montzar_mobile'),
'password' => $this->config()->Config('montzar_password'),
'provider' => 'customer',
'type_app' => 'client',
])
->json();
try {
$response = Http::timeout(10)
->acceptJson()
->post($this->endpoint('/server/auth/login'), [
'mobile' => $this->config()->Config('montzar_mobile'),
'password' => $this->config()->Config('montzar_password'),
'provider' => 'customer',
'type_app' => 'client',
])
->json();
} catch (\Throwable $e) {
Log::error('Montzar login request failed', [
'base_url' => $this->baseUrl(),
'error' => $e->getMessage(),
]);
throw new UnprocessableEntityHttpException(
'سرویس موقتا در دسترس نیست، لطفاً با پشتیبانی تماس بگیرید.'
);
}
$token = $response['access_token'] ?? $response['token'] ?? null;
$token = is_array($response)
? ($response['access_token'] ?? $response['token'] ?? null)
: null;
if (!$token) {
Log::error('Montzar login failed', [
@@ -51,7 +63,10 @@ class MontzarService
'mobile' => $this->config()->Config('montzar_mobile'),
'response' => $response,
]);
throw new \Exception('Montzar login failed: ' . json_encode($response, JSON_UNESCAPED_UNICODE));
// 422 تا فرانت toast نشان دهد؛ جزئیات فنی فقط در لاگ می‌ماند
throw new UnprocessableEntityHttpException(
'سرویس موقتا در دسترس نیست، لطفاً با پشتیبانی تماس بگیرید.'
);
}
Setting::updateOrCreate(
+23 -4
View File
@@ -25,7 +25,8 @@ use Rooyesh\Wallet\models\Transaction;
* 4) ماندهٔ تازهٔ پرگار (از جمله صفر/تغییر دستی حسابدار) بلافاصله اعمال می‌شود —
* دیگر منتظر دو snapshot یکسان یا بلوک صفر نمی‌مانیم.
* 5) اگر واریز/برداشت ریالی‌ای اصلاً سند پرگار نداشته باشد، مانده اعمال نمی‌شود؛
* پرگار از آن پول خبر ندارد و اعمال مانده موجودی مشتری را پاک می‌کند.
* همزمان سند جاافتاده خودکار صف می‌شود. پرگار از آن پول خبر ندارد و اعمال مانده
* موجودی مشتری را پاک می‌کند.
* 6) دکمهٔ ادمین با force می‌تواند گیت‌های سند pending را رد کند (ابزار اصلاح دستی).
*/
class PargarInboundWalletService
@@ -502,11 +503,28 @@ class PargarInboundWalletService
$missing = $this->unexportedCashMovement($customer, $lastConfirmedAt);
if ($missing !== null) {
// ترمیم خودکار: سند جاافتاده را صف کن تا pending بماند و بعد از ثبت در پرگار مانده باز شود
$queued = 0;
try {
$queued = app(PargarSyncService::class)->queueMissingCashDocuments(
$customer,
$missing['rows'],
'mande_auto_missing'
);
} catch (\Throwable $e) {
report($e);
}
$suffix = $queued > 0
? ' — سند در صف ثبت قرار گرفت؛ تا تأیید پرگار، مانده اعمال نمی‌شود'
: ' — تا ثبت سند، مانده اعمال نمی‌شود';
return [
'reason' => 'unexported_wallet_movement',
'message' => 'حرکت ریالی '
. number_format($missing['amount'])
. ' ریال هنوز سندی در پرگار ندارد — تا ثبت سند، مانده اعمال نمی‌شود',
. ' ریال هنوز سندی در پرگار ندارد'
. $suffix,
];
}
@@ -529,7 +547,7 @@ class PargarInboundWalletService
* تا وقتی پرگار از این حرکت خبر ندارد، ماندهٔ آن هم شامل این پول نیست؛ اعمال آن
* موجودی مشتری را پاک می‌کند. پس مانده را نگه می‌داریم تا سند ساخته و ثبت شود.
*
* @return array{amount: float, withdraw_ids: list<int>}|null
* @return array{amount: float, withdraw_ids: list<int>, rows: \Illuminate\Support\Collection}|null
*/
private function unexportedCashMovement(Customer $customer, ?Carbon $lastConfirmedAt): ?array
{
@@ -552,7 +570,7 @@ class PargarInboundWalletService
->whereColumn('pargar_sync_logs.withdraw_id', 'transactions.withdraw_id')
->where('pargar_sync_logs.type', PargarSyncService::TYPE_WALLET_CASH);
})
->get(['withdraw_id', 'cash']);
->get(['withdraw_id', 'cash', 'note']);
if ($rows->isEmpty()) {
return null;
@@ -561,6 +579,7 @@ class PargarInboundWalletService
return [
'amount' => (float) $rows->sum(fn ($row) => abs((float) $row->cash)),
'withdraw_ids' => $rows->pluck('withdraw_id')->map(fn ($id) => (int) $id)->all(),
'rows' => $rows,
];
}
+143 -4
View File
@@ -151,9 +151,18 @@ class PargarSyncService
];
}
/**
* سقف تاریخی ستون amount وقتی هنوز DOUBLE(8,2) است (~۹۹۹٬۹۹۹٫۹۹).
* مبلغ واقعی همیشه در data.signed_amount نگه داشته می‌شود.
*/
private const AMOUNT_COLUMN_SAFE_MAX = 999999.99;
/**
* صف سند کیف برای سینک مانده پرگار با اپ.
*
* سند pending باید حتماً ساخته شود؛ اگر ستون amount مبلغ بزرگ را نپذیرد،
* بدون amount ثبت می‌شود تا حرکت کیف بی‌سندی نماند و مانده قفل نشود.
*
* @param 'cash'|'gold' $kind
* @param array{withdraw_id?:int|null,note?:string|null,product_id?:int|null,source?:string|null} $meta
*/
@@ -201,9 +210,14 @@ class PargarSyncService
$desc = $isDeposit ? 'شارژ طلای کیف اصلی — ارسال به پرگار' : 'برداشت طلای کیف اصلی — ارسال به پرگار';
}
$absAmount = abs($signedAmount);
// اگر هنوز مایگریشن widen اجرا نشده، مبلغ بزرگ را از همان اول در ستون نگذار
// تا insert نترکد؛ خروجی/مانده از signed_amount می‌خوانند.
$columnAmount = $absAmount <= self::AMOUNT_COLUMN_SAFE_MAX ? $absAmount : null;
$attributes = [
'customer_id' => $customer->id,
'amount' => abs($signedAmount),
'amount' => $columnAmount,
'desc' => $desc,
'withdraw_id' => $withdrawId,
'date' => now(),
@@ -219,20 +233,145 @@ class PargarSyncService
], JSON_UNESCAPED_UNICODE),
];
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
{
try {
// اگر ستون 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
);
}
}
return PargarSyncLog::create($attributes);
} catch (QueryException $e) {
report($e);
// مبلغ واقعی در data.signed_amount است و خروجی از همان می‌خواند؛
// پس اگر ستون amount مبلغ را نپذیرفت، سند را بدون آن ثبت می‌کنیم تا گم نشود.
$attributes['amount'] = null;
$attributes['sync_log'] = mb_substr('amount ثبت نشد: ' . $e->getMessage(), 0, 500);
return PargarSyncLog::create($attributes);
try {
return PargarSyncLog::create($attributes);
} catch (\Throwable $e2) {
report($e2);
return $this->insertWalletSyncLogRaw($attributes, $e2);
}
}
}
/**
* آخرین پناه: 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);
if ($withdrawId <= 0 || abs($cash) < 1) {
continue;
}
$log = $this->queueWalletDocument($customer, 'cash', $cash, [
'withdraw_id' => $withdrawId,
'note' => $row->note ?? null,
'source' => $source,
]);
if ($log !== null) {
$queued++;
}
}
return $queued;
}
/**
* @param array{
* payment_kind?: string,
@@ -24,7 +24,8 @@ return new class extends Migration
Schema::create('pargar_sync_logs', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('customer_id')->nullable()->index();
$table->float('amount')->nullable();
// decimal نه float — float() لاراول ۸ می‌شود DOUBLE(8,2) و مبالغ بزرگ گم می‌شوند
$table->decimal('amount', 20, 2)->nullable();
$table->string('desc')->nullable();
$table->unsignedBigInteger('order_id')->nullable()->index();
$table->unsignedBigInteger('withdraw_id')->nullable()->index();
@@ -8,6 +8,9 @@ use Illuminate\Support\Facades\Schema;
* ستون amount با float() ساخته شده بود و لاراول ۸ آن را DOUBLE(8,2) می‌سازد؛
* سقفش ۹۹۹٬۹۹۹٫۹۹ است. هر سند ریالی بزرگ‌تر از یک میلیون در insert خطا می‌داد و
* چون فراخوانی‌ها داخل try/catch بودند، سند بی‌صدا گم می‌شد و پرگار واریز را نمی‌دید.
*
* DECIMAL(20,2) برای مبالغ ریالی بزرگ (میلیاردها) کافی و امن است.
* idempotent: اگر از قبل DECIMAL/DOUBLE بدون سقف باشد هم آسیب نمی‌زند.
*/
return new class extends Migration
{
@@ -17,7 +20,7 @@ return new class extends Migration
return;
}
DB::statement('ALTER TABLE `pargar_sync_logs` MODIFY `amount` DOUBLE NULL');
DB::statement('ALTER TABLE `pargar_sync_logs` MODIFY `amount` DECIMAL(20, 2) NULL');
}
public function down(): void
@@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
/**
* اگر هنوز DOUBLE(8,2) یا DOUBLE بدون مقیاس پول‌پسند باشد، به DECIMAL(20,2) ببر.
* مبالغ میلیاردی ریال باید در ستون amount هم قابل ذخیره باشند.
*/
return new class extends Migration
{
public function up(): void
{
if (! Schema::hasTable('pargar_sync_logs') || ! Schema::hasColumn('pargar_sync_logs', 'amount')) {
return;
}
$type = DB::selectOne(
"SELECT COLUMN_TYPE AS column_type
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'pargar_sync_logs'
AND COLUMN_NAME = 'amount'"
);
$columnType = strtolower((string) ($type->column_type ?? ''));
if ($columnType === 'decimal(20,2)') {
return;
}
DB::statement('ALTER TABLE `pargar_sync_logs` MODIFY `amount` DECIMAL(20, 2) NULL');
}
public function down(): void
{
// برگشت عمداً انجام نمی‌شود.
}
};
+2 -10
View File
@@ -2,18 +2,10 @@
-- ستون pargar_sync_logs.amount با float() ساخته شده بود = DOUBLE(8,2) با سقف ۹۹۹٬۹۹۹٫۹۹
-- هر سند واریز/برداشت ریالی بزرگ‌تر از این سقف در insert خطا می‌داد و سند اصلاً ساخته نمی‌شد،
-- پس پرگار واریز مشتری را نمی‌دید و در تطبیق مانده، موجودی ریالی کیف صفر می‌شد.
-- DECIMAL(20,2) برای مبالغ میلیاردی کافی است.
-- idempotent: چند بار قابل اجرا
SET @db = DATABASE();
SET @sql = IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'pargar_sync_logs'
AND COLUMN_NAME = 'amount' AND UPPER(COLUMN_TYPE) <> 'DOUBLE') = 1,
'ALTER TABLE `pargar_sync_logs` MODIFY `amount` DOUBLE NULL',
'SELECT ''skip pargar_sync_logs.amount'' AS info'
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
ALTER TABLE `pargar_sync_logs` MODIFY `amount` DECIMAL(20, 2) NULL;
SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE
FROM information_schema.COLUMNS