53fb7b7760
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
144 lines
5.8 KiB
PHP
144 lines
5.8 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace Tests\Support\Imitation;
|
||
|
||
use App\Models\Project;
|
||
use App\Models\Tenant;
|
||
use Illuminate\Support\Facades\DB;
|
||
|
||
/**
|
||
* Рычаги условий для имитационного стенда (Phase 1).
|
||
*
|
||
* Напрямую пишет в реальные колонки, которые читает прод-код:
|
||
* - tenants.balance_rub — LedgerService (bcmath balance_rub*100 >= price)
|
||
* - tenants.frozen_by_balance_at — PreflightBalanceService (NULL = активен)
|
||
* - projects.is_active — SnapshotRebuildCommand eligibility
|
||
* - projects.delivered_today — LeadRouter остаток лимита
|
||
* - projects.delivery_days_mask — LeadRouter / SnapshotRebuildCommand
|
||
* - projects.regions — LeadRouter regional cascade
|
||
* - projects.preflight_blocked_at — SnapshotRebuildCommand eligibility
|
||
*
|
||
* Имена колонок подтверждены чтением db/schema.sql и прод-кода:
|
||
* - balance_rub: tenants, DECIMAL(12,2) DEFAULT 0
|
||
* - frozen_by_balance_at: tenants, TIMESTAMPTZ NULL (NULL = не заморожен)
|
||
* - regions: projects, INT[] NOT NULL DEFAULT '{}' (порядковые коды 1..89, НЕ ГИБДД)
|
||
* - delivery_days_mask: projects, INT NOT NULL DEFAULT 127 (bit 0=Пн..bit 6=Вс)
|
||
* - daily_limit_target: projects, INT NOT NULL DEFAULT 10
|
||
* - delivered_today: projects, INT (остаток лимита)
|
||
* - preflight_blocked_at: projects, TIMESTAMPTZ NULL
|
||
*
|
||
* Коды субъектов — ПОРЯДКОВЫЕ 1..89 (конституционный порядок), НЕ коды ГИБДД.
|
||
* Использовать только через App\Support\RussianRegions::CODE_TO_NAME / nameToCode().
|
||
* Например: Москва = 82, Санкт-Петербург = 83.
|
||
*
|
||
* Task 3 — Phase 1 Portal Client Imitation Harness.
|
||
* Spec: docs/superpowers/specs/2026-06-03-portal-client-imitation-phase1-design.md
|
||
*/
|
||
final class ConditionLevers
|
||
{
|
||
/**
|
||
* Установить баланс тенанта (в рублях, как DECIMAL(12,2)).
|
||
*
|
||
* @param int|float|string $rub Сумма в рублях (например 500.00 или 0).
|
||
*/
|
||
public static function setBalance(Tenant $tenant, int|float|string $rub): void
|
||
{
|
||
DB::table('tenants')
|
||
->where('id', $tenant->id)
|
||
->update(['balance_rub' => $rub]);
|
||
}
|
||
|
||
/**
|
||
* Обнулить баланс тенанта до 0 (лид не пройдёт LedgerService::chargeForDelivery).
|
||
*/
|
||
public static function drainBalance(Tenant $tenant): void
|
||
{
|
||
self::setBalance($tenant, 0);
|
||
}
|
||
|
||
/**
|
||
* Выставить delivered_today = daily_limit_target у проекта,
|
||
* чтобы LeadRouter не считал его eligible (лимит исчерпан).
|
||
*
|
||
* LeadRouter: `projects.delivered_today < snap.daily_limit` — равенство = не eligible.
|
||
*/
|
||
public static function fillToLimit(Project $project): void
|
||
{
|
||
$limit = (int) DB::table('projects')
|
||
->where('id', $project->id)
|
||
->value('daily_limit_target');
|
||
|
||
DB::table('projects')
|
||
->where('id', $project->id)
|
||
->update(['delivered_today' => $limit]);
|
||
}
|
||
|
||
/**
|
||
* Приостановить проект: is_active = false + paused_at = NOW().
|
||
*
|
||
* SnapshotRebuildCommand исключает проекты с is_active = false из нового snapshot.
|
||
* (Проверено: команда WHERE p.is_active = true.)
|
||
*/
|
||
public static function pause(Project $project): void
|
||
{
|
||
DB::table('projects')
|
||
->where('id', $project->id)
|
||
->update([
|
||
'is_active' => false,
|
||
'paused_at' => now(),
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Заморозить тенанта по балансу: frozen_by_balance_at = NOW().
|
||
*
|
||
* LeadRouter: WHERE tenants.frozen_by_balance_at IS NULL — заморожен = не eligible.
|
||
* SnapshotRebuildCommand: WHERE t.frozen_by_balance_at IS NULL — не попадёт в snapshot.
|
||
*/
|
||
public static function freeze(Tenant $tenant): void
|
||
{
|
||
DB::table('tenants')
|
||
->where('id', $tenant->id)
|
||
->update(['frozen_by_balance_at' => now()]);
|
||
}
|
||
|
||
/**
|
||
* Установить регионы проекта (порядковые коды 1..89, НЕ коды ГИБДД).
|
||
*
|
||
* LeadRouter Фаза 1: exact — ?::int = ANY(snap.regions).
|
||
* Пустой массив = «вся РФ» (LeadRouter Фаза 2, regions = '{}').
|
||
*
|
||
* Пример: [82] = только Москва, [82, 83] = Москва + СПб, [] = вся РФ.
|
||
* Коды через App\Support\RussianRegions::CODE_TO_NAME / nameToCode().
|
||
*
|
||
* @param array<int> $codes Порядковые коды субъектов (1..89).
|
||
*/
|
||
public static function setRegions(Project $project, array $codes): void
|
||
{
|
||
// PostgreSQL int[] литерал: '{82,83}' или '{}'.
|
||
$pgArray = '{'.implode(',', array_map('intval', $codes)).'}';
|
||
|
||
DB::table('projects')
|
||
->where('id', $project->id)
|
||
->update(['regions' => $pgArray]);
|
||
}
|
||
|
||
/**
|
||
* Установить битмаску дней приёма лидов.
|
||
*
|
||
* Бит 0 (1) = Понедельник, бит 6 (64) = Воскресенье.
|
||
* 127 = все 7 дней; 31 = Пн–Пт; 96 = Сб+Вс.
|
||
* SnapshotRebuildCommand: WHERE (p.delivery_days_mask & weekdayBit) <> 0.
|
||
*
|
||
* @param int $mask Битмаска 0..127.
|
||
*/
|
||
public static function setDays(Project $project, int $mask): void
|
||
{
|
||
DB::table('projects')
|
||
->where('id', $project->id)
|
||
->update(['delivery_days_mask' => $mask]);
|
||
}
|
||
}
|