feat(server): evaluate price alerts per product and fix gold discharge rounding
Alerts now bind to a product_id and share one evaluator; displayed gold within a rounding step discharges the full wallet. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Mail\YourMailableClass;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Price;
|
||||
use App\Models\PriceTrigger;
|
||||
use App\Models\Product;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class PriceTriggerService
|
||||
{
|
||||
public function evaluateActive(array $payload = []): void
|
||||
{
|
||||
$triggers = PriceTrigger::query()->where('is_active', true)->get();
|
||||
if ($triggers->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$config = app(ConfigService::class);
|
||||
$display = app(ProductDisplayPriceService::class);
|
||||
$lastPrice = Price::query()->latest('id')->first();
|
||||
$boardPrice = (float) ($payload['price'] ?? $lastPrice?->price ?? 0);
|
||||
|
||||
$productsById = collect();
|
||||
if (PriceTrigger::hasProductIdColumn()) {
|
||||
$ids = $triggers->pluck('product_id')->filter()->unique()->all();
|
||||
if ($ids !== []) {
|
||||
$productsById = Product::query()->whereIn('id', $ids)->get()->keyBy('id');
|
||||
if ($productsById->isNotEmpty()) {
|
||||
$display->warmLivePriceCache();
|
||||
$display->enrichForApi($productsById);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($triggers as $trigger) {
|
||||
try {
|
||||
$this->tryFire($trigger, $productsById, $display, $config, $boardPrice);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('Price trigger evaluate failed', [
|
||||
'id' => $trigger->id,
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function tryFire(
|
||||
PriceTrigger $trigger,
|
||||
$productsById,
|
||||
ProductDisplayPriceService $display,
|
||||
ConfigService $config,
|
||||
float $boardPrice
|
||||
): void {
|
||||
$product = $this->resolveProduct($trigger, $productsById);
|
||||
$current = $this->resolveCurrentPrice($trigger, $product, $display, $config, $boardPrice);
|
||||
if ($current <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->conditionMet((string) $trigger->price_condition, $current, (float) $trigger->target_price)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$customer = Customer::query()->find($trigger->customer_id);
|
||||
if (! $customer) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->notify($trigger, $customer, $product, $current);
|
||||
|
||||
$trigger->is_active = false;
|
||||
$trigger->triggered_at = now();
|
||||
$trigger->save();
|
||||
}
|
||||
|
||||
private function resolveProduct(PriceTrigger $trigger, $productsById): ?Product
|
||||
{
|
||||
if (! PriceTrigger::hasProductIdColumn()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$productId = (int) ($trigger->product_id ?? 0);
|
||||
if ($productId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$product = $productsById->get($productId);
|
||||
if ($product instanceof Product) {
|
||||
return $product;
|
||||
}
|
||||
|
||||
return Product::query()->find($productId);
|
||||
}
|
||||
|
||||
private function resolveCurrentPrice(
|
||||
PriceTrigger $trigger,
|
||||
?Product $product,
|
||||
ProductDisplayPriceService $display,
|
||||
ConfigService $config,
|
||||
float $boardPrice
|
||||
): float {
|
||||
if ($product instanceof Product) {
|
||||
$enriched = (int) (($product->price ?? 0) + ($product->price_profit ?? 0));
|
||||
if ($enriched > 0) {
|
||||
return (float) $enriched;
|
||||
}
|
||||
|
||||
if ($display->usesAppPrice($product)) {
|
||||
$base = $boardPrice > 0 ? $boardPrice : $display->resolveOrderBasePrice($product);
|
||||
if ($base <= 0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$trade = $config->buildTradeDisplayPrices((float) $base, $config->priceProvider());
|
||||
$direction = (int) ($trigger->direction ?? 0);
|
||||
|
||||
return (float) max(0, (int) ($direction === 2 ? $trade['sell'] : $trade['buy']));
|
||||
}
|
||||
|
||||
return max(0.0, (float) $display->resolveOrderBasePrice($product));
|
||||
}
|
||||
|
||||
if ($boardPrice <= 0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$direction = (int) ($trigger->direction ?? $trigger->dirction ?? 0);
|
||||
$diff = $direction === 1
|
||||
? $config->displaySellPriceDiffRial()
|
||||
: ($direction === 2 ? $config->displayBuyPriceDiffRial() : 0.0);
|
||||
|
||||
if ($direction === 1) {
|
||||
return max(0.0, $boardPrice - $diff);
|
||||
}
|
||||
|
||||
if ($direction === 2) {
|
||||
return $boardPrice + $diff;
|
||||
}
|
||||
|
||||
return $boardPrice;
|
||||
}
|
||||
|
||||
private function conditionMet(string $op, float $current, float $target): bool
|
||||
{
|
||||
return match ($op) {
|
||||
'>' => $current > $target,
|
||||
'<' => $current < $target,
|
||||
'>=', 'gte' => $current >= $target,
|
||||
'<=', 'lte' => $current <= $target,
|
||||
'==', '=' => abs($current - $target) < 1,
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
private function notify(PriceTrigger $trigger, Customer $customer, ?Product $product, float $current): void
|
||||
{
|
||||
$title = trim((string) ($product?->title ?? ''));
|
||||
if ($title === '') {
|
||||
$title = 'طلا';
|
||||
}
|
||||
|
||||
$shopName = (string) Controller::Config('name_fa');
|
||||
$message = 'قیمت ' . $title . ' به ' . number_format((int) $current) . " رسید\n" . $shopName;
|
||||
|
||||
if ($trigger->action === 'email' && ! empty($customer->email)) {
|
||||
Mail::to($customer->email)->send(new YourMailableClass($message, '', '', '', ''));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($trigger->action === 'sms' && Controller::isSmsNotifEnabled('sms_notif_price_trigger')) {
|
||||
Controller::Send($customer->mobile, $message);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user