# sales_service Step 1 Audit

## Executive Summary

`sales_service` уже содержит широкий MVP data/API layer для продаж: Lead, Deal, CommunicationSession, Message, Call, Transcript, Offer, DealCondition, Counterparty, Document, Invoice, Payment, StageHistory, Escalation, AutomationTask, ChannelSwitch и ExternalLink.

Это пока не зрелый sales workflow engine. После шагов 2-4 уже добавлены MVP tenant boundary, configurable pipeline/stages и публикация sales events в `event_outbox`. Основные оставшиеся ограничения: нет state machine для допустимых переходов, automation tasks не исполняются worker-ом, webhook-и не проверяют подпись провайдера, `sales_notes` существует только как таблица/модель без API.

## Step 4 Delta: Sales Events

После шагов 2-4 состояние изменилось:

- `sales_service` пишет sales domain events в общий `event_outbox` через `SalesEventPublisher`.
- Registry событий находится в `services/sales_service/sales_events.py`; текущая версия событий `1`.
- События создаются в той же SQLAlchemy session/transaction, что и бизнес-действие, до `session.commit()`.
- `tenant_id`, `aggregate_type`, `aggregate_id`, actor metadata и causation/correlation metadata передаются в `payload` event envelope; общий `event_outbox` schema не расширялся.
- Consumer-ы для sales events пока не реализованы: на шаге 4 добавлена только публикация в outbox.

Публикуемые MVP events:

- Lead/Deal: `lead.entered_crm`, `lead.enrichment_completed`, `deal.stage_changed`, `deal.scenario_selected`, `deal.next_action_scheduled`, `deal.closed`, `deal.lost`, `deal.won`.
- Communications: `communication.started`, `message.received`, `message.sent`, `call.received`, `call.completed`, `communication.summary_created`.
- Commercial: `offer.created`, `offer.sent`, `offer.accepted`, `offer.rejected`, `deal.conditions_confirmed`.
- Counterparty/Documents: `counterparty.completed`, `document.created`, `document.sent`, `document.confirmed`, `document.signed`.
- Invoice/Payment: `invoice.created`, `invoice.sent`, `invoice.overdue`, `payment.received`, `invoice.paid`.

Step 4 verification:

- `tests/test_sales_events.py` covers outbox writes for lead entry, stage changes, inbound message/call, call completion, offer/invoice actions, payment, invoice paid, deal won, tenant scope and failed-action rollback behavior.
- Current focused run: `40 passed / 2 xfailed` for `tests/test_sales_events.py`, `tests/test_sales_service.py`, `tests/test_sales_tenant_isolation.py`, `tests/test_sales_pipeline_stages.py`, `tests/test_schema_migrations.py`.

## Test Environment

Фактический baseline:

- `python3` на машине: Python 3.9.6.
- `python3.11`: Python 3.11.15.
- Проектный runbook требует Python 3.10+.
- Зависимости из `requirements.txt` успешно установлены в локальный venv на Python 3.11.
- `pytest` установлен через `requirements.txt`.
- Тестовая DB настроена в `tests/conftest.py`: SQLite в `call-center/.testdata/mvp_cc_test.db`.
- `conftest.py` чистит таблицы перед тестами через SQLAlchemy metadata.

Команда:

```bash
cd call-center
python3.11 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
.venv/bin/python -m pytest tests/test_sales_service.py -q
```

Результат `tests/test_sales_service.py`:

- 6 тестов собрались и запустились.
- 4 passed.
- 2 failed.

Падающие тесты фиксируют рассинхрон API/workspace-контракта:

- `test_sales_internal_telegram_sync_auto_creates_workspace`: `workspace["communications"][0]["channel_provider"]` отсутствует. Сейчас provider лежит в `communication.metadata`, а не на верхнем уровне `SalesCommunicationOut`.
- `test_sales_internal_voice_sync_creates_call_and_transcript`: `workspace["transcripts"]` отсутствует. При этом transcript создается и привязывается к call/communication через `transcript_id`.

Полный вывод сохранен в `sales_service_test_results.txt`.

## API Snapshot

OpenAPI выгружен в `sales_service_openapi.json`.

Фактически найдено 67 service routes без `/docs`, `/openapi.json`, `/redoc`.

По блокам:

- Leads: create/list/get/update/enrich/convert-to-deal.
- Deals: create/list/get/update/workspace/change-stage/select-scenario/schedule-next-action/close/escalate.
- Communications: start text/start voice/list/summary/switch-channel/bind-external.
- Messages: inbound webhook/outbound/list by deal.
- Calls: inbound webhook/outbound/complete/list by deal/attach transcript.
- Offers: create/get/send/accept/reject.
- Conditions: upsert by deal.
- Counterparty: create/patch/get by deal.
- Documents: create/get/send/confirm/sign-status-webhook.
- Invoices: create/get/send/mark-overdue.
- Payments: payment webhook/list by deal/reconcile.
- Internal sync: Telegram and Voice sync into sales workspace.
- Dashboard: sales summary.
- Pipelines/Stages: list/get/create/update/set-default pipelines, list/create/update stages.

Нет отдельных API для:

- `Tenant`.
- `AutomationTask` CRUD/list/execute/retry.
- `DealNote` create/list/update.
- Escalation resolve/reassign.
- Event outbox inspection scoped to sales.

## ER Snapshot

ER-карта из SQLAlchemy metadata сохранена в `sales_service_er_map.md`, машинная схема - в `sales_service_er_schema.json`.

Дополнительно сгенерирована ER-карта фактической SQLite-схемы после `scripts/migrate_core_db.py`: `sales_service_er_db_sqlite.md` и `sales_service_er_db_sqlite.json`.

Ключевые факты:

- Таблица из ТЗ `sales_deal_conditions` отсутствует; текущая реализация использует `sales_conditions`.
- Все sales-связи хранятся строковыми id (`deal_id`, `lead_id`, `customer_id`, `communication_id`, `invoice_id`, etc.).
- Физических foreign keys в sales-таблицах нет.
- Unique есть в основном на surrogate business ids (`lead_id`, `deal_id`, `message_id`, etc.).
- `sales_counterparties.deal_id` unique, то есть один counterparty record на сделку.
- `sales_payments.external_payment_id` индексирован; после tenant hardening добавлен partial unique на `(tenant_id, payment_provider, external_payment_id)` для ненулевых external ids.
- `sales_messages(channel_provider, external_message_id)` индексирован, но не unique.
- `sales_calls(provider, external_call_id)` индексирован, но не unique.

Важное отличие metadata от миграций:

- SQLAlchemy metadata содержит много `index=True` на отдельных колонках.
- Проверка чистой SQLite DB после migration-runner показывает, что миграции создают только явно прописанные индексы. Например, `sales_deals` после миграций имеет 3 индекса, а не все single-column индексы из metadata.
- Для Postgres это означает, что performance-план нужно сверять именно с SQL migrations, а не только с ORM-моделью.

Критичные nullable-связи:

- `sales_deals.lead_id` и `sales_deals.customer_id` nullable: сделка может быть без лида или без клиента.
- `sales_communication_sessions.lead_id/customer_id/transcript_id` nullable: контекст сделки не всегда содержит клиента/транскрипт.
- `sales_invoices.customer_id/basis_document_id` nullable: счет может быть без клиента и без документа-основания.
- `sales_payments.invoice_id` nullable: платеж может быть привязан только к сделке.
- Внешние ids (`external_message_id`, `external_call_id`, `external_payment_id`) nullable и не unique.

Таблицы, которые есть:

- `sales_leads`
- `sales_deals`
- `sales_communication_sessions`
- `sales_messages`
- `sales_calls`
- `sales_transcripts`
- `sales_notes`
- `sales_offers`
- `sales_conditions`
- `sales_counterparties`
- `sales_documents`
- `sales_invoices`
- `sales_payments`
- `sales_stage_history`
- `sales_escalations`
- `sales_automation_tasks`
- `sales_channel_switches`
- `sales_external_links`

## Migrations

Sales-миграции:

- `migrations/sql/0023_sales_sqlite.sql`
- `migrations/sql/0023_sales_postgres.sql`
- `migrations/sql/0024_sales_external_links_sqlite.sql`
- `migrations/sql/0024_sales_external_links_postgres.sql`
- `migrations/sql/0025_tenants_sqlite.sql`
- `migrations/sql/0025_tenants_postgres.sql`
- `migrations/sql/0026_sales_pipelines_sqlite.sql`
- `migrations/sql/0026_sales_pipelines_postgres.sql`

Migration smoke check на чистой SQLite DB прошел:

- `scripts/migrate_core_db.py` применил все SQLite migrations до `0026_sales_pipelines_sqlite.sql`.
- `missing_migration_versions()` вернул пустой список.
- `services.sales_service.app` импортируется при `SCHEMA_MANAGEMENT_MODE=migrations`.

Полный вывод сохранен в `sales_service_migration_check.txt`.

## Architecture Gaps

| Area | Status | Gap |
|---|---|---|
| Tenant | MVP подключен | Есть `Tenant`, tenant settings/integrations и tenant-scoped sales API; полная subscription/billing модель еще не реализована. |
| Stages | MVP подключен | Есть tenant `Pipeline`/`PipelineStage`, default pipeline/stages и real `pip_*`/`pst_*`; нет state machine для transition rules. |
| Events | MVP подключен | Sales domain events пишутся в общий `event_outbox`; consumer-ы и обработчики пока не реализованы. |
| Automation | частично | `sales_automation_tasks` создаются, но worker/dispatcher для pending tasks не найден. |
| Payments | частично | Webhook идемпотентен по external payment id и двигает Invoice/Deal, но нет provider signature verification и сверки суммы платежей с invoice total. |
| Notes | частично | `sales_notes` и `SalesNoteRow` есть, но API/model output/workspace integration отсутствуют. |
| Escalation | частично | Escalation create есть, но нет resolve/reassign lifecycle и SLA/queue integration. |

## Current Baseline Verdict

Шаг 1 завершен как audit baseline: API, ER, миграции, тестовый статус и основные точки риска зафиксированы. Перед шагом проектирования MVP workflow engine стоит отдельно решить, что делаем первым: tenant boundary, configurable stages/state machine, event contract/outbox, automation executor или payment hardening.
