From a69316e0de547361bda2667666818f5f4cfab4f6 Mon Sep 17 00:00:00 2001 From: adonis Date: Wed, 1 Jul 2026 20:59:24 +0330 Subject: [PATCH] fix some bug --- .../Controllers/Admin/WalletController.php | 12 ++- .../Single/SingleOrderController.php | 28 +++++-- app/Models/Withdraw.php | 14 +++- app/Services/AssetSummaryService.php | 80 +++++++++++++++++++ app/Services/ConfigService.php | 9 +++ app/Services/WalletBalanceService.php | 70 ++++++++++++++++ database/sql/deploy_settings_const.sql | 6 ++ ...ngs_const_preserve_value_sync_can_edit.sql | 4 + database/sql/fee_deduct_from_gold_patch.sql | 15 ++++ packages/rooyesh/wallet/src/models/Wallet.php | 2 +- 10 files changed, 226 insertions(+), 14 deletions(-) create mode 100644 database/sql/fee_deduct_from_gold_patch.sql diff --git a/app/Http/Controllers/Admin/WalletController.php b/app/Http/Controllers/Admin/WalletController.php index a642465..a7ebaab 100644 --- a/app/Http/Controllers/Admin/WalletController.php +++ b/app/Http/Controllers/Admin/WalletController.php @@ -500,7 +500,13 @@ class WalletController extends Controller TableEngine::create($sumQuery, $request->all()) ->initSearch(['customer.name']); - $sumRows = (clone $sumQuery)->with('product:id,metal_id,unit,amount,group_type,trade_by_quantity,direct_price,title')->get(); + $walletTableWith = [ + 'customer:id,name,mobile', + 'fund:id,title', + 'product:id,metal_id,unit,amount,group_type,trade_by_quantity,direct_price,title', + ]; + + $sumRows = (clone $sumQuery)->with($walletTableWith)->get(); $metalBalances = $balanceService->buildWalletMetalSums($sumRows, $catalogMetalIds); $totalGold = array_sum(array_column($metalBalances, 'gold')); $totalGoldToCash = array_sum(array_column($metalBalances, 'goldToCash')); @@ -514,9 +520,9 @@ class WalletController extends Controller 'metalBalances' => $metalBalances, ]; - $records = Wallet::with(['customer', 'product:id,metal_id,unit,amount,group_type,trade_by_quantity,direct_price,title']); + $records = Wallet::with($walletTableWith); TableEngine::create($records, $request->all()) - ->initSearch(['customer.name']) + ->initSearch(['customer.name', 'customer.mobile', 'product.title', 'fund.title']) ->select(['*']) ->export($records); diff --git a/app/Http/Controllers/Single/SingleOrderController.php b/app/Http/Controllers/Single/SingleOrderController.php index 9606976..e0b0283 100644 --- a/app/Http/Controllers/Single/SingleOrderController.php +++ b/app/Http/Controllers/Single/SingleOrderController.php @@ -234,9 +234,10 @@ class SingleOrderController extends Controller { $response = []; + $config = app(ConfigService::class); + $feeFromGold = $config->feeDeductFromGold(); $data['price'] += $data['difference']; $data['total_price'] = $data['amount'] * $data['price']; - $data['payment_amount'] = $data['total_price']; $this->enforceProductOrderLimits($data, (float) $data['total_price']); @@ -248,7 +249,12 @@ class SingleOrderController extends Controller $data['service_charge'] = isset($data['fee_free']) && $data['fee_free'] === true ? 0 : $data['service_charge_percentage'] * $data['amount']; $data['service_charge_price'] = $data['service_charge'] * $data['price_original']; - $data['amount'] -= $data['service_charge']; + if ($feeFromGold) { + $data['payment_amount'] = $data['total_price'] + $data['service_charge_price']; + } else { + $data['payment_amount'] = $data['total_price']; + $data['amount'] -= $data['service_charge']; + } if ($wallet->cash < $data['payment_amount']) { throw new UnprocessableEntityHttpException('موجودی شما کمتر از میزان سفارش است'); } @@ -420,22 +426,30 @@ class SingleOrderController extends Controller private function processSellTransaction(&$data, $wallet, $wallet_interface) { $response = []; + $config = app(ConfigService::class); + $feeFromGold = $config->feeDeductFromGold(); $data['price'] -= $data['difference']; $grossTotal = $data['amount'] * $data['price']; $this->enforceProductOrderLimits($data, (float) $grossTotal); $data['service_charge'] = isset($data['fee_free']) && $data['fee_free'] === true ? 0 : $data['service_charge_percentage'] * $data['amount']; $data['service_charge_price'] = $data['service_charge'] * $data['price_original']; - $data['total_price'] = $data['amount'] * $data['price'] - $data['service_charge_price']; - $data['payment_amount'] = $data['total_price']; - $data['payment_amount'] -= $data['service_charge_price']; - if ($wallet->gold < $data['amount']) { + if ($feeFromGold) { + $data['total_price'] = $data['amount'] * $data['price']; + $data['payment_amount'] = $data['total_price']; + $goldDebit = $data['amount'] + $data['service_charge']; + } else { + $data['total_price'] = $data['amount'] * $data['price'] - $data['service_charge_price']; + $data['payment_amount'] = $data['total_price']; + $goldDebit = $data['amount']; + } + if ($wallet->gold < $goldDebit) { throw new UnprocessableEntityHttpException($this->insufficientGoldBalanceMessage($data)); } $data['order'] = $order = Order::create($data); $abshode = $this->resolveAbshodeProductContext($data); $productId = (int) ($data['product_id'] ?? $abshode['id'] ?? 0); $sellTransaction = [ - "gold" => $data['amount'], + "gold" => $goldDebit, "cash" => $data['total_price'], 'expire_at' => now(), 'active' => true, diff --git a/app/Models/Withdraw.php b/app/Models/Withdraw.php index 7ca344b..a8ea536 100644 --- a/app/Models/Withdraw.php +++ b/app/Models/Withdraw.php @@ -89,12 +89,13 @@ class Withdraw extends Model ->where('note', 'like', '%برداشت%'); }) ->orWhere(function ($deduct) { - $deduct->where('cash', '<', 0); + $deduct->where('cash', '<', 0) + ->orWhere('gold', '<', 0); }); }); } - /** واریز: ورود پول/طلا به کیف (درگاه، شارژ ادمین، پاداش) */ + /** واریز: ورود پول/طلا به کیف (درگاه، شارژ ادمین، پاداش) — بدون کسر ادمین */ public static function scopeDeposits($query) { return $query->where(function ($q) { @@ -105,7 +106,14 @@ class Withdraw extends Model })->where(function ($excludeWithdrawNotes) { $excludeWithdrawNotes->whereNull('note') ->orWhere('note', 'not like', '%برداشت%'); - })->whereNotBetween('sts', [50, 52]); + })->whereNotBetween('sts', [50, 52]) + ->where(function ($positive) { + $positive->where(function ($cash) { + $cash->whereNull('cash')->orWhere('cash', '>=', 0); + })->where(function ($gold) { + $gold->whereNull('gold')->orWhere('gold', '>=', 0); + }); + }); }); } } diff --git a/app/Services/AssetSummaryService.php b/app/Services/AssetSummaryService.php index db2074a..b2d819a 100644 --- a/app/Services/AssetSummaryService.php +++ b/app/Services/AssetSummaryService.php @@ -3,6 +3,7 @@ namespace App\Services; use App\Http\Controllers\Controller; +use App\Models\Order; use App\Models\Price; use App\Models\Product; use Illuminate\Support\Collection; @@ -60,6 +61,13 @@ class AssetSummaryService $mainWalletProductIds ); $totalRialValue = $cashWallet + $assetsRialValue; + $portfolio = $this->buildPortfolioInsight( + $customerId, + $metalTotals, + $cashWallet, + $assetsRialValue, + $totalRialValue + ); return [ 'meta' => [ @@ -78,6 +86,78 @@ class AssetSummaryService 'metal_totals' => $metalTotals, 'assets_rial_value' => round($assetsRialValue, 0), 'total_rial_value' => round($totalRialValue, 0), + 'profit_loss' => $portfolio['unrealized_profit_rial'], + 'portfolio' => $portfolio, + ]; + } + + /** + * خلاصه دارایی برای داشبورد — ارزش کل، تفکیک نقد/فلز، سود/زیان تحقق‌نیافته طلا. + * + * @return array{ + * total_rial_value: float, + * cash_rial: float, + * metal_rial_value: float, + * gold_weight: float, + * gold_rial_value: float, + * average_buy_price: float|null, + * cost_basis_rial: float|null, + * unrealized_profit_rial: float|null, + * unrealized_profit_percent: float|null, + * live_gold_price: float + * } + */ + private function buildPortfolioInsight( + int $customerId, + array $metalTotals, + float $cashWallet, + float $assetsRialValue, + float $totalRialValue + ): array { + $goldMetal = collect($metalTotals)->first(fn (array $row) => (int) ($row['metal_id'] ?? 0) === 1); + $goldWeight = (float) ($goldMetal['total_weight'] ?? 0); + $liveGoldPrice = $this->resolveLivePricePerUnit(1); + $goldRialValue = $goldWeight > 0 && $liveGoldPrice > 0 + ? round($goldWeight * $liveGoldPrice, 0) + : 0.0; + + $averageBuyPrice = null; + $costBasisRial = null; + $unrealizedProfitRial = null; + $unrealizedProfitPercent = null; + + if ($goldWeight > 0) { + $buyOrders = Order::query() + ->where('customer_id', $customerId) + ->where('direction', 1) + ->where('sts', 2) + ->where('amount', '>', 0) + ->get(['amount', 'price']); + + $totalBought = (float) $buyOrders->sum('amount'); + $weightedSum = (float) $buyOrders->sum(fn (Order $order) => (float) $order->price * (float) $order->amount); + + if ($totalBought > 0 && $weightedSum > 0) { + $averageBuyPrice = round($weightedSum / $totalBought, 0); + $costBasisRial = round($averageBuyPrice * $goldWeight, 0); + $unrealizedProfitRial = round($goldRialValue - $costBasisRial, 0); + if ($costBasisRial > 0) { + $unrealizedProfitPercent = round(($unrealizedProfitRial / $costBasisRial) * 100, 2); + } + } + } + + return [ + 'total_rial_value' => round($totalRialValue, 0), + 'cash_rial' => round($cashWallet, 0), + 'metal_rial_value' => round($assetsRialValue, 0), + 'gold_weight' => round($goldWeight, 3), + 'gold_rial_value' => $goldRialValue, + 'average_buy_price' => $averageBuyPrice, + 'cost_basis_rial' => $costBasisRial, + 'unrealized_profit_rial' => $unrealizedProfitRial, + 'unrealized_profit_percent' => $unrealizedProfitPercent, + 'live_gold_price' => round($liveGoldPrice, 0), ]; } diff --git a/app/Services/ConfigService.php b/app/Services/ConfigService.php index 1d76d0e..092d98e 100644 --- a/app/Services/ConfigService.php +++ b/app/Services/ConfigService.php @@ -139,6 +139,15 @@ class ConfigService return (float) config('fee.service_charge_percentage', 0); } + /** + * روشن: کارمزد از مقدار طلا (فروش = واریز ریالی گرد دقیق، خرید = دریافت کامل طلا). + * خاموش (پیش‌فرض): کارمزد از ریال در فروش و از طلا در خرید — رفتار قبلی. + */ + public function feeDeductFromGold(): bool + { + return $this->getBool('fee_deduct_from_gold', false); + } + /** * sell / buy / price_last — یک منبع حقیقت برای lastPrice و buildFromDatabase. * diff --git a/app/Services/WalletBalanceService.php b/app/Services/WalletBalanceService.php index 202a91e..bc6f963 100644 --- a/app/Services/WalletBalanceService.php +++ b/app/Services/WalletBalanceService.php @@ -242,12 +242,16 @@ class WalletBalanceService public function enrichWalletTableRow(mixed &$row, array $catalogMetalIds, int $mainWalletMetalId): void { $metalMap = $this->walletLineMetalGoldMap($row, $catalogMetalIds, $mainWalletMetalId); + $meta = $this->buildWalletTableRowMeta($row, $mainWalletMetalId); if (is_array($row)) { $row['metal_gold'] = $metalMap; foreach ($catalogMetalIds as $metalId) { $row['metal_gold_' . $metalId] = $metalMap[(string) $metalId] ?? 0.0; } + foreach ($meta as $key => $value) { + $row[$key] = $value; + } return; } @@ -257,9 +261,60 @@ class WalletBalanceService foreach ($catalogMetalIds as $metalId) { $row->{'metal_gold_' . $metalId} = $metalMap[(string) $metalId] ?? 0.0; } + foreach ($meta as $key => $value) { + $row->{$key} = $value; + } } } + /** + * @return array + */ + private function buildWalletTableRowMeta(mixed $row, int $mainWalletMetalId): array + { + $fundId = $this->lineValue($row, 'fund_id'); + $productId = $this->lineValue($row, 'product_id'); + $product = $this->lineProduct($row); + $fund = $this->lineFund($row); + $cash = (float) ($this->lineValue($row, 'cash') ?? 0); + + if ($fundId !== null) { + $kind = 'fund'; + $kindLabel = 'قلک (صندوق)'; + $fundTitle = trim((string) ($fund?->title ?? '')); + $subject = $fundTitle !== '' ? "قلک «{$fundTitle}»" : 'قلک بدون عنوان'; + } elseif ($productId === null) { + $kind = 'main'; + $kindLabel = 'کیف اصلی'; + $subject = 'کیف اصلی — ریال و طلای آبشده'; + } else { + $kind = 'product'; + $kindLabel = 'کیف محصول'; + $title = trim((string) ($product?->title ?? '')); + $subject = $title !== '' ? "محصول «{$title}»" : "محصول کد {$productId}"; + } + + $metalId = $this->resolveWalletLineMetalId($row, $mainWalletMetalId); + $goldGrams = $this->walletLineGoldGrams($row); + $goldToCash = $goldGrams > 0 ? (int) round($goldGrams * $this->livePricePerGram($metalId)) : 0; + + return [ + 'wallet_kind' => $kind, + 'wallet_kind_label' => $kindLabel, + 'wallet_subject' => $subject, + 'product_title' => $product?->title ?? null, + 'product_group_type' => $product?->group_type ?? null, + 'product_unit' => $product?->unit ?? null, + 'product_amount' => $product?->amount ?? null, + 'fund_title' => $fund?->title ?? null, + 'wallet_metal_id' => $metalId, + 'wallet_metal_label' => $this->balanceReportService->metalLabel($metalId), + 'gold_to_cash' => $goldToCash, + 'gold_grams' => round($goldGrams, 3), + 'total_rial_value' => (int) round($cash + $goldToCash), + ]; + } + private function livePricePerGram(int $metalId): float { if ($metalId === 1) { @@ -303,6 +358,21 @@ class WalletBalanceService return null; } + private function lineFund(mixed $line): ?object + { + if (is_object($line) && isset($line->fund)) { + return $line->fund; + } + + if (is_array($line) && isset($line['fund'])) { + $fund = $line['fund']; + + return is_object($fund) ? $fund : (object) $fund; + } + + return null; + } + private function walletLineGoldGrams(mixed $line): float { $gold = (float) ($this->lineValue($line, 'gold') ?? 0); diff --git a/database/sql/deploy_settings_const.sql b/database/sql/deploy_settings_const.sql index 1f4f0ba..d32b69d 100644 --- a/database/sql/deploy_settings_const.sql +++ b/database/sql/deploy_settings_const.sql @@ -421,6 +421,10 @@ INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `de SELECT 'fee_service_charge_percentage', '0.005', 'درصد کارمزد سفارش', 1, 1, NULL, 'number' 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 'fee_deduct_from_gold', 'false', 'کارمزد از مقدار طلا', 1, 1, NULL, 'switch' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'fee_deduct_from_gold' LIMIT 1); + INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) SELECT 'min_withdraw', '', 'حداقل برداشت (ریال)', 1, 1, NULL, 'price' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'min_withdraw' LIMIT 1); @@ -888,6 +892,7 @@ UPDATE `settings` SET `description` = 'اختلاف پایه (ریال) بین UPDATE `settings` SET `description` = 'مبلغ اضافه (ریال) روی قیمت پایه برای سمت خرید. خالی = استفاده از «اختلاف پایه».' WHERE `key` = 'buy_fee'; UPDATE `settings` SET `description` = 'مبلغ کسر (ریال) از قیمت پایه برای سمت فروش. خالی = استفاده از «اختلاف پایه».' WHERE `key` = 'sell_fee'; UPDATE `settings` SET `description` = 'درصد کارمزد هر سفارش از مبلغ معامله (اعشاری؛ مثلاً ۰٫۰۰۵ یعنی نیم در هزار).' WHERE `key` = 'fee_service_charge_percentage'; +UPDATE `settings` SET `description` = 'روشن: کارمزد از مقدار طلا کسر می‌شود — در فروش مبلغ ریالی دقیق واریز می‌شود. خاموش: کارمزد از ریال فروش کم می‌شود (رفتار قبلی).' WHERE `key` = 'fee_deduct_from_gold'; UPDATE `settings` SET `description` = 'تعداد اولین سفارش‌های هر کاربر که از کارمزد معاف می‌شوند. ۰ = غیرفعال.' WHERE `key` = 'fee_free'; UPDATE `settings` SET `description` = 'حداقل مبلغ درخواست برداشت وجه (ریال). درخواست‌های کمتر رد می‌شوند.' WHERE `key` = 'min_withdraw'; UPDATE `settings` SET `description` = 'حداقل مبلغ اولین خرید هر کاربر (ریال). برای فعال‌سازی کیف یا احراز سطح اول استفاده می‌شود.' WHERE `key` = 'min_first_buy'; @@ -1018,6 +1023,7 @@ UPDATE `settings` SET `title_fa` = 'فعال‌سازی API سفارش خارج UPDATE `settings` SET `title_fa` = 'کارمزد (قدیمی — درصد)' WHERE `key` = 'fee'; UPDATE `settings` SET `title_fa` = 'تعداد سفارش اول بدون کارمزد' WHERE `key` = 'fee_free'; UPDATE `settings` SET `title_fa` = 'درصد کارمزد سفارش' WHERE `key` = 'fee_service_charge_percentage'; +UPDATE `settings` SET `title_fa` = 'کارمزد از مقدار طلا' WHERE `key` = 'fee_deduct_from_gold'; UPDATE `settings` SET `title_fa` = 'اختلاف پایه نمایش' WHERE `key` = 'fee_single'; UPDATE `settings` SET `title_fa` = 'پاداش معرف‌شونده' WHERE `key` = 'gift_for_referral'; UPDATE `settings` SET `title_fa` = 'پاداش معرف' WHERE `key` = 'gift_for_referrer'; 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 ce23350..67f5a4d 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 @@ -419,6 +419,10 @@ INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `de SELECT 'fee_service_charge_percentage', '0.005', 'درصد کارمزد سفارش', 1, 1, NULL, 'number' 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 'fee_deduct_from_gold', 'false', 'کارمزد از مقدار طلا', 1, 1, NULL, 'switch' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'fee_deduct_from_gold' LIMIT 1); + INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) SELECT 'min_withdraw', '', 'حداقل برداشت (ریال)', 1, 1, NULL, 'price' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'min_withdraw' LIMIT 1); diff --git a/database/sql/fee_deduct_from_gold_patch.sql b/database/sql/fee_deduct_from_gold_patch.sql new file mode 100644 index 0000000..3ea7590 --- /dev/null +++ b/database/sql/fee_deduct_from_gold_patch.sql @@ -0,0 +1,15 @@ +-- سوییچ «کارمزد از مقدار طلا» — برای نصب‌های موجود (مثلاً abshodehrahimi) +-- پس از deploy کد، این فایل را یک‌بار روی DB اجرا کنید؛ سپس در ادمین > تنظیمات > کارمزد روشن کنید. + +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'fee_deduct_from_gold', 'false', 'کارمزد از مقدار طلا', 1, 1, + 'روشن: کارمزد از طلا — فروش = واریز ریالی دقیق. خاموش: کارمزد از ریال فروش (پیش‌فرض).', + 'switch' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'fee_deduct_from_gold' LIMIT 1); + +UPDATE `settings` +SET `title_fa` = 'کارمزد از مقدار طلا', + `description` = 'روشن: کارمزد از مقدار طلا کسر می‌شود — در فروش مبلغ ریالی دقیق واریز می‌شود. خاموش: کارمزد از ریال فروش کم می‌شود (رفتار قبلی).', + `type` = 'switch', + `can_edit` = 1 +WHERE `key` = 'fee_deduct_from_gold'; diff --git a/packages/rooyesh/wallet/src/models/Wallet.php b/packages/rooyesh/wallet/src/models/Wallet.php index e74f9fc..13b5ea1 100644 --- a/packages/rooyesh/wallet/src/models/Wallet.php +++ b/packages/rooyesh/wallet/src/models/Wallet.php @@ -14,7 +14,7 @@ class Wallet extends Model use HasFactory; public function customer() { - return $this->belongsTo('App\Models\Customer','owner')->select(['id','name']); + return $this->belongsTo('App\Models\Customer','owner')->select(['id','name','mobile']); } public function fund() {