Files
boursecu_server/app/Services/Automation/AutomationConditionQuery.php
T
2026-08-23 14:16:00 +03:30

167 lines
6.3 KiB
PHP

<?php
namespace App\Services\Automation;
use App\Models\Customer;
use Illuminate\Database\Eloquent\Builder;
class AutomationConditionQuery
{
public function baseQuery(): Builder
{
return Customer::query()
->where('type', 2)
->where('is_archive', false);
}
public function apply(Builder $query, array $conditions): Builder
{
$rows = array_values(array_filter($conditions, fn ($r) => is_array($r) && ! empty($r['field'])));
if ($rows === []) {
return $query;
}
return $query->where(function (Builder $outer) use ($rows) {
foreach ($rows as $i => $row) {
$or = $i > 0 && (($row['cr'] ?? 'and') === 'or');
$this->applyOne($outer, $row, $or);
}
});
}
public function matchesCustomer(Customer $customer, array $conditions): bool
{
return $this->apply($this->baseQuery()->where('id', $customer->id), $conditions)->exists();
}
private function applyOne(Builder $q, array $row, bool $or): void
{
$field = (string) $row['field'];
$op = (string) ($row['op'] ?? 'eq');
$value = $row['value'] ?? null;
if (is_string($value)) {
$value = str_replace(',', '', $value);
}
$days = max(0, (int) $value);
match ($field) {
'never_login' => $this->nullCol($q, 'last_seen_at', $or),
'last_seen_older_days' => $this->where($q, $or, function (Builder $inner) use ($days) {
$cutoff = now()->subDays($days);
$inner->whereNull('last_seen_at')->orWhere('last_seen_at', '<', $cutoff);
}),
'last_seen_within_days' => $this->cmp($q, 'last_seen_at', 'gte', now()->subDays($days), $or),
'is_legal' => $this->cmp($q, 'is_legal', 'eq', (int) $value, $or),
'active' => $this->cmp($q, 'active', 'eq', (int) $value, $or),
'registered_older_days' => $this->cmp($q, 'created_at', 'lt', now()->subDays($days), $or),
'registered_within_days' => $this->cmp($q, 'created_at', 'gte', now()->subDays($days), $or),
'orders_count' => $this->cmp($q, 'orders_count', $op, (int) $value, $or),
'wallet_cash' => $this->wallet($q, 'cash', $op, (float) $value, $or),
'wallet_gold' => $this->wallet($q, 'gold', $op, (float) $value, $or),
'never_ordered' => $this->orderExists($q, null, null, $or, true),
'has_buy_within_days' => $this->orderExists($q, 1, $days, $or, false),
'has_sell_within_days' => $this->orderExists($q, 2, $days, $or, false),
'last_order_older_days' => $this->lastOrderOlder($q, $days, $or),
'has_deposit_within_days' => $this->depositExists($q, $days, $or),
'has_withdraw_pending' => $this->withdrawPending($q, $or),
default => null,
};
}
private function where(Builder $q, bool $or, callable $fn): void
{
$method = $or ? 'orWhere' : 'where';
$q->{$method}($fn);
}
private function nullCol(Builder $q, string $col, bool $or): void
{
$method = $or ? 'orWhereNull' : 'whereNull';
$q->{$method}($col);
}
private function cmp(Builder $q, string $col, string $op, mixed $value, bool $or): void
{
$sqlOp = match ($op) {
'gt' => '>',
'gte' => '>=',
'lt' => '<',
'lte' => '<=',
'not' => '<>',
default => '=',
};
$method = $or ? 'orWhere' : 'where';
$q->{$method}($col, $sqlOp, $value);
}
private function wallet(Builder $q, string $col, string $op, float $value, bool $or): void
{
$sqlOp = match ($op) {
'gt' => '>',
'gte' => '>=',
'lt' => '<',
'lte' => '<=',
'not' => '<>',
default => '=',
};
$expr = "(select w.{$col} from wallets w where w.owner = customers.id and w.fund_id is null and w.product_id is null order by w.id asc limit 1)";
$method = $or ? 'orWhereRaw' : 'whereRaw';
$q->{$method}("coalesce({$expr}, 0) {$sqlOp} ?", [$value]);
}
private function orderExists(Builder $q, ?int $direction, ?int $days, bool $or, bool $notExists): void
{
$method = $or
? ($notExists ? 'orWhereNotExists' : 'orWhereExists')
: ($notExists ? 'whereNotExists' : 'whereExists');
$q->{$method}(function ($sub) use ($direction, $days) {
$sub->from('orders')
->whereColumn('orders.customer_id', 'customers.id')
->where('orders.sts', 2);
if ($direction) {
$sub->where('orders.direction', $direction);
}
if ($days !== null) {
$sub->where('orders.created_at', '>=', now()->subDays($days));
}
});
}
private function lastOrderOlder(Builder $q, int $days, bool $or): void
{
$cutoff = now()->subDays($days);
$this->where($q, $or, function (Builder $inner) use ($cutoff) {
$inner->whereNotExists(function ($sub) {
$sub->from('orders')
->whereColumn('orders.customer_id', 'customers.id')
->where('orders.sts', 2);
})->orWhereRaw(
'(select max(orders.created_at) from orders where orders.customer_id = customers.id and orders.sts = 2) < ?',
[$cutoff]
);
});
}
private function depositExists(Builder $q, int $days, bool $or): void
{
$method = $or ? 'orWhereExists' : 'whereExists';
$q->{$method}(function ($sub) use ($days) {
$sub->from('withdraws')
->whereColumn('withdraws.customer_id', 'customers.id')
->where('withdraws.cash', '>', 0)
->whereIn('withdraws.sts', [2, 4])
->where('withdraws.created_at', '>=', now()->subDays($days));
});
}
private function withdrawPending(Builder $q, bool $or): void
{
$method = $or ? 'orWhereExists' : 'whereExists';
$q->{$method}(function ($sub) {
$sub->from('withdraws')
->whereColumn('withdraws.customer_id', 'customers.id')
->where('withdraws.sts', 1);
});
}
}