53fb7b7760
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
282 lines
11 KiB
PHP
282 lines
11 KiB
PHP
<?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,
|
||
];
|
||
}
|
||
}
|