Skip to content

Commit bdcda88

Browse files
committed
chore: prior run values
1 parent eb760ff commit bdcda88

13 files changed

Lines changed: 519 additions & 195 deletions

File tree

openmeter/billing/charges/usagebased/service/creditheninvoice.go

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"github.com/openmeterio/openmeter/openmeter/billing"
1212
"github.com/openmeterio/openmeter/openmeter/billing/charges/meta"
1313
"github.com/openmeterio/openmeter/openmeter/billing/charges/usagebased"
14-
usagebasedrating "github.com/openmeterio/openmeter/openmeter/billing/charges/usagebased/service/rating"
1514
usagebasedrun "github.com/openmeterio/openmeter/openmeter/billing/charges/usagebased/service/run"
1615
"github.com/openmeterio/openmeter/openmeter/productcatalog"
1716
"github.com/openmeterio/openmeter/pkg/statelessx"
@@ -248,16 +247,15 @@ func (s *CreditThenInvoiceStateMachine) startInvoiceCreatedRun(
248247
}
249248

250249
result, err := s.Runs.CreateRatedRun(ctx, usagebasedrun.CreateRatedRunInput{
251-
Charge: s.Charge,
252-
CustomerOverride: s.CustomerOverride,
253-
FeatureMeter: s.FeatureMeter,
254-
Type: runType,
255-
StoredAtLT: storedAtLT,
256-
ServicePeriodTo: servicePeriodTo,
257-
LineID: lo.ToPtr(input.LineID),
258-
IgnoreMinimumCommitment: ignoreMinimumCommitmentForRunType(runType),
259-
CreditAllocation: usagebasedrun.CreditAllocationAvailable,
260-
CurrencyCalculator: s.CurrencyCalculator,
250+
Charge: s.Charge,
251+
CustomerOverride: s.CustomerOverride,
252+
FeatureMeter: s.FeatureMeter,
253+
Type: runType,
254+
StoredAtLT: storedAtLT,
255+
ServicePeriodTo: servicePeriodTo,
256+
LineID: lo.ToPtr(input.LineID),
257+
CreditAllocation: usagebasedrun.CreditAllocationAvailable,
258+
CurrencyCalculator: s.CurrencyCalculator,
261259
})
262260
if err != nil {
263261
return err
@@ -275,12 +273,6 @@ func (s *CreditThenInvoiceStateMachine) StartFinalInvoiceRun(ctx context.Context
275273
return s.startInvoiceCreatedRun(ctx, input, usagebased.RealizationRunTypeFinalRealization)
276274
}
277275

278-
func ignoreMinimumCommitmentForRunType(runType usagebased.RealizationRunType) bool {
279-
// Partial invoice runs are interim cumulative checkpoints. Minimum commitment is billed only on the
280-
// final realization, so partial runs must suppress it during both creation and later snapshotting.
281-
return runType == usagebased.RealizationRunTypePartialInvoice
282-
}
283-
284276
func resolveInvoiceCreatedTrigger(charge usagebased.Charge, billedPeriod timeutil.ClosedPeriod) meta.Trigger {
285277
if meta.NormalizeTimestamp(billedPeriod.To).Equal(meta.NormalizeTimestamp(charge.Intent.ServicePeriod.To)) {
286278
return meta.TriggerFinalInvoiceCreated
@@ -309,18 +301,18 @@ func (s *CreditThenInvoiceStateMachine) SnapshotInvoiceUsage(ctx context.Context
309301

310302
storedAtLT := meta.NormalizeTimestamp(currentRun.StoredAtLT)
311303

312-
ratingResult, err := s.Rater.GetDetailedLinesForUsage(ctx, usagebasedrating.GetDetailedLinesForUsageInput{
313-
Charge: s.Charge,
314-
PriorRuns: s.Charge.Realizations.Without(currentRun.ID),
315-
Customer: s.CustomerOverride,
316-
FeatureMeter: s.FeatureMeter,
317-
ServicePeriodTo: currentRun.ServicePeriodTo,
318-
StoredAtLT: storedAtLT,
319-
IgnoreMinimumCommitment: ignoreMinimumCommitmentForRunType(currentRun.Type),
304+
snapshot, err := s.Runs.SnapshotRunUsage(ctx, usagebasedrun.SnapshotRunUsageInput{
305+
Charge: s.Charge,
306+
CurrentRunID: currentRun.ID,
307+
StoredAtLT: storedAtLT,
308+
ServicePeriodTo: currentRun.ServicePeriodTo,
309+
CustomerOverride: s.CustomerOverride,
310+
FeatureMeter: s.FeatureMeter,
320311
})
321312
if err != nil {
322-
return fmt.Errorf("get rating for usage: %w", err)
313+
return fmt.Errorf("snapshot run usage: %w", err)
323314
}
315+
ratingResult := snapshot.Rating
324316

325317
currentTotals := ratingResult.Totals.RoundToPrecision(s.CurrencyCalculator)
326318

openmeter/billing/charges/usagebased/service/creditheninvoice_test.go

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,3 @@ func TestResolveInvoiceCreatedTrigger(t *testing.T) {
5858
require.Equal(t, meta.TriggerFinalInvoiceCreated, trigger)
5959
})
6060
}
61-
62-
func TestIgnoreMinimumCommitmentForRunType(t *testing.T) {
63-
t.Run("partial invoice run", func(t *testing.T) {
64-
require.True(t, ignoreMinimumCommitmentForRunType(usagebased.RealizationRunTypePartialInvoice))
65-
})
66-
67-
t.Run("final realization run", func(t *testing.T) {
68-
require.False(t, ignoreMinimumCommitmentForRunType(usagebased.RealizationRunTypeFinalRealization))
69-
})
70-
}

openmeter/billing/charges/usagebased/service/creditsonly.go

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99

1010
"github.com/openmeterio/openmeter/openmeter/billing/charges/meta"
1111
"github.com/openmeterio/openmeter/openmeter/billing/charges/usagebased"
12-
usagebasedrating "github.com/openmeterio/openmeter/openmeter/billing/charges/usagebased/service/rating"
1312
usagebasedrun "github.com/openmeterio/openmeter/openmeter/billing/charges/usagebased/service/run"
1413
"github.com/openmeterio/openmeter/openmeter/productcatalog"
1514
"github.com/openmeterio/openmeter/pkg/clock"
@@ -184,17 +183,18 @@ func (s *CreditsOnlyStateMachine) FinalizeRealizationRun(ctx context.Context) er
184183

185184
storedAtLT := meta.NormalizeTimestamp(currentRun.StoredAtLT)
186185

187-
ratingResult, err := s.Rater.GetDetailedLinesForUsage(ctx, usagebasedrating.GetDetailedLinesForUsageInput{
188-
Charge: s.Charge,
189-
PriorRuns: s.Charge.Realizations.Without(currentRun.ID),
190-
Customer: s.CustomerOverride,
191-
FeatureMeter: s.FeatureMeter,
192-
ServicePeriodTo: currentRun.ServicePeriodTo,
193-
StoredAtLT: storedAtLT,
186+
snapshot, err := s.Runs.SnapshotRunUsage(ctx, usagebasedrun.SnapshotRunUsageInput{
187+
Charge: s.Charge,
188+
CurrentRunID: currentRun.ID,
189+
StoredAtLT: storedAtLT,
190+
ServicePeriodTo: currentRun.ServicePeriodTo,
191+
CustomerOverride: s.CustomerOverride,
192+
FeatureMeter: s.FeatureMeter,
194193
})
195194
if err != nil {
196-
return fmt.Errorf("get rating for usage: %w", err)
195+
return fmt.Errorf("snapshot run usage: %w", err)
197196
}
197+
ratingResult := snapshot.Rating
198198

199199
currentTotals := ratingResult.Totals.RoundToPrecision(s.CurrencyCalculator)
200200
targetCreditsTotal := currentTotals.Total
@@ -219,12 +219,18 @@ func (s *CreditsOnlyStateMachine) FinalizeRealizationRun(ctx context.Context) er
219219
}
220220
currentRun.DetailedLines = mo.Some(runDetailedLines)
221221

222-
if _, err := s.Adapter.UpdateRealizationRun(ctx, usagebased.UpdateRealizationRunInput{
222+
currentRunBase, err := s.Adapter.UpdateRealizationRun(ctx, usagebased.UpdateRealizationRunInput{
223223
ID: currentRun.ID,
224224
StoredAtLT: mo.Some(storedAtLT),
225225
MeteredQuantity: mo.Some(ratingResult.Quantity),
226226
Totals: mo.Some(currentTotals),
227-
}); err != nil {
227+
})
228+
if err != nil {
229+
return fmt.Errorf("update realization run: %w", err)
230+
}
231+
currentRun.RealizationRunBase = currentRunBase
232+
233+
if err := s.Charge.Realizations.SetRealizationRun(currentRun); err != nil {
228234
return fmt.Errorf("update realization run: %w", err)
229235
}
230236

openmeter/billing/charges/usagebased/service/rating/details.go

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,26 @@ import (
1313
"github.com/openmeterio/openmeter/openmeter/productcatalog/feature"
1414
)
1515

16-
type GetDetailedLinesForUsageInput struct {
17-
Charge usagebased.Charge
18-
PriorRuns usagebased.RealizationRuns
19-
Customer billing.CustomerOverrideWithDetails
20-
FeatureMeter feature.FeatureMeter
16+
type GetDetailedRatingForUsageInput struct {
17+
// Charge general data
18+
19+
// Charge contains the charge intent and the prior runs with detailed lines expanded.
20+
Charge usagebased.Charge
21+
22+
// Current run's data
23+
24+
// ServicePeriodTo defines the rated event-time upper bound for the current run.
2125
ServicePeriodTo time.Time
22-
StoredAtLT time.Time
23-
// IgnoreMinimumCommitment suppresses minimum commitment while still applying the rest of the billing mutators.
24-
IgnoreMinimumCommitment bool
26+
// StoredAtLT defines the stored-at cutoff for current and prior snapshots.
27+
StoredAtLT time.Time
28+
29+
// Metering values
30+
31+
Customer billing.CustomerOverrideWithDetails
32+
FeatureMeter feature.FeatureMeter
2533
}
2634

27-
func (i GetDetailedLinesForUsageInput) Validate() error {
35+
func (i GetDetailedRatingForUsageInput) Validate() error {
2836
if err := i.Charge.Validate(); err != nil {
2937
return fmt.Errorf("charge: %w", err)
3038
}
@@ -37,11 +45,11 @@ func (i GetDetailedLinesForUsageInput) Validate() error {
3745
return fmt.Errorf("feature meter is required")
3846
}
3947

48+
period := i.Charge.Intent.ServicePeriod
4049
if i.ServicePeriodTo.IsZero() {
4150
return fmt.Errorf("service period to is required")
4251
}
4352

44-
period := i.Charge.Intent.ServicePeriod
4553
if !i.ServicePeriodTo.After(period.From) {
4654
return fmt.Errorf("service period to must be after charge service period from")
4755
}
@@ -54,46 +62,43 @@ func (i GetDetailedLinesForUsageInput) Validate() error {
5462
return fmt.Errorf("stored at lt is required")
5563
}
5664

57-
if err := i.PriorRuns.Validate(); err != nil {
58-
return fmt.Errorf("prior runs: %w", err)
59-
}
60-
61-
for idx, run := range i.PriorRuns {
62-
if !run.DetailedLines.IsPresent() {
65+
for idx, run := range i.Charge.Realizations {
66+
if run.ServicePeriodTo.Before(i.ServicePeriodTo) && !run.DetailedLines.IsPresent() {
6367
return fmt.Errorf("prior runs[%d]: detailed lines must be expanded", idx)
6468
}
6569
}
6670

6771
return nil
6872
}
6973

70-
type GetRatingForUsageResult struct {
74+
type GetDetailedRatingForUsageResult struct {
7175
billingrating.GenerateDetailedLinesResult
76+
// Quantity is the current run's meter value between [Charge.Intent.ServicePeriod.From, ServicePeriodTo)
77+
// capped at StoredAtLT.
7278
Quantity alpacadecimal.Decimal
7379
}
7480

75-
// GetDetailedLinesForUsage returns the rated detailed lines together with the metered quantity snapshot
76-
// used to compute them. Prefer GetTotalsForUsage when only totals are needed because it is faster.
77-
func (s *service) GetDetailedLinesForUsage(ctx context.Context, in GetDetailedLinesForUsageInput) (GetRatingForUsageResult, error) {
81+
func (s *service) GetDetailedRatingForUsage(ctx context.Context, in GetDetailedRatingForUsageInput) (GetDetailedRatingForUsageResult, error) {
7882
if err := in.Validate(); err != nil {
79-
return GetRatingForUsageResult{}, err
83+
return GetDetailedRatingForUsageResult{}, err
8084
}
8185

82-
servicePeriod := in.Charge.Intent.ServicePeriod
83-
servicePeriod.To = in.ServicePeriodTo
84-
85-
snapshotQuantity, err := s.snapshotQuantity(ctx, snapshotQuantityInput{
86-
Customer: in.Customer.Customer,
87-
FeatureMeter: in.FeatureMeter,
88-
ServicePeriod: servicePeriod,
89-
StoredAtLT: in.StoredAtLT,
86+
currentQuantity, err := s.getQuantityForUsage(ctx, getQuantityForUsageInput{
87+
Charge: in.Charge,
88+
Customer: in.Customer,
89+
FeatureMeter: in.FeatureMeter,
90+
ServicePeriodTo: in.ServicePeriodTo,
91+
StoredAtLT: in.StoredAtLT,
9092
})
9193
if err != nil {
92-
return GetRatingForUsageResult{}, fmt.Errorf("get snapshot quantity: %w", err)
94+
return GetDetailedRatingForUsageResult{}, fmt.Errorf("get current quantity: %w", err)
9395
}
9496

97+
servicePeriod := in.Charge.Intent.ServicePeriod
98+
servicePeriod.To = in.ServicePeriodTo
99+
95100
var opts []billingrating.GenerateDetailedLinesOption
96-
if in.IgnoreMinimumCommitment {
101+
if in.ServicePeriodTo.Before(in.Charge.Intent.ServicePeriod.To) {
97102
opts = append(opts, billingrating.WithMinimumCommitmentIgnored())
98103
}
99104

@@ -102,19 +107,19 @@ func (s *service) GetDetailedLinesForUsage(ctx context.Context, in GetDetailedLi
102107

103108
ratingResult, err := s.ratingService.GenerateDetailedLines(usagebased.RateableIntent{
104109
Intent: intent,
105-
MeterValue: snapshotQuantity,
110+
MeterValue: currentQuantity,
106111
}, opts...)
107112
if err != nil {
108-
return GetRatingForUsageResult{}, fmt.Errorf("rating: %w", err)
113+
return GetDetailedRatingForUsageResult{}, fmt.Errorf("rating: %w", err)
109114
}
110115

111116
ratingResult.DetailedLines = withServicePeriodInDetailedLineChildUniqueReferenceIDs(
112117
ratingResult.DetailedLines,
113118
servicePeriod,
114119
)
115120

116-
return GetRatingForUsageResult{
121+
return GetDetailedRatingForUsageResult{
117122
GenerateDetailedLinesResult: ratingResult,
118-
Quantity: snapshotQuantity,
123+
Quantity: currentQuantity,
119124
}, nil
120125
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package rating
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
"github.com/alpacahq/alpacadecimal"
9+
10+
"github.com/openmeterio/openmeter/openmeter/billing"
11+
"github.com/openmeterio/openmeter/openmeter/billing/charges/usagebased"
12+
"github.com/openmeterio/openmeter/openmeter/productcatalog/feature"
13+
)
14+
15+
type getQuantityForUsageInput struct {
16+
Charge usagebased.Charge
17+
Customer billing.CustomerOverrideWithDetails
18+
FeatureMeter feature.FeatureMeter
19+
ServicePeriodTo time.Time
20+
StoredAtLT time.Time
21+
}
22+
23+
func (i getQuantityForUsageInput) Validate() error {
24+
if err := i.Charge.Validate(); err != nil {
25+
return fmt.Errorf("charge: %w", err)
26+
}
27+
28+
if i.Customer.Customer == nil {
29+
return fmt.Errorf("customer is required")
30+
}
31+
32+
if i.FeatureMeter.Meter == nil {
33+
return fmt.Errorf("feature meter is required")
34+
}
35+
36+
if i.ServicePeriodTo.IsZero() {
37+
return fmt.Errorf("service period to is required")
38+
}
39+
40+
period := i.Charge.Intent.ServicePeriod
41+
if !i.ServicePeriodTo.After(period.From) {
42+
return fmt.Errorf("service period to must be after charge service period from")
43+
}
44+
45+
if i.ServicePeriodTo.After(period.To) {
46+
return fmt.Errorf("service period to must not be after charge service period to")
47+
}
48+
49+
if i.StoredAtLT.IsZero() {
50+
return fmt.Errorf("stored at lt is required")
51+
}
52+
53+
return nil
54+
}
55+
56+
func (s *service) getQuantityForUsage(ctx context.Context, in getQuantityForUsageInput) (alpacadecimal.Decimal, error) {
57+
if err := in.Validate(); err != nil {
58+
return alpacadecimal.Zero, err
59+
}
60+
61+
servicePeriod := in.Charge.Intent.ServicePeriod
62+
servicePeriod.To = in.ServicePeriodTo
63+
64+
return s.snapshotQuantity(ctx, snapshotQuantityInput{
65+
Customer: in.Customer.Customer,
66+
FeatureMeter: in.FeatureMeter,
67+
ServicePeriod: servicePeriod,
68+
StoredAtLT: in.StoredAtLT,
69+
})
70+
}

openmeter/billing/charges/usagebased/service/rating/service.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ type Service interface {
3232
// GetTotalsForUsage returns charge totals for a usage snapshot without generating detailed lines.
3333
// Prefer this when only totals are required because it is faster than generating detailed lines.
3434
GetTotalsForUsage(ctx context.Context, in GetTotalsForUsageInput) (totals.Totals, error)
35-
// GetDetailedLinesForUsage returns rated detailed lines and the metered quantity snapshot used to compute them.
35+
// GetDetailedRatingForUsage returns rated detailed lines and the metered quantity snapshot used to compute them.
3636
// Prefer GetTotalsForUsage when only totals are required because it is faster.
37-
GetDetailedLinesForUsage(ctx context.Context, in GetDetailedLinesForUsageInput) (GetRatingForUsageResult, error)
37+
GetDetailedRatingForUsage(ctx context.Context, in GetDetailedRatingForUsageInput) (GetDetailedRatingForUsageResult, error)
3838
}
3939

4040
type service struct {

0 commit comments

Comments
 (0)