From 277cf31d93f1ecd8974cdfccdd576ac663d5f71c Mon Sep 17 00:00:00 2001 From: adonis Date: Mon, 13 Jul 2026 19:03:30 +0330 Subject: [PATCH] fix bug 22/4 2 --- .../Controllers/Admin/ProductController.php | 37 +++++++++++-- .../Controllers/Customer/WalletController.php | 26 ++++----- .../Single/SingleOrderController.php | 20 ++++++- app/Models/Product.php | 41 +++++++++++--- app/Services/ProductDisplayPriceService.php | 21 ++++++++ ...dd_physical_delivery_to_products_table.php | 11 +++- ...in_physical_delivery_grams_to_quantity.php | 54 +++++++++++++++++++ ...000003_add_app_price_to_products_table.php | 36 +++++++++++++ 8 files changed, 217 insertions(+), 29 deletions(-) create mode 100644 database/migrations/2026_07_13_000002_rename_min_physical_delivery_grams_to_quantity.php create mode 100644 database/migrations/2026_07_13_000003_add_app_price_to_products_table.php diff --git a/app/Http/Controllers/Admin/ProductController.php b/app/Http/Controllers/Admin/ProductController.php index 8a6b641..d98ffc2 100644 --- a/app/Http/Controllers/Admin/ProductController.php +++ b/app/Http/Controllers/Admin/ProductController.php @@ -671,6 +671,22 @@ class ProductController extends SuperController unset($data['direct_price']); } + if (!Product::hasAppPriceColumn()) { + unset($data['app_price']); + } elseif (array_key_exists('app_price', $data)) { + $data['app_price'] = filter_var($data['app_price'], FILTER_VALIDATE_BOOLEAN); + + // قیمت اپ: مرجع خارجی ندارد، متغیر است (هر گرم × وزن) و قیمت دستی نگه داشته نمی‌شود + if ($data['app_price']) { + $data['reading_method'] = null; + if (Product::hasDirectPriceColumn()) { + $data['direct_price'] = 0; + } + $data['price'] = 0; + $data['price_profit'] = 0; + } + } + if (!Product::hasInventoryColumn()) { unset($data['inventory']); } @@ -682,24 +698,35 @@ class ProductController extends SuperController } if (!Product::hasPhysicalDeliveryColumns()) { - unset($data['has_physical_delivery'], $data['min_physical_delivery_grams']); + unset( + $data['has_physical_delivery'], + $data['min_physical_delivery_quantity'], + $data['min_physical_delivery_grams'] + ); } else { + // سازگاری با نسخهٔ قبلی که فیلد را min_physical_delivery_grams می‌فرستاد + if (!array_key_exists('min_physical_delivery_quantity', $data) + && array_key_exists('min_physical_delivery_grams', $data)) { + $data['min_physical_delivery_quantity'] = $data['min_physical_delivery_grams']; + } + unset($data['min_physical_delivery_grams']); + $hasDelivery = array_key_exists('has_physical_delivery', $data) ? filter_var($data['has_physical_delivery'], FILTER_VALIDATE_BOOLEAN) : false; $data['has_physical_delivery'] = $hasDelivery; if (!$hasDelivery) { - $data['min_physical_delivery_grams'] = null; + $data['min_physical_delivery_quantity'] = null; } else { - $rawMin = $data['min_physical_delivery_grams'] ?? null; + $rawMin = $data['min_physical_delivery_quantity'] ?? null; $min = $this->normalizeCashFeeValue($rawMin); if ($min <= 0) { throw new UnprocessableEntityHttpException( - 'برای فعال کردن تحویل فیزیکی باید حداقل گرم معتبر وارد شود' + 'برای فعال کردن تحویل فیزیکی باید حداقل مقدار معتبر وارد شود' ); } - $data['min_physical_delivery_grams'] = $min; + $data['min_physical_delivery_quantity'] = $min; } } diff --git a/app/Http/Controllers/Customer/WalletController.php b/app/Http/Controllers/Customer/WalletController.php index 0f25384..730244b 100644 --- a/app/Http/Controllers/Customer/WalletController.php +++ b/app/Http/Controllers/Customer/WalletController.php @@ -417,7 +417,7 @@ class WalletController extends Controller return response()->json($Withdraw); } - private function formatGramsNumber(float $value): string + private function formatDeliveryQuantity(float $value): string { $rounded = round($value, 3); if (fmod($rounded, 1.0) === 0.0) { @@ -466,7 +466,7 @@ class WalletController extends Controller $availableGold = $assetSummaryService ->productDisplayQuantity($customerId, $ownerType, $productId); - // گیت تحویل فیزیکی per-product — سوییچ محصول و حداقل گرم + // گیت تحویل فیزیکی per-product — سوییچ محصول و حداقل مقدار (به واحد خود محصول) $walletProduct = $data['wallets'][$key]['wallet']['product']; if (Product::hasPhysicalDeliveryColumns() && !$walletProduct->physicalDeliveryEnabled()) { return response()->json([ @@ -474,20 +474,22 @@ class WalletController extends Controller ], 422); } - $minDeliveryGrams = Product::hasPhysicalDeliveryColumns() - ? $walletProduct->minPhysicalDeliveryGrams() + $minDeliveryQuantity = Product::hasPhysicalDeliveryColumns() + ? $walletProduct->minPhysicalDeliveryQuantity() : 0.0; - if ($minDeliveryGrams > 0) { + if ($minDeliveryQuantity > 0) { + // موجودی و حداقل هر دو با واحد خود محصول سنجیده می‌شوند: + // تعدادی → عدد، وزنی → گرم/کیلوگرم (همسان با inventory_cards) $card = $assetSummaryService->productInventoryCard($customerId, $ownerType, $productId); - $isCount = ($card['display_type'] ?? null) === 'count'; - $unitWeight = (float) ($walletProduct['amount'] ?? 0); - $availableGrams = $isCount ? $availableGold * $unitWeight : $availableGold; + $unitLabel = $card['unit'] ?? Controller::productQuantityUnit( + Controller::productUnitPayload($walletProduct) + ); - if ($availableGrams + 0.0001 < $minDeliveryGrams) { - $remaining = max(0, $minDeliveryGrams - $availableGrams); + if ($availableGold + 0.0001 < $minDeliveryQuantity) { + $remaining = max(0, $minDeliveryQuantity - $availableGold); return response()->json([ - 'message' => $this->formatGramsNumber($remaining) - . " گرم مانده تا رسیدن به حداقل تحویل فیزیکی " + 'message' => $this->formatDeliveryQuantity($remaining) + . " {$unitLabel} مانده تا رسیدن به حداقل تحویل فیزیکی " . "{$walletProduct['title']}" ], 422); } diff --git a/app/Http/Controllers/Single/SingleOrderController.php b/app/Http/Controllers/Single/SingleOrderController.php index 6a6a06e..bbd16dc 100644 --- a/app/Http/Controllers/Single/SingleOrderController.php +++ b/app/Http/Controllers/Single/SingleOrderController.php @@ -116,7 +116,7 @@ class SingleOrderController extends Controller $data['service_charge_percentage'] = isset($data['fee_free']) && $data['fee_free'] === true ? 0 : ($data['product']['percent_fee_buy']) * $data['number']; $data['difference'] = $data['fee'] = $this->resolveCashPriceDiff($config, $data['product'], 1); $data['total_fee'] = $data['number'] * $data['fee']; - $data['price_original'] = $data['product']['price'] + $data['product']['price_profit']; + $data['price_original'] = $this->resolveProductUnitBasePrice($data['product']); $data['total_price_original'] = $data['product']['amount'] * ($data['price_original'] ?? 0); // Check if the method is cash @@ -134,7 +134,7 @@ class SingleOrderController extends Controller $data['service_charge_percentage'] = isset($data['fee_free']) && $data['fee_free'] === true ? 0 : ($data['product']['percent_fee_sell']) * $data['number']; $data['difference'] = $data['fee'] = $this->resolveCashPriceDiff($config, $data['product'], 2); $data['total_fee'] = $data['number'] * $data['fee']; - $data['price_original'] = $data['product']['price'] + $data['product']['price_profit']; + $data['price_original'] = $this->resolveProductUnitBasePrice($data['product']); $data['total_price_original'] = $data['product']['amount'] * ($data['price_original'] ?? 0); $this->assertMinimumSellCash($data); @@ -1171,6 +1171,22 @@ class SingleOrderController extends Controller ); } + /** + * قیمت پایهٔ هر واحد محصول (بدون ضرب در وزن — وزن در total_price_original اعمال می‌شود). + * محصول با پرچم «قیمت اپ» → قیمت زندهٔ هر گرم اپ (با تعدیل درصدی)؛ + * بقیه → همان قیمت ذخیره‌شدهٔ محصول. + */ + private function resolveProductUnitBasePrice(Product $product): int + { + $display = app(ProductDisplayPriceService::class); + + if ($display->usesFlaggedAppPrice($product)) { + return (int) round($display->resolveOrderBasePrice($product)); + } + + return (int) (($product->price ?? 0) + ($product->price_profit ?? 0)); + } + private function bonusGift($customer, $referrer) { $bonus = Setting::where('key', 'referral_bonus_amount')->where('is_active', 1)->first(); diff --git a/app/Models/Product.php b/app/Models/Product.php index 1eb4fdb..b82d090 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -21,6 +21,7 @@ class Product extends Model private static ?bool $showInAssetsColumnExists = null; private static ?bool $useLastPriceIfZeroColumnExists = null; private static ?bool $directPriceColumnExists = null; + private static ?bool $appPriceColumnExists = null; private static ?bool $tradeByQuantityColumnExists = null; private static ?bool $inventoryColumnExists = null; private static ?bool $overrideTradingHoursColumnExists = null; @@ -49,6 +50,9 @@ class Product extends Model if (!static::hasDirectPriceColumn()) { unset($product->attributes['direct_price']); } + if (!static::hasAppPriceColumn()) { + unset($product->attributes['app_price']); + } if (!static::hasTradeByQuantityColumn()) { unset($product->attributes['trade_by_quantity']); } @@ -67,7 +71,7 @@ class Product extends Model if (!static::hasPhysicalDeliveryColumns()) { unset( $product->attributes['has_physical_delivery'], - $product->attributes['min_physical_delivery_grams'] + $product->attributes['min_physical_delivery_quantity'] ); } }); @@ -82,7 +86,7 @@ class Product extends Model try { $table = (new static)->getTable(); static::$physicalDeliveryColumnsExist = Schema::hasColumn($table, 'has_physical_delivery') - && Schema::hasColumn($table, 'min_physical_delivery_grams'); + && Schema::hasColumn($table, 'min_physical_delivery_quantity'); } catch (\Throwable) { static::$physicalDeliveryColumnsExist = false; } @@ -235,6 +239,24 @@ class Product extends Model return static::$directPriceColumnExists; } + public static function hasAppPriceColumn(): bool + { + if (static::$appPriceColumnExists !== null) { + return static::$appPriceColumnExists; + } + + try { + static::$appPriceColumnExists = Schema::hasColumn( + (new static)->getTable(), + 'app_price' + ); + } catch (\Throwable) { + static::$appPriceColumnExists = false; + } + + return static::$appPriceColumnExists; + } + public static function hasTradeByQuantityColumn(): bool { if (static::$tradeByQuantityColumnExists !== null) { @@ -315,11 +337,11 @@ class Product extends Model return (bool) ($this->attributes['has_physical_delivery'] ?? false); } - public function minPhysicalDeliveryGrams(): float + public function minPhysicalDeliveryQuantity(): float { $value = static::hasPhysicalDeliveryColumns() - ? ($this->min_physical_delivery_grams ?? 0) - : ($this->attributes['min_physical_delivery_grams'] ?? 0); + ? ($this->min_physical_delivery_quantity ?? 0) + : ($this->attributes['min_physical_delivery_quantity'] ?? 0); return max(0.0, (float) $value); } @@ -363,8 +385,8 @@ class Product extends Model 'order_limit_enabled', 'min_order_cash', 'max_order_cash', - 'has_physical_delivery', // روشن: این محصول امکان تحویل فیزیکی دارد - 'min_physical_delivery_grams', // حداقل موجودی (گرم) برای فعال شدن تحویل فیزیکی + 'has_physical_delivery', // روشن: این محصول امکان تحویل فیزیکی دارد + 'min_physical_delivery_quantity', // حداقل موجودی برای فعال شدن تحویل (عدد برای تعدادی، گرم/کیلوگرم برای وزنی) 'metal_id', //the material of metal (#gold or #silver) 'price_offset_percent', // admin only: +/- % vs automatic reference (baked into price on sync) 'price_profit', @@ -372,6 +394,7 @@ class Product extends Model 'inventory', 'description', 'direct_price', + 'app_price', // روشن: قیمت واحد از قیمت زندهٔ اپ هر گرم × وزن محاسبه می‌شود ]; protected $attributes = [ @@ -380,6 +403,7 @@ class Product extends Model 'use_last_price_if_zero' => false, 'cash_fee_buy_is_percent' => false, 'cash_fee_sell_is_percent' => false, + 'app_price' => false, ]; protected $casts = [ @@ -402,13 +426,14 @@ class Product extends Model 'min_order_cash' => 'int', 'max_order_cash' => 'int', 'has_physical_delivery' => 'boolean', - 'min_physical_delivery_grams' => 'float', + 'min_physical_delivery_quantity' => 'float', 'cash_fee_buy' => 'float', 'cash_fee_sell' => 'float', 'cash_fee_buy_is_percent' => 'boolean', 'cash_fee_sell_is_percent' => 'boolean', 'price_offset_percent' => 'float', 'price_profit' => 'int', + 'app_price' => 'boolean', ]; static $METAL_ID = [ diff --git a/app/Services/ProductDisplayPriceService.php b/app/Services/ProductDisplayPriceService.php index 3ccef25..3efbb92 100644 --- a/app/Services/ProductDisplayPriceService.php +++ b/app/Services/ProductDisplayPriceService.php @@ -26,6 +26,11 @@ class ProductDisplayPriceService return false; } + // پرچم صریح «قیمت اپ» برای هر دسته‌ای (شمش، سکه، …) — قیمت هر گرم × وزن + if (Product::hasAppPriceColumn() && (bool) ($product->app_price ?? false)) { + return true; + } + if ((int) ($product->direct_price ?? 0) === 1) { return false; } @@ -34,6 +39,22 @@ class ProductDisplayPriceService || $this->usesQuantityAppPrice($product); } + /** محصولی که با پرچم صریح app_price قیمت اپ می‌گیرد (نه استنتاج آبشده/مقداری) */ + public function usesFlaggedAppPrice(Product $product): bool + { + return Product::hasAppPriceColumn() + && (bool) ($product->app_price ?? false) + && ! $product->reading_method; + } + + /** ضریب وزن برای محصول قیمت‌اپ: وزن واحد به گرم‌واحد محصول (amount) یا ۱ */ + public function appPriceWeightFactor(Product $product): float + { + $amount = (float) ($product->amount ?? 0); + + return $amount > 0 ? $amount : 1.0; + } + /** معامله مقداری با قیمت اپ — نه قیمت دستی قدیمی با price پر در DB */ private function usesQuantityAppPrice(Product $product): bool { diff --git a/database/migrations/2026_07_13_000001_add_physical_delivery_to_products_table.php b/database/migrations/2026_07_13_000001_add_physical_delivery_to_products_table.php index 1183db8..718dac0 100644 --- a/database/migrations/2026_07_13_000001_add_physical_delivery_to_products_table.php +++ b/database/migrations/2026_07_13_000001_add_physical_delivery_to_products_table.php @@ -16,8 +16,12 @@ return new class extends Migration if (! Schema::hasColumn('products', 'has_physical_delivery')) { $table->boolean('has_physical_delivery')->default(false); } - if (! Schema::hasColumn('products', 'min_physical_delivery_grams')) { - $table->decimal('min_physical_delivery_grams', 12, 3)->nullable(); + // مقدار حداقل تحویل بر حسب واحد خود محصول (عدد برای تعدادی، گرم/کیلوگرم برای وزنی) + if ( + ! Schema::hasColumn('products', 'min_physical_delivery_quantity') + && ! Schema::hasColumn('products', 'min_physical_delivery_grams') + ) { + $table->decimal('min_physical_delivery_quantity', 12, 3)->nullable(); } }); } @@ -29,6 +33,9 @@ return new class extends Migration } Schema::table('products', function (Blueprint $table) { + if (Schema::hasColumn('products', 'min_physical_delivery_quantity')) { + $table->dropColumn('min_physical_delivery_quantity'); + } if (Schema::hasColumn('products', 'min_physical_delivery_grams')) { $table->dropColumn('min_physical_delivery_grams'); } diff --git a/database/migrations/2026_07_13_000002_rename_min_physical_delivery_grams_to_quantity.php b/database/migrations/2026_07_13_000002_rename_min_physical_delivery_grams_to_quantity.php new file mode 100644 index 0000000..98bfbd4 --- /dev/null +++ b/database/migrations/2026_07_13_000002_rename_min_physical_delivery_grams_to_quantity.php @@ -0,0 +1,54 @@ +boolean('app_price')->default(false)->after('direct_price'); + } else { + $table->boolean('app_price')->default(false); + } + }); + } + } + + public function down(): void + { + if (! Schema::hasTable('products') || ! Schema::hasColumn('products', 'app_price')) { + return; + } + + Schema::table('products', function (Blueprint $table) { + $table->dropColumn('app_price'); + }); + } +};