diff --git a/.agents/skills/charges/SKILL.md b/.agents/skills/charges/SKILL.md index 6ff36f783c..7f4a8735a3 100644 --- a/.agents/skills/charges/SKILL.md +++ b/.agents/skills/charges/SKILL.md @@ -83,9 +83,9 @@ Important types: - `AdvanceAfter` - `usagebased.RealizationRunBase` stores: - `Type` - - `AsOf` - - `CollectionEnd` - - `MeterValue` + - `StoredAtLT` + - `ServicePeriodTo` + - `MeteredQuantity` - `Totals` - `usagebased.RealizationRun` can expand: - `DetailedLines` @@ -159,7 +159,7 @@ Rules: - `meta.NormalizeClosedPeriod(...)` and `Intent.Normalized()` helpers are the domain-level normalization entrypoints - normalize intent timestamps before validation and before any derived calculation that depends on durations or boundaries - flat-fee proration must use normalized periods, otherwise sub-second inputs can change `AmountAfterProration` -- for usage-based lifecycle timestamps (`AdvanceAfter`, `AsOf`, `CollectionEnd`, `storedAtOffset`), normalize the computed timestamp before persisting it or handing it to downstream persistence callbacks +- for usage-based lifecycle timestamps (`AdvanceAfter`, `StoredAtLT`, `ServicePeriodTo`), normalize the computed timestamp before persisting it or handing it to downstream persistence callbacks Important timestamp surfaces: @@ -170,15 +170,15 @@ Important timestamp surfaces: - `usagebased.Intent.InvoiceAt` - `flatfee.State.AdvanceAfter` - `usagebased.State.AdvanceAfter` -- `usagebased.CreateRealizationRunInput.AsOf` -- `usagebased.CreateRealizationRunInput.CollectionEnd` -- `usagebased.UpdateRealizationRunInput.AsOf` +- `usagebased.CreateRealizationRunInput.StoredAtLT` +- `usagebased.CreateRealizationRunInput.ServicePeriodTo` +- `usagebased.UpdateRealizationRunInput.StoredAtLT` Placement guidance: - prefer domain-side normalization when constructing or mutating intents and state (`Intent.Normalized()`, state-machine transition logic, temporary patch remap) - keep a persistence backstop in shared write helpers such as `charges/models/chargemeta` -- in adapters, normalize at the actual write setter (`SetInvoiceAt(...)`, `SetAsof(...)`, `SetCollectionEnd(...)`, `SetOrClearAdvanceAfter(...)`) rather than rewriting the whole input object at the top of the adapter method +- in adapters, normalize at the actual write setter (`SetInvoiceAt(...)`, `SetStoredAtLt(...)`, `SetServicePeriodTo(...)`, `SetOrClearAdvanceAfter(...)`) rather than rewriting the whole input object at the top of the adapter method - do not add redundant `.UTC()` calls after `meta.NormalizeTimestamp(...)`; the helper already returns UTC ## Currency Normalization @@ -431,13 +431,17 @@ The collection-period logic is central to this package. Rules: - `usagebased.InternalCollectionPeriod` is `1 minute` -- `StartFinalRealizationRun(...)` computes `storedAtOffset = clock.Now() - InternalCollectionPeriod` -- the realization run persists `CollectionEnd` -- waiting logic must use the persisted run `CollectionEnd`, not a recomputed value -- `AdvanceAfterCollectionPeriodEnd(...)` sets `AdvanceAfter = CollectionEnd + InternalCollectionPeriod` -- `IsAfterCollectionPeriod(...)` checks `clock.Now() >= CollectionEnd + InternalCollectionPeriod` - -`GetCollectionPeriodEnd(...)` currently uses: +- `StoredAtLT` is the exclusive stored-at query cap for the run (`stored_at < StoredAtLT`) +- `ServicePeriodTo` is the exclusive event-time upper bound for the run (`event_time < ServicePeriodTo`) +- final usage-based runs use the charge intent's service-period end as `ServicePeriodTo` +- final usage-based runs use the charge service-period end plus the billing profile collection interval as `StoredAtLT` +- partial invoice runs use the standard line period end as both `ServicePeriodTo` and `StoredAtLT` +- waiting logic must use the persisted run `StoredAtLT`, not a recomputed value +- `AdvanceAfterCollectionPeriodEnd(...)` sets `AdvanceAfter = StoredAtLT + InternalCollectionPeriod` +- `IsAfterCollectionPeriod(...)` checks `clock.Now() >= StoredAtLT + InternalCollectionPeriod` +- usage-based standard invoice lines should set `OverrideCollectionPeriodEnd = StoredAtLT + InternalCollectionPeriod` so invoice collection waits for the same internal buffer as the charge state machine + +Final-run `StoredAtLT` currently uses: - `CustomerOverride.MergedProfile.WorkflowConfig.Collection.Interval` - added to `Charge.Intent.ServicePeriod.To` @@ -450,9 +454,10 @@ Usage-based quantity is derived through `snapshotQuantity(...)`. Important behavior: -- query window uses the charge service period +- query window starts at the charge intent's service-period start +- query window ends at the run's `ServicePeriodTo` - stored-at filtering uses `stored_at < cutoff` -- the cutoff is the current `storedAtOffset` +- the cutoff is the run's `StoredAtLT` - the service-period end is expected to behave as exclusive in lifecycle tests 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. @@ -464,7 +469,7 @@ Realization runs are the persisted checkpoint for collection progress. Important rules: - the first final-realization advance creates a run -- `CollectionEnd` must be persisted on the run and mapped back into the domain model +- `StoredAtLT`, `ServicePeriodTo`, and `MeteredQuantity` must be persisted on the run and mapped back into the domain model - `CurrentRealizationRunID` points at the active run while waiting/finalizing - finalization must clear `CurrentRealizationRunID` @@ -504,12 +509,15 @@ Use these conventions for lifecycle tests: - if a returned charge is non-`nil`, at minimum match its status to the DB-loaded charge - install usage-based handler callbacks only in the subtests that expect them (handler is reset in `TearDownTest`) - use `streaming/testutils.WithStoredAt(...)` to simulate late events -- prefer `clock.FreezeTime(...)` for exact `AsOf` / `AllocateAt` assertions +- 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 +- when testing service-period cutoffs, remember the event-time window is half-open: an event with `event_time == ServicePeriodTo` is excluded +- prefer `clock.FreezeTime(...)` for exact `StoredAtLT` / `AllocateAt` assertions - rely on the default billing profile unless the test explicitly needs customer-specific override behavior - 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` - for flat fee credit-only tests, use `mustAdvanceFlatFeeCharges(...)` helper — it filters the advance result to flat fee charges only - flat fee credit-only handler callbacks (`onCreditsOnlyUsageAccrued`) must return credit allocations that sum to the input `AmountToAllocate` - when testing timestamp truncation, use sub-second fixtures and assert the persisted charge/run fields are second-aligned after create/advance +- `time.Time` fields on domain models are value typed; use `s.False(ts.IsZero())` instead of `s.NotNil(ts)` when asserting they are populated - cover the temporary shrink/extend remap path as well; it synthesizes new intents and must normalize the replacement period ends before re-create Test suite teardown: @@ -549,7 +557,7 @@ When changing usage-based charges: - confirm whether the change belongs in the facade, usage-based service, state machine, or adapter - preserve the `nil means noop` contract for `AdvanceCharge(...)` - preserve merged-profile based collection-period resolution -- keep `CollectionEnd` persisted on realization runs +- keep `StoredAtLT`, `ServicePeriodTo`, and `MeteredQuantity` persisted on realization runs - keep the `stored_at < cutoff` behavior explicit in tests - update lifecycle tests if late-event visibility changes When changing flat-fee charges: diff --git a/.agents/skills/ent/SKILL.md b/.agents/skills/ent/SKILL.md index 49c5c19c22..33b58c4dff 100644 --- a/.agents/skills/ent/SKILL.md +++ b/.agents/skills/ent/SKILL.md @@ -32,6 +32,7 @@ After any schema change, regenerate with `make generate` before running tests. - **Soft-delete unique indexes** include `deleted_at` in the unique constraint (e.g., `index.Fields("namespace", "key", "deleted_at").Unique()`) — always filter with `Where(db.DeletedAtIsNil())` in queries. - **Foreign keys** use `char(26)` schema type to match ULID IDs. - **Cascade deletes** use `entsql.OnDelete(entsql.Cascade)` on the parent edge. +- **PostgreSQL identifier length** is 63 bytes by default (PostgreSQL docs, “Lexical Structure” / `NAMEDATALEN`). Long Ent-generated table, index, and FK names can truncate and collide even when their full names differ. When a schema/entity/edge name is verbose, proactively shorten generated FK symbols with `StorageKey(edge.Symbol("..."))` and shorten index names with `StorageKey("...")` before generating migrations. - **JSONB fields** use `entutils.JSONStringValueScanner` — see `openmeter/ent/schema/llmcostprice.go`. - **Non-empty strings at the DB layer**: `field.String(...).NotEmpty()` enforces Ent-side validation, but Atlas may still diff only `SET NOT NULL` for existing tables. If the database must reject empty strings too, add an explicit `entsql.Checks(...)` annotation in the schema or mixin alongside `NotEmpty()`. diff --git a/openmeter/billing/charges/service/featureid_test.go b/openmeter/billing/charges/service/featureid_test.go index 5a8400024a..133fbca5ca 100644 --- a/openmeter/billing/charges/service/featureid_test.go +++ b/openmeter/billing/charges/service/featureid_test.go @@ -220,7 +220,7 @@ func (s *ChargeFeatureIDTestSuite) TestUsageBasedActivationRecalculatesFeatureID s.Equal(featureV2.ID, finalCharge.State.FeatureID) s.Len(finalCharge.Realizations, 1) s.Equal(featureV2.ID, finalCharge.Realizations[0].FeatureID) - s.True(alpacadecimal.NewFromInt(7).Equal(finalCharge.Realizations[0].MeterValue)) + s.True(alpacadecimal.NewFromInt(7).Equal(finalCharge.Realizations[0].MeteredQuantity)) } func (s *ChargeFeatureIDTestSuite) installMeters(ctx context.Context, meters ...meter.Meter) { diff --git a/openmeter/billing/charges/service/invoicable_test.go b/openmeter/billing/charges/service/invoicable_test.go index 6568902964..7253e95c85 100644 --- a/openmeter/billing/charges/service/invoicable_test.go +++ b/openmeter/billing/charges/service/invoicable_test.go @@ -466,8 +466,7 @@ func (s *InvoicableChargesTestSuite) TestUsageBasedCreditOnlyLifecycle() { finalAdvanceAt := datetime.MustParseTimeInLocation(s.T(), "2026-02-03T00:01:00Z", time.UTC).AsTime() // These are explicit cutoff timestamps rather than computed values so the test asserts the // one-minute internal collection period boundary directly. - firstStoredAtOffset := datetime.MustParseTimeInLocation(s.T(), "2026-02-01T11:59:00Z", time.UTC).AsTime() - finalStoredAtOffset := datetime.MustParseTimeInLocation(s.T(), "2026-02-03T00:00:00Z", time.UTC).AsTime() + finalStoredAtLT := datetime.MustParseTimeInLocation(s.T(), "2026-02-03T00:00:00Z", time.UTC).AsTime() expectedCollectionEnd := datetime.MustParseTimeInLocation(s.T(), "2026-02-03T00:00:00Z", time.UTC).AsTime() apiRequestsTotal := s.SetupApiRequestsTotalFeature(ctx, ns) @@ -611,15 +610,15 @@ func (s *InvoicableChargesTestSuite) TestUsageBasedCreditOnlyLifecycle() { meterSlug, 3, datetime.MustParseTimeInLocation(s.T(), "2026-01-15T02:00:00Z", time.UTC).AsTime(), - streamingtestutils.WithStoredAt(datetime.MustParseTimeInLocation(s.T(), "2026-02-01T12:00:00Z", time.UTC).AsTime()), + streamingtestutils.WithStoredAt(finalStoredAtLT), ) // When advancing the usage-based charge. advancedCharge := s.mustAdvanceSingleUsageBasedCharge(ctx, cust.GetID()) usageBasedFromDB := s.mustGetUsageBasedChargeByID(usageBasedChargeID) - // Then a new run is added, only the first two events are considered, totals are persisted, - // stored_at uses current time minus the internal collection period, and the start callback receives $3. + // Then a new run is added, only events before the exclusive stored_at cutoff are considered, + // totals are persisted, and the start callback receives $3. s.Require().NotNil(advancedCharge) s.Equal(usageBasedFromDB.Status, advancedCharge.Status) s.Equal(usagebased.StatusActiveFinalRealizationWaitingForCollection, usageBasedFromDB.Status) @@ -630,10 +629,10 @@ func (s *InvoicableChargesTestSuite) TestUsageBasedCreditOnlyLifecycle() { currentRun, err := usageBasedFromDB.Realizations.GetByID(*usageBasedFromDB.State.CurrentRealizationRunID) s.NoError(err) - s.True(firstStoredAtOffset.Equal(currentRun.AsOf)) - s.NotNil(currentRun.CollectionEnd) - s.True(expectedCollectionEnd.Equal(currentRun.CollectionEnd.UTC())) - s.Equal(float64(3), currentRun.MeterValue.InexactFloat64()) + s.True(finalStoredAtLT.Equal(currentRun.StoredAtLT)) + s.False(currentRun.StoredAtLT.IsZero()) + s.True(expectedCollectionEnd.Equal(currentRun.StoredAtLT.UTC())) + s.Equal(float64(3), currentRun.MeteredQuantity.InexactFloat64()) s.RequireTotals(billingtest.ExpectedTotals{ Amount: 3, CreditsTotal: 3, @@ -644,7 +643,7 @@ func (s *InvoicableChargesTestSuite) TestUsageBasedCreditOnlyLifecycle() { s.Len(startedCallbacks, 1) s.Equal(float64(3), startedCallbacks[0].Input.AmountToAllocate.InexactFloat64()) s.Equal(usagebased.RealizationRunTypeFinalRealization, startedCallbacks[0].Input.Run.Type) - s.True(firstStoredAtOffset.Equal(startedCallbacks[0].Input.AllocateAt)) + s.True(finalStoredAtLT.Equal(startedCallbacks[0].Input.AllocateAt)) }) s.Run("#3.2 second realization advance is noop", func() { @@ -715,8 +714,8 @@ func (s *InvoicableChargesTestSuite) TestUsageBasedCreditOnlyLifecycle() { advancedCharge := s.mustAdvanceSingleUsageBasedCharge(ctx, cust.GetID()) usageBasedFromDB := s.mustGetUsageBasedChargeByID(usageBasedChargeID) - // Then the previously late $3 event and the new $5 event are both included, - // the finalization callback receives incremental $8, totals are updated to $11, + // Then the new $5 event is included, + // the finalization callback receives incremental $5, totals are updated to $8, // and the charge becomes final. s.Require().NotNil(advancedCharge) s.Equal(usageBasedFromDB.Status, advancedCharge.Status) @@ -726,22 +725,22 @@ func (s *InvoicableChargesTestSuite) TestUsageBasedCreditOnlyLifecycle() { s.Nil(usageBasedFromDB.State.AdvanceAfter) finalRun := usageBasedFromDB.Realizations[0] - s.True(finalStoredAtOffset.Equal(finalRun.AsOf)) - s.NotNil(finalRun.CollectionEnd) - s.True(expectedCollectionEnd.Equal(finalRun.CollectionEnd.UTC())) - s.Equal(float64(11), finalRun.MeterValue.InexactFloat64()) + s.True(finalStoredAtLT.Equal(finalRun.StoredAtLT)) + s.False(finalRun.StoredAtLT.IsZero()) + s.True(expectedCollectionEnd.Equal(finalRun.StoredAtLT.UTC())) + s.Equal(float64(8), finalRun.MeteredQuantity.InexactFloat64()) s.RequireTotals(billingtest.ExpectedTotals{ - Amount: 11, - CreditsTotal: 11, + Amount: 8, + CreditsTotal: 8, }, finalRun.Totals) s.Len(finalRun.CreditsAllocated, 2) s.Equal(float64(3), finalRun.CreditsAllocated[0].Amount.InexactFloat64()) - s.Equal(float64(8), finalRun.CreditsAllocated[1].Amount.InexactFloat64()) + s.Equal(float64(5), finalRun.CreditsAllocated[1].Amount.InexactFloat64()) s.Len(finalizedCallbacks, 1) - s.Equal(float64(8), finalizedCallbacks[0].Input.AmountToAllocate.InexactFloat64()) + s.Equal(float64(5), finalizedCallbacks[0].Input.AmountToAllocate.InexactFloat64()) s.Equal(usagebased.RealizationRunTypeFinalRealization, finalizedCallbacks[0].Input.Run.Type) - s.True(finalStoredAtOffset.Equal(finalizedCallbacks[0].Input.AllocateAt)) + s.True(finalStoredAtLT.Equal(finalizedCallbacks[0].Input.AllocateAt)) }) s.Run("#5 final charge advance is noop", func() { @@ -781,8 +780,7 @@ func (s *InvoicableChargesTestSuite) TestUsageBasedCreditOnlyLifecycleVolumeTier } firstCollectionAdvanceAt := datetime.MustParseTimeInLocation(s.T(), "2026-02-01T12:00:00Z", time.UTC).AsTime() finalAdvanceAt := datetime.MustParseTimeInLocation(s.T(), "2026-02-03T00:01:00Z", time.UTC).AsTime() - firstStoredAtOffset := datetime.MustParseTimeInLocation(s.T(), "2026-02-01T11:59:00Z", time.UTC).AsTime() - finalStoredAtOffset := datetime.MustParseTimeInLocation(s.T(), "2026-02-03T00:00:00Z", time.UTC).AsTime() + finalStoredAtLT := datetime.MustParseTimeInLocation(s.T(), "2026-02-03T00:00:00Z", time.UTC).AsTime() expectedCollectionEnd := datetime.MustParseTimeInLocation(s.T(), "2026-02-03T00:00:00Z", time.UTC).AsTime() apiRequestsTotal := s.SetupApiRequestsTotalFeature(ctx, ns) @@ -871,9 +869,9 @@ func (s *InvoicableChargesTestSuite) TestUsageBasedCreditOnlyLifecycleVolumeTier s.Equal(usageBasedChargeID.ID, input.Charge.ID) s.Equal(productcatalog.CreditOnlySettlementMode, input.Charge.Intent.SettlementMode) s.Equal(usagebased.RealizationRunTypeFinalRealization, input.Run.Type) - s.True(firstStoredAtOffset.Equal(input.AllocateAt)) + s.True(finalStoredAtLT.Equal(input.AllocateAt)) s.Equal(float64(20), input.AmountToAllocate.InexactFloat64()) - s.Equal(float64(10), input.Run.MeterValue.InexactFloat64()) + s.Equal(float64(10), input.Run.MeteredQuantity.InexactFloat64()) s.RequireTotals(billingtest.ExpectedTotals{ Amount: 20, Total: 20, @@ -910,9 +908,9 @@ func (s *InvoicableChargesTestSuite) TestUsageBasedCreditOnlyLifecycleVolumeTier currentRun, err := usageBasedFromDB.Realizations.GetByID(*usageBasedFromDB.State.CurrentRealizationRunID) s.NoError(err) - s.True(firstStoredAtOffset.Equal(currentRun.AsOf)) - s.True(expectedCollectionEnd.Equal(currentRun.CollectionEnd.UTC())) - s.Equal(float64(10), currentRun.MeterValue.InexactFloat64()) + s.True(finalStoredAtLT.Equal(currentRun.StoredAtLT)) + s.True(expectedCollectionEnd.Equal(currentRun.StoredAtLT.UTC())) + s.Equal(float64(10), currentRun.MeteredQuantity.InexactFloat64()) s.RequireTotals(billingtest.ExpectedTotals{ Amount: 20, CreditsTotal: 20, @@ -940,8 +938,8 @@ func (s *InvoicableChargesTestSuite) TestUsageBasedCreditOnlyLifecycleVolumeTier s.Equal(usageBasedChargeID.ID, input.Charge.ID) s.Equal(productcatalog.CreditOnlySettlementMode, input.Charge.Intent.SettlementMode) s.Equal(usagebased.RealizationRunTypeFinalRealization, input.Run.Type) - s.True(finalStoredAtOffset.Equal(input.AllocateAt)) - s.Equal(float64(10), input.Run.MeterValue.InexactFloat64()) + s.True(finalStoredAtLT.Equal(input.AllocateAt)) + s.Equal(float64(10), input.Run.MeteredQuantity.InexactFloat64()) s.RequireTotals(billingtest.ExpectedTotals{ Amount: 20, CreditsTotal: 20, @@ -989,7 +987,7 @@ func (s *InvoicableChargesTestSuite) TestUsageBasedCreditOnlyLifecycleVolumeTier s.Require().NotNil(advancedCharge) s.Equal(meta.ChargeStatusFinal, meta.ChargeStatus(usageBasedFromDB.Status)) s.Len(correctedCallbacks, 1) - s.True(finalStoredAtOffset.Equal(correctedCallbacks[0].Input.AllocateAt)) + s.True(finalStoredAtLT.Equal(correctedCallbacks[0].Input.AllocateAt)) s.Len(correctedCallbacks[0].Input.Corrections, 1) s.Equal(float64(-9), correctedCallbacks[0].Input.Corrections[0].Amount.InexactFloat64()) @@ -998,9 +996,9 @@ func (s *InvoicableChargesTestSuite) TestUsageBasedCreditOnlyLifecycleVolumeTier s.Nil(usageBasedFromDB.State.AdvanceAfter) finalRun := usageBasedFromDB.Realizations[0] - s.True(finalStoredAtOffset.Equal(finalRun.AsOf)) - s.True(expectedCollectionEnd.Equal(finalRun.CollectionEnd.UTC())) - s.Equal(float64(11), finalRun.MeterValue.InexactFloat64()) + s.True(finalStoredAtLT.Equal(finalRun.StoredAtLT)) + s.True(expectedCollectionEnd.Equal(finalRun.StoredAtLT.UTC())) + s.Equal(float64(11), finalRun.MeteredQuantity.InexactFloat64()) s.RequireTotals(billingtest.ExpectedTotals{ Amount: 11, CreditsTotal: 11, @@ -1144,7 +1142,7 @@ func (s *InvoicableChargesTestSuite) TestUsageBasedCreditThenInvoiceLifecycle() currentRun, err := usageBasedCharge.GetCurrentRealizationRun() s.NoError(err) - s.Equal(float64(100), currentRun.MeterValue.InexactFloat64()) + s.Equal(float64(100), currentRun.MeteredQuantity.InexactFloat64()) s.Len(currentRun.CreditsAllocated, 1) s.Equal(float64(5), currentRun.CreditsAllocated[0].Amount.InexactFloat64()) s.True((*remainingCredits).IsZero()) @@ -1197,8 +1195,8 @@ func (s *InvoicableChargesTestSuite) TestUsageBasedCreditThenInvoiceLifecycle() currentRun, err := usageBasedCharge.GetCurrentRealizationRun() s.NoError(err) - s.Equal(float64(125), currentRun.MeterValue.InexactFloat64()) - s.True(currentRun.CollectionEnd.Equal(invoice.DefaultCollectionAtForStandardInvoice())) + s.Equal(float64(125), currentRun.MeteredQuantity.InexactFloat64()) + s.True(currentRun.StoredAtLT.Add(usagebased.InternalCollectionPeriod).Equal(invoice.DefaultCollectionAtForStandardInvoice())) s.NotNil(currentRun.LineID) s.Equal(stdLineID.ID, *currentRun.LineID) s.Len(currentRun.CreditsAllocated, 2) @@ -1225,7 +1223,7 @@ func (s *InvoicableChargesTestSuite) TestUsageBasedCreditThenInvoiceLifecycle() s.Equal(usageBasedChargeID.ID, input.Charge.ID) s.Equal(expectedLine.Period, input.ServicePeriod) s.Equal(float64(4.5), input.Amount.InexactFloat64()) - s.Equal(float64(125), input.Run.MeterValue.InexactFloat64()) + s.Equal(float64(125), input.Run.MeteredQuantity.InexactFloat64()) s.NotNil(input.Run.LineID) s.Equal(stdLineID.ID, *input.Run.LineID) }) @@ -1242,7 +1240,7 @@ func (s *InvoicableChargesTestSuite) TestUsageBasedCreditThenInvoiceLifecycle() s.Len(usageBasedCharge.Realizations, 1) finalRun := usageBasedCharge.Realizations[0] - s.Equal(float64(125), finalRun.MeterValue.InexactFloat64()) + s.Equal(float64(125), finalRun.MeteredQuantity.InexactFloat64()) s.NotNil(finalRun.LineID) s.Equal(stdLineID.ID, *finalRun.LineID) s.NotNil(finalRun.InvoiceUsage) @@ -1439,7 +1437,7 @@ func (s *InvoicableChargesTestSuite) TestUsageBasedCreditThenInvoiceFullyCredite currentRun, err := usageBasedCharge.GetCurrentRealizationRun() s.NoError(err) - s.Equal(float64(100), currentRun.MeterValue.InexactFloat64()) + s.Equal(float64(100), currentRun.MeteredQuantity.InexactFloat64()) s.Len(currentRun.CreditsAllocated, 1) s.Equal(float64(10), currentRun.CreditsAllocated[0].Amount.InexactFloat64()) }) @@ -1471,8 +1469,8 @@ func (s *InvoicableChargesTestSuite) TestUsageBasedCreditThenInvoiceFullyCredite currentRun, err := usageBasedCharge.GetCurrentRealizationRun() s.NoError(err) - s.Equal(float64(100), currentRun.MeterValue.InexactFloat64()) - s.True(currentRun.CollectionEnd.Equal(invoice.DefaultCollectionAtForStandardInvoice())) + s.Equal(float64(100), currentRun.MeteredQuantity.InexactFloat64()) + s.True(currentRun.StoredAtLT.Add(usagebased.InternalCollectionPeriod).Equal(invoice.DefaultCollectionAtForStandardInvoice())) s.NotNil(currentRun.LineID) s.Equal(stdLineID.ID, *currentRun.LineID) s.Len(currentRun.CreditsAllocated, 1) @@ -1497,7 +1495,7 @@ func (s *InvoicableChargesTestSuite) TestUsageBasedCreditThenInvoiceFullyCredite s.Len(usageBasedCharge.Realizations, 1) finalRun := usageBasedCharge.Realizations[0] - s.Equal(float64(100), finalRun.MeterValue.InexactFloat64()) + s.Equal(float64(100), finalRun.MeteredQuantity.InexactFloat64()) s.NotNil(finalRun.LineID) s.Equal(stdLineID.ID, *finalRun.LineID) s.Nil(finalRun.InvoiceUsage) @@ -1742,16 +1740,16 @@ func (s *InvoicableChargesTestSuite) TestUsageBasedCreateImmediatelyFinal() { // collectionEnd = servicePeriod.To + P2D = 2026-02-03T00:00:00Z // finalAdvanceAt = collectionEnd + InternalCollectionPeriod (1 minute) = 2026-02-03T00:01:00Z - // storedAtOffset = clock.Now() - InternalCollectionPeriod = finalAdvanceAt - 1min = collectionEnd + // storedAtLT = clock.Now() - InternalCollectionPeriod = finalAdvanceAt - 1min = collectionEnd finalAdvanceAt := datetime.MustParseTimeInLocation(s.T(), "2026-02-03T00:01:00Z", time.UTC).AsTime() expectedCollectionEnd := datetime.MustParseTimeInLocation(s.T(), "2026-02-03T00:00:00Z", time.UTC).AsTime() - expectedAsOf := finalAdvanceAt.Add(-usagebased.InternalCollectionPeriod) // == expectedCollectionEnd + expectedStoredAtLT := finalAdvanceAt.Add(-usagebased.InternalCollectionPeriod) // == expectedCollectionEnd apiRequestsTotal := s.SetupApiRequestsTotalFeature(ctx, ns) meterSlug := apiRequestsTotal.Feature.Key // Two events inside the service period; default StoredAt == event time so both are well below - // storedAtOffset (2026-02-03T00:00:00Z) and will be included in the rating. + // storedAtLT (2026-02-03T00:00:00Z) and will be included in the rating. s.MockStreamingConnector.AddSimpleEvent(meterSlug, 3, datetime.MustParseTimeInLocation(s.T(), "2026-01-15T00:00:00Z", time.UTC).AsTime(), ) @@ -1812,10 +1810,10 @@ func (s *InvoicableChargesTestSuite) TestUsageBasedCreateImmediatelyFinal() { s.Len(returnedCharge.Realizations, 1) finalRun := returnedCharge.Realizations[0] - s.True(expectedAsOf.Equal(finalRun.AsOf)) - s.NotNil(finalRun.CollectionEnd) - s.True(expectedCollectionEnd.Equal(finalRun.CollectionEnd.UTC())) - s.Equal(expectedUsage, finalRun.MeterValue.InexactFloat64()) + s.True(expectedStoredAtLT.Equal(finalRun.StoredAtLT)) + s.False(finalRun.StoredAtLT.IsZero()) + s.True(expectedCollectionEnd.Equal(finalRun.StoredAtLT.UTC())) + s.Equal(expectedUsage, finalRun.MeteredQuantity.InexactFloat64()) s.RequireTotals(billingtest.ExpectedTotals{ Amount: expectedUsage, CreditsTotal: expectedUsage, diff --git a/openmeter/billing/charges/service/truncation_test.go b/openmeter/billing/charges/service/truncation_test.go index 4c75d3b5bb..0f1ea0e6cf 100644 --- a/openmeter/billing/charges/service/truncation_test.go +++ b/openmeter/billing/charges/service/truncation_test.go @@ -165,10 +165,10 @@ func (s *ChargeTimestampTruncationTestSuite) TestUsageBasedAdvanceTruncatesPersi finalRun := finalCharge.Realizations[0] expectedCollectionEnd := datetime.MustParseTimeInLocation(s.T(), "2026-02-01T00:01:00Z", time.UTC).AsTime() - expectedAsOf := datetime.MustParseTimeInLocation(s.T(), "2026-02-01T00:01:00Z", time.UTC).AsTime() + expectedStoredAtLT := datetime.MustParseTimeInLocation(s.T(), "2026-02-01T00:01:00Z", time.UTC).AsTime() - s.True(expectedCollectionEnd.Equal(finalRun.CollectionEnd)) - s.True(expectedAsOf.Equal(finalRun.AsOf)) + s.True(expectedCollectionEnd.Equal(finalRun.StoredAtLT)) + s.True(expectedStoredAtLT.Equal(finalRun.StoredAtLT)) } func (s *ChargeTimestampTruncationTestSuite) TestTmpApplyPatchToCreateIntentTruncatesReplacementPeriods() { diff --git a/openmeter/billing/charges/service/usagebased_test.go b/openmeter/billing/charges/service/usagebased_test.go index d417039d5a..68101331f5 100644 --- a/openmeter/billing/charges/service/usagebased_test.go +++ b/openmeter/billing/charges/service/usagebased_test.go @@ -208,7 +208,8 @@ func (s *UsageBasedChargesTestSuite) TestUsageBasedCreditThenInvoicePartialInvoi s.Equal(usagebased.RealizationRunTypePartialInvoice, currentRun.Type) s.Require().NotNil(currentRun.LineID) s.Equal(stdLine.ID, *currentRun.LineID) - s.True(expectedPartialCollectionEnd.Equal(currentRun.CollectionEnd)) + s.True(midPeriodInvoiceAt.Equal(currentRun.ServicePeriodTo)) + s.True(midPeriodInvoiceAt.Equal(currentRun.StoredAtLT)) s.Require().NotNil(partialInvoice.CollectionAt) s.True(expectedPartialCollectionEnd.Equal(*partialInvoice.CollectionAt)) diff --git a/openmeter/billing/charges/usagebased/adapter/detailedline_test.go b/openmeter/billing/charges/usagebased/adapter/detailedline_test.go index 29e3febf40..4b75758ab9 100644 --- a/openmeter/billing/charges/usagebased/adapter/detailedline_test.go +++ b/openmeter/billing/charges/usagebased/adapter/detailedline_test.go @@ -126,11 +126,11 @@ func (s *DetailedLineAdapterSuite) TestUpsertRunDetailedLinesReplacesAndSoftDele charge := createdCharges[0] runBase, err := s.adapter.CreateRealizationRun(ctx, charge.GetChargeID(), usagebased.CreateRealizationRunInput{ - FeatureID: "feature-1", - Type: usagebased.RealizationRunTypeFinalRealization, - AsOf: servicePeriod.To, - CollectionEnd: servicePeriod.To, - MeterValue: alpacadecimal.NewFromInt(10), + FeatureID: "feature-1", + Type: usagebased.RealizationRunTypeFinalRealization, + StoredAtLT: servicePeriod.To, + ServicePeriodTo: servicePeriod.To, + MeteredQuantity: alpacadecimal.NewFromInt(10), Totals: totals.Totals{ Amount: alpacadecimal.NewFromInt(1), ChargesTotal: alpacadecimal.NewFromInt(1), diff --git a/openmeter/billing/charges/usagebased/adapter/mapper.go b/openmeter/billing/charges/usagebased/adapter/mapper.go index afda51adcb..6f47c8e25f 100644 --- a/openmeter/billing/charges/usagebased/adapter/mapper.go +++ b/openmeter/billing/charges/usagebased/adapter/mapper.go @@ -79,9 +79,9 @@ func MapRealizationRunsFromDB(entity *entdb.ChargeUsageBased) (usagebased.Realiz runs = nil } - // Let's keep the runs sorted by AsOf + // Let's keep the runs sorted by period end. slices.SortStableFunc(runs, func(a, b usagebased.RealizationRun) int { - return cmp.Compare(a.AsOf.UnixNano(), b.AsOf.UnixNano()) + return cmp.Compare(a.ServicePeriodTo.UnixNano(), b.ServicePeriodTo.UnixNano()) }) return runs, nil @@ -95,13 +95,13 @@ func MapRealizationRunBaseFromDB(dbRun *entdb.ChargeUsageBasedRuns) usagebased.R }, ManagedModel: entutils.MapTimeMixinFromDB(dbRun), - FeatureID: dbRun.FeatureID, - LineID: dbRun.LineID, - Type: dbRun.Type, - AsOf: dbRun.Asof.UTC(), - CollectionEnd: dbRun.CollectionEnd, - MeterValue: dbRun.MeterValue, - Totals: totals.FromDB(dbRun), + FeatureID: dbRun.FeatureID, + LineID: dbRun.LineID, + Type: dbRun.Type, + StoredAtLT: dbRun.StoredAtLt.UTC(), + ServicePeriodTo: dbRun.ServicePeriodTo.UTC(), + MeteredQuantity: dbRun.MeteredQuantity, + Totals: totals.FromDB(dbRun), } } diff --git a/openmeter/billing/charges/usagebased/adapter/realizationrun.go b/openmeter/billing/charges/usagebased/adapter/realizationrun.go index a4555bc40d..fc6aaf657d 100644 --- a/openmeter/billing/charges/usagebased/adapter/realizationrun.go +++ b/openmeter/billing/charges/usagebased/adapter/realizationrun.go @@ -27,10 +27,10 @@ func (a *adapter) CreateRealizationRun(ctx context.Context, chargeID meta.Charge SetChargeID(chargeID.ID). SetFeatureID(input.FeatureID). SetType(input.Type). - SetAsof(meta.NormalizeTimestamp(input.AsOf)). - SetCollectionEnd(meta.NormalizeTimestamp(input.CollectionEnd)). + SetStoredAtLt(meta.NormalizeTimestamp(input.StoredAtLT)). + SetServicePeriodTo(meta.NormalizeTimestamp(input.ServicePeriodTo)). SetNillableBillingInvoiceLineID(input.LineID). - SetMeterValue(input.MeterValue) + SetMeteredQuantity(input.MeteredQuantity) create = totals.Set(create, input.Totals) @@ -52,16 +52,16 @@ func (a *adapter) UpdateRealizationRun(ctx context.Context, input usagebased.Upd update := tx.db.ChargeUsageBasedRuns.UpdateOneID(input.ID.ID). Where(dbchargeusagebasedruns.NamespaceEQ(input.ID.Namespace)) - if input.AsOf.IsPresent() { - update = update.SetAsof(meta.NormalizeTimestamp(input.AsOf.OrEmpty())) + if input.StoredAtLT.IsPresent() { + update = update.SetStoredAtLt(meta.NormalizeTimestamp(input.StoredAtLT.OrEmpty())) } if input.LineID.IsPresent() { update = update.SetOrClearLineID(input.LineID.OrEmpty()) } - if input.MeterValue.IsPresent() { - update = update.SetMeterValue(input.MeterValue.OrEmpty()) + if input.MeteredQuantity.IsPresent() { + update = update.SetMeteredQuantity(input.MeteredQuantity.OrEmpty()) } if input.Totals.IsPresent() { diff --git a/openmeter/billing/charges/usagebased/realizationrun.go b/openmeter/billing/charges/usagebased/realizationrun.go index 30d238151f..075d0f9c61 100644 --- a/openmeter/billing/charges/usagebased/realizationrun.go +++ b/openmeter/billing/charges/usagebased/realizationrun.go @@ -46,18 +46,18 @@ func (i RealizationRunID) Validate() error { } type CreateRealizationRunInput struct { - FeatureID string `json:"featureId"` - Type RealizationRunType `json:"type"` - AsOf time.Time `json:"asOf"` - CollectionEnd time.Time `json:"collectionEnd,omitempty"` - LineID *string `json:"lineId,omitempty"` - MeterValue alpacadecimal.Decimal `json:"meterValue"` - Totals totals.Totals `json:"totals"` + FeatureID string `json:"featureId"` + Type RealizationRunType `json:"type"` + StoredAtLT time.Time `json:"storedAtLT"` + ServicePeriodTo time.Time `json:"servicePeriodTo"` + LineID *string `json:"lineId,omitempty"` + MeteredQuantity alpacadecimal.Decimal `json:"meteredQuantity"` + Totals totals.Totals `json:"totals"` } func (r CreateRealizationRunInput) Normalized() CreateRealizationRunInput { - r.AsOf = meta.NormalizeTimestamp(r.AsOf) - r.CollectionEnd = meta.NormalizeTimestamp(r.CollectionEnd) + r.StoredAtLT = meta.NormalizeTimestamp(r.StoredAtLT) + r.ServicePeriodTo = meta.NormalizeTimestamp(r.ServicePeriodTo) return r } @@ -73,20 +73,20 @@ func (r CreateRealizationRunInput) Validate() error { errs = append(errs, fmt.Errorf("feature id must be set")) } - if r.AsOf.IsZero() { - errs = append(errs, fmt.Errorf("as of must be set")) + if r.StoredAtLT.IsZero() { + errs = append(errs, fmt.Errorf("stored at lt must be set")) } - if r.MeterValue.IsNegative() { - errs = append(errs, fmt.Errorf("meter value must be zero or positive")) + if r.MeteredQuantity.IsNegative() { + errs = append(errs, fmt.Errorf("metered quantity must be zero or positive")) } if err := r.Totals.Validate(); err != nil { errs = append(errs, fmt.Errorf("totals: %w", err)) } - if r.CollectionEnd.IsZero() { - errs = append(errs, fmt.Errorf("collection end must be set")) + if r.ServicePeriodTo.IsZero() { + errs = append(errs, fmt.Errorf("service period to must be set")) } if r.LineID != nil && *r.LineID == "" { @@ -99,16 +99,16 @@ func (r CreateRealizationRunInput) Validate() error { type UpdateRealizationRunInput struct { ID RealizationRunID - AsOf mo.Option[time.Time] `json:"asOf"` - LineID mo.Option[*string] `json:"lineId,omitempty"` - MeterValue mo.Option[alpacadecimal.Decimal] `json:"meterValue"` - Totals mo.Option[totals.Totals] `json:"totals"` + StoredAtLT mo.Option[time.Time] `json:"storedAtLT"` + LineID mo.Option[*string] `json:"lineId,omitempty"` + MeteredQuantity mo.Option[alpacadecimal.Decimal] `json:"meteredQuantity"` + Totals mo.Option[totals.Totals] `json:"totals"` } func (r UpdateRealizationRunInput) Normalized() UpdateRealizationRunInput { - if r.AsOf.IsPresent() { - asOf := r.AsOf.OrEmpty() - r.AsOf = mo.Some(meta.NormalizeTimestamp(asOf)) + if r.StoredAtLT.IsPresent() { + storedAtLT := r.StoredAtLT.OrEmpty() + r.StoredAtLT = mo.Some(meta.NormalizeTimestamp(storedAtLT)) } return r @@ -121,8 +121,8 @@ func (r UpdateRealizationRunInput) Validate() error { errs = append(errs, fmt.Errorf("namespaced id: %w", err)) } - if r.AsOf.IsPresent() && r.AsOf.OrEmpty().IsZero() { - errs = append(errs, fmt.Errorf("as of must be non-zero when set")) + if r.StoredAtLT.IsPresent() && r.StoredAtLT.OrEmpty().IsZero() { + errs = append(errs, fmt.Errorf("stored at lt must be non-zero when set")) } if r.LineID.IsPresent() { @@ -132,8 +132,8 @@ func (r UpdateRealizationRunInput) Validate() error { } } - if r.MeterValue.IsPresent() && r.MeterValue.OrEmpty().IsNegative() { - errs = append(errs, fmt.Errorf("meter value must be zero or positive")) + if r.MeteredQuantity.IsPresent() && r.MeteredQuantity.OrEmpty().IsNegative() { + errs = append(errs, fmt.Errorf("metered quantity must be zero or positive")) } if r.Totals.IsPresent() { @@ -152,16 +152,18 @@ type RealizationRunBase struct { FeatureID string `json:"featureId"` LineID *string `json:"lineId,omitempty"` - Type RealizationRunType `json:"type"` - AsOf time.Time `json:"asOf"` - CollectionEnd time.Time `json:"collectionEnd,omitempty"` - MeterValue alpacadecimal.Decimal `json:"meterValue"` - Totals totals.Totals `json:"totals"` + Type RealizationRunType `json:"type"` + StoredAtLT time.Time `json:"storedAtLT"` + // ServicePeriodTo is the end of the service period for the realization run. + ServicePeriodTo time.Time `json:"servicePeriodTo"` + // MeteredQuantity is the metered quantity for time IN [intent.servicePeriod.from, servicePeriodTo) capped by stored_at < StoredAtLT. + MeteredQuantity alpacadecimal.Decimal `json:"meteredQuantity"` + Totals totals.Totals `json:"totals"` } func (r RealizationRunBase) Normalized() RealizationRunBase { - r.AsOf = meta.NormalizeTimestamp(r.AsOf) - r.CollectionEnd = meta.NormalizeTimestamp(r.CollectionEnd) + r.StoredAtLT = meta.NormalizeTimestamp(r.StoredAtLT) + r.ServicePeriodTo = meta.NormalizeTimestamp(r.ServicePeriodTo) return r } @@ -189,20 +191,20 @@ func (r RealizationRunBase) Validate() error { errs = append(errs, fmt.Errorf("type: %w", err)) } - if r.AsOf.IsZero() { - errs = append(errs, fmt.Errorf("as of must be set")) + if r.StoredAtLT.IsZero() { + errs = append(errs, fmt.Errorf("stored at lt must be set")) } - if r.MeterValue.IsNegative() { - errs = append(errs, fmt.Errorf("meter value must be zero or positive")) + if r.MeteredQuantity.IsNegative() { + errs = append(errs, fmt.Errorf("metered quantity must be zero or positive")) } if err := r.Totals.Validate(); err != nil { errs = append(errs, fmt.Errorf("totals: %w", err)) } - if r.CollectionEnd.IsZero() { - errs = append(errs, fmt.Errorf("collection end must be set")) + if r.ServicePeriodTo.IsZero() { + errs = append(errs, fmt.Errorf("service period to must be set")) } return models.NewNillableGenericValidationError(errors.Join(errs...)) diff --git a/openmeter/billing/charges/usagebased/service/creditheninvoice.go b/openmeter/billing/charges/usagebased/service/creditheninvoice.go index 4522db96cd..5b857ef11d 100644 --- a/openmeter/billing/charges/usagebased/service/creditheninvoice.go +++ b/openmeter/billing/charges/usagebased/service/creditheninvoice.go @@ -14,7 +14,6 @@ import ( usagebasedrating "github.com/openmeterio/openmeter/openmeter/billing/charges/usagebased/service/rating" usagebasedrun "github.com/openmeterio/openmeter/openmeter/billing/charges/usagebased/service/run" "github.com/openmeterio/openmeter/openmeter/productcatalog" - "github.com/openmeterio/openmeter/pkg/clock" "github.com/openmeterio/openmeter/pkg/statelessx" "github.com/openmeterio/openmeter/pkg/timeutil" ) @@ -212,8 +211,8 @@ func (s *CreditThenInvoiceStateMachine) DeleteCharge(ctx context.Context, _ meta } type invoiceCreatedInput struct { - LineID string - OverrideCollectionPeriodEnd *time.Time + LineID string + ServicePeriodTo time.Time } func (i invoiceCreatedInput) Validate() error { @@ -221,8 +220,8 @@ func (i invoiceCreatedInput) Validate() error { return fmt.Errorf("line id is required") } - if i.OverrideCollectionPeriodEnd != nil && i.OverrideCollectionPeriodEnd.IsZero() { - return fmt.Errorf("override collection period end must not be zero when set") + if i.ServicePeriodTo.IsZero() { + return fmt.Errorf("service period to is required") } return nil @@ -237,14 +236,15 @@ func (s *CreditThenInvoiceStateMachine) startInvoiceCreatedRun( return fmt.Errorf("validate invoice created input: %w", err) } - storedAtOffset := meta.NormalizeTimestamp(clock.Now()) - collectionEnd := lo.FromPtr(input.OverrideCollectionPeriodEnd) - if collectionEnd.IsZero() { + storedAtLT := meta.NormalizeTimestamp(input.ServicePeriodTo) + servicePeriodTo := storedAtLT + if runType == usagebased.RealizationRunTypeFinalRealization { var err error - collectionEnd, err = s.GetCollectionPeriodEnd(ctx) + storedAtLT, err = s.getFinalRunStoredAtLT() if err != nil { - return fmt.Errorf("get collection period end: %w", err) + return fmt.Errorf("get stored at lt: %w", err) } + servicePeriodTo = meta.NormalizeTimestamp(s.Charge.Intent.ServicePeriod.To) } result, err := s.Runs.CreateRatedRun(ctx, usagebasedrun.CreateRatedRunInput{ @@ -252,8 +252,8 @@ func (s *CreditThenInvoiceStateMachine) startInvoiceCreatedRun( CustomerOverride: s.CustomerOverride, FeatureMeter: s.FeatureMeter, Type: runType, - AsOf: storedAtOffset, - CollectionEnd: collectionEnd, + StoredAtLT: storedAtLT, + ServicePeriodTo: servicePeriodTo, LineID: lo.ToPtr(input.LineID), IgnoreMinimumCommitment: ignoreMinimumCommitmentForRunType(runType), CreditAllocation: usagebasedrun.CreditAllocationAvailable, @@ -307,14 +307,15 @@ func (s *CreditThenInvoiceStateMachine) SnapshotInvoiceUsage(ctx context.Context return fmt.Errorf("get current realization run: %w", err) } - storedAtOffset := meta.NormalizeTimestamp(currentRun.CollectionEnd) + storedAtLT := meta.NormalizeTimestamp(currentRun.StoredAtLT) ratingResult, err := s.Rater.GetDetailedLinesForUsage(ctx, usagebasedrating.GetDetailedLinesForUsageInput{ Charge: s.Charge, PriorRuns: s.Charge.Realizations.Without(currentRun.ID), Customer: s.CustomerOverride, FeatureMeter: s.FeatureMeter, - StoredAtOffset: storedAtOffset, + ServicePeriodTo: currentRun.ServicePeriodTo, + StoredAtLT: storedAtLT, IgnoreMinimumCommitment: ignoreMinimumCommitmentForRunType(currentRun.Type), }) if err != nil { @@ -326,7 +327,7 @@ func (s *CreditThenInvoiceStateMachine) SnapshotInvoiceUsage(ctx context.Context reconcileResult, err := s.Runs.ReconcileCredits(ctx, usagebasedrun.ReconcileCreditRealizationsInput{ Charge: s.Charge, Run: currentRun, - AllocateAt: storedAtOffset, + AllocateAt: storedAtLT, TargetAmount: currentTotals.Total, CurrencyCalculator: s.CurrencyCalculator, ExactAllocation: false, @@ -346,10 +347,10 @@ func (s *CreditThenInvoiceStateMachine) SnapshotInvoiceUsage(ctx context.Context currentRun.DetailedLines = mo.Some(runDetailedLines) currentRunBase, err := s.Adapter.UpdateRealizationRun(ctx, usagebased.UpdateRealizationRunInput{ - ID: currentRun.ID, - AsOf: mo.Some(storedAtOffset), - MeterValue: mo.Some(ratingResult.Quantity), - Totals: mo.Some(currentTotals), + ID: currentRun.ID, + StoredAtLT: mo.Some(storedAtLT), + MeteredQuantity: mo.Some(ratingResult.Quantity), + Totals: mo.Some(currentTotals), }) if err != nil { return fmt.Errorf("update realization run: %w", err) diff --git a/openmeter/billing/charges/usagebased/service/creditheninvoice_test.go b/openmeter/billing/charges/usagebased/service/creditheninvoice_test.go index 631b625353..3b7e580ba3 100644 --- a/openmeter/billing/charges/usagebased/service/creditheninvoice_test.go +++ b/openmeter/billing/charges/usagebased/service/creditheninvoice_test.go @@ -1,7 +1,6 @@ package service import ( - "context" "testing" "time" @@ -14,20 +13,18 @@ import ( func TestStartInvoiceCreatedRunValidatesInput(t *testing.T) { var machine CreditThenInvoiceStateMachine - overrideCollectionPeriodEnd := time.Time{} err := machine.startInvoiceCreatedRun( - context.Background(), + t.Context(), invoiceCreatedInput{ - LineID: "line-1", - OverrideCollectionPeriodEnd: &overrideCollectionPeriodEnd, + LineID: "line-1", }, usagebased.RealizationRunTypePartialInvoice, ) require.Error(t, err) require.ErrorContains(t, err, "validate invoice created input") - require.ErrorContains(t, err, "override collection period end must not be zero when set") + require.ErrorContains(t, err, "service period to is required") } func TestResolveInvoiceCreatedTrigger(t *testing.T) { diff --git a/openmeter/billing/charges/usagebased/service/creditsonly.go b/openmeter/billing/charges/usagebased/service/creditsonly.go index 0db1a8694c..74af4ac294 100644 --- a/openmeter/billing/charges/usagebased/service/creditsonly.go +++ b/openmeter/billing/charges/usagebased/service/creditsonly.go @@ -145,10 +145,9 @@ func (s *CreditsOnlyStateMachine) DeleteCharge(ctx context.Context, policy meta. } func (s *CreditsOnlyStateMachine) StartFinalRealizationRun(ctx context.Context) error { - storedAtOffset := meta.NormalizeTimestamp(clock.Now().Add(-usagebased.InternalCollectionPeriod)) - collectionEnd, err := s.GetCollectionPeriodEnd(ctx) + storedAtLT, err := s.getFinalRunStoredAtLT() if err != nil { - return fmt.Errorf("get collection period end: %w", err) + return fmt.Errorf("get stored at lt: %w", err) } result, err := s.Runs.CreateRatedRun(ctx, usagebasedrun.CreateRatedRunInput{ @@ -156,8 +155,8 @@ func (s *CreditsOnlyStateMachine) StartFinalRealizationRun(ctx context.Context) CustomerOverride: s.CustomerOverride, FeatureMeter: s.FeatureMeter, Type: usagebased.RealizationRunTypeFinalRealization, - AsOf: storedAtOffset, - CollectionEnd: collectionEnd, + StoredAtLT: storedAtLT, + ServicePeriodTo: meta.NormalizeTimestamp(s.Charge.Intent.ServicePeriod.To), CreditAllocation: usagebasedrun.CreditAllocationExact, CurrencyCalculator: s.CurrencyCalculator, }) @@ -183,14 +182,15 @@ func (s *CreditsOnlyStateMachine) FinalizeRealizationRun(ctx context.Context) er return fmt.Errorf("get current realization run: %w", err) } - storedAtOffset := meta.NormalizeTimestamp(clock.Now().Add(-usagebased.InternalCollectionPeriod)) + storedAtLT := meta.NormalizeTimestamp(currentRun.StoredAtLT) ratingResult, err := s.Rater.GetDetailedLinesForUsage(ctx, usagebasedrating.GetDetailedLinesForUsageInput{ - Charge: s.Charge, - PriorRuns: s.Charge.Realizations.Without(currentRun.ID), - Customer: s.CustomerOverride, - FeatureMeter: s.FeatureMeter, - StoredAtOffset: storedAtOffset, + Charge: s.Charge, + PriorRuns: s.Charge.Realizations.Without(currentRun.ID), + Customer: s.CustomerOverride, + FeatureMeter: s.FeatureMeter, + ServicePeriodTo: currentRun.ServicePeriodTo, + StoredAtLT: storedAtLT, }) if err != nil { return fmt.Errorf("get rating for usage: %w", err) @@ -202,7 +202,7 @@ func (s *CreditsOnlyStateMachine) FinalizeRealizationRun(ctx context.Context) er if _, err := s.Runs.ReconcileCredits(ctx, usagebasedrun.ReconcileCreditRealizationsInput{ Charge: s.Charge, Run: currentRun, - AllocateAt: storedAtOffset, + AllocateAt: storedAtLT, TargetAmount: targetCreditsTotal, CurrencyCalculator: s.CurrencyCalculator, ExactAllocation: true, @@ -220,10 +220,10 @@ func (s *CreditsOnlyStateMachine) FinalizeRealizationRun(ctx context.Context) er currentRun.DetailedLines = mo.Some(runDetailedLines) if _, err := s.Adapter.UpdateRealizationRun(ctx, usagebased.UpdateRealizationRunInput{ - ID: currentRun.ID, - AsOf: mo.Some(storedAtOffset), - MeterValue: mo.Some(ratingResult.Quantity), - Totals: mo.Some(currentTotals), + ID: currentRun.ID, + StoredAtLT: mo.Some(storedAtLT), + MeteredQuantity: mo.Some(ratingResult.Quantity), + Totals: mo.Some(currentTotals), }); err != nil { return fmt.Errorf("update realization run: %w", err) } diff --git a/openmeter/billing/charges/usagebased/service/currenttotals.go b/openmeter/billing/charges/usagebased/service/currenttotals.go index 8a87d1dc8a..5443ce3ecb 100644 --- a/openmeter/billing/charges/usagebased/service/currenttotals.go +++ b/openmeter/billing/charges/usagebased/service/currenttotals.go @@ -49,10 +49,10 @@ func (s *service) GetCurrentTotals(ctx context.Context, input usagebased.GetCurr } dueTotals, err := s.rater.GetTotalsForUsage(ctx, usagebasedrating.GetTotalsForUsageInput{ - Charge: charge, - Customer: customerOverride, - FeatureMeter: featureMeter, - StoredAtOffset: clock.Now(), + Charge: charge, + Customer: customerOverride, + FeatureMeter: featureMeter, + StoredAtLT: clock.Now(), }) if err != nil { return usagebased.GetCurrentTotalsResult{}, fmt.Errorf("get totals for usage: %w", err) diff --git a/openmeter/billing/charges/usagebased/service/get.go b/openmeter/billing/charges/usagebased/service/get.go index 36f8c0f00d..7eb4e7b5e5 100644 --- a/openmeter/billing/charges/usagebased/service/get.go +++ b/openmeter/billing/charges/usagebased/service/get.go @@ -144,10 +144,10 @@ func (s *service) expandChargesUsage(ctx context.Context, namespace string, char var dueTotals totals.Totals dueTotals, err = s.rater.GetTotalsForUsage(ctx, usagebasedrating.GetTotalsForUsageInput{ - Charge: charge, - Customer: customerOverridesById[charge.GetCustomerID()], - FeatureMeter: featureMeter, - StoredAtOffset: storedAt, + Charge: charge, + Customer: customerOverridesById[charge.GetCustomerID()], + FeatureMeter: featureMeter, + StoredAtLT: storedAt, }) if err != nil { err = fmt.Errorf("get totals for charge %s: %w", charge.ID, err) diff --git a/openmeter/billing/charges/usagebased/service/lineengine.go b/openmeter/billing/charges/usagebased/service/lineengine.go index dfa28e664e..013a882fd8 100644 --- a/openmeter/billing/charges/usagebased/service/lineengine.go +++ b/openmeter/billing/charges/usagebased/service/lineengine.go @@ -149,14 +149,9 @@ func (e *LineEngine) OnStandardInvoiceCreated(ctx context.Context, input billing } } - if trigger == meta.TriggerPartialInvoiceCreated { - // A past Period.To intentionally means "collect now" so late events can still produce an immediately collectable partial invoice. - stdLine.OverrideCollectionPeriodEnd = lo.ToPtr(stdLine.Period.To.Add(usagebased.InternalCollectionPeriod)) - } - if err := stateMachine.FireAndActivate(ctx, trigger, invoiceCreatedInput{ - LineID: stdLine.ID, - OverrideCollectionPeriodEnd: stdLine.OverrideCollectionPeriodEnd, + LineID: stdLine.ID, + ServicePeriodTo: stdLine.Period.To, }); err != nil { return nil, fmt.Errorf("triggering %s for charge[%s]: %w", trigger, stateMachine.GetCharge().ID, err) } @@ -309,8 +304,9 @@ func populateUsageBasedStandardLineFromRun(stdLine *billing.StandardLine, run us stdLine.UsageBased = &billing.UsageBasedLine{} } - stdLine.UsageBased.Quantity = lo.ToPtr(run.MeterValue) - stdLine.UsageBased.MeteredQuantity = lo.ToPtr(run.MeterValue) + stdLine.OverrideCollectionPeriodEnd = lo.ToPtr(run.StoredAtLT.Add(usagebased.InternalCollectionPeriod)) + stdLine.UsageBased.Quantity = lo.ToPtr(run.MeteredQuantity) + stdLine.UsageBased.MeteredQuantity = lo.ToPtr(run.MeteredQuantity) stdLine.UsageBased.PreLinePeriodQuantity = lo.ToPtr(alpacadecimal.Zero) stdLine.UsageBased.MeteredPreLinePeriodQuantity = lo.ToPtr(alpacadecimal.Zero) diff --git a/openmeter/billing/charges/usagebased/service/rating/details.go b/openmeter/billing/charges/usagebased/service/rating/details.go index 08cd10e923..c58c6d0645 100644 --- a/openmeter/billing/charges/usagebased/service/rating/details.go +++ b/openmeter/billing/charges/usagebased/service/rating/details.go @@ -14,11 +14,12 @@ import ( ) type GetDetailedLinesForUsageInput struct { - Charge usagebased.Charge - PriorRuns usagebased.RealizationRuns - Customer billing.CustomerOverrideWithDetails - FeatureMeter feature.FeatureMeter - StoredAtOffset time.Time + Charge usagebased.Charge + PriorRuns usagebased.RealizationRuns + Customer billing.CustomerOverrideWithDetails + FeatureMeter feature.FeatureMeter + ServicePeriodTo time.Time + StoredAtLT time.Time // IgnoreMinimumCommitment suppresses minimum commitment while still applying the rest of the billing mutators. IgnoreMinimumCommitment bool } @@ -36,8 +37,21 @@ func (i GetDetailedLinesForUsageInput) Validate() error { return fmt.Errorf("feature meter is required") } - if i.StoredAtOffset.IsZero() { - return fmt.Errorf("stored at offset is required") + if i.ServicePeriodTo.IsZero() { + return fmt.Errorf("service period to is required") + } + + period := i.Charge.Intent.ServicePeriod + if !i.ServicePeriodTo.After(period.From) { + return fmt.Errorf("service period to must be after charge service period from") + } + + if i.ServicePeriodTo.After(period.To) { + return fmt.Errorf("service period to must not be after charge service period to") + } + + if i.StoredAtLT.IsZero() { + return fmt.Errorf("stored at lt is required") } if err := i.PriorRuns.Validate(); err != nil { @@ -65,11 +79,14 @@ func (s *service) GetDetailedLinesForUsage(ctx context.Context, in GetDetailedLi return GetRatingForUsageResult{}, err } + servicePeriod := in.Charge.Intent.ServicePeriod + servicePeriod.To = in.ServicePeriodTo + snapshotQuantity, err := s.snapshotQuantity(ctx, snapshotQuantityInput{ - Customer: in.Customer.Customer, - FeatureMeter: in.FeatureMeter, - ServicePeriod: in.Charge.Intent.ServicePeriod, - StoredAtOffset: in.StoredAtOffset, + Customer: in.Customer.Customer, + FeatureMeter: in.FeatureMeter, + ServicePeriod: servicePeriod, + StoredAtLT: in.StoredAtLT, }) if err != nil { return GetRatingForUsageResult{}, fmt.Errorf("get snapshot quantity: %w", err) @@ -80,8 +97,11 @@ func (s *service) GetDetailedLinesForUsage(ctx context.Context, in GetDetailedLi opts = append(opts, billingrating.WithMinimumCommitmentIgnored()) } + intent := in.Charge.Intent + intent.ServicePeriod = servicePeriod + ratingResult, err := s.ratingService.GenerateDetailedLines(usagebased.RateableIntent{ - Intent: in.Charge.Intent, + Intent: intent, MeterValue: snapshotQuantity, }, opts...) if err != nil { @@ -90,7 +110,7 @@ func (s *service) GetDetailedLinesForUsage(ctx context.Context, in GetDetailedLi ratingResult.DetailedLines = withServicePeriodInDetailedLineChildUniqueReferenceIDs( ratingResult.DetailedLines, - in.Charge.Intent.ServicePeriod, + servicePeriod, ) return GetRatingForUsageResult{ diff --git a/openmeter/billing/charges/usagebased/service/rating/quantitysnapshot.go b/openmeter/billing/charges/usagebased/service/rating/quantitysnapshot.go index 02149c8973..27090811d9 100644 --- a/openmeter/billing/charges/usagebased/service/rating/quantitysnapshot.go +++ b/openmeter/billing/charges/usagebased/service/rating/quantitysnapshot.go @@ -16,10 +16,10 @@ import ( ) type snapshotQuantityInput struct { - Customer streaming.Customer - FeatureMeter feature.FeatureMeter - ServicePeriod timeutil.ClosedPeriod - StoredAtOffset time.Time + Customer streaming.Customer + FeatureMeter feature.FeatureMeter + ServicePeriod timeutil.ClosedPeriod + StoredAtLT time.Time } func (i snapshotQuantityInput) Validate() error { @@ -31,8 +31,8 @@ func (i snapshotQuantityInput) Validate() error { return fmt.Errorf("service period: %w", err) } - if i.StoredAtOffset.IsZero() { - return fmt.Errorf("stored at offset is required") + if i.StoredAtLT.IsZero() { + return fmt.Errorf("stored at lt is required") } return nil @@ -52,7 +52,7 @@ func (s *service) snapshotQuantity(ctx context.Context, in snapshotQuantityInput FilterGroupBy: in.FeatureMeter.Feature.MeterGroupByFilters, FilterStoredAt: &filter.FilterTimeUnix{ FilterTime: filter.FilterTime{ - Lt: &in.StoredAtOffset, + Lt: &in.StoredAtLT, }, }, } diff --git a/openmeter/billing/charges/usagebased/service/rating/service_test.go b/openmeter/billing/charges/usagebased/service/rating/service_test.go index 0fedbf2ad6..de55de5cd6 100644 --- a/openmeter/billing/charges/usagebased/service/rating/service_test.go +++ b/openmeter/billing/charges/usagebased/service/rating/service_test.go @@ -179,7 +179,8 @@ func newGetDetailedLinesForUsageFixture(t *testing.T, result billingrating.Gener ValueProperty: lo.ToPtr("value"), }, }, - StoredAtOffset: time.Date(2025, 2, 2, 0, 0, 0, 0, time.UTC), + ServicePeriodTo: servicePeriod.To, + StoredAtLT: time.Date(2025, 2, 2, 0, 0, 0, 0, time.UTC), }, rater: ratingService, } diff --git a/openmeter/billing/charges/usagebased/service/rating/totals.go b/openmeter/billing/charges/usagebased/service/rating/totals.go index 55ecea85ba..b70a38ac84 100644 --- a/openmeter/billing/charges/usagebased/service/rating/totals.go +++ b/openmeter/billing/charges/usagebased/service/rating/totals.go @@ -12,10 +12,10 @@ import ( ) type GetTotalsForUsageInput struct { - Charge usagebased.Charge - Customer billing.CustomerOverrideWithDetails - FeatureMeter feature.FeatureMeter - StoredAtOffset time.Time + Charge usagebased.Charge + Customer billing.CustomerOverrideWithDetails + FeatureMeter feature.FeatureMeter + StoredAtLT time.Time } func (i GetTotalsForUsageInput) Validate() error { @@ -31,8 +31,8 @@ func (i GetTotalsForUsageInput) Validate() error { return fmt.Errorf("feature meter is required") } - if i.StoredAtOffset.IsZero() { - return fmt.Errorf("stored at offset is required") + if i.StoredAtLT.IsZero() { + return fmt.Errorf("stored at lt is required") } return nil @@ -46,10 +46,10 @@ func (s *service) GetTotalsForUsage(ctx context.Context, in GetTotalsForUsageInp } snapshotQuantity, err := s.snapshotQuantity(ctx, snapshotQuantityInput{ - Customer: in.Customer.Customer, - FeatureMeter: in.FeatureMeter, - ServicePeriod: in.Charge.Intent.ServicePeriod, - StoredAtOffset: in.StoredAtOffset, + Customer: in.Customer.Customer, + FeatureMeter: in.FeatureMeter, + ServicePeriod: in.Charge.Intent.ServicePeriod, + StoredAtLT: in.StoredAtLT, }) if err != nil { return totals.Totals{}, fmt.Errorf("get snapshot quantity: %w", err) diff --git a/openmeter/billing/charges/usagebased/service/run/create.go b/openmeter/billing/charges/usagebased/service/run/create.go index ad5dd5029e..bd0f8fa34b 100644 --- a/openmeter/billing/charges/usagebased/service/run/create.go +++ b/openmeter/billing/charges/usagebased/service/run/create.go @@ -21,8 +21,8 @@ type CreateRatedRunInput struct { CustomerOverride billing.CustomerOverrideWithDetails FeatureMeter feature.FeatureMeter Type usagebased.RealizationRunType - AsOf time.Time - CollectionEnd time.Time + StoredAtLT time.Time + ServicePeriodTo time.Time LineID *string // IgnoreMinimumCommitment keeps interim realization runs from booking final-only minimum commitment. IgnoreMinimumCommitment bool @@ -51,12 +51,21 @@ func (i CreateRatedRunInput) Validate() error { return fmt.Errorf("type: %w", err) } - if i.AsOf.IsZero() { - return fmt.Errorf("as of is required") + if i.StoredAtLT.IsZero() { + return fmt.Errorf("stored at lt is required") } - if i.CollectionEnd.IsZero() { - return fmt.Errorf("collection end is required") + if i.ServicePeriodTo.IsZero() { + return fmt.Errorf("service period to is required") + } + + period := i.Charge.Intent.ServicePeriod + if !i.ServicePeriodTo.After(period.From) { + return fmt.Errorf("service period to must be after charge service period from") + } + + if i.ServicePeriodTo.After(period.To) { + return fmt.Errorf("service period to must not be after charge service period to") } if i.LineID != nil && *i.LineID == "" { @@ -126,7 +135,8 @@ func (s *Service) CreateRatedRun(ctx context.Context, in CreateRatedRunInput) (C PriorRuns: in.Charge.Realizations, Customer: in.CustomerOverride, FeatureMeter: in.FeatureMeter, - StoredAtOffset: in.AsOf, + ServicePeriodTo: in.ServicePeriodTo, + StoredAtLT: in.StoredAtLT, IgnoreMinimumCommitment: in.IgnoreMinimumCommitment, }) if err != nil { @@ -143,13 +153,13 @@ func (s *Service) CreateRatedRun(ctx context.Context, in CreateRatedRunInput) (C } updatedCharge, err := s.createNewRealizationRun(ctx, in.Charge, usagebased.CreateRealizationRunInput{ - FeatureID: in.Charge.State.FeatureID, - Type: in.Type, - AsOf: in.AsOf, - CollectionEnd: in.CollectionEnd, - LineID: in.LineID, - MeterValue: ratingResult.Quantity, - Totals: runTotals, + FeatureID: in.Charge.State.FeatureID, + Type: in.Type, + StoredAtLT: in.StoredAtLT, + ServicePeriodTo: in.ServicePeriodTo, + LineID: in.LineID, + MeteredQuantity: ratingResult.Quantity, + Totals: runTotals, }) if err != nil { return CreateRatedRunResult{}, fmt.Errorf("create new realization run: %w", err) @@ -170,7 +180,7 @@ func (s *Service) CreateRatedRun(ctx context.Context, in CreateRatedRunInput) (C allocationResult, err := s.allocate(ctx, allocateCreditRealizationsInput{ Charge: updatedCharge, Run: currentRun, - AllocateAt: in.AsOf, + AllocateAt: in.StoredAtLT, AmountToAllocate: runTotals.Total, CurrencyCalculator: in.CurrencyCalculator, Exact: in.CreditAllocation == CreditAllocationExact, @@ -184,10 +194,10 @@ func (s *Service) CreateRatedRun(ctx context.Context, in CreateRatedRunInput) (C runTotals.Total = in.CurrencyCalculator.RoundToPrecision(runTotals.Total.Sub(allocationResult.Allocated)) currentRunBase, err := s.adapter.UpdateRealizationRun(ctx, usagebased.UpdateRealizationRunInput{ - ID: currentRun.ID, - AsOf: mo.Some(in.AsOf), - MeterValue: mo.Some(ratingResult.Quantity), - Totals: mo.Some(runTotals), + ID: currentRun.ID, + StoredAtLT: mo.Some(in.StoredAtLT), + MeteredQuantity: mo.Some(ratingResult.Quantity), + Totals: mo.Some(runTotals), }) if err != nil { return CreateRatedRunResult{}, fmt.Errorf("update realization run: %w", err) diff --git a/openmeter/billing/charges/usagebased/service/run/payment_test.go b/openmeter/billing/charges/usagebased/service/run/payment_test.go index 4e40eba3eb..7bead7d444 100644 --- a/openmeter/billing/charges/usagebased/service/run/payment_test.go +++ b/openmeter/billing/charges/usagebased/service/run/payment_test.go @@ -197,14 +197,14 @@ func newUsageBasedRun(lineID string) usagebased.RealizationRun { now := time.Now().UTC() return usagebased.RealizationRun{ RealizationRunBase: usagebased.RealizationRunBase{ - ID: usagebased.RealizationRunID(models.NamespacedID{Namespace: "ns", ID: "run-1"}), - ManagedModel: models.ManagedModel{CreatedAt: now, UpdatedAt: now}, - FeatureID: "feature-1", - LineID: &lineID, - Type: usagebased.RealizationRunTypeFinalRealization, - AsOf: now, - CollectionEnd: now, - MeterValue: alpacadecimal.NewFromInt(10), + ID: usagebased.RealizationRunID(models.NamespacedID{Namespace: "ns", ID: "run-1"}), + ManagedModel: models.ManagedModel{CreatedAt: now, UpdatedAt: now}, + FeatureID: "feature-1", + LineID: &lineID, + Type: usagebased.RealizationRunTypeFinalRealization, + StoredAtLT: now, + ServicePeriodTo: now, + MeteredQuantity: alpacadecimal.NewFromInt(10), Totals: totals.Totals{ Amount: alpacadecimal.NewFromInt(10), Total: alpacadecimal.NewFromInt(10), diff --git a/openmeter/billing/charges/usagebased/service/statemachine.go b/openmeter/billing/charges/usagebased/service/statemachine.go index aec51126ba..a49f4ac7a7 100644 --- a/openmeter/billing/charges/usagebased/service/statemachine.go +++ b/openmeter/billing/charges/usagebased/service/statemachine.go @@ -147,33 +147,33 @@ func (s *stateMachine) AdvanceAfterServicePeriodFrom(ctx context.Context) error } func (s *stateMachine) AdvanceAfterCollectionPeriodEnd(ctx context.Context) error { - collectionPeriodEnd, err := s.getCurrentRunCollectionEnd() + snapshotAfter, err := s.getCurrentRunSnapshotAfter() if err != nil { return err } - s.Charge.State.AdvanceAfter = lo.ToPtr(meta.NormalizeTimestamp(collectionPeriodEnd.Add(usagebased.InternalCollectionPeriod))) + s.Charge.State.AdvanceAfter = lo.ToPtr(snapshotAfter) return nil } func (s *stateMachine) IsAfterCollectionPeriod(ctx context.Context, _ ...any) bool { - collectionPeriodEnd, err := s.getCurrentRunCollectionEnd() + snapshotAfter, err := s.getCurrentRunSnapshotAfter() if err != nil { - s.Logger.ErrorContext(ctx, "failed to get collection period end", "error", err, "customerID", s.Charge.Intent.CustomerID) + s.Logger.ErrorContext(ctx, "failed to get snapshot after", "error", err, "customerID", s.Charge.Intent.CustomerID) return false } - return !clock.Now().Before(collectionPeriodEnd.Add(usagebased.InternalCollectionPeriod)) + return !clock.Now().Before(snapshotAfter) } -func (s *stateMachine) GetCollectionPeriodEnd(_ context.Context) (time.Time, error) { +func (s *stateMachine) getFinalRunStoredAtLT() (time.Time, error) { collectionPeriod := s.CustomerOverride.MergedProfile.WorkflowConfig.Collection.Interval - collectionPeriodEnd, _ := collectionPeriod.AddTo(s.Charge.Intent.ServicePeriod.To) - return meta.NormalizeTimestamp(collectionPeriodEnd), nil + storedAtLT, _ := collectionPeriod.AddTo(s.Charge.Intent.ServicePeriod.To) + return meta.NormalizeTimestamp(storedAtLT), nil } -func (s *stateMachine) getCurrentRunCollectionEnd() (time.Time, error) { +func (s *stateMachine) getCurrentRunSnapshotAfter() (time.Time, error) { if s.Charge.State.CurrentRealizationRunID == nil { return time.Time{}, fmt.Errorf("no realization run in progress [charge_id=%s]", s.Charge.ID) } @@ -183,7 +183,7 @@ func (s *stateMachine) getCurrentRunCollectionEnd() (time.Time, error) { return time.Time{}, fmt.Errorf("get current realization run: %w", err) } - return meta.NormalizeTimestamp(currentRun.CollectionEnd), nil + return meta.NormalizeTimestamp(currentRun.StoredAtLT.Add(usagebased.InternalCollectionPeriod)), nil } func (s *stateMachine) ensureDetailedLinesLoadedForRating(ctx context.Context) error { diff --git a/openmeter/billing/worker/subscriptionsync/service/creditsonly_test.go b/openmeter/billing/worker/subscriptionsync/service/creditsonly_test.go index 1040ede65d..a158aa4a4a 100644 --- a/openmeter/billing/worker/subscriptionsync/service/creditsonly_test.go +++ b/openmeter/billing/worker/subscriptionsync/service/creditsonly_test.go @@ -957,7 +957,7 @@ func (s *CreditsOnlySubscriptionHandlerTestSuite) TestCreditsOnlyUsageBasedMidPe s.Len(afterShrinkCharge.Realizations, 1) finalRun := afterShrinkCharge.Realizations[0] - s.Equal(float64(8), finalRun.MeterValue.InexactFloat64()) + s.Equal(float64(8), finalRun.MeteredQuantity.InexactFloat64()) s.Equal(float64(0), finalRun.Totals.Total.InexactFloat64()) s.Equal(float64(8), finalRun.Totals.CreditsTotal.InexactFloat64()) s.Len(finalRun.CreditsAllocated, 1) diff --git a/openmeter/ent/db/chargeusagebasedruns.go b/openmeter/ent/db/chargeusagebasedruns.go index 56d222a6c7..fe589a6411 100644 --- a/openmeter/ent/db/chargeusagebasedruns.go +++ b/openmeter/ent/db/chargeusagebasedruns.go @@ -54,14 +54,14 @@ type ChargeUsageBasedRuns struct { FeatureID string `json:"feature_id,omitempty"` // Type holds the value of the "type" field. Type usagebased.RealizationRunType `json:"type,omitempty"` - // Asof holds the value of the "asof" field. - Asof time.Time `json:"asof,omitempty"` - // CollectionEnd holds the value of the "collection_end" field. - CollectionEnd time.Time `json:"collection_end,omitempty"` + // StoredAtLt holds the value of the "stored_at_lt" field. + StoredAtLt time.Time `json:"stored_at_lt,omitempty"` + // ServicePeriodTo holds the value of the "service_period_to" field. + ServicePeriodTo time.Time `json:"service_period_to,omitempty"` // LineID holds the value of the "line_id" field. LineID *string `json:"line_id,omitempty"` - // MeterValue holds the value of the "meter_value" field. - MeterValue alpacadecimal.Decimal `json:"meter_value,omitempty"` + // MeteredQuantity holds the value of the "metered_quantity" field. + MeteredQuantity alpacadecimal.Decimal `json:"metered_quantity,omitempty"` // Edges holds the relations/edges for other nodes in the graph. // The values are being populated by the ChargeUsageBasedRunsQuery when eager-loading is set. Edges ChargeUsageBasedRunsEdges `json:"edges"` @@ -167,11 +167,11 @@ func (*ChargeUsageBasedRuns) scanValues(columns []string) ([]any, error) { values := make([]any, len(columns)) for i := range columns { switch columns[i] { - case chargeusagebasedruns.FieldAmount, chargeusagebasedruns.FieldTaxesTotal, chargeusagebasedruns.FieldTaxesInclusiveTotal, chargeusagebasedruns.FieldTaxesExclusiveTotal, chargeusagebasedruns.FieldChargesTotal, chargeusagebasedruns.FieldDiscountsTotal, chargeusagebasedruns.FieldCreditsTotal, chargeusagebasedruns.FieldTotal, chargeusagebasedruns.FieldMeterValue: + case chargeusagebasedruns.FieldAmount, chargeusagebasedruns.FieldTaxesTotal, chargeusagebasedruns.FieldTaxesInclusiveTotal, chargeusagebasedruns.FieldTaxesExclusiveTotal, chargeusagebasedruns.FieldChargesTotal, chargeusagebasedruns.FieldDiscountsTotal, chargeusagebasedruns.FieldCreditsTotal, chargeusagebasedruns.FieldTotal, chargeusagebasedruns.FieldMeteredQuantity: values[i] = new(alpacadecimal.Decimal) case chargeusagebasedruns.FieldID, chargeusagebasedruns.FieldNamespace, chargeusagebasedruns.FieldChargeID, chargeusagebasedruns.FieldFeatureID, chargeusagebasedruns.FieldType, chargeusagebasedruns.FieldLineID: values[i] = new(sql.NullString) - case chargeusagebasedruns.FieldCreatedAt, chargeusagebasedruns.FieldUpdatedAt, chargeusagebasedruns.FieldDeletedAt, chargeusagebasedruns.FieldAsof, chargeusagebasedruns.FieldCollectionEnd: + case chargeusagebasedruns.FieldCreatedAt, chargeusagebasedruns.FieldUpdatedAt, chargeusagebasedruns.FieldDeletedAt, chargeusagebasedruns.FieldStoredAtLt, chargeusagebasedruns.FieldServicePeriodTo: values[i] = new(sql.NullTime) default: values[i] = new(sql.UnknownType) @@ -285,17 +285,17 @@ func (_m *ChargeUsageBasedRuns) assignValues(columns []string, values []any) err } else if value.Valid { _m.Type = usagebased.RealizationRunType(value.String) } - case chargeusagebasedruns.FieldAsof: + case chargeusagebasedruns.FieldStoredAtLt: if value, ok := values[i].(*sql.NullTime); !ok { - return fmt.Errorf("unexpected type %T for field asof", values[i]) + return fmt.Errorf("unexpected type %T for field stored_at_lt", values[i]) } else if value.Valid { - _m.Asof = value.Time + _m.StoredAtLt = value.Time } - case chargeusagebasedruns.FieldCollectionEnd: + case chargeusagebasedruns.FieldServicePeriodTo: if value, ok := values[i].(*sql.NullTime); !ok { - return fmt.Errorf("unexpected type %T for field collection_end", values[i]) + return fmt.Errorf("unexpected type %T for field service_period_to", values[i]) } else if value.Valid { - _m.CollectionEnd = value.Time + _m.ServicePeriodTo = value.Time } case chargeusagebasedruns.FieldLineID: if value, ok := values[i].(*sql.NullString); !ok { @@ -304,11 +304,11 @@ func (_m *ChargeUsageBasedRuns) assignValues(columns []string, values []any) err _m.LineID = new(string) *_m.LineID = value.String } - case chargeusagebasedruns.FieldMeterValue: + case chargeusagebasedruns.FieldMeteredQuantity: if value, ok := values[i].(*alpacadecimal.Decimal); !ok { - return fmt.Errorf("unexpected type %T for field meter_value", values[i]) + return fmt.Errorf("unexpected type %T for field metered_quantity", values[i]) } else if value != nil { - _m.MeterValue = *value + _m.MeteredQuantity = *value } default: _m.selectValues.Set(columns[i], values[i]) @@ -428,19 +428,19 @@ func (_m *ChargeUsageBasedRuns) String() string { builder.WriteString("type=") builder.WriteString(fmt.Sprintf("%v", _m.Type)) builder.WriteString(", ") - builder.WriteString("asof=") - builder.WriteString(_m.Asof.Format(time.ANSIC)) + builder.WriteString("stored_at_lt=") + builder.WriteString(_m.StoredAtLt.Format(time.ANSIC)) builder.WriteString(", ") - builder.WriteString("collection_end=") - builder.WriteString(_m.CollectionEnd.Format(time.ANSIC)) + builder.WriteString("service_period_to=") + builder.WriteString(_m.ServicePeriodTo.Format(time.ANSIC)) builder.WriteString(", ") if v := _m.LineID; v != nil { builder.WriteString("line_id=") builder.WriteString(*v) } builder.WriteString(", ") - builder.WriteString("meter_value=") - builder.WriteString(fmt.Sprintf("%v", _m.MeterValue)) + builder.WriteString("metered_quantity=") + builder.WriteString(fmt.Sprintf("%v", _m.MeteredQuantity)) builder.WriteByte(')') return builder.String() } diff --git a/openmeter/ent/db/chargeusagebasedruns/chargeusagebasedruns.go b/openmeter/ent/db/chargeusagebasedruns/chargeusagebasedruns.go index 99e40ac4f4..68d5ea8ecd 100644 --- a/openmeter/ent/db/chargeusagebasedruns/chargeusagebasedruns.go +++ b/openmeter/ent/db/chargeusagebasedruns/chargeusagebasedruns.go @@ -46,14 +46,14 @@ const ( FieldFeatureID = "feature_id" // FieldType holds the string denoting the type field in the database. FieldType = "type" - // FieldAsof holds the string denoting the asof field in the database. - FieldAsof = "asof" - // FieldCollectionEnd holds the string denoting the collection_end field in the database. - FieldCollectionEnd = "collection_end" + // FieldStoredAtLt holds the string denoting the stored_at_lt field in the database. + FieldStoredAtLt = "stored_at_lt" + // FieldServicePeriodTo holds the string denoting the service_period_to field in the database. + FieldServicePeriodTo = "service_period_to" // FieldLineID holds the string denoting the line_id field in the database. FieldLineID = "line_id" - // FieldMeterValue holds the string denoting the meter_value field in the database. - FieldMeterValue = "meter_value" + // FieldMeteredQuantity holds the string denoting the metered_quantity field in the database. + FieldMeteredQuantity = "metered_quantity" // EdgeUsageBased holds the string denoting the usage_based edge name in mutations. EdgeUsageBased = "usage_based" // EdgeFeature holds the string denoting the feature edge name in mutations. @@ -139,10 +139,10 @@ var Columns = []string{ FieldChargeID, FieldFeatureID, FieldType, - FieldAsof, - FieldCollectionEnd, + FieldStoredAtLt, + FieldServicePeriodTo, FieldLineID, - FieldMeterValue, + FieldMeteredQuantity, } // ValidColumn reports if the column name is valid (part of the table columns). @@ -265,14 +265,14 @@ func ByType(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldType, opts...).ToFunc() } -// ByAsof orders the results by the asof field. -func ByAsof(opts ...sql.OrderTermOption) OrderOption { - return sql.OrderByField(FieldAsof, opts...).ToFunc() +// ByStoredAtLt orders the results by the stored_at_lt field. +func ByStoredAtLt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldStoredAtLt, opts...).ToFunc() } -// ByCollectionEnd orders the results by the collection_end field. -func ByCollectionEnd(opts ...sql.OrderTermOption) OrderOption { - return sql.OrderByField(FieldCollectionEnd, opts...).ToFunc() +// ByServicePeriodTo orders the results by the service_period_to field. +func ByServicePeriodTo(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldServicePeriodTo, opts...).ToFunc() } // ByLineID orders the results by the line_id field. @@ -280,9 +280,9 @@ func ByLineID(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldLineID, opts...).ToFunc() } -// ByMeterValue orders the results by the meter_value field. -func ByMeterValue(opts ...sql.OrderTermOption) OrderOption { - return sql.OrderByField(FieldMeterValue, opts...).ToFunc() +// ByMeteredQuantity orders the results by the metered_quantity field. +func ByMeteredQuantity(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldMeteredQuantity, opts...).ToFunc() } // ByUsageBasedField orders the results by usage_based field. diff --git a/openmeter/ent/db/chargeusagebasedruns/where.go b/openmeter/ent/db/chargeusagebasedruns/where.go index f376f547a1..9f4551ddd7 100644 --- a/openmeter/ent/db/chargeusagebasedruns/where.go +++ b/openmeter/ent/db/chargeusagebasedruns/where.go @@ -137,14 +137,14 @@ func FeatureID(v string) predicate.ChargeUsageBasedRuns { return predicate.ChargeUsageBasedRuns(sql.FieldEQ(FieldFeatureID, v)) } -// Asof applies equality check predicate on the "asof" field. It's identical to AsofEQ. -func Asof(v time.Time) predicate.ChargeUsageBasedRuns { - return predicate.ChargeUsageBasedRuns(sql.FieldEQ(FieldAsof, v)) +// StoredAtLt applies equality check predicate on the "stored_at_lt" field. It's identical to StoredAtLtEQ. +func StoredAtLt(v time.Time) predicate.ChargeUsageBasedRuns { + return predicate.ChargeUsageBasedRuns(sql.FieldEQ(FieldStoredAtLt, v)) } -// CollectionEnd applies equality check predicate on the "collection_end" field. It's identical to CollectionEndEQ. -func CollectionEnd(v time.Time) predicate.ChargeUsageBasedRuns { - return predicate.ChargeUsageBasedRuns(sql.FieldEQ(FieldCollectionEnd, v)) +// ServicePeriodTo applies equality check predicate on the "service_period_to" field. It's identical to ServicePeriodToEQ. +func ServicePeriodTo(v time.Time) predicate.ChargeUsageBasedRuns { + return predicate.ChargeUsageBasedRuns(sql.FieldEQ(FieldServicePeriodTo, v)) } // LineID applies equality check predicate on the "line_id" field. It's identical to LineIDEQ. @@ -152,9 +152,9 @@ func LineID(v string) predicate.ChargeUsageBasedRuns { return predicate.ChargeUsageBasedRuns(sql.FieldEQ(FieldLineID, v)) } -// MeterValue applies equality check predicate on the "meter_value" field. It's identical to MeterValueEQ. -func MeterValue(v alpacadecimal.Decimal) predicate.ChargeUsageBasedRuns { - return predicate.ChargeUsageBasedRuns(sql.FieldEQ(FieldMeterValue, v)) +// MeteredQuantity applies equality check predicate on the "metered_quantity" field. It's identical to MeteredQuantityEQ. +func MeteredQuantity(v alpacadecimal.Decimal) predicate.ChargeUsageBasedRuns { + return predicate.ChargeUsageBasedRuns(sql.FieldEQ(FieldMeteredQuantity, v)) } // NamespaceEQ applies the EQ predicate on the "namespace" field. @@ -832,84 +832,84 @@ func TypeNotIn(vs ...usagebased.RealizationRunType) predicate.ChargeUsageBasedRu return predicate.ChargeUsageBasedRuns(sql.FieldNotIn(FieldType, v...)) } -// AsofEQ applies the EQ predicate on the "asof" field. -func AsofEQ(v time.Time) predicate.ChargeUsageBasedRuns { - return predicate.ChargeUsageBasedRuns(sql.FieldEQ(FieldAsof, v)) +// StoredAtLtEQ applies the EQ predicate on the "stored_at_lt" field. +func StoredAtLtEQ(v time.Time) predicate.ChargeUsageBasedRuns { + return predicate.ChargeUsageBasedRuns(sql.FieldEQ(FieldStoredAtLt, v)) } -// AsofNEQ applies the NEQ predicate on the "asof" field. -func AsofNEQ(v time.Time) predicate.ChargeUsageBasedRuns { - return predicate.ChargeUsageBasedRuns(sql.FieldNEQ(FieldAsof, v)) +// StoredAtLtNEQ applies the NEQ predicate on the "stored_at_lt" field. +func StoredAtLtNEQ(v time.Time) predicate.ChargeUsageBasedRuns { + return predicate.ChargeUsageBasedRuns(sql.FieldNEQ(FieldStoredAtLt, v)) } -// AsofIn applies the In predicate on the "asof" field. -func AsofIn(vs ...time.Time) predicate.ChargeUsageBasedRuns { - return predicate.ChargeUsageBasedRuns(sql.FieldIn(FieldAsof, vs...)) +// StoredAtLtIn applies the In predicate on the "stored_at_lt" field. +func StoredAtLtIn(vs ...time.Time) predicate.ChargeUsageBasedRuns { + return predicate.ChargeUsageBasedRuns(sql.FieldIn(FieldStoredAtLt, vs...)) } -// AsofNotIn applies the NotIn predicate on the "asof" field. -func AsofNotIn(vs ...time.Time) predicate.ChargeUsageBasedRuns { - return predicate.ChargeUsageBasedRuns(sql.FieldNotIn(FieldAsof, vs...)) +// StoredAtLtNotIn applies the NotIn predicate on the "stored_at_lt" field. +func StoredAtLtNotIn(vs ...time.Time) predicate.ChargeUsageBasedRuns { + return predicate.ChargeUsageBasedRuns(sql.FieldNotIn(FieldStoredAtLt, vs...)) } -// AsofGT applies the GT predicate on the "asof" field. -func AsofGT(v time.Time) predicate.ChargeUsageBasedRuns { - return predicate.ChargeUsageBasedRuns(sql.FieldGT(FieldAsof, v)) +// StoredAtLtGT applies the GT predicate on the "stored_at_lt" field. +func StoredAtLtGT(v time.Time) predicate.ChargeUsageBasedRuns { + return predicate.ChargeUsageBasedRuns(sql.FieldGT(FieldStoredAtLt, v)) } -// AsofGTE applies the GTE predicate on the "asof" field. -func AsofGTE(v time.Time) predicate.ChargeUsageBasedRuns { - return predicate.ChargeUsageBasedRuns(sql.FieldGTE(FieldAsof, v)) +// StoredAtLtGTE applies the GTE predicate on the "stored_at_lt" field. +func StoredAtLtGTE(v time.Time) predicate.ChargeUsageBasedRuns { + return predicate.ChargeUsageBasedRuns(sql.FieldGTE(FieldStoredAtLt, v)) } -// AsofLT applies the LT predicate on the "asof" field. -func AsofLT(v time.Time) predicate.ChargeUsageBasedRuns { - return predicate.ChargeUsageBasedRuns(sql.FieldLT(FieldAsof, v)) +// StoredAtLtLT applies the LT predicate on the "stored_at_lt" field. +func StoredAtLtLT(v time.Time) predicate.ChargeUsageBasedRuns { + return predicate.ChargeUsageBasedRuns(sql.FieldLT(FieldStoredAtLt, v)) } -// AsofLTE applies the LTE predicate on the "asof" field. -func AsofLTE(v time.Time) predicate.ChargeUsageBasedRuns { - return predicate.ChargeUsageBasedRuns(sql.FieldLTE(FieldAsof, v)) +// StoredAtLtLTE applies the LTE predicate on the "stored_at_lt" field. +func StoredAtLtLTE(v time.Time) predicate.ChargeUsageBasedRuns { + return predicate.ChargeUsageBasedRuns(sql.FieldLTE(FieldStoredAtLt, v)) } -// CollectionEndEQ applies the EQ predicate on the "collection_end" field. -func CollectionEndEQ(v time.Time) predicate.ChargeUsageBasedRuns { - return predicate.ChargeUsageBasedRuns(sql.FieldEQ(FieldCollectionEnd, v)) +// ServicePeriodToEQ applies the EQ predicate on the "service_period_to" field. +func ServicePeriodToEQ(v time.Time) predicate.ChargeUsageBasedRuns { + return predicate.ChargeUsageBasedRuns(sql.FieldEQ(FieldServicePeriodTo, v)) } -// CollectionEndNEQ applies the NEQ predicate on the "collection_end" field. -func CollectionEndNEQ(v time.Time) predicate.ChargeUsageBasedRuns { - return predicate.ChargeUsageBasedRuns(sql.FieldNEQ(FieldCollectionEnd, v)) +// ServicePeriodToNEQ applies the NEQ predicate on the "service_period_to" field. +func ServicePeriodToNEQ(v time.Time) predicate.ChargeUsageBasedRuns { + return predicate.ChargeUsageBasedRuns(sql.FieldNEQ(FieldServicePeriodTo, v)) } -// CollectionEndIn applies the In predicate on the "collection_end" field. -func CollectionEndIn(vs ...time.Time) predicate.ChargeUsageBasedRuns { - return predicate.ChargeUsageBasedRuns(sql.FieldIn(FieldCollectionEnd, vs...)) +// ServicePeriodToIn applies the In predicate on the "service_period_to" field. +func ServicePeriodToIn(vs ...time.Time) predicate.ChargeUsageBasedRuns { + return predicate.ChargeUsageBasedRuns(sql.FieldIn(FieldServicePeriodTo, vs...)) } -// CollectionEndNotIn applies the NotIn predicate on the "collection_end" field. -func CollectionEndNotIn(vs ...time.Time) predicate.ChargeUsageBasedRuns { - return predicate.ChargeUsageBasedRuns(sql.FieldNotIn(FieldCollectionEnd, vs...)) +// ServicePeriodToNotIn applies the NotIn predicate on the "service_period_to" field. +func ServicePeriodToNotIn(vs ...time.Time) predicate.ChargeUsageBasedRuns { + return predicate.ChargeUsageBasedRuns(sql.FieldNotIn(FieldServicePeriodTo, vs...)) } -// CollectionEndGT applies the GT predicate on the "collection_end" field. -func CollectionEndGT(v time.Time) predicate.ChargeUsageBasedRuns { - return predicate.ChargeUsageBasedRuns(sql.FieldGT(FieldCollectionEnd, v)) +// ServicePeriodToGT applies the GT predicate on the "service_period_to" field. +func ServicePeriodToGT(v time.Time) predicate.ChargeUsageBasedRuns { + return predicate.ChargeUsageBasedRuns(sql.FieldGT(FieldServicePeriodTo, v)) } -// CollectionEndGTE applies the GTE predicate on the "collection_end" field. -func CollectionEndGTE(v time.Time) predicate.ChargeUsageBasedRuns { - return predicate.ChargeUsageBasedRuns(sql.FieldGTE(FieldCollectionEnd, v)) +// ServicePeriodToGTE applies the GTE predicate on the "service_period_to" field. +func ServicePeriodToGTE(v time.Time) predicate.ChargeUsageBasedRuns { + return predicate.ChargeUsageBasedRuns(sql.FieldGTE(FieldServicePeriodTo, v)) } -// CollectionEndLT applies the LT predicate on the "collection_end" field. -func CollectionEndLT(v time.Time) predicate.ChargeUsageBasedRuns { - return predicate.ChargeUsageBasedRuns(sql.FieldLT(FieldCollectionEnd, v)) +// ServicePeriodToLT applies the LT predicate on the "service_period_to" field. +func ServicePeriodToLT(v time.Time) predicate.ChargeUsageBasedRuns { + return predicate.ChargeUsageBasedRuns(sql.FieldLT(FieldServicePeriodTo, v)) } -// CollectionEndLTE applies the LTE predicate on the "collection_end" field. -func CollectionEndLTE(v time.Time) predicate.ChargeUsageBasedRuns { - return predicate.ChargeUsageBasedRuns(sql.FieldLTE(FieldCollectionEnd, v)) +// ServicePeriodToLTE applies the LTE predicate on the "service_period_to" field. +func ServicePeriodToLTE(v time.Time) predicate.ChargeUsageBasedRuns { + return predicate.ChargeUsageBasedRuns(sql.FieldLTE(FieldServicePeriodTo, v)) } // LineIDEQ applies the EQ predicate on the "line_id" field. @@ -987,44 +987,44 @@ func LineIDContainsFold(v string) predicate.ChargeUsageBasedRuns { return predicate.ChargeUsageBasedRuns(sql.FieldContainsFold(FieldLineID, v)) } -// MeterValueEQ applies the EQ predicate on the "meter_value" field. -func MeterValueEQ(v alpacadecimal.Decimal) predicate.ChargeUsageBasedRuns { - return predicate.ChargeUsageBasedRuns(sql.FieldEQ(FieldMeterValue, v)) +// MeteredQuantityEQ applies the EQ predicate on the "metered_quantity" field. +func MeteredQuantityEQ(v alpacadecimal.Decimal) predicate.ChargeUsageBasedRuns { + return predicate.ChargeUsageBasedRuns(sql.FieldEQ(FieldMeteredQuantity, v)) } -// MeterValueNEQ applies the NEQ predicate on the "meter_value" field. -func MeterValueNEQ(v alpacadecimal.Decimal) predicate.ChargeUsageBasedRuns { - return predicate.ChargeUsageBasedRuns(sql.FieldNEQ(FieldMeterValue, v)) +// MeteredQuantityNEQ applies the NEQ predicate on the "metered_quantity" field. +func MeteredQuantityNEQ(v alpacadecimal.Decimal) predicate.ChargeUsageBasedRuns { + return predicate.ChargeUsageBasedRuns(sql.FieldNEQ(FieldMeteredQuantity, v)) } -// MeterValueIn applies the In predicate on the "meter_value" field. -func MeterValueIn(vs ...alpacadecimal.Decimal) predicate.ChargeUsageBasedRuns { - return predicate.ChargeUsageBasedRuns(sql.FieldIn(FieldMeterValue, vs...)) +// MeteredQuantityIn applies the In predicate on the "metered_quantity" field. +func MeteredQuantityIn(vs ...alpacadecimal.Decimal) predicate.ChargeUsageBasedRuns { + return predicate.ChargeUsageBasedRuns(sql.FieldIn(FieldMeteredQuantity, vs...)) } -// MeterValueNotIn applies the NotIn predicate on the "meter_value" field. -func MeterValueNotIn(vs ...alpacadecimal.Decimal) predicate.ChargeUsageBasedRuns { - return predicate.ChargeUsageBasedRuns(sql.FieldNotIn(FieldMeterValue, vs...)) +// MeteredQuantityNotIn applies the NotIn predicate on the "metered_quantity" field. +func MeteredQuantityNotIn(vs ...alpacadecimal.Decimal) predicate.ChargeUsageBasedRuns { + return predicate.ChargeUsageBasedRuns(sql.FieldNotIn(FieldMeteredQuantity, vs...)) } -// MeterValueGT applies the GT predicate on the "meter_value" field. -func MeterValueGT(v alpacadecimal.Decimal) predicate.ChargeUsageBasedRuns { - return predicate.ChargeUsageBasedRuns(sql.FieldGT(FieldMeterValue, v)) +// MeteredQuantityGT applies the GT predicate on the "metered_quantity" field. +func MeteredQuantityGT(v alpacadecimal.Decimal) predicate.ChargeUsageBasedRuns { + return predicate.ChargeUsageBasedRuns(sql.FieldGT(FieldMeteredQuantity, v)) } -// MeterValueGTE applies the GTE predicate on the "meter_value" field. -func MeterValueGTE(v alpacadecimal.Decimal) predicate.ChargeUsageBasedRuns { - return predicate.ChargeUsageBasedRuns(sql.FieldGTE(FieldMeterValue, v)) +// MeteredQuantityGTE applies the GTE predicate on the "metered_quantity" field. +func MeteredQuantityGTE(v alpacadecimal.Decimal) predicate.ChargeUsageBasedRuns { + return predicate.ChargeUsageBasedRuns(sql.FieldGTE(FieldMeteredQuantity, v)) } -// MeterValueLT applies the LT predicate on the "meter_value" field. -func MeterValueLT(v alpacadecimal.Decimal) predicate.ChargeUsageBasedRuns { - return predicate.ChargeUsageBasedRuns(sql.FieldLT(FieldMeterValue, v)) +// MeteredQuantityLT applies the LT predicate on the "metered_quantity" field. +func MeteredQuantityLT(v alpacadecimal.Decimal) predicate.ChargeUsageBasedRuns { + return predicate.ChargeUsageBasedRuns(sql.FieldLT(FieldMeteredQuantity, v)) } -// MeterValueLTE applies the LTE predicate on the "meter_value" field. -func MeterValueLTE(v alpacadecimal.Decimal) predicate.ChargeUsageBasedRuns { - return predicate.ChargeUsageBasedRuns(sql.FieldLTE(FieldMeterValue, v)) +// MeteredQuantityLTE applies the LTE predicate on the "metered_quantity" field. +func MeteredQuantityLTE(v alpacadecimal.Decimal) predicate.ChargeUsageBasedRuns { + return predicate.ChargeUsageBasedRuns(sql.FieldLTE(FieldMeteredQuantity, v)) } // HasUsageBased applies the HasEdge predicate on the "usage_based" edge. diff --git a/openmeter/ent/db/chargeusagebasedruns_create.go b/openmeter/ent/db/chargeusagebasedruns_create.go index aa36f656ef..669c44d419 100644 --- a/openmeter/ent/db/chargeusagebasedruns_create.go +++ b/openmeter/ent/db/chargeusagebasedruns_create.go @@ -146,15 +146,15 @@ func (_c *ChargeUsageBasedRunsCreate) SetType(v usagebased.RealizationRunType) * return _c } -// SetAsof sets the "asof" field. -func (_c *ChargeUsageBasedRunsCreate) SetAsof(v time.Time) *ChargeUsageBasedRunsCreate { - _c.mutation.SetAsof(v) +// SetStoredAtLt sets the "stored_at_lt" field. +func (_c *ChargeUsageBasedRunsCreate) SetStoredAtLt(v time.Time) *ChargeUsageBasedRunsCreate { + _c.mutation.SetStoredAtLt(v) return _c } -// SetCollectionEnd sets the "collection_end" field. -func (_c *ChargeUsageBasedRunsCreate) SetCollectionEnd(v time.Time) *ChargeUsageBasedRunsCreate { - _c.mutation.SetCollectionEnd(v) +// SetServicePeriodTo sets the "service_period_to" field. +func (_c *ChargeUsageBasedRunsCreate) SetServicePeriodTo(v time.Time) *ChargeUsageBasedRunsCreate { + _c.mutation.SetServicePeriodTo(v) return _c } @@ -172,9 +172,9 @@ func (_c *ChargeUsageBasedRunsCreate) SetNillableLineID(v *string) *ChargeUsageB return _c } -// SetMeterValue sets the "meter_value" field. -func (_c *ChargeUsageBasedRunsCreate) SetMeterValue(v alpacadecimal.Decimal) *ChargeUsageBasedRunsCreate { - _c.mutation.SetMeterValue(v) +// SetMeteredQuantity sets the "metered_quantity" field. +func (_c *ChargeUsageBasedRunsCreate) SetMeteredQuantity(v alpacadecimal.Decimal) *ChargeUsageBasedRunsCreate { + _c.mutation.SetMeteredQuantity(v) return _c } @@ -403,19 +403,19 @@ func (_c *ChargeUsageBasedRunsCreate) check() error { return &ValidationError{Name: "type", err: fmt.Errorf(`db: validator failed for field "ChargeUsageBasedRuns.type": %w`, err)} } } - if _, ok := _c.mutation.Asof(); !ok { - return &ValidationError{Name: "asof", err: errors.New(`db: missing required field "ChargeUsageBasedRuns.asof"`)} + if _, ok := _c.mutation.StoredAtLt(); !ok { + return &ValidationError{Name: "stored_at_lt", err: errors.New(`db: missing required field "ChargeUsageBasedRuns.stored_at_lt"`)} } - if _, ok := _c.mutation.CollectionEnd(); !ok { - return &ValidationError{Name: "collection_end", err: errors.New(`db: missing required field "ChargeUsageBasedRuns.collection_end"`)} + if _, ok := _c.mutation.ServicePeriodTo(); !ok { + return &ValidationError{Name: "service_period_to", err: errors.New(`db: missing required field "ChargeUsageBasedRuns.service_period_to"`)} } if v, ok := _c.mutation.LineID(); ok { if err := chargeusagebasedruns.LineIDValidator(v); err != nil { return &ValidationError{Name: "line_id", err: fmt.Errorf(`db: validator failed for field "ChargeUsageBasedRuns.line_id": %w`, err)} } } - if _, ok := _c.mutation.MeterValue(); !ok { - return &ValidationError{Name: "meter_value", err: errors.New(`db: missing required field "ChargeUsageBasedRuns.meter_value"`)} + if _, ok := _c.mutation.MeteredQuantity(); !ok { + return &ValidationError{Name: "metered_quantity", err: errors.New(`db: missing required field "ChargeUsageBasedRuns.metered_quantity"`)} } if len(_c.mutation.UsageBasedIDs()) == 0 { return &ValidationError{Name: "usage_based", err: errors.New(`db: missing required edge "ChargeUsageBasedRuns.usage_based"`)} @@ -511,17 +511,17 @@ func (_c *ChargeUsageBasedRunsCreate) createSpec() (*ChargeUsageBasedRuns, *sqlg _spec.SetField(chargeusagebasedruns.FieldType, field.TypeEnum, value) _node.Type = value } - if value, ok := _c.mutation.Asof(); ok { - _spec.SetField(chargeusagebasedruns.FieldAsof, field.TypeTime, value) - _node.Asof = value + if value, ok := _c.mutation.StoredAtLt(); ok { + _spec.SetField(chargeusagebasedruns.FieldStoredAtLt, field.TypeTime, value) + _node.StoredAtLt = value } - if value, ok := _c.mutation.CollectionEnd(); ok { - _spec.SetField(chargeusagebasedruns.FieldCollectionEnd, field.TypeTime, value) - _node.CollectionEnd = value + if value, ok := _c.mutation.ServicePeriodTo(); ok { + _spec.SetField(chargeusagebasedruns.FieldServicePeriodTo, field.TypeTime, value) + _node.ServicePeriodTo = value } - if value, ok := _c.mutation.MeterValue(); ok { - _spec.SetField(chargeusagebasedruns.FieldMeterValue, field.TypeOther, value) - _node.MeterValue = value + if value, ok := _c.mutation.MeteredQuantity(); ok { + _spec.SetField(chargeusagebasedruns.FieldMeteredQuantity, field.TypeOther, value) + _node.MeteredQuantity = value } if nodes := _c.mutation.UsageBasedIDs(); len(nodes) > 0 { edge := &sqlgraph.EdgeSpec{ @@ -816,15 +816,15 @@ func (u *ChargeUsageBasedRunsUpsert) UpdateTotal() *ChargeUsageBasedRunsUpsert { return u } -// SetAsof sets the "asof" field. -func (u *ChargeUsageBasedRunsUpsert) SetAsof(v time.Time) *ChargeUsageBasedRunsUpsert { - u.Set(chargeusagebasedruns.FieldAsof, v) +// SetStoredAtLt sets the "stored_at_lt" field. +func (u *ChargeUsageBasedRunsUpsert) SetStoredAtLt(v time.Time) *ChargeUsageBasedRunsUpsert { + u.Set(chargeusagebasedruns.FieldStoredAtLt, v) return u } -// UpdateAsof sets the "asof" field to the value that was provided on create. -func (u *ChargeUsageBasedRunsUpsert) UpdateAsof() *ChargeUsageBasedRunsUpsert { - u.SetExcluded(chargeusagebasedruns.FieldAsof) +// UpdateStoredAtLt sets the "stored_at_lt" field to the value that was provided on create. +func (u *ChargeUsageBasedRunsUpsert) UpdateStoredAtLt() *ChargeUsageBasedRunsUpsert { + u.SetExcluded(chargeusagebasedruns.FieldStoredAtLt) return u } @@ -846,15 +846,15 @@ func (u *ChargeUsageBasedRunsUpsert) ClearLineID() *ChargeUsageBasedRunsUpsert { return u } -// SetMeterValue sets the "meter_value" field. -func (u *ChargeUsageBasedRunsUpsert) SetMeterValue(v alpacadecimal.Decimal) *ChargeUsageBasedRunsUpsert { - u.Set(chargeusagebasedruns.FieldMeterValue, v) +// SetMeteredQuantity sets the "metered_quantity" field. +func (u *ChargeUsageBasedRunsUpsert) SetMeteredQuantity(v alpacadecimal.Decimal) *ChargeUsageBasedRunsUpsert { + u.Set(chargeusagebasedruns.FieldMeteredQuantity, v) return u } -// UpdateMeterValue sets the "meter_value" field to the value that was provided on create. -func (u *ChargeUsageBasedRunsUpsert) UpdateMeterValue() *ChargeUsageBasedRunsUpsert { - u.SetExcluded(chargeusagebasedruns.FieldMeterValue) +// UpdateMeteredQuantity sets the "metered_quantity" field to the value that was provided on create. +func (u *ChargeUsageBasedRunsUpsert) UpdateMeteredQuantity() *ChargeUsageBasedRunsUpsert { + u.SetExcluded(chargeusagebasedruns.FieldMeteredQuantity) return u } @@ -890,8 +890,8 @@ func (u *ChargeUsageBasedRunsUpsertOne) UpdateNewValues() *ChargeUsageBasedRunsU if _, exists := u.create.mutation.GetType(); exists { s.SetIgnore(chargeusagebasedruns.FieldType) } - if _, exists := u.create.mutation.CollectionEnd(); exists { - s.SetIgnore(chargeusagebasedruns.FieldCollectionEnd) + if _, exists := u.create.mutation.ServicePeriodTo(); exists { + s.SetIgnore(chargeusagebasedruns.FieldServicePeriodTo) } })) return u @@ -1071,17 +1071,17 @@ func (u *ChargeUsageBasedRunsUpsertOne) UpdateTotal() *ChargeUsageBasedRunsUpser }) } -// SetAsof sets the "asof" field. -func (u *ChargeUsageBasedRunsUpsertOne) SetAsof(v time.Time) *ChargeUsageBasedRunsUpsertOne { +// SetStoredAtLt sets the "stored_at_lt" field. +func (u *ChargeUsageBasedRunsUpsertOne) SetStoredAtLt(v time.Time) *ChargeUsageBasedRunsUpsertOne { return u.Update(func(s *ChargeUsageBasedRunsUpsert) { - s.SetAsof(v) + s.SetStoredAtLt(v) }) } -// UpdateAsof sets the "asof" field to the value that was provided on create. -func (u *ChargeUsageBasedRunsUpsertOne) UpdateAsof() *ChargeUsageBasedRunsUpsertOne { +// UpdateStoredAtLt sets the "stored_at_lt" field to the value that was provided on create. +func (u *ChargeUsageBasedRunsUpsertOne) UpdateStoredAtLt() *ChargeUsageBasedRunsUpsertOne { return u.Update(func(s *ChargeUsageBasedRunsUpsert) { - s.UpdateAsof() + s.UpdateStoredAtLt() }) } @@ -1106,17 +1106,17 @@ func (u *ChargeUsageBasedRunsUpsertOne) ClearLineID() *ChargeUsageBasedRunsUpser }) } -// SetMeterValue sets the "meter_value" field. -func (u *ChargeUsageBasedRunsUpsertOne) SetMeterValue(v alpacadecimal.Decimal) *ChargeUsageBasedRunsUpsertOne { +// SetMeteredQuantity sets the "metered_quantity" field. +func (u *ChargeUsageBasedRunsUpsertOne) SetMeteredQuantity(v alpacadecimal.Decimal) *ChargeUsageBasedRunsUpsertOne { return u.Update(func(s *ChargeUsageBasedRunsUpsert) { - s.SetMeterValue(v) + s.SetMeteredQuantity(v) }) } -// UpdateMeterValue sets the "meter_value" field to the value that was provided on create. -func (u *ChargeUsageBasedRunsUpsertOne) UpdateMeterValue() *ChargeUsageBasedRunsUpsertOne { +// UpdateMeteredQuantity sets the "metered_quantity" field to the value that was provided on create. +func (u *ChargeUsageBasedRunsUpsertOne) UpdateMeteredQuantity() *ChargeUsageBasedRunsUpsertOne { return u.Update(func(s *ChargeUsageBasedRunsUpsert) { - s.UpdateMeterValue() + s.UpdateMeteredQuantity() }) } @@ -1318,8 +1318,8 @@ func (u *ChargeUsageBasedRunsUpsertBulk) UpdateNewValues() *ChargeUsageBasedRuns if _, exists := b.mutation.GetType(); exists { s.SetIgnore(chargeusagebasedruns.FieldType) } - if _, exists := b.mutation.CollectionEnd(); exists { - s.SetIgnore(chargeusagebasedruns.FieldCollectionEnd) + if _, exists := b.mutation.ServicePeriodTo(); exists { + s.SetIgnore(chargeusagebasedruns.FieldServicePeriodTo) } } })) @@ -1500,17 +1500,17 @@ func (u *ChargeUsageBasedRunsUpsertBulk) UpdateTotal() *ChargeUsageBasedRunsUpse }) } -// SetAsof sets the "asof" field. -func (u *ChargeUsageBasedRunsUpsertBulk) SetAsof(v time.Time) *ChargeUsageBasedRunsUpsertBulk { +// SetStoredAtLt sets the "stored_at_lt" field. +func (u *ChargeUsageBasedRunsUpsertBulk) SetStoredAtLt(v time.Time) *ChargeUsageBasedRunsUpsertBulk { return u.Update(func(s *ChargeUsageBasedRunsUpsert) { - s.SetAsof(v) + s.SetStoredAtLt(v) }) } -// UpdateAsof sets the "asof" field to the value that was provided on create. -func (u *ChargeUsageBasedRunsUpsertBulk) UpdateAsof() *ChargeUsageBasedRunsUpsertBulk { +// UpdateStoredAtLt sets the "stored_at_lt" field to the value that was provided on create. +func (u *ChargeUsageBasedRunsUpsertBulk) UpdateStoredAtLt() *ChargeUsageBasedRunsUpsertBulk { return u.Update(func(s *ChargeUsageBasedRunsUpsert) { - s.UpdateAsof() + s.UpdateStoredAtLt() }) } @@ -1535,17 +1535,17 @@ func (u *ChargeUsageBasedRunsUpsertBulk) ClearLineID() *ChargeUsageBasedRunsUpse }) } -// SetMeterValue sets the "meter_value" field. -func (u *ChargeUsageBasedRunsUpsertBulk) SetMeterValue(v alpacadecimal.Decimal) *ChargeUsageBasedRunsUpsertBulk { +// SetMeteredQuantity sets the "metered_quantity" field. +func (u *ChargeUsageBasedRunsUpsertBulk) SetMeteredQuantity(v alpacadecimal.Decimal) *ChargeUsageBasedRunsUpsertBulk { return u.Update(func(s *ChargeUsageBasedRunsUpsert) { - s.SetMeterValue(v) + s.SetMeteredQuantity(v) }) } -// UpdateMeterValue sets the "meter_value" field to the value that was provided on create. -func (u *ChargeUsageBasedRunsUpsertBulk) UpdateMeterValue() *ChargeUsageBasedRunsUpsertBulk { +// UpdateMeteredQuantity sets the "metered_quantity" field to the value that was provided on create. +func (u *ChargeUsageBasedRunsUpsertBulk) UpdateMeteredQuantity() *ChargeUsageBasedRunsUpsertBulk { return u.Update(func(s *ChargeUsageBasedRunsUpsert) { - s.UpdateMeterValue() + s.UpdateMeteredQuantity() }) } diff --git a/openmeter/ent/db/chargeusagebasedruns_update.go b/openmeter/ent/db/chargeusagebasedruns_update.go index 7a8f5282ac..eef362c273 100644 --- a/openmeter/ent/db/chargeusagebasedruns_update.go +++ b/openmeter/ent/db/chargeusagebasedruns_update.go @@ -172,16 +172,16 @@ func (_u *ChargeUsageBasedRunsUpdate) SetNillableTotal(v *alpacadecimal.Decimal) return _u } -// SetAsof sets the "asof" field. -func (_u *ChargeUsageBasedRunsUpdate) SetAsof(v time.Time) *ChargeUsageBasedRunsUpdate { - _u.mutation.SetAsof(v) +// SetStoredAtLt sets the "stored_at_lt" field. +func (_u *ChargeUsageBasedRunsUpdate) SetStoredAtLt(v time.Time) *ChargeUsageBasedRunsUpdate { + _u.mutation.SetStoredAtLt(v) return _u } -// SetNillableAsof sets the "asof" field if the given value is not nil. -func (_u *ChargeUsageBasedRunsUpdate) SetNillableAsof(v *time.Time) *ChargeUsageBasedRunsUpdate { +// SetNillableStoredAtLt sets the "stored_at_lt" field if the given value is not nil. +func (_u *ChargeUsageBasedRunsUpdate) SetNillableStoredAtLt(v *time.Time) *ChargeUsageBasedRunsUpdate { if v != nil { - _u.SetAsof(*v) + _u.SetStoredAtLt(*v) } return _u } @@ -206,16 +206,16 @@ func (_u *ChargeUsageBasedRunsUpdate) ClearLineID() *ChargeUsageBasedRunsUpdate return _u } -// SetMeterValue sets the "meter_value" field. -func (_u *ChargeUsageBasedRunsUpdate) SetMeterValue(v alpacadecimal.Decimal) *ChargeUsageBasedRunsUpdate { - _u.mutation.SetMeterValue(v) +// SetMeteredQuantity sets the "metered_quantity" field. +func (_u *ChargeUsageBasedRunsUpdate) SetMeteredQuantity(v alpacadecimal.Decimal) *ChargeUsageBasedRunsUpdate { + _u.mutation.SetMeteredQuantity(v) return _u } -// SetNillableMeterValue sets the "meter_value" field if the given value is not nil. -func (_u *ChargeUsageBasedRunsUpdate) SetNillableMeterValue(v *alpacadecimal.Decimal) *ChargeUsageBasedRunsUpdate { +// SetNillableMeteredQuantity sets the "metered_quantity" field if the given value is not nil. +func (_u *ChargeUsageBasedRunsUpdate) SetNillableMeteredQuantity(v *alpacadecimal.Decimal) *ChargeUsageBasedRunsUpdate { if v != nil { - _u.SetMeterValue(*v) + _u.SetMeteredQuantity(*v) } return _u } @@ -469,11 +469,11 @@ func (_u *ChargeUsageBasedRunsUpdate) sqlSave(ctx context.Context) (_node int, e if value, ok := _u.mutation.Total(); ok { _spec.SetField(chargeusagebasedruns.FieldTotal, field.TypeOther, value) } - if value, ok := _u.mutation.Asof(); ok { - _spec.SetField(chargeusagebasedruns.FieldAsof, field.TypeTime, value) + if value, ok := _u.mutation.StoredAtLt(); ok { + _spec.SetField(chargeusagebasedruns.FieldStoredAtLt, field.TypeTime, value) } - if value, ok := _u.mutation.MeterValue(); ok { - _spec.SetField(chargeusagebasedruns.FieldMeterValue, field.TypeOther, value) + if value, ok := _u.mutation.MeteredQuantity(); ok { + _spec.SetField(chargeusagebasedruns.FieldMeteredQuantity, field.TypeOther, value) } if _u.mutation.BillingInvoiceLineCleared() { edge := &sqlgraph.EdgeSpec{ @@ -810,16 +810,16 @@ func (_u *ChargeUsageBasedRunsUpdateOne) SetNillableTotal(v *alpacadecimal.Decim return _u } -// SetAsof sets the "asof" field. -func (_u *ChargeUsageBasedRunsUpdateOne) SetAsof(v time.Time) *ChargeUsageBasedRunsUpdateOne { - _u.mutation.SetAsof(v) +// SetStoredAtLt sets the "stored_at_lt" field. +func (_u *ChargeUsageBasedRunsUpdateOne) SetStoredAtLt(v time.Time) *ChargeUsageBasedRunsUpdateOne { + _u.mutation.SetStoredAtLt(v) return _u } -// SetNillableAsof sets the "asof" field if the given value is not nil. -func (_u *ChargeUsageBasedRunsUpdateOne) SetNillableAsof(v *time.Time) *ChargeUsageBasedRunsUpdateOne { +// SetNillableStoredAtLt sets the "stored_at_lt" field if the given value is not nil. +func (_u *ChargeUsageBasedRunsUpdateOne) SetNillableStoredAtLt(v *time.Time) *ChargeUsageBasedRunsUpdateOne { if v != nil { - _u.SetAsof(*v) + _u.SetStoredAtLt(*v) } return _u } @@ -844,16 +844,16 @@ func (_u *ChargeUsageBasedRunsUpdateOne) ClearLineID() *ChargeUsageBasedRunsUpda return _u } -// SetMeterValue sets the "meter_value" field. -func (_u *ChargeUsageBasedRunsUpdateOne) SetMeterValue(v alpacadecimal.Decimal) *ChargeUsageBasedRunsUpdateOne { - _u.mutation.SetMeterValue(v) +// SetMeteredQuantity sets the "metered_quantity" field. +func (_u *ChargeUsageBasedRunsUpdateOne) SetMeteredQuantity(v alpacadecimal.Decimal) *ChargeUsageBasedRunsUpdateOne { + _u.mutation.SetMeteredQuantity(v) return _u } -// SetNillableMeterValue sets the "meter_value" field if the given value is not nil. -func (_u *ChargeUsageBasedRunsUpdateOne) SetNillableMeterValue(v *alpacadecimal.Decimal) *ChargeUsageBasedRunsUpdateOne { +// SetNillableMeteredQuantity sets the "metered_quantity" field if the given value is not nil. +func (_u *ChargeUsageBasedRunsUpdateOne) SetNillableMeteredQuantity(v *alpacadecimal.Decimal) *ChargeUsageBasedRunsUpdateOne { if v != nil { - _u.SetMeterValue(*v) + _u.SetMeteredQuantity(*v) } return _u } @@ -1137,11 +1137,11 @@ func (_u *ChargeUsageBasedRunsUpdateOne) sqlSave(ctx context.Context) (_node *Ch if value, ok := _u.mutation.Total(); ok { _spec.SetField(chargeusagebasedruns.FieldTotal, field.TypeOther, value) } - if value, ok := _u.mutation.Asof(); ok { - _spec.SetField(chargeusagebasedruns.FieldAsof, field.TypeTime, value) + if value, ok := _u.mutation.StoredAtLt(); ok { + _spec.SetField(chargeusagebasedruns.FieldStoredAtLt, field.TypeTime, value) } - if value, ok := _u.mutation.MeterValue(); ok { - _spec.SetField(chargeusagebasedruns.FieldMeterValue, field.TypeOther, value) + if value, ok := _u.mutation.MeteredQuantity(); ok { + _spec.SetField(chargeusagebasedruns.FieldMeteredQuantity, field.TypeOther, value) } if _u.mutation.BillingInvoiceLineCleared() { edge := &sqlgraph.EdgeSpec{ diff --git a/openmeter/ent/db/migrate/schema.go b/openmeter/ent/db/migrate/schema.go index 8ee7cf62ed..58ab80c77a 100644 --- a/openmeter/ent/db/migrate/schema.go +++ b/openmeter/ent/db/migrate/schema.go @@ -2722,9 +2722,9 @@ var ( {Name: "credits_total", Type: field.TypeOther, SchemaType: map[string]string{"postgres": "numeric"}}, {Name: "total", Type: field.TypeOther, SchemaType: map[string]string{"postgres": "numeric"}}, {Name: "type", Type: field.TypeEnum, Enums: []string{"final_realization", "partial_invoice"}}, - {Name: "asof", Type: field.TypeTime}, - {Name: "collection_end", Type: field.TypeTime}, - {Name: "meter_value", Type: field.TypeOther, SchemaType: map[string]string{"postgres": "numeric"}}, + {Name: "stored_at_lt", Type: field.TypeTime}, + {Name: "service_period_to", Type: field.TypeTime}, + {Name: "metered_quantity", Type: field.TypeOther, SchemaType: map[string]string{"postgres": "numeric"}}, {Name: "line_id", Type: field.TypeString, Unique: true, Nullable: true, SchemaType: map[string]string{"postgres": "char(26)"}}, {Name: "charge_id", Type: field.TypeString, SchemaType: map[string]string{"postgres": "char(26)"}}, {Name: "feature_id", Type: field.TypeString, SchemaType: map[string]string{"postgres": "char(26)"}}, diff --git a/openmeter/ent/db/mutation.go b/openmeter/ent/db/mutation.go index 7f8853b75f..41db9c4e25 100644 --- a/openmeter/ent/db/mutation.go +++ b/openmeter/ent/db/mutation.go @@ -61212,9 +61212,9 @@ type ChargeUsageBasedRunsMutation struct { credits_total *alpacadecimal.Decimal total *alpacadecimal.Decimal _type *usagebased.RealizationRunType - asof *time.Time - collection_end *time.Time - meter_value *alpacadecimal.Decimal + stored_at_lt *time.Time + service_period_to *time.Time + metered_quantity *alpacadecimal.Decimal clearedFields map[string]struct{} usage_based *string clearedusage_based bool @@ -61894,76 +61894,76 @@ func (m *ChargeUsageBasedRunsMutation) ResetType() { m._type = nil } -// SetAsof sets the "asof" field. -func (m *ChargeUsageBasedRunsMutation) SetAsof(t time.Time) { - m.asof = &t +// SetStoredAtLt sets the "stored_at_lt" field. +func (m *ChargeUsageBasedRunsMutation) SetStoredAtLt(t time.Time) { + m.stored_at_lt = &t } -// Asof returns the value of the "asof" field in the mutation. -func (m *ChargeUsageBasedRunsMutation) Asof() (r time.Time, exists bool) { - v := m.asof +// StoredAtLt returns the value of the "stored_at_lt" field in the mutation. +func (m *ChargeUsageBasedRunsMutation) StoredAtLt() (r time.Time, exists bool) { + v := m.stored_at_lt if v == nil { return } return *v, true } -// OldAsof returns the old "asof" field's value of the ChargeUsageBasedRuns entity. +// OldStoredAtLt returns the old "stored_at_lt" field's value of the ChargeUsageBasedRuns entity. // If the ChargeUsageBasedRuns object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *ChargeUsageBasedRunsMutation) OldAsof(ctx context.Context) (v time.Time, err error) { +func (m *ChargeUsageBasedRunsMutation) OldStoredAtLt(ctx context.Context) (v time.Time, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldAsof is only allowed on UpdateOne operations") + return v, errors.New("OldStoredAtLt is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldAsof requires an ID field in the mutation") + return v, errors.New("OldStoredAtLt requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldAsof: %w", err) + return v, fmt.Errorf("querying old value for OldStoredAtLt: %w", err) } - return oldValue.Asof, nil + return oldValue.StoredAtLt, nil } -// ResetAsof resets all changes to the "asof" field. -func (m *ChargeUsageBasedRunsMutation) ResetAsof() { - m.asof = nil +// ResetStoredAtLt resets all changes to the "stored_at_lt" field. +func (m *ChargeUsageBasedRunsMutation) ResetStoredAtLt() { + m.stored_at_lt = nil } -// SetCollectionEnd sets the "collection_end" field. -func (m *ChargeUsageBasedRunsMutation) SetCollectionEnd(t time.Time) { - m.collection_end = &t +// SetServicePeriodTo sets the "service_period_to" field. +func (m *ChargeUsageBasedRunsMutation) SetServicePeriodTo(t time.Time) { + m.service_period_to = &t } -// CollectionEnd returns the value of the "collection_end" field in the mutation. -func (m *ChargeUsageBasedRunsMutation) CollectionEnd() (r time.Time, exists bool) { - v := m.collection_end +// ServicePeriodTo returns the value of the "service_period_to" field in the mutation. +func (m *ChargeUsageBasedRunsMutation) ServicePeriodTo() (r time.Time, exists bool) { + v := m.service_period_to if v == nil { return } return *v, true } -// OldCollectionEnd returns the old "collection_end" field's value of the ChargeUsageBasedRuns entity. +// OldServicePeriodTo returns the old "service_period_to" field's value of the ChargeUsageBasedRuns entity. // If the ChargeUsageBasedRuns object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *ChargeUsageBasedRunsMutation) OldCollectionEnd(ctx context.Context) (v time.Time, err error) { +func (m *ChargeUsageBasedRunsMutation) OldServicePeriodTo(ctx context.Context) (v time.Time, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldCollectionEnd is only allowed on UpdateOne operations") + return v, errors.New("OldServicePeriodTo is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldCollectionEnd requires an ID field in the mutation") + return v, errors.New("OldServicePeriodTo requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldCollectionEnd: %w", err) + return v, fmt.Errorf("querying old value for OldServicePeriodTo: %w", err) } - return oldValue.CollectionEnd, nil + return oldValue.ServicePeriodTo, nil } -// ResetCollectionEnd resets all changes to the "collection_end" field. -func (m *ChargeUsageBasedRunsMutation) ResetCollectionEnd() { - m.collection_end = nil +// ResetServicePeriodTo resets all changes to the "service_period_to" field. +func (m *ChargeUsageBasedRunsMutation) ResetServicePeriodTo() { + m.service_period_to = nil } // SetLineID sets the "line_id" field. @@ -62015,40 +62015,40 @@ func (m *ChargeUsageBasedRunsMutation) ResetLineID() { delete(m.clearedFields, chargeusagebasedruns.FieldLineID) } -// SetMeterValue sets the "meter_value" field. -func (m *ChargeUsageBasedRunsMutation) SetMeterValue(a alpacadecimal.Decimal) { - m.meter_value = &a +// SetMeteredQuantity sets the "metered_quantity" field. +func (m *ChargeUsageBasedRunsMutation) SetMeteredQuantity(a alpacadecimal.Decimal) { + m.metered_quantity = &a } -// MeterValue returns the value of the "meter_value" field in the mutation. -func (m *ChargeUsageBasedRunsMutation) MeterValue() (r alpacadecimal.Decimal, exists bool) { - v := m.meter_value +// MeteredQuantity returns the value of the "metered_quantity" field in the mutation. +func (m *ChargeUsageBasedRunsMutation) MeteredQuantity() (r alpacadecimal.Decimal, exists bool) { + v := m.metered_quantity if v == nil { return } return *v, true } -// OldMeterValue returns the old "meter_value" field's value of the ChargeUsageBasedRuns entity. +// OldMeteredQuantity returns the old "metered_quantity" field's value of the ChargeUsageBasedRuns entity. // If the ChargeUsageBasedRuns object wasn't provided to the builder, the object is fetched from the database. // An error is returned if the mutation operation is not UpdateOne, or the database query fails. -func (m *ChargeUsageBasedRunsMutation) OldMeterValue(ctx context.Context) (v alpacadecimal.Decimal, err error) { +func (m *ChargeUsageBasedRunsMutation) OldMeteredQuantity(ctx context.Context) (v alpacadecimal.Decimal, err error) { if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldMeterValue is only allowed on UpdateOne operations") + return v, errors.New("OldMeteredQuantity is only allowed on UpdateOne operations") } if m.id == nil || m.oldValue == nil { - return v, errors.New("OldMeterValue requires an ID field in the mutation") + return v, errors.New("OldMeteredQuantity requires an ID field in the mutation") } oldValue, err := m.oldValue(ctx) if err != nil { - return v, fmt.Errorf("querying old value for OldMeterValue: %w", err) + return v, fmt.Errorf("querying old value for OldMeteredQuantity: %w", err) } - return oldValue.MeterValue, nil + return oldValue.MeteredQuantity, nil } -// ResetMeterValue resets all changes to the "meter_value" field. -func (m *ChargeUsageBasedRunsMutation) ResetMeterValue() { - m.meter_value = nil +// ResetMeteredQuantity resets all changes to the "metered_quantity" field. +func (m *ChargeUsageBasedRunsMutation) ResetMeteredQuantity() { + m.metered_quantity = nil } // SetUsageBasedID sets the "usage_based" edge to the ChargeUsageBased entity by id. @@ -62424,17 +62424,17 @@ func (m *ChargeUsageBasedRunsMutation) Fields() []string { if m._type != nil { fields = append(fields, chargeusagebasedruns.FieldType) } - if m.asof != nil { - fields = append(fields, chargeusagebasedruns.FieldAsof) + if m.stored_at_lt != nil { + fields = append(fields, chargeusagebasedruns.FieldStoredAtLt) } - if m.collection_end != nil { - fields = append(fields, chargeusagebasedruns.FieldCollectionEnd) + if m.service_period_to != nil { + fields = append(fields, chargeusagebasedruns.FieldServicePeriodTo) } if m.billing_invoice_line != nil { fields = append(fields, chargeusagebasedruns.FieldLineID) } - if m.meter_value != nil { - fields = append(fields, chargeusagebasedruns.FieldMeterValue) + if m.metered_quantity != nil { + fields = append(fields, chargeusagebasedruns.FieldMeteredQuantity) } return fields } @@ -62474,14 +62474,14 @@ func (m *ChargeUsageBasedRunsMutation) Field(name string) (ent.Value, bool) { return m.FeatureID() case chargeusagebasedruns.FieldType: return m.GetType() - case chargeusagebasedruns.FieldAsof: - return m.Asof() - case chargeusagebasedruns.FieldCollectionEnd: - return m.CollectionEnd() + case chargeusagebasedruns.FieldStoredAtLt: + return m.StoredAtLt() + case chargeusagebasedruns.FieldServicePeriodTo: + return m.ServicePeriodTo() case chargeusagebasedruns.FieldLineID: return m.LineID() - case chargeusagebasedruns.FieldMeterValue: - return m.MeterValue() + case chargeusagebasedruns.FieldMeteredQuantity: + return m.MeteredQuantity() } return nil, false } @@ -62521,14 +62521,14 @@ func (m *ChargeUsageBasedRunsMutation) OldField(ctx context.Context, name string return m.OldFeatureID(ctx) case chargeusagebasedruns.FieldType: return m.OldType(ctx) - case chargeusagebasedruns.FieldAsof: - return m.OldAsof(ctx) - case chargeusagebasedruns.FieldCollectionEnd: - return m.OldCollectionEnd(ctx) + case chargeusagebasedruns.FieldStoredAtLt: + return m.OldStoredAtLt(ctx) + case chargeusagebasedruns.FieldServicePeriodTo: + return m.OldServicePeriodTo(ctx) case chargeusagebasedruns.FieldLineID: return m.OldLineID(ctx) - case chargeusagebasedruns.FieldMeterValue: - return m.OldMeterValue(ctx) + case chargeusagebasedruns.FieldMeteredQuantity: + return m.OldMeteredQuantity(ctx) } return nil, fmt.Errorf("unknown ChargeUsageBasedRuns field %s", name) } @@ -62643,19 +62643,19 @@ func (m *ChargeUsageBasedRunsMutation) SetField(name string, value ent.Value) er } m.SetType(v) return nil - case chargeusagebasedruns.FieldAsof: + case chargeusagebasedruns.FieldStoredAtLt: v, ok := value.(time.Time) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetAsof(v) + m.SetStoredAtLt(v) return nil - case chargeusagebasedruns.FieldCollectionEnd: + case chargeusagebasedruns.FieldServicePeriodTo: v, ok := value.(time.Time) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetCollectionEnd(v) + m.SetServicePeriodTo(v) return nil case chargeusagebasedruns.FieldLineID: v, ok := value.(string) @@ -62664,12 +62664,12 @@ func (m *ChargeUsageBasedRunsMutation) SetField(name string, value ent.Value) er } m.SetLineID(v) return nil - case chargeusagebasedruns.FieldMeterValue: + case chargeusagebasedruns.FieldMeteredQuantity: v, ok := value.(alpacadecimal.Decimal) if !ok { return fmt.Errorf("unexpected type %T for field %s", value, name) } - m.SetMeterValue(v) + m.SetMeteredQuantity(v) return nil } return fmt.Errorf("unknown ChargeUsageBasedRuns field %s", name) @@ -62780,17 +62780,17 @@ func (m *ChargeUsageBasedRunsMutation) ResetField(name string) error { case chargeusagebasedruns.FieldType: m.ResetType() return nil - case chargeusagebasedruns.FieldAsof: - m.ResetAsof() + case chargeusagebasedruns.FieldStoredAtLt: + m.ResetStoredAtLt() return nil - case chargeusagebasedruns.FieldCollectionEnd: - m.ResetCollectionEnd() + case chargeusagebasedruns.FieldServicePeriodTo: + m.ResetServicePeriodTo() return nil case chargeusagebasedruns.FieldLineID: m.ResetLineID() return nil - case chargeusagebasedruns.FieldMeterValue: - m.ResetMeterValue() + case chargeusagebasedruns.FieldMeteredQuantity: + m.ResetMeteredQuantity() return nil } return fmt.Errorf("unknown ChargeUsageBasedRuns field %s", name) diff --git a/openmeter/ent/schema/chargesusagebased.go b/openmeter/ent/schema/chargesusagebased.go index bb2d367c64..01399f2ada 100644 --- a/openmeter/ent/schema/chargesusagebased.go +++ b/openmeter/ent/schema/chargesusagebased.go @@ -164,9 +164,9 @@ func (ChargeUsageBasedRuns) Fields() []ent.Field { GoType(usagebased.RealizationRunType("")). Immutable(), - field.Time("asof"), + field.Time("stored_at_lt"), - field.Time("collection_end"). + field.Time("service_period_to"). Immutable(), field.String("line_id"). @@ -177,7 +177,7 @@ func (ChargeUsageBasedRuns) Fields() []ent.Field { NotEmpty(). Nillable(), - field.Other("meter_value", alpacadecimal.Decimal{}). + field.Other("metered_quantity", alpacadecimal.Decimal{}). SchemaType(map[string]string{ dialect.Postgres: "numeric", }), diff --git a/openmeter/ledger/chargeadapter/usagebased_test.go b/openmeter/ledger/chargeadapter/usagebased_test.go index 482843ffba..96822d2ae9 100644 --- a/openmeter/ledger/chargeadapter/usagebased_test.go +++ b/openmeter/ledger/chargeadapter/usagebased_test.go @@ -419,11 +419,11 @@ func (e *usageBasedHandlerTestEnv) newRun() chargeusagebased.RealizationRun { CreatedAt: now, UpdatedAt: now, }, - Type: chargeusagebased.RealizationRunTypeFinalRealization, - AsOf: now, - CollectionEnd: now, - MeterValue: alpacadecimal.NewFromInt(30), - FeatureID: featureID, + Type: chargeusagebased.RealizationRunTypeFinalRealization, + StoredAtLT: now, + ServicePeriodTo: now, + MeteredQuantity: alpacadecimal.NewFromInt(30), + FeatureID: featureID, Totals: totals.Totals{ Amount: alpacadecimal.NewFromInt(30), CreditsTotal: alpacadecimal.NewFromInt(30), diff --git a/tools/migrate/migrations/20260424091352_usagebased_run_data_structure.down.sql b/tools/migrate/migrations/20260424091352_usagebased_run_data_structure.down.sql new file mode 100644 index 0000000000..ff35ac2496 --- /dev/null +++ b/tools/migrate/migrations/20260424091352_usagebased_run_data_structure.down.sql @@ -0,0 +1,7 @@ +-- reverse: modify "charge_usage_based_runs" table +ALTER TABLE "charge_usage_based_runs" ADD COLUMN "collection_end" timestamptz; +UPDATE "charge_usage_based_runs" SET "collection_end" = "stored_at_lt"; +ALTER TABLE "charge_usage_based_runs" ALTER COLUMN "collection_end" SET NOT NULL; +ALTER TABLE "charge_usage_based_runs" DROP COLUMN "service_period_to"; +ALTER TABLE "charge_usage_based_runs" RENAME COLUMN "metered_quantity" TO "meter_value"; +ALTER TABLE "charge_usage_based_runs" RENAME COLUMN "stored_at_lt" TO "asof"; diff --git a/tools/migrate/migrations/20260424091352_usagebased_run_data_structure.up.sql b/tools/migrate/migrations/20260424091352_usagebased_run_data_structure.up.sql new file mode 100644 index 0000000000..d40d00fa26 --- /dev/null +++ b/tools/migrate/migrations/20260424091352_usagebased_run_data_structure.up.sql @@ -0,0 +1,25 @@ +-- modify "charge_usage_based_runs" table +-- atlas:nolint BC102 +ALTER TABLE "charge_usage_based_runs" RENAME COLUMN "asof" TO "stored_at_lt"; +-- atlas:nolint BC102 +ALTER TABLE "charge_usage_based_runs" RENAME COLUMN "meter_value" TO "metered_quantity"; +ALTER TABLE "charge_usage_based_runs" ADD COLUMN "service_period_to" timestamptz; +-- Existing runtime snapshots used collection_end as the actual metering cutoff; asof was only +-- the run creation timestamp. Preserve the query cutoff under the new stored_at_lt name. +UPDATE "charge_usage_based_runs" SET "stored_at_lt" = "collection_end"; +-- Final runs can backfill their service period from the immutable parent charge intent. Partial +-- runs should not exist in persisted production data yet, but keep a safe fallback to the old +-- collection_end value for any non-final rows. +UPDATE "charge_usage_based_runs" AS "r" +SET "service_period_to" = CASE + WHEN "r"."type" = 'final_realization' THEN "c"."service_period_to" + ELSE "r"."collection_end" +END +FROM "charge_usage_based" AS "c" +WHERE "r"."namespace" = "c"."namespace" + AND "r"."charge_id" = "c"."id" + AND "r"."service_period_to" IS NULL; +-- atlas:nolint MF104 +ALTER TABLE "charge_usage_based_runs" ALTER COLUMN "service_period_to" SET NOT NULL; +-- atlas:nolint DS103 +ALTER TABLE "charge_usage_based_runs" DROP COLUMN "collection_end"; diff --git a/tools/migrate/migrations/atlas.sum b/tools/migrate/migrations/atlas.sum index 9426485183..d421d6e717 100644 --- a/tools/migrate/migrations/atlas.sum +++ b/tools/migrate/migrations/atlas.sum @@ -1,4 +1,4 @@ -h1:sW2r0CYu5hHcpXhotHW4RwpS5S5jcY/zk1y4JtWYEB0= +h1:LQIt5iDD5CEbAfy4XW82C/KUc8Kb9OyoMUxusGhIU48= 20240826120919_init.up.sql h1:tc1V91/smlmaeJGQ8h+MzTEeFjjnrrFDbDAjOYJK91o= 20240903155435_entitlement-expired-index.up.sql h1:Hp8u5uckmLXc1cRvWU0AtVnnK8ShlpzZNp8pbiJLhac= 20240917172257_billing-entities.up.sql h1:Q1dAMo0Vjiit76OybClNfYPGC5nmvov2/M2W1ioi4Kw= @@ -185,3 +185,4 @@ h1:sW2r0CYu5hHcpXhotHW4RwpS5S5jcY/zk1y4JtWYEB0= 20260421150206_require_child_unique_reference_id_on_detailed_lines.up.sql h1:XReNPIrVLZunPJ1vyUTwxBBASZDqgljQO3HBAMMMzqA= 20260422051837_simplify_mixin_detailed_line_partial_indexes.up.sql h1:3AsRyULK4pPWEcmfDd17bP6o4QuCcdH34ByUKsQdGHg= 20260422140242_add_standard_invoice_line_override_collection_period_end.up.sql h1:V70I7Jj6NNZOwAunNXZub7V8PUoJtAaVt0I9s+RLLY8= +20260424091352_usagebased_run_data_structure.up.sql h1:ZsdIVq5+DCj/qTL3xxDFcegVc83WudhnfSOHcFIxGe0=