2026-08-16 22:00:23 +03:30
|
|
|
<?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);
|
|
|
|
|
|
2026-08-17 13:11:00 +03:30
|
|
|
if (isset($payload['buy']) && (int) $payload['buy'] > 0 && $buyExtra !== 0) {
|
|
|
|
|
$payload['buy'] = max(0, (int) $payload['buy'] + $buyExtra);
|
2026-08-16 22:00:23 +03:30
|
|
|
}
|
2026-08-17 13:11:00 +03:30
|
|
|
if (isset($payload['sell']) && (int) $payload['sell'] > 0 && $sellExtra !== 0) {
|
2026-08-16 22:00:23 +03:30
|
|
|
$payload['sell'] = max(0, (int) $payload['sell'] - $sellExtra);
|
|
|
|
|
}
|
2026-08-17 13:11:00 +03:30
|
|
|
if (isset($payload['price_last']) && (int) $payload['price_last'] > 0 && $buyExtra !== 0) {
|
|
|
|
|
$payload['price_last'] = max(0, (int) $payload['price_last'] + $buyExtra);
|
2026-08-16 22:00:23 +03:30
|
|
|
}
|
2026-08-17 13:11:00 +03:30
|
|
|
if (isset($payload['priceLast']) && (int) $payload['priceLast'] > 0 && $buyExtra !== 0) {
|
|
|
|
|
$payload['priceLast'] = max(0, (int) $payload['priceLast'] + $buyExtra);
|
2026-08-16 22:00:23 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$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,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|