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

282 lines
11 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);
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,
];
}
}