Files
sendico/api/payments/quotation/internal/service/plan/plan_builder_templates.go
2026-02-18 20:38:08 +01:00

212 lines
7.2 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")
}
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
}