Files
portal/app/tests/Support/Imitation/ImitationClientsSeeder.php
T

282 lines
11 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
namespace Tests\Support\Imitation;
use App\Models\Project;
use App\Models\SupplierProject;
use App\Models\Tenant;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
/**
* Seeder for the Phase 1 imitation harness.
*
* Creates the single-project matrix (36 projects) covering all combinations of:
* signal ∈ {site, call} — 2
* regions ∈ {[], [82], [82,83]} — 3 (empty=all-RF, [82]=Москва, [82,83]=Москва+СПб)
* 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-.
* Topology helpers (G1/G2/G4) also use IMIT- prefix.
*
* Region codes follow ordinal 1..89 (constitutional order), NOT ГИБДД codes.
* Москва = 82, Санкт-Петербург = 83 (verified via App\Support\RussianRegions::CODE_TO_NAME).
*
* Task 4 — Phase 1 Portal Client Imitation Harness.
* Spec: docs/superpowers/specs/2026-06-03-portal-client-imitation-phase1-design.md
* Plan: docs/superpowers/plans/2026-06-03-portal-client-imitation-phase1.md
*/
final class ImitationClientsSeeder
{
/** Shared SupplierProject used by all matrix cells (B2 site-signal). */
private ?SupplierProject $sharedSupplier = null;
/**
* Run the seeder: creates the 36-cell single-project matrix.
* Topology helpers G1/G2/G4 (used by Task 13) are available as separate methods.
*/
public function run(): void
{
$this->sharedSupplier = $this->makeSharedSupplierProject();
$this->seedSingleProjectMatrix();
}
// -------------------------------------------------------------------------
// Matrix seeding
// -------------------------------------------------------------------------
private function seedSingleProjectMatrix(): void
{
$signals = ['site', 'call'];
// regions: empty = all-RF; [82] = Москва; [82, 83] = Москва + СПб
$regions = [[], [82], [82, 83]];
$dayMasks = [127, 31];
$limits = [3, 30, 300];
$i = 0;
foreach ($signals as $signal) {
foreach ($regions as $regionSet) {
foreach ($dayMasks as $daysMask) {
foreach ($limits as $limit) {
$i++;
$this->makeSingleProjectCell(
index: $i,
signal: $signal,
regions: $regionSet,
daysMask: $daysMask,
limit: $limit,
);
}
}
}
}
}
/**
* Create one Tenant + User + Project + pivot link for a matrix cell.
*
* @param array<int> $regions Ordinal subject codes 1..89 (empty = all-RF).
*/
private function makeSingleProjectCell(
int $index,
string $signal,
array $regions,
int $daysMask,
int $limit,
): void {
$tenant = Tenant::factory()->create();
User::factory()->create(['tenant_id' => $tenant->id]);
// Unique signal identifier to avoid UNIQUE constraint violations.
$uniqueSuffix = Str::random(6);
$signalIdentifier = $signal === 'site'
? "imit-{$index}-{$uniqueSuffix}.test"
: '7'.str_pad((string) (9000000000 + $index), 10, '0', STR_PAD_LEFT).$uniqueSuffix;
// Pass regions as a PHP int[] — the PostgresIntArray Eloquent cast
// converts it to the PostgreSQL literal '{82,83}' or '{}' in set().
$project = $signal === 'site'
? Project::factory()
->asSiteSignal($signalIdentifier)
->create([
'name' => "IMIT-single-{$index}",
'tenant_id' => $tenant->id,
'regions' => $regions,
'delivery_days_mask' => $daysMask,
'daily_limit_target' => $limit,
])
: Project::factory()
->asCallSignal($signalIdentifier)
->create([
'name' => "IMIT-single-{$index}",
'tenant_id' => $tenant->id,
'regions' => $regions,
'delivery_days_mask' => $daysMask,
'daily_limit_target' => $limit,
]);
DB::table('project_supplier_links')->insert([
'project_id' => $project->id,
'supplier_project_id' => $this->sharedSupplier->id,
'platform' => $this->sharedSupplier->platform,
'subject_code' => null,
]);
}
// -------------------------------------------------------------------------
// Shared supplier project factory
// -------------------------------------------------------------------------
/**
* Create a shared SupplierProject (B2, site signal) used by all matrix cells.
* B2 supports both site and call signals (no B1+sms constraint).
*/
private function makeSharedSupplierProject(): SupplierProject
{
return SupplierProject::factory()->create([
'platform' => 'B2',
'signal_type' => 'site',
]);
}
// -------------------------------------------------------------------------
// Topology helpers — used by Task 13 (TopologyMoneyIntakeTest)
// -------------------------------------------------------------------------
/**
* G1 topology: one client (Tenant) with one Project linked to TWO different
* SupplierProjects (B1 + B2).
*
* Validates that LeadRouter can route leads from multiple supplier sources
* to the same project when the project is linked to multiple suppliers.
*
* @return array{tenant: Tenant, project: Project, suppliers: list<SupplierProject>}
*/
public function seedG1(string $namePrefix = 'IMIT-G1'): array
{
$tenant = Tenant::factory()->create();
User::factory()->create(['tenant_id' => $tenant->id]);
$project = Project::factory()
->asSiteSignal("g1-{$namePrefix}-".Str::random(6).'.test')
->create([
'name' => "{$namePrefix}-project",
'tenant_id' => $tenant->id,
]);
$supplier1 = SupplierProject::factory()->create(['platform' => 'B1', 'signal_type' => 'site']);
$supplier2 = SupplierProject::factory()->create(['platform' => 'B2', 'signal_type' => 'site']);
foreach ([$supplier1, $supplier2] as $supplier) {
DB::table('project_supplier_links')->insert([
'project_id' => $project->id,
'supplier_project_id' => $supplier->id,
'platform' => $supplier->platform,
'subject_code' => null,
]);
}
return ['tenant' => $tenant, 'project' => $project, 'suppliers' => [$supplier1, $supplier2]];
}
/**
* G2 topology: TWO clients (Tenants/Projects) linked to the SAME SupplierProject.
*
* Validates weighted lottery and fair distribution between competing clients
* sharing a single supplier source.
*
* @param array<string, mixed> $overrides1 ProjectFactory overrides for client 1.
* @param array<string, mixed> $overrides2 ProjectFactory overrides for client 2.
* @return array{supplier: SupplierProject, projects: list<Project>, tenants: list<Tenant>}
*/
public function seedG2(array $overrides1 = [], array $overrides2 = []): array
{
$supplier = SupplierProject::factory()->create(['platform' => 'B2', 'signal_type' => 'site']);
$projects = [];
$tenants = [];
foreach ([$overrides1, $overrides2] as $idx => $overrides) {
$tenant = Tenant::factory()->create();
User::factory()->create(['tenant_id' => $tenant->id]);
$tenants[] = $tenant;
$project = Project::factory()
->asSiteSignal("g2-client-{$idx}-".Str::random(6).'.test')
->create(array_merge([
'name' => "IMIT-G2-client-{$idx}",
'tenant_id' => $tenant->id,
], $overrides));
$projects[] = $project;
DB::table('project_supplier_links')->insert([
'project_id' => $project->id,
'supplier_project_id' => $supplier->id,
'platform' => $supplier->platform,
'subject_code' => null,
]);
}
return ['supplier' => $supplier, 'projects' => $projects, 'tenants' => $tenants];
}
/**
* G4 topology: one client with TWO Projects on the SAME SupplierProject,
* each targeting a different region.
*
* Validates that LeadRouter dispatches leads to the project whose region
* matches the lead's resolved subject code.
*
* @param int $regionA Ordinal subject code for project A (e.g. 82 = Москва).
* @param int $regionB Ordinal subject code for project B (e.g. 83 = СПб).
* @return array{supplier: SupplierProject, tenant: Tenant, projectA: Project, projectB: Project}
*/
public function seedG4(int $regionA = 82, int $regionB = 83): array
{
$tenant = Tenant::factory()->create();
User::factory()->create(['tenant_id' => $tenant->id]);
$supplier = SupplierProject::factory()->create(['platform' => 'B2', 'signal_type' => 'site']);
$uniqueA = Str::random(6);
$uniqueB = Str::random(6);
$projectA = Project::factory()
->asSiteSignal("g4-region-{$regionA}-{$uniqueA}.test")
->create([
'name' => "IMIT-G4-region-{$regionA}",
'tenant_id' => $tenant->id,
'regions' => [$regionA], // PHP int[] — PostgresIntArray cast handles conversion
]);
$projectB = Project::factory()
->asSiteSignal("g4-region-{$regionB}-{$uniqueB}.test")
->create([
'name' => "IMIT-G4-region-{$regionB}",
'tenant_id' => $tenant->id,
'regions' => [$regionB], // PHP int[] — PostgresIntArray cast handles conversion
]);
foreach ([$projectA, $projectB] as $project) {
DB::table('project_supplier_links')->insert([
'project_id' => $project->id,
'supplier_project_id' => $supplier->id,
'platform' => $supplier->platform,
'subject_code' => null,
]);
}
return [
'supplier' => $supplier,
'tenant' => $tenant,
'projectA' => $projectA,
'projectB' => $projectB,
];
}
}