Files
Bridge-and-Join-s/docs/tasks/PR-4-m2m-core-skeleton.md
zuevav 3fdc526031 docs: план проекта + промпты задач PR-1..PR-6 для Claude Code на ВМ
- docs/architecture/plan.md — полный план проекта (архитектура,
  стек, SLA, регуляторика, Реестр ПО, roadmap M1–M5, открытые
  вопросы и решения).
- docs/tasks/README.md — индекс задач и инструкция запуска для
  Claude Code на dev-ВМ.
- docs/tasks/PR-1-go-models-m2m.md — Go-модели M2M, парсер
  windows-1251, NSDDateTime, round-trip тесты на эталонах.
- docs/tasks/PR-2-fansy-ddl.md — DDL принимающей БД для команды
  Fansy (контракт данных, ETL-требования, словарь полей,
  тестовые данные).
- docs/tasks/PR-3-lk-openapi.md — OpenAPI контракт lk-gateway по
  ESIA Finance API V1, для синхронизации с командой ЛК.
- docs/tasks/PR-4-m2m-core-skeleton.md — FSM сделки,
  репозиторий, идемпотентность по GUID, метрики SLA.
- docs/tasks/PR-5-nsd-adapter-skeleton.md — REST-клиент ИШ НРД,
  маршрутизация типов пакетов (M2MTR/M2MTD/M2MER/SUBBR/SUBER/SUB16).
- docs/tasks/PR-6-crypto-service-skeleton.md — gRPC-каркас
  Java-сервиса криптографии (КриптоПро JCP, UDS, Provider-абстракция).

С этого коммита дальнейшая разработка идёт на dev-ВМ через
запущенный там Claude Code. Промпты PR-1..PR-3 готовы к параллельному
запуску; PR-4 после PR-1; PR-5 и PR-6 ждут поставку артефактов
(ИШ НРД, сертификаты, КриптоПро JCP).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 22:48:21 +03:00

187 lines
7.3 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.
# PR-4: Каркас `m2m-core` — FSM сделки, репозитории, идемпотентность
## Цель
Реализовать ядро бизнес-логики M2M-перевода: конечный автомат сделки,
типизированный доступ к БД, идемпотентность по GUID, метрики SLA по
этапам. Сетевые интеграции (НРД, ЛК, crypto-service) пока
**заглушками через интерфейсы** — реальные клиенты подключаются
в PR-5 и далее.
## Зависимости
- PR-1 (Go-модели M2M) — для типизированных сообщений.
- PR-2 (DDL Fansy) — на схему БД.
## Источники
- `docs/architecture/plan.md` — раздел «Поток одной заявки end-to-end»
и «SLA и регуляторные сроки».
- `internal/m2m/` — модели сообщений (после PR-1).
## Состав PR
### 1. Конечный автомат сделки
`internal/m2mcore/fsm.go`:
```
Draft → Validated → SubmittedToNSD → AwaitingDecision →
Confirmed → AwaitingSUB16 → Done
↘ Rejected
↘ TimedOut (после 10→5 мин без Decision)
↘ ManualApproval (опц. ветка)
```
Переходы — явные методы:
- `(d *Deal) Validate() error`,
- `(d *Deal) Submit(ctx) error`,
- `(d *Deal) ReceiveDecision(ctx, m2m.M2MTransferDecision) error`,
- `(d *Deal) Timeout(ctx) error`,
- `(d *Deal) SendToManualApproval(ctx, reason string) error`,
- `(d *Deal) ApproveManually(ctx, operator string) error`,
- `(d *Deal) RejectManually(ctx, operator, code, comment string) error`.
Каждый переход:
- проверяет текущее состояние (нельзя `Submit` если не `Validated`);
- пишет событие в `deal_events` (event sourcing для аудита);
- эмитит метрику `m2m_stage_duration_seconds{stage=...}`.
### 2. Доменная модель сделки
`internal/m2mcore/deal.go`:
```go
type Deal struct {
ID uuid.UUID
GUID m2m.UUID // тот, что в M2MTransferRequest.Header.GUID
State State
InvestorID uuid.UUID
SignedClaim []byte // подписанный XML от ЛК
Request *m2m.M2MTransferRequest
Response *m2m.M2MTransferResponse
Decision *m2m.M2MTransferDecision
CreatedAt time.Time
UpdatedAt time.Time
Stages []StageRecord // история состояний с timestamp
}
```
### 3. Репозиторий
`internal/m2mcore/repo.go`:
Интерфейс `Repository` с реализацией на pgx:
```go
type Repository interface {
Create(ctx, *Deal) error
GetByGUID(ctx, m2m.UUID) (*Deal, error)
Update(ctx, *Deal) error
List(ctx, Filter) ([]*Deal, error)
AppendEvent(ctx, *Deal, Event) error
}
```
Идемпотентность: `Create` использует `INSERT ... ON CONFLICT (guid) DO NOTHING RETURNING ...`
повторный запрос с тем же GUID не создаёт дубль, а возвращает существующий.
### 4. Внешние зависимости — интерфейсы (для PR-5+)
`internal/m2mcore/ports.go`:
```go
type NSDSender interface {
Send(ctx, *m2m.M2MTransferRequest) (*m2m.M2MTransferResponse, error)
SendDecision(ctx, *m2m.M2MTransferDecision) error
}
type LKCallbackClient interface {
UpdateStatus(ctx, claimID, status, reason string) error
}
type CryptoVerifier interface {
VerifyXMLDSig(ctx, payload []byte) (CertInfo, error)
}
type FansyStore interface {
GetClientByID(ctx, uuid.UUID) (*Client, error)
GetDepoAccount(ctx, deponentCode, accountID string) (*DepoAccount, error)
CheckBalance(ctx, depoAccountID uuid.UUID, isin string) (Quantity, error)
}
```
В PR-4 — заглушки `noopNSDSender`, `noopLKCallbackClient` и т. п.
Реальные клиенты подключаются в PR-5, PR-6.
### 5. Сборка enrichment Request из заявки
`internal/m2mcore/enrich.go`:
Функция «из заявки от ЛК + данные из `fansy-store``M2MTransferRequest`».
Шаги:
1. Распарсить подписанное заявление (XML от ЛК).
2. Поднять реквизиты сторон, депо-счета, остатки из FansyStore.
3. Проверить достаточность остатков по каждой ЦБ.
4. Сгенерировать `GUID` (uuid v4) и `ReferenceId` для каждой ЦБ
(`M2M` + дата `YYYYMMDD` + 5 случайных символов A-Z0-9).
5. Заполнить `Header.CreationTimestamp` = `nsdxml.NSDDateTime.Now()`.
6. Вернуть готовый `*m2m.M2MTransferRequest`, валидированный.
### 6. Метрики SLA
`internal/m2mcore/metrics.go`:
Prometheus метрики:
- `m2m_stage_duration_seconds{stage="..."}` — histogram длительности этапа,
- `m2m_deals_total{state="..."}` — counter сделок по итоговому состоянию,
- `m2m_sla_breaches_total{stage="...",budget="5m|2m"}` — counter
превышений 80% бюджета SLA.
Endpoint `/metrics` поднимается в `cmd/m2m-core/main.go`.
### 7. Заменить заглушку `cmd/m2m-core/main.go`
Минимальный рабочий сервер:
- читает конфиг (env: `BJ_DSN`, `BJ_LOG_LEVEL`, ...),
- открывает соединение с Postgres,
- инициализирует Repository,
- поднимает HTTP-эндпоинты `/healthz`, `/metrics`,
- логирует все запуски.
### 8. Тесты
- Юнит-тесты FSM (все переходы, в т. ч. недопустимые).
- Интеграционный тест с PostgreSQL через `testcontainers-go` (или
`dockertest`): создал сделку, обновил, прочитал, проверил
идемпотентность по GUID.
- Тест `enrich.go` на эталонной заявке (fixtures из примеров ESIA
Finance API).
## Требования
- Без эмодзи. Комментарии — на русском.
- `make ci` зелёный.
- Покрытие тестами `internal/m2mcore/` — не менее 60%.
## Коммит
```
feat(m2m-core): FSM сделки, репозиторий на pgx, идемпотентность по GUID, метрики SLA
- internal/m2mcore/fsm.go: конечный автомат с переходами и аудит-событиями
- internal/m2mcore/deal.go: доменная модель сделки
- internal/m2mcore/repo.go: репозиторий на pgx с идемпотентным Create
- internal/m2mcore/ports.go: интерфейсы для внешних зависимостей (заглушки)
- internal/m2mcore/enrich.go: сборка M2MTransferRequest из заявки + Fansy
- internal/m2mcore/metrics.go: Prometheus-метрики этапов и SLA
- cmd/m2m-core/main.go: минимальный сервер с /healthz и /metrics
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
```
## После коммита
Обновить `docs/tasks/README.md`: PR-4 — «выполнено».