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); }); } }