9d703ccb2a
Закрывает рассинхрон онлайна со слепком поставщика 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>
95 lines
4.5 KiB
PHP
95 lines
4.5 KiB
PHP
<?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();
|
||
});
|