From 06b3113b98da72026f1e80b1bc13cc0b91fc5fc3 Mon Sep 17 00:00:00 2001 From: adonis Date: Sun, 26 Jul 2026 14:15:26 +0330 Subject: [PATCH] fix bug of pargar --- .../PargarRequeueWalletDocumentsCommand.php | 111 ++++++++++++++++++ .../Controllers/Admin/GiftCardController.php | 15 +++ .../Controllers/Admin/ProductController.php | 7 ++ .../Controllers/Admin/WalletController.php | 56 ++++++++- .../Controllers/Customer/WalletController.php | 16 +++ .../Controllers/Finance/FinanceController.php | 15 +++ .../Single/SingleOrderController.php | 32 +++++ app/Models/Product.php | 22 ++++ app/Services/PargarInboundWalletService.php | 86 +++++++++++++- app/Services/PargarSyncService.php | 18 ++- ...roduct_details_and_product_description.php | 47 ++++++++ ...6_000002_widen_pargar_sync_logs_amount.php | 27 +++++ database/sql/deploy_settings_const.sql | 12 +- ...ngs_const_preserve_value_sync_can_edit.sql | 11 +- .../sql/pargar_sync_logs_amount_widen.sql | 20 ++++ database/sql/show_product_details_patch.sql | 35 ++++++ .../src/interfaces/TransactionInterface.php | 57 +++++++-- 17 files changed, 561 insertions(+), 26 deletions(-) create mode 100644 app/Console/Commands/PargarRequeueWalletDocumentsCommand.php create mode 100644 database/migrations/2026_07_26_000001_add_show_product_details_and_product_description.php create mode 100644 database/migrations/2026_07_26_000002_widen_pargar_sync_logs_amount.php create mode 100644 database/sql/pargar_sync_logs_amount_widen.sql create mode 100644 database/sql/show_product_details_patch.sql diff --git a/app/Console/Commands/PargarRequeueWalletDocumentsCommand.php b/app/Console/Commands/PargarRequeueWalletDocumentsCommand.php new file mode 100644 index 0000000..dd1e901 --- /dev/null +++ b/app/Console/Commands/PargarRequeueWalletDocumentsCommand.php @@ -0,0 +1,111 @@ +isSyncEnabled()) { + $this->error('همگام‌سازی پرگار فعال نیست.'); + + return self::FAILURE; + } + + $days = max(1, (int) $this->option('days')); + $execute = (bool) $this->option('execute'); + $customerId = (int) $this->option('customer'); + + $query = Transaction::query() + ->whereIn('type', [3, 4]) + ->whereNotNull('withdraw_id') + ->where('created_at', '>=', now()->subDays($days)) + ->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); + }) + ->orderBy('id'); + + if ($customerId > 0) { + $query->where('customer_id', $customerId); + } + + $rows = $query->get(['id', 'customer_id', 'withdraw_id', 'cash', 'note', 'created_at']); + if ($rows->isEmpty()) { + $this->info('سند جاافتاده‌ای پیدا نشد.'); + + return self::SUCCESS; + } + + $customers = Customer::whereIn('id', $rows->pluck('customer_id')->unique())->get()->keyBy('id'); + $table = []; + $queued = 0; + $skipped = 0; + + foreach ($rows as $row) { + $customer = $customers->get((int) $row->customer_id); + $status = 'آماده'; + + if ($customer === null || ! $customer->hasValidPargarAccountId()) { + $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++; + } + + $table[] = [ + $row->id, + $row->customer_id, + $customer->name ?? '—', + $row->withdraw_id, + number_format((float) $row->cash), + (string) $row->created_at, + $status, + ]; + } + + $this->table( + ['تراکنش', 'مشتری', 'نام', 'سند برداشت', 'مبلغ', 'تاریخ', 'وضعیت'], + $table + ); + + if (! $execute) { + $this->warn('پیش‌نمایش بود. برای ثبت واقعی --execute بزنید.'); + + return self::SUCCESS; + } + + $this->info("ثبت‌شده: {$queued} — ردشده: {$skipped}"); + + return self::SUCCESS; + } +} diff --git a/app/Http/Controllers/Admin/GiftCardController.php b/app/Http/Controllers/Admin/GiftCardController.php index d53fb14..8f670e2 100644 --- a/app/Http/Controllers/Admin/GiftCardController.php +++ b/app/Http/Controllers/Admin/GiftCardController.php @@ -142,6 +142,21 @@ class GiftCardController extends Controller 'withdraw_id' => $Withdraw->id, 'active' => true ], 3, false, true); + + try { + app(\App\Services\PargarSyncService::class)->queueWalletDocument( + $user, + 'cash', + (float) $giftcode->amount, + [ + 'withdraw_id' => $Withdraw->id, + 'note' => 'کارت هدیه', + 'source' => 'gift_card', + ] + ); + } catch (\Throwable $e) { + report($e); + } } //set text for sms to customer diff --git a/app/Http/Controllers/Admin/ProductController.php b/app/Http/Controllers/Admin/ProductController.php index f45bd8e..f0aae1c 100644 --- a/app/Http/Controllers/Admin/ProductController.php +++ b/app/Http/Controllers/Admin/ProductController.php @@ -666,6 +666,13 @@ class ProductController extends SuperController unset($data['inventory']); } + if (!Product::hasDescriptionColumn()) { + unset($data['description']); + } elseif (array_key_exists('description', $data)) { + $desc = is_string($data['description']) ? trim($data['description']) : ''; + $data['description'] = $desc === '' ? null : $desc; + } + if (!Product::hasOverrideTradingHoursColumn()) { unset($data['override_trading_hours']); } elseif (array_key_exists('override_trading_hours', $data)) { diff --git a/app/Http/Controllers/Admin/WalletController.php b/app/Http/Controllers/Admin/WalletController.php index c64dce3..0d1fbb4 100644 --- a/app/Http/Controllers/Admin/WalletController.php +++ b/app/Http/Controllers/Admin/WalletController.php @@ -33,6 +33,40 @@ use Hekmatinasser\Verta\Verta; class WalletController extends Controller { + /** موجودی طلا با سه رقم اعشار نمایش داده می‌شود؛ نصف پلهٔ نمایش = بیشترین اختلاف ممکن با مقدار واقعی */ + private const GOLD_DISPLAY_STEP = 0.0005; + + /** پنجرهٔ تشخیص سند تکراری — دوبار کلیک یا ارسال مجدد فرم */ + private const DUPLICATE_CHARGE_WINDOW_SECONDS = 60; + + /** + * جلوگیری از ثبت دوبارهٔ همان سند دستی. فقط روی کسر اعمال می‌شود تا شارژهای + * مثبتِ پشت‌سرهم (مثلاً دو واریز یکسان) بسته نشوند. + */ + private function assertNotDuplicateCharge(int $customerId, float $value, string $column): void + { + if ($value >= 0) { + return; + } + + $recent = Withdraw::query() + ->where('customer_id', $customerId) + ->where('sts', 4) + ->where('created_at', '>=', now()->subSeconds(self::DUPLICATE_CHARGE_WINDOW_SECONDS)); + + if ($column === 'cash') { + $recent->where('cash', (int) $value); + } else { + $recent->whereBetween('gold', [$value - 1e-6, $value + 1e-6]); + } + + if ($recent->exists()) { + throw new UnprocessableEntityHttpException( + 'همین سند لحظاتی پیش برای این مشتری ثبت شده است. اگر تکرارش عمدی است یک دقیقه صبر کنید.' + ); + } + } + public function charge2(Request $request) { $data = $request->all(); @@ -121,6 +155,7 @@ class WalletController extends Controller if (!is_null($wallet) && $wallet->cash < abs($data['value']) && !$deposit) { throw new UnprocessableEntityHttpException('مقدار موجودی کمتر از درخواست شماست'); } + $this->assertNotDuplicateCharge((int) $customer->id, (float) $data['value'], 'cash'); $orders = Order::where('customer_id', $data['customer_id'])->where('sts', 1)->get(); $gold_block = 0; @@ -213,12 +248,20 @@ class WalletController extends Controller } $wallet_interface = new WalletInterface($data['customer_id'], (int) $customer->type); $wallet = $wallet_interface->wallet; -// if (is_null($wallet) && !$deposit) { -// throw new UnprocessableEntityHttpException('مقدار موجودی کمتر از درخواست شماست'); -// } -// if (!is_null($wallet) && $wallet->cash < abs($data['value']) && !$deposit) { -// throw new UnprocessableEntityHttpException('مقدار موجودی کمتر از درخواست شماست'); -// } + // کسر دستی نباید موجودی طلا را منفی کند؛ شارژ مثبت بدون هیچ شرطی باز است + if (!$deposit) { + $available = is_null($wallet) ? 0.0 : (float) $wallet->gold; + $requested = abs((float) $data['value']); + // ادمین عدد را از نمایش سه‌رقمی می‌خواند؛ اختلاف تا نصف پلهٔ نمایش یعنی «کل موجودی» + if ($requested > $available && $requested - $available <= self::GOLD_DISPLAY_STEP) { + $requested = $available; + $data['value'] = -$available; + } + if ($requested <= 0 || $requested > $available) { + throw new UnprocessableEntityHttpException('مقدار موجودی طلا کمتر از درخواست شماست'); + } + } + $this->assertNotDuplicateCharge((int) $customer->id, (float) $data['value'], 'gold'); $gold_block = 0; $user = auth()->user(); $customer = Customer::findOrFail($data["customer_id"]); @@ -386,6 +429,7 @@ class WalletController extends Controller if (!$deposit && $wallet !== null && (float) $wallet->gold < abs((float) $data['value'])) { throw new UnprocessableEntityHttpException('مقدار موجودی کمتر از درخواست شماست'); } + $this->assertNotDuplicateCharge((int) $customer->id, (float) $data['value'], 'gold'); $goldBlock = 0; $user = auth()->user(); diff --git a/app/Http/Controllers/Customer/WalletController.php b/app/Http/Controllers/Customer/WalletController.php index a585c8c..a28d7b8 100644 --- a/app/Http/Controllers/Customer/WalletController.php +++ b/app/Http/Controllers/Customer/WalletController.php @@ -391,6 +391,22 @@ class WalletController extends Controller 'withdraw_id' => $Withdraw->id, 'active' => true ], 4, false, false); + + try { + app(\App\Services\PargarSyncService::class)->queueWalletDocument( + $customer, + 'cash', + -1 * (float) $data['value'], + [ + 'withdraw_id' => $Withdraw->id, + 'note' => 'برداشت ریالی', + 'source' => 'customer_withdraw', + ] + ); + } catch (\Throwable $e) { + report($e); + } + $userMobile = $customer->mobile; // User's mobile number $userName = $customer->name; // User's name $withdrawAmount = $data['value']; // amount diff --git a/app/Http/Controllers/Finance/FinanceController.php b/app/Http/Controllers/Finance/FinanceController.php index 40add5d..8c9b49d 100644 --- a/app/Http/Controllers/Finance/FinanceController.php +++ b/app/Http/Controllers/Finance/FinanceController.php @@ -195,6 +195,21 @@ class FinanceController extends Controller 'active' => true ], 3, false, true); + try { + app(\App\Services\PargarSyncService::class)->queueWalletDocument( + $User, + 'cash', + (float) ($decodedResponse['data']['amount'] * 10), + [ + 'withdraw_id' => (int) $_GET['clientrefid'], + 'note' => 'واریز درگاه', + 'source' => 'gateway', + ] + ); + } catch (\Throwable $e) { + report($e); + } + //----------sms // User information $userMobile = $Withdraw->customer->mobile; // User's mobile number diff --git a/app/Http/Controllers/Single/SingleOrderController.php b/app/Http/Controllers/Single/SingleOrderController.php index cd87d63..094955b 100644 --- a/app/Http/Controllers/Single/SingleOrderController.php +++ b/app/Http/Controllers/Single/SingleOrderController.php @@ -1589,6 +1589,22 @@ class SingleOrderController extends Controller 'withdraw_id' => $Withdraw->id, 'active' => true, ], 3, true, true, 0); + + try { + app(\App\Services\PargarSyncService::class)->queueWalletDocument( + $customer, + 'cash', + (float) $customer_bonus, + [ + 'withdraw_id' => $Withdraw->id, + 'note' => 'پاداش اولین سفارش', + 'source' => 'bonus_first_order', + ] + ); + } catch (\Throwable $e) { + report($e); + } + $message = 'کیف پول شما به اندازه ' . number_format($customer_bonus) . ' ریال در تاریخ ' . verta()->format('Y/n/j H:i:s') . " شارژ شد بابت\n" . 'پاداش اولین سفارش' . ' ' @@ -1626,6 +1642,22 @@ class SingleOrderController extends Controller 'withdraw_id' => $Withdraw->id, 'active' => true, ], 3, true, true, 0); + + try { + app(\App\Services\PargarSyncService::class)->queueWalletDocument( + $referrer, + 'cash', + (float) $referrer_bonus, + [ + 'withdraw_id' => $Withdraw->id, + 'note' => 'پاداش اولین سفارش زیرمجموعه', + 'source' => 'bonus_referrer', + ] + ); + } catch (\Throwable $e) { + report($e); + } + $message = 'کیف پول شما به اندازه ' . number_format($referrer_bonus) . ' ریال در تاریخ ' . verta()->format('Y/n/j H:i:s') . " شارژ شد بابت\n" . 'بابت پاداش اولین سفارش زیرمجموعه' . ' ' diff --git a/app/Models/Product.php b/app/Models/Product.php index f209c7c..db5c5b2 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -27,6 +27,7 @@ class Product extends Model private static ?bool $overrideTradingHoursColumnExists = null; private static ?bool $cashFeePercentModeColumnsExist = null; private static ?bool $physicalDeliveryColumnsExist = null; + private static ?bool $descriptionColumnExists = null; protected static function booted(): void { @@ -59,6 +60,9 @@ class Product extends Model if (!static::hasInventoryColumn()) { unset($product->attributes['inventory']); } + if (!static::hasDescriptionColumn()) { + unset($product->attributes['description']); + } if (!static::hasOverrideTradingHoursColumn()) { unset($product->attributes['override_trading_hours']); } @@ -293,6 +297,24 @@ class Product extends Model return static::$inventoryColumnExists; } + public static function hasDescriptionColumn(): bool + { + if (static::$descriptionColumnExists !== null) { + return static::$descriptionColumnExists; + } + + try { + static::$descriptionColumnExists = Schema::hasColumn( + (new static)->getTable(), + 'description' + ); + } catch (\Throwable) { + static::$descriptionColumnExists = false; + } + + return static::$descriptionColumnExists; + } + public function orderLimitsEnabled(): bool { if (!static::hasOrderLimitColumns() || !($this->order_limit_enabled ?? false)) { diff --git a/app/Services/PargarInboundWalletService.php b/app/Services/PargarInboundWalletService.php index d50bb94..747177c 100644 --- a/app/Services/PargarInboundWalletService.php +++ b/app/Services/PargarInboundWalletService.php @@ -6,9 +6,11 @@ use App\Models\Customer; use App\Models\PargarSyncLog; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Rooyesh\Wallet\interfaces\TransactionInterface; use Rooyesh\Wallet\interfaces\WalletInterface; +use Rooyesh\Wallet\models\Transaction; /** * همگام‌سازی کیف با پرگار. @@ -21,7 +23,9 @@ use Rooyesh\Wallet\interfaces\WalletInterface; * پیش از ثبت سند گرفته شده باشد، اعمال نمی‌شود و منتظر ماندهٔ بعدی می‌مانیم. * 4) اعمال فقط با دو snapshot پیاپیِ هم‌مقدار — تا مانده‌ای که وسط ثبت اسناد گرفته شده * موجودی مشتری را عقب نبرد. - * 5) دکمهٔ ادمین با force می‌تواند همهٔ گیت‌های بالا را رد کند (ابزار اصلاح دستی). + * 5) اگر واریز/برداشت ریالی‌ای اصلاً سند پرگار نداشته باشد، مانده اعمال نمی‌شود؛ + * پرگار از آن پول خبر ندارد و اعمال مانده موجودی مشتری را پاک می‌کند. + * 6) دکمهٔ ادمین با force می‌تواند همهٔ گیت‌های بالا را رد کند (ابزار اصلاح دستی). */ class PargarInboundWalletService { @@ -29,6 +33,9 @@ class PargarInboundWalletService public const KILL_SWITCH_CACHE_KEY = 'pargar_wallet_apply_disabled'; + /** واریز و برداشت ریالی — دو نوعی که باید سند ریالی پرگار داشته باشند */ + public const CASH_DOCUMENT_TX_TYPES = [3, 4]; + /** سقف تعداد دسته برای علامت‌زدن در هر کلیک */ private const MAX_BATCHES_PER_SYNC = 50; @@ -342,12 +349,16 @@ class PargarInboundWalletService $input['cash'] = abs($rialDiff); } + // مانده پرگار می‌تواند منفی باشد و کیف باید همان را نشان دهد؛ کف موجودی اینجا اعمال نمی‌شود new TransactionInterface( $walletInterface, $input, self::TX_TYPE_PARGAR, $goldDiff > 0, - $rialDiff > 0 + $rialDiff > 0, + 0, + [], + true ); $data['wallet_applied_from_mande'] = true; @@ -404,6 +415,16 @@ class PargarInboundWalletService ]; } + $missing = $this->unexportedCashMovement($customer, $lastConfirmedAt); + if ($missing !== null) { + return [ + 'reason' => 'unexported_wallet_movement', + 'message' => 'حرکت ریالی ' + . number_format($missing['amount']) + . ' ریال هنوز سندی در پرگار ندارد — تا ثبت سند، مانده اعمال نمی‌شود', + ]; + } + $previous = $this->previousComparableMande($log, $lastConfirmedAt); if ($previous === null) { return [ @@ -429,6 +450,67 @@ class PargarInboundWalletService return null; } + /** + * واریز/برداشت ریالی که سند پرگارش ساخته نشده است. + * + * تا وقتی پرگار از این حرکت خبر ندارد، ماندهٔ آن هم شامل این پول نیست؛ اعمال آن + * موجودی مشتری را پاک می‌کند. پس مانده را نگه می‌داریم تا سند ساخته و ثبت شود. + * + * @return array{amount: float, withdraw_ids: list}|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; + } + /** * ماندهٔ صفرِ هر دو فیلد برای مشتری‌ای که کیفش خالی نیست، تقریباً همیشه یعنی * پرگار حساب را پیدا نکرده — نه اینکه مشتری تسویه کرده باشد. diff --git a/app/Services/PargarSyncService.php b/app/Services/PargarSyncService.php index 01e9085..7ce0258 100644 --- a/app/Services/PargarSyncService.php +++ b/app/Services/PargarSyncService.php @@ -6,6 +6,7 @@ use App\Models\Customer; use App\Models\Order; use App\Models\PargarSyncLog; use App\Models\Product; +use Illuminate\Database\QueryException; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Schema; @@ -199,7 +200,7 @@ class PargarSyncService $desc = $isDeposit ? 'شارژ طلای کیف اصلی — ارسال به پرگار' : 'برداشت طلای کیف اصلی — ارسال به پرگار'; } - return PargarSyncLog::create([ + $attributes = [ 'customer_id' => $customer->id, 'amount' => abs($signedAmount), 'desc' => $desc, @@ -215,7 +216,20 @@ class PargarSyncService 'product_id' => $meta['product_id'] ?? null, 'source' => $meta['source'] ?? null, ], JSON_UNESCAPED_UNICODE), - ]); + ]; + + try { + 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); + } } /** diff --git a/database/migrations/2026_07_26_000001_add_show_product_details_and_product_description.php b/database/migrations/2026_07_26_000001_add_show_product_details_and_product_description.php new file mode 100644 index 0000000..8d3b3ff --- /dev/null +++ b/database/migrations/2026_07_26_000001_add_show_product_details_and_product_description.php @@ -0,0 +1,47 @@ +text('description')->nullable(); + }); + } + + if (! Schema::hasTable('settings')) { + return; + } + + if (! DB::table('settings')->where('key', 'show_product_details')->exists()) { + DB::table('settings')->insert([ + 'key' => 'show_product_details', + 'value' => 'true', + 'title_fa' => 'نمایش جزئیات اطلاعات محصول', + 'type' => 'switch', + 'can_edit' => 1, + 'is_active' => 1, + 'description' => 'روشن: بعد از انتخاب محصول در صفحه خرید/فروش، متن توضیح محصول نمایش داده می‌شود. خاموش: توضیح نشان داده نمی‌شود.', + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + } + + public function down(): void + { + if (Schema::hasTable('settings')) { + DB::table('settings')->where('key', 'show_product_details')->delete(); + } + // ستون description را حذف نمی‌کنیم — ممکن است داده داشته باشد + } +}; diff --git a/database/migrations/2026_07_26_000002_widen_pargar_sync_logs_amount.php b/database/migrations/2026_07_26_000002_widen_pargar_sync_logs_amount.php new file mode 100644 index 0000000..67e9369 --- /dev/null +++ b/database/migrations/2026_07_26_000002_widen_pargar_sync_logs_amount.php @@ -0,0 +1,27 @@ + '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; + +SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE +FROM information_schema.COLUMNS +WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'pargar_sync_logs' AND COLUMN_NAME = 'amount'; diff --git a/database/sql/show_product_details_patch.sql b/database/sql/show_product_details_patch.sql new file mode 100644 index 0000000..067ffa6 --- /dev/null +++ b/database/sql/show_product_details_patch.sql @@ -0,0 +1,35 @@ +-- سوییچ «نمایش جزئیات اطلاعات محصول» + ستون توضیح محصولات +-- idempotent: چند بار قابل اجرا +-- • اولین بار: INSERT با value='true' (پیش‌فرض روشن) +-- • اجرای مجدد: value و is_active دست نمی‌خورند — title_fa / description / type / can_edit همیشه override می‌شوند + +START TRANSACTION; + +SET @db = DATABASE(); + +SET @sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'products' AND COLUMN_NAME = 'description') = 0, + 'ALTER TABLE `products` ADD COLUMN `description` TEXT NULL', + 'SELECT ''skip products.description'' AS info' +); +PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'show_product_details', 'true', 'نمایش جزئیات اطلاعات محصول', 1, 1, + 'روشن: بعد از انتخاب محصول در صفحه خرید/فروش، متن توضیح محصول نمایش داده می‌شود. خاموش: توضیح نشان داده نمی‌شود.', + 'switch' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'show_product_details' LIMIT 1); + +UPDATE `settings` +SET `title_fa` = 'نمایش جزئیات اطلاعات محصول', + `description` = 'روشن: بعد از انتخاب محصول در صفحه خرید/فروش، متن توضیح محصول نمایش داده می‌شود. خاموش: توضیح نشان داده نمی‌شود.', + `type` = 'switch', + `can_edit` = 1 +WHERE `key` = 'show_product_details'; + +COMMIT; + +SELECT `key`, `title_fa`, LEFT(`description`, 60) AS desc_preview, `can_edit`, `type`, `value` +FROM `settings` +WHERE `key` = 'show_product_details'; diff --git a/packages/rooyesh/wallet/src/interfaces/TransactionInterface.php b/packages/rooyesh/wallet/src/interfaces/TransactionInterface.php index b8ae934..ba1d767 100644 --- a/packages/rooyesh/wallet/src/interfaces/TransactionInterface.php +++ b/packages/rooyesh/wallet/src/interfaces/TransactionInterface.php @@ -18,10 +18,10 @@ class TransactionInterface private $wallet_interface; private $transaction; - function __construct($wallet_interface = null, $_inp = null, $_type = null,$deposit_gold = false,$deposit_cash=false, $gold_block = 0,$meta = []) + function __construct($wallet_interface = null, $_inp = null, $_type = null,$deposit_gold = false,$deposit_cash=false, $gold_block = 0,$meta = [], $allow_negative = false) { - $this->transaction = TransactionInterface::apply_transaction($wallet_interface, $_inp, $_type,$deposit_gold ,$deposit_cash,$gold_block,$meta); + $this->transaction = TransactionInterface::apply_transaction($wallet_interface, $_inp, $_type,$deposit_gold ,$deposit_cash,$gold_block,$meta,$allow_negative); } static function validate_last_transaction($wallet_id) @@ -53,20 +53,53 @@ class TransactionInterface $transaction->save(); } - public static function apply_transaction($wallet_interface, $_inp, $_type,$deposit_gold,$deposit_cash,$gold_block, $meta = []) + /** + * کف موجودی طلا — فقط سمت کسر. شارژ مثبت هیچ‌وقت بسته نمی‌شود، حتی اگر مانده منفی باشد. + * ستون gold از نوع float است، پس رواداری نسبی می‌گذاریم تا «فروش کل موجودی» به خطای گرد کردن نخورد. + */ + private static function assert_gold_floor($wallet, $_inp, $deposit_gold) + { + if ($deposit_gold || !isset($_inp['gold'])) { + return; + } + + $requested = abs((float) $_inp['gold']); + if ($requested <= 0) { + return; + } + + $balance = (float) $wallet->gold; + $tolerance = 1e-6 + abs($balance) * 1e-6; + if ($balance + $tolerance < $requested) { + throw new UnprocessableEntityHttpException('مقدار وارد شده بیشتر از موجودی طلای شماست'); + } + } + + /** کف موجودی ریالی — فقط سمت کسر؛ واریز و شارژ مثبت همیشه باز است. */ + private static function assert_cash_floor($wallet, $_inp, $deposit_cash) + { + if ($deposit_cash || !isset($_inp['cash'])) { + return; + } + + $requested = abs((float) $_inp['cash']); + if ($requested > 0 && (float) $wallet->cash < $requested) { + throw new UnprocessableEntityHttpException('موجودی ریالی کافی نیست'); + } + } + + /** + * @param bool $allow_negative فقط برای تطبیق مانده با سیستم حسابداری؛ آنجا مانده می‌تواند + * واقعاً منفی باشد و کیف باید همان را بازتاب دهد. + */ + public static function apply_transaction($wallet_interface, $_inp, $_type,$deposit_gold,$deposit_cash,$gold_block, $meta = [], $allow_negative = false) { $wallet = $wallet_interface->wallet; TransactionInterface::validate_last_transaction($wallet->id); #-------------Check Balance -------- - if (!$deposit_gold) { -// if ($_type === 1 && $wallet->cash < $_inp['value']) { -// throw new Exceptions\NotEnoughCashBalance(); -// -// } - if ($_type === 2 && $wallet->gold < $_inp['gold']) { - throw new UnprocessableEntityHttpException('مقدار وارد شده کمتر از موجودی شماست'); - - } + if (!$allow_negative) { + TransactionInterface::assert_gold_floor($wallet, $_inp, $deposit_gold); + TransactionInterface::assert_cash_floor($wallet, $_inp, $deposit_cash); } // $_inp['value'] = abs($_inp['value']) * ($deposit ? 1 : -1); if (isset($_inp['gold'])){