Skip to content

fix(billing): disallow charge intent field overrides#4617

Merged
turip merged 3 commits into
mainfrom
feat/charge-override-provenance
Jun 30, 2026
Merged

fix(billing): disallow charge intent field overrides#4617
turip merged 3 commits into
mainfrom
feat/charge-override-provenance

Conversation

@turip

@turip turip commented Jun 30, 2026

Copy link
Copy Markdown
Member

Summary

  • keep charge feature keys and tax configs immutable in charge intents instead of line override layers
  • reject invoice line edits that try to override feature key or tax config, with billing errors explaining deletion is required
  • deprecate persisted override columns and update adapters, generated Ent code, fixtures, and tests

Validation

  • make test
  • nix develop --impure .#ci -c make lint-go-fast

Summary by CodeRabbit

  • New Features
    • Charge attribution (feature key) and tax settings are now handled consistently as immutable intent data across flat-fee, usage-based, and credit-purchase flows.
  • Bug Fixes
    • Improved feature-key handling when unset/empty, and refined tax configuration propagation to use the immutable intent values.
    • Manual invoice line edits now block changes to protected charge intent fields (feature key and tax config).
    • Charge update paths no longer modify existing charge annotations unintentionally.
  • Chores
    • Updated persistence rules, constraints, and tests to reflect the new immutable placement and validation behavior.

@turip
turip requested a review from a team as a code owner June 30, 2026 15:42
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

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: 1019fc5f-7279-4f62-828d-e037dc36e24b

📥 Commits

Reviewing files that changed from the base of the PR and between ce9e80d and 6a4b211.

📒 Files selected for processing (1)
  • openmeter/billing/charges/flatfee/adapter/intentoverride_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • openmeter/billing/charges/flatfee/adapter/intentoverride_test.go

📝 Walkthrough

Walkthrough

TaxConfig is moved to immutable intent data and FeatureKey becomes immutable for flat-fee and usage-based charges. Override-layer tax and feature-key handling is removed, validation rejects those edits, and adapters, services, schemas, and tests are updated to match the new layout.

Changes

Immutable charge intent and override handling

Layer / File(s) Summary
Core intent contracts
openmeter/billing/charges/meta/intent.go, openmeter/billing/charges/flatfee/charge.go, openmeter/billing/charges/usagebased/charge.go, openmeter/billing/errors.go
meta.Intent gains TaxConfig; flat-fee and usage-based intents gain immutable feature attribution; mutable intent fields drop TaxConfig and FeatureKey; override accessors now read immutable base values; new validation errors are added.
Schema and persistence rules
openmeter/ent/schema/chargesflatfee.go, openmeter/ent/schema/chargesusagebased.go, openmeter/ent/schema/chargescreditpurchase.go, openmeter/billing/charges/models/chargemeta/mixin.go, tools/migrate/migrations/20260630145544_*
Base charge tax/feature fields are marked immutable, override tax/feature fields are deprecated, chargemeta update/create wiring shifts tax config to immutable intent data, and the feature-key nullability migration is added.
Adapters and API mapping
api/v3/handlers/customers/charges/convert.go, openmeter/billing/charges/*/adapter/..., openmeter/billing/charges/*/service/..., openmeter/billing/creditgrant/service/service.go, openmeter/billing/charges/service/pendinglines.go
API conversion, charge adapters, manual-edit builders, pending-line mapping, and credit-grant creation now place tax config on the immutable intent and stop persisting override-layer tax/feature data.
Validation and consumption flow
openmeter/billing/charges/meta/patchlinemanualedit.go, openmeter/billing/charges/meta/patchlinemanualedit_test.go, openmeter/billing/charges/flatfee/service/creditheninvoice.go, openmeter/billing/charges/usagebased/service/lineengine.go, openmeter/billing/charges/charge.go, openmeter/billing/charges/flatfee/service/realizations/service.go, openmeter/ledger/...
Manual invoice-line patch validation rejects immutable FeatureKey and TaxConfig edits, and ledger/customer-balance consumers switch to GetFeatureKey() / GetTaxConfig().
Fixture and assertion updates
openmeter/billing/charges/*/*_test.go, openmeter/ledger/.../*_test.go, test/credits/*, test/app/stripe/*
Helpers and assertions are updated to build intents with the new field placement and to assert against GetTaxConfig() / GetFeatureKey() instead of the removed effective accessors.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • openmeterio/openmeter#4556: Introduced DB-backed intent override support for flat-fee/usage-based charges, which this PR updates by removing tax/feature-key persistence from override layers.
  • openmeterio/openmeter#4588: Modified flat-fee manual-edit wiring for FeatureKey and TaxConfig; this PR changes that same flow to enforce immutability instead.
  • openmeterio/openmeter#4607: Refactored line-manual-edit patch targeting/mutation, and this PR adds validation on top of that path for immutable intent fields.

Suggested labels

area/billing

Suggested reviewers

  • tothandras
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.23% 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 clearly matches the main change: charge intent fields are no longer overridable.
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/charge-override-provenance

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.

@turip turip added release-note/deprecation Release note: Deprecations area/billing labels Jun 30, 2026
@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR makes charge feature and tax attribution immutable on charge intents. The main changes are:

  • Moves tax config onto shared charge intent metadata.
  • Moves feature keys onto flat-fee and usage-based base intents.
  • Blocks manual line edits that try to override feature key or tax config.
  • Stops persisting those fields through charge override layers.
  • Updates generated Ent code, migrations, fixtures, and tests.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.
  • The manual edit guards now cover the changed charge-backed edit paths.
  • The immutable intent-field mapping is consistent across the inspected adapters.

Important Files Changed

Filename Overview
openmeter/billing/charges/meta/patchlinemanualedit.go Adds shared validation that rejects invoice-line overrides for immutable charge fields.
openmeter/billing/charges/flatfee/service/creditheninvoice.go Rejects protected flat-fee manual edit fields before applying line overrides.
openmeter/billing/charges/usagebased/service/lineengine.go Rejects protected usage-based manual edit fields before returning the existing update error.
openmeter/billing/charges/models/chargemeta/mixin.go Centralizes immutable tax config on base charge metadata and removes update-time tax writes.
tools/migrate/migrations/20260630145544_deprecate_charge_override_tax_feature.down.sql Keeps the rollback shape for the deprecated usage-based override feature key column.

Reviews (3): Last reviewed commit: "fix(billing): handle optional flat fee f..." | Re-trigger Greptile

Comment thread openmeter/billing/charges/meta/patchlinemanualedit.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)
openmeter/billing/charges/usagebased/charge.go (1)

394-415: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Tiny duplication heads-up (optional).

GetTaxConfig (Line 396) and GetBaseTaxConfig (Line 413) now return the exact same i.intent.TaxConfig, and similarly GetFeatureKey mirrors GetBaseFeatureKey. Since tax/feature are immutable now, the "base" vs effective distinction has collapsed. If the dual naming isn't needed for an external API contract, you could collapse each pair to one method to avoid future confusion. Totally fine to leave as-is if the names carry intent for callers.

🤖 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/usagebased/charge.go` around lines 394 - 415,
`OverridableIntent` now has duplicate accessors for the same immutable values:
`GetTaxConfig` and `GetBaseTaxConfig` both return `i.intent.TaxConfig`, and the
same pattern exists for the feature-key methods. If the separate “base” naming
is not required for the public API, collapse each pair into a single accessor
and update callers to use the remaining method; otherwise, keep both but make
the naming intent explicit in the `OverridableIntent` methods to avoid
confusion.
🤖 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 `@api/v3/handlers/customers/charges/convert.go`:
- Line 70: The flat-fee FeatureKey field is now optional and should remain null
when unset, so update the customer charge conversion in convert.go to preserve
the pointer value from intent.FeatureKey rather than coercing it to an empty
string. Check the response mapping around the FeatureKey assignment in the
charge conversion path and make sure the API output reflects nil/omitted for
unset values while still passing through real feature keys unchanged.

---

Nitpick comments:
In `@openmeter/billing/charges/usagebased/charge.go`:
- Around line 394-415: `OverridableIntent` now has duplicate accessors for the
same immutable values: `GetTaxConfig` and `GetBaseTaxConfig` both return
`i.intent.TaxConfig`, and the same pattern exists for the feature-key methods.
If the separate “base” naming is not required for the public API, collapse each
pair into a single accessor and update callers to use the remaining method;
otherwise, keep both but make the naming intent explicit in the
`OverridableIntent` methods to avoid confusion.
🪄 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: f79dff40-3383-4e1b-8bdf-531d95bf1dc5

📥 Commits

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

⛔ Files ignored due to path filters (17)
  • openmeter/ent/db/chargecreditpurchase_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargecreditpurchase_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeflatfee_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeflatfee_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeflatfeeoverride.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeflatfeeoverride/chargeflatfeeoverride.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebased_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebased_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedoverride.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedoverride/chargeusagebasedoverride.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedoverride/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedoverride_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedoverride_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/setorclear.go is excluded by !**/ent/db/**
  • tools/migrate/migrations/atlas.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (59)
  • api/v3/handlers/customers/charges/convert.go
  • openmeter/billing/charges/charge.go
  • openmeter/billing/charges/creditpurchase/adapter/charge.go
  • openmeter/billing/charges/flatfee/adapter/charge.go
  • openmeter/billing/charges/flatfee/adapter/detailedline_test.go
  • openmeter/billing/charges/flatfee/adapter/intentoverride.go
  • openmeter/billing/charges/flatfee/adapter/intentoverride_test.go
  • openmeter/billing/charges/flatfee/adapter/mapper.go
  • openmeter/billing/charges/flatfee/adapter/realizationrun_test.go
  • openmeter/billing/charges/flatfee/charge.go
  • openmeter/billing/charges/flatfee/service/create.go
  • openmeter/billing/charges/flatfee/service/creditheninvoice.go
  • openmeter/billing/charges/flatfee/service/manualedit.go
  • openmeter/billing/charges/flatfee/service/realizations/service.go
  • openmeter/billing/charges/meta/intent.go
  • openmeter/billing/charges/meta/patchlinemanualedit.go
  • openmeter/billing/charges/meta/patchlinemanualedit_test.go
  • openmeter/billing/charges/models/chargemeta/mixin.go
  • openmeter/billing/charges/service/base_test.go
  • openmeter/billing/charges/service/pendinglines.go
  • openmeter/billing/charges/service/taxcode_test.go
  • openmeter/billing/charges/service/truncation_test.go
  • openmeter/billing/charges/usagebased/adapter/charge.go
  • openmeter/billing/charges/usagebased/adapter/detailedline_test.go
  • openmeter/billing/charges/usagebased/adapter/intentoverride.go
  • openmeter/billing/charges/usagebased/adapter/intentoverride_test.go
  • openmeter/billing/charges/usagebased/adapter/mapper.go
  • openmeter/billing/charges/usagebased/charge.go
  • openmeter/billing/charges/usagebased/service/creditheninvoice_test.go
  • openmeter/billing/charges/usagebased/service/creditsonly_test.go
  • openmeter/billing/charges/usagebased/service/lineengine.go
  • openmeter/billing/charges/usagebased/service/rating/service_test.go
  • openmeter/billing/charges/usagebased/service/rating/subtract/subtract_test.go
  • openmeter/billing/charges/usagebased/service/rating/testutils/testutils.go
  • openmeter/billing/charges/usagebased/service/run/credits.go
  • openmeter/billing/charges/usagebased/service/run/payment_test.go
  • openmeter/billing/creditgrant/service/service.go
  • openmeter/billing/errors.go
  • openmeter/billing/worker/subscriptionsync/service/reconciler/patchcharge_test.go
  • openmeter/billing/worker/subscriptionsync/service/reconciler/patchchargeflatfee.go
  • openmeter/billing/worker/subscriptionsync/service/reconciler/patchchargeusagebased.go
  • openmeter/billing/worker/subscriptionsync/service/sync_credittheninvoice_test.go
  • openmeter/ent/schema/chargescreditpurchase.go
  • openmeter/ent/schema/chargesflatfee.go
  • openmeter/ent/schema/chargesusagebased.go
  • openmeter/ledger/chargeadapter/creditpurchase_test.go
  • openmeter/ledger/chargeadapter/flatfee.go
  • openmeter/ledger/chargeadapter/flatfee_test.go
  • openmeter/ledger/chargeadapter/usagebased.go
  • openmeter/ledger/chargeadapter/usagebased_test.go
  • openmeter/ledger/customerbalance/calculation.go
  • openmeter/ledger/customerbalance/expired_loader_test.go
  • openmeter/ledger/customerbalance/service.go
  • openmeter/ledger/customerbalance/testenv_test.go
  • test/app/stripe/invoice_credits_test.go
  • test/credits/base.go
  • test/credits/sanity_test.go
  • tools/migrate/migrations/20260630145544_deprecate_charge_override_tax_feature.down.sql
  • tools/migrate/migrations/20260630145544_deprecate_charge_override_tax_feature.up.sql

Comment thread api/v3/handlers/customers/charges/convert.go
@turip

turip commented Jun 30, 2026

Copy link
Copy Markdown
Member Author

Greptile follow-up:

  1. Legacy invoice-line edits bypassing the charge-specific immutable-field guard is expected. The invariant in this PR is for charge-backed line engines and charge intent provenance; legacy invoice-engine lines are not governed by that charge intent provenance path.

  2. The down-migration rollback concern around nullable usage-based override feature keys is acceptable for this PR. This is an inactive feature for now, so rollback compatibility for rows created by the new inactive path is not required here.

Comment thread openmeter/billing/charges/meta/patchlinemanualedit.go
@turip
turip enabled auto-merge (squash) June 30, 2026 16:27
@turip
turip merged commit bb2cf2b into main Jun 30, 2026
26 checks passed
@turip
turip deleted the feat/charge-override-provenance branch June 30, 2026 16:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants