3a5d721478
- normalizeLive матчит наши строки по supplier_external_id (tag=регион, не _lidpotok) — иначе live всегда пуст - shouldBeOff исключает ключи, активные по формуле (нет ложного should_be_off) - recordRunSummary → insertGetId, проверка получает sync_run_id - обновлён phpstan-baseline.neon: count mock() 2→3 (новый тест на баг 2) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
152 lines
6.9 KiB
PHP
152 lines
6.9 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
use App\Jobs\Supplier\VerifySupplierOrderJob;
|
||
use App\Mail\SupplierOrderMismatchMail;
|
||
use App\Models\Project;
|
||
use App\Models\SupplierProject;
|
||
use App\Models\Tenant;
|
||
use App\Services\Supplier\SupplierPortalClient;
|
||
use Carbon\Carbon;
|
||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Illuminate\Support\Facades\Mail;
|
||
use Tests\Concerns\SharesSupplierPdo;
|
||
|
||
uses(DatabaseTransactions::class);
|
||
uses(SharesSupplierPdo::class);
|
||
|
||
beforeEach(function (): void {
|
||
Carbon::setTestNow(Carbon::parse('2026-07-09 18:04:00', 'Europe/Moscow'));
|
||
});
|
||
|
||
afterEach(function (): void {
|
||
Carbon::setTestNow();
|
||
});
|
||
|
||
/**
|
||
* Активный call-проект daily_limit_target=3 → задуманный заказ order=max(3, ceil(3/3))=3,
|
||
* поделённый на B1/B2/B3 (largest-remainder) → по 1 на каждую площадку.
|
||
* Снимок на завтра ОБЯЗАТЕЛЕН — collectEligibleProjects() читает project_routing_snapshots,
|
||
* не live projects (Task 2.9 slepok-инвариант).
|
||
*/
|
||
function seedActiveCallProject(string $identifier, int $limit): Project
|
||
{
|
||
$tenant = Tenant::factory()->create(['frozen_by_balance_at' => null]);
|
||
$project = Project::factory()->for($tenant)->create([
|
||
'is_active' => true,
|
||
'signal_type' => 'call',
|
||
'signal_identifier' => $identifier,
|
||
'daily_limit_target' => $limit,
|
||
'delivery_days_mask' => 127,
|
||
'regions' => [],
|
||
]);
|
||
insertSnapshotForTomorrow($project, dailyLimit: $limit, deliveryDaysMask: 127);
|
||
|
||
return $project;
|
||
}
|
||
|
||
/**
|
||
* Наша supplier_project, которую мы уже выключили (inactive_since IS NOT NULL) —
|
||
* SupplierOrderVerifier::diff должен поймать kind=should_be_off, если поставщик
|
||
* до сих пор показывает её включённой (status=1). supplier_external_id — признак
|
||
* «наша строка» в normalizeLive() (баг 1: tag НЕ годится — робот шлёт регион/«РФ»).
|
||
*/
|
||
function seedPausedSupplierProject(string $identifier, string $platform, string $signalType, string $externalId): SupplierProject
|
||
{
|
||
return SupplierProject::factory()->create([
|
||
'platform' => $platform,
|
||
'signal_type' => $signalType,
|
||
'unique_key' => $identifier,
|
||
'supplier_external_id' => $externalId,
|
||
'inactive_since' => now(),
|
||
]);
|
||
}
|
||
|
||
it('нет расхождений → письма нет, статус ok', function (): void {
|
||
Mail::fake();
|
||
seedActiveCallProject('79135397707', 3);
|
||
|
||
SupplierProject::factory()->create([
|
||
'platform' => 'B1', 'signal_type' => 'call', 'unique_key' => '79135397707',
|
||
'supplier_external_id' => '1001', 'current_limit' => 1, 'inactive_since' => null,
|
||
]);
|
||
SupplierProject::factory()->create([
|
||
'platform' => 'B2', 'signal_type' => 'call', 'unique_key' => '79135397707',
|
||
'supplier_external_id' => '1002', 'current_limit' => 1, 'inactive_since' => null,
|
||
]);
|
||
SupplierProject::factory()->create([
|
||
'platform' => 'B3', 'signal_type' => 'call', 'unique_key' => '79135397707',
|
||
'supplier_external_id' => '1003', 'current_limit' => 1, 'inactive_since' => null,
|
||
]);
|
||
|
||
$this->mock(SupplierPortalClient::class, function ($m): void {
|
||
$m->shouldReceive('listProjects')->andReturn([
|
||
['id' => '1001', 'name' => 'B1_79135397707', 'content' => '79135397707', 'type' => 'calls', 'lim' => 1, 'status' => '1', 'tag' => 'РФ'],
|
||
['id' => '1002', 'name' => 'B2_79135397707', 'content' => '79135397707', 'type' => 'calls', 'lim' => 1, 'status' => '1', 'tag' => 'РФ'],
|
||
['id' => '1003', 'name' => 'B3_79135397707', 'content' => '79135397707', 'type' => 'calls', 'lim' => 1, 'status' => '1', 'tag' => 'РФ'],
|
||
]);
|
||
});
|
||
|
||
(new VerifySupplierOrderJob(attempt: 2))->handle();
|
||
|
||
Mail::assertNothingQueued();
|
||
$row = DB::connection('pgsql_supplier')->table('supplier_order_checks')->latest('id')->first();
|
||
expect($row)->not->toBeNull();
|
||
expect($row->status)->toBe('ok')->and($row->mismatch_count)->toBe(0);
|
||
});
|
||
|
||
it('пауза не дошла (should_be_off) → письмо + статус mismatch на попытке 2', function (): void {
|
||
Mail::fake();
|
||
seedPausedSupplierProject('automoney.ru', 'B3', 'site', '2001');
|
||
|
||
$this->mock(SupplierPortalClient::class, function ($m): void {
|
||
$m->shouldReceive('listProjects')->andReturn([
|
||
['id' => '2001', 'name' => 'B3_automoney.ru', 'content' => 'automoney.ru', 'type' => 'hosts', 'lim' => 1, 'status' => '1', 'tag' => 'РФ'],
|
||
]);
|
||
});
|
||
|
||
(new VerifySupplierOrderJob(attempt: 2))->handle();
|
||
|
||
Mail::assertQueued(SupplierOrderMismatchMail::class);
|
||
$row = DB::connection('pgsql_supplier')->table('supplier_order_checks')->latest('id')->first();
|
||
expect($row)->not->toBeNull();
|
||
expect($row->status)->toBe('mismatch')->and($row->mismatch_count)->toBe(1);
|
||
});
|
||
|
||
it('ключ активен по формуле, но inactive_since завис → НЕ ложная тревога (баг 2)', function (): void {
|
||
Mail::fake();
|
||
seedActiveCallProject('79999999999', 3);
|
||
|
||
// B1 «завис» с inactive_since, хотя группа сейчас активна по формуле —
|
||
// должен быть исключён из shouldBeOff, а не дать ложный should_be_off.
|
||
SupplierProject::factory()->create([
|
||
'platform' => 'B1', 'signal_type' => 'call', 'unique_key' => '79999999999',
|
||
'supplier_external_id' => '3001', 'current_limit' => 1, 'inactive_since' => now(),
|
||
]);
|
||
SupplierProject::factory()->create([
|
||
'platform' => 'B2', 'signal_type' => 'call', 'unique_key' => '79999999999',
|
||
'supplier_external_id' => '3002', 'current_limit' => 1, 'inactive_since' => null,
|
||
]);
|
||
SupplierProject::factory()->create([
|
||
'platform' => 'B3', 'signal_type' => 'call', 'unique_key' => '79999999999',
|
||
'supplier_external_id' => '3003', 'current_limit' => 1, 'inactive_since' => null,
|
||
]);
|
||
|
||
$this->mock(SupplierPortalClient::class, function ($m): void {
|
||
$m->shouldReceive('listProjects')->andReturn([
|
||
['id' => '3001', 'name' => 'B1_79999999999', 'content' => '79999999999', 'type' => 'calls', 'lim' => 1, 'status' => '1', 'tag' => 'РФ'],
|
||
['id' => '3002', 'name' => 'B2_79999999999', 'content' => '79999999999', 'type' => 'calls', 'lim' => 1, 'status' => '1', 'tag' => 'РФ'],
|
||
['id' => '3003', 'name' => 'B3_79999999999', 'content' => '79999999999', 'type' => 'calls', 'lim' => 1, 'status' => '1', 'tag' => 'РФ'],
|
||
]);
|
||
});
|
||
|
||
(new VerifySupplierOrderJob(attempt: 2))->handle();
|
||
|
||
Mail::assertNothingQueued();
|
||
$row = DB::connection('pgsql_supplier')->table('supplier_order_checks')->latest('id')->first();
|
||
expect($row)->not->toBeNull();
|
||
expect($row->status)->toBe('ok')->and($row->mismatch_count)->toBe(0);
|
||
});
|