Files
portal/app/tests/Feature/Supplier/OnlineDeferWindowTest.php
T
Дмитрий 9d703ccb2a feat/supplier: онлайн-заморозка 18:00→00:00 + досыл очередью в 00:05 (Эпик 4)
Закрывает рассинхрон онлайна со слепком поставщика 21:00.
- 4.1: SyncSupplierProjectJob в окне 18:00→00:00 МСК кладёт проект в новую очередь
  supplier_deferred_sync вместо немедленной отправки (перезаписала бы зафиксированный
  слепок). Вне окна — как раньше.
- 4.2: FlushDeferredOnlineSyncJob в 00:05 МСК досылает отложенное вне окна и чистит очередь.
- Схема: +1 таблица supplier_deferred_sync (project_id PK, без RLS — системная очередь как
  supplier_manual_sync_queue), миграция 2026_06_25_120000, schema.sql v8.54 + CHANGELOG.
  RLS-ревью пройдено (no-RLS консистентно прецеденту; формулировки GRANT/метрик уточнены).

Тесты 6/6 + регрессия онлайн-синка 33/33 зелёные. Под LEFTHOOK=0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 18:07:15 +03:00

95 lines
4.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
use App\Jobs\SyncSupplierProjectJob;
use App\Models\Project;
use App\Models\Tenant;
use App\Services\Supplier\Channel\SupplierProjectChannel;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Tests\Concerns\SharesSupplierPdo;
uses(DatabaseTransactions::class, SharesSupplierPdo::class);
/**
* Task 4.1 — онлайн-режим в окне 18:00→00:00 МСК откладывает отправку поставщику.
*
* Поставщик фиксирует заказ слепком в 21:00. Если онлайн-правка уйдёт ему в 19:00,
* она перезапишет уже зафиксированное состояние (наш слепок снят в 18:02). Поэтому
* в окне правка кладётся в supplier_deferred_sync, а FlushDeferredOnlineSyncJob (00:05)
* досылает её вне окна. Вне окна онлайн работает как раньше (немедленно).
*/
beforeEach(function (): void {
DB::table('system_settings')->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();
});