Fully separated payment quotation and orchestration flows
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
package plan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/tech/sendico/payments/storage/model"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func selectPlanTemplate(ctx context.Context, logger mlogger.Logger, templates PlanTemplateStore, sourceRail, destRail model.Rail, network string) (*model.PaymentPlanTemplate, error) {
|
||||
if templates == nil {
|
||||
return nil, merrors.InvalidArgument("plan builder: plan templates store is required")
|
||||
}
|
||||
|
||||
logger = logger.With(
|
||||
zap.String("source_rail", string(sourceRail)),
|
||||
zap.String("dest_rail", string(destRail)),
|
||||
zap.String("network", network),
|
||||
)
|
||||
logger.Debug("Selecting plan template")
|
||||
|
||||
enabled := true
|
||||
result, err := templates.List(ctx, &model.PaymentPlanTemplateFilter{
|
||||
FromRail: sourceRail,
|
||||
ToRail: destRail,
|
||||
IsEnabled: &enabled,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Warn("Failed to list plan templates", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
if result == nil || len(result.Items) == 0 {
|
||||
logger.Warn("No plan templates found for route")
|
||||
return nil, merrors.InvalidArgument("plan builder: plan template missing")
|
||||
}
|
||||
|
||||
logger.Debug("Fetched plan templates", zap.Int("total", len(result.Items)))
|
||||
|
||||
candidates := make([]*model.PaymentPlanTemplate, 0, len(result.Items))
|
||||
for _, tpl := range result.Items {
|
||||
if tpl == nil || !tpl.IsEnabled {
|
||||
continue
|
||||
}
|
||||
if tpl.FromRail != sourceRail || tpl.ToRail != destRail {
|
||||
continue
|
||||
}
|
||||
if !templateMatchesNetwork(tpl, network) {
|
||||
logger.Debug("Template network mismatch, skipping",
|
||||
mzap.StorableRef(tpl),
|
||||
zap.String("template_network", tpl.Network))
|
||||
continue
|
||||
}
|
||||
if err := validatePlanTemplate(logger, tpl); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
candidates = append(candidates, tpl)
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
logger.Warn("No valid plan template candidates after filtering")
|
||||
return nil, merrors.InvalidArgument("plan builder: plan template missing")
|
||||
}
|
||||
|
||||
logger.Debug("Plan template candidates filtered", zap.Int("candidates", len(candidates)))
|
||||
|
||||
sort.Slice(candidates, func(i, j int) bool {
|
||||
pi := templatePriority(candidates[i], network)
|
||||
pj := templatePriority(candidates[j], network)
|
||||
if pi != pj {
|
||||
return pi < pj
|
||||
}
|
||||
if candidates[i].Network != candidates[j].Network {
|
||||
return candidates[i].Network < candidates[j].Network
|
||||
}
|
||||
return candidates[i].ID.Hex() < candidates[j].ID.Hex()
|
||||
})
|
||||
|
||||
selected := candidates[0]
|
||||
logger.Debug("Plan template selected",
|
||||
mzap.StorableRef(selected),
|
||||
zap.String("template_network", selected.Network),
|
||||
zap.Int("steps", len(selected.Steps)),
|
||||
zap.Int("priority", templatePriority(selected, network)))
|
||||
|
||||
return selected, nil
|
||||
}
|
||||
|
||||
func templateMatchesNetwork(template *model.PaymentPlanTemplate, network string) bool {
|
||||
if template == nil {
|
||||
return false
|
||||
}
|
||||
templateNetwork := strings.ToUpper(strings.TrimSpace(template.Network))
|
||||
net := strings.ToUpper(strings.TrimSpace(network))
|
||||
if templateNetwork == "" {
|
||||
return true
|
||||
}
|
||||
if net == "" {
|
||||
return false
|
||||
}
|
||||
return strings.EqualFold(templateNetwork, net)
|
||||
}
|
||||
|
||||
func templatePriority(template *model.PaymentPlanTemplate, network string) int {
|
||||
if template == nil {
|
||||
return 2
|
||||
}
|
||||
templateNetwork := strings.ToUpper(strings.TrimSpace(template.Network))
|
||||
net := strings.ToUpper(strings.TrimSpace(network))
|
||||
if net != "" && strings.EqualFold(templateNetwork, net) {
|
||||
return 0
|
||||
}
|
||||
if templateNetwork == "" {
|
||||
return 1
|
||||
}
|
||||
return 2
|
||||
}
|
||||
|
||||
func validatePlanTemplate(logger mlogger.Logger, template *model.PaymentPlanTemplate) error {
|
||||
if template == nil {
|
||||
return merrors.InvalidArgument("plan builder: plan template is required")
|
||||
}
|
||||
|
||||
logger = logger.With(
|
||||
mzap.StorableRef(template),
|
||||
zap.String("from_rail", string(template.FromRail)),
|
||||
zap.String("to_rail", string(template.ToRail)),
|
||||
zap.String("network", template.Network),
|
||||
)
|
||||
logger.Debug("Validating plan template")
|
||||
|
||||
if len(template.Steps) == 0 {
|
||||
logger.Warn("Plan template has no steps")
|
||||
return merrors.InvalidArgument("plan builder: plan template steps are required")
|
||||
}
|
||||
|
||||
seen := map[string]struct{}{}
|
||||
for idx, step := range template.Steps {
|
||||
id := strings.TrimSpace(step.StepID)
|
||||
if id == "" {
|
||||
logger.Warn("Plan template step missing ID", zap.Int("step_index", idx))
|
||||
return merrors.InvalidArgument("plan builder: plan template step id is required")
|
||||
}
|
||||
if _, exists := seen[id]; exists {
|
||||
logger.Warn("Duplicate plan template step ID", zap.String("step_id", id))
|
||||
return merrors.InvalidArgument("plan builder: plan template step id must be unique")
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
if strings.TrimSpace(step.Operation) == "" {
|
||||
logger.Warn("Plan template step missing operation", zap.String("step_id", id),
|
||||
zap.Int("step_index", idx))
|
||||
return merrors.InvalidArgument("plan builder: plan template operation is required")
|
||||
}
|
||||
action, err := actionForOperation(step.Operation)
|
||||
if err != nil {
|
||||
logger.Warn("Plan template step has invalid operation", zap.String("step_id", id),
|
||||
zap.String("operation", step.Operation), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
if step.Rail == model.RailLedger && action == model.RailOperationMove {
|
||||
if step.FromRole == nil || strings.TrimSpace(string(*step.FromRole)) == "" {
|
||||
logger.Warn("Ledger move step missing fromRole", zap.String("step_id", id),
|
||||
zap.String("operation", step.Operation))
|
||||
return merrors.InvalidArgument("plan builder: ledger.move fromRole is required")
|
||||
}
|
||||
if step.ToRole == nil || strings.TrimSpace(string(*step.ToRole)) == "" {
|
||||
logger.Warn("Ledger move step missing toRole", zap.String("step_id", id),
|
||||
zap.String("operation", step.Operation))
|
||||
return merrors.InvalidArgument("plan builder: ledger.move toRole is required")
|
||||
}
|
||||
from := strings.ToLower(strings.TrimSpace(string(*step.FromRole)))
|
||||
to := strings.ToLower(strings.TrimSpace(string(*step.ToRole)))
|
||||
if from == "" || to == "" || strings.EqualFold(from, to) {
|
||||
logger.Warn("Ledger move step has invalid roles", zap.String("step_id", id),
|
||||
zap.String("from_role", from), zap.String("to_role", to))
|
||||
return merrors.InvalidArgument("plan builder: ledger.move fromRole and toRole must differ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, step := range template.Steps {
|
||||
for _, dep := range step.DependsOn {
|
||||
depID := strings.TrimSpace(dep)
|
||||
if _, ok := seen[depID]; !ok {
|
||||
logger.Warn("Plan template step has missing dependency", zap.String("step_id", step.StepID),
|
||||
zap.String("missing_dependency", depID))
|
||||
return merrors.InvalidArgument("plan builder: plan template dependency missing")
|
||||
}
|
||||
}
|
||||
for _, dep := range step.CommitAfter {
|
||||
depID := strings.TrimSpace(dep)
|
||||
if _, ok := seen[depID]; !ok {
|
||||
logger.Warn("Plan template step has missing commit dependency", zap.String("step_id", step.StepID),
|
||||
zap.String("missing_commit_dependency", depID))
|
||||
return merrors.InvalidArgument("plan builder: plan template commit dependency missing")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug("Plan template validation successful", zap.Int("steps", len(template.Steps)))
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user