File size: 5,098 Bytes
5a22efd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | package lineengine
import (
"context"
"fmt"
"github.com/samber/lo"
"github.com/openmeterio/openmeter/openmeter/billing"
"github.com/openmeterio/openmeter/openmeter/productcatalog"
"github.com/openmeterio/openmeter/openmeter/streaming"
"github.com/openmeterio/openmeter/pkg/clock"
"github.com/openmeterio/openmeter/pkg/timeutil"
)
type SplitLineGroupAdapter interface {
CreateSplitLineGroup(ctx context.Context, input billing.CreateSplitLineGroupAdapterInput) (billing.SplitLineGroup, error)
GetSplitLineGroupHeaders(ctx context.Context, input billing.GetSplitLineGroupHeadersInput) (billing.SplitLineGroupHeaders, error)
}
func (e *Engine) SplitGatheringLine(ctx context.Context, in billing.SplitGatheringLineInput) (billing.SplitGatheringLineResult, error) {
res := billing.SplitGatheringLineResult{}
if err := in.Validate(); err != nil {
return res, err
}
line := in.Line
if !line.ServicePeriod.Contains(in.SplitAt) {
return res, fmt.Errorf("line[%s]: splitAt is not within the line period", line.ID)
}
var splitLineGroupID string
if line.SplitLineGroupID == nil {
splitLineGroup, err := e.adapter.CreateSplitLineGroup(ctx, billing.CreateSplitLineGroupAdapterInput{
Namespace: line.Namespace,
SplitLineGroupMutableFields: billing.SplitLineGroupMutableFields{
Name: line.Name,
Description: line.Description,
ServicePeriod: timeutil.ClosedPeriod{
From: line.ServicePeriod.From,
To: line.ServicePeriod.To,
},
RatecardDiscounts: line.RateCardDiscounts,
},
UniqueReferenceID: line.ChildUniqueReferenceID,
Currency: line.Currency,
Price: lo.ToPtr(line.Price),
FeatureKey: lo.EmptyableToPtr(line.FeatureKey),
Subscription: line.Subscription,
})
if err != nil {
return res, fmt.Errorf("creating split line group: %w", err)
}
splitLineGroupID = splitLineGroup.ID
} else {
splitLineGroupID = lo.FromPtr(line.SplitLineGroupID)
if splitLineGroupID == "" {
return res, fmt.Errorf("split line group id is empty")
}
}
postSplitAtLine, err := line.CloneForCreate(func(l *billing.GatheringLine) {
l.ServicePeriod.From = in.SplitAt
l.SplitLineGroupID = lo.ToPtr(splitLineGroupID)
l.ChildUniqueReferenceID = nil
})
if err != nil {
return res, fmt.Errorf("cloning post split line: %w", err)
}
postSplitAtLineEmpty, err := isPeriodEmptyConsideringTruncations(postSplitAtLine)
if err != nil {
return res, fmt.Errorf("checking if post split line is empty: %w", err)
}
if !postSplitAtLineEmpty {
if err := postSplitAtLine.Validate(); err != nil {
return res, fmt.Errorf("validating post split line: %w", err)
}
}
line.ServicePeriod.To = in.SplitAt
line.InvoiceAt = in.SplitAt
line.SplitLineGroupID = lo.ToPtr(splitLineGroupID)
line.ChildUniqueReferenceID = nil
preSplitAtLine := line
preSplitAtLineEmpty, err := isPeriodEmptyConsideringTruncations(preSplitAtLine)
if err != nil {
return res, fmt.Errorf("checking if pre split line is empty: %w", err)
}
if preSplitAtLineEmpty {
preSplitAtLine.DeletedAt = lo.ToPtr(clock.Now())
} else {
if err := preSplitAtLine.Validate(); err != nil {
return res, fmt.Errorf("validating pre split line: %w", err)
}
}
var postSplitAtLinePtr *billing.GatheringLine
if !postSplitAtLineEmpty {
postSplitAtLinePtr = &postSplitAtLine
}
return billing.SplitGatheringLineResult{
PreSplitAtLine: preSplitAtLine,
PostSplitAtLine: postSplitAtLinePtr,
}, nil
}
func (e *Engine) ResolveSplitLineGroupHeaders(ctx context.Context, ns string, lines billing.StandardLines) error {
splitLineGroupIDs := lo.Uniq(
lo.Filter(
lo.Map(lines, func(line *billing.StandardLine, _ int) string { return lo.FromPtr(line.SplitLineGroupID) }),
func(id string, _ int) bool { return id != "" },
),
)
if len(splitLineGroupIDs) == 0 {
return nil
}
splitLineGroupHeaders, err := e.adapter.GetSplitLineGroupHeaders(ctx, billing.GetSplitLineGroupHeadersInput{
Namespace: ns,
SplitLineGroupIDs: splitLineGroupIDs,
})
if err != nil {
return fmt.Errorf("getting split line group headers: %w", err)
}
splitLineGroupHeadersByID := lo.SliceToMap(splitLineGroupHeaders, func(header billing.SplitLineGroup) (string, billing.SplitLineGroup) {
return header.ID, header
})
for idx := range lines {
if lines[idx].SplitLineGroupID == nil {
continue
}
splitLineGroupHeader, ok := splitLineGroupHeadersByID[lo.FromPtr(lines[idx].SplitLineGroupID)]
if !ok {
return fmt.Errorf("split line group header not found for line[%s]: id[%s]", lines[idx].ID, lo.FromPtr(lines[idx].SplitLineGroupID))
}
lines[idx].SplitLineHierarchy = &billing.SplitLineHierarchy{
Group: splitLineGroupHeader,
}
}
return nil
}
func isPeriodEmptyConsideringTruncations(line billing.GatheringLine) (bool, error) {
price := line.GetPrice()
if price == nil {
return false, fmt.Errorf("price is nil")
}
if price.Type() == productcatalog.FlatPriceType {
return false, nil
}
return line.GetServicePeriod().Truncate(streaming.MinimumWindowSizeDuration).IsEmpty(), nil
}
|