2026-06-03 19:37:49 +03:00
|
|
|
|
<?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
|
|
|
|
|
|
{
|
2026-06-18 19:33:33 +03:00
|
|
|
|
$signals = ['site', 'call'];
|
2026-06-03 19:37:49 +03:00
|
|
|
|
// regions: empty = all-RF; [82] = Москва; [82, 83] = Москва + СПб
|
2026-06-18 19:33:33 +03:00
|
|
|
|
$regions = [[], [82], [82, 83]];
|
2026-06-03 19:37:49 +03:00
|
|
|
|
$dayMasks = [127, 31];
|
2026-06-18 19:33:33 +03:00
|
|
|
|
$limits = [3, 30, 300];
|
2026-06-03 19:37:49 +03:00
|
|
|
|
|
|
|
|
|
|
$i = 0;
|
|
|
|
|
|
foreach ($signals as $signal) {
|
|
|
|
|
|
foreach ($regions as $regionSet) {
|
|
|
|
|
|
foreach ($dayMasks as $daysMask) {
|
|
|
|
|
|
foreach ($limits as $limit) {
|
|
|
|
|
|
$i++;
|
|
|
|
|
|
$this->makeSingleProjectCell(
|
2026-06-18 19:33:33 +03:00
|
|
|
|
index: $i,
|
|
|
|
|
|
signal: $signal,
|
|
|
|
|
|
regions: $regionSet,
|
2026-06-03 19:37:49 +03:00
|
|
|
|
daysMask: $daysMask,
|
2026-06-18 19:33:33 +03:00
|
|
|
|
limit: $limit,
|
2026-06-03 19:37:49 +03:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 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"
|
2026-06-18 19:33:33 +03:00
|
|
|
|
: '7'.str_pad((string) (9000000000 + $index), 10, '0', STR_PAD_LEFT).$uniqueSuffix;
|
2026-06-03 19:37:49 +03:00
|
|
|
|
|
|
|
|
|
|
// 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([
|
2026-06-18 19:33:33 +03:00
|
|
|
|
'name' => "IMIT-single-{$index}",
|
|
|
|
|
|
'tenant_id' => $tenant->id,
|
|
|
|
|
|
'regions' => $regions,
|
2026-06-03 19:37:49 +03:00
|
|
|
|
'delivery_days_mask' => $daysMask,
|
|
|
|
|
|
'daily_limit_target' => $limit,
|
|
|
|
|
|
])
|
|
|
|
|
|
: Project::factory()
|
|
|
|
|
|
->asCallSignal($signalIdentifier)
|
|
|
|
|
|
->create([
|
2026-06-18 19:33:33 +03:00
|
|
|
|
'name' => "IMIT-single-{$index}",
|
|
|
|
|
|
'tenant_id' => $tenant->id,
|
|
|
|
|
|
'regions' => $regions,
|
2026-06-03 19:37:49 +03:00
|
|
|
|
'delivery_days_mask' => $daysMask,
|
|
|
|
|
|
'daily_limit_target' => $limit,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
DB::table('project_supplier_links')->insert([
|
2026-06-18 19:33:33 +03:00
|
|
|
|
'project_id' => $project->id,
|
2026-06-03 19:37:49 +03:00
|
|
|
|
'supplier_project_id' => $this->sharedSupplier->id,
|
2026-06-18 19:33:33 +03:00
|
|
|
|
'platform' => $this->sharedSupplier->platform,
|
|
|
|
|
|
'subject_code' => null,
|
2026-06-03 19:37:49 +03:00
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// -------------------------------------------------------------------------
|
|
|
|
|
|
// 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([
|
2026-06-18 19:33:33 +03:00
|
|
|
|
'platform' => 'B2',
|
2026-06-03 19:37:49 +03:00
|
|
|
|
'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()
|
2026-06-18 19:33:33 +03:00
|
|
|
|
->asSiteSignal("g1-{$namePrefix}-".Str::random(6).'.test')
|
2026-06-03 19:37:49 +03:00
|
|
|
|
->create([
|
2026-06-18 19:33:33 +03:00
|
|
|
|
'name' => "{$namePrefix}-project",
|
2026-06-03 19:37:49 +03:00
|
|
|
|
'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([
|
2026-06-18 19:33:33 +03:00
|
|
|
|
'project_id' => $project->id,
|
2026-06-03 19:37:49 +03:00
|
|
|
|
'supplier_project_id' => $supplier->id,
|
2026-06-18 19:33:33 +03:00
|
|
|
|
'platform' => $supplier->platform,
|
|
|
|
|
|
'subject_code' => null,
|
2026-06-03 19:37:49 +03:00
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 = [];
|
2026-06-18 19:33:33 +03:00
|
|
|
|
$tenants = [];
|
2026-06-03 19:37:49 +03:00
|
|
|
|
|
|
|
|
|
|
foreach ([$overrides1, $overrides2] as $idx => $overrides) {
|
|
|
|
|
|
$tenant = Tenant::factory()->create();
|
|
|
|
|
|
User::factory()->create(['tenant_id' => $tenant->id]);
|
|
|
|
|
|
$tenants[] = $tenant;
|
|
|
|
|
|
|
|
|
|
|
|
$project = Project::factory()
|
2026-06-18 19:33:33 +03:00
|
|
|
|
->asSiteSignal("g2-client-{$idx}-".Str::random(6).'.test')
|
2026-06-03 19:37:49 +03:00
|
|
|
|
->create(array_merge([
|
2026-06-18 19:33:33 +03:00
|
|
|
|
'name' => "IMIT-G2-client-{$idx}",
|
2026-06-03 19:37:49 +03:00
|
|
|
|
'tenant_id' => $tenant->id,
|
|
|
|
|
|
], $overrides));
|
|
|
|
|
|
$projects[] = $project;
|
|
|
|
|
|
|
|
|
|
|
|
DB::table('project_supplier_links')->insert([
|
2026-06-18 19:33:33 +03:00
|
|
|
|
'project_id' => $project->id,
|
2026-06-03 19:37:49 +03:00
|
|
|
|
'supplier_project_id' => $supplier->id,
|
2026-06-18 19:33:33 +03:00
|
|
|
|
'platform' => $supplier->platform,
|
|
|
|
|
|
'subject_code' => null,
|
2026-06-03 19:37:49 +03:00
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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([
|
2026-06-18 19:33:33 +03:00
|
|
|
|
'name' => "IMIT-G4-region-{$regionA}",
|
2026-06-03 19:37:49 +03:00
|
|
|
|
'tenant_id' => $tenant->id,
|
2026-06-18 19:33:33 +03:00
|
|
|
|
'regions' => [$regionA], // PHP int[] — PostgresIntArray cast handles conversion
|
2026-06-03 19:37:49 +03:00
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
$projectB = Project::factory()
|
|
|
|
|
|
->asSiteSignal("g4-region-{$regionB}-{$uniqueB}.test")
|
|
|
|
|
|
->create([
|
2026-06-18 19:33:33 +03:00
|
|
|
|
'name' => "IMIT-G4-region-{$regionB}",
|
2026-06-03 19:37:49 +03:00
|
|
|
|
'tenant_id' => $tenant->id,
|
2026-06-18 19:33:33 +03:00
|
|
|
|
'regions' => [$regionB], // PHP int[] — PostgresIntArray cast handles conversion
|
2026-06-03 19:37:49 +03:00
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
foreach ([$projectA, $projectB] as $project) {
|
|
|
|
|
|
DB::table('project_supplier_links')->insert([
|
2026-06-18 19:33:33 +03:00
|
|
|
|
'project_id' => $project->id,
|
2026-06-03 19:37:49 +03:00
|
|
|
|
'supplier_project_id' => $supplier->id,
|
2026-06-18 19:33:33 +03:00
|
|
|
|
'platform' => $supplier->platform,
|
|
|
|
|
|
'subject_code' => null,
|
2026-06-03 19:37:49 +03:00
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
'supplier' => $supplier,
|
2026-06-18 19:33:33 +03:00
|
|
|
|
'tenant' => $tenant,
|
2026-06-03 19:37:49 +03:00
|
|
|
|
'projectA' => $projectA,
|
|
|
|
|
|
'projectB' => $projectB,
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|