Skip to content

feat: snapshot unit config on invoice lines - OM-398#4642

Open
rolosp wants to merge 3 commits into
mainfrom
feat/om-398-unit-config-snapshot-on-invoice-lines
Open

feat: snapshot unit config on invoice lines - OM-398#4642
rolosp wants to merge 3 commits into
mainfrom
feat/om-398-unit-config-snapshot-on-invoice-lines

Conversation

@rolosp

@rolosp rolosp commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Overview

Snapshots applied_unit_config on usage-based invoice lines so re-rating converts from raw metered quantity using the config applied at billing time, even after the rate card changes. Adds one nullable jsonb column; converted value is recomputed on read.

Written on both billing paths (charges line-mapper + legacy line-engine via AsNewStandardLine), so unitConfig works even when lines route to the legacy engine. StandardLine.GetUnitConfig() now returns the snapshot.

Summary by CodeRabbit

  • New Features
    • Billing now supports persisting a billing-time unit_config snapshot (NULL when unset) and exposes it on usage-based invoice lines, including legacy gathering lines. Standard lines can surface the snapshot via GetUnitConfig().
  • Bug Fixes
    • Upserts no longer overwrite existing unit_config when omitted; conflict re-sync refreshes the snapshot per line.
    • Rating-time unit_config mutation now validates inputs to fail gracefully on corrupt data.
    • Invoice line upserts now also refresh description and ratecard_discounts (plus additional related fields).
  • Tests
    • Added coverage for snapshot round-trip, propagation (including deep-copy), and legacy unit-config rating behavior.

@rolosp rolosp requested a review from a team as a code owner July 3, 2026 12:42
@rolosp rolosp added the release-note/misc Miscellaneous changes label Jul 3, 2026
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 43eec25e-309c-4983-baeb-bb6dd5544221

📥 Commits

Reviewing files that changed from the base of the PR and between e35777d and 8fe9912.

⛔ Files ignored due to path filters (10)
  • openmeter/ent/db/billinginvoiceusagebasedlineconfig.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoiceusagebasedlineconfig/billinginvoiceusagebasedlineconfig.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoiceusagebasedlineconfig/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoiceusagebasedlineconfig_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoiceusagebasedlineconfig_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/migrate/schema.go is excluded by !**/ent/db/**
  • openmeter/ent/db/mutation.go is excluded by !**/ent/db/**
  • openmeter/ent/db/runtime.go is excluded by !**/ent/db/**
  • openmeter/ent/db/setorclear.go is excluded by !**/ent/db/**
  • tools/migrate/migrations/atlas.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (19)
  • .agents/skills/ent/SKILL.md
  • openmeter/billing/adapter/gatheringlines.go
  • openmeter/billing/adapter/stdinvoicelinemapper.go
  • openmeter/billing/adapter/stdinvoicelines.go
  • openmeter/billing/charges/service/unitconfig_rating_test.go
  • openmeter/billing/charges/usagebased/service/linemapper.go
  • openmeter/billing/derived.gen.go
  • openmeter/billing/gatheringinvoice.go
  • openmeter/billing/gatheringinvoice_test.go
  • openmeter/billing/rating/service/mutator/unitconfig.go
  • openmeter/billing/rating/service/mutator/unitconfig_test.go
  • openmeter/billing/stdinvoiceline.go
  • openmeter/billing/worker/subscriptionsync/service/targetstate/targetstateitem.go
  • openmeter/ent/schema/billing.go
  • test/billing/adapter_test.go
  • test/billing/collection_test.go
  • test/billing/unitconfig_legacy_test.go
  • tools/migrate/migrations/20260703084504_applied-unit-config-snapshot.down.sql
  • tools/migrate/migrations/20260703084504_applied-unit-config-snapshot.up.sql
✅ Files skipped from review due to trivial changes (2)
  • tools/migrate/migrations/20260703084504_applied-unit-config-snapshot.up.sql
  • .agents/skills/ent/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (16)
  • openmeter/billing/charges/usagebased/service/linemapper.go
  • openmeter/billing/adapter/stdinvoicelinemapper.go
  • openmeter/billing/rating/service/mutator/unitconfig.go
  • tools/migrate/migrations/20260703084504_applied-unit-config-snapshot.down.sql
  • openmeter/billing/stdinvoiceline.go
  • openmeter/billing/worker/subscriptionsync/service/targetstate/targetstateitem.go
  • openmeter/billing/charges/service/unitconfig_rating_test.go
  • openmeter/ent/schema/billing.go
  • test/billing/unitconfig_legacy_test.go
  • openmeter/billing/adapter/gatheringlines.go
  • test/billing/collection_test.go
  • openmeter/billing/gatheringinvoice.go
  • openmeter/billing/gatheringinvoice_test.go
  • openmeter/billing/rating/service/mutator/unitconfig_test.go
  • openmeter/billing/derived.gen.go
  • test/billing/adapter_test.go

📝 Walkthrough

Walkthrough

This PR adds unit-config snapshots to billing lines, persists them through gathering and invoice storage, propagates them into standard lines, and validates unit configs before rating mutation. Tests cover round trips, cloning, and invalid input handling.

Changes

Applied unit config snapshot feature

Layer / File(s) Summary
Schema and line fields
tools/migrate/migrations/20260703084504_applied-unit-config-snapshot.{up,down}.sql, openmeter/ent/schema/billing.go, openmeter/billing/gatheringinvoice.go, openmeter/billing/stdinvoiceline.go, openmeter/billing/derived.gen.go
Adds the persisted unit_config column, adds UnitConfig fields, and extends equality checks to include them.
Gathering and standard line snapshot propagation
openmeter/billing/gatheringinvoice.go, openmeter/billing/stdinvoiceline.go, openmeter/billing/adapter/*, openmeter/billing/worker/subscriptionsync/service/targetstate/targetstateitem.go
Deep-clones gathering-line unit config, carries it into new standard lines, persists and maps the snapshot through adapters, and reads it back through GetUnitConfig().
Rating-time capture and validation
openmeter/billing/charges/usagebased/service/linemapper.go, openmeter/billing/rating/service/mutator/unitconfig.go, .agents/skills/ent/SKILL.md, openmeter/billing/charges/service/unitconfig_rating_test.go
Captures the effective unit config on billed lines, validates unit config before mutation, and documents the nil-guarded create/upsert pattern.
Snapshot and validation tests
openmeter/billing/*_test.go, test/billing/*
Adds tests for snapshot propagation, persistence round trips, deep-copy behavior, rating assertions, and invalid config handling.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant StateItem
  participant GatheringLine
  participant Adapter
  participant Database
  participant StandardLine

  StateItem->>GatheringLine: GetExpectedLine()
  GatheringLine->>GatheringLine: clone UnitConfig if present
  GatheringLine->>Adapter: upsert usage-based line config
  Adapter->>Database: persist unit_config
  Database-->>Adapter: read stored snapshot
  Adapter->>StandardLine: map UnitConfig
  GatheringLine->>StandardLine: AsNewStandardLine()
  StandardLine-->>StateItem: GetUnitConfig()
Loading
sequenceDiagram
  participant RatingService
  participant UnitConfigMutator
  participant Line

  RatingService->>UnitConfigMutator: Mutate(unitConfig, line)
  UnitConfigMutator->>UnitConfigMutator: unitConfig.Validate()
  alt invalid config
    UnitConfigMutator-->>RatingService: error "invalid unit_config on line"
  else valid config
    UnitConfigMutator->>Line: ApplyUnitConfig
    UnitConfigMutator-->>RatingService: success
  end
Loading

Possibly related PRs

Suggested labels: release-note/feature, area/billing, kind/feature

Suggested reviewers: turip, tothandras, borbelyr-kong

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: snapshotting unit config on invoice lines.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/om-398-unit-config-snapshot-on-invoice-lines

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR completes the unit_config snapshot feature (OM-398) by persisting the billing-time conversion config on usage-based invoice lines so re-rating always converts from raw metered quantities using the config that was in effect when the line was first billed, even if the rate card is later edited.

  • Adds a nullable unit_config jsonb column to billing_invoice_usage_based_line_configs; written on both the charges path (via linemapper.go) and the legacy line-engine path (via GatheringLineBase.UnitConfigAsNewStandardLineUsageBasedLine.UnitConfig), and surfaced by StandardLine.GetUnitConfig() for the rating mutator.
  • Fixes a correctness gap in the rating mutator: a Validate() call is added before Apply() to turn a corrupt zero-factor config into a clear error rather than a divide-by-zero panic in the billing worker.
  • Bundles several ancillary upsert fixes: Description, RatecardDiscounts, PreLinePeriodQuantity, MeteredQuantity, and MeteredPreLinePeriodQuantity are now explicitly included in their respective conflict-resolution clauses so nil values properly clear stale DB values on re-upsert.

Confidence Score: 5/5

Safe to merge — the change is additive (nullable column, no existing row affected), both write paths are covered by tests, and the upsert conflict-resolution logic is validated by a dedicated mixed-batch adapter test.

The implementation follows the established Ent upsert pattern consistently: unit_config is left unset (not set to nil) on create so nullable rows store SQL NULL, and UpdateUnitConfig() in the conflict clause uses EXCLUDED.unit_config per row so nil-bearing rows clear stale snapshots without cross-contaminating other rows in the same batch. The validation guard in the mutator prevents the only runtime hazard (zero-factor panic). Test coverage spans unit, adapter, collection, and end-to-end legacy-path scenarios.

The generated SetOrClearUnitConfig helper in openmeter/ent/db/setorclear.go has a double-pointer signature that can silently call SetUnitConfig(nil) if a caller passes &nilPtr; the SKILL.md was updated to document the safe alternative pattern, but the helper itself is unused today. Future callers should use the explicit if value != nil { SetUnitConfig } else { ClearUnitConfig } pattern documented there.

Important Files Changed

Filename Overview
openmeter/ent/schema/billing.go Adds unit_config as optional nullable JSONB field using the UnitConfigValueScanner, matching the price field pattern; schema definition is correct.
openmeter/billing/adapter/gatheringlines.go Sets unit_config on create when non-nil; adds UpdateUnitConfig() to the conflict clause (resolves per-row, clearing stale snapshots when nil); reads ubpLine.UnitConfig back on map. Also adds UpdateDescription() and UpdateRatecardDiscounts() to the gathering-line conflict clause.
openmeter/billing/adapter/stdinvoicelines.go Adds UpdateUnitConfig() plus explicit UpdatePreLinePeriodQuantity/MeteredQuantity/MeteredPreLinePeriodQuantity() to the UBP config conflict clause; fixes several other nullable fields missing from upsert conflict resolution.
openmeter/billing/stdinvoiceline.go Adds UnitConfig *productcatalog.UnitConfig to UsageBasedLine; replaces the nil-returning TODO stub in GetUnitConfig() with i.UsageBased.UnitConfig, completing the seam for the rating mutator.
openmeter/billing/gatheringinvoice.go Adds UnitConfig field to GatheringLineBase; implements deep-clone in Clone(); propagates via AsNewStandardLine() with its own deep-copy so mutations to the standard line's config cannot alias back into the gathering line.
openmeter/billing/charges/usagebased/service/linemapper.go Snapshots input.UnitConfig onto stdLine.UsageBased.UnitConfig after the conversion step, completing the charges billing path so the audit trail is fully deterministic.
openmeter/billing/rating/service/mutator/unitconfig.go Adds a Validate() call before Apply() so a corrupt zero-factor config returns an error instead of panicking the billing worker via a divide-by-zero.
openmeter/billing/worker/subscriptionsync/service/targetstate/targetstateitem.go Copies the rate card's UnitConfig (deep-cloned) onto the gathering line during subscription sync, ensuring the legacy line-engine path picks up the config at gathering time.
openmeter/ent/db/setorclear.go Generated SetOrClearUnitConfig(**productcatalog.UnitConfig) helper; double-pointer signature is awkward for a GoType pointer field. Unused in this PR so no runtime risk today.
test/billing/unitconfig_legacy_test.go New end-to-end test proving the legacy line-engine path converts raw metered quantities via the snapshot identically to the charges path.
test/billing/adapter_test.go Adds TestUnitConfigSnapshotHandling and TestStandardLineOptionalFieldMutability covering the upsert conflict resolution for all new/fixed nullable fields.
tools/migrate/migrations/20260703084504_applied-unit-config-snapshot.up.sql Single nullable jsonb column addition; down migration correctly drops the column. Non-breaking for existing rows.
openmeter/billing/derived.gen.go Regenerated equality derivation includes UnitConfig.Equal(...) for both GatheringLineBase and UsageBasedLine, keeping change-detection logic consistent.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    subgraph WritePaths["Write: snapshot at billing time"]
        A1["Subscription Sync\ntargetstateitem.go\nRateCard.UnitConfig to GatheringLine.UnitConfig"] --> B1["Gathering Line DB\nunit_config jsonb column\ngatheringlines.go upsert"]
        B1 --> C1["AsNewStandardLine()\nDeep-clone to StandardLine\n.UsageBased.UnitConfig"]
        A2["Charges Path\nlinemapper.go\ninput.UnitConfig to stdLine.UsageBased.UnitConfig"] --> C2["StandardLine DB\nunit_config jsonb column\nstdinvoicelines.go upsert"]
        C1 --> C2
    end
    subgraph ReadPath["Read: re-rating uses snapshot"]
        C2 --> D["StandardLine.GetUnitConfig()\nreturns UsageBased.UnitConfig"]
        D --> E["UnitConfig Mutator\nMutate() validates + applies\nconversion to raw MeteredQuantity"]
        E --> F["Billed Quantity\nceil / floor / round(raw / factor)"]
    end
    subgraph Guard["Safety guard"]
        E -->|"factor <= 0"| G["Return error not panic"]
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    subgraph WritePaths["Write: snapshot at billing time"]
        A1["Subscription Sync\ntargetstateitem.go\nRateCard.UnitConfig to GatheringLine.UnitConfig"] --> B1["Gathering Line DB\nunit_config jsonb column\ngatheringlines.go upsert"]
        B1 --> C1["AsNewStandardLine()\nDeep-clone to StandardLine\n.UsageBased.UnitConfig"]
        A2["Charges Path\nlinemapper.go\ninput.UnitConfig to stdLine.UsageBased.UnitConfig"] --> C2["StandardLine DB\nunit_config jsonb column\nstdinvoicelines.go upsert"]
        C1 --> C2
    end
    subgraph ReadPath["Read: re-rating uses snapshot"]
        C2 --> D["StandardLine.GetUnitConfig()\nreturns UsageBased.UnitConfig"]
        D --> E["UnitConfig Mutator\nMutate() validates + applies\nconversion to raw MeteredQuantity"]
        E --> F["Billed Quantity\nceil / floor / round(raw / factor)"]
    end
    subgraph Guard["Safety guard"]
        E -->|"factor <= 0"| G["Return error not panic"]
    end
Loading

Reviews (5): Last reviewed commit: "fix: explicitly update optional fields o..." | Re-trigger Greptile

Comment thread test/billing/collection_test.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
test/billing/adapter_test.go (1)

443-469: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

"Re-upsert keeps the snapshot" test can't actually catch an overwrite regression.

The re-upserted updated line still carries the exact same unitConfig value/pointer as the original, so this assertion would pass identically whether the upsert path truly preserves the existing DB snapshot on conflict, or naively overwrites it with whatever AppliedUnitConfig happens to be on the incoming struct. Since the write-once contract is described as behavioral (the caller never sends a different config), a regression where a caller mistakenly sends a different/nil config on a later upsert wouldn't be caught here.

Consider adding a case where the second upsert carries a different (or nil) AppliedUnitConfig value and asserting the persisted snapshot doesn't change — that would actually exercise the "write-once" guarantee this test's comment describes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/billing/adapter_test.go` around lines 443 - 469, The “re-upserting the
line keeps the snapshot” test is not exercising the overwrite path because the
second upsert reuses the same AppliedUnitConfig as the original line. Update the
test around UpsertInvoiceLines and ListInvoiceLines so the re-upserted
StandardLine carries a different or nil AppliedUnitConfig, then assert the
persisted UsageBased.AppliedUnitConfig still matches the original snapshot. Use
the existing unitConfig and updated line setup to verify the write-once behavior
actually survives a conflicting incoming config.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/billing/collection_test.go`:
- Around line 185-186: The round-trip test uses non-fatal s.NoError(err) before
dereferencing pointer results like created and stdLine, which can lead to nil
panics and hide the real failure. Update the checks in the test around the
created/standard line assertions to use s.Require().NoError(err) in the same
style already used later in the test, so execution stops immediately on error
before accessing created.Lines or stdLine.UsageBased...

---

Nitpick comments:
In `@test/billing/adapter_test.go`:
- Around line 443-469: The “re-upserting the line keeps the snapshot” test is
not exercising the overwrite path because the second upsert reuses the same
AppliedUnitConfig as the original line. Update the test around
UpsertInvoiceLines and ListInvoiceLines so the re-upserted StandardLine carries
a different or nil AppliedUnitConfig, then assert the persisted
UsageBased.AppliedUnitConfig still matches the original snapshot. Use the
existing unitConfig and updated line setup to verify the write-once behavior
actually survives a conflicting incoming config.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dac6024e-b47d-474b-b639-a3d2b3e3bdbe

📥 Commits

Reviewing files that changed from the base of the PR and between f7f3d35 and db6a1b3.

⛔ Files ignored due to path filters (10)
  • openmeter/ent/db/billinginvoiceusagebasedlineconfig.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoiceusagebasedlineconfig/billinginvoiceusagebasedlineconfig.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoiceusagebasedlineconfig/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoiceusagebasedlineconfig_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoiceusagebasedlineconfig_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/migrate/schema.go is excluded by !**/ent/db/**
  • openmeter/ent/db/mutation.go is excluded by !**/ent/db/**
  • openmeter/ent/db/runtime.go is excluded by !**/ent/db/**
  • openmeter/ent/db/setorclear.go is excluded by !**/ent/db/**
  • tools/migrate/migrations/atlas.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (18)
  • .agents/skills/ent/SKILL.md
  • openmeter/billing/adapter/gatheringlines.go
  • openmeter/billing/adapter/stdinvoicelinemapper.go
  • openmeter/billing/adapter/stdinvoicelines.go
  • openmeter/billing/charges/service/unitconfig_rating_test.go
  • openmeter/billing/charges/usagebased/service/linemapper.go
  • openmeter/billing/derived.gen.go
  • openmeter/billing/gatheringinvoice.go
  • openmeter/billing/gatheringinvoice_test.go
  • openmeter/billing/rating/service/mutator/unitconfig.go
  • openmeter/billing/rating/service/mutator/unitconfig_test.go
  • openmeter/billing/stdinvoiceline.go
  • openmeter/billing/worker/subscriptionsync/service/targetstate/targetstateitem.go
  • openmeter/ent/schema/billing.go
  • test/billing/adapter_test.go
  • test/billing/collection_test.go
  • tools/migrate/migrations/20260703084504_applied-unit-config-snapshot.down.sql
  • tools/migrate/migrations/20260703084504_applied-unit-config-snapshot.up.sql

Comment thread test/billing/collection_test.go Outdated
@rolosp rolosp force-pushed the feat/om-398-unit-config-snapshot-on-invoice-lines branch 2 times, most recently from e4592de to 61749a1 Compare July 3, 2026 15:30

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
test/billing/unitconfig_legacy_test.go (1)

65-99: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Second clock.FreezeTime call isn't paired with its own UnFreeze.

The coding guidelines call for pairing every clock.FreezeTime(...) with an immediate defer clock.UnFreeze(). Line 65 does this correctly, but the re-freeze at Line 99 has no matching unfreeze call nearby — it just relies on the single deferred unfreeze from earlier. Since UnFreeze is presumably idempotent (just clears the frozen state), this isn't causing a bug here, but it deviates from the stated pattern and could be confusing if someone copies this test as a template.

🕒 Suggested fix
 	// Collect after the line's invoice-at so the completed-period line is billable.
 	clock.FreezeTime(invoiceAt.Add(time.Hour))
+	defer clock.UnFreeze()

As per coding guidelines: "When using clock.FreezeTime(...) in tests, immediately pair it with defer clock.UnFreeze() in the same scope."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/billing/unitconfig_legacy_test.go` around lines 65 - 99, The test has a
second clock.FreezeTime call that is not immediately paired with its own defer
clock.UnFreeze(), which breaks the test pattern. In this test around the
repeated FreezeTime usage in unitconfig_legacy_test.go, add a matching UnFreeze
right after the second freeze so each frozen time scope is self-contained and
consistent with the existing pattern used earlier in the test.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@test/billing/unitconfig_legacy_test.go`:
- Around line 65-99: The test has a second clock.FreezeTime call that is not
immediately paired with its own defer clock.UnFreeze(), which breaks the test
pattern. In this test around the repeated FreezeTime usage in
unitconfig_legacy_test.go, add a matching UnFreeze right after the second freeze
so each frozen time scope is self-contained and consistent with the existing
pattern used earlier in the test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b5ebe7c1-1e3f-473a-856b-67aef41d0354

📥 Commits

Reviewing files that changed from the base of the PR and between e4592de and 61749a1.

⛔ Files ignored due to path filters (10)
  • openmeter/ent/db/billinginvoiceusagebasedlineconfig.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoiceusagebasedlineconfig/billinginvoiceusagebasedlineconfig.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoiceusagebasedlineconfig/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoiceusagebasedlineconfig_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoiceusagebasedlineconfig_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/migrate/schema.go is excluded by !**/ent/db/**
  • openmeter/ent/db/mutation.go is excluded by !**/ent/db/**
  • openmeter/ent/db/runtime.go is excluded by !**/ent/db/**
  • openmeter/ent/db/setorclear.go is excluded by !**/ent/db/**
  • tools/migrate/migrations/atlas.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (19)
  • .agents/skills/ent/SKILL.md
  • openmeter/billing/adapter/gatheringlines.go
  • openmeter/billing/adapter/stdinvoicelinemapper.go
  • openmeter/billing/adapter/stdinvoicelines.go
  • openmeter/billing/charges/service/unitconfig_rating_test.go
  • openmeter/billing/charges/usagebased/service/linemapper.go
  • openmeter/billing/derived.gen.go
  • openmeter/billing/gatheringinvoice.go
  • openmeter/billing/gatheringinvoice_test.go
  • openmeter/billing/rating/service/mutator/unitconfig.go
  • openmeter/billing/rating/service/mutator/unitconfig_test.go
  • openmeter/billing/stdinvoiceline.go
  • openmeter/billing/worker/subscriptionsync/service/targetstate/targetstateitem.go
  • openmeter/ent/schema/billing.go
  • test/billing/adapter_test.go
  • test/billing/collection_test.go
  • test/billing/unitconfig_legacy_test.go
  • tools/migrate/migrations/20260703084504_applied-unit-config-snapshot.down.sql
  • tools/migrate/migrations/20260703084504_applied-unit-config-snapshot.up.sql
✅ Files skipped from review due to trivial changes (1)
  • .agents/skills/ent/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (17)
  • openmeter/billing/adapter/stdinvoicelinemapper.go
  • tools/migrate/migrations/20260703084504_applied-unit-config-snapshot.up.sql
  • openmeter/billing/gatheringinvoice_test.go
  • openmeter/billing/charges/usagebased/service/linemapper.go
  • openmeter/billing/charges/service/unitconfig_rating_test.go
  • openmeter/billing/rating/service/mutator/unitconfig.go
  • openmeter/billing/derived.gen.go
  • openmeter/billing/gatheringinvoice.go
  • openmeter/ent/schema/billing.go
  • openmeter/billing/rating/service/mutator/unitconfig_test.go
  • openmeter/billing/stdinvoiceline.go
  • tools/migrate/migrations/20260703084504_applied-unit-config-snapshot.down.sql
  • openmeter/billing/adapter/stdinvoicelines.go
  • openmeter/billing/adapter/gatheringlines.go
  • openmeter/billing/worker/subscriptionsync/service/targetstate/targetstateitem.go
  • test/billing/adapter_test.go
  • test/billing/collection_test.go

// row is never re-upserted with a different (or removed) unit_config. Left unset
// when nil (no unit_config rate card) so non-unit_config lines write NULL on
// create, matching the standard-line convention; because the value is never
// changed non-nil->nil on an existing row, ResolveWithNewValues preserving an

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine, but I would prefer that we do:

UpdateUnitConfig() in the upsert items, as that reflects a codebug if we are not updating it
We shuold also do the same for all missing optional fields.

(Sorry, I know that this is housekeeping as we had this bad practice already, but this behavior will make updates special, and as price is mutable so unit config is via manual invoice edits or charges patching)

UBPConfigID: ubpLine.ID,
FeatureKey: lo.FromPtr(ubpLine.FeatureKey),
Price: lo.FromPtr(ubpLine.Price),
UnitConfig: ubpLine.AppliedUnitConfig,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not UnitConfig? this is a diverging pattern.

// following the price precedent: the guarantee is behavioral — finalized lines
// are never re-upserted through this path, and no caller carries a different
// config for an existing line — not enforced at the SQL level. Left unset when
// nil so a conflicting re-upsert cannot clear an already-captured snapshot.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

charges/lines can be updated via invoice edit apis.

@@ -0,0 +1,2 @@
-- modify "billing_invoice_usage_based_line_configs" table
ALTER TABLE "billing_invoice_usage_based_line_configs" ADD COLUMN "applied_unit_config" jsonb NULL;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's call it unit config imho

@rolosp rolosp force-pushed the feat/om-398-unit-config-snapshot-on-invoice-lines branch from 61749a1 to e35777d Compare July 6, 2026 14:32

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
test/billing/adapter_test.go (1)

415-441: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add given/when/then intent comments to these subtests.

The "snapshot round-trips through persistence" and "line without a unit config stays nil" subtests don't open with an intent comment, while the later two subtests in this same test do. As per path instructions, **/*_test.go non-trivial subtests should start with concise given/when/then comments — worth adding here for consistency with the rest of the test.

✏️ Example
 	s.Run("snapshot round-trips through persistence", func() {
+		// given: a line created with a unit_config; when: reading it back;
+		// then: the persisted snapshot and GetUnitConfig() both match the original config.
 		readBack, err := s.BillingAdapter.ListInvoiceLines(ctx, billing.ListInvoiceLinesAdapterInput{
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/billing/adapter_test.go` around lines 415 - 441, Add concise
given/when/then intent comments at the start of the two subtests in s.Run so
they match the style used by the later subtests in adapter_test.go. In the
"snapshot round-trips through persistence" and "line without a unit config stays
nil" blocks, insert a short comment before the assertions that states the setup,
action, and expected outcome, keeping the wording consistent with the
surrounding billing adapter tests.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@test/billing/adapter_test.go`:
- Around line 415-441: Add concise given/when/then intent comments at the start
of the two subtests in s.Run so they match the style used by the later subtests
in adapter_test.go. In the "snapshot round-trips through persistence" and "line
without a unit config stays nil" blocks, insert a short comment before the
assertions that states the setup, action, and expected outcome, keeping the
wording consistent with the surrounding billing adapter tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 77e95bfd-0f80-4beb-a2f1-5557a97e4997

📥 Commits

Reviewing files that changed from the base of the PR and between 61749a1 and e35777d.

⛔ Files ignored due to path filters (10)
  • openmeter/ent/db/billinginvoiceusagebasedlineconfig.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoiceusagebasedlineconfig/billinginvoiceusagebasedlineconfig.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoiceusagebasedlineconfig/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoiceusagebasedlineconfig_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoiceusagebasedlineconfig_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/migrate/schema.go is excluded by !**/ent/db/**
  • openmeter/ent/db/mutation.go is excluded by !**/ent/db/**
  • openmeter/ent/db/runtime.go is excluded by !**/ent/db/**
  • openmeter/ent/db/setorclear.go is excluded by !**/ent/db/**
  • tools/migrate/migrations/atlas.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (19)
  • .agents/skills/ent/SKILL.md
  • openmeter/billing/adapter/gatheringlines.go
  • openmeter/billing/adapter/stdinvoicelinemapper.go
  • openmeter/billing/adapter/stdinvoicelines.go
  • openmeter/billing/charges/service/unitconfig_rating_test.go
  • openmeter/billing/charges/usagebased/service/linemapper.go
  • openmeter/billing/derived.gen.go
  • openmeter/billing/gatheringinvoice.go
  • openmeter/billing/gatheringinvoice_test.go
  • openmeter/billing/rating/service/mutator/unitconfig.go
  • openmeter/billing/rating/service/mutator/unitconfig_test.go
  • openmeter/billing/stdinvoiceline.go
  • openmeter/billing/worker/subscriptionsync/service/targetstate/targetstateitem.go
  • openmeter/ent/schema/billing.go
  • test/billing/adapter_test.go
  • test/billing/collection_test.go
  • test/billing/unitconfig_legacy_test.go
  • tools/migrate/migrations/20260703084504_applied-unit-config-snapshot.down.sql
  • tools/migrate/migrations/20260703084504_applied-unit-config-snapshot.up.sql
✅ Files skipped from review due to trivial changes (2)
  • openmeter/billing/derived.gen.go
  • .agents/skills/ent/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (10)
  • openmeter/billing/rating/service/mutator/unitconfig.go
  • openmeter/billing/worker/subscriptionsync/service/targetstate/targetstateitem.go
  • openmeter/billing/charges/service/unitconfig_rating_test.go
  • openmeter/billing/charges/usagebased/service/linemapper.go
  • openmeter/billing/adapter/gatheringlines.go
  • test/billing/collection_test.go
  • test/billing/unitconfig_legacy_test.go
  • openmeter/billing/gatheringinvoice.go
  • openmeter/billing/gatheringinvoice_test.go
  • openmeter/billing/rating/service/mutator/unitconfig_test.go

@rolosp rolosp force-pushed the feat/om-398-unit-config-snapshot-on-invoice-lines branch from e362114 to 8fe9912 Compare July 6, 2026 17:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-note/misc Miscellaneous changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants