Skip to content

feat: apply unit config in rating via mutator - OM-397#4616

Open
rolosp wants to merge 3 commits into
mainfrom
feat/om-397-apply-unit-config
Open

feat: apply unit config in rating via mutator - OM-397#4616
rolosp wants to merge 3 commits into
mainfrom
feat/om-397-apply-unit-config

Conversation

@rolosp

@rolosp rolosp commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Overview

Apply unit_config in rating (charges path)

Wires unit_config into the billing rating engine so a usage-based rate card's conversion (operation × factor, then rounding) is applied to the metered quantity before pricing — previously authored and persisted (OM-395, OM-396) but inert at billing time. Dark behind unitConfig.enabled; with the flag off, rating is byte-identical to today.

This covers the charges path end-to-end. Standard/legacy invoice lines return no config yet — that's the next ticket (persist applied_unit_config on invoice lines), where the convert+round also gets applied in the charges line-mapper.

Changes

  • New UnitConfig pre-calculation mutator: converts + rounds on the cumulative endpoints (round(convert(pre+qty)) − round(convert(pre))) so progressive splits
    round on the cumulative boundary, then hands converted units to the existing
    unit/graduated/volume pricers (unchanged, conversion-unaware). Uses UnitConfig.Apply.
  • Registered ahead of DiscountUsage (convert → round → discount), gated by the flag.
  • unitConfig.enabled threaded into the rating service via DI, defaulting off.
  • GetUnitConfig() on the rating line accessor; RateableIntent returns the
    snapshotted charge-intent config.

Testing

  • Mutator unit tests (multiply/divide, all rounding modes, cumulative boundary, idempotency).
  • Pricer registration test (flag on/off ordering, flat price unaffected).
  • End-to-end rating + DB-backed charges integration tests: flag-on bills the converted
    quantity, flag-off bills raw.

Summary by CodeRabbit

  • New Features

    • Enabled unit_config support in billing rating for eligible usage-based prices, including unit conversion with rounding.
    • Usage-based charge intents and rating now preserve unit_config from rate cards so calculations use the correct units.
  • Bug Fixes

    • When unit_config is present but the feature is disabled, rating is now rejected instead of falling back to raw quantities.
    • Unit conversion applies correctly for split/progressive usage calculations, including tier thresholds and discounts.
  • Tests

    • Added new end-to-end and unit tests covering enabled/disabled scenarios and rounding behavior.

@rolosp rolosp requested a review from a team as a code owner June 30, 2026 15:05
@rolosp rolosp added the release-note/feature Release note: Exciting New Features label Jun 30, 2026
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds unit-config-aware billing rating, including a feature flag, a new conversion/rejection mutator pair, updated rate-card validation, and wiring changes through app, tests, and intent snapshots.

Changes

UnitConfig Rating Feature

Layer / File(s) Summary
Pricing contracts and unit_config accessors
openmeter/productcatalog/price.go, openmeter/productcatalog/price_test.go, openmeter/productcatalog/ratecard.go, openmeter/billing/rating/line.go, openmeter/billing/stdinvoiceline.go, openmeter/billing/charges/usagebased/rating.go
Adds SupportsUnitConfig() on Price, updates rate-card validation to use it, and exposes optional unit-config accessors on billing line types.
UnitConfig mutators and errors
openmeter/billing/rating/service/mutator/errors.go, openmeter/billing/rating/service/mutator/forbidunitconfig.go, openmeter/billing/rating/service/mutator/forbidunitconfig_test.go, openmeter/billing/rating/service/mutator/unitconfig.go, openmeter/billing/rating/service/mutator/unitconfig_test.go
Adds unit-config-specific errors, a mutator that converts usage quantities when enabled, and a mutator that rejects unit-config-bearing inputs when disabled, with tests.
Rating service config and pricer pipeline
openmeter/billing/rating/service/service.go, openmeter/billing/rating/service/pricer.go, openmeter/billing/rating/service/detailedline.go, openmeter/billing/rating/service/billableperiod.go, openmeter/billing/rating/service/testutil/ubptest.go, openmeter/billing/rating/service/unitconfig_registration_test.go, openmeter/billing/rating/service/rate/unitconfig_rating_test.go
Adds Config{UnitConfigEnabled} to the rating service, threads it into pricer selection, conditionally registers the new mutators, and covers the resulting rating behavior in unit and integration tests.
App and Wire provider wiring
app/common/config.go, app/common/billing.go, cmd/billing-worker/wire_gen.go, cmd/jobs/internal/wire_gen.go, cmd/server/wire_gen.go
Extends shared config wiring with UnitConfig and updates generated/app constructors to pass unit-config configuration into billing rating service creation.
Charge service wiring and end-to-end unit_config tests
openmeter/billing/charges/service/base_test.go, openmeter/billing/charges/service/unitconfig_rating_test.go, openmeter/billing/charges/testutils/service.go
Carries unit-config flags and intent data through charge-service test wiring and adds an end-to-end billing test for enabled and disabled unit-config scenarios.
Usage-based intent unit_config snapshot
openmeter/billing/worker/subscriptionsync/service/reconciler/patchchargeusagebased.go, openmeter/billing/worker/subscriptionsync/service/reconciler/patchchargeusagebased_test.go
Copies rate-card unit_config into generated usage-based charge intents and verifies cloning and nil handling.
Existing test and fixture constructor updates
openmeter/billing/charges/usagebased/service/rating/delta/base_test.go, openmeter/billing/charges/usagebased/service/rating/periodpreserving/engine_test.go, openmeter/billing/charges/usagebased/service/rating/service_test.go, openmeter/billing/charges/usagebased/service/rating/subtract/subtract_test.go, openmeter/ledger/customerbalance/testenv_test.go, test/app/testenv.go, test/billing/suite.go, test/customer/testenv.go, test/subscription/framework_test.go
Updates remaining billing rating test environments to use explicit rating-service configuration objects.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

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

Suggested reviewers: tothandras, turip, borosr

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.59% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: applying unit_config during rating through a mutator.
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-397-apply-unit-config

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 Jun 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR wires unit_config into usage-based billing on the charges path. The main changes are:

  • Adds a rating mutator that converts and rounds metered quantities before pricing.
  • Gates conversion behind unitConfig.enabled and fails closed when config is present while disabled.
  • Passes unit_config from subscription rate cards into charge intents.
  • Adds tests for conversion, rounding, mutator ordering, and charges-path billing.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
openmeter/billing/rating/service/mutator/unitconfig.go Adds the enabled-path unit_config mutator that converts rated usage before pricing.
openmeter/billing/rating/service/mutator/forbidunitconfig.go Adds disabled-path handling that rejects unit_config instead of billing raw usage.
openmeter/billing/charges/usagebased/rating.go Exposes the charge intent unit_config to the rating pipeline.
openmeter/billing/worker/subscriptionsync/service/reconciler/patchchargeusagebased.go Copies rate card unit_config into usage-based charge intents during subscription sync.
openmeter/billing/rating/service/pricer.go Registers unit_config handling before usage discounts in the pricing pipeline.
openmeter/billing/stdinvoiceline.go Adds the standard-line unit_config accessor required by the expanded rating interface.

Reviews (3): Last reviewed commit: "fix: apply unit_config to subscription c..." | Re-trigger Greptile

Comment thread openmeter/billing/stdinvoiceline.go
@rolosp rolosp changed the title feat: apply unit config in rating via mutator - OM-397 - STILL WIP feat: apply unit config in rating via mutator - OM-397 Jun 30, 2026

@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

🤖 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 `@openmeter/billing/rating/service/rate/unitconfig_rating_test.go`:
- Around line 18-155: The test suite in TestUnitConfigRating only covers
zero-start usage paths, so it can miss regressions in PreLinePeriodQty handling.
Add one end-to-end split-line case in this test that uses a non-zero pre-line
quantity (for example a 1400 -> 2700 interval with ceiling rounding) and assert
the converted, rounded billed amount. Use the existing TestUnitConfigRating,
GenerateDetailedLines, and mutator.UnitConfig wiring context to verify the
cumulative start/end behavior is exercised.
🪄 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: fe7d7f63-b6e4-4730-860f-5b0d9525e71b

📥 Commits

Reviewing files that changed from the base of the PR and between 8b9649e and e8e7cef.

📒 Files selected for processing (32)
  • app/common/billing.go
  • app/common/config.go
  • cmd/billing-worker/wire_gen.go
  • cmd/jobs/internal/wire_gen.go
  • cmd/server/wire_gen.go
  • openmeter/billing/charges/service/base_test.go
  • openmeter/billing/charges/service/unitconfig_rating_test.go
  • openmeter/billing/charges/testutils/service.go
  • openmeter/billing/charges/usagebased/rating.go
  • openmeter/billing/charges/usagebased/service/rating/delta/base_test.go
  • openmeter/billing/charges/usagebased/service/rating/periodpreserving/engine_test.go
  • openmeter/billing/charges/usagebased/service/rating/service_test.go
  • openmeter/billing/charges/usagebased/service/rating/subtract/subtract_test.go
  • openmeter/billing/rating/line.go
  • openmeter/billing/rating/service/billableperiod.go
  • openmeter/billing/rating/service/detailedline.go
  • openmeter/billing/rating/service/mutator/unitconfig.go
  • openmeter/billing/rating/service/mutator/unitconfig_test.go
  • openmeter/billing/rating/service/pricer.go
  • openmeter/billing/rating/service/rate/unitconfig_rating_test.go
  • openmeter/billing/rating/service/service.go
  • openmeter/billing/rating/service/testutil/ubptest.go
  • openmeter/billing/rating/service/unitconfig_registration_test.go
  • openmeter/billing/stdinvoiceline.go
  • openmeter/ledger/customerbalance/testenv_test.go
  • openmeter/productcatalog/price.go
  • openmeter/productcatalog/price_test.go
  • openmeter/productcatalog/ratecard.go
  • test/app/testenv.go
  • test/billing/suite.go
  • test/customer/testenv.go
  • test/subscription/framework_test.go

Comment thread openmeter/billing/rating/service/rate/unitconfig_rating_test.go
@rolosp rolosp force-pushed the feat/om-397-apply-unit-config branch from e8e7cef to d713239 Compare June 30, 2026 17:07
Comment on lines +77 to +79
if unitConfigEnabled {
preCalculationMutators = append(preCalculationMutators, &mutator.UnitConfig{})
}

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.

Call me paranoid, but instead of this, I would do something like this:

	if unitConfigEnabled {
		preCalculationMutators = append(preCalculationMutators, &mutator.UnitConfig{})
	} else {
        preCalculationMutators = append(preCalculationMutators, &mutator.ForbidUnitConfig{})
    }

Forbid unit config is a noop, but returns error when the unit config is set on the rateable. This way we are not accidentally ignoring unit configs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

added ForbidUnitConfig

}

_, start := unitConfig.Apply(usage.PreLinePeriodQuantity)
_, end := unitConfig.Apply(usage.PreLinePeriodQuantity.Add(usage.Quantity))

@turip turip Jul 1, 2026

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.

just convert the quantity, don't do calculations here. It is not necessarily true that quantity = perlineperiodquantity + periodquantity (e.g. for exotic meters, and charges rating)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

Comment thread test/app/testenv.go Outdated
require.NoError(t, err)

billingRatingService := billingratingservice.New()
billingRatingService := billingratingservice.New(billingratingservice.Config{})

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.

enable in all test envs, so that you are validating that there's no regression due to this being enabled.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

return r.Discounts.Clone()
}

func (r RateableIntent) GetUnitConfig() *productcatalog.UnitConfig {

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.

[P1] Subscription charges drop unit_config before rating

This new rating path only sees unit_config through RateableIntent.GetUnitConfig(), but normal subscription-created usage-based charges still never populate Intent.UnitConfig: newUsageBasedChargeIntent in openmeter/billing/worker/subscriptionsync/service/reconciler/patchchargeusagebased.go copies price and discounts from rateCardMeta but leaves UnitConfig behind. With unitConfig.enabled on, a plan/subscription rate card using unit_config will still invoice raw metered quantity, while only direct charge-intent tests exercise the converted path. Please snapshot rateCardMeta.UnitConfig into the usage-based charge intent, with a subscription round-trip/invoice test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done, newUsageBasedChargeIntent now snapshots rateCardMeta.UnitConfig onto the usage-based charge intent

price := l.GetPrice()
if price == nil || !price.SupportsUnitConfig() {
return l, nil
}

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 should be an error. Don't ignore inconsistencies in billing or rating, instead make them surfaced.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

Comment on lines +59 to 70
// unitConfigLineAccessor wraps a StandardLine to expose a unit_config, which the
// StandardLine type itself never carries.
type unitConfigLineAccessor struct {
*billing.StandardLine

unitConfig *productcatalog.UnitConfig
}

func (l unitConfigLineAccessor) GetUnitConfig() *productcatalog.UnitConfig {
return l.unitConfig
}

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.

I would prefer if you add this to the line even if it's not persisted, or add a TODO here to use the line's unit config once it's there.

Given you have done it for charges, you might aswell do this for standard lines.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a TODO comment and deferred the standard-line wiring

@rolosp rolosp force-pushed the feat/om-397-apply-unit-config branch from d713239 to 2e269dd Compare July 1, 2026 14:09

@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 (2)
openmeter/billing/charges/service/unitconfig_rating_test.go (2)

128-130: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

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

Line 168 re-freezes time to invoiceAt without a fresh defer clock.UnFreeze() next to it — it only works because the first defer (line 129) still fires at the end. As per coding guidelines, **/*_test.go: "When using clock.FreezeTime(...) in tests, immediately pair it with defer clock.UnFreeze() in the same scope."

Also applies to: 168-168

🤖 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 `@openmeter/billing/charges/service/unitconfig_rating_test.go` around lines 128
- 130, The test in unitconfig_rating_test.go freezes time more than once without
pairing each freeze with its own immediate defer UnFreeze call. Update the test
around the second clock.FreezeTime usage so it is followed in the same scope by
defer clock.UnFreeze(), matching the existing pattern used near the first freeze
and keeping the test cleanup local and explicit.

Source: Coding guidelines


40-42: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant TearDownTest overrides.

Both suites override TearDownTest just to forward to s.BaseSuite.TearDownTest() — since BaseSuite is embedded, that method is already promoted automatically. These overrides can just be dropped.

♻️ Drop the no-op overrides
-func (s *unitConfigRatingEnabledSuite) TearDownTest() {
-	s.BaseSuite.TearDownTest()
-}
-

(and the equivalent block for unitConfigRatingDisabledSuite)

Also applies to: 83-85

🤖 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 `@openmeter/billing/charges/service/unitconfig_rating_test.go` around lines 40
- 42, Remove the no-op TearDownTest overrides from both
unitConfigRatingEnabledSuite and unitConfigRatingDisabledSuite, since the
embedded BaseSuite already promotes TearDownTest automatically. Keep the suite
types intact and delete the forwarding methods so the test setup/teardown
behavior continues to come from BaseSuite without redundant wrappers.
🤖 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 `@openmeter/billing/charges/service/unitconfig_rating_test.go`:
- Around line 128-130: The test in unitconfig_rating_test.go freezes time more
than once without pairing each freeze with its own immediate defer UnFreeze
call. Update the test around the second clock.FreezeTime usage so it is followed
in the same scope by defer clock.UnFreeze(), matching the existing pattern used
near the first freeze and keeping the test cleanup local and explicit.
- Around line 40-42: Remove the no-op TearDownTest overrides from both
unitConfigRatingEnabledSuite and unitConfigRatingDisabledSuite, since the
embedded BaseSuite already promotes TearDownTest automatically. Keep the suite
types intact and delete the forwarding methods so the test setup/teardown
behavior continues to come from BaseSuite without redundant wrappers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b2f31ac0-ae17-4963-ac0f-b6df6f9605ef

📥 Commits

Reviewing files that changed from the base of the PR and between d713239 and 2e269dd.

📒 Files selected for processing (37)
  • app/common/billing.go
  • app/common/config.go
  • cmd/billing-worker/wire_gen.go
  • cmd/jobs/internal/wire_gen.go
  • cmd/server/wire_gen.go
  • openmeter/billing/charges/service/base_test.go
  • openmeter/billing/charges/service/unitconfig_rating_test.go
  • openmeter/billing/charges/testutils/service.go
  • openmeter/billing/charges/usagebased/rating.go
  • openmeter/billing/charges/usagebased/service/rating/delta/base_test.go
  • openmeter/billing/charges/usagebased/service/rating/periodpreserving/engine_test.go
  • openmeter/billing/charges/usagebased/service/rating/service_test.go
  • openmeter/billing/charges/usagebased/service/rating/subtract/subtract_test.go
  • openmeter/billing/rating/line.go
  • openmeter/billing/rating/service/billableperiod.go
  • openmeter/billing/rating/service/detailedline.go
  • openmeter/billing/rating/service/mutator/errors.go
  • openmeter/billing/rating/service/mutator/forbidunitconfig.go
  • openmeter/billing/rating/service/mutator/forbidunitconfig_test.go
  • openmeter/billing/rating/service/mutator/unitconfig.go
  • openmeter/billing/rating/service/mutator/unitconfig_test.go
  • openmeter/billing/rating/service/pricer.go
  • openmeter/billing/rating/service/rate/unitconfig_rating_test.go
  • openmeter/billing/rating/service/service.go
  • openmeter/billing/rating/service/testutil/ubptest.go
  • openmeter/billing/rating/service/unitconfig_registration_test.go
  • openmeter/billing/stdinvoiceline.go
  • openmeter/billing/worker/subscriptionsync/service/reconciler/patchchargeusagebased.go
  • openmeter/billing/worker/subscriptionsync/service/reconciler/patchchargeusagebased_test.go
  • openmeter/ledger/customerbalance/testenv_test.go
  • openmeter/productcatalog/price.go
  • openmeter/productcatalog/price_test.go
  • openmeter/productcatalog/ratecard.go
  • test/app/testenv.go
  • test/billing/suite.go
  • test/customer/testenv.go
  • test/subscription/framework_test.go
✅ Files skipped from review due to trivial changes (4)
  • openmeter/productcatalog/price_test.go
  • cmd/jobs/internal/wire_gen.go
  • cmd/server/wire_gen.go
  • openmeter/billing/charges/usagebased/service/rating/subtract/subtract_test.go
🚧 Files skipped from review as they are similar to previous changes (20)
  • openmeter/billing/rating/service/unitconfig_registration_test.go
  • openmeter/ledger/customerbalance/testenv_test.go
  • openmeter/productcatalog/ratecard.go
  • openmeter/billing/rating/service/detailedline.go
  • app/common/billing.go
  • openmeter/productcatalog/price.go
  • openmeter/billing/rating/service/billableperiod.go
  • openmeter/billing/stdinvoiceline.go
  • openmeter/billing/charges/usagebased/rating.go
  • cmd/billing-worker/wire_gen.go
  • openmeter/billing/charges/usagebased/service/rating/periodpreserving/engine_test.go
  • test/billing/suite.go
  • app/common/config.go
  • openmeter/billing/charges/usagebased/service/rating/service_test.go
  • openmeter/billing/rating/service/service.go
  • openmeter/billing/rating/service/pricer.go
  • openmeter/billing/charges/usagebased/service/rating/delta/base_test.go
  • openmeter/billing/rating/service/testutil/ubptest.go
  • openmeter/billing/rating/line.go
  • openmeter/billing/charges/service/base_test.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-note/feature Release note: Exciting New Features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants