granular fees plans
This commit is contained in:
@@ -366,13 +366,36 @@ func ruleMatchesAttributes(rule model.FeeRule, attributes map[string]string) boo
|
||||
if attributes == nil {
|
||||
return false
|
||||
}
|
||||
if attrValue, ok := attributes[key]; !ok || attrValue != value {
|
||||
attrValue, ok := attributes[key]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !matchesAttributeValue(value, attrValue) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func matchesAttributeValue(expected, actual string) bool {
|
||||
trimmed := strings.TrimSpace(expected)
|
||||
if trimmed == "" {
|
||||
return actual == ""
|
||||
}
|
||||
|
||||
values := strings.Split(trimmed, ",")
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
if value == "*" || value == actual {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func mapLineType(lineType string) accountingv1.PostingLineType {
|
||||
switch strings.ToLower(lineType) {
|
||||
case "tax":
|
||||
|
||||
@@ -3,6 +3,7 @@ package resolver
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/billing/fees/storage"
|
||||
@@ -95,6 +96,24 @@ func (r *feeResolver) ResolveFeeRule(ctx context.Context, orgRef *primitive.Obje
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
selectedFields := []zap.Field{
|
||||
zap.String("trigger", string(trigger)),
|
||||
zap.Time("booked_at", at),
|
||||
zap.Any("attributes", attrs),
|
||||
zap.String("rule_id", rule.RuleID),
|
||||
zap.Int("rule_priority", rule.Priority),
|
||||
zap.Any("rule_applies_to", rule.AppliesTo),
|
||||
zap.Time("rule_effective_from", rule.EffectiveFrom),
|
||||
}
|
||||
if rule.EffectiveTo != nil {
|
||||
selectedFields = append(selectedFields, zap.Time("rule_effective_to", *rule.EffectiveTo))
|
||||
}
|
||||
if orgRef != nil && !orgRef.IsZero() {
|
||||
selectedFields = append(selectedFields, mzap.ObjRef("org_ref", *orgRef))
|
||||
}
|
||||
selectedFields = append(selectedFields, zapFieldsForPlan(plan)...)
|
||||
r.logger.Debug("Selected fee rule", selectedFields...)
|
||||
|
||||
return plan, rule, nil
|
||||
}
|
||||
|
||||
@@ -159,13 +178,36 @@ func matchesAppliesTo(appliesTo map[string]string, attrs map[string]string) bool
|
||||
if attrs == nil {
|
||||
return false
|
||||
}
|
||||
if attrs[key] != value {
|
||||
attrValue, ok := attrs[key]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !matchesAppliesValue(value, attrValue) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func matchesAppliesValue(expected, actual string) bool {
|
||||
trimmed := strings.TrimSpace(expected)
|
||||
if trimmed == "" {
|
||||
return actual == ""
|
||||
}
|
||||
|
||||
values := strings.Split(trimmed, ",")
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
if value == "*" || value == actual {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func zapFieldsForPlan(plan *model.FeePlan) []zap.Field {
|
||||
if plan == nil {
|
||||
return []zap.Field{zap.Bool("plan_present", false)}
|
||||
|
||||
@@ -189,6 +189,47 @@ func TestResolver_AppliesToFiltering(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolver_AppliesToFilteringSupportsListsAndWildcard(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
now := time.Now()
|
||||
plan := &model.FeePlan{
|
||||
Active: true,
|
||||
EffectiveFrom: now.Add(-time.Hour),
|
||||
Rules: []model.FeeRule{
|
||||
{RuleID: "network_multi", Trigger: model.TriggerCapture, Priority: 300, Percentage: "0.03", AppliesTo: map[string]string{"network": "tron, solana"}, EffectiveFrom: now.Add(-time.Hour)},
|
||||
{RuleID: "asset_any", Trigger: model.TriggerCapture, Priority: 200, Percentage: "0.02", AppliesTo: map[string]string{"asset": "*"}, EffectiveFrom: now.Add(-time.Hour)},
|
||||
{RuleID: "default", Trigger: model.TriggerCapture, Priority: 100, Percentage: "0.05", EffectiveFrom: now.Add(-time.Hour)},
|
||||
},
|
||||
}
|
||||
store := &memoryPlansStore{plans: []*model.FeePlan{plan}}
|
||||
resolver := New(store, zap.NewNop())
|
||||
|
||||
_, rule, err := resolver.ResolveFeeRule(context.Background(), nil, model.TriggerCapture, now, map[string]string{"network": "tron"})
|
||||
if err != nil {
|
||||
t.Fatalf("expected list match rule, got error: %v", err)
|
||||
}
|
||||
if rule.RuleID != "network_multi" {
|
||||
t.Fatalf("expected network list rule, got %s", rule.RuleID)
|
||||
}
|
||||
|
||||
_, rule, err = resolver.ResolveFeeRule(context.Background(), nil, model.TriggerCapture, now, map[string]string{"asset": "USDT"})
|
||||
if err != nil {
|
||||
t.Fatalf("expected wildcard rule, got error: %v", err)
|
||||
}
|
||||
if rule.RuleID != "asset_any" {
|
||||
t.Fatalf("expected asset wildcard rule, got %s", rule.RuleID)
|
||||
}
|
||||
|
||||
_, rule, err = resolver.ResolveFeeRule(context.Background(), nil, model.TriggerCapture, now, map[string]string{"network": "eth"})
|
||||
if err != nil {
|
||||
t.Fatalf("expected default rule, got error: %v", err)
|
||||
}
|
||||
if rule.RuleID != "default" {
|
||||
t.Fatalf("expected default rule, got %s", rule.RuleID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolver_MissingTriggerReturnsErr(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user