From bd8284e4f8e7ae37cf534bd691ff141820f3ee8e Mon Sep 17 00:00:00 2001 From: adonis Date: Sun, 16 Aug 2026 22:00:23 +0330 Subject: [PATCH] added setting for user --- .../CustomerSpecialPricePackageController.php | 264 ++++++++++++++++ .../Controllers/Admin/ProductController.php | 5 + .../Single/SingleOrderController.php | 34 ++- .../Site/PriceStreamController.php | 11 + .../Controllers/Site/ReportController.php | 30 +- app/Models/CustomerSpecialPricePackage.php | 51 ++++ .../CustomerSpecialPricingService.php | 287 ++++++++++++++++++ ...customer_special_price_packages_tables.php | 60 ++++ database/sql/deploy_settings_const.sql | 15 +- ...ngs_const_preserve_value_sync_can_edit.sql | 15 +- routes/admin.php | 9 + 11 files changed, 769 insertions(+), 12 deletions(-) create mode 100644 app/Http/Controllers/Admin/CustomerSpecialPricePackageController.php create mode 100644 app/Models/CustomerSpecialPricePackage.php create mode 100644 app/Services/CustomerSpecialPricingService.php create mode 100644 database/migrations/2026_08_16_000001_create_customer_special_price_packages_tables.php diff --git a/app/Http/Controllers/Admin/CustomerSpecialPricePackageController.php b/app/Http/Controllers/Admin/CustomerSpecialPricePackageController.php new file mode 100644 index 0000000..74a339f --- /dev/null +++ b/app/Http/Controllers/Admin/CustomerSpecialPricePackageController.php @@ -0,0 +1,264 @@ +assertFeatureEnabled(); + + $query = CustomerSpecialPricePackage::query() + ->withCount(['customers', 'products']) + ->orderByDesc('id'); + + TableEngine::create($query, $request->all()) + ->initSearch(['title']) + ->select(['*']) + ->export($query); + + return $query; + } + + public function show(int $id) + { + $this->assertFeatureEnabled(); + + $package = CustomerSpecialPricePackage::query() + ->with([ + 'customers:id,name,mobile', + 'products:id,title,group_type', + ]) + ->findOrFail($id); + + $categoryNames = []; + if (Schema::hasTable('customer_special_price_package_category')) { + $categoryNames = DB::table('customer_special_price_package_category') + ->where('package_id', $package->id) + ->pluck('category_name') + ->all(); + } + + return response()->json([ + 'package' => $package, + 'customer_ids' => $package->customers->pluck('id')->values(), + 'product_ids' => $package->products->pluck('id')->values(), + 'category_names' => array_values($categoryNames), + 'customers' => $package->customers, + 'products' => $package->products, + ]); + } + + public function store(Request $request) + { + $this->assertFeatureEnabled(); + $data = $this->validatedPayload($request); + + $warnings = app(CustomerSpecialPricingService::class)->overlapWarnings( + $data['customer_ids'], + $data['product_ids'] + ); + + $package = DB::transaction(function () use ($data) { + $package = CustomerSpecialPricePackage::create([ + 'title' => $data['title'], + 'profit_buy' => $data['profit_buy'], + 'profit_sell' => $data['profit_sell'], + 'show_product' => $data['show_product'], + 'is_active' => $data['is_active'], + ]); + $this->syncRelations($package, $data); + + return $package->fresh(); + }); + + return response()->json([ + 'message' => 'پکیج قیمت ویژه ثبت شد.', + 'package' => $package, + 'warnings' => $warnings, + ]); + } + + public function update(Request $request, int $id) + { + $this->assertFeatureEnabled(); + $package = CustomerSpecialPricePackage::findOrFail($id); + $data = $this->validatedPayload($request); + + $warnings = app(CustomerSpecialPricingService::class)->overlapWarnings( + $data['customer_ids'], + $data['product_ids'], + (int) $package->id + ); + + DB::transaction(function () use ($package, $data) { + $package->update([ + 'title' => $data['title'], + 'profit_buy' => $data['profit_buy'], + 'profit_sell' => $data['profit_sell'], + 'show_product' => $data['show_product'], + 'is_active' => $data['is_active'], + ]); + $this->syncRelations($package, $data); + }); + + return response()->json([ + 'message' => 'پکیج قیمت ویژه به‌روز شد.', + 'package' => $package->fresh(), + 'warnings' => $warnings, + ]); + } + + public function destroy(int $id) + { + $this->assertFeatureEnabled(); + $package = CustomerSpecialPricePackage::findOrFail($id); + + DB::transaction(function () use ($package) { + $package->customers()->detach(); + $package->products()->detach(); + if (Schema::hasTable('customer_special_price_package_category')) { + DB::table('customer_special_price_package_category')->where('package_id', $package->id)->delete(); + } + $package->delete(); + }); + + return response()->json(['message' => 'پکیج حذف شد.']); + } + + public function toggleActive(int $id) + { + $this->assertFeatureEnabled(); + $package = CustomerSpecialPricePackage::findOrFail($id); + $package->is_active = ! $package->is_active; + $package->save(); + + return response()->json([ + 'message' => $package->is_active ? 'پکیج فعال شد.' : 'پکیج غیرفعال شد.', + 'is_active' => $package->is_active, + ]); + } + + public function searchCustomers(Request $request) + { + $this->assertFeatureEnabled(); + $q = trim((string) $request->get('q', '')); + + $rows = Customer::query() + ->where('type', 2) + ->where(function ($query) { + $query->where('is_archive', false)->orWhereNull('is_archive'); + }) + ->when($q !== '', function ($query) use ($q) { + $query->where(function ($inner) use ($q) { + $inner->where('name', 'like', '%' . $q . '%') + ->orWhere('mobile', 'like', '%' . $q . '%') + ->orWhere('id', $q); + }); + }) + ->orderByDesc('id') + ->limit(40) + ->get(['id', 'name', 'mobile']); + + return response()->json(['customers' => $rows]); + } + + public function productsByCategories(Request $request) + { + $this->assertFeatureEnabled(); + $names = array_values(array_filter(array_map('strval', (array) $request->input('category_names', [])))); + + if ($names === []) { + return response()->json(['products' => []]); + } + + $products = Product::query() + ->whereIn('group_type', $names) + ->orderBy('title') + ->get(['id', 'title', 'group_type', 'active']); + + return response()->json(['products' => $products]); + } + + private function validatedPayload(Request $request): array + { + $title = trim((string) $request->input('title', '')); + if ($title === '') { + throw new UnprocessableEntityHttpException('عنوان پکیج الزامی است.'); + } + + $customerIds = array_values(array_unique(array_filter(array_map('intval', (array) $request->input('customer_ids', []))))); + $productIds = array_values(array_unique(array_filter(array_map('intval', (array) $request->input('product_ids', []))))); + $categoryNames = array_values(array_unique(array_filter(array_map( + fn ($n) => trim((string) $n), + (array) $request->input('category_names', []) + )))); + + if ($customerIds === []) { + throw new UnprocessableEntityHttpException('حداقل یک کاربر انتخاب کنید.'); + } + if ($categoryNames === []) { + throw new UnprocessableEntityHttpException('حداقل یک گروه دسته‌بندی انتخاب کنید.'); + } + if ($productIds === []) { + throw new UnprocessableEntityHttpException('حداقل یک محصول انتخاب کنید.'); + } + + $profitBuy = (int) preg_replace('/[^\d]/', '', (string) $request->input('profit_buy', 0)); + $profitSell = (int) preg_replace('/[^\d]/', '', (string) $request->input('profit_sell', 0)); + + return [ + 'title' => $title, + 'customer_ids' => $customerIds, + 'product_ids' => $productIds, + 'category_names' => $categoryNames, + 'profit_buy' => max(0, $profitBuy), + 'profit_sell' => max(0, $profitSell), + 'show_product' => filter_var($request->input('show_product', true), FILTER_VALIDATE_BOOLEAN), + 'is_active' => filter_var($request->input('is_active', true), FILTER_VALIDATE_BOOLEAN), + ]; + } + + private function syncRelations(CustomerSpecialPricePackage $package, array $data): void + { + $package->customers()->sync($data['customer_ids']); + $package->products()->sync($data['product_ids']); + + if (Schema::hasTable('customer_special_price_package_category')) { + DB::table('customer_special_price_package_category')->where('package_id', $package->id)->delete(); + $rows = []; + foreach ($data['category_names'] as $name) { + $rows[] = [ + 'package_id' => $package->id, + 'category_name' => $name, + ]; + } + if ($rows !== []) { + DB::table('customer_special_price_package_category')->insert($rows); + } + } + } + + private function assertFeatureEnabled(): void + { + if (! app(CustomerSpecialPricingService::class)->tablesReady()) { + throw new UnprocessableEntityHttpException('جدول پکیج قیمت ویژه وجود ندارد. مایگریشن را اجرا کنید.'); + } + + if (! app(ConfigService::class)->getBool(CustomerSpecialPricingService::SETTING_KEY, false)) { + throw new UnprocessableEntityHttpException('قابلیت قیمت ویژه مشتری غیرفعال است.'); + } + } +} diff --git a/app/Http/Controllers/Admin/ProductController.php b/app/Http/Controllers/Admin/ProductController.php index 14ad250..b29d808 100644 --- a/app/Http/Controllers/Admin/ProductController.php +++ b/app/Http/Controllers/Admin/ProductController.php @@ -572,6 +572,11 @@ class ProductController extends SuperController $products->each(function (Product $product) use ($displayPriceService) { $product->setAttribute('show', $displayPriceService->resolveCustomerShow($product)); }); + + $customerId = (int) (auth('customer')->id() ?? 0); + if ($customerId > 0) { + app(\App\Services\CustomerSpecialPricingService::class)->applyToProducts($products, $customerId); + } } /** diff --git a/app/Http/Controllers/Single/SingleOrderController.php b/app/Http/Controllers/Single/SingleOrderController.php index 32b5cbb..48d41d4 100644 --- a/app/Http/Controllers/Single/SingleOrderController.php +++ b/app/Http/Controllers/Single/SingleOrderController.php @@ -1451,10 +1451,42 @@ class SingleOrderController extends Controller ]; } + $customerId = (int) (auth('customer')->id() ?? 0); + if ($customerId <= 0) { + $customerId = (int) (request()->input('customer_id') ?? 0); + } + $specialSvc = app(\App\Services\CustomerSpecialPricingService::class); + $specialRule = $specialSvc->ruleForCustomerProduct($customerId, (int) ($product->id ?? 0)); + if ($specialRule && ! ($specialRule['show_product'] ?? true)) { + throw new UnprocessableEntityHttpException('این محصول برای شما در دسترس نیست.'); + } + $specialAmt = $specialSvc->profitForDirection($specialRule, $direction); + if ($specialAmt > 0) { + $amount += $specialAmt; + $keys[] = 'customer_special_pricing'; + $lines[] = [ + 'kind' => 'special_pricing', + 'key' => 'customer_special_pricing', + 'label' => 'قیمت ویژه مشتری · ' . ($specialRule['title'] ?? '') . ' · ' . $side, + 'is_percent' => false, + 'raw' => $specialAmt, + 'per_unit' => $specialAmt, + 'detail' => number_format($specialAmt) . ' ریال / گرم', + 'package_id' => $specialRule['package_id'] ?? null, + ]; + } + $hasApp = collect($lines)->contains(fn ($l) => ($l['kind'] ?? '') === 'app_spread'); $hasProduct = collect($lines)->contains(fn ($l) => ($l['kind'] ?? '') === 'product_spread'); + $hasSpecial = collect($lines)->contains(fn ($l) => ($l['kind'] ?? '') === 'special_pricing'); - if ($hasApp && $hasProduct) { + if ($hasSpecial && ($hasApp || $hasProduct)) { + $source = 'app+product+special'; + $label = 'اختلاف پایه/محصول + قیمت ویژه مشتری = ' . number_format($amount) . ' ریال / گرم'; + } elseif ($hasSpecial) { + $source = 'special'; + $label = ($lines[array_key_last($lines)]['label'] ?? 'قیمت ویژه مشتری') . ' = ' . number_format($amount) . ' ریال'; + } elseif ($hasApp && $hasProduct) { $source = 'app+product'; $label = 'قیمت صفحه اصلی اپلیکیشن + تب اختلاف محصول = ' . number_format($amount) . ' ریال / گرم'; } elseif ($hasApp) { diff --git a/app/Http/Controllers/Site/PriceStreamController.php b/app/Http/Controllers/Site/PriceStreamController.php index d148e12..145d487 100644 --- a/app/Http/Controllers/Site/PriceStreamController.php +++ b/app/Http/Controllers/Site/PriceStreamController.php @@ -54,6 +54,10 @@ class PriceStreamController extends Controller $cache = app(LivePriceCacheService::class); $last = $cache->get() ?? $cache->buildFromDatabase(); if (is_array($last) && $last !== []) { + if ($customerId > 0) { + $last = app(\App\Services\CustomerSpecialPricingService::class) + ->applyToLastPayload($last, $customerId); + } $send('last', app(ZaarboxPriceService::class)->attachGoldOunceToPayload($last)); } @@ -62,6 +66,13 @@ class PriceStreamController extends Controller $send('list', $list); } + if ($customerId > 0) { + $send( + 'special-pricing', + app(\App\Services\CustomerSpecialPricingService::class)->clientPayloadForCustomer($customerId) + ); + } + $customerLastId = 0; if ($customerId > 0) { $assets = app(AssetSummaryService::class)->buildForCustomer( diff --git a/app/Http/Controllers/Site/ReportController.php b/app/Http/Controllers/Site/ReportController.php index d9abe5d..810d8aa 100644 --- a/app/Http/Controllers/Site/ReportController.php +++ b/app/Http/Controllers/Site/ReportController.php @@ -197,14 +197,14 @@ class ReportController extends Controller if ($cached !== null) { $cached['gold_ounce'] = $this->resolveGoldOuncePayload(); - return $cached; + return $this->personalizeLastPriceForCustomer($cached); } // فقط وقتی کران اخیراً tick زده — وگرنه API زنده (جلوگیری از قفل قیمت) if ($cache->hasRecentTick()) { $dbBuilt = $cache->buildFromDatabase(); if (($dbBuilt['price'] ?? 0) > 0) { - return $this->appendUpPrices($dbBuilt, false); + return $this->personalizeLastPriceForCustomer($this->appendUpPrices($dbBuilt, false)); } } } @@ -226,7 +226,7 @@ class ReportController extends Controller ); } - return $this->appendUpPrices($storePayload, $forceRefresh); + return $this->personalizeLastPriceForCustomer($this->appendUpPrices($storePayload, $forceRefresh)); } $closed = false; @@ -246,7 +246,7 @@ class ReportController extends Controller }; $zeroResponse = function () use ($forceRefresh) { - return $this->appendUpPrices([ + return $this->personalizeLastPriceForCustomer($this->appendUpPrices([ 'sell' => 0, 'buy' => 0, 'price_last' => 0, @@ -256,7 +256,7 @@ class ReportController extends Controller 'zaarbox' => 0, 'closed' => true, 'time' => now(), - ], $forceRefresh); + ], $forceRefresh)); }; $priceProvider = $config->priceProvider(); @@ -440,7 +440,7 @@ class ReportController extends Controller $priceSource = 'database_last'; } else { // مظنه زنده نداریم و show_last_price خاموش است → نه «بسته»، بلکه «مظنه در دسترس نیست» - return $this->appendUpPrices([ + return $this->personalizeLastPriceForCustomer($this->appendUpPrices([ 'sell' => 0, 'buy' => 0, 'price_last' => 0, @@ -451,7 +451,7 @@ class ReportController extends Controller 'closed' => false, 'status' => 'no_quote', 'time' => now(), - ], $forceRefresh); + ], $forceRefresh)); } } @@ -485,7 +485,7 @@ class ReportController extends Controller ); } - return $this->appendUpPrices([ + return $this->personalizeLastPriceForCustomer($this->appendUpPrices([ 'sell' => $sell, 'buy' => $buy, 'price_last' => $priceLast, @@ -496,7 +496,19 @@ class ReportController extends Controller 'zaarbox' => $zaarbox_price ?? 0, 'closed' => false, 'time' => now(), - ], $forceRefresh); + ], $forceRefresh)); + } + + /** اختلاف پکیج ویژه — فقط روی پاسخ مشتری؛ کش مشترک را آلوده نمی‌کند */ + protected function personalizeLastPriceForCustomer(array $payload): array + { + $customerId = (int) (auth('customer')->id() ?? 0); + if ($customerId <= 0) { + return $payload; + } + + return app(\App\Services\CustomerSpecialPricingService::class) + ->applyToLastPayload($payload, $customerId); } protected function recordHalfHourPriceSnapshot( diff --git a/app/Models/CustomerSpecialPricePackage.php b/app/Models/CustomerSpecialPricePackage.php new file mode 100644 index 0000000..baa9902 --- /dev/null +++ b/app/Models/CustomerSpecialPricePackage.php @@ -0,0 +1,51 @@ + 'integer', + 'profit_sell' => 'integer', + 'show_product' => 'boolean', + 'is_active' => 'boolean', + ]; + + public function customers(): BelongsToMany + { + return $this->belongsToMany( + Customer::class, + 'customer_special_price_package_customer', + 'package_id', + 'customer_id' + )->withPivot([]); + } + + public function products(): BelongsToMany + { + return $this->belongsToMany( + Product::class, + 'customer_special_price_package_product', + 'package_id', + 'product_id' + )->withPivot([]); + } + + public function scopeActive($query) + { + return $query->where('is_active', true); + } +} diff --git a/app/Services/CustomerSpecialPricingService.php b/app/Services/CustomerSpecialPricingService.php new file mode 100644 index 0000000..0a48389 --- /dev/null +++ b/app/Services/CustomerSpecialPricingService.php @@ -0,0 +1,287 @@ +tablesReady()) { + return false; + } + + return app(ConfigService::class)->getBool(self::SETTING_KEY, false); + } + + /** + * @return array{package_id:int,title:string,profit_buy:int,profit_sell:int,show_product:bool}|null + */ + public function ruleForCustomerProduct(int $customerId, int $productId): ?array + { + if (! $this->isEnabled() || $customerId <= 0 || $productId <= 0) { + return null; + } + + $row = CustomerSpecialPricePackage::query() + ->active() + ->whereHas('customers', fn ($q) => $q->where('customers.id', $customerId)) + ->whereHas('products', fn ($q) => $q->where('products.id', $productId)) + ->orderByDesc('updated_at') + ->orderByDesc('id') + ->first(['id', 'title', 'profit_buy', 'profit_sell', 'show_product', 'updated_at']); + + if (! $row) { + return null; + } + + return [ + 'package_id' => (int) $row->id, + 'title' => (string) $row->title, + 'profit_buy' => (int) ($row->profit_buy ?? 0), + 'profit_sell' => (int) ($row->profit_sell ?? 0), + 'show_product' => (bool) $row->show_product, + ]; + } + + /** + * قوانین مؤثر برای مشتری — هر محصول یک قانون (آخرین پکیج). + * + * @return array + */ + public function rulesMapForCustomer(int $customerId): array + { + if (! $this->isEnabled() || $customerId <= 0) { + return []; + } + + $packages = CustomerSpecialPricePackage::query() + ->active() + ->whereHas('customers', fn ($q) => $q->where('customers.id', $customerId)) + ->with(['products:id']) + ->orderByDesc('updated_at') + ->orderByDesc('id') + ->get(['id', 'title', 'profit_buy', 'profit_sell', 'show_product', 'updated_at']); + + $map = []; + foreach ($packages as $package) { + foreach ($package->products as $product) { + $pid = (int) $product->id; + if (isset($map[$pid])) { + continue; + } + $map[$pid] = [ + 'package_id' => (int) $package->id, + 'title' => (string) $package->title, + 'profit_buy' => (int) ($package->profit_buy ?? 0), + 'profit_sell' => (int) ($package->profit_sell ?? 0), + 'show_product' => (bool) $package->show_product, + ]; + } + } + + return $map; + } + + public function profitForDirection(?array $rule, int $direction): int + { + if (! $rule) { + return 0; + } + + return $direction === 2 + ? (int) ($rule['profit_sell'] ?? 0) + : (int) ($rule['profit_buy'] ?? 0); + } + + /** + * اعمال روی payload تابلو last (buy/sell/price_last) برای محصول اصلی یا اولین قانون آبشده. + */ + public function applyToLastPayload(array $payload, int $customerId): array + { + if (! $this->isEnabled() || $customerId <= 0) { + return $payload; + } + + $mainId = app(ConfigService::class)->mainProductId(); + $rule = null; + if ($mainId) { + $rule = $this->ruleForCustomerProduct($customerId, (int) $mainId); + } + + if (! $rule) { + $map = $this->rulesMapForCustomer($customerId); + $rule = $map ? reset($map) : null; + if (is_array($rule) && ! ($rule['show_product'] ?? true)) { + // فقط برای اختلاف قیمت؛ مخفی‌سازی روی لیست محصول است + } + } + + if (! $rule) { + return $payload; + } + + $buyExtra = (int) ($rule['profit_buy'] ?? 0); + $sellExtra = (int) ($rule['profit_sell'] ?? 0); + + if (isset($payload['buy']) && (int) $payload['buy'] > 0 && $buyExtra > 0) { + $payload['buy'] = (int) $payload['buy'] + $buyExtra; + } + if (isset($payload['sell']) && (int) $payload['sell'] > 0 && $sellExtra > 0) { + $payload['sell'] = max(0, (int) $payload['sell'] - $sellExtra); + } + if (isset($payload['price_last']) && (int) $payload['price_last'] > 0 && $buyExtra > 0) { + $payload['price_last'] = (int) $payload['price_last'] + $buyExtra; + } + if (isset($payload['priceLast']) && (int) $payload['priceLast'] > 0 && $buyExtra > 0) { + $payload['priceLast'] = (int) $payload['priceLast'] + $buyExtra; + } + + $payload['special_pricing'] = [ + 'package_id' => $rule['package_id'], + 'title' => $rule['title'], + 'profit_buy' => $buyExtra, + 'profit_sell' => $sellExtra, + ]; + + return $payload; + } + + /** + * روی کالکشن محصول: ویژه را روی مدل می‌نشاند و در صورت نیاز show را false می‌کند. + * + * @param Collection|iterable $products + */ + public function applyToProducts($products, int $customerId): void + { + if (! $this->isEnabled() || $customerId <= 0) { + return; + } + + $map = $this->rulesMapForCustomer($customerId); + if ($map === []) { + return; + } + + $collection = $products instanceof Collection ? $products : collect($products); + foreach ($collection as $product) { + if (! $product instanceof Product) { + continue; + } + $pid = (int) ($product->id ?? 0); + $rule = $map[$pid] ?? null; + if (! $rule) { + continue; + } + + $product->setAttribute('special_profit_buy', (int) $rule['profit_buy']); + $product->setAttribute('special_profit_sell', (int) $rule['profit_sell']); + $product->setAttribute('special_package_id', (int) $rule['package_id']); + $product->setAttribute('special_package_title', (string) $rule['title']); + + if (! ($rule['show_product'] ?? true)) { + $product->setAttribute('show', false); + } + } + } + + /** + * هشدار تداخل: مشتری+محصول در پکیج دیگری هم هست. + * + * @param list $customerIds + * @param list $productIds + * @return list + */ + public function overlapWarnings(array $customerIds, array $productIds, ?int $excludePackageId = null): array + { + if (! $this->tablesReady() || $customerIds === [] || $productIds === []) { + return []; + } + + $customerIds = array_values(array_unique(array_map('intval', $customerIds))); + $productIds = array_values(array_unique(array_map('intval', $productIds))); + + $q = DB::table('customer_special_price_packages as p') + ->join('customer_special_price_package_customer as pc', 'pc.package_id', '=', 'p.id') + ->join('customer_special_price_package_product as pp', 'pp.package_id', '=', 'p.id') + ->join('customers as c', 'c.id', '=', 'pc.customer_id') + ->join('products as pr', 'pr.id', '=', 'pp.product_id') + ->where('p.is_active', true) + ->whereIn('pc.customer_id', $customerIds) + ->whereIn('pp.product_id', $productIds) + ->select([ + 'p.id as package_id', + 'p.title as package_title', + 'c.id as customer_id', + 'c.name as customer_name', + 'pr.id as product_id', + 'pr.title as product_title', + ]); + + if ($excludePackageId) { + $q->where('p.id', '!=', $excludePackageId); + } + + $rows = $q->limit(50)->get(); + if ($rows->isEmpty()) { + return []; + } + + $warnings = []; + foreach ($rows as $row) { + $warnings[] = sprintf( + 'تداخل: مشتری «%s» (#%d) و محصول «%s» (#%d) در پکیج «%s» (#%d) هم هست — آخرین پکیج اعمال می‌شود.', + $row->customer_name ?: '-', + (int) $row->customer_id, + $row->product_title ?: '-', + (int) $row->product_id, + $row->package_title ?: '-', + (int) $row->package_id + ); + } + + return $warnings; + } + + /** payload سبک برای SSE / me */ + public function clientPayloadForCustomer(int $customerId): array + { + $map = $this->rulesMapForCustomer($customerId); + $products = []; + foreach ($map as $productId => $rule) { + $products[] = array_merge(['product_id' => (int) $productId], $rule); + } + + $mainId = app(ConfigService::class)->mainProductId(); + $board = null; + if ($mainId && isset($map[(int) $mainId])) { + $board = $map[(int) $mainId]; + } elseif ($products !== []) { + $board = $products[0]; + } + + return [ + 'enabled' => $this->isEnabled(), + 'products' => $products, + 'board' => $board, + ]; + } +} diff --git a/database/migrations/2026_08_16_000001_create_customer_special_price_packages_tables.php b/database/migrations/2026_08_16_000001_create_customer_special_price_packages_tables.php new file mode 100644 index 0000000..b93edb1 --- /dev/null +++ b/database/migrations/2026_08_16_000001_create_customer_special_price_packages_tables.php @@ -0,0 +1,60 @@ +id(); + $table->string('title'); + $table->unsignedBigInteger('profit_buy')->default(0); + $table->unsignedBigInteger('profit_sell')->default(0); + $table->boolean('show_product')->default(true); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + if (! Schema::hasTable('customer_special_price_package_customer')) { + Schema::create('customer_special_price_package_customer', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('package_id'); + $table->unsignedBigInteger('customer_id'); + $table->unique(['package_id', 'customer_id'], 'cspp_customer_unique'); + $table->index('customer_id', 'cspp_customer_id_idx'); + }); + } + + if (! Schema::hasTable('customer_special_price_package_category')) { + Schema::create('customer_special_price_package_category', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('package_id'); + $table->string('category_name'); + $table->unique(['package_id', 'category_name'], 'cspp_category_unique'); + }); + } + + if (! Schema::hasTable('customer_special_price_package_product')) { + Schema::create('customer_special_price_package_product', function (Blueprint $table) { + $table->id(); + $table->unsignedBigInteger('package_id'); + $table->unsignedBigInteger('product_id'); + $table->unique(['package_id', 'product_id'], 'cspp_product_unique'); + $table->index('product_id', 'cspp_product_id_idx'); + }); + } + } + + public function down(): void + { + Schema::dropIfExists('customer_special_price_package_product'); + Schema::dropIfExists('customer_special_price_package_category'); + Schema::dropIfExists('customer_special_price_package_customer'); + Schema::dropIfExists('customer_special_price_packages'); + } +}; diff --git a/database/sql/deploy_settings_const.sql b/database/sql/deploy_settings_const.sql index 5b60354..317a76c 100644 --- a/database/sql/deploy_settings_const.sql +++ b/database/sql/deploy_settings_const.sql @@ -554,6 +554,12 @@ SELECT 'group_sms', 'false', 'پیامک گروهی', 0, 1, 'switch' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'group_sms' LIMIT 1); +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'customer_special_pricing', 'false', 'قیمت ویژه مشتری', 0, 1, + 'روشن: زیرمنوی قیمت ویژه در مشتریان و اعمال سود خرید/فروش پکیج روی مظنه و سفارش برای کاربران انتخاب‌شده. خاموش: غیرفعال. فقط از دیپلوی قابل تغییر است.', + 'switch' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'customer_special_pricing' LIMIT 1); + INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) SELECT 'sms_panel', 'true', 'پنل پیامک (کیف پول)', 0, 1, 'روشن: پنل پیامک و کیف پول billing فعال است. خاموش: پیامک‌ها مثل قبل ارسال می‌شوند بدون billing.', 'switch' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'sms_panel' LIMIT 1); @@ -1159,7 +1165,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', 'has_page_gift_card', - 'enable_customer_level_profit', 'charge_notif', 'group_sms', 'sms_panel', 'customer_direct_sms', + 'enable_customer_level_profit', 'charge_notif', 'group_sms', 'customer_special_pricing', 'sms_panel', 'customer_direct_sms', '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', @@ -1774,6 +1780,12 @@ UPDATE `settings` SET `title_fa` = 'اعمال مبلغ دقیق در معامل UPDATE `settings` SET `title_fa` = 'پاداش معرف‌شونده' WHERE `key` = 'gift_for_referral'; UPDATE `settings` SET `title_fa` = 'پاداش معرف' WHERE `key` = 'gift_for_referrer'; UPDATE `settings` SET `title_fa` = 'پیامک گروهی' WHERE `key` = 'group_sms'; +UPDATE `settings` SET + `title_fa` = 'قیمت ویژه مشتری', + `type` = 'switch', + `can_edit` = 0, + `description` = 'روشن: زیرمنوی قیمت ویژه در مشتریان و اعمال سود خرید/فروش پکیج روی مظنه و سفارش برای کاربران انتخاب‌شده. خاموش: غیرفعال. فقط از دیپلوی قابل تغییر است.' +WHERE `key` = 'customer_special_pricing'; UPDATE `settings` SET `title_fa` = 'پنل پیامک (کیف پول)' WHERE `key` = 'sms_panel'; UPDATE `settings` SET `title_fa` = 'چت پشتیبانی', @@ -1931,6 +1943,7 @@ UPDATE `settings` SET `can_edit` = 0 WHERE `key` IN ( 'fee_deduct_from_gold', 'referral_bonus_amount', 'group_sms', + 'customer_special_pricing', 'customer_direct_sms', 'enable_conditional_order', 'enable_product_funds', 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 7581642..8f0f707 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 @@ -497,6 +497,12 @@ SELECT 'group_sms', 'false', 'پیامک گروهی', 0, 1, 'switch' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'group_sms' LIMIT 1); +INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) +SELECT 'customer_special_pricing', 'false', 'قیمت ویژه مشتری', 0, 1, + 'روشن: زیرمنوی قیمت ویژه در مشتریان و اعمال سود خرید/فروش پکیج روی مظنه و سفارش برای کاربران انتخاب‌شده. خاموش: غیرفعال. فقط از دیپلوی قابل تغییر است.', + 'switch' +WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'customer_special_pricing' LIMIT 1); + INSERT INTO `settings` (`key`, `value`, `title_fa`, `can_edit`, `is_active`, `description`, `type`) SELECT 'sms_panel', 'true', 'پنل پیامک (کیف پول)', 0, 1, 'روشن: پنل پیامک و کیف پول billing فعال است. خاموش: پیامک‌ها مثل قبل ارسال می‌شوند بدون billing.', 'switch' WHERE NOT EXISTS (SELECT 1 FROM `settings` WHERE `key` = 'sms_panel' LIMIT 1); @@ -898,7 +904,7 @@ UPDATE `settings` SET `type` = 'switch' WHERE `key` IN ( 'products_site_link_enabled', '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', + 'has_page_earn_money', 'has_page_price_trigger', 'has_page_notification', 'charge_notif', 'group_sms', 'customer_special_pricing', 'sms_panel', 'customer_direct_sms', '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', @@ -1408,6 +1414,12 @@ UPDATE `settings` SET `description` = 'رمز مشترک بین لاراول و UPDATE `settings` SET `title_fa` = 'پیامک پیام پشتیبانی به ادمین' WHERE `key` = 'sms_notif_support_admin'; UPDATE `settings` SET `title_fa` = 'رمز وب‌هوک چت' WHERE `key` = 'chat_webhook_secret'; UPDATE `settings` SET `description` = 'روشن: دکمه ارسال پیامک گروهی در پنل ادمین فعال است (ارسال ۱۰تایی). خاموش: ارسال پیامک گروهی غیرفعال است. فقط از تنظیمات پیشرفته.' WHERE `key` = 'group_sms'; +UPDATE `settings` SET + `title_fa` = 'قیمت ویژه مشتری', + `type` = 'switch', + `can_edit` = 0, + `description` = 'روشن: زیرمنوی قیمت ویژه در مشتریان و اعمال سود خرید/فروش پکیج روی مظنه و سفارش برای کاربران انتخاب‌شده. خاموش: غیرفعال. فقط از دیپلوی قابل تغییر است.' +WHERE `key` = 'customer_special_pricing'; UPDATE `settings` SET `description` = 'روشن: پنل پیامک و کیف پول billing فعال است. خاموش: پیامک‌ها مثل قبل ارسال می‌شوند بدون billing.' WHERE `key` = 'sms_panel'; UPDATE `settings` SET `description` = 'محصولاتی که موجودی‌شان از کیف طلای اصلی خوانده می‌شود. چند محصول با کاما. پیش‌فرض: 13' WHERE `key` = 'main_wallet_products'; @@ -1457,6 +1469,7 @@ UPDATE `settings` SET `can_edit` = 0 WHERE `key` IN ( 'external_order_api_enabled', 'referral_bonus_amount', 'group_sms', + 'customer_special_pricing', 'customer_direct_sms', 'enable_conditional_order', 'enable_product_funds', diff --git a/routes/admin.php b/routes/admin.php index f5010d0..496fb98 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -66,6 +66,15 @@ Route::middleware('auth:user')->group(function (){ Route::put('conditional_orders/{id}', [\App\Http\Controllers\Admin\ConditionalOrderController::class, 'update']); Route::put('conditional_orders/{id}/toggle', [\App\Http\Controllers\Admin\ConditionalOrderController::class, 'toggleActive']); Route::delete('conditional_orders/{id}', [\App\Http\Controllers\Admin\ConditionalOrderController::class, 'destroy']); + //------------Customer special price packages + Route::post('customer_special_price_packages/table', [\App\Http\Controllers\Admin\CustomerSpecialPricePackageController::class, 'table']); + Route::get('customer_special_price_packages/search-customers', [\App\Http\Controllers\Admin\CustomerSpecialPricePackageController::class, 'searchCustomers']); + Route::post('customer_special_price_packages/products-by-categories', [\App\Http\Controllers\Admin\CustomerSpecialPricePackageController::class, 'productsByCategories']); + Route::get('customer_special_price_packages/{id}', [\App\Http\Controllers\Admin\CustomerSpecialPricePackageController::class, 'show']); + Route::post('customer_special_price_packages', [\App\Http\Controllers\Admin\CustomerSpecialPricePackageController::class, 'store']); + Route::put('customer_special_price_packages/{id}', [\App\Http\Controllers\Admin\CustomerSpecialPricePackageController::class, 'update']); + Route::put('customer_special_price_packages/{id}/toggle', [\App\Http\Controllers\Admin\CustomerSpecialPricePackageController::class, 'toggleActive']); + Route::delete('customer_special_price_packages/{id}', [\App\Http\Controllers\Admin\CustomerSpecialPricePackageController::class, 'destroy']); //-----------product Route::post('/product/table', [ProductController::class, 'tableProduct']); Route::post('/product/archive/table', [ProductController::class, 'archiveTable']);