= 0), delivery_days_mask INT NOT NULL CHECK (delivery_days_mask BETWEEN 0 AND 127), regions INT[] NOT NULL DEFAULT '{}', signal_type TEXT NOT NULL CHECK (signal_type IN ('call','site','sms')), signal_identifier TEXT, sms_senders JSONB, sms_keyword TEXT, expected_volume INT NOT NULL CHECK (expected_volume >= 0), delivered_count INT NOT NULL DEFAULT 0 CHECK (delivered_count >= 0), created_at TIMESTAMP NOT NULL DEFAULT NOW(), PRIMARY KEY (snapshot_date, project_id), FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE ) PARTITION BY RANGE (snapshot_date) SQL); try { DB::statement( 'CREATE INDEX project_routing_snapshots_tenant_date_idx ON project_routing_snapshots (tenant_id, snapshot_date)' ); } catch (Throwable) { /* idempotent */ } try { DB::statement('ALTER TABLE project_routing_snapshots ENABLE ROW LEVEL SECURITY'); DB::statement( "CREATE POLICY project_routing_snapshots_tenant_isolation ON project_routing_snapshots USING (tenant_id = current_setting('app.current_tenant_id', true)::bigint)" ); } catch (Throwable) { /* idempotent */ } // 3b. phone_ranges_imports + phone_ranges + lead_region_resolution_log // (delta migration 2026_05_31_100000) DB::unprepared(<<<'SQL' CREATE TABLE IF NOT EXISTS phone_ranges_imports ( id BIGSERIAL PRIMARY KEY, imported_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), source_url TEXT NOT NULL, rows_inserted INTEGER NOT NULL DEFAULT 0, rows_updated INTEGER NOT NULL DEFAULT 0, checksum_sha256 TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'in_progress' CHECK (status IN ('in_progress','completed','failed','rolled_back')), error TEXT, completed_at TIMESTAMPTZ ) SQL); DB::unprepared(<<<'SQL' CREATE TABLE IF NOT EXISTS phone_ranges ( id BIGSERIAL PRIMARY KEY, def_code SMALLINT NOT NULL, from_num BIGINT NOT NULL, to_num BIGINT NOT NULL, operator TEXT NOT NULL, region TEXT NOT NULL, region_normalized TEXT, subject_code SMALLINT, imported_at TIMESTAMPTZ NOT NULL, import_id BIGINT NOT NULL REFERENCES phone_ranges_imports(id), CONSTRAINT chk_phone_ranges_def_code CHECK (def_code BETWEEN 300 AND 999), CONSTRAINT chk_phone_ranges_subject_code CHECK (subject_code IS NULL OR subject_code BETWEEN 1 AND 89), CONSTRAINT chk_phone_ranges_range_valid CHECK (from_num <= to_num) ) SQL); DB::unprepared(<<<'SQL' CREATE TABLE IF NOT EXISTS lead_region_resolution_log ( id BIGSERIAL, supplier_lead_id BIGINT NOT NULL, received_at TIMESTAMPTZ NOT NULL, phone_masked TEXT NOT NULL, subject_code_resolved SMALLINT, subject_code_from_tag SMALLINT, region_source TEXT NOT NULL CHECK (region_source IN ('dadata','rossvyaz','tag','unknown')), dadata_qc SMALLINT, dadata_provider TEXT, dadata_type TEXT, dadata_response_masked JSONB, rossvyaz_matched BOOLEAN NOT NULL DEFAULT FALSE, actual_subject_code SMALLINT CHECK (actual_subject_code IS NULL OR actual_subject_code BETWEEN 1 AND 89), substituted_subject_code SMALLINT CHECK (substituted_subject_code IS NULL OR substituted_subject_code BETWEEN 1 AND 89), routing_step SMALLINT, cache_hit BOOLEAN NOT NULL DEFAULT FALSE, duration_ms INTEGER, error TEXT, PRIMARY KEY (id, received_at) ) PARTITION BY RANGE (received_at) SQL); try { DB::statement('ALTER TABLE lead_region_resolution_log ENABLE ROW LEVEL SECURITY'); } catch (Throwable) { /* idempotent */ } // Step 4: Create month partitions — with shared PDO, pgsql_supplier sees // all the tables we just created within the same transaction. Artisan::call('partitions:create-months', ['--ahead' => 2]); // Step 5: Mark the delta migrations as "ran" so they don't re-run if // someone calls Artisan::call('migrate') later in the test suite. $deltaRan = [ '2026_05_27_120000_create_project_routing_snapshots_table', '2026_05_31_100000_create_phone_ranges_and_resolution_log', ]; foreach ($deltaRan as $migration) { try { DB::table('migrations')->updateOrInsert( ['migration' => $migration], ['batch' => 1], ); } catch (Throwable) { /* ok if migrations table doesn't exist */ } } } // Seed pricing tiers (required by LedgerService::chargeForDelivery). (new PricingTierSeeder)->run(); // Allow cross-tenant reads during seeding. DB::statement("SELECT set_config('app.current_tenant_id', '0', true)"); }); /** * Task 4 — ImitationClientsSeeder: single-project matrix (36 rows). * * Matrix axes: * signal ∈ {site, call} — 2 * regions ∈ {[], [82], [82,83]} — 3 (empty=all-RF, [82]=Moscow, [82,83]=Moscow+SPb) * days ∈ {127 (7 days), 31 (Mon-Fri)} — 2 * limit ∈ {3, 30, 300} — 3 * Total: 2 × 3 × 2 × 3 = 36 * * All project names are prefixed `IMIT-single-`. */ it('seeds the single-project matrix', function (): void { (new ImitationClientsSeeder)->run(); expect(Project::where('name', 'like', 'IMIT-single-%')->count())->toBe(36); })->group('imitation');