669e161017
Self-contained app-namespace artisan command (NEVER on production) that funds local imitation clients on a shared B2 supplier, disables DaData (region from tag), rebuilds the routing snapshot, then injects synthetic leads through the real RouteSupplierLeadJob so deals/charges/notifications appear for hands-on UI review. The lead payload encodes the supplier unique_key as a domain so RouteSupplierLeadJob re-resolves the real supplier (parseProjectField then resolveOrStub). Test asserts exit 0 + new deals.
143 lines
5.4 KiB
PHP
143 lines
5.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Console\Commands\Imitation;
|
|
|
|
use App\Jobs\RouteSupplierLeadJob;
|
|
use App\Models\Project;
|
|
use App\Models\SupplierLead;
|
|
use App\Models\SupplierProject;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Support\RussianRegions;
|
|
use Carbon\Carbon;
|
|
use Database\Seeders\PricingTierSeeder;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\Artisan;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
|
|
/**
|
|
* Populate a LOCAL portal with imitation clients and leads for hands-on UI review
|
|
* (Phase 1 imitation harness). It NEVER runs on production.
|
|
*
|
|
* Self-contained on purpose (it must not depend on test-harness helpers): it funds
|
|
* a few tenant balances locally, disables the external DaData call (region is taken
|
|
* from the lead tag), builds the routing snapshot for the active date, then injects
|
|
* synthetic leads through the real RouteSupplierLeadJob so deals, charges and
|
|
* notifications appear exactly as they would in production.
|
|
*
|
|
* Spec: docs/superpowers/specs/2026-06-03-portal-client-imitation-phase1-design.md
|
|
*/
|
|
final class ImitationSeedCommand extends Command
|
|
{
|
|
protected $signature = 'imitation:seed
|
|
{--leads=20 : Number of synthetic leads to inject}
|
|
{--clients=3 : Number of imitation clients to create}';
|
|
|
|
protected $description = 'Populate the LOCAL portal with imitation clients and leads for UI review (never on production)';
|
|
|
|
public function handle(): int
|
|
{
|
|
if ($this->getLaravel()->environment('production')) {
|
|
$this->error('imitation:seed is forbidden in production.');
|
|
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$leads = max(1, (int) $this->option('leads'));
|
|
$clients = max(1, (int) $this->option('clients'));
|
|
|
|
// Region comes from the lead tag — no external (paid) DaData call.
|
|
config(['services.dadata.enabled' => false]);
|
|
|
|
// Reference data required by the ledger.
|
|
(new PricingTierSeeder)->run();
|
|
|
|
$moscow = RussianRegions::nameToCode()['Москва']; // ordinal 82
|
|
|
|
// One shared supplier source (B2 site signal). The unique_key must be a
|
|
// domain-like string: RouteSupplierLeadJob re-resolves the supplier from the
|
|
// lead payload by (platform, unique_key) and infers signal_type from the
|
|
// identifier shape (see parseProjectField/resolveOrStub) — a domain → 'site'.
|
|
$supplierKey = 'imitseed-'.strtolower(Str::random(8)).'.test';
|
|
$supplier = SupplierProject::factory()->create([
|
|
'platform' => 'B2',
|
|
'signal_type' => 'site',
|
|
'unique_key' => $supplierKey,
|
|
]);
|
|
|
|
// Funded imitation clients, all targeting Москва, full week, generous limit.
|
|
for ($i = 1; $i <= $clients; $i++) {
|
|
$tenant = Tenant::factory()->create(['balance_rub' => '100000.00']);
|
|
User::factory()->create(['tenant_id' => $tenant->id]);
|
|
|
|
$project = Project::factory()
|
|
->asSiteSignal('imitseed-'.$i.'-'.Str::random(6).'.test')
|
|
->create([
|
|
'name' => "IMIT-seed-client-{$i}",
|
|
'tenant_id' => $tenant->id,
|
|
'regions' => [$moscow],
|
|
'delivery_days_mask' => 127,
|
|
'daily_limit_target' => 1000,
|
|
'is_active' => true,
|
|
]);
|
|
|
|
DB::table('project_supplier_links')->insert([
|
|
'project_id' => $project->id,
|
|
'supplier_project_id' => $supplier->id,
|
|
'platform' => $supplier->platform,
|
|
'subject_code' => null,
|
|
]);
|
|
}
|
|
|
|
// Build the routing snapshot for the active date the router will query.
|
|
Artisan::call('snapshot:rebuild', ['--date' => $this->activeDate()]);
|
|
|
|
// Inject synthetic leads through the real routing + ledger pipeline.
|
|
$injected = 0;
|
|
for ($n = 1; $n <= $leads; $n++) {
|
|
$phone = '79'.str_pad((string) random_int(0, 999_999_999), 9, '0', STR_PAD_LEFT);
|
|
$vid = random_int(100_000_000, 999_999_999);
|
|
|
|
$lead = SupplierLead::factory()->create([
|
|
'supplier_project_id' => $supplier->id,
|
|
'platform' => $supplier->platform,
|
|
'phone' => $phone,
|
|
'vid' => $vid,
|
|
'raw_payload' => [
|
|
'vid' => $vid,
|
|
'project' => $supplier->platform.'_'.$supplierKey,
|
|
'tag' => 'Москва',
|
|
'phone' => $phone,
|
|
'phones' => [$phone],
|
|
'time' => now()->getTimestamp(),
|
|
],
|
|
'received_at' => now(),
|
|
'source' => 'webhook',
|
|
'processed_at' => null,
|
|
'deals_created_count' => null,
|
|
]);
|
|
|
|
RouteSupplierLeadJob::dispatchSync($lead->id);
|
|
$injected++;
|
|
}
|
|
|
|
$this->info("imitation:seed done — {$clients} clients, {$injected} leads injected (region from tag, DaData disabled).");
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* Active snapshot date — mirrors LeadRouter::activeSnapshotDate()
|
|
* (today before 21:00 MSK, tomorrow at/after).
|
|
*/
|
|
private function activeDate(): string
|
|
{
|
|
$msk = Carbon::now('Europe/Moscow');
|
|
|
|
return ($msk->hour >= 21 ? $msk->copy()->addDay() : $msk)->format('Y-m-d');
|
|
}
|
|
}
|