added setting for user
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\DB\Te\TableEngine;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Customer;
|
||||
use App\Models\CustomerSpecialPricePackage;
|
||||
use App\Models\Product;
|
||||
use App\Services\ConfigService;
|
||||
use App\Services\CustomerSpecialPricingService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
||||
|
||||
class CustomerSpecialPricePackageController extends Controller
|
||||
{
|
||||
public function table(Request $request)
|
||||
{
|
||||
$this->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('قابلیت قیمت ویژه مشتری غیرفعال است.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
|
||||
class CustomerSpecialPricePackage extends Model
|
||||
{
|
||||
protected $table = 'customer_special_price_packages';
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'profit_buy',
|
||||
'profit_sell',
|
||||
'show_product',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'profit_buy' => '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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\CustomerSpecialPricePackage;
|
||||
use App\Models\Product;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
/**
|
||||
* قیمت ویژه مشتری — افزون بر buy_fee/sell_fee و cash_fee محصول.
|
||||
* تداخل: آخرین پکیج (updated_at، سپس id).
|
||||
*/
|
||||
class CustomerSpecialPricingService
|
||||
{
|
||||
public const SETTING_KEY = 'customer_special_pricing';
|
||||
|
||||
public function tablesReady(): bool
|
||||
{
|
||||
return Schema::hasTable('customer_special_price_packages')
|
||||
&& Schema::hasTable('customer_special_price_package_customer')
|
||||
&& Schema::hasTable('customer_special_price_package_product');
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
if (! $this->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<int, array{package_id:int,title:string,profit_buy:int,profit_sell:int,show_product:bool}>
|
||||
*/
|
||||
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<int, Product>|iterable<Product> $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<int> $customerIds
|
||||
* @param list<int> $productIds
|
||||
* @return list<string>
|
||||
*/
|
||||
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,
|
||||
];
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('customer_special_price_packages')) {
|
||||
Schema::create('customer_special_price_packages', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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']);
|
||||
|
||||
Reference in New Issue
Block a user