Skip to content

Commit f20fe83

Browse files
committed
chore: prior run values
1 parent 33fd73b commit f20fe83

16 files changed

Lines changed: 554 additions & 206 deletions

File tree

.agents/skills/charges/SKILL.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,9 @@ Important behavior:
459459
- stored-at filtering uses `stored_at < cutoff`
460460
- the cutoff is the run's `StoredAtLT`
461461
- the service-period end is expected to behave as exclusive in lifecycle tests
462+
- `GetDetailedRatingForUsage(...)` owns the current-run filtering rule: only realization runs with `ServicePeriodTo < input.ServicePeriodTo` are prior runs; a current run already present on the charge must be ignored rather than stripped by mutating the charge in the caller
463+
- minimum commitment is final-only for usage-based snapshots; detailed rating ignores it when the current service-period end is before the charge intent service-period end
464+
- realtime/current totals should ignore minimum commitment before the charge intent service-period end and include it at/after the service-period end
462465

463466
This means late-arriving events can become eligible in later advances if their `stored_at` was previously too new but later falls before the next cutoff.
464467

@@ -511,6 +514,7 @@ Use these conventions for lifecycle tests:
511514
- use `streaming/testutils.WithStoredAt(...)` to simulate late events
512515
- when testing stored-at cutoffs, remember the predicate is exclusive: an event with `stored_at == StoredAtLT` is excluded, and an event with `stored_at` before `StoredAtLT` is included
513516
- when testing service-period cutoffs, remember the event-time window is half-open: an event with `event_time == ServicePeriodTo` is excluded
517+
- prefer `streamingtestutils.NewMockStreamingConnector(...)` plus the real billing rating service when a usage-based rating test should exercise production quantity lookup, pricing, discounts, or commitments end-to-end
514518
- prefer `clock.FreezeTime(...)` for exact `StoredAtLT` / `AllocateAt` assertions
515519
- rely on the default billing profile unless the test explicitly needs customer-specific override behavior
516520
- for credit-only charges (usage-based or flat fee), `Create(...)` itself may return an already-advanced charge — assert the returned charge's status, do not assume it will be `created`

openmeter/billing/charges/usagebased/rating.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ var _ rating.StandardLineAccessor = (*RateableIntent)(nil)
1616
type RateableIntent struct {
1717
Intent
1818

19+
ServicePeriod timeutil.ClosedPeriod
1920
MeterValue alpacadecimal.Decimal
2021
CreditsApplied billing.CreditsApplied
2122
}

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

Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -248,16 +248,15 @@ func (s *CreditThenInvoiceStateMachine) startInvoiceCreatedRun(
248248
}
249249

250250
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,
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+
CreditAllocation: usagebasedrun.CreditAllocationAvailable,
259+
CurrencyCalculator: s.CurrencyCalculator,
261260
})
262261
if err != nil {
263262
return err
@@ -275,12 +274,6 @@ func (s *CreditThenInvoiceStateMachine) StartFinalInvoiceRun(ctx context.Context
275274
return s.startInvoiceCreatedRun(ctx, input, usagebased.RealizationRunTypeFinalRealization)
276275
}
277276

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-
284277
func resolveInvoiceCreatedTrigger(charge usagebased.Charge, billedPeriod timeutil.ClosedPeriod) meta.Trigger {
285278
if meta.NormalizeTimestamp(billedPeriod.To).Equal(meta.NormalizeTimestamp(charge.Intent.ServicePeriod.To)) {
286279
return meta.TriggerFinalInvoiceCreated
@@ -309,17 +302,15 @@ func (s *CreditThenInvoiceStateMachine) SnapshotInvoiceUsage(ctx context.Context
309302

310303
storedAtLT := meta.NormalizeTimestamp(currentRun.StoredAtLT)
311304

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),
305+
ratingResult, err := s.Rater.GetDetailedRatingForUsage(ctx, usagebasedrating.GetDetailedRatingForUsageInput{
306+
Charge: s.Charge,
307+
StoredAtLT: storedAtLT,
308+
ServicePeriodTo: currentRun.ServicePeriodTo,
309+
Customer: 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("get detailed rating for usage: %w", err)
323314
}
324315

325316
currentTotals := ratingResult.Totals.RoundToPrecision(s.CurrencyCalculator)

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: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -184,16 +184,15 @@ func (s *CreditsOnlyStateMachine) FinalizeRealizationRun(ctx context.Context) er
184184

185185
storedAtLT := meta.NormalizeTimestamp(currentRun.StoredAtLT)
186186

187-
ratingResult, err := s.Rater.GetDetailedLinesForUsage(ctx, usagebasedrating.GetDetailedLinesForUsageInput{
187+
ratingResult, err := s.Rater.GetDetailedRatingForUsage(ctx, usagebasedrating.GetDetailedRatingForUsageInput{
188188
Charge: s.Charge,
189-
PriorRuns: s.Charge.Realizations.Without(currentRun.ID),
189+
StoredAtLT: storedAtLT,
190+
ServicePeriodTo: currentRun.ServicePeriodTo,
190191
Customer: s.CustomerOverride,
191192
FeatureMeter: s.FeatureMeter,
192-
ServicePeriodTo: currentRun.ServicePeriodTo,
193-
StoredAtLT: storedAtLT,
194193
})
195194
if err != nil {
196-
return fmt.Errorf("get rating for usage: %w", err)
195+
return fmt.Errorf("get detailed rating for usage: %w", err)
197196
}
198197

199198
currentTotals := ratingResult.Totals.RoundToPrecision(s.CurrencyCalculator)
@@ -219,12 +218,18 @@ func (s *CreditsOnlyStateMachine) FinalizeRealizationRun(ctx context.Context) er
219218
}
220219
currentRun.DetailedLines = mo.Some(runDetailedLines)
221220

222-
if _, err := s.Adapter.UpdateRealizationRun(ctx, usagebased.UpdateRealizationRunInput{
221+
currentRunBase, err := s.Adapter.UpdateRealizationRun(ctx, usagebased.UpdateRealizationRunInput{
223222
ID: currentRun.ID,
224223
StoredAtLT: mo.Some(storedAtLT),
225224
MeteredQuantity: mo.Some(ratingResult.Quantity),
226225
Totals: mo.Some(currentTotals),
227-
}); err != nil {
226+
})
227+
if err != nil {
228+
return fmt.Errorf("update realization run: %w", err)
229+
}
230+
currentRun.RealizationRunBase = currentRunBase
231+
232+
if err := s.Charge.Realizations.SetRealizationRun(currentRun); err != nil {
228233
return fmt.Errorf("update realization run: %w", err)
229234
}
230235

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,14 @@ func (s *service) GetCurrentTotals(ctx context.Context, input usagebased.GetCurr
4848
return usagebased.GetCurrentTotalsResult{}, err
4949
}
5050

51+
now := clock.Now()
52+
5153
dueTotals, err := s.rater.GetTotalsForUsage(ctx, usagebasedrating.GetTotalsForUsageInput{
52-
Charge: charge,
53-
Customer: customerOverride,
54-
FeatureMeter: featureMeter,
55-
StoredAtLT: clock.Now(),
54+
Charge: charge,
55+
Customer: customerOverride,
56+
FeatureMeter: featureMeter,
57+
StoredAtLT: now,
58+
IgnoreMinimumCommitment: now.Before(charge.Intent.ServicePeriod.To),
5659
})
5760
if err != nil {
5861
return usagebased.GetCurrentTotalsResult{}, fmt.Errorf("get totals for usage: %w", err)

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,11 @@ func (s *service) expandChargesUsage(ctx context.Context, namespace string, char
144144

145145
var dueTotals totals.Totals
146146
dueTotals, err = s.rater.GetTotalsForUsage(ctx, usagebasedrating.GetTotalsForUsageInput{
147-
Charge: charge,
148-
Customer: customerOverridesById[charge.GetCustomerID()],
149-
FeatureMeter: featureMeter,
150-
StoredAtLT: storedAt,
147+
Charge: charge,
148+
Customer: customerOverridesById[charge.GetCustomerID()],
149+
FeatureMeter: featureMeter,
150+
StoredAtLT: storedAt,
151+
IgnoreMinimumCommitment: storedAt.Before(charge.Intent.ServicePeriod.To),
151152
})
152153
if err != nil {
153154
err = fmt.Errorf("get totals for charge %s: %w", charge.ID, err)

0 commit comments

Comments
 (0)