Files
han-app/VM1_app/documentation/module-06-bitrix-local-app.md

646 lines
31 KiB
Markdown
Raw Permalink 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.
# module-06. Проектная спецификация `bitrix-local-app`
> Статус: целевая production-спецификация MVP.
> Портал: `han0107.bitrix24.ru`; connector: `han_mobile_app`; Open Line: `8`.
> Источники: [`README.md`](README.md), [`arch-00-glossary.md`](../../architectory/arch-00-glossary.md), [`arch-01-system-architecture.md`](../../architectory/arch-01-system-architecture.md), [`arch-02-api-contracts.md`](../../architectory/arch-02-api-contracts.md), [`arch-03-docker-compose-blueprint.md`](../../architectory/arch-03-docker-compose-blueprint.md), [`arch-04-settings-and-content.md`](../../architectory/arch-04-settings-and-content.md), [`arch-05-agent-development-process.md`](../../architectory/arch-05-agent-development-process.md), [`arch-06-service-hosting-security.md`](../../architectory/arch-06-service-hosting-security.md), [`module-01-api-backend.md`](module-01-api-backend.md).
## 1. Назначение и приоритет
Сервис является локальным серверным приложением Bitrix24 и адаптером Open Lines. Он изолирует OAuth и протокол `imconnector` от `api-backend`, надёжно доставляет разрешённые сообщения клиента оператору и события оператора обратно в HAN.
При конфликте действуют приоритеты `README.md`. Настоящий документ детализирует существующие контракты, но не меняет их. Любой новый внешний/internal endpoint сначала фиксируется в `arch-02`.
Канонический URL канала:
```text
https://han0107.bitrix24.ru/contact_center/connector/?ID=han_mobile_app&LINE=8
```
## 2. Ответственность и границы
Сервис отвечает за:
- install/lifecycle локального приложения и OAuth Bitrix24;
- шифрованное хранение и безопасное обновление portal tokens;
- `imconnector.register`, `imconnector.activate`, `event.bind`, status/retry setup;
- публичный приём `ONAPP*` и `ONIMCONNECTOR*`;
- tolerant parsing JSON/form/multipart и PHP-style массивов;
- проверку callback, нормализацию, durable inbox, retry и DLQ;
- `dialog_sessions`: `external_chat_id` (= `dialog_id`) ↔ `bitrix_chat_id``session_id`;
- идемпотентный outbound `api-backend``imconnector.send.messages`;
- forward входящих сообщений/файлов и `dialog.closed` в `api-backend`;
- `imconnector.send.status.delivery` только после durable ack API;
- health, telemetry, audit технических переходов.
Сервис не отвечает за:
- JWT/пользовательскую авторизацию, Message Safety и App DB;
- хранение истории HAN, realtime и S3;
- CRM Contact/profile sync — зона `bitrix-sync` на ВМ2, не этого модуля;
- изменение `Dialog.status` в `han_app`;
- публикацию internal API на edge.
## 3. Технологический профиль и структура
- Python 3.12+, FastAPI, Pydantic v2, Uvicorn.
- SQLAlchemy 2 async + `asyncpg`; Alembic.
- Один долгоживущий `httpx.AsyncClient` с bounded pool.
- PostgreSQL managed, только схема `bitrix_local`.
- OpenTelemetry и JSON logging.
```text
bitrix-local-app/
app/
main.py
settings.py
api/{public_bitrix,internal_openlines,health,schemas,errors,auth}.py
application/{install,setup,outbound,inbound,forward,delivery_ack}.py
domain/{entities,enums,policies}.py
infrastructure/
bitrix/{client,oauth,connector,parser,normalizer}.py
db/{models,repositories,uow}.py
crypto/{token_cipher,keyring}.py
resilience/{retry,circuit,rate_limit}.py
observability/{logging,metrics,tracing}.py
workers/{inbox_forward,outbox_delivery,setup_reconcile}.py
alembic/
tests/{unit,integration,contract,e2e}/
openapi.yaml
Dockerfile
docker-compose.yml
```
Router только валидирует/аутентифицирует; use case задаёт транзакцию; Bitrix adapter скрывает внешний payload.
## 4. Публичные endpoint
Сервис предоставляет следующие endpoint. Корневой nginx публикует первые три; health остаются internal по умолчанию и открываются exact-route только при явно выбранной ops/monitoring policy:
| Method | Path | Назначение |
|---|---|---|
| GET/POST | `/bitrix/handler` | probe и callbacks `ONAPP*`/`ONIMCONNECTOR*` |
| GET/POST | `/bitrix/install` | install callback/probe |
| GET | `/bitrix/placement` | минимальный HTML placement |
| GET | `/health/live` | liveness; internal по умолчанию |
| GET | `/health/ready` | readiness; internal по умолчанию |
`GET handler/install` возвращает безопасный `200`, не раскрывая OAuth/setup. POST принимает только bounded body и разрешённые content types. Placement имеет отдельный CSP `frame-ancestors` с точным allow-list Bitrix24.
Internal `/internal/openlines/v1/*` доступны только по Docker/VPC network и **не маршрутизируются nginx наружу**.
## 5. Internal Open Lines API
Все вызовы требуют:
```text
Authorization: Bearer ${BITRIX_INTERNAL_API_TOKEN}
X-Request-ID: UUID/ULID
traceparent: optional W3C
```
Caller `api-backend` передаёт `BITRIX_LOCAL_APP_INTERNAL_TOKEN`, значение которого равно `BITRIX_INTERNAL_API_TOKEN`. Сравнение constant-time.
### 5.1. `POST /internal/openlines/v1/messages`
Одна операция — одно сообщение MVP. `Idempotency-Key` обязателен и равен `message_id`.
```json
{
"message_id": "uuid",
"external_chat_id": "uuid",
"occurred_at": "2026-07-10T09:00:00Z",
"user": {
"id": "uuid",
"display_name": "Новый клиент HAN",
"phone": "+79990000000"
},
"message": {
"content_kind": "text",
"text": "Здравствуйте",
"files": []
}
}
```
Файловый вариант:
```json
{
"message_id": "uuid",
"external_chat_id": "uuid",
"occurred_at": "2026-07-10T09:00:00Z",
"user": {"id": "uuid", "display_name": "Новый клиент HAN", "phone": "+79990000000"},
"message": {
"content_kind": "file",
"text": "",
"files": [{
"attachment_id": "uuid",
"name": "document.pdf",
"mime_type": "application/pdf",
"size_bytes": 12345,
"download_url": "https://short-lived-signed-url"
}]
}
}
```
Правила:
- `external_chat_id` строго UUID и равен App `dialog_id`;
- `text` xor один file; unknown fields запрещены;
- signed URL не сохраняется в обычные логи и редактируется в durable payload по истечении необходимости;
- `display_name``ClientProfile.full_name`, если пусто — `Новый клиент HAN`;
- телефон клиента передаётся в `user.phone` и мапится в `MESSAGES[0][user][phone]` для CRM-лида; email не передаётся;
- fingerprint строится по стабильным полям без signed query;
- тот же key/fingerprint возвращает прежний результат;
- тот же key с иным fingerprint → `409 idempotency_key_reused`.
Успех `200/201`:
```json
{
"status": "delivered",
"message_id": "uuid",
"external_chat_id": "uuid",
"bitrix_message_id": "string-or-null",
"dialog_session": {
"bitrix_chat_id": 1807,
"session_id": "sess-42"
}
}
```
`api-backend` выставляет `delivery_status=delivered` только после этого ответа/duplicate result. `202` не считается финальной доставкой в основном синхронном flow.
### 5.2. `GET /internal/openlines/v1/dialogs/{external_chat_id}`
Возвращает active mapping:
```json
{
"external_chat_id": "uuid",
"bitrix_chat_id": 1807,
"session_id": "sess-42",
"status": "open",
"updated_at": "2026-07-10T09:00:00Z"
}
```
`404` — mapping отсутствует/soft-deleted. Пользовательская PII не возвращается.
### 5.3. `GET /internal/openlines/v1/status`
Возвращает безопасный статус portal OAuth, connector registration/activation, event bindings, worker backlog и circuit state; токены и raw Bitrix response исключены. `200` может иметь `status=degraded`; `503` — нет usable OAuth/БД.
### 5.4. `POST /internal/openlines/v1/setup/retry`
Идемпотентно запускает reconcile `register → activate line 8 → event.bind`. Одновременно разрешён один run по portal advisory lock/DB lease. Ответ содержит per-step status. Endpoint ops-only с тем же Bearer и дополнительным service rate limit.
## 6. Outbound: HAN → Open Lines
1. Аутентифицировать caller и зарезервировать `outbound_messages` по `message_id`.
2. При completed вернуть сохранённый sanitized result.
3. Собрать `MESSAGES` Bitrix: `user.id/name/phone`, `message.id/date/text/files`, `chat.id`.
4. Вызвать `imconnector.send.messages` с `CONNECTOR=han_mobile_app`, `LINE=8`.
5. Извлечь `CHAT_ID`, session `ID`, Bitrix message id из допускаемых вариантов ответа.
6. В одной транзакции upsert `dialog_sessions`, записать result, status `delivered`.
7. Вернуть ack API.
Ambiguous timeout не разрешает слепой повтор без idempotency/reconciliation. Worker сверяет локальный state/session и повторяет только если метод/Bitrix semantics не создадут дубль; иначе `manual_review`/DLQ. Automatic retry допустим для connect failure до отправки, explicit rate-limit и известных transient ошибок.
## 7. Install, OAuth и setup
### 7.1. Install
POST install/handler с `ONAPPINSTALL`:
1. parse и strict validate `auth`;
2. проверить expected portal domain `han0107.bitrix24.ru`, HTTPS `client_endpoint`, `member_id`;
3. сохранить tokens до внешнего setup;
4. создать `install_runs`;
5. выполнить setup идемпотентно;
6. вернуть `installed` либо `installed_with_errors`; partial setup не теряет OAuth.
`ONAPPUNINSTALL` помечает portal installation `uninstalled`, запрещает outbound и планирует revocation/retention. В отличие от прототипа, usable tokens не остаются active.
### 7.2. Token storage и encryption
- `access_token`, `refresh_token`, `application_token` шифруются application-level envelope encryption (AES-256-GCM или эквивалент AEAD).
- Master key только secret env/mount: `BITRIX_TOKEN_ENCRYPTION_KEY`; в БД — `ciphertext`, `nonce`, `key_version`.
- AAD связывает ciphertext с `member_id`, portal domain и token type.
- Поддерживается keyring current+previous для rolling rotation и re-encryption job.
- Токены никогда не логируются, не экспортируются в metrics/traces и не возвращаются API.
- DB/TLS и backup encryption остаются дополнительными слоями.
### 7.3. Refresh
- refresh заранее, когда `expires_at - now <= skew` (ориентир 60 с);
- single-flight на portal через DB advisory lock/lease;
- POST только на allow-listed `https://oauth.bitrix.info/oauth/token/`;
- refresh token rotation сохраняется атомарно;
- при `expired_token` — максимум один refresh+replay;
- `invalid_grant` переводит installation в `reauth_required`, readiness degraded, outbound fail-closed;
- timeout/retry bounded; secret/client credentials не попадают в exception text.
### 7.4. Connector setup
Используемые методы:
- `imconnector.register`: `ID=han_mobile_app`, name/icon, `{BITRIX_PUBLIC_BASE_URL}/placement`;
- `imconnector.activate`: connector, `LINE=8`, `ACTIVE=1`;
- `event.bind`: `OnImConnectorMessageAdd`, `OnImConnectorDialogStart`, `OnImConnectorDialogFinish`;
- `imconnector.status` для reconcile/readiness;
- `imconnector.send.messages`;
- `imconnector.send.status.delivery`.
Каждый setup step хранит desired/observed state, attempts и safe error. Повтор не создаёт duplicate binding; если API Bitrix не гарантирует это, сначала проверяется status/list binding.
## 8. Webhook parsing и безопасность
Поддерживаются JSON, form-urlencoded, multipart и PHP-style keys/числовые dict. Parser:
- ограничивает body/header/field count, nesting, array/message count и строковые длины;
- NFKC не применяется к opaque ids/tokens;
- не сохраняет неизвестный raw body без redaction;
- принимает только известные events; прочие безопасно `ignored` с metric;
- проверяет connector `han_mobile_app`, line `8`, expected member/domain;
- проверяет `auth.application_token` constant-time против расшифрованного portal token и/или `BITRIX_APPLICATION_TOKEN`;
- не доверяет IP как единственной аутентификации, но nginx edge limit/allow policy дополняет token;
- всегда отвечает достаточно быстро после durable insert, чтобы Bitrix retry не создал storm.
Невалидный security token не маскируется как успешная обработка в telemetry: внешний ответ может быть нейтральным, но audit/metric фиксируют reject. Callback secret и payload не логируются.
## 9. Нормализация и inbox-контракт API
Owned receiver находится в `api-backend`:
```text
POST http://api-backend:8000/internal/openlines/v1/inbox
Authorization: Bearer ${BITRIX_API_FORWARD_TOKEN}
```
Значение равно `BITRIX_API_INBOX_TOKEN` на API.
`message.new`:
```json
{
"event_id": "stable-opaque",
"event_type": "message.new",
"external_chat_id": "uuid",
"bitrix_message_id": "string",
"occurred_at": "2026-07-10T09:00:00Z",
"message": {
"text": "Ответ оператора или пустая строка",
"files": [{
"name": "scan.pdf",
"mime_type": "application/pdf",
"size_bytes": 12345,
"download_url": "https://..."
}]
}
}
```
`dialog.closed`:
```json
{
"event_id": "stable-opaque",
"event_type": "dialog.closed",
"external_chat_id": "uuid",
"bitrix_message_id": null,
"occurred_at": "2026-07-10T09:00:00Z",
"message": null
}
```
`event_id` обязателен согласно допущению module-01 A2; предпочтительно используется Bitrix event/message/session id, иначе versioned SHA-256 стабильных полей. `message.new` дополнительно unique по `(external_chat_id, bitrix_message_id)`.
Пустые text+files отклоняются. URL файла передаётся только API; API защищается от SSRF, скачивает с лимитами и сохраняет в S3-data. Local app не скачивает/не хранит файл.
## 10. Delivery ack входящего события
Критический инвариант:
```text
Bitrix webhook → durable inbox → API 201/duplicate 200/204
→ только затем imconnector.send.status.delivery
```
Ack запрещён при timeout/5xx/неприменённом `404` API. Если API commit успешен, но HTTP response потерян, повтор forward получает duplicate ack, после чего delivery status безопасно отправляется. Ack имеет собственный outbox/retry. Ошибка ack не повторяет application события в API.
## 11. Inbox, outbox, DLQ и backoff
### Inbox
Webhook transaction сохраняет event, normalized payload/fingerprint и initial status. Worker использует `FOR UPDATE SKIP LOCKED`, lease и heartbeat.
States:
```text
received → forwarding → api_acked → ack_pending → completed
↘ retry
received/forwarding/retry → dead_letter
```
### Outbound messages
States: `received | sending | delivered | retry | ambiguous | dead_letter`. Unique `message_id`; payload versioned; signed URLs не должны переживать TTL — при retry API обязан дать актуальный URL по согласованному recovery контракту либо операция уходит в reconciliation.
### Backoff
- exponential full jitter, ориентир 1, 2, 4, 8… max 300 с;
- учитывать `Retry-After` Bitrix/API;
- max attempts и max age — infra env;
- permanent 4xx/schema/auth не повторяются автоматически;
- DLQ содержит safe error code, не token/raw PII;
- replay — ops runbook/CLI с audit, не публичный endpoint MVP.
## 12. PostgreSQL `bitrix_local`
Общие правила: UUID/timestamptz, schema-qualified DDL, soft delete для прикладных records, технические queue rows архивируются/удаляются по retention. Runtime role `bitrix_local_app`; отдельная migration role. Прямого доступа к `han_app` нет.
### 12.1. `portal_installations`
`id`, `member_id` unique, `domain`, `client_endpoint`, encrypted token columns, `expires_at`, `scope`, `key_version`, `install_status`, `setup_status`, `last_refresh_at`, `last_error_code`, common fields.
Indexes: unique active `member_id`; unique active normalized domain. MVP разрешает только один active expected portal.
### 12.2. `connector_setup`
`id`, `portal_id`, `connector_id`, `line_id`, `registered`, `activated`, `bindings_json`, `desired_version`, `observed_at`, `next_retry_at`, `attempt_count`, lease/error fields. Unique `(portal_id, connector_id, line_id)`.
### 12.3. `dialog_sessions`
`id`, `external_chat_id uuid`, `bitrix_chat_id bigint NULL`, `session_id varchar NULL`, `portal_id`, `status open|closed`, common fields.
Indexes:
- unique active `external_chat_id`;
- index `(bitrix_chat_id) WHERE record_status='A'`;
- index `(session_id)`;
- `(status, updated_at)`.
Связь с user_id не нужна: идентичность принадлежит App DB.
### 12.4. `inbox_events`
`id`, `event_id`, `event_type`, `external_chat_id`, `bitrix_message_id`, `payload_fingerprint`, `normalized_json`, `status`, attempts/next/lease, `api_ack_status`, `delivery_ack_status`, safe error, timestamps.
Unique `event_id`; unique partial `(external_chat_id, bitrix_message_id)`; worker index `(status,next_attempt_at)`.
Raw payload хранится только если необходим для forensic, зашифрован/редактирован и с коротким retention; preferred — минимальный normalized payload.
### 12.5. `outbound_messages`
`id`, `message_id uuid unique`, `external_chat_id uuid`, `request_fingerprint`, `payload_json`, `status`, `bitrix_message_id`, `response_json`, attempts/lease/error/timestamps. Index worker `(status,next_attempt_at)`.
### 12.6. `delivery_ack_outbox`
Unique inbox event; status/attempt/next/lease, minimal Bitrix delivery DTO. Не содержит API token.
### 12.7. `install_runs` и `audit_events`
Append-only setup step/results и security/ops actions без tokens/raw payload. BRIN/date indexes при росте.
## 13. Alembic и транзакции
- Никакого `CREATE TABLE IF NOT EXISTS` при startup.
- `alembic upgrade head` — отдельный deploy step.
- Expand/migrate/contract, forward-fix; destructive migration только после backup/согласования.
- Smoke upgrade пустой и предыдущей версии.
- Внешний HTTP не выполняется внутри DB transaction.
- Claim → commit lease → external call → finalize under row lock.
- Setup/refresh используют portal-scoped lock.
## 14. Bitrix rate limits и resilience
- Ограничить concurrency (начально 2 на portal) и локальный token bucket.
- Разделить quotas setup, outbound, ack/status.
- На Bitrix rate-limit учитывать headers/body code и `Retry-After`.
- Circuit breakers отдельно: OAuth endpoint, portal REST, API forward.
- Timeout: connect 3 с, обычный REST/read 1015 с, OAuth 10 с; значения infra env.
- 4xx domain/schema не открывает circuit; 429/transient/timeout учитываются по policy.
- Half-open имеет один probe; retry storms предотвращаются jitter/queue concurrency.
- Один `httpx` pool; TLS verify обязателен; redirects для token/REST запрещены либо allow-listed.
## 15. Health
`GET /health/live`: только процесс/event loop, `200`.
`GET /health/ready` с коротким timeout проверяет:
- PostgreSQL `SELECT 1`, expected Alembic revision;
- usable active portal OAuth либо сообщает `portal_not_installed`;
- connector desired state register+line 8+bindings;
- workers heartbeat/lease;
- backlog age/DLQ thresholds;
- forward URL/token configured;
- circuit state.
DB/schema failure → `503`. До install сервис может быть `200 degraded` или `503 portal_not_installed` согласно ops policy; для production traffic выбран `503`, liveness остаётся 200. Ответ не делает внешних Bitrix calls на каждый probe — использует свежий cached observed state.
## 16. Observability
JSON fields: timestamp, level, `service.name=bitrix-local-app`, module, event, request_id, trace/span id, route, event_type, portal hash/member hash, message/event id hash, attempt, queue age, dependency, status/error code, duration.
Не логируются OAuth/application/service tokens, Authorization, raw callback, message text, phone/email/name, filenames с PII, file/download URL, response body Bitrix.
Metrics:
- HTTP latency/status;
- callback accepted/rejected/duplicate;
- parser variants/errors;
- OAuth refresh success/failure/time-to-expiry;
- connector setup desired/observed;
- outbound success/retry/ambiguous/DLQ;
- inbox depth/oldest age/retry/DLQ;
- API forward and delivery ack;
- Bitrix REST latency/rate-limit/circuit;
- DB pool/lease/readiness.
IDs не metric labels. Trace context передаётся в API; внешний Bitrix call — child span без token/query.
## 17. Security
- TLS boundary — root nginx; internal HTTP только backend network.
- Internal endpoints не edge-routed, Bearer token обязателен.
- Exact host/domain/connector/line allow-list.
- `client_endpoint` из callback валидируется против portal allow-list для защиты SSRF.
- Strict DTO/body limits; parameterized SQL.
- OAuth encryption+key rotation; secrets только env/secret mount.
- Non-root, read-only root fs, tmpfs, dropped capabilities.
- OpenAPI UI off production; committed OpenAPI 3.1 обязателен.
- CORS не нужен; placement не получает secrets.
- Error envelope не раскрывает host/stack/raw dependency response.
- Dependency/image scanning и pinned lock/image.
## 18. Env
Канонические из arch-04:
```text
BITRIX_DATABASE_URL
BITRIX_CLIENT_ID
BITRIX_CLIENT_SECRET
BITRIX_CONNECTOR_ID=han_mobile_app
BITRIX_CONNECTOR_NAME=HAN Mobile App
BITRIX_OPEN_LINE_ID=8
BITRIX_PUBLIC_BASE_URL=https://tohin.ru/bitrix
BITRIX_APPLICATION_TOKEN
BITRIX_INTERNAL_API_TOKEN
BITRIX_API_FORWARD_URL=http://api-backend:8000/internal/openlines/v1/inbox
BITRIX_API_FORWARD_TOKEN
OTEL_EXPORTER_OTLP_ENDPOINT
APP_ENV
LOG_LEVEL
```
Предлагаемые infra env, которые до реализации нужно добавить в arch-04:
```text
BITRIX_TOKEN_ENCRYPTION_KEY
BITRIX_TOKEN_ENCRYPTION_KEY_VERSION
BITRIX_HTTP_TIMEOUT_SEC=15
BITRIX_HTTP_MAX_CONCURRENCY=2
BITRIX_RETRY_MAX_ATTEMPTS=10
BITRIX_RETRY_MAX_DELAY_SEC=300
BITRIX_INBOX_RETENTION_DAYS
BITRIX_DLQ_ALERT_AGE_SEC
```
Business settings здесь не хранятся. Legacy `BITRIX_SYNC_FORWARD_*` удаляются после migration window и не являются каноническими.
## 19. Docker и deployment
- service `bitrix-local-app`, `expose: 8080`, без `ports`;
- networks `backend`,`observability`; root nginx отдельно;
- managed PostgreSQL вне compose, TLS обязательно;
- нет SQLite volume production;
- healthcheck `/health/live`; readiness — orchestration/monitoring;
- migration one-shot job до rollout;
- graceful shutdown: stop claims, finish in-flight до grace, release leases, close pools;
- stateless filesystem.
Прототипные `deploy/nginx/*`, certbot/SSL scripts и отдельный compose-stack не переносятся: сертификат и routing принадлежат корневому nginx/compose.
## 20. Что переиспользуется из прототипа
Концептуально переиспользуются и покрываются новыми тестами:
- tolerant parser JSON/form/multipart и `auth[...]`;
- преобразование PHP-style `MESSAGES` list/dict;
- разделение client/connector/handler/normalizer/session store;
- setup `register → activate → bind`;
- refresh до expiry и один replay `expired_token`;
- extraction session `CHAT_ID`/`ID`;
- `application_token` и Bearer constant-time compare;
- deterministic idempotency event key как основа fingerprint;
- `dialog_sessions` и enrichment;
- отключение docs production;
- различение Open Lines и CRM sync.
Обязательно меняется:
- `/internal/v1/*` → только `/internal/openlines/v1/*`;
- forward envelope → канонический `POST /internal/openlines/v1/inbox`;
- immediate delivery ack до API запрещён;
- single-attempt forward → durable worker/backoff/DLQ;
- plaintext tokens → AEAD encryption/key rotation;
- sync psycopg2/thread lock → async pool/transactions/leases;
- SQLite и DDL-on-start не используются production;
- отдельный nginx/certbot/compose удаляются из production topology;
- `/bitrix-internal/` edge alias не нужен: internal API не публикуется;
- raw payload/error storage/logging минимизируется;
- uninstall деактивирует installation;
- Alembic и OpenAPI 3.1 обязательны.
## 21. Тестовая матрица
### Unit
- parser variants/nesting/limits;
- normalizer message/file/start/finish;
- token encryption/decryption/AAD/rotation;
- fingerprint/idempotency;
- session extraction variants;
- retry classification/backoff/jitter;
- URL/portal validation and redaction.
### Integration
- Alembic empty/upgrade;
- concurrent duplicate webhook;
- outbound same/different fingerprint;
- `SKIP LOCKED`, lease expiry, crash recovery;
- refresh single-flight;
- setup reconcile;
- DB constraints/soft delete;
- no DDL at startup.
### Contract
- all public/internal schemas in committed OpenAPI;
- tokens and paired names with module-01;
- `message.new`/`dialog.closed` inbox;
- API 201/duplicate before delivery ack;
- request-id/trace propagation;
- Bitrix fixture payloads and response variants.
### E2E/failure
- install portal → connector visible on line 8;
- text/file send and mapping;
- operator text/file → API → ack;
- duplicate/reordered callbacks;
- API outage, Bitrix 429/5xx/timeout, OAuth expiry/invalid_grant;
- crash at every checkpoint;
- DLQ/replay;
- circuit half-open;
- logs contain no secrets/PII/URLs.
## 22. Definition of Done
- portal/connector/line fixed and validated;
- public and internal paths exactly match arch-02;
- internal API is unreachable from public edge;
- install/OAuth encryption/refresh/setup reconciliation complete;
- outbound idempotency survives crash/ambiguous response;
- inbox retry/DLQ and ack-after-API invariant proven;
- operator text/files and `dialog.closed` contract-tested;
- `bitrix_local` schema, indexes and Alembic migrations tested;
- rate-limit/circuit/timeout/graceful shutdown implemented;
- health/metrics/traces/JSON logs secure;
- OpenAPI 3.1 committed and parity checked;
- root Compose starts non-root container without published port;
- runbooks: reinstall, key rotation, OAuth failure, setup retry, backlog/DLQ, migration/rollback;
- no SQLite, startup DDL or separate production nginx.
## 23. Решения, допущения и TBD
**Решения:**
- B1: canonical internal prefix только `/internal/openlines/v1`.
- B2: delivery ack только после API commit/duplicate ack.
- B3: OAuth tokens шифруются application-level AEAD.
- B4: durable PostgreSQL inbox/outbox/DLQ; Redis не требуется.
- B5: `external_chat_id=dialog_id`; local app не хранит user profile.
- B6: production только managed PostgreSQL + Alembic.
**Допущения:**
- A1: один active portal `han0107.bitrix24.ru` в MVP.
- A2: Bitrix fixtures позволят стабильно извлечь event/message/session ids; иначе versioned fingerprint.
- A3: API может повторно выдать актуальный signed file URL при delayed outbound recovery; exact handshake требуется в contract test.
**TBD:**
- B-TBD1: точные Bitrix REST quotas/headers и safe retry матрица по официальной документации/portal tests.
- B-TBD2: окончательный outbound DTO user display name и file fields в OpenAPI.
- B-TBD3: exact stable `event_id` для dialog events (согласовать с module-01 TBD-3).
- B-TBD4: retention/RPO/RTO и DLQ replay authorization.
- B-TBD5: encryption key source/rotation runbook до production.
- B-TBD6: точный CSP `frame-ancestors` placement.
- B7: operator files считаются trusted-channel данными MVP; api-backend применяет MIME/size/audit без Message Safety/AV, residual malware risk принят.