fix pargar and login
This commit is contained in:
@@ -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[] = [
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
Reference in New Issue
Block a user