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") } if !model.IsValidReportVisibility(step.ReportVisibility) { logger.Warn("Plan template step has invalid report visibility", zap.String("step_id", id), zap.String("report_visibility", string(step.ReportVisibility))) return merrors.InvalidArgument("plan builder: plan template report visibility is invalid") } 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 }