diff --git a/app/Http/Controllers/Admin/CustomerController.php b/app/Http/Controllers/Admin/CustomerController.php index f285833..de43545 100644 --- a/app/Http/Controllers/Admin/CustomerController.php +++ b/app/Http/Controllers/Admin/CustomerController.php @@ -204,14 +204,16 @@ class CustomerController extends SuperController $catalogMetalIds = $balanceService->catalogMetalIds(); $mainWalletMetalId = $balanceService->resolveMainWalletMetalId(); $productCols = implode(',', self::WALLET_PRODUCT_COLUMNS); + $ownerType = (int) (Customer::find($customer_id)?->type ?? 0); - // فقط کیف محصول‌ها (نه کیف اصلی) — هر ردیف = موجودی واقعی یک محصول/فلز + // فقط کیف محصول آزاد (نه کیف اصلی، نه قلک) — هر ردیف = یک دارایی $query_table = Wallet::with([ 'product' => static function ($q) use ($productCols) { $q->withTrashed()->select(explode(',', $productCols)); }, ]) ->where('owner', $customer_id) + ->whereNull('fund_id') ->whereNotNull('product_id'); if ($mainWalletProductIds !== []) { // کیف‌های قدیمیِ محصولات متصل به کیف اصلی نمایش داده نمی‌شوند @@ -240,11 +242,15 @@ class CustomerController extends SuperController ->first(); if ($representative && $mainWallet) { + $freeGold = $balanceService->effectiveMainWalletGold((int) $customer_id, $ownerType); + $fundGold = $balanceService->fundGoldForMainWalletProducts((int) $customer_id, $ownerType); $mainRow = [ 'id' => $mainWallet->id, 'owner' => (int) $customer_id, 'cash' => 0, - 'gold' => (float) $mainWallet->gold, + 'gold' => $freeGold + $fundGold, + 'free_gold' => $freeGold, + 'fund_gold' => $fundGold, 'fund_id' => null, 'product_id' => (int) $representative->id, 'created_at' => $mainWallet->created_at, @@ -260,7 +266,6 @@ class CustomerController extends SuperController if (isset($query_table['rows'])) { foreach ($query_table['rows'] as &$row) { $balanceService->enrichWalletTableRow($row, $catalogMetalIds, $mainWalletMetalId); - // اگر eager-load محصول نیامده بود، از lookup پر کن $productId = (int) (is_array($row) ? ($row['product_id'] ?? 0) : ($row->product_id ?? 0)); $hasProduct = is_array($row) ? ! empty($row['product']) @@ -278,12 +283,149 @@ class CustomerController extends SuperController $balanceService->enrichWalletTableRow($row, $catalogMetalIds, $mainWalletMetalId); } } + + // جمع دارایی = آزاد + قلک‌های همان محصول (برای ردیف اصلی قبلاً ست شده) + $isMain = is_array($row) + ? ! empty($row['is_main_wallet']) + : ! empty($row->is_main_wallet); + if ($productId > 0 && ! $isMain) { + $freeGold = $balanceService->freeProductGold((int) $customer_id, $productId, $ownerType); + $fundGold = $balanceService->fundGoldForProduct((int) $customer_id, $productId, $ownerType); + if (is_array($row)) { + $row['free_gold'] = $freeGold; + $row['fund_gold'] = $fundGold; + $row['gold'] = $freeGold + $fundGold; + } else { + $row->free_gold = $freeGold; + $row->fund_gold = $fundGold; + $row->gold = $freeGold + $fundGold; + } + } } unset($row); } return response()->json($query_table); } + + /** جدول قلک‌های واقعی مشتری (مدل Fund) */ + public function piggyFunds(Request $request, $customer_id) + { + $query_table = \App\Models\Fund::with(['wallet', 'product']) + ->where('customer_id', $customer_id); + + TableEngine::create($query_table, $request) + ->initSearch(['title']) + ->select(['*']) + ->export($query_table); + + if (isset($query_table['rows'])) { + foreach ($query_table['rows'] as &$row) { + $wallets = is_array($row) + ? collect($row['wallet'] ?? []) + : collect($row->wallet ?? []); + $gold = (float) $wallets->sum('gold'); + if (is_array($row)) { + $row['gold'] = $gold; + $row['product_title'] = $row['product']['title'] ?? ($row['product']->title ?? null); + } else { + $row->gold = $gold; + $row->product_title = $row->product?->title; + } + } + unset($row); + } + + return response()->json($query_table); + } + + /** قلک‌های یک دارایی (محصول) */ + public function assetPiggyFunds(Request $request, $customer_id, $product_id) + { + $request->merge(['product_id' => (int) $product_id]); + $query_table = \App\Models\Fund::with(['wallet', 'product']) + ->where('customer_id', $customer_id) + ->where('product_id', (int) $product_id); + + TableEngine::create($query_table, $request) + ->initSearch(['title']) + ->select(['*']) + ->export($query_table); + + if (isset($query_table['rows'])) { + foreach ($query_table['rows'] as &$row) { + $wallets = is_array($row) + ? collect($row['wallet'] ?? []) + : collect($row->wallet ?? []); + $gold = (float) $wallets->sum('gold'); + if (is_array($row)) { + $row['gold'] = $gold; + } else { + $row->gold = $gold; + } + } + unset($row); + } + + return response()->json($query_table); + } + + /** تراکنش‌های مربوط به یک دارایی (کیف آزاد + قلک‌های همان محصول) */ + public function assetTransactions(Request $request, $customer_id, $product_id) + { + $config = app(ConfigService::class); + $productId = (int) $product_id; + $walletIds = Wallet::query() + ->where('owner', $customer_id) + ->where(function ($q) use ($config, $productId) { + if ($config->isMainWalletProduct($productId)) { + $q->where(function ($inner) { + $inner->whereNull('fund_id')->whereNull('product_id'); + })->orWhere(function ($inner) use ($productId) { + $inner->whereNotNull('fund_id') + ->where(function ($f) use ($productId) { + $f->where('product_id', $productId)->orWhereNull('product_id'); + }); + }); + } else { + $q->where('product_id', $productId); + } + }) + ->pluck('id'); + + $records = \Rooyesh\Wallet\models\Transaction::whereIn('wallet', $walletIds); + TableEngine::create($records, $request->all()) + ->initSearch([]) + ->select([ + '*', + \Rooyesh\Wallet\models\Transaction::$SELECT_CAJ, + \Rooyesh\Wallet\models\Transaction::$SELECT_TYPE_STR, + ])->export($records); + + return response()->json($records); + } + + /** تراکنش‌های یک قلک */ + public function piggyFundTransactions(Request $request, $customer_id, $fund_id) + { + $fund = \App\Models\Fund::where('customer_id', $customer_id)->findOrFail($fund_id); + $walletIds = Wallet::query() + ->where('owner', $customer_id) + ->where('fund_id', $fund->id) + ->pluck('id'); + + $records = \Rooyesh\Wallet\models\Transaction::whereIn('wallet', $walletIds); + TableEngine::create($records, $request->all()) + ->initSearch([]) + ->select([ + '*', + \Rooyesh\Wallet\models\Transaction::$SELECT_CAJ, + \Rooyesh\Wallet\models\Transaction::$SELECT_TYPE_STR, + ])->export($records); + + return response()->json($records); + } + public function archive($id){ $query = Customer::findOrFail($id); $query->update([ diff --git a/app/Http/Controllers/Customer/FundController.php b/app/Http/Controllers/Customer/FundController.php index 508b40f..a2c6f9a 100644 --- a/app/Http/Controllers/Customer/FundController.php +++ b/app/Http/Controllers/Customer/FundController.php @@ -2,96 +2,243 @@ namespace App\Http\Controllers\Customer; -use App\DB\Te\TableEngine; -use App\Http\Controllers\Controller; use App\Http\Controllers\SuperController; use App\Models\Fund; +use App\Models\Product; +use App\Services\ConfigService; +use App\Services\WalletBalanceService; use Illuminate\Http\Request; use Rooyesh\Wallet\interfaces\TransactionInterface; use Rooyesh\Wallet\interfaces\WalletInterface; -use Rooyesh\Wallet\models\Transaction; use Rooyesh\Wallet\models\Wallet; use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; +/** + * قلک per-product — انتقال فقط TransactionInterface؛ بدون سند حسابداری. + */ class FundController extends SuperController { public function __construct(Request $request) { $model = Fund::class; - $this->with=['wallet']; + $this->with = ['wallet', 'product']; $select = ['*']; - $search=['name']; - $request->request->add(['customer_id'=>auth('customer')->id()]); - $query = Fund::with($this->with)->where('customer_id',auth('customer')->id()); - parent::__construct($model, $this->with, $select, null,$query,$search); + $search = ['title']; + $request->request->add(['customer_id' => auth('customer')->id()]); + $query = Fund::with($this->with)->where('customer_id', auth('customer')->id()); + parent::__construct($model, $this->with, $select, null, $query, $search); } - public function deposit(Request $request){ + + public function store(Request $request) + { + $this->assertProductFundsEnabled(); + $data = $request->all(); - $wallet_interface = new WalletInterface(auth('customer')->id(), auth('customer')->user()->type,null); - $wallet = $wallet_interface->wallet; - if($wallet->gold < $data['value']){ + $productId = (int) ($data['product_id'] ?? 0); + if ($productId <= 0) { + throw new UnprocessableEntityHttpException('انتخاب محصول برای قلک الزامی است'); + } + + $product = Product::query()->find($productId); + if (! $product) { + throw new UnprocessableEntityHttpException('محصول یافت نشد'); + } + + $title = trim((string) ($data['title'] ?? '')); + if ($title === '') { + throw new UnprocessableEntityHttpException('عنوان قلک الزامی است'); + } + + $customerId = (int) auth('customer')->id(); + $fund = Fund::create([ + 'title' => $title, + 'file_id' => $data['file_id'] ?? null, + 'customer_id' => $customerId, + 'product_id' => $productId, + ]); + + // کیف قلک را از همان ابتدا با product_id بساز (بدون سند حسابداری) + new WalletInterface( + $customerId, + auth('customer')->user()->type, + $fund->id, + $productId + ); + + return response()->json($fund->load(['wallet', 'product'])); + } + + public function update(Request $request, $id) + { + $this->assertProductFundsEnabled(); + + $fund = $this->ownedFund((int) $id); + $title = trim((string) ($request->input('title') ?? $fund->title)); + if ($title === '') { + throw new UnprocessableEntityHttpException('عنوان قلک الزامی است'); + } + + // product_id بعد از ساخت عوض نمی‌شود + $fund->update([ + 'title' => $title, + 'file_id' => $request->input('file_id', $fund->file_id), + ]); + + return response()->json($fund->fresh(['wallet', 'product'])); + } + + public function deposit(Request $request) + { + $this->assertProductFundsEnabled(); + + $data = $request->all(); + $fund = $this->ownedFund((int) ($data['fund_id'] ?? 0)); + $value = (float) ($data['value'] ?? 0); + if ($value <= 0) { + throw new UnprocessableEntityHttpException('مقدار نامعتبر است'); + } + + $customer = auth('customer')->user(); + $customerId = (int) $customer->id; + $ownerType = (int) $customer->type; + $productId = (int) ($fund->product_id ?? 0); + + $balanceService = app(WalletBalanceService::class); + $free = $productId > 0 + ? $balanceService->freeProductGold($customerId, $productId, $ownerType) + : $balanceService->effectiveMainWalletGold($customerId, $ownerType); + + if ($free < $value) { throw new UnprocessableEntityHttpException('موجودی کمتر از مقدار وارد شده است'); } - $transaction = new TransactionInterface($wallet_interface, $_inp = [ - "gold" => $data['value'], - "fund_id"=>$data["fund_id"], - 'expire_at'=>now(), - 'active'=>true + [$sourceFundId, $sourceProductId] = $this->sourceWalletKeys($productId); + $source = new WalletInterface($customerId, $ownerType, $sourceFundId, $sourceProductId); + $dest = new WalletInterface($customerId, $ownerType, $fund->id, $productId > 0 ? $productId : null); + + // فقط جابه‌جایی داخلی — بدون Accounting* + new TransactionInterface($source, [ + 'gold' => $value, + 'fund_id' => $fund->id, + 'product_id' => $productId > 0 ? $productId : null, + 'expire_at' => now(), + 'active' => true, ], 6, false); - $wallet_interface_fund = new WalletInterface(auth('customer')->id(), auth('customer')->user()->type,$data["fund_id"]); - $transaction = new TransactionInterface($wallet_interface_fund, $_inp = [ - "gold" => $data['value'], - "fund_id"=>$data["fund_id"], - 'expire_at'=>now(), - 'active'=>true + + new TransactionInterface($dest, [ + 'gold' => $value, + 'fund_id' => $fund->id, + 'product_id' => $productId > 0 ? $productId : null, + 'expire_at' => now(), + 'active' => true, ], 5, true); + return response()->json(true); } - public function withdraw(Request $request){ - $data = $request->all(); - $wallet_interface_fund = new WalletInterface(auth('customer')->id(), auth('customer')->user()->type,$data["fund_id"]); - $wallet = $wallet_interface_fund->wallet; - if($wallet->gold < $data['value']){ - throw new UnprocessableEntityHttpException('موجودی کمتر از مقدار وارد شده است'); - } - $transaction = new TransactionInterface($wallet_interface_fund, $_inp = [ - "gold" => $data['value'], - "fund_id"=>$data["fund_id"], - 'expire_at'=>now(), - 'active'=>true - ], 6, false); - $wallet_interface = new WalletInterface(auth('customer')->id(), auth('customer')->user()->type); - $transaction = new TransactionInterface($wallet_interface, $_inp = [ - "gold" => $data['value'], - "fund_id"=>$data["fund_id"], - 'expire_at'=>now(), - 'active'=>true - ], 5, true); - return response()->json(true); - } - public function show($id){ - $query = Fund::with($this->with)->findOrFail($id); - return response()->json($query); - } - public function destroy($id){ - $wallet_interface_fund = Wallet::where('owner',auth('customer')->id())->where('fund_id',$id)->first(); - if (!is_null($wallet_interface_fund)){ - if ($wallet_interface_fund->gold > 0){ - throw new UnprocessableEntityHttpException('به علت داشتن موجودی امکان حذف صندوق وجود ندارد'); - } + public function withdraw(Request $request) + { + $this->assertProductFundsEnabled(); + + $data = $request->all(); + $fund = $this->ownedFund((int) ($data['fund_id'] ?? 0)); + $value = (float) ($data['value'] ?? 0); + if ($value <= 0) { + throw new UnprocessableEntityHttpException('مقدار نامعتبر است'); } - Fund::destroy($id); + + $customer = auth('customer')->user(); + $customerId = (int) $customer->id; + $ownerType = (int) $customer->type; + $productId = (int) ($fund->product_id ?? 0); + + $source = new WalletInterface($customerId, $ownerType, $fund->id, $productId > 0 ? $productId : null); + if ((float) $source->wallet->gold < $value) { + throw new UnprocessableEntityHttpException('موجودی کمتر از مقدار وارد شده است'); + } + + [$destFundId, $destProductId] = $this->sourceWalletKeys($productId); + $dest = new WalletInterface($customerId, $ownerType, $destFundId, $destProductId); + + new TransactionInterface($source, [ + 'gold' => $value, + 'fund_id' => $fund->id, + 'product_id' => $productId > 0 ? $productId : null, + 'expire_at' => now(), + 'active' => true, + ], 6, false); + + new TransactionInterface($dest, [ + 'gold' => $value, + 'fund_id' => $fund->id, + 'product_id' => $productId > 0 ? $productId : null, + 'expire_at' => now(), + 'active' => true, + ], 5, true); + return response()->json(true); } - public function transactionTable(Request $request){ - $records = Transaction::where('wallet',$request->wallet_id); - TableEngine::create($records, $request->all()) - ->initSearch([]) - ->select([ - '*',Transaction::$SELECT_CAJ,Transaction::$SELECT_TYPE_STR - ])->export($records); - return response()->json($records); + + public function show($id) + { + $fund = $this->ownedFund((int) $id); + + return response()->json($fund->load(['wallet', 'product'])); + } + + public function destroy($id) + { + $this->assertProductFundsEnabled(); + + $fund = $this->ownedFund((int) $id); + $wallet = Wallet::where('owner', auth('customer')->id()) + ->where('fund_id', $fund->id) + ->first(); + + if ($wallet && (float) $wallet->gold > 0) { + throw new UnprocessableEntityHttpException('به علت داشتن موجودی امکان حذف قلک وجود ندارد'); + } + + Fund::destroy($fund->id); + + return response()->json(true); + } + + private function assertProductFundsEnabled(): void + { + if (! app(ConfigService::class)->isProductFundsEnabled()) { + throw new UnprocessableEntityHttpException('قابلیت قلک محصول غیرفعال است'); + } + } + + private function ownedFund(int $id): Fund + { + if ($id <= 0) { + throw new UnprocessableEntityHttpException('قلک نامعتبر است'); + } + + $fund = Fund::with(['wallet', 'product']) + ->where('customer_id', auth('customer')->id()) + ->find($id); + + if (! $fund) { + throw new UnprocessableEntityHttpException('قلک یافت نشد'); + } + + return $fund; + } + + /** @return array{0: null, 1: int|null} */ + private function sourceWalletKeys(int $productId): array + { + if ($productId <= 0) { + return [null, null]; + } + + if (app(ConfigService::class)->isMainWalletProduct($productId)) { + return [null, null]; + } + + return [null, $productId]; } } diff --git a/app/Http/Controllers/Customer/WalletController.php b/app/Http/Controllers/Customer/WalletController.php index 02f4287..f06e3fc 100644 --- a/app/Http/Controllers/Customer/WalletController.php +++ b/app/Http/Controllers/Customer/WalletController.php @@ -145,7 +145,16 @@ class WalletController extends Controller }, 'wallet.product', 'product', - ])->whereIn('wallet', $walletIds)->whereNull('fund_id'); + ])->whereIn('wallet', $walletIds); + + // جدول عمومی تراکنش‌ها: جابه‌جایی قلک را قاطی نکند؛ برای کیف قلک همهٔ ردیف‌ها بیاید + $includesFundWallet = Wallet::query() + ->whereIn('id', $walletIds) + ->whereNotNull('fund_id') + ->exists(); + if (! $includesFundWallet) { + $records->whereNull('fund_id'); + } TableEngine::create($records, $request->all()) ->initSearch(['customer.name']) diff --git a/app/Http/Controllers/Pargar/PargarInboundController.php b/app/Http/Controllers/Pargar/PargarInboundController.php index 986c524..7c338a1 100644 --- a/app/Http/Controllers/Pargar/PargarInboundController.php +++ b/app/Http/Controllers/Pargar/PargarInboundController.php @@ -323,9 +323,9 @@ class PargarInboundController extends Controller $walletInterface = new WalletInterface($customer->id, $ownerType); $wallet = $walletInterface->wallet; - // GoldInv پرگار فقط با طلای کیف اصلی مقایسه می‌شود — محصولات جدا سهمی ندارند + // GoldInv پرگار با جمع آزاد+قلک محصولات کیف اصلی مقایسه می‌شود (انتقال قلک سند نمی‌زند) $appGold = app(WalletBalanceService::class) - ->effectiveMainWalletGold((int) $customer->id, $ownerType); + ->accountingVisibleMainWalletGold((int) $customer->id, $ownerType); $appCash = (float) ($wallet->cash ?? 0); $goldDiff = $goldInv - $appGold; $rialDiff = $rialInv - $appCash; diff --git a/app/Models/Fund.php b/app/Models/Fund.php index dd15935..bbf67c0 100644 --- a/app/Models/Fund.php +++ b/app/Models/Fund.php @@ -9,14 +9,27 @@ use Rooyesh\Wallet\models\Wallet; class Fund extends Model { - use HasFactory,Selector,SoftDeletes; - protected $fillable =[ + use HasFactory, Selector, SoftDeletes; + + protected $fillable = [ 'title', 'file_id', 'customer_id', + 'product_id', ]; + public function wallet() { return $this->hasMany(Wallet::class); } + + public function product() + { + return $this->belongsTo(Product::class, 'product_id'); + } + + public function customer() + { + return $this->belongsTo(Customer::class, 'customer_id'); + } } diff --git a/app/Services/ConfigService.php b/app/Services/ConfigService.php index 33569c6..3329cdd 100644 --- a/app/Services/ConfigService.php +++ b/app/Services/ConfigService.php @@ -747,6 +747,12 @@ class ConfigService return $this->getBool('enable_conditional_order', false); } + /** سوییچ دیپلوی قلک per-product (can_edit=0، پیش‌فرض خاموش). */ + public function isProductFundsEnabled(): bool + { + return $this->getBool('enable_product_funds', false); + } + /** * سوییچ اصلی جیبیت (can_edit=0، پیش‌فرض روشن). * خاموش = هیچ فراخوانی احراز هویت به جیبیت انجام نمی‌شود. diff --git a/app/Services/PargarInboundWalletService.php b/app/Services/PargarInboundWalletService.php index ed3e247..1e88667 100644 --- a/app/Services/PargarInboundWalletService.php +++ b/app/Services/PargarInboundWalletService.php @@ -399,7 +399,7 @@ class PargarInboundWalletService $wallet = $walletInterface->wallet; $mainGold = app(WalletBalanceService::class) - ->effectiveMainWalletGold((int) $customer->id, $ownerType); + ->accountingVisibleMainWalletGold((int) $customer->id, $ownerType); $goldDiff = $goldInv - $mainGold; $rialDiff = $rialInv - (float) ($wallet->cash ?? 0); diff --git a/app/Services/WalletBalanceService.php b/app/Services/WalletBalanceService.php index ed09f97..0db0e66 100644 --- a/app/Services/WalletBalanceService.php +++ b/app/Services/WalletBalanceService.php @@ -77,6 +77,95 @@ class WalletBalanceService return round($mainGold + $orphanedGold, 3); } + /** + * طلای داخل قلک‌های یک محصول (یا همهٔ محصولات main_wallet اگر $productId=null و فقط main). + */ + public function fundGoldForProduct(int $ownerId, ?int $productId, ?int $ownerType = null): float + { + $query = Wallet::query() + ->where('owner', $ownerId) + ->whereNotNull('fund_id'); + if ($ownerType !== null) { + $query->where('owner_type', $ownerType); + } + if ($productId === null) { + $query->whereNull('product_id'); + } else { + $query->where('product_id', $productId); + } + + return round((float) $query->sum('gold'), 3); + } + + /** + * جمع طلای قلک‌های محصولات متصل به کیف اصلی (+ قلک‌های legacy بدون product_id). + */ + public function fundGoldForMainWalletProducts(int $ownerId, ?int $ownerType = null): float + { + $productIds = $this->mainWalletProductIds(); + $query = Wallet::query() + ->where('owner', $ownerId) + ->whereNotNull('fund_id'); + if ($ownerType !== null) { + $query->where('owner_type', $ownerType); + } + if ($productIds === []) { + $query->whereNull('product_id'); + } else { + $query->where(function ($q) use ($productIds) { + $q->whereIn('product_id', $productIds)->orWhereNull('product_id'); + }); + } + + return round((float) $query->sum('gold'), 3); + } + + /** + * موجودی قابل‌مقایسه با حسابداری برای طلای کیف اصلی = + * آزاد (effectiveMain) + قلک‌های همان محصولات. + * برای معامله استفاده نشود — فقط sync/مانده. + */ + public function accountingVisibleMainWalletGold(int $ownerId, ?int $ownerType = null): float + { + return round( + $this->effectiveMainWalletGold($ownerId, $ownerType) + + $this->fundGoldForMainWalletProducts($ownerId, $ownerType), + 3 + ); + } + + /** + * موجودی آزاد یک محصول (بدون قلک) — برای معامله و واریز به قلک. + */ + public function freeProductGold(int $ownerId, int $productId, ?int $ownerType = null): float + { + if ($this->configService->isMainWalletProduct($productId)) { + return $this->effectiveMainWalletGold($ownerId, $ownerType); + } + + $query = Wallet::query() + ->where('owner', $ownerId) + ->whereNull('fund_id') + ->where('product_id', $productId); + if ($ownerType !== null) { + $query->where('owner_type', $ownerType); + } + + return round((float) $query->sum('gold'), 3); + } + + /** + * جمع نمایشی دارایی یک محصول = آزاد + قلک‌های همان محصول. + */ + public function totalProductGold(int $ownerId, int $productId, ?int $ownerType = null): float + { + return round( + $this->freeProductGold($ownerId, $productId, $ownerType) + + $this->fundGoldForProduct($ownerId, $productId, $ownerType), + 3 + ); + } + /** * جمع طلای قابل‌نمایش: کیف اصلی + کیف محصولات + قلک‌ها. * کیف‌های legacy محصولات main_wallet_products شمرده نمی‌شوند. diff --git a/database/migrations/2026_08_01_000002_add_product_id_to_funds_table.php b/database/migrations/2026_08_01_000002_add_product_id_to_funds_table.php new file mode 100644 index 0000000..66998d2 --- /dev/null +++ b/database/migrations/2026_08_01_000002_add_product_id_to_funds_table.php @@ -0,0 +1,34 @@ +unsignedBigInteger('product_id')->nullable()->after('customer_id'); + $table->index('product_id'); + } + }); + } + + public function down(): void + { + if (! Schema::hasTable('funds') || ! Schema::hasColumn('funds', 'product_id')) { + return; + } + + Schema::table('funds', function (Blueprint $table) { + $table->dropIndex(['product_id']); + $table->dropColumn('product_id'); + }); + } +}; diff --git a/database/sql/deploy_settings_const.sql b/database/sql/deploy_settings_const.sql index 3c0cec9..5a6d5ce 100644 --- a/database/sql/deploy_settings_const.sql +++ b/database/sql/deploy_settings_const.sql @@ -399,6 +399,12 @@ SELECT 'enable_conditional_order', 'false', 'سفارش شرطی', 0, 1, 'switch' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'enable_conditional_order' LIMIT 1); +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'enable_product_funds', 'false', 'قلک محصول', 0, 1, + 'روشن: ساخت و مدیریت قلک per-product در اپ مشتری فعال است (یک محصول در هر قلک؛ انتقال بدون سند حسابداری). خاموش: منو و API ساخت قلک جدید در دسترس نیست. فقط از دیپلوی قابل تغییر است.', + 'switch' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'enable_product_funds' LIMIT 1); + INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) SELECT 'has_page_price_trigger', 'true', 'هشدار قیمت', 0, 1, 'روشن: منوی هشدار قیمت در اپ کاربر نمایش داده می‌شود. خاموش: منو و صفحه مخفی است. فقط از دیپلوی قابل تغییر است.', @@ -1077,7 +1083,7 @@ UPDATE `settings` SET `type` = 'switch' WHERE `key` IN ( 'require_zero_main_gold_before_other_buy', 'profit_gold_api_enabled', 'payping_round_toman', 'payping_strict_failed_withdraw', 'has_page_earn_money', 'has_page_price_trigger', 'has_page_notification', 'charge_notif', 'group_sms', 'sms_panel', 'customer_direct_sms', - 'enable_conditional_order', + 'enable_conditional_order', 'enable_product_funds', 'sms_notif_order_admin', 'sms_notif_order_customer', 'sms_notif_withdraw_admin', 'sms_notif_withdraw_otp', 'sms_notif_login_otp', 'sms_notif_gateway_charge_admin', 'sms_notif_register_customer', 'sms_notif_register_admin', @@ -1551,6 +1557,8 @@ UPDATE `settings` SET `title_fa` = 'پیامک تکی به مشتری از لی UPDATE `settings` SET `description` = 'روشن: سرویس احراز هویت جیبیت (تطبیق کد ملی/موبایل و کارت بانکی) فعال است. خاموش: هیچ فراخوانی به جیبیت انجام نمی‌شود و احراز هویت خودکار کار نمی‌کند. فقط از صفحه دیپلوی قابل تغییر است.' WHERE `key` = 'jibit_enabled'; UPDATE `settings` SET `title_fa` = 'احراز هویت جیبیت' WHERE `key` = 'jibit_enabled'; UPDATE `settings` SET `title_fa` = 'سفارش شرطی' WHERE `key` = 'enable_conditional_order'; +UPDATE `settings` SET `title_fa` = 'قلک محصول' WHERE `key` = 'enable_product_funds'; +UPDATE `settings` SET `description` = 'روشن: ساخت و مدیریت قلک per-product در اپ مشتری فعال است (یک محصول در هر قلک؛ انتقال بدون سند حسابداری). خاموش: منو و API ساخت قلک جدید در دسترس نیست. فقط از دیپلوی قابل تغییر است.' WHERE `key` = 'enable_product_funds'; UPDATE `settings` SET `description` = 'روشن: امکان ارسال پیامک گروهی از پنل ادمین فعال است.' WHERE `key` = 'group_sms'; UPDATE `settings` SET `description` = 'روشن: پنل پیامک و کیف پول billing فعال است. خاموش: پیامک‌ها مثل قبل ارسال می‌شوند بدون billing.' WHERE `key` = 'sms_panel'; @@ -1805,6 +1813,7 @@ UPDATE `settings` SET `can_edit` = 0 WHERE `key` IN ( 'group_sms', 'customer_direct_sms', 'enable_conditional_order', + 'enable_product_funds', 'has_page_price_trigger', 'has_page_notification', 'jibit_enabled', 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 f4a2308..e622b77 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 @@ -354,6 +354,12 @@ SELECT 'enable_conditional_order', 'false', 'سفارش شرطی', 0, 1, 'switch' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'enable_conditional_order' LIMIT 1); +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'enable_product_funds', 'false', 'قلک محصول', 0, 1, + 'روشن: ساخت و مدیریت قلک per-product در اپ مشتری فعال است (یک محصول در هر قلک؛ انتقال بدون سند حسابداری). خاموش: منو و API ساخت قلک جدید در دسترس نیست. فقط از دیپلوی قابل تغییر است.', + 'switch' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'enable_product_funds' LIMIT 1); + INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) SELECT 'has_page_price_trigger', 'true', 'هشدار قیمت', 0, 1, 'روشن: منوی هشدار قیمت در اپ کاربر نمایش داده می‌شود. خاموش: منو و صفحه مخفی است. فقط از دیپلوی قابل تغییر است.', @@ -861,7 +867,7 @@ UPDATE `settings` SET `type` = 'switch' WHERE `key` IN ( 'fee_deduct_from_gold', 'send_to_tahesab', 'profit_gold_api_enabled', 'payping_round_toman', 'payping_strict_failed_withdraw', 'has_page_earn_money', 'has_page_price_trigger', 'has_page_notification', 'charge_notif', 'group_sms', 'sms_panel', 'customer_direct_sms', - 'enable_conditional_order', + 'enable_conditional_order', 'enable_product_funds', 'sms_notif_order_admin', 'sms_notif_order_customer', 'sms_notif_withdraw_admin', 'sms_notif_withdraw_otp', 'sms_notif_login_otp', 'sms_notif_gateway_charge_admin', 'sms_notif_register_customer', 'sms_notif_register_admin', @@ -1333,6 +1339,9 @@ UPDATE `settings` SET `title_fa` = 'پیامک تکی به مشتری از لی UPDATE `settings` SET `description` = 'روشن: سرویس احراز هویت جیبیت (تطبیق کد ملی/موبایل و کارت بانکی) فعال است. خاموش: هیچ فراخوانی به جیبیت انجام نمی‌شود و احراز هویت خودکار کار نمی‌کند. فقط از صفحه دیپلوی قابل تغییر است.' WHERE `key` = 'jibit_enabled'; UPDATE `settings` SET `title_fa` = 'احراز هویت جیبیت' WHERE `key` = 'jibit_enabled'; UPDATE `settings` SET `title_fa` = 'سفارش شرطی' WHERE `key` = 'enable_conditional_order'; +UPDATE `settings` SET `title_fa` = 'قلک محصول' WHERE `key` = 'enable_product_funds'; +UPDATE `settings` SET `description` = 'روشن: ساخت و مدیریت قلک per-product در اپ مشتری فعال است (یک محصول در هر قلک؛ انتقال بدون سند حسابداری). خاموش: منو و API ساخت قلک جدید در دسترس نیست. فقط از دیپلوی قابل تغییر است.' WHERE `key` = 'enable_product_funds'; +UPDATE `settings` SET `can_edit` = 0 WHERE `key` = 'enable_product_funds'; UPDATE `settings` SET `description` = 'روشن: بعد از شارژ یا کسر کیف توسط ادمین، پیامک به مشتری و ادمین‌های دارای «ارسال پیامک» می‌رود. خاموش: هیچ‌کدام ارسال نمی‌شود.' WHERE `key` = 'charge_notif'; UPDATE `settings` SET `description` = 'روشن: بعد از خرید یا فروش موفق، پیامک خلاصه معامله به ادمین‌های دارای «ارسال پیامک» می‌رود. خاموش: ادمین پیامک معامله نمی‌گیرد.' WHERE `key` = 'sms_notif_order_admin'; UPDATE `settings` SET `description` = 'روشن: بعد از خرید یا فروش موفق، پیامک رسید به مشتری می‌رود. خاموش: مشتری پیامک معامله نمی‌گیرد.' WHERE `key` = 'sms_notif_order_customer'; @@ -1401,6 +1410,7 @@ UPDATE `settings` SET `can_edit` = 0 WHERE `key` IN ( 'group_sms', 'customer_direct_sms', 'enable_conditional_order', + 'enable_product_funds', 'has_page_price_trigger', 'has_page_notification', 'jibit_enabled', diff --git a/routes/admin.php b/routes/admin.php index fe381d7..80430ea 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -118,6 +118,10 @@ Route::middleware('auth:user')->group(function (){ Route::get('/customer/list',[CustomerController::class,'listUser']); Route::post('/customers/{customer_id}/referrals',[CustomerController::class,'referrals']); Route::post('/customers/{customer_id}/funds',[CustomerController::class,'funds']); + Route::post('/customers/{customer_id}/piggy-funds',[CustomerController::class,'piggyFunds']); + Route::post('/customers/{customer_id}/assets/{product_id}/piggy-funds',[CustomerController::class,'assetPiggyFunds']); + Route::post('/customers/{customer_id}/assets/{product_id}/transactions',[CustomerController::class,'assetTransactions']); + Route::post('/customers/{customer_id}/piggy-funds/{fund_id}/transactions',[CustomerController::class,'piggyFundTransactions']); Route::put('/customer/{id}',[CustomerController::class,'updateAdmin']); Route::resource('/customer', CustomerController::class)->except(['update']);