From 147a9681799a619737dfe31996bc0af1af0ef6b7 Mon Sep 17 00:00:00 2001 From: adonis Date: Tue, 23 Jun 2026 20:35:13 +0330 Subject: [PATCH] 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';