Files
portal/app/tests/Feature/Plan4/Schema/SchemaDeltaTest.php
T
Дмитрий 6e1f5355b8 refactor(webhook): Phase 4 — DROP migration + schema v8.35 + test/factory cleanup
Task 4.1 Steps 1–7: legacy direct webhook channel DDL removal.

Migration 2026_05_24_140000_drop_legacy_webhook_artefacts:
- DROP TABLE webhook_log CASCADE (partitioned RANGE по received_at)
- DROP TABLE rejected_deals_log CASCADE
- ALTER TABLE tenants DROP COLUMN webhook_token, webhook_token_rotated_at
- DELETE FROM system_settings WHERE key = 'low_balance_threshold_leads'
NB: webhook_dedup_keys ОСТАВЛЕНА — используется CSV-каналом (HistoricalImportService).

Services fixed (не покрыты Phase 3):
- MonthlyPartitionManager::PARTITIONED_TABLES — убрана строка webhook_log
- PdErasureService::eraseSubject() — убрана секция 4 (SELECT/UPDATE webhook_log)

Factory + tests cleanup (webhook_token column gone):
- TenantFactory: убрано webhook_token из definition()
- 7 test files: убраны вставки webhook_token в DB::table('tenants')->insert(...)
- storage/_demo_split_tenants.php: убрана строка webhook_token

Schema v8.35:
- −2 таблицы (webhook_log partitioned + rejected_deals_log)
- −5 индексов (idx_webhook_log_*, idx_rejected_*, idx_tenants_webhook_token)
- −2 RLS-политики
- db/CHANGELOG_schema.md: запись v8.35

Tests updated:
- SchemaDeltaTest: 66 base tables / 120 indexes / 40 RLS policies
- PartitionsCreateMonthsTest: webhook_log убрана из regex / 48 skipped вместо 54

Smoke: 36/36 passed (RlsSmoke, AdminBilling, AdminPdSubject, PartitionsCreateMonths, SchemaDelta).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 18:51:17 +03:00

92 lines
4.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
use App\Models\Deal;
use App\Models\Tenant;
use Illuminate\Database\QueryException;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
// NOTE: \Tests\TestCase auto-binds via tests/Pest.php (->in('Feature')); explicit
// uses(\Tests\TestCase::class) conflicts ("already uses the test case").
// DatabaseTransactions — изоляция: каждый тест выполняется в транзакции, rollback после.
// Project convention: LeadChargeTest / PricingTierTest используют тот же паттерн.
uses(DatabaseTransactions::class);
it('tenants table has delivered_in_month column with CHECK >= 0', function () {
expect(Schema::hasColumn('tenants', 'delivered_in_month'))->toBeTrue();
DB::table('tenants')->where('id', '<', 0)->update(['delivered_in_month' => 5]); // no-op
expect(fn () => DB::statement(
'INSERT INTO tenants (subdomain, organization_name, contact_email, delivered_in_month) '.
"VALUES ('t-neg-test', 'X', 'x@x', -1)"
))->toThrow(QueryException::class);
});
it('lead_charges table has charge_source column with CHECK on prepaid=zero-price', function () {
expect(Schema::hasColumn('lead_charges', 'charge_source'))->toBeTrue();
$tenant = Tenant::factory()->create();
$deal = Deal::factory()->create(['tenant_id' => $tenant->id]);
expect(fn () => DB::table('lead_charges')->insert([
'tenant_id' => $tenant->id,
'deal_id' => $deal->id,
'deal_received_at' => $deal->received_at,
'tier_no' => 1,
'price_per_lead_kopecks' => 50000,
'charge_source' => 'prepaid',
'charged_at' => now(),
'created_at' => now(),
]))->toThrow(QueryException::class);
});
it('supplier_leads table has recovered_from_csv_at column', function () {
expect(Schema::hasColumn('supplier_leads', 'recovered_from_csv_at'))->toBeTrue();
});
it('supplier_csv_reconcile_log table exists with required columns and status CHECK', function () {
expect(Schema::hasTable('supplier_csv_reconcile_log'))->toBeTrue();
expect(Schema::hasColumns('supplier_csv_reconcile_log', [
'id', 'started_at', 'finished_at', 'window_start', 'window_end',
'total_csv_rows', 'matched_count', 'recovered_count', 'drift_ratio',
'status', 'error_message', 'alert_email_sent_at', 'created_at',
]))->toBeTrue();
expect(fn () => DB::table('supplier_csv_reconcile_log')->insert([
'started_at' => now(),
'window_start' => now()->subDay(),
'window_end' => now(),
'status' => 'unknown_status',
]))->toThrow(QueryException::class);
});
it('schema.sql v8.35 has correct metrics — 66 base tables, 120 indexes, 40 RLS policies', function () {
// Замена destructive `migrate:fresh` (cross-test coupling: после DROP CASCADE остальные
// Feature-тесты в той же сессии видели пустую БД). Static parse `db/schema.sql` —
// источник истины метрик.
// v8.21 (Sprint 4): +1 таблица import_unknown_statuses, +1 индекс, +1 RLS-политика.
// v8.22 (Plan 6/C9): +1 GIN-индекс idx_projects_regions.
// v8.25 (supplier-failover): +1 таблица supplier_manual_sync_queue, +2 индекса.
// v8.26 (project-migration-redesign Plans 1-3): +1 таблица project_supplier_links (M:N pivot)
// + 2 индекса (supplier_projects_platform_key_subject_unique, idx_psl_*).
// v8.30: +1 таблица scheduler_heartbeats (SaaS-level, hole #6).
// v8.31: 7 audit-таблиц переведены в PARTITION BY RANGE, hole #2.
// v8.35 (legacy webhook removal): 2 таблицы (webhook_log partitioned + rejected_deals_log)
// −5 индексов, −2 RLS-политики, −2 колонки tenants.webhook_token/webhook_token_rotated_at.
$schemaPath = dirname(base_path()).DIRECTORY_SEPARATOR.'db'.DIRECTORY_SEPARATOR.'schema.sql';
expect(is_file($schemaPath) && is_readable($schemaPath))->toBeTrue();
$schema = file_get_contents($schemaPath);
expect($schema)->not->toBeFalse();
// 66 base tables = все CREATE TABLE минус PARTITION OF.
$createTables = preg_match_all('/^CREATE TABLE\b/m', $schema);
$partitionOf = preg_match_all('/CREATE TABLE\s+\w+\s+PARTITION OF\b/m', $schema);
$baseTables = $createTables - $partitionOf;
expect($baseTables)->toBe(66);
$createIndexes = preg_match_all('/^CREATE\s+(?:UNIQUE\s+)?INDEX\b/m', $schema);
expect($createIndexes)->toBe(120); // v8.35: 5 индексов (webhook_log ×2, rejected_deals_log ×2, tenants.webhook_token ×1)
$createPolicies = preg_match_all('/^CREATE\s+POLICY\b/m', $schema);
expect($createPolicies)->toBe(40); // v8.35: 2 политики (webhook_log + rejected_deals_log)
});