str_contains(basename($path), 'client_sms') || str_contains(basename($path), 'sms_global_optouts'), )); // Модуль продаж (`sales_sms`) — чужая работа, сюда не берём. return array_values(array_filter( $mine, fn (string $path) => ! str_contains(basename($path), 'sales_sms'), )); } /** * Отдельный случай, найденный проверяющим при приёмке Этапа 2 (журнал В-80, З-5). * * Повторный запуск — это не только «не сломаться», но и «доделать недоделанное». * Миграция ключа заказа состоит из ДВУХ шагов: колонка и уникальный индекс. Если * ручная подача SQL на бою прервалась между ними, колонка есть, а индекса нет — * и повторный накат, выходящий по наличию колонки, молча пройдёт мимо. Исчезнет * последняя линия обороны от двойного заказа: два одновременных запроса с одним * ключом создадут две рассылки и спишут деньги дважды. * * Класс поломки прежний — «тихо стало хуже»: ошибок нет, тесты зелёные, защиты нет. */ it('повторный запуск восстанавливает индекс ключа заказа, если его нет', function () { $indexExists = fn () => DB::selectOne(<<<'SQL' SELECT 1 AS ok FROM pg_indexes WHERE schemaname = 'public' AND indexname = 'client_sms_campaigns_idem_uniq' SQL) !== null; expect($indexExists())->toBeTrue('Индекса нет ещё до проверки — миграция не применилась'); // Так выглядит база после прерванной ручной подачи SQL: колонка легла, индекс — нет. DB::statement('DROP INDEX client_sms_campaigns_idem_uniq'); expect($indexExists())->toBeFalse(); $path = collect(clientSmsMigrationFiles()) ->first(fn (string $p) => str_contains(basename($p), 'idempotency_key')); expect($path)->not->toBeNull('Миграция ключа заказа не найдена по имени файла'); (require $path)->up(); expect($indexExists())->toBeTrue( 'Повторный запуск не восстановил уникальный индекс — защита от двойного заказа потеряна молча', ); }); it('все миграции модуля переживают повторный запуск', function () { $files = clientSmsMigrationFiles(); expect($files)->not->toBeEmpty(); foreach ($files as $path) { $migration = require $path; try { $migration->up(); } catch (Throwable $e) { $this->fail(sprintf( "Миграция %s не переживает повторный запуск:\n%s", basename($path), $e->getMessage(), )); } } });