Перейти до змісту

Прохідна та вагова (Gatehouse)

Layer: Horizontal plugin поверх Essentials. Code: gatehouse (Django app), appGatehouse (License + sidebar pillar). Status: Усі 5 sprints shipped 2026-04-28 — universal core + hardware bridge + vehicle gate scenario + weighing correlator + personnel turnstile + FortNet integration + ContainerHub bridge. Source of truth: docs/planning/gatehouse-plugin.md (5 спринтів, ADR-рішення §0). Hardware ADR — docs/eswf/architecture/hardware-integration.md.

Інші документи цієї теки: - testing-guide.md — детальна QA/dev інструкція по перевірці кожного спринта (DoD checklists + troubleshooting). - user-guide.md — інструкція оператора (без термінів JWT/WS/migrate).

Призначення

Універсальна точка контролю на межі підприємства, що масштабується на всі вертикалі (ContainerHub, Logistic, Fleet, GrainElevator). Замість трьох незалежних модулів — один плагін з спільною моделлю події, класифікацією прохідних і шаром equipment-драйверів. Доменні модулі (HRM, ContainerHub, Essentials) споживають ці події через адаптери.

Три типові сценарії:

Кейс Прохідна Документ
Турнікет з картою personnel_turnstile hrm.AttendanceLog (Sprint 5)
Прохідна авто з ANPR vehicle_gate containerhub.GateTransaction / logistic.ContainerVisit (Sprint 5)
Промислова вагова weighing_station essentials.GoodsReceipt через WeighingTicket (Sprint 4)

Моделі (Sprint 1)

Модель Базовий клас Призначення
GatePoint MasterDataModel Фізична точка контролю + purpose (turnstile / vehicle / weighing / mixed).
GateEquipment MasterDataModel Обладнання на точці (ваги, ANPR, картрідер, etc.) — у Sprint 1 лише config-рядок, у Sprint 2 — реальний driver через device-bridge.
GateEvent TransactionModel Immutable подія перетину. У Sprint 1 створюється оператором руками.
GateCheckpoint TenantAwareModel Проміжкові чекпойнти території між гейт-в'їздом і виїздом. Sprint 3 будує timeline.
WeighingTicket TransactionModel Бізнес-документ зважування (брутто + тара) з власним state machine. У Sprint 1 — ручний ввід; Sprint 4 додає ANPR+scale correlator.

GateEventWeighingTicket — той самий патерн, що PaymentInvoice у бухгалтерії: подія immutable, документ має lifecycle.

Endpoints (REST)

/api/v1/gatehouse/gate-points/
/api/v1/gatehouse/gate-equipment/
/api/v1/gatehouse/gate-events/
/api/v1/gatehouse/gate-events/{id}/register/
/api/v1/gatehouse/gate-events/{id}/reject/
/api/v1/gatehouse/gate-checkpoints/
/api/v1/gatehouse/weighing-tickets/
/api/v1/gatehouse/weighing-tickets/{id}/record-weight-in/
/api/v1/gatehouse/weighing-tickets/{id}/record-weight-out/
/api/v1/gatehouse/weighing-tickets/{id}/close/
/api/v1/gatehouse/weighing-tickets/{id}/reopen/

Service layer

backend/gatehouse/services.py:

  • record_weight_in(ticket, weight, when, user) — фіксує брутто, переводить ticket у weighed_in.
  • record_weight_out(ticket, weight, when, user) — фіксує тару, переводить у weighed_out.
  • close_weighing_ticket(ticket, user) — закриває (вимагає обидва weighings, валідує, що для in_loaded_out_empty брутто > тара).
  • reopen_weighing_ticket(ticket, user) — відкат із closed назад у weighed_out / weighed_in.
  • register_gate_event(event, user)draftregistered.
  • reject_gate_event(event, reason, user) — будь-який статус → rejected з нотаткою у note.

Frontend

Section composition тримається канонічного порядку груп Master Data → Documents → Reports → Processes (live-dashboards живуть у Processes, не в Reports). Sprint 1 пропускає групу Reports — вона з'являється у S3 (vehicle trace report) і S4-S5 (weighing statistics, shift attendance).

Маршрут Компонент
/gatehouse SectionDashboardPage (стандарт)
/gatehouse/gatehousemasterdata/gatePoints + gateEquipment стандартний MasterDataPage
/gatehouse/gatehousetransactiondata/weighingTickets WeighingTicketForm — кастомна форма з кнопками Брутто / Тара / Закрити / Перевідкрити
/gatehouse/gatehousetransactiondata/gateEvents стандартний TransactionList для gateEvents
/gatehouse/gatehousetransactiondata/gateCheckpoints стандартний MasterDataPage (Sprint 3 додає timeline-view)
/gatehouse/gatehouseprocesses/gatehouseDashboard GatehouseDashboard — operator overview (Sprint 2 додасть live WebSocket)

Sidebar: під Essentials → Plugins → Прохідна та вагова (бо forModules: ["essentials"], appType: "plugin").

Seed

python manage.py seed_gatehouse [--clear] (інкрементальний):

  • 4 GatePoint (gate_main / gate_personnel / gate_weighbridge / checkpoint_yard_1)
  • 5 Vehicle (top-up — фактично fleet master data)
  • 5 Driver + 5 Employee
  • 10 історичних GateEvent за минулий тиждень (mix personnel / vehicle / anonymous)
  • 2 закритих WeighingTicket з реальним net (13.4 / 13.65 т)

Sprint 2 розширить seed equipment-драйверами; Sprint 3 — vehicle gate events з QR-наклейками; Sprint 4 — weighing tickets з ANPR+scale correlator; Sprint 5 — FortNet-CSV-emulator + AttendanceLog історія.

Roadmap (5 послідовних спринтів)

Sprint Зона Статус
S1 Universal core, manual input, seed v1 ✅ shipped 2026-04-28
S2 Hardware bridge layer, EasyOCR ANPR через webcam, 4 драйвери + 3 emulator-CLI, WebSocket live-feed ✅ shipped 2026-04-28
S3 Vehicle gate scenario — auto-Vehicle, qr_label_uid, PDF gate-pass, mobile QR scanner, vehicle trace timeline ✅ shipped 2026-04-28
S4 Weighing scenario — ANPR+scale correlator у вікні 30с, автоматичні WeighingTicket, bridge у GoodsReceipt одним кліком ✅ shipped 2026-04-28
S5 Personnel turnstile + FortNet CSV reference integration + cross-module bridges ✅ shipped 2026-04-28

Sprint 2 deliverables (2026-04-28)

Backend

  • POST /api/v1/gatehouse/ocr/license-plate/ — EasyOCR lazy-singleton, UA regex post-process, повертає {plate, plate_display, confidence, bbox, all_candidates}. 503 fallback якщо easyocr не встановлено.
  • GET /api/v1/gatehouse/equipment-drivers/ — каталог 6 драйверів з config-schema.
  • POST /api/v1/gatehouse/equipment-events/ — REST ingest від device-bridge / webcam-tester. Tenant ownership check, broadcast у WS, опційне auto-створення GateEvent, оновлення GateEquipment.last_seen_at.
  • WebSocket /ws/gatehouse/{tenant_id}/[gate_point_id]/?token=<JWT> — multi-tenant ізоляція (4 close codes: 4001 auth, 4002 no tenant, 4003 mismatch, 4004 gate-point not in tenant). Patten за GPSConsumer.
  • gatehouse/broadcaster.py — sync helpers push_equipment_event / push_gate_event_created / push_weighing_ticket_updated.

Device Bridge (device-bridge/)

Окремий Python-сервіс, що працює на ПК оператора. Драйвер-as-plugin registry, asyncio-таски, SQLite outbox для backpressure.

Driver code Kind Транспорт
file_scale_generic scale watch файла
serial_scale_ascii scale pyserial-asyncio + regex
serial_scale_cas_ci scale pyserial-asyncio (CAS CI протокол)
card_reader_emulator card_reader watch файла

ANPR драйвери (anpr_emulator_http, anpr_webcam_browser) живуть поза device-bridge — перший — CLI emulator, другий — frontend-driven через WebcamANPRTester.

3 emulator-CLI: scale_emulator.py (file pattern, in_loaded_out_empty cycle), card_emulator.py, anpr_cli.py.

Frontend

  • WebcamANPRTester.tsxgetUserMedia + frame capture + canvas overlay з bbox + manual/auto modes.
  • useGatehouseSocket.ts — auto-reconnect WS hook з ring-buffer 50 events.
  • GatehouseDashboard.tsx — додано WS-індикатор + live-equipment секцію (12 останніх подій).
  • Sidebar: Processes → Operator → Webcam ANPR Tester.

Seed v2

  • 7 GateEquipment (1-3 на кожну прохідну) з прив'язаними driver-codes і connection_config готовою для device-bridge.toml.

Sprint 3 deliverables (2026-04-28)

Backend

  • services.register_vehicle_gate_event(plate, direction, ...) — ANPR plate → lookup fleet.Vehicle by license_plate (case-insensitive, dash/space-stripped) → auto-create draft Vehicle if missing → open new GateEvent(subject_type=vehicle, qr_label_uid=...) for direction=in, OR close the matching open visit for direction=out. Defensive needs_review if exit ANPR has no matching entry.
  • services.accept_gate_event(event) — operator one-click promotion draft → registered. Issues qr_label_uid if missing.
  • services.record_checkpoint(qr_label_uid, gate_point) — yard QR-scan handler. Looks up the parent visit by UID, creates GateCheckpoint. 30-second idempotency window — re-scanning the same QR at the same post within 30s returns the existing checkpoint instead of duplicating.
  • Endpoints:
  • POST /api/v1/gatehouse/gate-events/{id}/accept/ — operator accept (auto-issues QR + WS broadcast).
  • POST /api/v1/gatehouse/checkpoints/scan/{qr_label_uid, gate_point_id} → 200 with checkpoint payload + WS broadcast; 404 if QR unknown.
  • equipment_event_ingest extended: ANPR kind=anpr_camera now goes through register_vehicle_gate_event (not raw GateEvent.create) — auto-Vehicle, QR generation, direction-aware open/close.
  • PrintContract vehicle-gate-pass (backend/gatehouse/print/contracts.py) — A6 landscape PDF with plate, vehicle metadata, gate point, entry timestamp, and an inline-SVG QR encoding qr_label_uid. Template in backend/gatehouse/print_templates/vehicle-gate-pass/base.html. Frontend mapping vehicleGatePass → vehicle-gate-pass added to lib/printPdf.ts.

Frontend

  • GateOperator.tsx — operator screen at a vehicle gate. Subscribes to per-gate-point WS. Cards for each pending draft event: [Accept + print] / [Reject]. Accept → POST /accept/ → auto-open PDF in new tab. Manual override field for cases when ANPR misses (POST to /equipment-events/ with driver=manual_operator_input). «Currently on territory» list with reprint + trace shortcuts.
  • CheckpointScanner.tsx — mobile-first QR scanner. Uses html5-qrcode (~50KB) for the rear camera. Decoded UID → POST /checkpoints/scan/. 5-second per-UID dedup на клієнті щоб не флудити при decode'і кадру кадрі. Recent-scans sidebar з trace-shortcuts.
  • VehicleTrace.tsx — Mantine Timeline одного візиту: gate-in → checkpoints (chronological) → gate-out. Time deltas between consecutive points. Open-visit (no exit yet) показаний як НА ТЕРИТОРІЇ бейдж з біжучим total.
  • Sidebar (gatehouse.ts): нова subgroup Yard posts під Processes з checkpointScanner + vehicleTrace. Нова item gateOperator приєднується до Operator subgroup.

Seed v3

  • 5 closed історичних vehicle visits за минулий тиждень (кожен з qr_label_uid, 3-4 GateCheckpoint 15-45 хв apart, парний exit event, ANPR-photo evidence stub).
  • 1 currently-open visit (авто заїхало ~1h тому, 1 yard checkpoint, без exit) — для дашборда «на території».

User testing flow (DoD)

  1. seed_gatehouse v3 створює 5 closed visits + 1 open.
  2. Open /gatehouse/gatehouseprocesses/gateOperator — обрати gate_main. WS-онлайн.
  3. Webcam → photo of plate АА1234ВВ (з seed) — WebcamANPRTester пушить ANPR-event → у Operator screen картка з [Прийняти + друк] / [Відхилити].
  4. Click Прийняти + друк → PDF відкривається у новій вкладці (A6 landscape з QR).
  5. На іншій вкладці (як планшет посту) → /gatehouse/gatehouseprocesses/checkpointScanner/checkpoint_yard_1 → старт сканера → камера на надрукованому QR (PDF на смартфоні) → toast «Чекпойнт записано».
  6. На третій вкладці → /gatehouse/gatehouseprocesses/vehicleTrace/<event_id> → Timeline: Заїзд → Чекпойнт → ....
  7. Exit ANPR (через webcam-tester з direction=out) — Timeline закривається Виїзд, статус → ЗАВЕРШЕНО.

Sprint 4 deliverables (2026-04-28)

Backend

  • Event correlator (services.correlate_weighing_event). Stateful — per-process in-memory ring buffer ({(tenant_id, gate_point_id) → deque}) з 30-сек expiry. Кожен ANPR + scale event на weighing_station / mixed gate point проходить через корелятор:
  • якщо у буфері є counterpart-event у вікні 30с → пара утворює WeighingTicket (open / close залежно від наявності ticket'а для цього vehicle у останніх 24h);
  • якщо ні → event буферизується, чекаємо counterpart;
  • при матчі обидва events видаляються з буфера → одна пара = один тикет, без подвійного створення.
  • Open / close decision tree (_open_or_close_ticket):
  • 1-ше зважування → новий WeighingTicket(weight_in, status=weighed_in);
  • 2-ге зважування → знайти open ticket для того ж vehicle на тому ж gate point у останніх 24h → закрити weight_out, обчислити direction_pattern (weight_in > weight_out → приймання);
  • конфлікт (нульова вага, vehicle mismatch) → status='needs_review'.
  • services.create_goods_receipt_from_weighing(ticket) — bridge у essentials.GoodsReceipt. Створює GR з одним GoodsReceiptLine(item=cargo_item, quantity=net_weight, unit=kg). Якщо Item.base_unit у тонах → конверсія kg → t. Idempotent: повторний виклик повертає існуючий GR замість дубля. Налаштовує ticket.goods_receipt = gr, ticket.status = 'posted'.
  • Endpoint: POST /api/v1/gatehouse/weighing-tickets/{id}/create-goods-receipt/ — повертає {goods_receipt_id, goods_receipt_number, ticket} + WS broadcast weighing_ticket_updated.
  • equipment_event_ingest extended: для gate_point.purpose in (weighing_station, mixed) усі ANPR + scale events ідуть через correlator перш. Тільки коли correlator не утворив пари (буферизується) — fallback до Sprint 3 register_vehicle_gate_event (для ANPR на mixed-gate'і).

Frontend

  • WeighingTicketForm.tsx enhancement:
  • Mantine Stepper (Чернетка → Брутто → Тара → Закрито → Прихід) — кольори: blue нормально, orange для needs_review, teal для posted. Описи кроків наповнені фактичними часами.
  • Кнопка Створити прихід (GoodsReceipt) (teal) — активна якщо status=closed AND direction_pattern=in_loaded_out_empty AND !goods_receipt AND cargo_item AND partner. Tooltip пояснює чому disabled.
  • Якщо GR уже створений — замість кнопки показуємо Alert(teal) з посиланням на GR.
  • needs_review-статус показує Alert(orange) з ticket.note.
  • WeighbridgeOperator.tsx — новий screen /gatehouse/gatehouseprocesses/weighbridgeOperator. Картки live-зчитувань (last scale + last ANPR), список відкритих талонів (status weighed_in/weighed_out/needs_review) з підсвіткою needs_review, окремий блок «Готові до приходу» з teal-net-weight.
  • Sidebar (gatehouse.ts): додано пункт weighbridgeOperator у Processes → Operator.

Seed v4

  • 3 closed WeighingTicket (event_in + event_out wired до scale GateEvents на gate_weighbridge); 2 з них уже мають FK у GoodsReceipt через create_goods_receipt_from_weighing під час seed'у.
  • 1 open ticket у статусі weighed_in (авто на території ~35 хв, тара ще не зафіксована).
  • 1 ticket у needs_review — симуляція correlator-конфлікту з note='Suspicious weight 0 kg ...'.

User testing flow (DoD Sprint 4)

  1. Backend + frontend dev + bridge запущені, scale-emulator на C:/weights/weighbridge.txt (вагова), ANPR через webcam.
  2. Відкрив /gatehouse/gatehouseprocesses/weighbridgeOperator → обрав gate_weighbridge.
  3. Запустив scale-emulator з --value 25000 → у Live readings бачиш 25000 kg.
  4. У webcam-tester (інша вкладка) → фото номера AA1234BB → ANPR пушить event на gate_weighbridge.
  5. Correlator зв'язує ANPR + scale у вікні 30с → автоматично відкривається WeighingTicket(weight_in=25000) → з'являється у списку «Відкриті талони».
  6. Перемкнув scale-emulator на --value 12000 (порожнє авто) → знову webcam того ж номера → correlator знаходить open ticket, пише weight_out=12000, статус → closed, net=13000 kg.
  7. Кліцнув на ticket → відкрилася форма з Stepper'ом на Закрито + кнопка Створити прихід.
  8. Кнопка Створити прихід → новий GoodsReceipt відкривається у новій вкладці з рядком 13 т зерна, FK weighing_ticket назад.

End-to-end від webcam-фото зі смартфона до проведеного GR без жодного ручного вводу ваги.

Sprint 5 deliverables (2026-04-28)

Backend

  • HRM AttendanceLog model (backend/hrm/models/attendance.py) — daily roll-up з (in_at, out_at), source ∈ {gatehouse, fortnet, manual}, is_open flag для «currently in the building» індикатора. Constraint UniqueConstraint(tenant, employee, date). Endpoint attendance-logs/ через стандартний TenantFilterMixin viewset.
  • HRM Employee SCUD fields: external_scud_id, card_uid, access_level (1-4). Indexed для швидкого matching на pull.
  • hrm.services.attendance.upsert_attendance_log_from_gate_event — приймає GateEvent з subject_type=employee, відповідно оновлює in_at (earliest) / out_at (latest). Source — derived з evidence (fortnet якщо {source: 'fortnet'} payload є у списку, inakshe gatehouse).
  • hrm.services.attendance.recompute_days_worked(period, employee=None) — re-derives PayrollSlip.days_worked. MVP правило: будь-яка пара (in, out) на дату = 1 робочий день. Тонкощі (нічні, переривчасті) — Deferred. Endpoint action /payroll-slips/{id}/recompute-days-worked/.
  • GateEvent.source + external_event_id + UniqueConstraint(tenant, source, external_event_id) — idempotency для replays зовнішнього SCUD pull-у.
  • gatehouse.services.register_personnel_gate_event — Employee resolution by external_scud_idcard_uid fallback → silent drop якщо нічого не знайшлось (FortNet seed з невідомими employee'ами не падає halt'ом). Forwards у HRM adapter.
  • gatehouse.services.bridge_to_containerhub_if_installed — якщо ANPR-event несе container_number і ContainerHub plugin встановлено → створює containerhub.GateTransaction(container, vehicle, transaction_time, transaction_type, evidence). Defensive try/except: bridge non-fatal — Gatehouse продовжує працювати незалежно від ContainerHub schema mismatch.
  • POST /api/v1/gatehouse/external-events/ endpoint — універсальний для зовнішніх СКД pull-сервісів. Payload {source, external_event_id, employee_external_id або card_uid, gate_external_id або gate_point_id, event_type IN/OUT, event_time, raw_data}. Idempotent через register_personnel_gate_event.
  • integrations/fortnet_scud/ Django app з FortnetIntegrationSettings (singleton per tenant: csv_path/db_dsn, last_synced_event_id cursor, pull_interval_sec, gate_mapping JSON), FortnetSyncLog (per-pull audit), pull_events_from_fortnet service (CSV reader → register_personnel_gate_event per row), fortnet_pull management command для cron, REST endpoints /integrations/fortnet/sync/ + /fortnet/status/.
  • Seed CSV: backend/integrations/fortnet_scud/seed_data/access_log_2026_04.csv — 50 рядків (5 employees × 5 days × 2 events per day = 25 IN + 25 OUT, з різними access_level).
  • equipment_event_ingest extended: ANPR з container_number у payload → після register_vehicle_gate_event викликається bridge_to_containerhub_if_installed.

Frontend

  • FortnetIntegrationPage.tsx (frontend/erp/src/components/Gatehouse/FortnetIntegrationPage.tsx) — admin сторінка: налаштування CSV path / pull interval / enabled, поточний cursor, останній sync log, manual «Sync now» кнопка, історія pull-ів з status badges + counts (+/replay/fail).
  • gatehouse.ts: новий subgroup Integrations під Processes з fortnetIntegration пунктом (gated by plugin: "fortnet_scud").
  • applications.ts + seed_shop: appFortnetSCUD як appType: "integration", requires: ['appEssentials', 'appGatehouse', 'appManager'], forModules: ['essentials'], installed_by_default=True.

Seed v5

  • Top-up SCUD-полів на 5 seeded employees (EMP001–EMP005, AABBCC01-05, access_level 1-4).
  • FortnetIntegrationSettings(csv_path=<seed CSV>, pull_interval_sec=60, is_enabled=True, gate_mapping={'GATE-MAIN': 'gate_personnel'}).
  • gate_mapping мапує FortNet's GATE-MAIN на DOP GatePoint.code='gate_personnel' (бо у нашій схемі gate_main — mixed, а turnstile-only — це gate_personnel).
  • Реальна історія AttendanceLog будується через перший python manage.py fortnet_pull --tenant <id> після seed-у — це за патерном "seed без дублювання даних, які може створити сервіс".

User testing flow (DoD Sprint 5)

  1. Backend + frontend запущені, seed v5 виконаний.
  2. Власний канал: запусти card-emulator через лаунчер з --card AABBCC01 → bridge зловить → card_reader_emulator driver → POST /equipment-events/ → AttendanceLog для EMP001 з source='gatehouse'.
  3. FortNet канал: відкрий /gatehouse/gatehouseprocesses/fortnetIntegration → бачиш «cursor: пусто, last sync: ніколи». Клацни «Sync now» → ~50 GateEvent створено → 25 AttendanceLog з source='fortnet' → видно у /hrm/operations/attendanceLogs/.
  4. Інкрементальний pull: додай вручну рядок у CSV (event_id=1051,EMP001,...,IN,2026-04-26 09:00:00) → клацни «Sync now» → з'явиться 1 event (replays=49, created=1, failed=0) — cursor рухається уперед.
  5. Recompute days_worked: на PayrollSlip для EMP001 за квітень → action recompute-days-workeddays_worked=5 (5 робочих днів з парами IN/OUT у CSV).
  6. ContainerHub bridge: на ContainerHub-tenant'і (де є Containers + GateTransaction model) → ANPR з payload={plate, container_number} → у ContainerHub з'являється GateTransaction з тим же vehicle + container + evidence.

Усі 5 спринтів послідовно дають повний демо всіх сценаріїв через webcam, emulator'и і CSV-FortNet без жодного фізичного hardware.

User testing flow (DoD)

  1. python manage.py seed_gatehouse створює 4 GatePoint + 7 Equipment.
  2. cd device-bridge && python -m venv venv && pip install -r requirements.txt.
  3. Заповнити device-bridge.toml з JWT bridge-користувача + gate_point_id=<gate_main id>.
  4. python device-bridge/main.py — bridge запускається з потрібними драйверами.
  5. python device-bridge/emulators/scale_emulator.py --file C:/weights/current.txt --pattern in_loaded_out_empty — генерує ваги.
  6. У браузері /gatehouse/gatehouseprocesses/gatehouseDashboardWS онлайн, у Live-feed з'являються події (вага кожні 0.5с).
  7. /gatehouse/gatehouseprocesses/webcamAnprTester — дозволити webcam, показати фото номера зі смартфона → bbox з UA-номером + confidence → «Підтвердити» → подія у Live-feed → автоматичний GateEvent у списку.

Status

MVP shipped (Sprint 1, 2026-04-28). Production-ready тільки після Sprint 2+ (без обладнання — лише ручний flow).

🔮 Deferred / Ideas

Tech debt і розширення після MVP (всі 5 sprints + post-MVP polish 2026-04-30 — 2026-05-07). Закриті пункти переїхали в done.md. Тригер реактивації нижче — або реальний customer-flow, або суміжний модуль, що залежить від цього debt'у.

P1 — production / horizontal-scaling blockers

  • [ ] Лабораторія якості (QualityInspection). QC-крок плану візиту тригериться через gate_point.role.step_kind_system_code='qc' + record_checkpoint (див. business-rules.md § QC checkpoint mechanism). Substitute доти, доки немає реального документа з параметрами огляду / fail-pass / прив'язкою лабораторного протоколу. Поле VisitPlanStep.quality_inspection_id (loose-FK PositiveBigIntegerField) уже зарезервовано. Локація: essentials_quality plugin (вже існує — додати документ) або новий core документ. Trigger: реальний customer з вимогами до лабораторії якості.
  • [ ] ANPR + scale correlator: Redis-backing для multi-worker. Сьогодні services._recent: dict[(tenant_id, gate_point_id), deque] — in-memory per-process. Один Daphne worker — OK. Горизонтальне масштабування ламається: подія летить у worker А, парна — у worker B, кореляція не відбувається. Public API (correlate_event(payload) → ticket | None) лишається той самий. Див. hardware-integration.md § 7. Trigger: реальний прокід > 1 RPS на гейт або deploy multi-worker.
  • [ ] VisitPlan materialization edge case — entry без template. Якщо pick_visit_template повернув None (профіль авто не відповідає жодному applies_to), візит реєструється, але без плану → exit-block check не спрацьовує, і force_exit не потрібен. Fallback: за-tenant налаштовуваний «default minimal plan» (тільки entry → exit) щоб шар exit-control завжди працював. Локація: services.py:1594.

P2 — UX / operational ergonomics

  • [ ] EasyOCR pre-warm на startup. /api/v1/gatehouse/ocr/license-plate/ блокує перший запит ~1–2 хв (download model + warmup). Користувач натиснув «Розпізнати» на WebcamANPRTester і думає що зависло. Варіанти: (а) async warmup при старті Daphne; (б) окремий celery / RQ worker з ready-стейтом + 503 поки не готово; (в) explicit endpoint POST /ocr/warmup/ що запускається cron-ом раз на день. Локація: backend/gatehouse/ocr.py.
  • [ ] GateOperator settings drawer. TODO-заглушка показує «Coming soon» notification: frontend/erp/src/components/Gatehouse/GateOperator.tsx. Має бути drawer з: auto-accept whitelist (vehicle-profiles бо template з auto_accept_event=True), exit-without-tare policy (force-exit-без-зважування для VIP), equipment binding mapping (live видно scale/ANPR last_seen_at).
  • [ ] GateEquipment.connection_config schema-validation. Зараз JSON Textarea без runtime-валідації → invalid configs зберігаються → backend driver падає в runtime, error невиразний. Drivers вже виставляють required поля у gatehouse/driver_registry.py. Зробити explicit validator на serializer-рівні: validate_connection_config(value, driver) шукає схему driver'а, перевіряє required + types.
  • [ ] FortNet integration — повний UI для settings. csv_path / pull_interval_sec / is_enabled уже редаговані; не реалізовано: db_dsn (production-режим), gate_mapping (zip externalGateID → DOP GatePoint). Без них production-перемикач = manage.py shell для tenant_admin. Локація: frontend/erp/src/components/Gatehouse/FortnetIntegrationPage.tsx.
  • [ ] Status marked ≠ delete. Документи Gatehouse мають universal state ∈ {draft, posted, marked}, але UI не показує marked-стан окремо — він тільки фільтрується з default списку. На GateEvent / WeighingTicket / QueueTicket / VisitorPass треба чіткий індикатор «помічено для видалення» + права на unmark.

P3 — codebase hygiene / docs-debt

  • [ ] QueueKiosk + WebcamANPRTester — статус не ясний. QueueKiosk уже не в config/gatehouse.ts (фактично прихований), але файл живе. WebcamANPRTester — dev-tool без production-use-case. Вирішити: документувати як kiosk-only public або deprecated → видалити. Якщо лишаємо — додати в README.md як reference dev-screen.
  • [ ] GateCheckpoint itemType=MASTERDATA — misleading. В gatehouse.ts записаний як MASTERDATA, але це read-only journal (immutable child of GateEvent). Існуючий JOURNAL/LEDGER itemType не повністю підходить. Варіанти: (а) новий itemType JOURNAL_READONLY; (б) explicit prop readOnlyJournal: true на MasterData item; (в) лишити як є + явний коментар (showInMenu:false уже мітигує).
  • [ ] Bundle docs не дотягують до essentials/invoices/ reference. У docs/ai/domains/gatehouse/ є README.md, entities.md, business-rules.md. Відсутні: api-contract.md (OpenAPI snippets), test-scenarios.md (Given/When/Then), ui-spec.md (стек-нейтральний UX опис). Заповнити коли почнеться POC-порт Gatehouse в інший стек, або як планове доповнення.