35d51df6f7
DeleteSupplierProjectJob перед удалением донора проверяет hasActiveSnapshotTail: если за сегодня/завтра есть снимок маршрутизации с источником этого донора (sms по sender+keyword, site с поддоменом, call по identifier — зеркало LeadRouter), удаление откладывается до следующего CleanupInactiveSupplierProjectsJob (02:00). Иначе удалив донора оборвём матч хвостового лида по старому источнику. Эпик 2 Task 2.4. baseline +1 (Mockery once-noise, как DeleteSupplierProjectJobTest). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
103 lines
4.5 KiB
PHP
103 lines
4.5 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
use App\Jobs\Supplier\DeleteSupplierProjectJob;
|
||
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 Tests\Concerns\SharesSupplierPdo;
|
||
|
||
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
|
||
|
||
/**
|
||
* Task 2.4 — отложенное удаление донора, пока летит хвост слепка.
|
||
*
|
||
* Если у supplier_project нет других потребителей, но за активную/завтрашнюю дату
|
||
* есть снимок маршрутизации с его источником — хвостовой лид ещё может прийти, и
|
||
* LeadRouter ищет snapshot по источнику донора. Удалив донора, мы оборвём матч.
|
||
* Поэтому удаление откладывается до следующего CleanupInactiveSupplierProjectsJob.
|
||
*/
|
||
function insertTailSnapshot(
|
||
Tenant $tenant,
|
||
string $signalType,
|
||
?string $signalIdentifier,
|
||
?array $smsSenders = null,
|
||
?string $smsKeyword = null,
|
||
?string $date = null,
|
||
): void {
|
||
DB::table('project_routing_snapshots')->insert([
|
||
'snapshot_date' => $date ?? Carbon::tomorrow('Europe/Moscow')->toDateString(),
|
||
'project_id' => random_int(900000, 999999),
|
||
'tenant_id' => $tenant->id,
|
||
'daily_limit' => 10,
|
||
'delivery_days_mask' => 127,
|
||
'regions' => '{}',
|
||
'signal_type' => $signalType,
|
||
'signal_identifier' => $signalIdentifier,
|
||
'sms_senders' => $smsSenders === null ? null : json_encode($smsSenders),
|
||
'sms_keyword' => $smsKeyword,
|
||
'expected_volume' => 10,
|
||
'delivered_count' => 0,
|
||
'created_at' => now(),
|
||
]);
|
||
}
|
||
|
||
it('не удаляет supplier_project, пока есть снимок с его источником за активную дату', function (): void {
|
||
$tenant = Tenant::factory()->create();
|
||
$sp = SupplierProject::query()->create([
|
||
'platform' => 'B1', 'signal_type' => 'call', 'unique_key' => '79991110000',
|
||
'supplier_external_id' => '555', 'current_limit' => 1,
|
||
]);
|
||
// Снимок-хвост по тому же источнику (call/79991110000), потребителей pivot — нет.
|
||
insertTailSnapshot($tenant, 'call', '79991110000');
|
||
|
||
$mock = Mockery::mock(SupplierPortalClient::class);
|
||
$mock->shouldNotReceive('deleteProject');
|
||
app()->instance(SupplierPortalClient::class, $mock);
|
||
|
||
(new DeleteSupplierProjectJob([$sp->id]))->handle(app(SupplierPortalClient::class));
|
||
|
||
// Донор НЕ удалён — удаление отложено, пока летит хвост.
|
||
expect(SupplierProject::find($sp->id))->not->toBeNull();
|
||
});
|
||
|
||
it('откладывает удаление sms-донора, пока снимок несёт его sender в хвосте', function (): void {
|
||
$tenant = Tenant::factory()->create();
|
||
$sp = SupplierProject::query()->create([
|
||
'platform' => 'B3', 'signal_type' => 'sms', 'unique_key' => 'Caranga',
|
||
'supplier_external_id' => '777', 'current_limit' => 1,
|
||
]);
|
||
insertTailSnapshot($tenant, 'sms', null, smsSenders: ['Caranga']);
|
||
|
||
$mock = Mockery::mock(SupplierPortalClient::class);
|
||
$mock->shouldNotReceive('deleteProject');
|
||
app()->instance(SupplierPortalClient::class, $mock);
|
||
|
||
(new DeleteSupplierProjectJob([$sp->id]))->handle(app(SupplierPortalClient::class));
|
||
|
||
expect(SupplierProject::find($sp->id))->not->toBeNull();
|
||
});
|
||
|
||
it('удаляет донора, когда хвоста слепка нет (регрессия базового удаления)', function (): void {
|
||
$tenant = Tenant::factory()->create();
|
||
$sp = SupplierProject::query()->create([
|
||
'platform' => 'B1', 'signal_type' => 'call', 'unique_key' => '79992220000',
|
||
'supplier_external_id' => '600', 'current_limit' => 1,
|
||
]);
|
||
// Снимок есть, но по ДРУГОМУ источнику — хвоста для этого донора нет.
|
||
insertTailSnapshot($tenant, 'call', '70000000000');
|
||
|
||
$mock = Mockery::mock(SupplierPortalClient::class);
|
||
$mock->shouldReceive('deleteProject')->once()->with(600);
|
||
app()->instance(SupplierPortalClient::class, $mock);
|
||
|
||
(new DeleteSupplierProjectJob([$sp->id]))->handle(app(SupplierPortalClient::class));
|
||
|
||
expect(SupplierProject::find($sp->id))->toBeNull();
|
||
});
|