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

7.3 KiB
Raw Permalink Blame History

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:

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:

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:

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-storeM2MTransferRequest».

Шаги:

  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 — «выполнено».