206 lines
6.9 KiB
Go
206 lines
6.9 KiB
Go
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
|
|
}
|