Remove email/in-app paths and enforce sms action on create/update so price alerts match the product requirement for SMS-only delivery. Co-authored-by: Cursor <cursoragent@cursor.com>
179 lines
5.4 KiB
PHP
179 lines
5.4 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Customer;
|
|
use App\Models\Price;
|
|
use App\Models\PriceTrigger;
|
|
use App\Models\Product;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
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 (! Controller::isSmsNotifEnabled('sms_notif_price_trigger')) {
|
|
return;
|
|
}
|
|
|
|
if (trim((string) ($customer->mobile ?? '')) === '') {
|
|
return;
|
|
}
|
|
|
|
Controller::Send($customer->mobile, $message);
|
|
}
|
|
}
|