feat(m2m-core): FSM сделки, репозиторий, идемпотентность по GUID, метрики SLA
- internal/m2mcore/fsm.go: конечный автомат с переходами Draft → Validated → SubmittedToNSD → AwaitingDecision → Confirmed → AwaitingSUB16 → Done, ветки Rejected/TimedOut/ManualApproval - internal/m2mcore/deal.go: доменная модель Deal с методами Validate/Submit/ReceiveDecision/Timeout/SendToManualApproval/ApproveManually/RejectManually/CompleteSUB16, журнал событий - internal/m2mcore/uuid.go: генератор UUID v4 без внешних зависимостей (crypto/rand) - internal/m2mcore/repo.go: порт Repository + MemoryRepository с идемпотентным Create по GUID - internal/m2mcore/ports.go: порты NSDSender/LKCallbackClient/CryptoVerifier/FansyStore с no-op заглушками для M1 - internal/m2mcore/enrich.go: EnrichRequest — сборка M2MTransferRequest из ClaimInput + Fansy, генерация ReferenceID по каждой ЦБ - internal/m2mcore/metrics.go: порт Recorder + MemoryRecorder в Prometheus-text формате - cmd/m2m-core/main.go: HTTP-сервер с /healthz и /metrics, graceful shutdown - migrations/m2m-core/001__deals.sql: схема для PostgreSQL-Repository (для M2) Покрытие: 63.1%. make ci зелёный. Без внешних Go-зависимостей (pgx и prometheus подключим в M2, когда прокси zetit откроет Go-модули). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
package m2mcore
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// State — конечное состояние сделки M2M.
|
||||
type State string
|
||||
|
||||
const (
|
||||
StateDraft State = "draft"
|
||||
StateValidated State = "validated"
|
||||
StateSubmittedToNSD State = "submitted_to_nsd"
|
||||
StateAwaitingDecision State = "awaiting_decision"
|
||||
StateConfirmed State = "confirmed"
|
||||
StateAwaitingSUB16 State = "awaiting_sub16"
|
||||
StateDone State = "done"
|
||||
StateRejected State = "rejected"
|
||||
StateTimedOut State = "timed_out"
|
||||
StateManualApproval State = "manual_approval"
|
||||
)
|
||||
|
||||
// ErrInvalidTransition возвращается при попытке перейти в состояние,
|
||||
// которое не разрешено из текущего.
|
||||
var ErrInvalidTransition = errors.New("m2mcore: недопустимый переход FSM")
|
||||
|
||||
// allowedTransitions — карта разрешённых переходов FSM сделки.
|
||||
// Любая попытка перейти в state, отсутствующее в списке для текущего,
|
||||
// заканчивается ErrInvalidTransition.
|
||||
var allowedTransitions = map[State]map[State]struct{}{
|
||||
StateDraft: {
|
||||
StateValidated: {},
|
||||
StateRejected: {},
|
||||
StateManualApproval: {},
|
||||
},
|
||||
StateValidated: {
|
||||
StateSubmittedToNSD: {},
|
||||
StateRejected: {},
|
||||
StateManualApproval: {},
|
||||
},
|
||||
StateSubmittedToNSD: {
|
||||
StateAwaitingDecision: {},
|
||||
StateRejected: {},
|
||||
},
|
||||
StateAwaitingDecision: {
|
||||
StateConfirmed: {},
|
||||
StateRejected: {},
|
||||
StateTimedOut: {},
|
||||
StateManualApproval: {},
|
||||
},
|
||||
StateConfirmed: {
|
||||
StateAwaitingSUB16: {},
|
||||
StateDone: {},
|
||||
},
|
||||
StateAwaitingSUB16: {
|
||||
StateDone: {},
|
||||
},
|
||||
StateManualApproval: {
|
||||
StateValidated: {},
|
||||
StateConfirmed: {},
|
||||
StateRejected: {},
|
||||
},
|
||||
// Завершающие состояния — без выходов.
|
||||
StateDone: {},
|
||||
StateRejected: {},
|
||||
StateTimedOut: {},
|
||||
}
|
||||
|
||||
// IsTerminal возвращает true для завершающих состояний.
|
||||
func IsTerminal(s State) bool {
|
||||
switch s {
|
||||
case StateDone, StateRejected, StateTimedOut:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// CanTransition сообщает, разрешён ли переход from -> to.
|
||||
func CanTransition(from, to State) bool {
|
||||
allowed, ok := allowedTransitions[from]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
_, ok = allowed[to]
|
||||
return ok
|
||||
}
|
||||
|
||||
// transition проверяет переход и возвращает обёрнутую ошибку с
|
||||
// контекстом, если он недопустим.
|
||||
func transition(from, to State) error {
|
||||
if !CanTransition(from, to) {
|
||||
return fmt.Errorf("%w: %s -> %s", ErrInvalidTransition, from, to)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user