updateOrInsert( ['key' => 'supplier_export_mode'], ['value' => 'online'], ); }); afterEach(fn () => Carbon::setTestNow()); it('в окне 18:00→00:00 онлайн не шлёт поставщику, а откладывает', function (): void { Carbon::setTestNow(Carbon::parse('2026-06-25 19:00:00', 'Europe/Moscow')); $tenant = Tenant::factory()->create(); $project = Project::factory()->for($tenant)->create([ 'signal_type' => 'site', 'signal_identifier' => 'okna.ru', 'is_active' => true, 'daily_limit_target' => 10, ]); // Канал НЕ должен дёргаться — defer происходит до любых обращений к поставщику. $channel = Mockery::mock(SupplierProjectChannel::class); $channel->shouldNotReceive('createProject'); $channel->shouldNotReceive('updateProject'); app()->instance(SupplierProjectChannel::class, $channel); (new SyncSupplierProjectJob($project->id))->handle(app(SupplierProjectChannel::class)); expect(DB::table('supplier_deferred_sync')->where('project_id', $project->id)->exists())->toBeTrue(); }); it('повторная правка в окне не плодит дублей (project_id PK, ON CONFLICT DO NOTHING)', function (): void { Carbon::setTestNow(Carbon::parse('2026-06-25 19:30:00', 'Europe/Moscow')); $tenant = Tenant::factory()->create(); $project = Project::factory()->for($tenant)->create([ 'signal_type' => 'site', 'signal_identifier' => 'okna.ru', 'is_active' => true, 'daily_limit_target' => 10, ]); $channel = Mockery::mock(SupplierProjectChannel::class); app()->instance(SupplierProjectChannel::class, $channel); (new SyncSupplierProjectJob($project->id))->handle(app(SupplierProjectChannel::class)); (new SyncSupplierProjectJob($project->id))->handle(app(SupplierProjectChannel::class)); expect(DB::table('supplier_deferred_sync')->where('project_id', $project->id)->count())->toBe(1); }); it('вне окна (до 18:00) онлайн НЕ откладывает — идёт обычным путём', function (): void { Carbon::setTestNow(Carbon::parse('2026-06-25 12:00:00', 'Europe/Moscow')); $tenant = Tenant::factory()->create(); $project = Project::factory()->for($tenant)->create([ 'signal_type' => 'site', 'signal_identifier' => 'okna.ru', 'is_active' => true, 'daily_limit_target' => 10, ]); // Вне окна defer-ветка не срабатывает: канал ПОЛУЧИТ обращения (online sync идёт). // Не мокаем портал целиком — достаточно проверить, что в defer-очередь НЕ попало. $channel = Mockery::mock(SupplierProjectChannel::class); $channel->shouldReceive('updateProject')->andReturnNull(); $channel->shouldReceive('createProject')->andReturn(1); app()->instance(SupplierProjectChannel::class, $channel); try { (new SyncSupplierProjectJob($project->id))->handle(app(SupplierProjectChannel::class)); } catch (\Throwable) { // Онлайн-путь может бросить retry при неполном портал-ответе — нам важна только defer-очередь. } expect(DB::table('supplier_deferred_sync')->where('project_id', $project->id)->exists())->toBeFalse(); });