From 27d58e8a11ed818025e9c2364a88b96162f34b06 Mon Sep 17 00:00:00 2001 From: adonis Date: Mon, 22 Jun 2026 19:14:24 +0330 Subject: [PATCH 01/19] fixed show true when price is 0 --- .../Single/SingleOrderController.php | 75 +++++++++++++++++-- app/Services/ProductDisplayPriceService.php | 34 +++------ app/Services/ProductPriceService.php | 17 +++++ database/sql/deploy_settings_const.sql | 45 +++++++---- ...ngs_const_preserve_value_sync_can_edit.sql | 45 +++++++---- 5 files changed, 161 insertions(+), 55 deletions(-) diff --git a/app/Http/Controllers/Single/SingleOrderController.php b/app/Http/Controllers/Single/SingleOrderController.php index 8c325f6..33ce376 100644 --- a/app/Http/Controllers/Single/SingleOrderController.php +++ b/app/Http/Controllers/Single/SingleOrderController.php @@ -32,6 +32,10 @@ class SingleOrderController extends Controller public function store(Request $request) { $data = $request->all(); + $data['payment_product_id'] = $this->resolveGoldPaymentProductId($data); + if (preg_match('/^gold:\d+$/', (string) ($data['method'] ?? ''))) { + $data['method'] = 'gold'; + } $data['is_katod'] = ($data['method'] ?? null) === 'katod'; if ($data['is_katod']) { $data['method'] = 'gold'; @@ -332,22 +336,69 @@ class SingleOrderController extends Controller $data['total_fee'] = 0; $data['service_charge_price'] = $data['service_charge'] * $data['price_original']; $data['wallet'] = $wallet; - if ($wallet->gold < $data['total_amount']) { + + $paymentProductId = (int) ($data['payment_product_id'] ?? 0); + $paymentWalletInterface = $wallet_interface; + $paymentProduct = null; + $paymentWallet = $wallet; + + if ($paymentProductId > 0) { + $paymentProduct = Product::find($paymentProductId); + if ( + !$paymentProduct + || (int) ($paymentProduct->metal_id ?? 0) !== (int) ($data['product']['metal_id'] ?? 0) + ) { + throw new UnprocessableEntityHttpException('روش پرداخت نامعتبر است.'); + } + + $paymentWalletInterface = $this->productWalletInterface($paymentProductId); + $paymentWallet = $paymentWalletInterface->wallet; + if (!$paymentWallet) { + throw new UnprocessableEntityHttpException('کیف پول یافت نشد.'); + } + } + + if ((float) $paymentWallet->gold < $data['total_amount']) { throw new UnprocessableEntityHttpException($this->insufficientGoldBalanceMessage($data)); } if (!$data['product']['open_buy']) { throw new UnprocessableEntityHttpException('خرید این محصول در این لحظه مقدور نمیباشد'); } $data['order'] = $order = Order::create($data); - $data['transaction'][] = new TransactionInterface($wallet_interface, $_inp = [ + + $paymentSellTitle = $paymentProduct + ? self::productSellTitle($paymentProduct->title ?? '') + : self::mainMetalSellTitle(); + + $paymentTransaction = [ "number" => $data['number'], "gold" => $data['total_amount'], - "product_id" => $data["product_id"], 'expire_at' => now(), 'active' => true, 'order_id' => $order->id, - 'title' => self::mainMetalSellTitle(), - ], 12, false, true); + 'title' => $paymentSellTitle, + ]; + + if ($paymentProductId > 0) { + $paymentTransaction['product_id'] = $paymentProductId; + $this->normalizeProductWalletGold($paymentWalletInterface); + $data['transaction'][] = new TransactionInterface( + $paymentWalletInterface, + $paymentTransaction, + 1, + false, + false + ); + } else { + $data['transaction'][] = new TransactionInterface( + $paymentWalletInterface, + $paymentTransaction, + 12, + false, + true + ); + } + $wallet_interface_product = $this->productWalletInterface((int) $data["product_id"]); $this->normalizeProductWalletGold($wallet_interface_product); new TransactionInterface($wallet_interface_product, [ @@ -794,6 +845,20 @@ class SingleOrderController extends Controller return null; } + private function resolveGoldPaymentProductId(array $data): ?int + { + if (!empty($data['payment_product_id'])) { + return (int) $data['payment_product_id']; + } + + $method = (string) ($data['method'] ?? ''); + if (preg_match('/^gold:(\d+)$/', $method, $matches)) { + return (int) $matches[1]; + } + + return null; + } + private function resolveAbshodeProductContext(array $data): array { $productId = (int) ($data['product_id'] ?? $data['abshode_id'] ?? 0); diff --git a/app/Services/ProductDisplayPriceService.php b/app/Services/ProductDisplayPriceService.php index e8060ec..c15fb14 100644 --- a/app/Services/ProductDisplayPriceService.php +++ b/app/Services/ProductDisplayPriceService.php @@ -100,7 +100,7 @@ class ProductDisplayPriceService ? $this->resolveLiveBuyPriceWithFallback() : $this->resolveStrictLiveBuyPrice(); - if ($unitBuy <= 0 && app(ConfigService::class)->getBool('show_last_price', false)) { + if ($unitBuy <= 0 && $useLast) { $unitBuy = $this->resolveHistoricalBasePrice(); } @@ -262,25 +262,11 @@ class ProductDisplayPriceService /** @param array> $feed */ private function applyReferenceFeedPrice(Product $product, array $feed, ZaarboxPriceService $zaarbox): void { - if (Product::hasStoredPriceColumns()) { - $stored = (int) ($product->price ?? 0) + (int) ($product->price_profit ?? 0); - if ($stored > 0) { - $product->setAttribute('price', (int) ($product->price ?? 0)); - if (Schema::hasColumn($product->getTable(), 'price_profit')) { - $product->setAttribute('price_profit', (int) ($product->price_profit ?? 0)); - } - - return; - } - } - + $useLast = $this->productUsesLastPriceIfZero($product); $row = $zaarbox->findFeedRow($feed, (string) $product->reading_method); if ($row === null) { - $useLast = $this->productUsesLastPriceIfZero($product); $offset = ProductPriceService::offsetPercent($product); - $historical = app(ConfigService::class)->getBool('show_last_price', false) - ? $this->resolveHistoricalBasePrice() - : 0; + $historical = $useLast ? $this->resolveHistoricalBasePrice() : 0; $computed = $historical > 0 ? ProductPriceService::adjustedTotal($historical, 0, $offset) : 0; @@ -291,7 +277,7 @@ class ProductDisplayPriceService $computed = $zaarbox->computeAdjustedProductPrice($product, $row); if ($computed <= 0) { - if ($this->productUsesLastPriceIfZero($product)) { + if ($useLast) { $fallback = $this->resolveLastPriceFallback($product); if ($fallback > 0) { $product->setAttribute('price', $fallback); @@ -455,10 +441,11 @@ class ProductDisplayPriceService /** * نمایش در اپ کاربر: - * • خاموش (show=false در DB) → false + * • خاموش (active=false در DB) → false + * • خارج از ساعات معامله → false * • قیمت نمایشی بعد از enrich → true - * • قیمت صفر و show_last_price خاموش → false - * • قیمت صفر و show_last_price روشن و fallback موجود → true + * • قیمت صفر و use_last_price_if_zero خاموش → false + * • قیمت صفر و use_last_price_if_zero روشن و fallback موجود → true */ public function resolveCustomerShow(Product $product): bool { @@ -474,11 +461,10 @@ class ProductDisplayPriceService return true; } - if (! app(ConfigService::class)->getBool('show_last_price', false)) { + if (! $this->productUsesLastPriceIfZero($product)) { return false; } - return $this->resolveLastPriceFallback($product) > 0 - || $this->resolveHistoricalBasePrice() > 0; + return $this->resolveLastPriceFallback($product) > 0; } } diff --git a/app/Services/ProductPriceService.php b/app/Services/ProductPriceService.php index ea536d1..1b5b849 100644 --- a/app/Services/ProductPriceService.php +++ b/app/Services/ProductPriceService.php @@ -51,6 +51,10 @@ class ProductPriceService $data['price_profit'] = 0; } + if (\Illuminate\Support\Facades\Schema::hasColumn($product->getTable(), 'show')) { + $data['show'] = self::resolveShowAfterPriceSync($product, $adjustedPrice); + } + Product::where('id', $product->id)->update($data); return [ @@ -67,6 +71,19 @@ class ProductPriceService ]; } + private static function resolveShowAfterPriceSync(Product $product, int $adjustedPrice): bool + { + if (! ($product->active ?? true)) { + return false; + } + + if (! app(ConfigService::class)->isProductWithinTradingHours($product)) { + return false; + } + + return $adjustedPrice > 0; + } + /** وقتی فقط درصد در ادمین عوض می‌شود (بدون sync جدید) */ public static function reapplyOffsetPercent(Product $product, float $newOffsetPercent, ?float $oldOffsetPercent = null): void { diff --git a/database/sql/deploy_settings_const.sql b/database/sql/deploy_settings_const.sql index f6f3bba..617811f 100644 --- a/database/sql/deploy_settings_const.sql +++ b/database/sql/deploy_settings_const.sql @@ -425,19 +425,19 @@ SELECT 'referral_id', 'required', 'کد معرف اجباری', 1, 1, 'روشن WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'referral_id' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) -SELECT 'fee_single', '500000', 'اختلاف پایه نمایش', 1, 1, NULL, 'number' +SELECT 'fee_single', '500000', 'اختلاف پایه نمایش', 1, 1, NULL, 'price' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'fee_single' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) -SELECT 'buy_fee', '', 'سود خرید نمایش', 1, 1, NULL, 'number' +SELECT 'buy_fee', '', 'سود خرید نمایش', 1, 1, NULL, 'price' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'buy_fee' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) -SELECT 'sell_fee', '', 'سود فروش نمایش', 1, 1, NULL, 'number' +SELECT 'sell_fee', '', 'سود فروش نمایش', 1, 1, NULL, 'price' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'sell_fee' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) -SELECT 'fee_cash', '500000', 'کارمزد ریالی سفارش (legacy)', 0, 1, NULL, 'number' +SELECT 'fee_cash', '500000', 'کارمزد ریالی سفارش (legacy)', 0, 1, NULL, 'price' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'fee_cash' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) @@ -445,15 +445,15 @@ SELECT 'fee_service_charge_percentage', '0.005', 'درصد کارمزد سفار WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'fee_service_charge_percentage' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) -SELECT 'min_withdraw', '', 'حداقل برداشت (ریال)', 1, 1, NULL, 'number' +SELECT 'min_withdraw', '', 'حداقل برداشت (ریال)', 1, 1, NULL, 'price' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'min_withdraw' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) -SELECT 'min_first_buy', '', 'حداقل اولین خرید (ریال)', 1, 1, NULL, 'number' +SELECT 'min_first_buy', '', 'حداقل اولین خرید (ریال)', 1, 1, NULL, 'price' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'min_first_buy' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) -SELECT 'minimum_sell', '', 'حداقل فروش', 1, 1, NULL, 'number' +SELECT 'minimum_sell', '', 'حداقل فروش', 1, 1, NULL, 'price' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'minimum_sell' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) @@ -639,12 +639,18 @@ UPDATE `settings` SET `type` = 'switch' WHERE `key` IN ( 'banner_enabled' ); --- number -UPDATE `settings` SET `type` = 'number' WHERE `key` IN ( - 'fee_single', 'buy_fee', 'sell_fee', 'fee_cash', 'fee_service_charge_percentage', 'fee_free', +-- price (مبالغ ریالی) +UPDATE `settings` SET `type` = 'price' WHERE `key` IN ( + 'fee_single', 'buy_fee', 'sell_fee', 'fee_cash', 'min_withdraw', 'min_first_buy', 'minimum_sell', - 'montzar_product_id', 'gift_for_referrer', - 'gift_for_referral', 'up_price', 'down_price', 'payping_fin_customer_id', 'tahesab_port', + 'up_price', 'down_price' +); + +-- number (شماره، شناسه، درصد، تعداد — بدون جداکننده هزارگان) +UPDATE `settings` SET `type` = 'number' WHERE `key` IN ( + 'fee_service_charge_percentage', 'fee_free', 'service_charge_percentage', + 'montzar_product_id', 'gift_for_referrer', 'gift_for_referral', + 'payping_fin_customer_id', 'tahesab_port', 'referral_limit', 'deposit_sms_support_number' ); @@ -659,7 +665,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', 'sse_path', - 'tahesab_bank_account', 'telegram_bot_token', 'bale_bot_token', 'support_messenger_id', 'referral_limit', + 'tahesab_bank_account', 'telegram_bot_token', 'bale_bot_token', 'support_messenger_id', 'referral_bonus_amount', 'balance_report_exclude_customer_id', 'metal_type', 'montify_name', 'price_extreme_date', 'default_vc_number', 'liman_token' ); @@ -1104,6 +1110,19 @@ UPDATE `settings` SET `can_edit` = 1 WHERE `key` IN ( 'text_about', 'text_rule' ); +-- تفکیک نهایی type (بعد از همه batchها — override روی دیتابیس موجود) +UPDATE `settings` SET `type` = 'price' WHERE `key` IN ( + 'fee_single', 'buy_fee', 'sell_fee', 'fee_cash', + 'min_withdraw', 'min_first_buy', 'minimum_sell', + 'up_price', 'down_price' +); +UPDATE `settings` SET `type` = 'number' WHERE `key` IN ( + 'fee_service_charge_percentage', 'fee_free', 'service_charge_percentage', + 'montzar_product_id', 'gift_for_referrer', 'gift_for_referral', + 'payping_fin_customer_id', 'tahesab_port', 'referral_limit', + 'deposit_sms_support_number' +); + COMMIT; SELECT 'deploy_settings_const.sql finished' AS status; diff --git a/database/sql/deploy_settings_const_preserve_value_sync_can_edit.sql b/database/sql/deploy_settings_const_preserve_value_sync_can_edit.sql index b83f50f..040e834 100644 --- a/database/sql/deploy_settings_const_preserve_value_sync_can_edit.sql +++ b/database/sql/deploy_settings_const_preserve_value_sync_can_edit.sql @@ -405,19 +405,19 @@ SELECT 'referral_id', 'required', 'کد معرف اجباری', 1, 1, 'روشن WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'referral_id' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) -SELECT 'fee_single', '500000', 'اختلاف پایه نمایش', 1, 1, NULL, 'number' +SELECT 'fee_single', '500000', 'اختلاف پایه نمایش', 1, 1, NULL, 'price' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'fee_single' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) -SELECT 'buy_fee', '', 'سود خرید نمایش', 1, 1, NULL, 'number' +SELECT 'buy_fee', '', 'سود خرید نمایش', 1, 1, NULL, 'price' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'buy_fee' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) -SELECT 'sell_fee', '', 'سود فروش نمایش', 1, 1, NULL, 'number' +SELECT 'sell_fee', '', 'سود فروش نمایش', 1, 1, NULL, 'price' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'sell_fee' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) -SELECT 'fee_cash', '500000', 'کارمزد ریالی سفارش (legacy)', 0, 1, NULL, 'number' +SELECT 'fee_cash', '500000', 'کارمزد ریالی سفارش (legacy)', 0, 1, NULL, 'price' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'fee_cash' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) @@ -425,15 +425,15 @@ SELECT 'fee_service_charge_percentage', '0.005', 'درصد کارمزد سفار WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'fee_service_charge_percentage' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) -SELECT 'min_withdraw', '', 'حداقل برداشت (ریال)', 1, 1, NULL, 'number' +SELECT 'min_withdraw', '', 'حداقل برداشت (ریال)', 1, 1, NULL, 'price' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'min_withdraw' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) -SELECT 'min_first_buy', '', 'حداقل اولین خرید (ریال)', 1, 1, NULL, 'number' +SELECT 'min_first_buy', '', 'حداقل اولین خرید (ریال)', 1, 1, NULL, 'price' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'min_first_buy' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) -SELECT 'minimum_sell', '', 'حداقل فروش', 1, 1, NULL, 'number' +SELECT 'minimum_sell', '', 'حداقل فروش', 1, 1, NULL, 'price' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'minimum_sell' LIMIT 1); INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) @@ -615,12 +615,18 @@ UPDATE `settings` SET `type` = 'switch' WHERE `key` IN ( 'register_referral_sms_to_referrer', 'register_referral_bonus_message', 'price_from_nasiryan' ); --- number -UPDATE `settings` SET `type` = 'number' WHERE `key` IN ( - 'fee_single', 'buy_fee', 'sell_fee', 'fee_cash', 'fee_service_charge_percentage', 'fee_free', +-- price (مبالغ ریالی) +UPDATE `settings` SET `type` = 'price' WHERE `key` IN ( + 'fee_single', 'buy_fee', 'sell_fee', 'fee_cash', 'min_withdraw', 'min_first_buy', 'minimum_sell', - 'montzar_product_id', 'gift_for_referrer', - 'gift_for_referral', 'up_price', 'down_price', 'payping_fin_customer_id', 'tahesab_port', + 'up_price', 'down_price' +); + +-- number (شماره، شناسه، درصد، تعداد — بدون جداکننده هزارگان) +UPDATE `settings` SET `type` = 'number' WHERE `key` IN ( + 'fee_service_charge_percentage', 'fee_free', 'service_charge_percentage', + 'montzar_product_id', 'gift_for_referrer', 'gift_for_referral', + 'payping_fin_customer_id', 'tahesab_port', 'referral_limit', 'deposit_sms_support_number' ); @@ -635,7 +641,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', 'sse_path', - 'tahesab_bank_account', 'telegram_bot_token', 'bale_bot_token', 'support_messenger_id', 'referral_limit', + 'tahesab_bank_account', 'telegram_bot_token', 'bale_bot_token', 'support_messenger_id', 'referral_bonus_amount', 'balance_report_exclude_customer_id', 'metal_type', 'montify_name', 'price_extreme_date', 'default_vc_number', 'liman_token' ); @@ -1158,6 +1164,19 @@ JOIN ( ) d ON d.`key` = s.`key` SET s.`can_edit` = d.`can_edit`; +-- تفکیک نهایی type (بعد از همه batchها — override روی دیتابیس موجود) +UPDATE `settings` SET `type` = 'price' WHERE `key` IN ( + 'fee_single', 'buy_fee', 'sell_fee', 'fee_cash', + 'min_withdraw', 'min_first_buy', 'minimum_sell', + 'up_price', 'down_price' +); +UPDATE `settings` SET `type` = 'number' WHERE `key` IN ( + 'fee_service_charge_percentage', 'fee_free', 'service_charge_percentage', + 'montzar_product_id', 'gift_for_referrer', 'gift_for_referral', + 'payping_fin_customer_id', 'tahesab_port', 'referral_limit', + 'deposit_sms_support_number' +); + COMMIT; SELECT 'deploy_settings_const.sql finished' AS status; From 8858cf968b55536a77faf27ca03f2f2621b72df4 Mon Sep 17 00:00:00 2001 From: adonis Date: Mon, 22 Jun 2026 19:35:20 +0330 Subject: [PATCH 02/19] fixed show true when price is 0 2 --- app/Services/ProductDisplayPriceService.php | 39 +++++++++++++++++---- app/Services/ProductPriceService.php | 17 --------- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/app/Services/ProductDisplayPriceService.php b/app/Services/ProductDisplayPriceService.php index c15fb14..3f37689 100644 --- a/app/Services/ProductDisplayPriceService.php +++ b/app/Services/ProductDisplayPriceService.php @@ -73,6 +73,19 @@ class ProductDisplayPriceService continue; } + if ($product->reading_method && ! $this->productUsesLastPriceIfZero($product)) { + $product->setAttribute('reference_provider_active', false); + $this->zeroProductPrice($product); + + continue; + } + + if ($product->reading_method) { + $product->setAttribute('reference_provider_active', true); + + continue; + } + if ($this->isWeightBasedStoreProduct($product)) { $this->enrichWeightBasedPrice($product); } @@ -100,7 +113,7 @@ class ProductDisplayPriceService ? $this->resolveLiveBuyPriceWithFallback() : $this->resolveStrictLiveBuyPrice(); - if ($unitBuy <= 0 && $useLast) { + if ($unitBuy <= 0 && app(ConfigService::class)->getBool('show_last_price', false)) { $unitBuy = $this->resolveHistoricalBasePrice(); } @@ -265,6 +278,7 @@ class ProductDisplayPriceService $useLast = $this->productUsesLastPriceIfZero($product); $row = $zaarbox->findFeedRow($feed, (string) $product->reading_method); if ($row === null) { + $product->setAttribute('reference_provider_active', $useLast); $offset = ProductPriceService::offsetPercent($product); $historical = $useLast ? $this->resolveHistoricalBasePrice() : 0; $computed = $historical > 0 @@ -275,6 +289,17 @@ class ProductDisplayPriceService return; } + $normalized = $zaarbox->normalizeFeedRow($row); + $providerRaw = (int) round((float) ($normalized['price'] ?? 0) + (float) ($normalized['profit'] ?? 0)); + $providerActive = $useLast || $providerRaw > 0; + $product->setAttribute('reference_provider_active', $providerActive); + + if (! $useLast && $providerRaw <= 0) { + $this->zeroProductPrice($product); + + return; + } + $computed = $zaarbox->computeAdjustedProductPrice($product, $row); if ($computed <= 0) { if ($useLast) { @@ -441,11 +466,10 @@ class ProductDisplayPriceService /** * نمایش در اپ کاربر: - * • خاموش (active=false در DB) → false - * • خارج از ساعات معامله → false + * • خاموش (show=false در DB) → false * • قیمت نمایشی بعد از enrich → true - * • قیمت صفر و use_last_price_if_zero خاموش → false - * • قیمت صفر و use_last_price_if_zero روشن و fallback موجود → true + * • قیمت صفر و show_last_price خاموش → false + * • قیمت صفر و show_last_price روشن و fallback موجود → true */ public function resolveCustomerShow(Product $product): bool { @@ -461,10 +485,11 @@ class ProductDisplayPriceService return true; } - if (! $this->productUsesLastPriceIfZero($product)) { + if (! app(ConfigService::class)->getBool('show_last_price', false)) { return false; } - return $this->resolveLastPriceFallback($product) > 0; + return $this->resolveLastPriceFallback($product) > 0 + || $this->resolveHistoricalBasePrice() > 0; } } diff --git a/app/Services/ProductPriceService.php b/app/Services/ProductPriceService.php index 1b5b849..ea536d1 100644 --- a/app/Services/ProductPriceService.php +++ b/app/Services/ProductPriceService.php @@ -51,10 +51,6 @@ class ProductPriceService $data['price_profit'] = 0; } - if (\Illuminate\Support\Facades\Schema::hasColumn($product->getTable(), 'show')) { - $data['show'] = self::resolveShowAfterPriceSync($product, $adjustedPrice); - } - Product::where('id', $product->id)->update($data); return [ @@ -71,19 +67,6 @@ class ProductPriceService ]; } - private static function resolveShowAfterPriceSync(Product $product, int $adjustedPrice): bool - { - if (! ($product->active ?? true)) { - return false; - } - - if (! app(ConfigService::class)->isProductWithinTradingHours($product)) { - return false; - } - - return $adjustedPrice > 0; - } - /** وقتی فقط درصد در ادمین عوض می‌شود (بدون sync جدید) */ public static function reapplyOffsetPercent(Product $product, float $newOffsetPercent, ?float $oldOffsetPercent = null): void { From 2a91837c2bec1f8bda211a09c878f0b4b24f9739 Mon Sep 17 00:00:00 2001 From: adonis Date: Mon, 22 Jun 2026 19:50:45 +0330 Subject: [PATCH 03/19] fixed show true when price is 0 3 --- app/Services/ProductDisplayPriceService.php | 5 +- app/Services/ZaarboxPriceService.php | 89 ++++++++++++++------- 2 files changed, 63 insertions(+), 31 deletions(-) diff --git a/app/Services/ProductDisplayPriceService.php b/app/Services/ProductDisplayPriceService.php index 3f37689..35443a6 100644 --- a/app/Services/ProductDisplayPriceService.php +++ b/app/Services/ProductDisplayPriceService.php @@ -289,8 +289,7 @@ class ProductDisplayPriceService return; } - $normalized = $zaarbox->normalizeFeedRow($row); - $providerRaw = (int) round((float) ($normalized['price'] ?? 0) + (float) ($normalized['profit'] ?? 0)); + $providerRaw = $zaarbox->resolveReferenceProviderTotal($row); $providerActive = $useLast || $providerRaw > 0; $product->setAttribute('reference_provider_active', $providerActive); @@ -435,7 +434,7 @@ class ProductDisplayPriceService $row = app(ZaarboxPriceService::class)->normalizeFeedRow($row); - return (int) round((float) ($row['price'] ?? 0) + (float) ($row['profit'] ?? 0)); + return app(ZaarboxPriceService::class)->resolveReferenceProviderTotal($row); } private function isMeltedTradeProduct(Product $product): bool diff --git a/app/Services/ZaarboxPriceService.php b/app/Services/ZaarboxPriceService.php index 1b0be70..9437e3b 100644 --- a/app/Services/ZaarboxPriceService.php +++ b/app/Services/ZaarboxPriceService.php @@ -221,9 +221,7 @@ class ZaarboxPriceService return $empty; } - $base = (float) ($row['price'] ?? 0); - $profit = (float) ($row['profit'] ?? 0); - $orderPrice = (int) round($base + $profit); + $orderPrice = $this->resolveReferenceProviderTotal($row); return ['price' => $orderPrice, 'order_price' => $orderPrice, 'raw' => $row]; } catch (\Throwable $e) { @@ -339,7 +337,7 @@ class ZaarboxPriceService return $empty; } - $orderPrice = (int) round((float) ($row['price'] ?? 0) + (float) ($row['profit'] ?? 0)); + $orderPrice = $this->resolveReferenceProviderTotal($row); return ['order_price' => $orderPrice, 'raw' => $row]; } catch (\Throwable $e) { @@ -386,23 +384,24 @@ class ZaarboxPriceService } $row = $this->normalizeFeedRow($RPrice); + $parts = $this->resolveReferenceBaseProfit($row); if (($row['name'] ?? '') === 'TGJU فلز مس') { - $copper = ($row['price'] ?? 0) + ($row['profit'] ?? 0); + $copper = $parts['price'] + $parts['profit']; } if (($row['name'] ?? '') === 'TGJU دلار') { - $dollar = ($row['price'] ?? 0) + ($row['profit'] ?? 0); + $dollar = $parts['price'] + $parts['profit']; } ProductPriceService::syncFromReference( $product, - (int) $row['price'], - (int) ($row['profit'] ?? 0) + $parts['price'], + $parts['profit'] ); $productPrice = (float) ProductPriceService::adjustedTotal( - (int) $row['price'], - (int) ($row['profit'] ?? 0), + $parts['price'], + $parts['profit'], ProductPriceService::offsetPercent($product) ); @@ -477,22 +476,6 @@ class ZaarboxPriceService return null; } - /** - * قیمت نمایشی محصول از یک ردیف فید + offset درصد — بدون نوشتن DB - * - * @param array $row - */ - public function computeAdjustedProductPrice(Product $product, array $row): int - { - $normalized = $this->normalizeFeedRow($row); - - return ProductPriceService::adjustedTotal( - (int) ($normalized['price'] ?? 0), - (int) ($normalized['profit'] ?? 0), - ProductPriceService::offsetPercent($product) - ); - } - /** @param array $row */ public function normalizeFeedRow(array $row): array { @@ -503,6 +486,55 @@ class ZaarboxPriceService return $row; } + /** + * base/profit مرجع zaarbox — وقتی price صفر است provider قیمت نداده (profit مقدار قدیمی است). + * + * @param array $row + * @return array{price: int, profit: int} + */ + public function resolveReferenceBaseProfit(array $row): array + { + $normalized = $this->normalizeFeedRow($row); + $price = (int) round((float) ($normalized['price'] ?? 0)); + if ($price <= 0) { + return ['price' => 0, 'profit' => 0]; + } + + return [ + 'price' => $price, + 'profit' => (int) round((float) ($normalized['profit'] ?? 0)), + ]; + } + + /** + * @param array $row + */ + public function resolveReferenceProviderTotal(array $row): int + { + $parts = $this->resolveReferenceBaseProfit($row); + + return $parts['price'] + $parts['profit']; + } + + /** + * قیمت نمایشی محصول از یک ردیف فید + offset درصد — بدون نوشتن DB + * + * @param array $row + */ + public function computeAdjustedProductPrice(Product $product, array $row): int + { + $parts = $this->resolveReferenceBaseProfit($row); + if ($parts['price'] <= 0) { + return 0; + } + + return ProductPriceService::adjustedTotal( + $parts['price'], + $parts['profit'], + ProductPriceService::offsetPercent($product) + ); + } + public function syncProductFromApi(Product $product): bool { if (!Product::hasStoredPriceColumns()) { @@ -524,11 +556,12 @@ class ZaarboxPriceService } $row = $this->normalizeFeedRow($RPrice); + $parts = $this->resolveReferenceBaseProfit($row); ProductPriceService::syncFromReference( $product, - (int) $row['price'], - (int) ($row['profit'] ?? 0) + $parts['price'], + $parts['profit'] ); return true; From 6ebd02beb853b0c1322ddf9a028198847a1c18e4 Mon Sep 17 00:00:00 2001 From: adonis Date: Mon, 22 Jun 2026 20:09:56 +0330 Subject: [PATCH 04/19] fixed show true when price is 0 4 --- .../Controllers/Admin/ProductController.php | 4 ++++ app/Models/Product.php | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/app/Http/Controllers/Admin/ProductController.php b/app/Http/Controllers/Admin/ProductController.php index 42d90b6..5526f4c 100644 --- a/app/Http/Controllers/Admin/ProductController.php +++ b/app/Http/Controllers/Admin/ProductController.php @@ -612,6 +612,10 @@ class ProductController extends SuperController unset($data['direct_price']); } + if (!Product::hasInventoryColumn()) { + unset($data['inventory']); + } + return $data; } } diff --git a/app/Models/Product.php b/app/Models/Product.php index 135c751..56ce3a9 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -22,6 +22,7 @@ class Product extends Model private static ?bool $useLastPriceIfZeroColumnExists = null; private static ?bool $directPriceColumnExists = null; private static ?bool $tradeByQuantityColumnExists = null; + private static ?bool $inventoryColumnExists = null; protected static function booted(): void { @@ -48,6 +49,9 @@ class Product extends Model if (!static::hasTradeByQuantityColumn()) { unset($product->attributes['trade_by_quantity']); } + if (!static::hasInventoryColumn()) { + unset($product->attributes['inventory']); + } }); } @@ -175,6 +179,24 @@ class Product extends Model return static::$tradeByQuantityColumnExists; } + public static function hasInventoryColumn(): bool + { + if (static::$inventoryColumnExists !== null) { + return static::$inventoryColumnExists; + } + + try { + static::$inventoryColumnExists = Schema::hasColumn( + (new static)->getTable(), + 'inventory' + ); + } catch (\Throwable) { + static::$inventoryColumnExists = false; + } + + return static::$inventoryColumnExists; + } + public function orderLimitsEnabled(): bool { if (!static::hasOrderLimitColumns() || !($this->order_limit_enabled ?? false)) { From e8d17c1c044710e8b55a208123ef6d92f3b57cc5 Mon Sep 17 00:00:00 2001 From: adonis Date: Mon, 22 Jun 2026 21:29:25 +0330 Subject: [PATCH 05/19] =?UTF-8?q?fixed=20=D8=B9=D8=AF=D9=87=D9=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/Controller.php | 82 ++++++++++++++++++- .../Controllers/Customer/WalletController.php | 55 +++++++++---- .../rooyesh/wallet/src/models/Transaction.php | 11 ++- packages/rooyesh/wallet/src/models/Wallet.php | 8 +- 4 files changed, 136 insertions(+), 20 deletions(-) diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index adc83ec..e1cb245 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -416,6 +416,86 @@ class Controller extends BaseController return in_array($group, ['سکه', 'گوی', 'مکعب', 'شمش', 'سکه پارسیان'], true); } + public static function productUnitPayload($product): array + { + if (!$product) { + return []; + } + + if (is_array($product)) { + return $product; + } + + return [ + 'unit' => $product->unit ?? null, + 'direct_price' => $product->direct_price ?? null, + 'group_type' => $product->group_type ?? null, + 'amount' => $product->amount ?? null, + 'trade_by_quantity' => $product->trade_by_quantity ?? null, + ]; + } + + public static function convertWeight(float $value, ?string $fromUnit, ?string $toUnit): float + { + $from = trim((string) ($fromUnit ?? '')); + $to = trim((string) ($toUnit ?? '')); + + if ($from === '' || $to === '' || $from === $to) { + return $value; + } + + $grams = self::weightToGrams($value, $from); + + return $to === 'کیلوگرم' ? $grams / 1000 : $grams; + } + + /** + * مقدار و واحد نمایشی تراکنش — منبع حقیقت: unit محصول در ادمین. + * معامله مقداری + وزن دارد: تعداد ذخیره‌شده × وزن هر واحد. + */ + public static function resolveTransactionDisplayGoldAndUnit(?array $payload, float $gold, bool $onMainWallet = false): array + { + if ($payload === []) { + return [$gold, self::tradeWeightUnit()]; + } + + $sign = $gold < 0 ? -1 : 1; + $absGold = abs($gold); + + if ( + self::usesQuantityTrade($payload) + && self::hasProductUnitWeight($payload) + && self::isWeightBasedProductUnit($payload['unit'] ?? null) + ) { + $factor = (float) ($payload['amount'] ?? 1); + + return [ + round($absGold * $factor, 3) * $sign, + trim((string) $payload['unit']), + ]; + } + + $unit = self::productQuantityUnit($payload); + $productWeightUnit = self::isWeightBasedProductUnit($payload['unit'] ?? null) + ? trim((string) $payload['unit']) + : null; + + if ($onMainWallet && $productWeightUnit) { + $tradeUnit = self::tradeWeightUnit(); + + if ($productWeightUnit !== $tradeUnit) { + return [ + round(self::convertWeight($absGold, $tradeUnit, $productWeightUnit), 3) * $sign, + $productWeightUnit, + ]; + } + + return [$gold, $productWeightUnit]; + } + + return [$gold, $unit]; + } + public static function productQuantityUnit(?array $product): string { $unit = trim((string) ($product['unit'] ?? '')); @@ -427,7 +507,7 @@ class Controller extends BaseController return 'عدد'; } - return 'گرم'; + return self::tradeWeightUnit(); } /** مطابق ادمین: وزن «دارد» => amount > 0 ؛ «ندارد» => amount = 0 */ diff --git a/app/Http/Controllers/Customer/WalletController.php b/app/Http/Controllers/Customer/WalletController.php index bf1de39..0bafceb 100644 --- a/app/Http/Controllers/Customer/WalletController.php +++ b/app/Http/Controllers/Customer/WalletController.php @@ -137,11 +137,7 @@ class WalletController extends Controller } else { $record->title = ($record->gold > 0 ? self::productBuyTitle($wallet->product->title) : self::productSellTitle($wallet->product->title)); } - $record->unit = self::productQuantityUnit([ - 'unit' => $wallet->product->unit ?? null, - 'direct_price' => $wallet->product->direct_price ?? null, - 'group_type' => $wallet->product->group_type ?? null, - ]); + $record->unit = self::productQuantityUnit(self::productUnitPayload($wallet->product)); } else { $record->title = $this->resolveMainWalletTransactionTitle($record); } @@ -560,30 +556,55 @@ class WalletController extends Controller } if (!empty($record->wallet?->product)) { - $record->unit = self::productQuantityUnit([ - 'unit' => $record->wallet->product->unit ?? null, - 'direct_price' => $record->wallet->product->direct_price ?? null, - 'group_type' => $record->wallet->product->group_type ?? null, - ]); + $this->applyProductTransactionUnit($record); } } + private function resolveTransactionProduct($record): ?Product + { + $partial = $record->wallet->product ?? $record->product ?? null; + $productId = (int) ($partial->id ?? $record->product_id ?? 0); + + if ($productId <= 0) { + return $partial instanceof Product ? $partial : null; + } + + return Product::query() + ->select([ + 'id', + 'title', + 'group_type', + 'unit', + 'direct_price', + 'amount', + 'trade_by_quantity', + 'metal_id', + ]) + ->find($productId) ?? ($partial instanceof Product ? $partial : null); + } + private function applyProductTransactionUnit($record): void { - $product = $record->wallet->product ?? $record->product ?? null; + $product = $this->resolveTransactionProduct($record); + if (!$product) { - if (!empty($record->gold) && empty($record->wallet?->product)) { + if (!empty($record->gold)) { $record->unit = self::tradeWeightUnit(); } return; } - $record->unit = self::productQuantityUnit([ - 'unit' => $product->unit ?? null, - 'direct_price' => $product->direct_price ?? null, - 'group_type' => $product->group_type ?? null, - ]); + $payload = self::productUnitPayload($product); + $onMainWallet = empty($record->wallet?->product_id); + [$displayGold, $unit] = self::resolveTransactionDisplayGoldAndUnit( + $payload, + (float) ($record->gold ?? 0), + $onMainWallet + ); + + $record->gold = $displayGold; + $record->unit = $unit; } private function isProductWalletTransaction($record): bool diff --git a/packages/rooyesh/wallet/src/models/Transaction.php b/packages/rooyesh/wallet/src/models/Transaction.php index 3fab637..9843a8f 100644 --- a/packages/rooyesh/wallet/src/models/Transaction.php +++ b/packages/rooyesh/wallet/src/models/Transaction.php @@ -43,7 +43,16 @@ class Transaction extends Model } public function product() { - return $this->belongsTo('App\Models\Product','product_id')->select(['id','title','group_type']); + return $this->belongsTo('App\Models\Product', 'product_id')->select([ + 'id', + 'title', + 'group_type', + 'unit', + 'direct_price', + 'amount', + 'trade_by_quantity', + 'metal_id', + ]); } public function order() { diff --git a/packages/rooyesh/wallet/src/models/Wallet.php b/packages/rooyesh/wallet/src/models/Wallet.php index f1d1e88..763761e 100644 --- a/packages/rooyesh/wallet/src/models/Wallet.php +++ b/packages/rooyesh/wallet/src/models/Wallet.php @@ -22,7 +22,13 @@ class Wallet extends Model } public function Product() { - return $this->belongsTo('App\Models\Product','product_id'); + return $this->belongsTo('App\Models\Product', 'product_id'); + } + + /** @alias Product */ + public function product() + { + return $this->Product(); } /** کیف اصلی مشتری — بدون fund و بدون product */ From f13ffe0dff2db844d39ddf3f1b25b313d922e82b Mon Sep 17 00:00:00 2001 From: adonis Date: Mon, 22 Jun 2026 21:31:21 +0330 Subject: [PATCH 06/19] fixed unit --- packages/rooyesh/wallet/src/models/Wallet.php | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/rooyesh/wallet/src/models/Wallet.php b/packages/rooyesh/wallet/src/models/Wallet.php index 763761e..e74f9fc 100644 --- a/packages/rooyesh/wallet/src/models/Wallet.php +++ b/packages/rooyesh/wallet/src/models/Wallet.php @@ -20,15 +20,9 @@ class Wallet extends Model { return $this->belongsTo('App\Models\Fund','fund_id')->select(['id','title']); } - public function Product() - { - return $this->belongsTo('App\Models\Product', 'product_id'); - } - - /** @alias Product */ public function product() { - return $this->Product(); + return $this->belongsTo('App\Models\Product', 'product_id'); } /** کیف اصلی مشتری — بدون fund و بدون product */ From bd36b74dad0a12a55f40130a159b98c6b3525bff Mon Sep 17 00:00:00 2001 From: adonis Date: Mon, 22 Jun 2026 21:42:48 +0330 Subject: [PATCH 07/19] fixed product title --- .../Controllers/Customer/WalletController.php | 113 ++++++++++++++---- .../Single/SingleOrderController.php | 49 ++++---- 2 files changed, 113 insertions(+), 49 deletions(-) diff --git a/app/Http/Controllers/Customer/WalletController.php b/app/Http/Controllers/Customer/WalletController.php index 0bafceb..be80518 100644 --- a/app/Http/Controllers/Customer/WalletController.php +++ b/app/Http/Controllers/Customer/WalletController.php @@ -402,17 +402,12 @@ class WalletController extends Controller return; } - $productId = $this->resolveAbshodeProductId($record); - if (!$productId) { - return; - } - - $productTitle = $record->product->title - ?? Product::query()->whereKey($productId)->value('title'); - if (!$productTitle) { + $product = $this->resolveTransactionProduct($record); + if (!$product || trim((string) $product->title) === '') { return; } + $productTitle = trim((string) $product->title); $direction = (int) ($record->order->direction ?? 0); if ($direction === 1 || ($direction === 0 && (float) ($record->gold ?? 0) > 0)) { $record->title = self::productBuyTitle($productTitle); @@ -430,9 +425,14 @@ class WalletController extends Controller return (int) $record->product_id; } - $note = (string) ($record->note ?? ''); - if ($note !== '' && preg_match('/(?:^|;)product_id:(\d+)/', $note, $matches)) { - return (int) $matches[1]; + $noteProductId = $this->productIdFromTransactionNote($record->note ?? null); + if ($noteProductId) { + return $noteProductId; + } + + $wallet = $this->resolveTransactionWallet($record); + if ($wallet && !empty($wallet->product_id)) { + return (int) $wallet->product_id; } if (empty($record->order_id)) { @@ -445,7 +445,33 @@ class WalletController extends Controller ->where('product_id', '>', 0) ->value('product_id'); - return $siblingProductId ? (int) $siblingProductId : null; + if ($siblingProductId) { + return (int) $siblingProductId; + } + + $siblingNotes = Transaction::query() + ->where('order_id', $record->order_id) + ->whereNotNull('note') + ->pluck('note'); + + foreach ($siblingNotes as $note) { + $productId = $this->productIdFromTransactionNote($note); + if ($productId) { + return $productId; + } + } + + return null; + } + + private function productIdFromTransactionNote(?string $note): ?int + { + $note = (string) ($note ?? ''); + if ($note === '' || !preg_match('/(?:^|;)product_id:(\d+)/', $note, $matches)) { + return null; + } + + return (int) $matches[1]; } private function applyMetalTypeTransactionTitle($record): void @@ -458,7 +484,7 @@ class WalletController extends Controller return; } - $wallet = $record->wallet ?? null; + $wallet = $this->resolveTransactionWallet($record); if ($wallet && !empty($wallet->product)) { return; } @@ -488,18 +514,12 @@ class WalletController extends Controller return; } - $productId = $this->resolveAbshodeProductId($record); - if (!$productId) { - return; - } - - $productTitle = $record->product->title - ?? $record->wallet->product->title - ?? Product::query()->whereKey($productId)->value('title'); - if (!$productTitle) { + $product = $this->resolveTransactionProduct($record); + if (!$product || trim((string) $product->title) === '') { return; } + $productTitle = trim((string) $product->title); $expectedBuy = self::productBuyTitle($productTitle); $expectedSell = self::productSellTitle($productTitle); @@ -515,11 +535,28 @@ class WalletController extends Controller if ($direction === 2 || ($direction === 0 && (float) ($record->gold ?? 0) < 0)) { $record->title = $expectedSell; + return; + } + + if (self::isGenericMetalTransactionTitle($record->title)) { + $record->title = $expectedBuy; } } private function resolveMainWalletTransactionTitle($record): string { + $product = $this->resolveTransactionProduct($record); + if ($product && trim((string) $product->title) !== '') { + $productTitle = trim((string) $product->title); + if ((int) $record->type === 1) { + return self::productBuyTitle($productTitle); + } + + if ((int) $record->type === 2) { + return self::productSellTitle($productTitle); + } + } + if ((int) $record->type === 1) { return self::meltedGoldBuyTitle(); } @@ -560,15 +597,39 @@ class WalletController extends Controller } } + private function resolveTransactionWallet($record): ?Wallet + { + if ($record instanceof Transaction) { + $relation = $record->getRelationValue('wallet'); + if ($relation instanceof Wallet) { + return $relation; + } + } + + $walletId = (int) ($record->getAttributes()['wallet'] ?? 0); + if ($walletId <= 0) { + return null; + } + + return Wallet::with('product')->find($walletId); + } + private function resolveTransactionProduct($record): ?Product { - $partial = $record->wallet->product ?? $record->product ?? null; - $productId = (int) ($partial->id ?? $record->product_id ?? 0); + $productId = $this->resolveAbshodeProductId($record) ?? 0; + $wallet = $this->resolveTransactionWallet($record); if ($productId <= 0) { + $partial = $wallet?->product ?? $record->product ?? null; + return $partial instanceof Product ? $partial : null; } + $partial = $record->product ?? $wallet?->product ?? null; + if ($partial instanceof Product && (int) $partial->id === $productId && trim((string) $partial->title) !== '') { + return $partial; + } + return Product::query() ->select([ 'id', @@ -609,7 +670,9 @@ class WalletController extends Controller private function isProductWalletTransaction($record): bool { - return !empty($record->wallet?->product); + $wallet = $this->resolveTransactionWallet($record); + + return !empty($wallet?->product); } /** diff --git a/app/Http/Controllers/Single/SingleOrderController.php b/app/Http/Controllers/Single/SingleOrderController.php index 33ce376..c91244f 100644 --- a/app/Http/Controllers/Single/SingleOrderController.php +++ b/app/Http/Controllers/Single/SingleOrderController.php @@ -249,6 +249,7 @@ class SingleOrderController extends Controller } $data['order'] = $order = Order::create($data); $abshode = $this->resolveAbshodeProductContext($data); + $productId = (int) ($data['product_id'] ?? $abshode['id'] ?? 0); $buyTransaction = [ "cash" => $data['payment_amount'], "gold" => $data['amount'], @@ -256,13 +257,11 @@ class SingleOrderController extends Controller 'active' => true, 'order_id' => $order->id, 'start_at' => now(), - 'title' => $abshode['title'] !== '' - ? self::productBuyTitle($abshode['title']) - : self::meltedGoldBuyTitle(), + 'title' => $this->meltedOrProductBuyTitle($data), ]; - if ($abshode['id'] > 0) { - $buyTransaction['product_id'] = $abshode['id']; - $buyTransaction['note'] = 'product_id:' . $abshode['id']; + if ($productId > 0) { + $buyTransaction['product_id'] = $productId; + $buyTransaction['note'] = 'product_id:' . $productId; } new TransactionInterface($wallet_interface, $buyTransaction, 1, true, false); $response['1'] = 2; @@ -316,7 +315,7 @@ class SingleOrderController extends Controller 'active' => true, 'order_id' => $order->id, 'start_at' => now(), - 'title' => self::productBuyTitle($data['product']['title'] ?? ''), + 'title' => $this->meltedOrProductBuyTitle($data), ], 1, true, false); @@ -408,7 +407,7 @@ class SingleOrderController extends Controller 'active' => true, 'order_id' => $order->id, 'start_at' => now(), - 'title' => self::productBuyTitle($data['product']['title'] ?? ''), + 'title' => $this->meltedOrProductBuyTitle($data), ], 1, true, false); } @@ -429,19 +428,18 @@ class SingleOrderController extends Controller } $data['order'] = $order = Order::create($data); $abshode = $this->resolveAbshodeProductContext($data); + $productId = (int) ($data['product_id'] ?? $abshode['id'] ?? 0); $sellTransaction = [ "gold" => $data['amount'], "cash" => $data['total_price'], 'expire_at' => now(), 'active' => true, 'order_id' => $order->id, - 'title' => $abshode['title'] !== '' - ? self::productSellTitle($abshode['title']) - : self::meltedGoldSellTitle(), + 'title' => $this->meltedOrProductSellTitle($data), ]; - if ($abshode['id'] > 0) { - $sellTransaction['product_id'] = $abshode['id']; - $sellTransaction['note'] = 'product_id:' . $abshode['id']; + if ($productId > 0) { + $sellTransaction['product_id'] = $productId; + $sellTransaction['note'] = 'product_id:' . $productId; } new TransactionInterface($wallet_interface, $sellTransaction, 2, false, true); @@ -479,7 +477,7 @@ class SingleOrderController extends Controller 'active' => true, 'order_id' => $order->id, 'start_at' => now(), - 'title' => self::productSellTitle($data['product']['title'] ?? ''), + 'title' => $this->meltedOrProductSellTitle($data), ], 1, false, false); $data['transaction'][] = new TransactionInterface($wallet_interface, $_inp = [ "number" => $data['number'], @@ -520,7 +518,7 @@ class SingleOrderController extends Controller 'expire_at' => now(), 'active' => true, 'order_id' => $order->id, - 'title' => self::mainMetalBuyTitle(), + 'title' => $this->meltedOrProductSellTitle($data), ], 12, true, false); $this->normalizeProductWalletGold($wallet_interface_product); new TransactionInterface($wallet_interface_product, [ @@ -530,7 +528,7 @@ class SingleOrderController extends Controller 'active' => true, 'order_id' => $order->id, 'start_at' => now(), - 'title' => self::productSellTitle($data['product']['title'] ?? ''), + 'title' => $this->meltedOrProductSellTitle($data), ], 1, false, false); } @@ -890,17 +888,20 @@ class SingleOrderController extends Controller private function productTitleFromData(array $data): ?string { $product = $data['product'] ?? null; - if (!$product) { - return null; - } + $title = null; - if (is_array($product)) { - $title = $product['title'] ?? null; - } else { - $title = $product->title ?? null; + if ($product) { + if (is_array($product)) { + $title = $product['title'] ?? null; + } else { + $title = $product->title ?? null; + } } $title = trim((string) $title); + if ($title === '' && !empty($data['product_id'])) { + $title = trim((string) Product::query()->whereKey((int) $data['product_id'])->value('title')); + } return $title !== '' ? $title : null; } From adeb5d7c9d58b9e88e8090fc79ba5529a5e42fbd Mon Sep 17 00:00:00 2001 From: adonis Date: Tue, 23 Jun 2026 14:04:27 +0330 Subject: [PATCH 08/19] fixed trade by quantity --- app/Http/Controllers/Controller.php | 4 ++++ app/Services/AssetSummaryService.php | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index e1cb245..223d69e 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -400,6 +400,10 @@ class Controller extends BaseController return false; } + if (self::usesQuantityTrade($product)) { + return false; + } + if (self::isWeightBasedProductUnit($product['unit'] ?? null)) { return false; } diff --git a/app/Services/AssetSummaryService.php b/app/Services/AssetSummaryService.php index 38fda1b..db7523c 100644 --- a/app/Services/AssetSummaryService.php +++ b/app/Services/AssetSummaryService.php @@ -532,11 +532,16 @@ class AssetSummaryService 'direct_price' => $product->direct_price ?? null, 'group_type' => $product->group_type ?? null, 'amount' => $product->amount ?? null, + 'trade_by_quantity' => $product->trade_by_quantity ?? null, ]; } private function productUnitWeightLabel(Product $product): ?string { + if (!$this->isCountBasedProduct($product)) { + return null; + } + if (!Controller::hasProductUnitWeight($this->productPayload($product))) { return null; } From ea4e8d73a5a87f96ead7efbb79e92cee86f8dd85 Mon Sep 17 00:00:00 2001 From: adonis Date: Tue, 23 Jun 2026 14:20:45 +0330 Subject: [PATCH 09/19] fixed upload file --- app/Http/Controllers/Admin/FileController.php | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/app/Http/Controllers/Admin/FileController.php b/app/Http/Controllers/Admin/FileController.php index c4676c8..2e0088b 100644 --- a/app/Http/Controllers/Admin/FileController.php +++ b/app/Http/Controllers/Admin/FileController.php @@ -14,16 +14,20 @@ class FileController extends Controller * * @return string */ - protected function createDir() + protected function createDir(): string { $now = Verta(); $path = preg_replace('/-/', '/', $now->formatDate()); - if (!Storage::exists($path)) { - Storage::makeDirectory($path); - Storage::makeDirectory($path . '/' . 'thumb'); + $disk = Storage::disk('public'); + + if (! $disk->exists($path)) { + $disk->makeDirectory($path); + $disk->makeDirectory($path.'/thumb'); } + return $path; } + public function store(Request $request) { // Store FIle in Public Storage By Date @@ -31,17 +35,17 @@ class FileController extends Controller $data = $request->all(); $path = $this->createDir(); - $data['original_name'] = $request->file('file')->getClientOriginalName(); - $data['ext'] = $request->file('file')->getClientOriginalExtension(); + $uploaded = $request->file('file'); + $data['original_name'] = $uploaded->getClientOriginalName(); + $data['ext'] = $uploaded->getClientOriginalExtension(); if (!isset($data['title']) || is_null($data['title'])) { $data['title'] = $data['original_name']; } $file_name_src = md5(time() . $data['original_name']); $file_name = $file_name_src . '.' . $data['ext']; - $file = $request->file('file'); $data['has_thumb'] = false; - $data['size'] = $file->getSize(); - $file->move(public_path('storage/' . $path . '/'), $file_name); + $data['size'] = $uploaded->getSize(); + Storage::disk('public')->putFileAs($path, $uploaded, $file_name); $data['src'] = 'storage/' . $path . '/' . $file_name; // $data['size'] = $file->getSize(); $data['user_id'] = auth()->id(); From c7638918ff254e8aa01fffed9feefe39cf3a3320 Mon Sep 17 00:00:00 2001 From: adonis Date: Tue, 23 Jun 2026 15:55:12 +0330 Subject: [PATCH 10/19] fixed upload file 2 --- app/Services/ProductDisplayPriceService.php | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/app/Services/ProductDisplayPriceService.php b/app/Services/ProductDisplayPriceService.php index 35443a6..59cab0b 100644 --- a/app/Services/ProductDisplayPriceService.php +++ b/app/Services/ProductDisplayPriceService.php @@ -30,7 +30,22 @@ class ProductDisplayPriceService return false; } - return $this->isMeltedTradeProduct($product); + return $this->isMeltedTradeProduct($product) + || $this->usesQuantityAppPrice($product); + } + + /** معامله مقداری با قیمت اپ — نه قیمت دستی قدیمی با price پر در DB */ + private function usesQuantityAppPrice(Product $product): bool + { + if (! ($product->trade_by_quantity ?? false)) { + return false; + } + + if ($product->reading_method || (int) ($product->direct_price ?? 0) === 1) { + return false; + } + + return (int) ($product->price ?? 0) <= 0; } /** From 147a9681799a619737dfe31996bc0af1af0ef6b7 Mon Sep 17 00:00:00 2001 From: adonis Date: Tue, 23 Jun 2026 20:35:13 +0330 Subject: [PATCH 11/19] fixed taraz for all metals --- .../Controllers/Admin/DashboardController.php | 29 ++- app/Services/BalanceReportService.php | 222 ++++++++++++++++++ app/Services/CategoryService.php | 11 +- database/sql/deploy_settings_const.sql | 9 +- ...ngs_const_preserve_value_sync_can_edit.sql | 7 +- 5 files changed, 266 insertions(+), 12 deletions(-) create mode 100644 app/Services/BalanceReportService.php diff --git a/app/Http/Controllers/Admin/DashboardController.php b/app/Http/Controllers/Admin/DashboardController.php index 2a65961..028ad83 100644 --- a/app/Http/Controllers/Admin/DashboardController.php +++ b/app/Http/Controllers/Admin/DashboardController.php @@ -10,6 +10,7 @@ use App\Models\Price; use App\Models\Product; use App\Models\TransactionsAdmin; use App\Models\Withdraw; +use App\Services\BalanceReportService; use App\Services\ConfigService; use Carbon\Carbon; use Hekmatinasser\Verta\Verta; @@ -149,7 +150,7 @@ class DashboardController extends Controller return $cnt; } - public function getBalance(Request $request) + public function getBalance(Request $request, BalanceReportService $balanceReportService) { $startDate = Carbon::parse($request->startDate)->startOfDay(); $endDate = Carbon::parse($request->endDate)->endOfDay(); @@ -158,6 +159,12 @@ class DashboardController extends Controller $response["kalhor"]=[]; $excludeCustomerId = (int) app(ConfigService::class)->getInt('balance_report_exclude_customer_id', 4); + $catalogMetalIds = $balanceReportService->catalogMetalIds(); + if ($catalogMetalIds === []) { + $catalogMetalIds = [1]; + } + $limanMetalBuckets = $balanceReportService->initMetalBalanceBuckets($catalogMetalIds); + $kalhorMetalBuckets = $balanceReportService->initMetalBalanceBuckets($catalogMetalIds); $response["pay_at_update"] = Order::whereNull('pay_at')->update(['pay_at' => DB::raw('created_at')]); //liman @@ -171,6 +178,8 @@ class DashboardController extends Controller $response["liman"]["balance_abs"]["gold"] = 0; $response["liman"]["balance_abs"]["cash"] = 0; foreach ($response["liman"]["orders"] as $OrderGoldBalanceRow) { + $balanceReportService->applyOrderToBuckets($limanMetalBuckets, $OrderGoldBalanceRow); + if ($OrderGoldBalanceRow->direction == 1){ $response["liman"]["balance"]["gold"] += $OrderGoldBalanceRow->amount; $response["liman"]["balance"]["cash"] -= $OrderGoldBalanceRow->total_price; @@ -212,10 +221,22 @@ class DashboardController extends Controller $response["kalhor"]["balance"]['gold'] += $data['Count']; $response["kalhor"]["balance"]['cash'] -= $data['TotalPrice']; $direction =2; + $balanceReportService->applyKalhorSlice( + $kalhorMetalBuckets, + (float) $data['Count'], + (float) $data['TotalPrice'], + 2 + ); } elseif ($data['DealTypeStr'] == "فروش" or $data['DealTypeStr'] == "فروش _ دستی") { $response["kalhor"]["balance"]['gold'] -= $data['Count']; $response["kalhor"]["balance"]['cash'] += $data['TotalPrice']; $direction =1; + $balanceReportService->applyKalhorSlice( + $kalhorMetalBuckets, + (float) $data['Count'], + (float) $data['TotalPrice'], + 1 + ); } $response["kalhor"]["balance_abs"]['gold'] += $data['Count']; $response["kalhor"]["balance_abs"]['cash'] += $data['TotalPrice']; @@ -244,6 +265,12 @@ class DashboardController extends Controller //main $response['cashBalance']= -$response["liman"]["balance"]["cash"]+$response["kalhor"]["balance"]["cash"]; $response['goldBalance']= -$response["liman"]["balance"]["gold"]+$response["kalhor"]["balance"]["gold"]; + + $metalBalances = $balanceReportService->buildMetalBalancesResponse($limanMetalBuckets, $kalhorMetalBuckets); + $response['metalBalances'] = $metalBalances['items']; + if (!empty($metalBalances['items'])) { + $response['goldBalance'] = $metalBalances['totalGoldBalance']; + } //mazaneh $changeMesghal = config('change_mesghal'); diff --git a/app/Services/BalanceReportService.php b/app/Services/BalanceReportService.php new file mode 100644 index 0000000..7cc25a6 --- /dev/null +++ b/app/Services/BalanceReportService.php @@ -0,0 +1,222 @@ + 'طلا', + 2 => 'نقره', + 3 => 'مس', + ]; + + /** @return list */ + public function catalogMetalIds(): array + { + return Product::query() + ->where('active', 1) + ->whereNotNull('metal_id') + ->where('metal_id', '>', 0) + ->distinct() + ->orderBy('metal_id') + ->pluck('metal_id') + ->map(static fn ($id) => (int) $id) + ->values() + ->all(); + } + + public function metalLabel(int $metalId): string + { + return self::METAL_LABELS[$metalId] ?? "فلز {$metalId}"; + } + + /** + * @return array + */ + public function resolveOrderMetalSlices(Order $order): array + { + $order->loadMissing(['transactions.product']); + + $slices = []; + $txWithProduct = $order->transactions->filter( + static fn ($tx) => $tx->product_id && abs((float) ($tx->gold ?? 0)) > 0 + ); + + if ($txWithProduct->isNotEmpty()) { + foreach ($txWithProduct as $tx) { + $metalId = (int) ($tx->product?->metal_id ?? 1); + if ($metalId <= 0) { + $metalId = 1; + } + $gold = abs((float) ($tx->gold ?? 0)); + if ($gold <= 0) { + continue; + } + $slices[$metalId] = ($slices[$metalId] ?? 0.0) + $gold; + } + + $totalGold = array_sum($slices); + $result = []; + foreach ($slices as $metalId => $gold) { + $cashShare = $totalGold > 0 + ? (float) $order->total_price * ($gold / $totalGold) + : 0.0; + $result[(int) $metalId] = ['gold' => $gold, 'cash' => $cashShare]; + } + + return $result; + } + + $amount = (float) ($order->amount ?? 0); + $totalPrice = (float) ($order->total_price ?? 0); + if ($amount <= 0) { + return []; + } + + $metalId = 1; + $tx = $order->transactions->first(static fn ($t) => $t->product_id); + if ($tx?->product) { + $metalId = max(1, (int) ($tx->product->metal_id ?? 1)); + } + + return [$metalId => ['gold' => $amount, 'cash' => $totalPrice]]; + } + + /** + * @param list $metalIds + * @return array + */ + public function initMetalBalanceBuckets(array $metalIds): array + { + $buckets = []; + foreach ($metalIds as $metalId) { + $buckets[(int) $metalId] = [ + 'gold' => 0.0, + 'cash' => 0.0, + 'gold_abs' => 0.0, + 'cash_abs' => 0.0, + ]; + } + + return $buckets; + } + + /** + * @param array $buckets + */ + public function applyOrderToBuckets(array &$buckets, Order $order): void + { + $slices = $this->resolveOrderMetalSlices($order); + foreach ($slices as $metalId => $slice) { + $metalId = (int) $metalId; + if (! isset($buckets[$metalId])) { + $buckets[$metalId] = [ + 'gold' => 0.0, + 'cash' => 0.0, + 'gold_abs' => 0.0, + 'cash_abs' => 0.0, + ]; + } + + $gold = (float) $slice['gold']; + $cash = (float) $slice['cash']; + + if ((int) $order->direction === 1) { + $buckets[$metalId]['gold'] += $gold; + $buckets[$metalId]['cash'] -= $cash; + } elseif ((int) $order->direction === 2) { + $buckets[$metalId]['gold'] -= $gold; + $buckets[$metalId]['cash'] += $cash; + } + + $buckets[$metalId]['gold_abs'] += $gold; + $buckets[$metalId]['cash_abs'] += $cash; + } + } + + /** + * @param array $limanBuckets + * @param array $kalhorBuckets + */ + public function buildMetalBalancesResponse(array $limanBuckets, array $kalhorBuckets): array + { + $metalIds = array_values(array_unique(array_merge( + array_keys($limanBuckets), + array_keys($kalhorBuckets) + ))); + sort($metalIds); + + $items = []; + $totalGoldBalance = 0.0; + + foreach ($metalIds as $metalId) { + $metalId = (int) $metalId; + $liman = $limanBuckets[$metalId] ?? [ + 'gold' => 0.0, + 'cash' => 0.0, + 'gold_abs' => 0.0, + 'cash_abs' => 0.0, + ]; + $kalhor = $kalhorBuckets[$metalId] ?? [ + 'gold' => 0.0, + 'cash' => 0.0, + 'gold_abs' => 0.0, + 'cash_abs' => 0.0, + ]; + + $goldBalance = -$liman['gold'] + $kalhor['gold']; + $cashBalance = -$liman['cash'] + $kalhor['cash']; + $goldBalanceOfLiman = -$liman['gold']; + $goldBalanceOfKalhor = $kalhor['gold']; + + $items[] = [ + 'metal_id' => $metalId, + 'label' => $this->metalLabel($metalId), + 'balance' => round($goldBalance, 4), + 'goldBalance' => round($goldBalance, 4), + 'cashBalance' => round($cashBalance, 2), + 'goldBalanceOfLiman' => round($goldBalanceOfLiman, 4), + 'goldBalanceOfKalhor' => round($goldBalanceOfKalhor, 4), + ]; + + $totalGoldBalance += $goldBalance; + } + + return [ + 'items' => $items, + 'totalGoldBalance' => round($totalGoldBalance, 4), + ]; + } + + /** @param array $buckets */ + public function applyKalhorSlice( + array &$buckets, + float $gold, + float $cash, + int $direction, + int $metalId = 1 + ): void { + if (! isset($buckets[$metalId])) { + $buckets[$metalId] = [ + 'gold' => 0.0, + 'cash' => 0.0, + 'gold_abs' => 0.0, + 'cash_abs' => 0.0, + ]; + } + + if ($direction === 2) { + $buckets[$metalId]['gold'] += $gold; + $buckets[$metalId]['cash'] -= $cash; + } elseif ($direction === 1) { + $buckets[$metalId]['gold'] -= $gold; + $buckets[$metalId]['cash'] += $cash; + } + + $buckets[$metalId]['gold_abs'] += $gold; + $buckets[$metalId]['cash_abs'] += $cash; + } +} diff --git a/app/Services/CategoryService.php b/app/Services/CategoryService.php index 6c86ea8..6d5c923 100644 --- a/app/Services/CategoryService.php +++ b/app/Services/CategoryService.php @@ -97,27 +97,22 @@ class CategoryService { $names = $this->collectTargetCategoryNames(); $created = 0; - $order = 0; + $maxOrder = (int) Category::max('sort_order'); foreach ($names as $name) { - $order++; $existing = Category::where('name', $name)->first(); if ($existing) { - if ((int) $existing->sort_order !== $order) { - $existing->sort_order = $order; - $existing->save(); - } - continue; } + $maxOrder++; Category::create([ 'name' => $name, 'active' => 1, 'show_in_filter' => 1, 'show_in_select' => 1, - 'sort_order' => $order, + 'sort_order' => $maxOrder, ]); $created++; } diff --git a/database/sql/deploy_settings_const.sql b/database/sql/deploy_settings_const.sql index 617811f..3e3cb03 100644 --- a/database/sql/deploy_settings_const.sql +++ b/database/sql/deploy_settings_const.sql @@ -371,6 +371,10 @@ INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `de SELECT 'metal_type', 'طلای آبشده', 'عنوان فلز در UI', 0, 1, NULL WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'metal_type' LIMIT 1); +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'main_metal', 'طلا', 'نام فلز اصلی (ادمین)', 0, 1, NULL, 'text' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'main_metal' LIMIT 1); + -- درگاه — کلیدهای اختیاری payping INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`) SELECT 'payping_fin_sync_enabled', 'false', 'همگام‌سازی fin', 1, 1, NULL @@ -666,7 +670,7 @@ UPDATE `settings` SET `type` = 'text' WHERE `key` IN ( 'node_client_id', 'node_shared_secret', 'sse_path', 'tahesab_bank_account', 'telegram_bot_token', 'bale_bot_token', 'support_messenger_id', - 'referral_bonus_amount', 'balance_report_exclude_customer_id', 'metal_type', 'montify_name', 'price_extreme_date', + 'referral_bonus_amount', 'balance_report_exclude_customer_id', 'metal_type', 'main_metal', 'montify_name', 'price_extreme_date', 'default_vc_number', 'liman_token' ); @@ -922,6 +926,7 @@ UPDATE `settings` SET `description` = 'آدرس تصویر لوگو در صفح UPDATE `settings` SET `description` = 'عنوان کوتاه برای نصب PWA روی موبایل. از دیپلوی مدیریت می‌شود.' WHERE `key` = 'title_pwa'; UPDATE `settings` SET `description` = 'عنوان نمایش‌داده‌شده در تب مرورگر (favicon). از دیپلوی مدیریت می‌شود.' WHERE `key` = 'title_fav'; UPDATE `settings` SET `description` = 'عنوان فلز اصلی در رابط کاربر (مثلاً «طلای آبشده» یا «کاتد مس»). در دارایی، کیف پول و برچسب‌های معامله دیده می‌شود. فقط از دیپلوی/SQL تغییر می‌کند.' WHERE `key` = 'metal_type'; +UPDATE `settings` SET `title_fa` = 'نام فلز اصلی (ادمین)', `description` = 'نام کوتاه فلز برای برچسب‌های پنل ادمین (مثلاً «مس» یا «طلا»). «تراز طلایی» می‌شود «تراز مس» — نه «تراز مسی». فقط از دیپلوی/SQL تغییر می‌کند.' WHERE `key` = 'main_metal'; UPDATE `settings` SET `description` = 'واحد نمایش وزن در دارایی‌ها، کیف پول و جزئیات معاملات|گرم,کیلوگرم' WHERE `key` = 'unit'; UPDATE `settings` SET `description` = 'روشن: کاربران جدید می‌توانند ثبت‌نام کنند. خاموش: فقط ورود کاربران موجود مجاز است.' WHERE `key` = 'open_register'; UPDATE `settings` SET `description` = 'روشن: حساب کاربر بلافاصله پس از ثبت‌نام فعال می‌شود. خاموش: نیاز به تأیید دستی ادمین دارد.' WHERE `key` = 'auto_active_customer'; @@ -1066,7 +1071,7 @@ UPDATE `settings` SET `can_edit` = 0 WHERE `key` IN ( 'has_page_earn_money', 'is_store', 'logo_login', 'logo_register', 'title_pwa', 'title_fav', - 'main_product', 'main_wallet_products', 'metal_type', + 'main_product', 'main_wallet_products', 'metal_type', 'main_metal', 'montify_name', 'montzar_base_url', 'montzar_closed', 'montzar_diff', 'montzar_last_product_log', 'montzar_live_price', 'montzar_mazaneh', diff --git a/database/sql/deploy_settings_const_preserve_value_sync_can_edit.sql b/database/sql/deploy_settings_const_preserve_value_sync_can_edit.sql index 040e834..6af90fa 100644 --- a/database/sql/deploy_settings_const_preserve_value_sync_can_edit.sql +++ b/database/sql/deploy_settings_const_preserve_value_sync_can_edit.sql @@ -351,6 +351,10 @@ INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `de SELECT 'metal_type', 'طلای آبشده', 'عنوان فلز در UI', 0, 1, NULL WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'metal_type' LIMIT 1); +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'main_metal', 'طلا', 'نام فلز اصلی (ادمین)', 0, 1, NULL, 'text' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'main_metal' LIMIT 1); + -- درگاه — کلیدهای اختیاری payping INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`) SELECT 'payping_fin_sync_enabled', 'false', 'همگام‌سازی fin', 1, 1, NULL @@ -642,7 +646,7 @@ UPDATE `settings` SET `type` = 'text' WHERE `key` IN ( 'node_client_id', 'node_shared_secret', 'sse_path', 'tahesab_bank_account', 'telegram_bot_token', 'bale_bot_token', 'support_messenger_id', - 'referral_bonus_amount', 'balance_report_exclude_customer_id', 'metal_type', 'montify_name', 'price_extreme_date', + 'referral_bonus_amount', 'balance_report_exclude_customer_id', 'metal_type', 'main_metal', 'montify_name', 'price_extreme_date', 'default_vc_number', 'liman_token' ); @@ -926,6 +930,7 @@ UPDATE `settings` SET `description` = 'آدرس تصویر لوگو در صفح UPDATE `settings` SET `description` = 'عنوان کوتاه برای نصب PWA روی موبایل. از دیپلوی مدیریت می‌شود.' WHERE `key` = 'title_pwa'; UPDATE `settings` SET `description` = 'عنوان نمایش‌داده‌شده در تب مرورگر (favicon). از دیپلوی مدیریت می‌شود.' WHERE `key` = 'title_fav'; UPDATE `settings` SET `description` = 'عنوان فلز اصلی در رابط کاربر (مثلاً «طلای آبشده» یا «کاتد مس»). در دارایی، کیف پول و برچسب‌های معامله دیده می‌شود. فقط از دیپلوی/SQL تغییر می‌کند.' WHERE `key` = 'metal_type'; +UPDATE `settings` SET `title_fa` = 'نام فلز اصلی (ادمین)', `description` = 'نام کوتاه فلز برای برچسب‌های پنل ادمین (مثلاً «مس» یا «طلا»). «تراز طلایی» می‌شود «تراز مس» — نه «تراز مسی». فقط از دیپلوی/SQL تغییر می‌کند.' WHERE `key` = 'main_metal'; UPDATE `settings` SET `description` = 'واحد نمایش وزن در دارایی‌ها، کیف پول و جزئیات معاملات|گرم,کیلوگرم' WHERE `key` = 'unit'; UPDATE `settings` SET `description` = 'روشن: کاربران جدید می‌توانند ثبت‌نام کنند. خاموش: فقط ورود کاربران موجود مجاز است.' WHERE `key` = 'open_register'; UPDATE `settings` SET `description` = 'روشن: حساب کاربر بلافاصله پس از ثبت‌نام فعال می‌شود. خاموش: نیاز به تأیید دستی ادمین دارد.' WHERE `key` = 'auto_active_customer'; From 0bc690591f354264e1f75ee38078a11236a287e5 Mon Sep 17 00:00:00 2001 From: adonis Date: Tue, 23 Jun 2026 21:04:29 +0330 Subject: [PATCH 12/19] fixed kilo in metals --- .../Controllers/Admin/CustomerController.php | 17 +- .../Controllers/Admin/WalletController.php | 21 +- app/Services/BalanceReportService.php | 29 +++ app/Services/WalletBalanceService.php | 243 +++++++++++++++++- 4 files changed, 301 insertions(+), 9 deletions(-) diff --git a/app/Http/Controllers/Admin/CustomerController.php b/app/Http/Controllers/Admin/CustomerController.php index 4007671..6a70fca 100644 --- a/app/Http/Controllers/Admin/CustomerController.php +++ b/app/Http/Controllers/Admin/CustomerController.php @@ -16,6 +16,7 @@ use Rooyesh\Wallet\interfaces\WalletInterface; use Rooyesh\Wallet\models\Transaction; use Rooyesh\Wallet\models\Wallet; use App\Models\Setting; +use App\Services\BalanceReportService; use App\Services\ConfigService; use App\Services\WalletBalanceService; use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; @@ -133,19 +134,27 @@ class CustomerController extends SuperController return response()->json(true); } public function tableWallets(Request $request){ - $query_table = Customer::with(['wallet','referral'])->where("is_archive",false)/*->where('type',1)*/; + $query_table = Customer::with(['wallet.product:id,metal_id', 'referral'])->where("is_archive",false)/*->where('type',1)*/; TableEngine::create($query_table, $request) ->initSearch($this->search) ->select(['*',Customer::$SELECT_TYPE_STR]) ->export($query_table); $balanceService = app(WalletBalanceService::class); - $mainWalletProductIds = app(ConfigService::class)->mainWalletProductIds(); + $balanceReport = app(BalanceReportService::class); + $catalogMetalIds = $balanceService->catalogMetalIds(); + $query_table['catalogMetals'] = $balanceReport->catalogMetalsMeta(); + if (isset($query_table['rows'])) { - $query_table['rows'] = collect($query_table['rows'])->map(function ($row) use ($balanceService, $mainWalletProductIds) { + $query_table['rows'] = collect($query_table['rows'])->map(function ($row) use ($balanceService, $catalogMetalIds) { $wallets = collect($row['wallet'] ?? []); $row['wallet_cash'] = $balanceService->mainWalletCash($wallets); - $row['wallet_gold'] = $balanceService->totalGoldWeight($wallets, $mainWalletProductIds); + $metalMap = $balanceService->buildCustomerWalletMetalDisplayMap($wallets, $catalogMetalIds); + $row['wallet_metal_gold'] = $metalMap; + foreach ($catalogMetalIds as $metalId) { + $row['wallet_metal_gold_' . $metalId] = $metalMap[(string) $metalId] ?? 0.0; + } + $row['wallet_gold'] = array_sum($metalMap); return $row; })->values(); diff --git a/app/Http/Controllers/Admin/WalletController.php b/app/Http/Controllers/Admin/WalletController.php index 42bfbb3..1f2049b 100644 --- a/app/Http/Controllers/Admin/WalletController.php +++ b/app/Http/Controllers/Admin/WalletController.php @@ -20,6 +20,7 @@ use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; use App\Http\Controllers\Admin\TahesabController; use App\Models\Setting; use App\Models\User; +use App\Services\WalletBalanceService; use Hekmatinasser\Verta\Verta; @@ -301,28 +302,42 @@ class WalletController extends Controller public function indexWallet(Request $request) { $lastPrice = (float) (Price::latest()->first()?->price ?? 0); + $balanceService = app(WalletBalanceService::class); + $catalogMetalIds = $balanceService->catalogMetalIds(); + $mainWalletMetalId = $balanceService->resolveMainWalletMetalId(); // همان فیلتر/جستجوی جدول — بدون صفحه‌بندی — برای مجموع ردیف‌های قابل مشاهده $sumQuery = Wallet::query(); TableEngine::create($sumQuery, $request->all()) ->initSearch(['customer.name']); - $totalGold = (float) (clone $sumQuery)->sum('gold'); + $sumRows = (clone $sumQuery)->with('product:id,metal_id')->get(); + $metalBalances = $balanceService->buildWalletMetalSums($sumRows, $catalogMetalIds); + $totalGold = array_sum(array_column($metalBalances, 'gold')); + $totalGoldToCash = array_sum(array_column($metalBalances, 'goldToCash')); $totalCash = (float) (clone $sumQuery)->sum('cash'); $sums = [ 'gold' => round($totalGold, 2), - 'goldToCash' => round($totalGold * $lastPrice, 2), + 'goldToCash' => round($totalGoldToCash, 2), 'cash' => round($totalCash, 2), 'cashToGold' => $lastPrice > 0 ? round($totalCash / $lastPrice, 2) : 0, + 'metalBalances' => $metalBalances, ]; - $records = Wallet::with('customer'); + $records = Wallet::with(['customer', 'product:id,metal_id']); TableEngine::create($records, $request->all()) ->initSearch(['customer.name']) ->select(['*']) ->export($records); + if (isset($records['rows'])) { + foreach ($records['rows'] as &$row) { + $balanceService->enrichWalletTableRow($row, $catalogMetalIds, $mainWalletMetalId); + } + unset($row); + } + $records['sums'] = $sums; return response()->json($records); diff --git a/app/Services/BalanceReportService.php b/app/Services/BalanceReportService.php index 7cc25a6..99c16b4 100644 --- a/app/Services/BalanceReportService.php +++ b/app/Services/BalanceReportService.php @@ -33,6 +33,35 @@ class BalanceReportService return self::METAL_LABELS[$metalId] ?? "فلز {$metalId}"; } + /** واحد نمایش موجودی/تراز هر فلز در ادمین — مس کیلوگرم، بقیه گرم */ + public function metalDisplayUnit(int $metalId): string + { + return (int) $metalId === 3 ? 'کیلوگرم' : 'گرم'; + } + + public function gramsToMetalDisplay(float $grams, int $metalId): float + { + return (int) $metalId === 3 ? $grams / 1000.0 : $grams; + } + + /** + * @return array + */ + public function catalogMetalsMeta(): array + { + $items = []; + foreach ($this->catalogMetalIds() as $metalId) { + $metalId = (int) $metalId; + $items[] = [ + 'metal_id' => $metalId, + 'label' => $this->metalLabel($metalId), + 'display_unit' => $this->metalDisplayUnit($metalId), + ]; + } + + return $items; + } + /** * @return array */ diff --git a/app/Services/WalletBalanceService.php b/app/Services/WalletBalanceService.php index d61db65..202a91e 100644 --- a/app/Services/WalletBalanceService.php +++ b/app/Services/WalletBalanceService.php @@ -2,6 +2,9 @@ namespace App\Services; +use App\Http\Controllers\Controller; +use App\Models\Price; +use App\Models\Product; use Illuminate\Support\Collection; /** @@ -12,8 +15,10 @@ use Illuminate\Support\Collection; */ class WalletBalanceService { - public function __construct(private ConfigService $configService) - { + public function __construct( + private ConfigService $configService, + private BalanceReportService $balanceReportService + ) { } public function mainWalletProductIds(): array @@ -79,6 +84,240 @@ class WalletBalanceService return $sum; } + /** @return list */ + public function catalogMetalIds(): array + { + $ids = $this->balanceReportService->catalogMetalIds(); + + return $ids !== [] ? $ids : [1]; + } + + public function resolveMainWalletMetalId(): int + { + $mainWalletProductIds = $this->mainWalletProductIds(); + if ($mainWalletProductIds !== []) { + $product = Product::query() + ->whereIn('id', $mainWalletProductIds) + ->orderBy('id') + ->first(); + + return max(1, (int) ($product?->metal_id ?? 1)); + } + + $metalIds = $this->catalogMetalIds(); + + return count($metalIds) === 1 ? (int) $metalIds[0] : 1; + } + + /** + * فلز مرتبط با یک ردیف کیف در جدول ادمین. + * + * @param Collection|array|null $wallets + */ + public function resolveWalletLineMetalId(mixed $line, ?int $mainWalletMetalId = null): int + { + $fundId = $this->lineValue($line, 'fund_id'); + $productId = $this->lineValue($line, 'product_id'); + + if ($fundId !== null) { + return 1; + } + + if ($productId !== null) { + $product = $this->lineProduct($line); + $metalId = (int) ($product?->metal_id ?? 0); + + return $metalId > 0 ? $metalId : 1; + } + + return $mainWalletMetalId ?? $this->resolveMainWalletMetalId(); + } + + /** + * @param list $catalogMetalIds + * @return array + */ + public function walletLineMetalGoldMap( + mixed $line, + array $catalogMetalIds, + ?int $mainWalletMetalId = null + ): array { + $map = []; + foreach ($catalogMetalIds as $metalId) { + $map[(string) $metalId] = 0.0; + } + + $goldGrams = $this->walletLineGoldGrams($line); + if ($goldGrams <= 0) { + return $map; + } + + $metalId = $this->resolveWalletLineMetalId($line, $mainWalletMetalId); + $map[(string) $metalId] = round( + $this->balanceReportService->gramsToMetalDisplay($goldGrams, $metalId), + 3 + ); + + return $map; + } + + /** + * @param Collection|array $walletLines + * @param list $catalogMetalIds + * @return array جمع به گرم + */ + public function sumWalletMetalGrams(Collection|array $walletLines, array $catalogMetalIds): array + { + $buckets = []; + foreach ($catalogMetalIds as $metalId) { + $buckets[(string) (int) $metalId] = 0.0; + } + + $mainWalletMetalId = $this->resolveMainWalletMetalId(); + foreach ($this->walletLines($walletLines) as $line) { + $goldGrams = $this->walletLineGoldGrams($line); + if ($goldGrams <= 0) { + continue; + } + + $metalId = (string) $this->resolveWalletLineMetalId($line, $mainWalletMetalId); + $buckets[$metalId] = ($buckets[$metalId] ?? 0.0) + $goldGrams; + } + + return $buckets; + } + + /** + * @param Collection|array $walletLines + * @param list $catalogMetalIds + * @return list + */ + public function buildWalletMetalSums(Collection|array $walletLines, array $catalogMetalIds): array + { + $gramBuckets = $this->sumWalletMetalGrams($walletLines, $catalogMetalIds); + + $items = []; + foreach ($catalogMetalIds as $metalId) { + $metalId = (int) $metalId; + $goldGrams = (float) ($gramBuckets[(string) $metalId] ?? 0.0); + $displayGold = round($this->balanceReportService->gramsToMetalDisplay($goldGrams, $metalId), 3); + $price = $this->livePricePerGram($metalId); + $items[] = [ + 'metal_id' => $metalId, + 'label' => $this->balanceReportService->metalLabel($metalId), + 'gold' => $displayGold, + 'display_unit' => $this->balanceReportService->metalDisplayUnit($metalId), + 'goldToCash' => round($goldGrams * $price, 2), + ]; + } + + return $items; + } + + /** + * @param Collection|array $walletLines + * @param list $catalogMetalIds + * @return array مقادیر نمایشی + */ + public function buildCustomerWalletMetalDisplayMap(Collection|array $walletLines, array $catalogMetalIds): array + { + $gramBuckets = $this->sumWalletMetalGrams($walletLines, $catalogMetalIds); + $map = []; + + foreach ($catalogMetalIds as $metalId) { + $metalId = (int) $metalId; + $goldGrams = (float) ($gramBuckets[(string) $metalId] ?? 0.0); + $map[(string) $metalId] = round( + $this->balanceReportService->gramsToMetalDisplay($goldGrams, $metalId), + 3 + ); + } + + return $map; + } + + /** + * @param list $catalogMetalIds + */ + public function enrichWalletTableRow(mixed &$row, array $catalogMetalIds, int $mainWalletMetalId): void + { + $metalMap = $this->walletLineMetalGoldMap($row, $catalogMetalIds, $mainWalletMetalId); + + if (is_array($row)) { + $row['metal_gold'] = $metalMap; + foreach ($catalogMetalIds as $metalId) { + $row['metal_gold_' . $metalId] = $metalMap[(string) $metalId] ?? 0.0; + } + + return; + } + + if (is_object($row)) { + $row->metal_gold = $metalMap; + foreach ($catalogMetalIds as $metalId) { + $row->{'metal_gold_' . $metalId} = $metalMap[(string) $metalId] ?? 0.0; + } + } + } + + private function livePricePerGram(int $metalId): float + { + if ($metalId === 1) { + $live = (int) app(ProductDisplayPriceService::class)->resolveAppBasePrice(); + if ($live > 0) { + return (float) $live; + } + + return (float) (Price::latest('id')->value('price') ?? 0); + } + + $product = Product::query() + ->where('active', 1) + ->where('metal_id', $metalId) + ->where('price', '>', 0) + ->orderBy('id') + ->first(); + + if (!$product) { + return 0.0; + } + + $unitPrice = (float) $product->price + (float) ($product->price_profit ?? 0); + $amount = (float) ($product->amount ?? 1); + + return $amount > 0 ? $unitPrice * $amount : $unitPrice; + } + + private function lineProduct(mixed $line): ?object + { + if (is_object($line) && isset($line->product)) { + return $line->product; + } + + if (is_array($line) && isset($line['product'])) { + $product = $line['product']; + + return is_object($product) ? $product : (object) $product; + } + + return null; + } + + private function walletLineGoldGrams(mixed $line): float + { + $gold = (float) ($this->lineValue($line, 'gold') ?? 0); + if ($gold <= 0) { + return 0.0; + } + + $product = $this->lineProduct($line); + if ($product !== null) { + return Controller::productLineWeightToGrams($product, $gold); + } + + return Controller::weightToGrams($gold); + } + /** @param Collection|array|null $wallets */ private function mainWalletLine(Collection|array|null $wallets): mixed { From 6c15ff18303cf37a2a0a651571f3f2cfaf4f2701 Mon Sep 17 00:00:00 2001 From: adonis Date: Tue, 23 Jun 2026 21:21:04 +0330 Subject: [PATCH 13/19] fixed kilo in metals 2 --- .../Controllers/Admin/WalletController.php | 42 +++---- app/Services/BalanceReportService.php | 119 ++++++++++++++++++ 2 files changed, 137 insertions(+), 24 deletions(-) diff --git a/app/Http/Controllers/Admin/WalletController.php b/app/Http/Controllers/Admin/WalletController.php index 1f2049b..3484e4b 100644 --- a/app/Http/Controllers/Admin/WalletController.php +++ b/app/Http/Controllers/Admin/WalletController.php @@ -20,6 +20,7 @@ use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; use App\Http\Controllers\Admin\TahesabController; use App\Models\Setting; use App\Models\User; +use App\Services\BalanceReportService; use App\Services\WalletBalanceService; use Hekmatinasser\Verta\Verta; @@ -219,35 +220,28 @@ class WalletController extends Controller public function index(Request $request) { - $records = Transaction::with(['customer'])->whereNull('fund_id'); + $balanceReport = app(BalanceReportService::class); + $balanceService = app(WalletBalanceService::class); + $catalogMetalIds = $balanceService->catalogMetalIds(); + + $records = Transaction::with([ + 'customer', + 'product:id,title,metal_id,group_type,unit,amount,trade_by_quantity,direct_price', + ])->whereNull('fund_id'); TableEngine::create($records, $request->all()) ->initSearch(['order_id', 'customer.name']) ->select([ '*', Transaction::$SELECT_CAJ, Transaction::$SELECT_TYPE_STR, Transaction::$SELECT_SAJ, Transaction::$SELECT_EAJ ])->export($records); -// foreach ($records['rows'] as &$record) { -// if (empty($record->title) or $record->title == "تعریف نشده"){ -// $record->title = $record->type_str ; -// $record->unit = "گرم"; -// if (isset($record->wallet)) { -// $wallet = Wallet::with('product')->where('id',$record->wallet)->first(); -// if (isset($wallet->product)) { -// if ($record->gold > 0){ -// $record->title = " خرید " ; -// -// }else{ -// $record->title = " فروش " ; -// -// } -// $record->title .= $wallet->product->title ; -// $record->unit = "عدد"; -// -// } -// } -// $record->save(); -// } -// -// } + + if (isset($records['rows'])) { + foreach ($records['rows'] as &$row) { + $balanceReport->enrichTransactionTableRow($row, $catalogMetalIds); + } + unset($row); + } + + $records['catalogMetals'] = $balanceReport->catalogMetalsMeta(); return response()->json($records); } diff --git a/app/Services/BalanceReportService.php b/app/Services/BalanceReportService.php index 99c16b4..31f9479 100644 --- a/app/Services/BalanceReportService.php +++ b/app/Services/BalanceReportService.php @@ -2,6 +2,7 @@ namespace App\Services; +use App\Http\Controllers\Controller; use App\Models\Order; use App\Models\Product; @@ -248,4 +249,122 @@ class BalanceReportService $buckets[$metalId]['gold_abs'] += $gold; $buckets[$metalId]['cash_abs'] += $cash; } + + public function resolveTransactionMetalId(mixed $transaction): int + { + $product = $this->transactionProduct($transaction); + if ($product !== null) { + $metalId = (int) ($product->metal_id ?? 0); + + return $metalId > 0 ? $metalId : 1; + } + + return 1; + } + + public function transactionGoldGrams(mixed $transaction): float + { + $gold = (float) ($this->lineValue($transaction, 'gold') ?? 0); + if (abs($gold) < 0.0000001) { + return 0.0; + } + + $product = $this->transactionProduct($transaction); + if ($product !== null) { + return Controller::productLineWeightToGrams($product, $gold); + } + + return Controller::weightToGrams($gold); + } + + /** + * @param list $catalogMetalIds + * @return array + */ + public function buildTransactionMetalDisplayMap(mixed $transaction, array $catalogMetalIds): array + { + $map = []; + foreach ($catalogMetalIds as $metalId) { + $map[(string) (int) $metalId] = 0.0; + } + + $type = (int) ($this->lineValue($transaction, 'type') ?? 0); + if (in_array($type, [3, 4], true)) { + return $map; + } + + $gold = (float) ($this->lineValue($transaction, 'gold') ?? 0); + if (abs($gold) < 0.0000001) { + return $map; + } + + $metalId = $this->resolveTransactionMetalId($transaction); + $grams = abs($this->transactionGoldGrams($transaction)); + $display = $this->gramsToMetalDisplay($grams, $metalId); + if ($gold < 0) { + $display *= -1; + } + + $map[(string) $metalId] = round($display, 3); + + return $map; + } + + /** + * @param list $catalogMetalIds + */ + public function enrichTransactionTableRow(mixed &$row, array $catalogMetalIds): void + { + $metalMap = $this->buildTransactionMetalDisplayMap($row, $catalogMetalIds); + $metalId = $this->resolveTransactionMetalId($row); + $metalLabel = $this->metalLabel($metalId); + + if (is_array($row)) { + $row['metal_gold'] = $metalMap; + $row['transaction_metal_id'] = $metalId; + $row['transaction_metal_label'] = $metalLabel; + foreach ($catalogMetalIds as $mid) { + $row['metal_gold_' . $mid] = $metalMap[(string) $mid] ?? 0.0; + } + + return; + } + + if (is_object($row)) { + $row->metal_gold = $metalMap; + $row->transaction_metal_id = $metalId; + $row->transaction_metal_label = $metalLabel; + foreach ($catalogMetalIds as $mid) { + $row->{'metal_gold_' . $mid} = $metalMap[(string) $mid] ?? 0.0; + } + } + } + + private function transactionProduct(mixed $transaction): ?object + { + if (is_object($transaction) && isset($transaction->product)) { + return $transaction->product; + } + + if (is_array($transaction) && isset($transaction['product'])) { + $product = $transaction['product']; + + return is_object($product) ? $product : (object) $product; + } + + return null; + } + + private function lineValue(mixed $line, string $key): mixed + { + if (is_array($line)) { + return $line[$key] ?? null; + } + + if (is_object($line)) { + return $line->{$key} ?? null; + } + + return null; + } } From f271df9cd679392426bfe024eb3a1b81f848d9fe Mon Sep 17 00:00:00 2001 From: adonis Date: Tue, 23 Jun 2026 21:28:49 +0330 Subject: [PATCH 14/19] fixed kilo in metals 3 --- .../Controllers/Admin/OrderController.php | 32 ++++++++- app/Services/BalanceReportService.php | 70 +++++++++++++++++++ 2 files changed, 100 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Admin/OrderController.php b/app/Http/Controllers/Admin/OrderController.php index e90db91..80e5693 100644 --- a/app/Http/Controllers/Admin/OrderController.php +++ b/app/Http/Controllers/Admin/OrderController.php @@ -5,26 +5,54 @@ namespace App\Http\Controllers\Admin; use App\DB\Te\TableEngine; use App\Http\Controllers\Controller; use App\Models\Order; +use App\Services\BalanceReportService; use Illuminate\Http\Request; class OrderController extends Controller { public function table(Request $request){ - $records = Order::with(['customer','transactions']); + $balanceReport = app(BalanceReportService::class); + + $records = Order::with([ + 'customer', + 'transactions.product:id,title,unit,metal_id,group_type,amount,trade_by_quantity,direct_price', + ]); TableEngine::create($records, $request->all()) ->initSearch(['customer.name']) ->select([ '*',Order::$SELECT_STS_STR ])->export($records); + + if (isset($records['rows'])) { + foreach ($records['rows'] as &$row) { + $balanceReport->enrichOrderTableRow($row); + } + unset($row); + } + return response()->json($records); } + public function singleOrderTable(Request $request){ - $records = Order::with(['customer'])->where('type',2); + $balanceReport = app(BalanceReportService::class); + + $records = Order::with([ + 'customer', + 'transactions.product:id,title,unit,metal_id,group_type,amount,trade_by_quantity,direct_price', + ])->where('type',2); TableEngine::create($records, $request->all()) ->initSearch(['customer.name']) ->select([ '*',Order::$SELECT_STS_STR ])->export($records); + + if (isset($records['rows'])) { + foreach ($records['rows'] as &$row) { + $balanceReport->enrichOrderTableRow($row); + } + unset($row); + } + return response()->json($records); } } diff --git a/app/Services/BalanceReportService.php b/app/Services/BalanceReportService.php index 31f9479..fe07e26 100644 --- a/app/Services/BalanceReportService.php +++ b/app/Services/BalanceReportService.php @@ -355,6 +355,76 @@ class BalanceReportService return null; } + public function enrichOrderTableRow(mixed &$row): void + { + $product = $this->resolveOrderProduct($row); + $unit = $this->resolveOrderAmountUnit($product); + $amount = (float) ($this->lineValue($row, 'amount') ?? 0); + $displayAmount = $this->resolveOrderDisplayAmount($amount, $product); + + if (is_array($row)) { + $row['amount_unit'] = $unit; + $row['amount_display'] = $displayAmount; + + return; + } + + if (is_object($row)) { + $row->amount_unit = $unit; + $row->amount_display = $displayAmount; + } + } + + public function resolveOrderAmountUnit(?object $product): string + { + if ($product === null) { + return Controller::tradeWeightUnit(); + } + + return Controller::productQuantityUnit(Controller::productUnitPayload($product)); + } + + private function resolveOrderDisplayAmount(float $amount, ?object $product): float + { + if ($product === null || abs($amount) < 0.0000001) { + return round($amount, 3); + } + + $payload = Controller::productUnitPayload($product); + $productUnit = trim((string) ($payload['unit'] ?? '')); + if (!Controller::isWeightBasedProductUnit($productUnit)) { + return round($amount, 3); + } + + $tradeUnit = Controller::tradeWeightUnit(); + if ($productUnit !== $tradeUnit) { + return round(Controller::convertWeight($amount, $tradeUnit, $productUnit), 3); + } + + return round($amount, 3); + } + + private function resolveOrderProduct(mixed $order): ?object + { + $transactions = $this->lineValue($order, 'transactions'); + if ($transactions instanceof \Illuminate\Support\Collection) { + $transactions = $transactions->all(); + } + + if (!is_array($transactions)) { + return null; + } + + foreach ($transactions as $tx) { + $product = is_object($tx) ? ($tx->product ?? null) : ($tx['product'] ?? null); + if ($product) { + return is_object($product) ? $product : (object) $product; + } + } + + return null; + } + private function lineValue(mixed $line, string $key): mixed { if (is_array($line)) { From ecae82c8e01137701c7dfb1d0efed52f7bef98b1 Mon Sep 17 00:00:00 2001 From: adonis Date: Tue, 23 Jun 2026 21:40:33 +0330 Subject: [PATCH 15/19] fixed kilo in metals 4 --- .../Controllers/Admin/OrderController.php | 11 ++++- app/Services/BalanceReportService.php | 48 +++++++++++++++++-- 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/app/Http/Controllers/Admin/OrderController.php b/app/Http/Controllers/Admin/OrderController.php index 80e5693..280d70c 100644 --- a/app/Http/Controllers/Admin/OrderController.php +++ b/app/Http/Controllers/Admin/OrderController.php @@ -6,12 +6,14 @@ use App\DB\Te\TableEngine; use App\Http\Controllers\Controller; use App\Models\Order; use App\Services\BalanceReportService; +use App\Services\WalletBalanceService; use Illuminate\Http\Request; class OrderController extends Controller { public function table(Request $request){ $balanceReport = app(BalanceReportService::class); + $catalogMetalIds = app(WalletBalanceService::class)->catalogMetalIds(); $records = Order::with([ 'customer', @@ -25,16 +27,19 @@ class OrderController extends Controller if (isset($records['rows'])) { foreach ($records['rows'] as &$row) { - $balanceReport->enrichOrderTableRow($row); + $balanceReport->enrichOrderTableRow($row, $catalogMetalIds); } unset($row); } + $records['catalogMetals'] = $balanceReport->catalogMetalsMeta(); + return response()->json($records); } public function singleOrderTable(Request $request){ $balanceReport = app(BalanceReportService::class); + $catalogMetalIds = app(WalletBalanceService::class)->catalogMetalIds(); $records = Order::with([ 'customer', @@ -48,11 +53,13 @@ class OrderController extends Controller if (isset($records['rows'])) { foreach ($records['rows'] as &$row) { - $balanceReport->enrichOrderTableRow($row); + $balanceReport->enrichOrderTableRow($row, $catalogMetalIds); } unset($row); } + $records['catalogMetals'] = $balanceReport->catalogMetalsMeta(); + return response()->json($records); } } diff --git a/app/Services/BalanceReportService.php b/app/Services/BalanceReportService.php index fe07e26..0419443 100644 --- a/app/Services/BalanceReportService.php +++ b/app/Services/BalanceReportService.php @@ -355,23 +355,61 @@ class BalanceReportService return null; } - public function enrichOrderTableRow(mixed &$row): void + public function resolveOrderMetalId(mixed $order): int + { + $product = $this->resolveOrderProduct($order); + if ($product !== null) { + $metalId = (int) ($product->metal_id ?? 0); + + return $metalId > 0 ? $metalId : 1; + } + + return 1; + } + + /** + * @param list $catalogMetalIds + */ + public function enrichOrderTableRow(mixed &$row, array $catalogMetalIds = []): void { $product = $this->resolveOrderProduct($row); $unit = $this->resolveOrderAmountUnit($product); $amount = (float) ($this->lineValue($row, 'amount') ?? 0); $displayAmount = $this->resolveOrderDisplayAmount($amount, $product); + $metalId = $this->resolveOrderMetalId($row); + $metalLabel = $this->metalLabel($metalId); + $price = (float) ($this->lineValue($row, 'price') ?? 0); + + $metalPrices = []; + foreach ($catalogMetalIds as $mid) { + $mid = (int) $mid; + $metalPrices[(string) $mid] = $mid === $metalId ? $price : 0.0; + } + + $payload = [ + 'amount_unit' => $unit, + 'amount_display' => $displayAmount, + 'order_metal_id' => $metalId, + 'order_metal_label' => $metalLabel, + 'metal_prices' => $metalPrices, + ]; + + foreach ($catalogMetalIds as $mid) { + $payload['metal_price_' . (int) $mid] = $metalPrices[(string) (int) $mid] ?? 0.0; + } if (is_array($row)) { - $row['amount_unit'] = $unit; - $row['amount_display'] = $displayAmount; + foreach ($payload as $key => $value) { + $row[$key] = $value; + } return; } if (is_object($row)) { - $row->amount_unit = $unit; - $row->amount_display = $displayAmount; + foreach ($payload as $key => $value) { + $row->{$key} = $value; + } } } From 1b310b47523958bd7d7edb5b36c4ad75e4d89e0d Mon Sep 17 00:00:00 2001 From: adonis Date: Tue, 23 Jun 2026 21:59:38 +0330 Subject: [PATCH 16/19] fixed kilo in metals 5 --- .../Controllers/Admin/WalletController.php | 156 ++++++++++++++++++ routes/admin.php | 2 + 2 files changed, 158 insertions(+) diff --git a/app/Http/Controllers/Admin/WalletController.php b/app/Http/Controllers/Admin/WalletController.php index 3484e4b..5adf6bf 100644 --- a/app/Http/Controllers/Admin/WalletController.php +++ b/app/Http/Controllers/Admin/WalletController.php @@ -21,6 +21,7 @@ use App\Http\Controllers\Admin\TahesabController; use App\Models\Setting; use App\Models\User; use App\Services\BalanceReportService; +use App\Services\ConfigService; use App\Services\WalletBalanceService; use Hekmatinasser\Verta\Verta; @@ -218,6 +219,161 @@ class WalletController extends Controller return response()->json(true); } + /** گزینه‌های شارژ کیف پول — نقدی + محصولات فعال */ + public function chargeTargets() + { + $configService = app(ConfigService::class); + $mainWalletProductIds = $configService->mainWalletProductIds(); + $balanceReport = app(BalanceReportService::class); + + $targets = [ + [ + 'key' => 'money', + 'kind' => 'money', + 'label' => 'نقدی (ریال)', + 'unit' => 'ریال', + ], + ]; + + $products = Product::query() + ->where('active', 1) + ->orderBy('metal_id') + ->orderBy('id') + ->get(['id', 'title', 'unit', 'metal_id', 'group_type', 'trade_by_quantity', 'amount', 'direct_price']); + + foreach ($products as $product) { + $usesMainWallet = in_array((int) $product->id, $mainWalletProductIds, true); + $label = (string) $product->title; + if ($usesMainWallet) { + $label .= ' (کیف اصلی)'; + } + + $targets[] = [ + 'key' => 'product:' . $product->id, + 'kind' => 'metal', + 'product_id' => (int) $product->id, + 'label' => $label, + 'unit' => $this->resolveProductChargeUnit($product), + 'metal_id' => max(1, (int) ($product->metal_id ?? 1)), + 'uses_main_wallet' => $usesMainWallet, + 'trade_by_quantity' => (bool) $product->trade_by_quantity, + 'group_type' => (string) ($product->group_type ?? ''), + ]; + } + + return response()->json([ + 'targets' => $targets, + 'catalogMetals' => $balanceReport->catalogMetalsMeta(), + ]); + } + + /** + * شارژ یکپارچه کیف پول — نقدی یا محصول (با رعایت کیف اصلی برای آبشده). + */ + public function charge_wallet(Request $request) + { + $productId = (int) $request->input('product_id', 0); + if ($productId > 0) { + return $this->chargeProductMetal($request, $productId); + } + + return $this->charge($request); + } + + private function chargeProductMetal(Request $request, int $productId) + { + $data = $request->all(); + $product = Product::findOrFail($productId); + $customer = Customer::findOrFail($data['customer_id']); + $deposit = ((float) ($data['value'] ?? 0)) > 0; + + $effectiveProductId = Controller::effectiveProductWalletId($productId); + $wallet_interface = new WalletInterface( + (int) $data['customer_id'], + (int) $customer->type, + null, + $effectiveProductId + ); + $wallet = $wallet_interface->wallet; + + if (!$deposit && $wallet !== null && (float) $wallet->gold < abs((float) $data['value'])) { + throw new UnprocessableEntityHttpException('مقدار موجودی کمتر از درخواست شماست'); + } + + $goldBlock = 0; + $user = auth()->user(); + $unitLabel = $this->resolveProductChargeUnit($product); + + $Withdraw = Withdraw::create([ + 'user_id' => $user->id, + 'customer_id' => $customer->id, + 'gold' => $data['value'], + 'note' => $data['note'] ?? null, + 'sts' => 4, + ]); + + new TransactionInterface($wallet_interface, [ + 'cash' => 0, + 'gold' => $data['value'], + 'product_id' => $productId, + 'expire_at' => now(), + 'note' => $data['note'] ?? null, + 'withdraw_id' => $Withdraw->id, + 'active' => true, + ], 7, $deposit, false, $goldBlock); + + if ($this->Config('send_to_tahesab') == 'true') { + try { + $data['direction'] = $deposit ? 1 : 0; + $tahesab = new TahesabController(); + $tahesab->walletGold($data); + } catch (\Exception $e) { + return response($e->getMessage(), 500); + } + } + + $status = $deposit ? ' شارژ شد ' : ' کسر شد '; + $formatted = self::formatSmsGoldWeight($data['value']); + + if ($this->Config('charge_notif') == 'true') { + $message = 'کیف پول کاربر ' . $customer->name . ' به مقدار ' . $formatted . ' ' . $unitLabel . ' در تاریخ ' . verta()->format('Y/n/j H:i:s') . $status . 'بابت' . "\n" + . ($data['note'] ?? '') + . "\n" + . $this->Config('name_fa'); + User::where('send_sms', 1)->get()->each(function ($admin) use ($message) { + $this->send($admin->mobile, $message); + }); + } + + $message = 'کیف پول شما به اندازه ' . $formatted . ' ' . $unitLabel . ' در تاریخ ' . verta()->format('Y/n/j H:i:s') . $status . ' بابت' . "\n" + . ($data['note'] ?? '') + . "\n" + . $this->Config('name_fa'); + $this->Send($customer->mobile, $message); + + return response()->json(true); + } + + private function resolveProductChargeUnit(Product $product): string + { + $payload = Controller::productUnitPayload($product); + + if ( + Controller::usesQuantityTrade($payload) + && !Controller::isWeightBasedProductUnit($payload['unit'] ?? null) + ) { + $unit = trim((string) ($payload['unit'] ?? '')); + + return $unit !== '' ? $unit : 'عدد'; + } + + if (Controller::isWeightBasedProductUnit($payload['unit'] ?? null)) { + return trim((string) $payload['unit']); + } + + return Controller::productStorageWeightUnit($product); + } + public function index(Request $request) { $balanceReport = app(BalanceReportService::class); diff --git a/routes/admin.php b/routes/admin.php index 19f5d83..201a319 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -104,6 +104,8 @@ Route::middleware('auth:user')->group(function (){ //-------------wallet Route::post('/wallet/charge',[WalletController::class,'charge']); Route::post('/wallet/charge_gold',[WalletController::class,'charge_gold']); + Route::post('/wallet/charge_wallet',[WalletController::class,'charge_wallet']); + Route::get('/wallet/charge-targets',[WalletController::class,'chargeTargets']); Route::post('/wallet/table',[WalletController::class,'indexWallet']); Route::post('/wallet/transaction/table',[WalletController::class,'index']); Route::post('/wallet/transaction/update',[WalletController::class,'updateTransaction']); From 0c46a0a24cd55505fd3552b6590ff94ac95c560d Mon Sep 17 00:00:00 2001 From: adonis Date: Tue, 23 Jun 2026 22:05:47 +0330 Subject: [PATCH 17/19] fixed kilo in metals 6 --- app/Http/Controllers/Controller.php | 54 ++++++++++++++++------ app/Services/MontzarService.php | 9 ++-- app/Services/WebsocketBroadcastService.php | 6 +-- 3 files changed, 44 insertions(+), 25 deletions(-) diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 223d69e..c10c5fb 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -73,6 +73,26 @@ class Controller extends BaseController return $formatted !== '' ? $formatted : '0'; } + /** + * گزینه‌های Guzzle/cURL — فقط وقتی افزونه curl و ثابت‌ها موجود باشند. + * + * @param array $curlOptions + * @return array> + */ + public static function httpClientOptions(array $curlOptions = []): array + { + $curl = $curlOptions; + + if (defined('CURLOPT_IPRESOLVE') && defined('CURL_IPRESOLVE_V4')) { + $resolveOpt = (int) constant('CURLOPT_IPRESOLVE'); + if (! array_key_exists($resolveOpt, $curl)) { + $curl[$resolveOpt] = (int) constant('CURL_IPRESOLVE_V4'); + } + } + + return $curl === [] ? [] : ['curl' => $curl]; + } + public static function Send($to , $msg, $is_customer = true) { $r = self::SendMessage($to , $msg, $is_customer); @@ -139,26 +159,32 @@ class Controller extends BaseController ]); $platforms[] = "0098:{$to}";*/ - $response = Http::timeout(15) - ->withHeaders([ - 'Content-Type' => 'application/json', - 'secret_key' => 'f21debfb-83ac-4624-a2f4-f08375043181', - ]) - ->withOptions([ - 'curl' => [ - \CURLOPT_IPRESOLVE => \CURL_IPRESOLVE_V4, - ] - ]) - ->post('http://185.231.112.101/api/createsms', [ + try { + $http = Http::timeout(15) + ->withHeaders([ + 'Content-Type' => 'application/json', + 'secret_key' => 'f21debfb-83ac-4624-a2f4-f08375043181', + ]); + + $curlOptions = self::httpClientOptions(); + if ($curlOptions !== []) { + $http = $http->withOptions($curlOptions); + } + + $response = $http->post('http://185.231.112.101/api/createsms', [ 'phone' => $to, 'host' => $config->Config('name_fa') ?? gethostname(), 'text' => trim($msg), 'platform' => 'rayegansms', ]); - $platforms[] = $response->successful() - ? "rayegansms:{$to}" - : "rayegansms_failed:{$to}:{$response->status()}"; + $platforms[] = $response->successful() + ? "rayegansms:{$to}" + : "rayegansms_failed:{$to}:{$response->status()}"; + } catch (\Throwable $e) { + report($e); + $platforms[] = "rayegansms_error:{$to}"; + } } diff --git a/app/Services/MontzarService.php b/app/Services/MontzarService.php index 10b729d..0b0a7eb 100644 --- a/app/Services/MontzarService.php +++ b/app/Services/MontzarService.php @@ -78,12 +78,9 @@ class MontzarService $response = Http::timeout(7) ->acceptJson() - ->withOptions([ - 'curl' => [ - CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4, - CURLOPT_TCP_FASTOPEN => 1, - ], - ]) + ->withOptions(self::httpClientOptions( + defined('CURLOPT_TCP_FASTOPEN') ? [(int) constant('CURLOPT_TCP_FASTOPEN') => 1] : [] + )) ->withToken($token) ->get($this->endpoint('/server/customer/product/list')) ->json(); diff --git a/app/Services/WebsocketBroadcastService.php b/app/Services/WebsocketBroadcastService.php index 7b35c6d..6a116cb 100644 --- a/app/Services/WebsocketBroadcastService.php +++ b/app/Services/WebsocketBroadcastService.php @@ -84,11 +84,7 @@ class WebsocketBroadcastService try { $response = Http::timeout(3) ->withHeaders(['Content-Type' => 'application/json']) - ->withOptions([ - 'curl' => [ - CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4, - ], - ]) + ->withOptions(\App\Http\Controllers\Controller::httpClientOptions()) ->post($url, $body); if (! $response->successful()) { From 93b1bfe0e8ebd570117ffabd4edf228d6cc9710b Mon Sep 17 00:00:00 2001 From: adonis Date: Tue, 23 Jun 2026 22:11:56 +0330 Subject: [PATCH 18/19] fixed kilo in metals 7 --- app/Services/AssetSummaryService.php | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/app/Services/AssetSummaryService.php b/app/Services/AssetSummaryService.php index db7523c..43e853a 100644 --- a/app/Services/AssetSummaryService.php +++ b/app/Services/AssetSummaryService.php @@ -35,7 +35,7 @@ class AssetSummaryService ->where('owner_type', $ownerType) ->get(); - $mainWallet = $userWallets->first(fn (Wallet $w) => $w->product_id === null); + $mainWallet = $this->resolveMainWallet($userWallets); $metalIds = $this->catalogMetalIds($catalog); $weightUnit = Controller::tradeWeightUnit(); $hasCountBased = $this->catalogHasCountBased($catalog); @@ -52,7 +52,7 @@ class AssetSummaryService $this->buildInventoryCards($userWallets, $catalogById, $mainWalletProductIds) ); $metalTotals = $this->buildMetalTotals($userWallets, $mainWallet, $metalIds, $weightUnit, $catalogById, $metalsWithProductWallet, $mainWalletProductIds); - $cashWallet = (float) ($mainWallet->cash ?? 0); + $cashWallet = (float) ($mainWallet?->cash ?? 0); $assetsRialValue = $this->sumAssetsRialValue( $metalTotals, $userWallets, @@ -112,7 +112,7 @@ class AssetSummaryService array $mainWalletProductIds = [] ): array { $cards = []; - $mainWalletGrams = Controller::weightToGrams((float) ($mainWallet->gold ?? 0)); + $mainWalletGrams = Controller::weightToGrams((float) ($mainWallet?->gold ?? 0)); $assignedMetalIds = []; $mainProductId = $this->configService->mainProductId(); @@ -123,7 +123,7 @@ class AssetSummaryService return []; } - $mainWalletWeight = (float) ($mainWallet->gold ?? 0); + $mainWalletWeight = (float) ($mainWallet?->gold ?? 0); if ($mainWalletWeight <= 0) { return []; } @@ -186,6 +186,13 @@ class AssetSummaryService return $cards; } + private function resolveMainWallet(Collection $userWallets): ?Wallet + { + return $userWallets->first( + fn (Wallet $w) => $w->fund_id === null && $w->product_id === null + ); + } + private function catalogProduct(Product $product, Collection $catalogById): Product { return $catalogById->get((int) $product->id) ?? $product; @@ -301,6 +308,10 @@ class AssetSummaryService $cards = []; foreach ($userWallets as $walletLine) { + if ($walletLine->fund_id !== null) { + continue; + } + if ($walletLine->product_id === null || (float) $walletLine->gold <= 0) { continue; } @@ -349,7 +360,7 @@ class AssetSummaryService return []; } - $mainWalletGrams = Controller::weightToGrams((float) ($mainWallet->gold ?? 0)); + $mainWalletGrams = Controller::weightToGrams((float) ($mainWallet?->gold ?? 0)); $totals = []; // فلز نمایندهٔ کیف اصلی در حالت main_wallet_products From 8b8dc12023bac032926e8b8e50db2757aba36d1d Mon Sep 17 00:00:00 2001 From: adonis Date: Tue, 23 Jun 2026 22:30:10 +0330 Subject: [PATCH 19/19] fixed kilo in metals 9 --- app/Services/AssetSummaryService.php | 90 +++++++++++++++++++++++++--- 1 file changed, 83 insertions(+), 7 deletions(-) diff --git a/app/Services/AssetSummaryService.php b/app/Services/AssetSummaryService.php index 43e853a..db2074a 100644 --- a/app/Services/AssetSummaryService.php +++ b/app/Services/AssetSummaryService.php @@ -46,10 +46,10 @@ class AssetSummaryService // محصولات متصل به کیف اصلی (main_wallet_products) — موجودی‌شان = gold کیف اصلی $mainWalletProductIds = $this->configService->mainWalletProductIds(); - $metalsWithProductWallet = $this->metalIdsWithProductWallet($userWallets, $catalogById, $mainWalletProductIds); + $metalsWithProductWallet = $this->metalIdsWithProductWallet($userWallets, $catalogById, $mainWalletProductIds, $mainWallet); $inventoryCards = array_merge( $this->buildMeltedInventoryCards($catalog, $catalogById, $mainWallet, $metalIds, $metalsWithProductWallet, $mainWalletProductIds), - $this->buildInventoryCards($userWallets, $catalogById, $mainWalletProductIds) + $this->buildInventoryCards($userWallets, $catalogById, $mainWalletProductIds, $mainWallet), ); $metalTotals = $this->buildMetalTotals($userWallets, $mainWallet, $metalIds, $weightUnit, $catalogById, $metalsWithProductWallet, $mainWalletProductIds); $cashWallet = (float) ($mainWallet?->cash ?? 0); @@ -193,6 +193,59 @@ class AssetSummaryService ); } + /** + * کیف legacy محصول آبشده/کاتد — وقتی همان فلز روی کیف اصلی موجودی دارد نباید دوباره شمرده شود. + */ + private function shouldIgnoreLegacyProductWalletLine( + Product $catalogProduct, + ?Wallet $mainWallet, + Collection $catalogById, + array $mainWalletProductIds + ): bool { + if (in_array((int) $catalogProduct->id, $mainWalletProductIds, true)) { + return true; + } + + if ((float) ($mainWallet?->gold ?? 0) <= 0) { + return false; + } + + if (!$this->isMeltedTradeProduct($catalogProduct)) { + return false; + } + + $mainMetalId = $this->resolveMainWalletMetalId($mainWallet, $catalogById, $mainWalletProductIds); + + return $mainMetalId > 0 && (int) ($catalogProduct->metal_id ?? 0) === $mainMetalId; + } + + private function resolveMainWalletMetalId( + ?Wallet $mainWallet, + Collection $catalogById, + array $mainWalletProductIds + ): int { + if ($mainWalletProductIds !== []) { + $representative = $this->resolveMainWalletRepresentative($catalogById, $mainWalletProductIds); + if ($representative) { + return max(1, (int) ($representative->metal_id ?? 1)); + } + } + + if ((float) ($mainWallet?->gold ?? 0) <= 0) { + return 0; + } + + $mainProductId = $this->configService->mainProductId(); + if ($mainProductId) { + $mainProduct = $catalogById->get((int) $mainProductId); + if ($mainProduct) { + return max(1, (int) ($mainProduct->metal_id ?? 1)); + } + } + + return 1; + } + private function catalogProduct(Product $product, Collection $catalogById): Product { return $catalogById->get((int) $product->id) ?? $product; @@ -253,9 +306,14 @@ class AssetSummaryService } /** فلزهایی که کاربر روی کیف محصول موجودی دارد — برای جلوگیری از کارت آبشدهٔ تکراری. */ - private function metalIdsWithProductWallet(Collection $userWallets, Collection $catalogById, array $mainWalletProductIds = []): array - { + private function metalIdsWithProductWallet( + Collection $userWallets, + Collection $catalogById, + array $mainWalletProductIds = [], + ?Wallet $mainWallet = null + ): array { $metalIds = []; + $mainWallet ??= $this->resolveMainWallet($userWallets); foreach ($userWallets as $walletLine) { if ($walletLine->product_id === null || (float) $walletLine->gold <= 0) { @@ -272,7 +330,12 @@ class AssetSummaryService continue; } - $metalId = (int) ($this->catalogProduct($product, $catalogById)->metal_id ?? 0); + $catalogProduct = $this->catalogProduct($product, $catalogById); + if ($this->shouldIgnoreLegacyProductWalletLine($catalogProduct, $mainWallet, $catalogById, $mainWalletProductIds)) { + continue; + } + + $metalId = (int) ($catalogProduct->metal_id ?? 0); if ($metalId > 0) { $metalIds[] = $metalId; } @@ -303,9 +366,14 @@ class AssetSummaryService return $catalog->first(fn (Product $product) => (int) ($product->metal_id ?? 0) === $metalId); } - private function buildInventoryCards(Collection $userWallets, Collection $catalogById, array $mainWalletProductIds = []): array - { + private function buildInventoryCards( + Collection $userWallets, + Collection $catalogById, + array $mainWalletProductIds = [], + ?Wallet $mainWallet = null + ): array { $cards = []; + $mainWallet ??= $this->resolveMainWallet($userWallets); foreach ($userWallets as $walletLine) { if ($walletLine->fund_id !== null) { @@ -327,6 +395,10 @@ class AssetSummaryService } $catalogProduct = $this->catalogProduct($product, $catalogById); + if ($this->shouldIgnoreLegacyProductWalletLine($catalogProduct, $mainWallet, $catalogById, $mainWalletProductIds)) { + continue; + } + $productPayload = $this->productPayload($catalogProduct); $isCountBased = $this->isCountBasedProduct($catalogProduct); @@ -384,6 +456,10 @@ class AssetSummaryService } $catalogProduct = $this->catalogProduct($walletLine->product, $catalogById); + if ($this->shouldIgnoreLegacyProductWalletLine($catalogProduct, $mainWallet, $catalogById, $mainWalletProductIds)) { + continue; + } + if ((int) ($catalogProduct->metal_id ?? 0) !== (int) $metalId) { continue; }