3fdc526031
- 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>
187 lines
7.3 KiB
Markdown
187 lines
7.3 KiB
Markdown
# 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 — «выполнено».
|