135 lines
4.9 KiB
PHP
135 lines
4.9 KiB
PHP
|
|
<?php
|
|||
|
|
|
|||
|
|
declare(strict_types=1);
|
|||
|
|
|
|||
|
|
use App\Jobs\RouteSupplierLeadJob;
|
|||
|
|
use App\Models\Project;
|
|||
|
|
use App\Models\SupplierLead;
|
|||
|
|
use App\Models\SupplierProject;
|
|||
|
|
use App\Models\Tenant;
|
|||
|
|
use App\Services\Billing\LedgerService;
|
|||
|
|
use App\Services\LeadDistributor;
|
|||
|
|
use App\Services\LeadRouter;
|
|||
|
|
use App\Services\NotificationService;
|
|||
|
|
use App\Services\RegionTagResolver;
|
|||
|
|
use App\Services\SupplierProjects\SupplierProjectResolver;
|
|||
|
|
use Carbon\Carbon;
|
|||
|
|
use Database\Seeders\PricingTierSeeder;
|
|||
|
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
|||
|
|
use Illuminate\Support\Facades\DB;
|
|||
|
|
use Tests\Concerns\SharesSupplierPdo;
|
|||
|
|
|
|||
|
|
uses(DatabaseTransactions::class);
|
|||
|
|
uses(SharesSupplierPdo::class);
|
|||
|
|
|
|||
|
|
beforeEach(function (): void {
|
|||
|
|
$this->seed(PricingTierSeeder::class);
|
|||
|
|
DB::statement("SELECT set_config('app.current_tenant_id', '0', true)");
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
function runSnapshotRouteJob(int $supplierLeadId): void
|
|||
|
|
{
|
|||
|
|
(new RouteSupplierLeadJob($supplierLeadId))->handle(
|
|||
|
|
app(LeadRouter::class),
|
|||
|
|
app(SupplierProjectResolver::class),
|
|||
|
|
app(NotificationService::class),
|
|||
|
|
app(LedgerService::class),
|
|||
|
|
app(LeadDistributor::class),
|
|||
|
|
app(RegionTagResolver::class),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
it('uses snapshot daily_limit, not live daily_limit_target (R-04/R-06)', function (): void {
|
|||
|
|
Carbon::setTestNow('2026-05-28 12:00:00', 'Europe/Moscow');
|
|||
|
|
|
|||
|
|
$tenant = Tenant::factory()->create(['balance_rub' => '500.00', 'frozen_by_balance_at' => null]);
|
|||
|
|
$project = Project::factory()->for($tenant)->create([
|
|||
|
|
'is_active' => true,
|
|||
|
|
'delivery_days_mask' => 127,
|
|||
|
|
'daily_limit_target' => 100, // live limit big
|
|||
|
|
'delivered_today' => 4,
|
|||
|
|
'delivered_in_month' => 0,
|
|||
|
|
]);
|
|||
|
|
$sp = SupplierProject::factory()->create(['signal_type' => 'site']);
|
|||
|
|
linkProjectToSupplier($project, $sp);
|
|||
|
|
// Snapshot имеет МАЛЕНЬКИЙ daily_limit=5 — после доставки 1 deal'a должно стать 5.
|
|||
|
|
createRoutingSnapshotFromProject(
|
|||
|
|
$project,
|
|||
|
|
date: '2026-05-28',
|
|||
|
|
signalType: 'site',
|
|||
|
|
signalIdentifier: $sp->unique_key,
|
|||
|
|
dailyLimit: 5,
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
$lead = SupplierLead::factory()->create([
|
|||
|
|
'supplier_project_id' => $sp->id,
|
|||
|
|
'raw_payload' => ['project' => $sp->platform.'_'.$sp->unique_key, 'phones' => ['79161234567']],
|
|||
|
|
'phone' => '79161234567',
|
|||
|
|
'vid' => 1001,
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
runSnapshotRouteJob($lead->id);
|
|||
|
|
|
|||
|
|
expect(DB::table('deals')->where('tenant_id', $tenant->id)->count())->toBe(1);
|
|||
|
|
|
|||
|
|
// delivered_today инкрементнут на live, delivered_count на snapshot.
|
|||
|
|
expect((int) DB::table('projects')->where('id', $project->id)->value('delivered_today'))->toBe(5);
|
|||
|
|
|
|||
|
|
$snap = DB::table('project_routing_snapshots')
|
|||
|
|
->where('snapshot_date', '2026-05-28')
|
|||
|
|
->where('project_id', $project->id)
|
|||
|
|
->first();
|
|||
|
|
expect((int) $snap->delivered_count)->toBe(1);
|
|||
|
|
|
|||
|
|
Carbon::setTestNow();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('rejects lead when is_active becomes false under lock (R-09)', function (): void {
|
|||
|
|
Carbon::setTestNow('2026-05-28 12:00:00', 'Europe/Moscow');
|
|||
|
|
|
|||
|
|
$tenant = Tenant::factory()->create(['balance_rub' => '500.00', 'frozen_by_balance_at' => null]);
|
|||
|
|
// Live state: is_active=true для snapshot:backfill, но потом клиент нажмёт «пауза»
|
|||
|
|
// (между matchEligibleProjects и handle). Имитируем это inline UPDATE'ом.
|
|||
|
|
$project = Project::factory()->for($tenant)->create([
|
|||
|
|
'is_active' => true,
|
|||
|
|
'delivery_days_mask' => 127,
|
|||
|
|
'daily_limit_target' => 10,
|
|||
|
|
'delivered_today' => 0,
|
|||
|
|
'delivered_in_month' => 0,
|
|||
|
|
]);
|
|||
|
|
$sp = SupplierProject::factory()->create(['signal_type' => 'site']);
|
|||
|
|
linkProjectToSupplier($project, $sp);
|
|||
|
|
// Snapshot НЕ paused (fixed до момента pause'а).
|
|||
|
|
createRoutingSnapshotFromProject(
|
|||
|
|
$project,
|
|||
|
|
date: '2026-05-28',
|
|||
|
|
signalType: 'site',
|
|||
|
|
signalIdentifier: $sp->unique_key,
|
|||
|
|
dailyLimit: 10,
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// ↓ Имитация: клиент paused проект в окне между matchEligible и handle.
|
|||
|
|
DB::table('projects')->where('id', $project->id)->update(['is_active' => false]);
|
|||
|
|
|
|||
|
|
$lead = SupplierLead::factory()->create([
|
|||
|
|
'supplier_project_id' => $sp->id,
|
|||
|
|
'raw_payload' => ['project' => $sp->platform.'_'.$sp->unique_key, 'phones' => ['79161234567']],
|
|||
|
|
'phone' => '79161234567',
|
|||
|
|
'vid' => 1002,
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
runSnapshotRouteJob($lead->id);
|
|||
|
|
|
|||
|
|
// Snapshot говорит «доставлять», но live is_active=false под lock'ом — НЕ доставляем.
|
|||
|
|
expect(DB::table('deals')->where('tenant_id', $tenant->id)->count())->toBe(0);
|
|||
|
|
expect((int) DB::table('projects')->where('id', $project->id)->value('delivered_today'))->toBe(0);
|
|||
|
|
|
|||
|
|
$snap = DB::table('project_routing_snapshots')
|
|||
|
|
->where('snapshot_date', '2026-05-28')
|
|||
|
|
->where('project_id', $project->id)
|
|||
|
|
->first();
|
|||
|
|
expect((int) $snap->delivered_count)->toBe(0);
|
|||
|
|
|
|||
|
|
Carbon::setTestNow();
|
|||
|
|
});
|