| package usagebased |
|
|
| import ( |
| "testing" |
| "time" |
|
|
| "github.com/alpacahq/alpacadecimal" |
| "github.com/samber/lo" |
| "github.com/stretchr/testify/require" |
|
|
| "github.com/openmeterio/openmeter/openmeter/billing/models/totals" |
| "github.com/openmeterio/openmeter/pkg/models" |
| "github.com/openmeterio/openmeter/pkg/timeutil" |
| ) |
|
|
| func TestRealizationRuns_MapToBillingMeteredQuantity(t *testing.T) { |
| periodStart := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) |
|
|
| tests := []struct { |
| name string |
| runs RealizationRuns |
| currentRun RealizationRun |
| wantLine float64 |
| wantPreLine float64 |
| wantErr bool |
| }{ |
| { |
| name: "first run has no pre-line period quantity", |
| currentRun: newRealizationRunForBillingMeteredQuantityTest( |
| "current", |
| RealizationRunTypePartialInvoice, |
| periodStart.Add(24*time.Hour), |
| 5, |
| ), |
| wantLine: 5, |
| wantPreLine: 0, |
| }, |
| { |
| name: "uses latest prior persisted cumulative quantity", |
| runs: RealizationRuns{ |
| newRealizationRunForBillingMeteredQuantityTest( |
| "run-1", |
| RealizationRunTypePartialInvoice, |
| periodStart.Add(24*time.Hour), |
| 5, |
| ), |
| newRealizationRunForBillingMeteredQuantityTest( |
| "run-2", |
| RealizationRunTypePartialInvoice, |
| periodStart.Add(48*time.Hour), |
| 8, |
| ), |
| }, |
| currentRun: newRealizationRunForBillingMeteredQuantityTest( |
| "current", |
| RealizationRunTypeFinalRealization, |
| periodStart.Add(72*time.Hour), |
| 20, |
| ), |
| wantLine: 12, |
| wantPreLine: 8, |
| }, |
| { |
| name: "errors when current cumulative quantity is below prior billed quantity", |
| runs: RealizationRuns{ |
| newRealizationRunForBillingMeteredQuantityTest( |
| "run-1", |
| RealizationRunTypePartialInvoice, |
| periodStart.Add(24*time.Hour), |
| 10, |
| ), |
| }, |
| currentRun: newRealizationRunForBillingMeteredQuantityTest( |
| "current", |
| RealizationRunTypeFinalRealization, |
| periodStart.Add(48*time.Hour), |
| 5, |
| ), |
| wantErr: true, |
| }, |
| { |
| name: "ignores deleted prior runs", |
| runs: RealizationRuns{ |
| func() RealizationRun { |
| run := newRealizationRunForBillingMeteredQuantityTest( |
| "deleted-run", |
| RealizationRunTypePartialInvoice, |
| periodStart.Add(48*time.Hour), |
| 18, |
| ) |
| run.DeletedAt = &periodStart |
| return run |
| }(), |
| newRealizationRunForBillingMeteredQuantityTest( |
| "run-1", |
| RealizationRunTypePartialInvoice, |
| periodStart.Add(24*time.Hour), |
| 5, |
| ), |
| }, |
| currentRun: newRealizationRunForBillingMeteredQuantityTest( |
| "current", |
| RealizationRunTypeFinalRealization, |
| periodStart.Add(72*time.Hour), |
| 20, |
| ), |
| wantLine: 15, |
| wantPreLine: 5, |
| }, |
| { |
| name: "ignores invalid unsupported credit note prior runs", |
| runs: RealizationRuns{ |
| newRealizationRunForBillingMeteredQuantityTest( |
| "run-1", |
| RealizationRunTypePartialInvoice, |
| periodStart.Add(24*time.Hour), |
| 5, |
| ), |
| newRealizationRunForBillingMeteredQuantityTest( |
| "invalid-run", |
| RealizationRunTypeInvalidDueToUnsupportedCreditNote, |
| periodStart.Add(48*time.Hour), |
| 18, |
| ), |
| }, |
| currentRun: newRealizationRunForBillingMeteredQuantityTest( |
| "current", |
| RealizationRunTypeFinalRealization, |
| periodStart.Add(72*time.Hour), |
| 20, |
| ), |
| wantLine: 15, |
| wantPreLine: 5, |
| }, |
| } |
|
|
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| billingMeteredQuantity, err := tt.runs.MapToBillingMeteredQuantity(tt.currentRun) |
| if tt.wantErr { |
| require.Error(t, err) |
| return |
| } |
|
|
| require.NoError(t, err) |
| require.Equal(t, tt.wantLine, billingMeteredQuantity.LinePeriod.InexactFloat64()) |
| require.Equal(t, tt.wantPreLine, billingMeteredQuantity.PreLinePeriod.InexactFloat64()) |
| }) |
| } |
| } |
|
|
| func TestRealizationRunType_IsVoidedBillingHistory(t *testing.T) { |
| require.False(t, RealizationRunTypeFinalRealization.IsVoidedBillingHistory()) |
| require.False(t, RealizationRunTypePartialInvoice.IsVoidedBillingHistory()) |
| require.True(t, RealizationRunTypeInvalidDueToUnsupportedCreditNote.IsVoidedBillingHistory()) |
| } |
|
|
| func TestRealizationRun_InvalidUnsupportedCreditNoteKeepsInitialType(t *testing.T) { |
| run := newRealizationRunForBillingMeteredQuantityTest( |
| "invalid-run", |
| RealizationRunTypeFinalRealization, |
| time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC), |
| 10, |
| ) |
| run.Type = RealizationRunTypeInvalidDueToUnsupportedCreditNote |
|
|
| require.NoError(t, run.Validate()) |
| require.Equal(t, RealizationRunTypeFinalRealization, run.InitialType) |
| require.True(t, run.IsVoidedBillingHistory()) |
| } |
|
|
| func TestRealizationRuns_SumSkipsVoidedBillingHistory(t *testing.T) { |
| periodStart := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) |
| deletedAt := periodStart.Add(time.Hour) |
|
|
| deletedRun := newRealizationRunForBillingMeteredQuantityTest( |
| "deleted-run", |
| RealizationRunTypePartialInvoice, |
| periodStart.Add(24*time.Hour), |
| 100, |
| ) |
| deletedRun.DeletedAt = &deletedAt |
|
|
| invalidRun := newRealizationRunForBillingMeteredQuantityTest( |
| "invalid-run", |
| RealizationRunTypeInvalidDueToUnsupportedCreditNote, |
| periodStart.Add(48*time.Hour), |
| 100, |
| ) |
|
|
| effectiveRun := newRealizationRunForBillingMeteredQuantityTest( |
| "effective-run", |
| RealizationRunTypeFinalRealization, |
| periodStart.Add(72*time.Hour), |
| 7, |
| ) |
|
|
| require.Equal(t, float64(7), RealizationRuns{deletedRun, invalidRun, effectiveRun}.Sum().Total.InexactFloat64()) |
| } |
|
|
| func TestRealizationRuns_Latest(t *testing.T) { |
| periodStart := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) |
|
|
| t.Run("empty", func(t *testing.T) { |
| _, ok := RealizationRuns{}.Latest() |
|
|
| require.False(t, ok) |
| }) |
|
|
| t.Run("latest service period end", func(t *testing.T) { |
| run, ok := RealizationRuns{ |
| newRealizationRunForBillingMeteredQuantityTest( |
| "run-1", |
| RealizationRunTypePartialInvoice, |
| periodStart.Add(24*time.Hour), |
| 1, |
| ), |
| newRealizationRunForBillingMeteredQuantityTest( |
| "run-2", |
| RealizationRunTypePartialInvoice, |
| periodStart.Add(48*time.Hour), |
| 1, |
| ), |
| }.Latest() |
|
|
| require.True(t, ok) |
| require.Equal(t, "run-2", run.ID.ID) |
| }) |
|
|
| t.Run("latest created at wins same service period end", func(t *testing.T) { |
| periodEnd := periodStart.Add(24 * time.Hour) |
| older := newRealizationRunForBillingMeteredQuantityTest( |
| "older", |
| RealizationRunTypePartialInvoice, |
| periodEnd, |
| 1, |
| ) |
| newer := newRealizationRunForBillingMeteredQuantityTest( |
| "newer", |
| RealizationRunTypePartialInvoice, |
| periodEnd, |
| 1, |
| ) |
| newer.CreatedAt = older.CreatedAt.Add(time.Hour) |
|
|
| run, ok := RealizationRuns{newer, older}.Latest() |
|
|
| require.True(t, ok) |
| require.Equal(t, "newer", run.ID.ID) |
| }) |
| } |
|
|
| func TestRealizationRuns_GetByLineID(t *testing.T) { |
| lineID := "line-1" |
| otherLineID := "line-2" |
|
|
| runs := RealizationRuns{ |
| { |
| RealizationRunBase: RealizationRunBase{ |
| ID: RealizationRunID{Namespace: "namespace", ID: "run-1"}, |
| LineID: &otherLineID, |
| }, |
| }, |
| { |
| RealizationRunBase: RealizationRunBase{ |
| ID: RealizationRunID{Namespace: "namespace", ID: "run-2"}, |
| LineID: &lineID, |
| }, |
| }, |
| } |
|
|
| run, err := runs.GetByLineID(lineID) |
| require.NoError(t, err) |
| require.Equal(t, "run-2", run.ID.ID) |
|
|
| _, err = runs.GetByLineID("missing-line") |
| require.ErrorContains(t, err, "realization run not found") |
| } |
|
|
| func TestRealizationRuns_BisectByTimestamp(t *testing.T) { |
| periodStart := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) |
| servicePeriod := timeutil.ClosedPeriod{ |
| From: periodStart, |
| To: periodStart.Add(96 * time.Hour), |
| } |
| at := periodStart.Add(36 * time.Hour) |
|
|
| deletedAt := periodStart.Add(time.Hour) |
| runs := RealizationRuns{ |
| func() RealizationRun { |
| run := newRealizationRunForBillingMeteredQuantityTest( |
| "deleted-run", |
| RealizationRunTypePartialInvoice, |
| periodStart.Add(96*time.Hour), |
| 1, |
| ) |
| run.DeletedAt = &deletedAt |
| return run |
| }(), |
| newRealizationRunForBillingMeteredQuantityTest( |
| "after-run", |
| RealizationRunTypePartialInvoice, |
| periodStart.Add(72*time.Hour), |
| 1, |
| ), |
| newRealizationRunForBillingMeteredQuantityTest( |
| "before-run", |
| RealizationRunTypePartialInvoice, |
| periodStart.Add(24*time.Hour), |
| 1, |
| ), |
| newRealizationRunForBillingMeteredQuantityTest( |
| "containing-run", |
| RealizationRunTypePartialInvoice, |
| periodStart.Add(48*time.Hour), |
| 1, |
| ), |
| } |
|
|
| before, containingOrAfter := runs.BisectByTimestamp(servicePeriod, at) |
|
|
| require.Equal(t, []string{"before-run"}, lo.Map(before, func(run RealizationRun, _ int) string { |
| return run.ID.ID |
| })) |
| require.Equal(t, []string{"containing-run", "after-run"}, lo.Map(containingOrAfter, func(run RealizationRun, _ int) string { |
| return run.ID.ID |
| })) |
|
|
| before, containingOrAfter = runs.BisectByTimestamp(servicePeriod, periodStart.Add(48*time.Hour)) |
|
|
| require.Equal(t, []string{"before-run", "containing-run"}, lo.Map(before, func(run RealizationRun, _ int) string { |
| return run.ID.ID |
| })) |
| require.Equal(t, []string{"after-run"}, lo.Map(containingOrAfter, func(run RealizationRun, _ int) string { |
| return run.ID.ID |
| })) |
| } |
|
|
| func newRealizationRunForBillingMeteredQuantityTest(id string, typ RealizationRunType, servicePeriodTo time.Time, meteredQuantity int64) RealizationRun { |
| return RealizationRun{ |
| RealizationRunBase: RealizationRunBase{ |
| ID: RealizationRunID{ |
| Namespace: "namespace", |
| ID: id, |
| }, |
| ManagedModel: models.ManagedModel{ |
| CreatedAt: servicePeriodTo.Add(-time.Hour), |
| UpdatedAt: servicePeriodTo.Add(-time.Hour), |
| }, |
| FeatureID: "feature-1", |
| Type: typ, |
| InitialType: typ, |
| StoredAtLT: servicePeriodTo, |
| ServicePeriodTo: servicePeriodTo, |
| MeteredQuantity: alpacadecimal.NewFromInt(meteredQuantity), |
| Totals: totals.Totals{ |
| Amount: alpacadecimal.NewFromInt(meteredQuantity), |
| Total: alpacadecimal.NewFromInt(meteredQuantity), |
| }, |
| }, |
| } |
| } |
|
|