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 }