11 KiB
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 передаются вpayloadevent envelope; общийevent_outboxschema не расширялся.- 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.pycovers 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 xfailedfortests/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.
Команда:
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.AutomationTaskCRUD/list/execute/retry.DealNotecreate/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_idunique, то есть один 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_idnullable: сделка может быть без лида или без клиента.sales_communication_sessions.lead_id/customer_id/transcript_idnullable: контекст сделки не всегда содержит клиента/транскрипт.sales_invoices.customer_id/basis_document_idnullable: счет может быть без клиента и без документа-основания.sales_payments.invoice_idnullable: платеж может быть привязан только к сделке.- Внешние ids (
external_message_id,external_call_id,external_payment_id) nullable и не unique.
Таблицы, которые есть:
sales_leadssales_dealssales_communication_sessionssales_messagessales_callssales_transcriptssales_notessales_offerssales_conditionssales_counterpartiessales_documentssales_invoicessales_paymentssales_stage_historysales_escalationssales_automation_taskssales_channel_switchessales_external_links
Migrations
Sales-миграции:
migrations/sql/0023_sales_sqlite.sqlmigrations/sql/0023_sales_postgres.sqlmigrations/sql/0024_sales_external_links_sqlite.sqlmigrations/sql/0024_sales_external_links_postgres.sqlmigrations/sql/0025_tenants_sqlite.sqlmigrations/sql/0025_tenants_postgres.sqlmigrations/sql/0026_sales_pipelines_sqlite.sqlmigrations/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.