diff --git a/app/Http/Controllers/Admin/ProductController.php b/app/Http/Controllers/Admin/ProductController.php index fa84ead..0046b15 100644 --- a/app/Http/Controllers/Admin/ProductController.php +++ b/app/Http/Controllers/Admin/ProductController.php @@ -460,6 +460,12 @@ class ProductController extends SuperController $data['use_last_price_if_zero'] = !empty($data['use_last_price_if_zero']); } + if (!Product::hasTradeByQuantityColumn()) { + unset($data['trade_by_quantity']); + } elseif (array_key_exists('trade_by_quantity', $data)) { + $data['trade_by_quantity'] = !empty($data['trade_by_quantity']); + } + if (!Product::hasDirectPriceColumn()) { unset($data['direct_price']); } diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 846b18c..adc83ec 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -451,6 +451,16 @@ class Controller extends BaseController return (float) $product['amount'] > 0; } + /** معامله مقداری: فقط فیلد trade_by_quantity */ + public static function usesQuantityTrade($product): bool + { + $value = is_array($product) + ? ($product['trade_by_quantity'] ?? null) + : (is_object($product) ? ($product->trade_by_quantity ?? null) : null); + + return !empty($value); + } + /** ضریب قیمت محصول — بدون وزن قطعه همیشه ۱ */ public static function productPriceMultiplier($product): float { diff --git a/app/Models/Product.php b/app/Models/Product.php index 7107ec8..135c751 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 $tradeByQuantityColumnExists = null; protected static function booted(): void { @@ -44,6 +45,9 @@ class Product extends Model if (!static::hasDirectPriceColumn()) { unset($product->attributes['direct_price']); } + if (!static::hasTradeByQuantityColumn()) { + unset($product->attributes['trade_by_quantity']); + } }); } @@ -153,6 +157,24 @@ class Product extends Model return static::$directPriceColumnExists; } + public static function hasTradeByQuantityColumn(): bool + { + if (static::$tradeByQuantityColumnExists !== null) { + return static::$tradeByQuantityColumnExists; + } + + try { + static::$tradeByQuantityColumnExists = Schema::hasColumn( + (new static)->getTable(), + 'trade_by_quantity' + ); + } catch (\Throwable) { + static::$tradeByQuantityColumnExists = false; + } + + return static::$tradeByQuantityColumnExists; + } + public function orderLimitsEnabled(): bool { if (!static::hasOrderLimitColumns() || !($this->order_limit_enabled ?? false)) { @@ -204,6 +226,7 @@ class Product extends Model 'for_barayand', // Whether it is for Barayand (boolean). 'show', // Whether to show the record (boolean). 'show_in_assets', // Show balance on customer assets page cards + 'trade_by_quantity', // UI معامله مقداری: ارزش کل + مقدار 'use_last_price_if_zero', // Keep last DB price when automatic provider returns zero 'open_sell', // Whether selling is open (boolean). 'auto_open_at', // Whether selling is open (boolean). @@ -230,6 +253,7 @@ class Product extends Model protected $attributes = [ 'show_in_assets' => false, + 'trade_by_quantity' => false, 'use_last_price_if_zero' => false, ]; @@ -238,6 +262,7 @@ class Product extends Model 'active' => 'boolean', // Cast to boolean. 'show' => 'boolean', // Cast to boolean. 'show_in_assets' => 'boolean', + 'trade_by_quantity' => 'boolean', 'use_last_price_if_zero' => 'boolean', 'for_barayand' => 'boolean', // Cast to boolean. 'open_sell' => 'boolean', // Cast to boolean. diff --git a/database/migrations/2026_06_16_000001_add_trade_by_quantity_to_products_table.php b/database/migrations/2026_06_16_000001_add_trade_by_quantity_to_products_table.php new file mode 100644 index 0000000..0995615 --- /dev/null +++ b/database/migrations/2026_06_16_000001_add_trade_by_quantity_to_products_table.php @@ -0,0 +1,98 @@ +boolean('trade_by_quantity')->default(false)->after('show_in_assets'); + } else { + $table->boolean('trade_by_quantity')->default(false); + } + }); + } + + $this->backfillQuantityTradeForLegacyProducts(); + } + + public function down(): void + { + if (! Schema::hasTable('products') || ! Schema::hasColumn('products', 'trade_by_quantity')) { + return; + } + + Schema::table('products', function (Blueprint $table) { + $table->dropColumn('trade_by_quantity'); + }); + } + + private function backfillQuantityTradeForLegacyProducts(): void + { + $mainProductIds = $this->resolveMainProductIds(); + + DB::table('products') + ->where(function ($query) use ($mainProductIds) { + $query->whereIn('group_type', ['آبشده', 'کاتد مس']); + + if ($mainProductIds !== []) { + $query->orWhereIn('id', $mainProductIds); + } + }) + ->where(function ($query) { + $query->where('trade_by_quantity', false) + ->orWhereNull('trade_by_quantity'); + }) + ->update(['trade_by_quantity' => true]); + } + + /** @return array */ + private function resolveMainProductIds(): array + { + $ids = []; + + if (Schema::hasTable('settings')) { + $value = DB::table('settings')->where('key', 'main_product')->value('value'); + $id = $this->parsePositiveInt($value); + if ($id !== null) { + $ids[] = $id; + } + } + + if (Schema::hasTable('const')) { + $value = DB::table('const')->where('key', 'main_product')->value('value'); + $id = $this->parsePositiveInt($value); + if ($id !== null) { + $ids[] = $id; + } + } + + return array_values(array_unique($ids)); + } + + private function parsePositiveInt(mixed $value): ?int + { + if ($value === null) { + return null; + } + + $value = trim((string) $value); + if ($value === '' || ! ctype_digit($value)) { + return null; + } + + $id = (int) $value; + + return $id > 0 ? $id : null; + } +}; diff --git a/database/sql/add_trade_by_quantity_to_products.sql b/database/sql/add_trade_by_quantity_to_products.sql new file mode 100644 index 0000000..51ce54c --- /dev/null +++ b/database/sql/add_trade_by_quantity_to_products.sql @@ -0,0 +1,40 @@ +-- معامله مقداری: UI «ارزش کل» + «مقدار» مستقل از دسته‌بندی +-- اجرا: mysql ... < add_trade_by_quantity_to_products.sql +-- +-- بک‌فیل: محصولات قبلی دسته آبشده/کاتد مس + محصول main_product (معمولاً id=13) + +SET @has_col := ( + SELECT COUNT(*) + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = 'products' + AND COLUMN_NAME = 'trade_by_quantity' +); + +SET @sql := IF( + @has_col = 0, + 'ALTER TABLE `products` ADD COLUMN `trade_by_quantity` TINYINT(1) NOT NULL DEFAULT 0 AFTER `show_in_assets`', + 'SELECT ''trade_by_quantity already exists'' AS info' +); + +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- محصولات آبشده/کاتد مس + main_product از settings/const +UPDATE `products` p +LEFT JOIN `settings` s + ON s.`key` = 'main_product' + AND TRIM(s.`value`) REGEXP '^[0-9]+$' + AND CAST(TRIM(s.`value`) AS UNSIGNED) = p.`id` +LEFT JOIN `const` c + ON c.`key` = 'main_product' + AND TRIM(c.`value`) REGEXP '^[0-9]+$' + AND CAST(TRIM(c.`value`) AS UNSIGNED) = p.`id` +SET p.`trade_by_quantity` = 1 +WHERE COALESCE(p.`trade_by_quantity`, 0) = 0 + AND ( + p.`group_type` IN ('آبشده', 'کاتد مس') + OR s.`id` IS NOT NULL + OR c.`id` IS NOT NULL + ); diff --git a/database/sql/deploy_settings_const.sql b/database/sql/deploy_settings_const.sql index 669525a..9ab7682 100644 --- a/database/sql/deploy_settings_const.sql +++ b/database/sql/deploy_settings_const.sql @@ -317,6 +317,21 @@ INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `de SELECT 'support_messenger_text', '', 'متن پشتیبانی پیام‌رسان', 1, 0, 'متن دکمه یا توضیح پشتیبانی در اپ کاربر' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'support_messenger_text' LIMIT 1); +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'invite_link_text', +'⭐️الان طلا بخر؛ آخر ماه سودشو ببر! +با خرید طلای آبشده در مقدارهای کم ولی مستمر، سود بیشتری ببر! + +این دعوت نامه از طرف "٪نام" عزیز برای شما ارسال شده تا شما هم از بازار طلای ما جا نمونید. +روی لینک زیر کلیک کنید و ثبت نام خود را تکمیل کنید. + +٪لینک', +'متن دعوتنامه اشتراک‌گذاری', +1, 1, +'متن پیام اشتراک‌گذاری دعوت در اپ کاربر (دکمه «اشتراک با دوستان» در صفحه اصلی، بخش معرف در پروفایل، صفحه کسب درآمد و مودال اشتراک). متغیرهای جایگزین: ٪نام یا %نام = نام کامل کاربر دعوت‌کننده؛ ٪لینک یا %لینک = آدرس کامل صفحه ثبت‌نام همراه با کد معرف (بر اساس تنظیم app_url یا دامنه فعلی مرورگر). فاصله اختیاری بعد از ٪ یا % مجاز است (مثلاً ٪ نام). اگر این فیلد خالی باشد، اپ از متن پیش‌فرض داخلی بر اساس نوع نصب استفاده می‌کند.', +'longtext' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'invite_link_text' LIMIT 1); + INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`) SELECT 'support_messenger', '', 'پیام‌رسان پشتیبانی', 1, 1, 'bale,telegram,whatsapp,rubika,eitaa,soroush' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'support_messenger' LIMIT 1); @@ -593,7 +608,7 @@ WHERE `key` = 'main_product'; -- ============================================================================= -- longtext -UPDATE `settings` SET `type` = 'longtext' WHERE `key` IN ('text_rule', 'text_about', 'support_messenger_text', 'montzar_order_response'); +UPDATE `settings` SET `type` = 'longtext' WHERE `key` IN ('text_rule', 'text_about', 'support_messenger_text', 'invite_link_text', 'montzar_order_response'); -- select (+ گزینه‌ها در description) UPDATE `settings` SET `type` = 'select', `description` = 'منبع فعال قیمت|montzar,zaarbox,main_product' WHERE `key` = 'price_provider'; @@ -633,7 +648,7 @@ UPDATE `settings` SET `type` = 'text' WHERE `key` IN ( 'payping_fin_token', 'payping_gateway_item', 'tahesab_host', 'tahesab_token', 'tahesab_db', 'node_client_id', 'node_shared_secret', 'wss_path', 'wss_link', 'wss_trigger_path', - 'tahesab_bank_account', 'telegram_bot_token', 'bale_bot_token', 'support_messenger_id', 'support_messenger_text', 'referral_limit', + 'tahesab_bank_account', 'telegram_bot_token', 'bale_bot_token', 'support_messenger_id', 'support_messenger_text', 'invite_link_text', 'referral_limit', 'referral_bonus_amount', 'balance_report_exclude_customer_id', 'metal_type', 'montify_name', 'price_extreme_date', 'default_vc_number', 'liman_token' ); @@ -809,6 +824,12 @@ UPDATE `settings` SET `description` = 'متن دکمه یا توضیح پشتیبانی در اپ کاربر' WHERE `key` = 'support_messenger_text'; +UPDATE `settings` SET + `title_fa` = 'متن دعوتنامه اشتراک‌گذاری', + `type` = 'longtext', + `description` = 'متن پیام اشتراک‌گذاری دعوت در اپ کاربر (دکمه «اشتراک با دوستان» در صفحه اصلی، بخش معرف در پروفایل، صفحه کسب درآمد و مودال اشتراک). متغیرهای جایگزین: ٪نام یا %نام = نام کامل کاربر دعوت‌کننده؛ ٪لینک یا %لینک = آدرس کامل صفحه ثبت‌نام همراه با کد معرف (بر اساس تنظیم app_url یا دامنه فعلی مرورگر). فاصله اختیاری بعد از ٪ یا % مجاز است (مثلاً ٪ نام). اگر این فیلد خالی باشد، اپ از متن پیش‌فرض داخلی بر اساس نوع نصب استفاده می‌کند.' +WHERE `key` = 'invite_link_text'; + UPDATE `settings` SET `title_fa` = 'پیام‌رسان پشتیبانی', `type` = 'select', @@ -987,6 +1008,7 @@ UPDATE `settings` SET `description` = 'روشن: واریز و برداشت مش UPDATE `settings` SET `description` = 'نوع پیام‌رسان برای دکمه پشتیبانی در اپ کاربر|bale,telegram,whatsapp,rubika,eitaa,soroush' WHERE `key` = 'support_messenger'; UPDATE `settings` SET `description` = 'شناسه تماس پشتیبانی: یوزرنیم (تلگرام)، شماره (واتساپ/بله) یا آیدی بسته به پیام‌رسان انتخاب‌شده.' WHERE `key` = 'support_messenger_id'; UPDATE `settings` SET `description` = 'متن دکمه یا توضیح کوتاه کنار لینک پشتیبانی در اپ کاربر.' WHERE `key` = 'support_messenger_text'; +UPDATE `settings` SET `description` = 'متن پیام اشتراک‌گذاری دعوت در اپ کاربر (دکمه «اشتراک با دوستان» در صفحه اصلی، بخش معرف در پروفایل، صفحه کسب درآمد و مودال اشتراک). متغیرهای جایگزین: ٪نام یا %نام = نام کامل کاربر دعوت‌کننده؛ ٪لینک یا %لینک = آدرس کامل صفحه ثبت‌نام همراه با کد معرف (بر اساس تنظیم app_url یا دامنه فعلی مرورگر). فاصله اختیاری بعد از ٪ یا % مجاز است (مثلاً ٪ نام). اگر این فیلد خالی باشد، اپ از متن پیش‌فرض داخلی بر اساس نوع نصب استفاده می‌کند.' WHERE `key` = 'invite_link_text'; UPDATE `settings` SET `description` = 'شماره موبایل پشتیبانی واریز؛ در صفحه شارژ حساب دکمه پیامک با متن «شماره حساب» نمایش داده می‌شود.' WHERE `key` = 'deposit_sms_support_number'; UPDATE `settings` SET `description` = 'توکن ربات تلگرام برای ارسال اعلان‌ها. از BotFather دریافت می‌شود. محرمانه.' WHERE `key` = 'telegram_bot_token'; diff --git a/database/sql/invite_link_text_setting.sql b/database/sql/invite_link_text_setting.sql new file mode 100644 index 0000000..4c7d41e --- /dev/null +++ b/database/sql/invite_link_text_setting.sql @@ -0,0 +1,29 @@ +-- invite_link_text: متن دعوتنامه اشتراک‌گذاری (longtext) +-- اجرا روی DB موجود: INSERT اگر نبود + به‌روزرسانی type و description + +START TRANSACTION; + +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'invite_link_text', +'⭐️الان طلا بخر؛ آخر ماه سودشو ببر! +با خرید طلای آبشده در مقدارهای کم ولی مستمر، سود بیشتری ببر! + +این دعوت نامه از طرف "٪نام" عزیز برای شما ارسال شده تا شما هم از بازار طلای ما جا نمونید. +روی لینک زیر کلیک کنید و ثبت نام خود را تکمیل کنید. + +٪لینک', +'متن دعوتنامه اشتراک‌گذاری', +1, 1, +'متن پیام اشتراک‌گذاری دعوت در اپ کاربر (دکمه «اشتراک با دوستان» در صفحه اصلی، بخش معرف در پروفایل، صفحه کسب درآمد و مودال اشتراک). متغیرهای جایگزین: ٪نام یا %نام = نام کامل کاربر دعوت‌کننده؛ ٪لینک یا %لینک = آدرس کامل صفحه ثبت‌نام همراه با کد معرف (بر اساس تنظیم app_url یا دامنه فعلی مرورگر). فاصله اختیاری بعد از ٪ یا % مجاز است (مثلاً ٪ نام). اگر این فیلد خالی باشد، اپ از متن پیش‌فرض داخلی بر اساس نوع نصب استفاده می‌کند.', +'longtext' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'invite_link_text' LIMIT 1); + +UPDATE `settings` SET + `title_fa` = 'متن دعوتنامه اشتراک‌گذاری', + `type` = 'longtext', + `can_edit` = 1, + `is_active` = 1, + `description` = 'متن پیام اشتراک‌گذاری دعوت در اپ کاربر (دکمه «اشتراک با دوستان» در صفحه اصلی، بخش معرف در پروفایل، صفحه کسب درآمد و مودال اشتراک). متغیرهای جایگزین: ٪نام یا %نام = نام کامل کاربر دعوت‌کننده؛ ٪لینک یا %لینک = آدرس کامل صفحه ثبت‌نام همراه با کد معرف (بر اساس تنظیم app_url یا دامنه فعلی مرورگر). فاصله اختیاری بعد از ٪ یا % مجاز است (مثلاً ٪ نام). اگر این فیلد خالی باشد، اپ از متن پیش‌فرض داخلی بر اساس نوع نصب استفاده می‌کند.' +WHERE `key` = 'invite_link_text'; + +COMMIT;