# scan-emulator Specification ## Purpose Эмулятор ИС Сканирование для разработки и тестов: воспроизводит протокол горячих папок и формат ответов Content AI, отдавая реальные ответы из `../fixtures/ocr` по имени файла, чтобы конвейер `scan-gateway` проверялся без доступа к банковской системе. ## Requirements ### Requirement: Эмулятор отвечает по протоколу горячих папок Процесс `python -m src.scan_emulator` SHALL опрашивать входящий каталог каждую секунду; файлы с суффиксами `.part`, `.tmp`, `.done` и файлы, начинающиеся с `.`, SHALL игнорироваться. Для нового файла `.` через `SCAN_EMULATOR_DELAY` секунд (по умолчанию 2) эмулятор SHALL создать каталог `//` с набором ответов (по одному файлу на документ); каталог SHALL появиться в `` атомарно целиком: набор записывается во временный каталог эмулятора и переименовывается. После обработки входящий файл SHALL быть переименован в `..done`, чтобы не обрабатываться повторно. #### Scenario: Один файл - **WHEN** во входящем каталоге появляется `3f1c….pdf` с содержимым `aep_5.pdf` - **THEN** через ≤ 5 с существует `Output/3f1c…/3f1c….json`, а входящий файл переименован в `3f1c….pdf.done` #### Scenario: Набор появляется целиком - **WHEN** эмулятор записывает набор из нескольких файлов - **THEN** каталог `Output/3f1c…/` либо не существует, либо уже содержит весь набор; частичного набора в `Output` нет #### Scenario: Временные файлы шлюза игнорируются - **WHEN** во входящем каталоге лежат `.3f1c….pdf.part` и `3f1c….pdf.done` - **THEN** эмулятор не создаёт для них ответов ### Requirement: Ответы берутся из фикстур по содержимому входящего файла Эмулятор SHALL индексировать `SCAN_FIXTURES_DIR`: для каждого файла, не являющегося `*.json`, вычисляется sha256 → основа имени `` (имя без расширения; регистр расширения не важен); индекс SHALL пересчитываться, когда меняется список файлов каталога. Для входящего файла SHALL вычисляться sha256 содержимого; при совпадении эмулятор SHALL собрать **все** файлы `.json` и `_*.json`: `.json` записывается как `.json`, `_.json` — как `_.json`, в каждом `file_uid = `. Фикстуры вида `<другие символы>.json` без разделителя `_` (например, `aep_30.json` для `aep_3`) MUST NOT попадать в набор. При промахе по хешу и заданном `SCAN_EMULATOR_DEFAULT=` с существующим набором эмулятор SHALL отдать набор ``; лог SHALL сообщать, что пара найдена по умолчанию. #### Scenario: Точное имя - **WHEN** входящий файл называется `.pdf`, а в фикстурах есть `aep_5.pdf` и `aep_5.json` - **THEN** имя входящего файла не используется для поиска: пара определяется только по содержимому (сценарий «Пара по содержимому») #### Scenario: Пара по содержимому - **WHEN** входящий файл байт в байт равен `aep_5.pdf`, в фикстурах есть `aep_5.json` - **THEN** в `Output//` один файл `.json`, равный `aep_5.json` с заменённым `file_uid` #### Scenario: Суффикс - **WHEN** входящий файл равен `aep_8.pdf`, в фикстурах есть `aep_8_form4.json` - **THEN** в `Output//` файл `_form4.json`, равный `aep_8_form4.json` с заменённым `file_uid` #### Scenario: Несколько файлов на одно задание - **WHEN** входящий файл равен `aep_3.pdf`, в фикстурах есть `aep_3.json` и `aep_3_ofr.json` - **THEN** в `Output//` два файла: `.json` и `_ofr.json`, в обоих `file_uid = ` #### Scenario: Чужой префикс - **WHEN** входящий файл равен `aep_3.pdf`, в фикстурах есть также `aep_30.json` - **THEN** `aep_30.json` в набор не попадает #### Scenario: Промах с набором по умолчанию - **WHEN** `SCAN_EMULATOR_DEFAULT=aep_6`, входящий файл — произвольный PDF, которого нет в фикстурах - **THEN** в `Output//` набор `aep_6` (`.json` из `aep_6.json`), в логе отметка «по умолчанию» #### Scenario: Имя входящего файла не влияет - **WHEN** входящий файл называется `.pdf`, а его содержимое равно `aep_6.pdf` - **THEN** отдан набор `aep_6` ### Requirement: Неизвестный файл — ошибка в формате Content AI Если пара по содержимому не найдена и `SCAN_EMULATOR_DEFAULT` не задан (или его набор отсутствует), эмулятор SHALL записать один ответ `.json` в формате реальных ответов-ошибок ИС (`aep_5.json`, `aep_8_form4.json`): `file_uid = `, `error = {"code": "INVALID_DOCUMENT", "message": ": нет пары по содержимому в фикстурах"}`, `data_ocr` — **объект** с пустыми полями (`label`, `organization`, `inn`, `units`, `date` — `{"form_name": "", "confidence": 0}`) и `table_structure: []`. #### Scenario: Нет пары - **WHEN** входящий файл — произвольный PDF, `SCAN_EMULATOR_DEFAULT` не задан - **THEN** ответ содержит `error.code = INVALID_DOCUMENT`, `data_ocr` — объект с пустыми полями и `table_structure: []`; `scan-gateway` публикует конверт с тем же кодом ### Requirement: Эмулятор запускается из того же образа В `docs/dev/docker-compose.yml` сервис `scan-emulator` SHALL использовать образ `scan-gateway` с командой `python -m src.scan_emulator`, общий том `hotfolder` и `../../fixtures/ocr`, смонтированный только для чтения. #### Scenario: Compose - **WHEN** выполнен `docker compose --profile services up -d` - **THEN** контейнер `scan-emulator` работает, его лог сообщает каталоги и число найденных пар фикстур