c8ec7a6616
collectEligibleProjects переопределяет signal_identifier/sms_senders/sms_keyword значениями слепка (как daily_limit/regions). Клиент мог сменить источник в окне 18:02 (snapshot) → 18:05 (sync), но завтрашний заказ поставщику фиксируется слепком (slepok-инвариант) — раздача доводит хвост по старому источнику. Тест-хелпер insertSnapshotForTomorrow теперь зеркалит источник проекта в слепок (как прод SnapshotProjectRoutingJob), иначе группировка офлайн-батча пустеет. Эпик 2 Task 2.6. Под LEFTHOOK=0 (окруженческий larastan-шум, см. память #111). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
115 lines
4.4 KiB
PHP
115 lines
4.4 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
use App\Jobs\Supplier\SyncSupplierProjectsJob;
|
||
use App\Models\Project;
|
||
use App\Models\Tenant;
|
||
use Carbon\Carbon;
|
||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Tests\Concerns\SharesSupplierPdo;
|
||
|
||
uses(DatabaseTransactions::class);
|
||
uses(SharesSupplierPdo::class);
|
||
|
||
/**
|
||
* Task 2.6 — офлайн-батч обязан заказывать у поставщика по источнику ИЗ СЛЕПКА,
|
||
* а не по live projects.* . Закрывает рассинхрон окна 18:02 (snapshot) → 18:05 (sync):
|
||
* клиент мог сменить источник между двумя cron'ами, но завтрашний заказ уже зафиксирован
|
||
* слепком (slepok-инвариант) — иначе поставщик заберёт лиды по НОВОМУ источнику,
|
||
* а раздача доводит хвост по СТАРОМУ → рассинхрон денег.
|
||
*/
|
||
function insertTomorrowSmsSnapshot(
|
||
Project $project,
|
||
array $smsSenders,
|
||
?string $smsKeyword = null,
|
||
): void {
|
||
$tomorrow = Carbon::tomorrow('Europe/Moscow')->toDateString();
|
||
DB::table('project_routing_snapshots')->insert([
|
||
'snapshot_date' => $tomorrow,
|
||
'project_id' => $project->id,
|
||
'tenant_id' => $project->tenant_id,
|
||
'daily_limit' => 10,
|
||
'delivery_days_mask' => 127,
|
||
'regions' => '{}',
|
||
'signal_type' => 'sms',
|
||
'signal_identifier' => null,
|
||
'sms_senders' => json_encode($smsSenders),
|
||
'sms_keyword' => $smsKeyword,
|
||
'expected_volume' => 10,
|
||
'delivered_count' => 0,
|
||
'created_at' => now(),
|
||
]);
|
||
}
|
||
|
||
it('батч заказывает по источнику из слепка, даже если live-источник сменили после слепка', function (): void {
|
||
Carbon::setTestNow(Carbon::parse('2026-05-27 18:04:00', 'Europe/Moscow'));
|
||
|
||
$tenant = Tenant::factory()->create(['frozen_by_balance_at' => null]);
|
||
$project = Project::factory()->for($tenant)->create([
|
||
'is_active' => true,
|
||
'signal_type' => 'sms',
|
||
'signal_identifier' => null,
|
||
'sms_senders' => ['Caranga'],
|
||
'sms_keyword' => null,
|
||
]);
|
||
// Снимок снят с источника Caranga.
|
||
insertTomorrowSmsSnapshot($project, smsSenders: ['Caranga']);
|
||
|
||
// ↓ Клиент сменил live-источник в окне 18:02→18:05 (после слепка).
|
||
DB::table('projects')->where('id', $project->id)->update([
|
||
'sms_senders' => json_encode(['NewSender']),
|
||
]);
|
||
|
||
$projects = (new SyncSupplierProjectsJob)->collectEligibleProjects();
|
||
|
||
$ours = $projects->where('id', $project->id);
|
||
expect($ours)->toHaveCount(1);
|
||
// Источник ИЗ СЛЕПКА (Caranga), не live (NewSender).
|
||
expect((array) $ours->first()->sms_senders)->toBe(['Caranga']);
|
||
|
||
Carbon::setTestNow();
|
||
});
|
||
|
||
it('батч переопределяет signal_identifier (site) значением слепка', function (): void {
|
||
Carbon::setTestNow(Carbon::parse('2026-05-27 18:04:00', 'Europe/Moscow'));
|
||
|
||
$tenant = Tenant::factory()->create(['frozen_by_balance_at' => null]);
|
||
$project = Project::factory()->for($tenant)->create([
|
||
'is_active' => true,
|
||
'signal_type' => 'site',
|
||
'signal_identifier' => 'old-site.ru',
|
||
'sms_senders' => null,
|
||
'sms_keyword' => null,
|
||
]);
|
||
$tomorrow = Carbon::tomorrow('Europe/Moscow')->toDateString();
|
||
DB::table('project_routing_snapshots')->insert([
|
||
'snapshot_date' => $tomorrow,
|
||
'project_id' => $project->id,
|
||
'tenant_id' => $project->tenant_id,
|
||
'daily_limit' => 10,
|
||
'delivery_days_mask' => 127,
|
||
'regions' => '{}',
|
||
'signal_type' => 'site',
|
||
'signal_identifier' => 'old-site.ru',
|
||
'sms_senders' => null,
|
||
'sms_keyword' => null,
|
||
'expected_volume' => 10,
|
||
'delivered_count' => 0,
|
||
'created_at' => now(),
|
||
]);
|
||
|
||
DB::table('projects')->where('id', $project->id)->update([
|
||
'signal_identifier' => 'new-site.ru',
|
||
]);
|
||
|
||
$projects = (new SyncSupplierProjectsJob)->collectEligibleProjects();
|
||
|
||
$ours = $projects->where('id', $project->id);
|
||
expect($ours)->toHaveCount(1);
|
||
expect($ours->first()->signal_identifier)->toBe('old-site.ru');
|
||
|
||
Carbon::setTestNow();
|
||
});
|