ledger autoresolution

This commit is contained in:
Stephan D
2026-01-26 04:48:09 +01:00
parent dce0f2b467
commit 81ba682d18
3 changed files with 90 additions and 13 deletions

View File

@@ -8,6 +8,7 @@ import (
"github.com/tech/sendico/billing/fees/storage"
"github.com/tech/sendico/billing/fees/storage/model"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mutil/mzap"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
)
@@ -49,12 +50,19 @@ func (r *feeResolver) ResolveFeeRule(ctx context.Context, orgRef *primitive.Obje
if rule, selErr := selectRule(plan, trigger, at, attrs); selErr == nil {
return plan, rule, nil
} else if !errors.Is(selErr, ErrNoFeeRuleFound) {
r.logger.Warn("failed selecting rule for org plan", zap.Error(selErr), zap.String("org_ref", orgRef.Hex()))
r.logger.Warn("Failed selecting rule for org plan", zap.Error(selErr), zap.String("org_ref", orgRef.Hex()))
return nil, nil, selErr
}
r.logger.Debug("no matching rule in org plan; falling back to global", zap.String("org_ref", orgRef.Hex()))
r.logger.Debug(
"No matching rule in org plan; falling back to global",
mzap.ObjRef("org_ref", *orgRef),
zap.String("trigger", string(trigger)),
zap.Time("booked_at", at),
zap.Any("attributes", attrs),
zapFieldsForPlan(plan)...,
)
} else if !errors.Is(err, storage.ErrFeePlanNotFound) {
r.logger.Warn("failed resolving org fee plan", zap.Error(err), zap.String("org_ref", orgRef.Hex()))
r.logger.Warn("Failed resolving org fee plan", zap.Error(err), zap.String("org_ref", orgRef.Hex()))
return nil, nil, err
}
}
@@ -62,16 +70,23 @@ func (r *feeResolver) ResolveFeeRule(ctx context.Context, orgRef *primitive.Obje
plan, err := r.getGlobalPlan(ctx, at)
if err != nil {
if errors.Is(err, storage.ErrFeePlanNotFound) {
r.logger.Debug("No applicable global fee plan found", zap.String("trigger", string(trigger)),
zap.Time("booked_at", at), zap.Any("attributes", attrs),
)
return nil, nil, merrors.NoData("fees: no applicable fee rule found")
}
r.logger.Warn("failed resolving global fee plan", zap.Error(err))
r.logger.Warn("Failed resolving global fee plan", zap.Error(err))
return nil, nil, err
}
rule, err := selectRule(plan, trigger, at, attrs)
if err != nil {
if !errors.Is(err, ErrNoFeeRuleFound) {
r.logger.Warn("failed selecting rule in global plan", zap.Error(err))
r.logger.Warn("Failed selecting rule in global plan", zap.Error(err))
} else {
r.logger.Debug("No matching rule in global plan", zap.String("trigger", string(trigger)),
zap.Time("booked_at", at), zap.Any("attributes", attrs), zapFieldsForPlan(plan)...,
)
}
return nil, nil, err
}
@@ -146,3 +161,29 @@ func matchesAppliesTo(appliesTo map[string]string, attrs map[string]string) bool
}
return true
}
func zapFieldsForPlan(plan *model.FeePlan) []zap.Field {
if plan == nil {
return []zap.Field{zap.Bool("plan_present", false)}
}
fields := []zap.Field{
zap.Bool("plan_present", true),
zap.Bool("plan_active", plan.Active),
zap.Time("plan_effective_from", plan.EffectiveFrom),
zap.Int("plan_rules_count", len(plan.Rules)),
}
if plan.EffectiveTo != nil {
fields = append(fields, zap.Time("plan_effective_to", *plan.EffectiveTo))
} else {
fields = append(fields, zap.Bool("plan_effective_to_set", false))
}
if plan.OrganizationRef != nil && !plan.OrganizationRef.IsZero() {
fields = append(fields, zap.String("plan_org_ref", plan.OrganizationRef.Hex()))
} else {
fields = append(fields, zap.Bool("plan_org_ref_set", false))
}
if plan.GetID() != nil && !plan.GetID().IsZero() {
fields = append(fields, zap.String("plan_id", plan.GetID().Hex()))
}
return fields
}

View File

@@ -5,6 +5,7 @@ import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
@@ -434,18 +435,18 @@ func (s *Service) computeQuoteWithTime(ctx context.Context, orgRef primitive.Obj
plan, rule, err := s.resolver.ResolveFeeRule(ctx, orgPtr, convertTrigger(intent.GetTrigger()), bookedAt, intent.GetAttributes())
if err != nil {
s.logger.Warn("Failed to resolve fee rule", zap.Error(err))
switch {
case errors.Is(err, merrors.ErrNoData):
return nil, nil, nil, status.Error(codes.NotFound, "fee rule not found")
return nil, nil, nil, status.Error(codes.NotFound, fmt.Sprintf("fee rule not found: %s", err.Error()))
case errors.Is(err, merrors.ErrDataConflict):
return nil, nil, nil, status.Error(codes.FailedPrecondition, "conflicting fee rules")
return nil, nil, nil, status.Error(codes.FailedPrecondition, fmt.Sprintf("conflicting fee rules: %s", err.Error()))
case errors.Is(err, storage.ErrConflictingFeePlans):
return nil, nil, nil, status.Error(codes.FailedPrecondition, "conflicting fee plans")
return nil, nil, nil, status.Error(codes.FailedPrecondition, fmt.Sprintf("conflicting fee plans: %s", err.Error()))
case errors.Is(err, storage.ErrFeePlanNotFound):
return nil, nil, nil, status.Error(codes.NotFound, "fee plan not found")
return nil, nil, nil, status.Error(codes.NotFound, fmt.Sprintf("fee plan not found: %s", err.Error()))
default:
logger.Warn("failed to resolve fee rule", zap.Error(err))
return nil, nil, nil, status.Error(codes.Internal, "failed to resolve fee rule")
return nil, nil, nil, status.Error(codes.Internal, fmt.Sprintf("failed to resolve fee rule: %s", err.Error()))
}
}