Skip to content

fix: validate billing discount correlation IDs - #4891

Open
turip wants to merge 1 commit into
mainfrom
feat/charge-discount-validation
Open

fix: validate billing discount correlation IDs#4891
turip wants to merge 1 commit into
mainfrom
feat/charge-discount-validation

Conversation

@turip

@turip turip commented Aug 9, 2026

Copy link
Copy Markdown
Member

Summary

  • require every present billing percentage or usage discount to have a non-empty correlation ID
  • keep API request discounts as product-catalog values until they enter billing ownership, then construct correlated billing discounts
  • preserve existing correlation IDs during invoice-line updates while assigning IDs to newly introduced discount kinds
  • normalize flat-fee and usage-based charge creates before validation while retaining persistence fallbacks
  • provide type-specific product-catalog-to-billing discount constructors and compose the aggregate constructor from them

Root cause

The original correlation-ID repair allocated IDs at charge lifecycle and persistence boundaries, but the billing discount validators still inherited product-catalog validation and therefore did not enforce billing identity. API conversion paths could also construct billing discount types directly from unowned request data. That made an uncorrelated billing-owned discount valid to the type's validator and left correctness dependent on every caller remembering a later fallback.

Impact

Billing-owned discounts now reject missing correlation IDs. Product-catalog discounts remain valid without billing identity, and create/update boundaries explicitly materialize or reconcile billing correlation IDs. Existing defensive upserts remain in place for compatibility.

Validation

  • go test -tags=dynamic ./openmeter/billing/... ./api/v3/handlers/billinginvoices ./api/v3/handlers/customers/charges
  • golangci-lint run --config .golangci-fast.yaml ./openmeter/billing/... ./api/v3/handlers/billinginvoices/... ./api/v3/handlers/customers/charges/...

Summary by CodeRabbit

  • New Features

    • Added consistent handling for percentage, usage-based, and flat-fee discounts across charges and invoices.
    • Discount conversion now automatically assigns stable correlation IDs where needed.
    • Discount updates preserve existing discount identity while applying changed values.
  • Bug Fixes

    • Fixed discount replacement behavior so client-provided IDs no longer overwrite existing billing identities.
    • Added validation to prevent discounts without required correlation IDs.
  • Documentation

    • Documented requirements for stable billing discount identifiers.

Greptile Summary

The PR establishes correlation IDs as required billing-owned identity for percentage and usage discounts.

  • Adds correlation-ID validation and product-catalog-to-billing constructors.
  • Preserves existing IDs when invoice-line discounts are replaced and allocates IDs for new discount kinds.
  • Normalizes charge-create inputs before validation and moves API conversion through product-catalog discount values.
  • Leaves a compatibility gap for legacy charge updates because adapter validation runs before the retained backfill.

Confidence Score: 4/5

The legacy charge-update compatibility failure should be fixed before merging because affected charges cannot advance or accept patches.

The new validators reject empty correlation IDs at the start of adapter updates, while the adapters' explicit legacy-data backfills execute only after validation and are therefore unreachable for the entities they are intended to repair.

Files Needing Attention: openmeter/billing/discount.go and the flat-fee and usage-based charge adapters

Important Files Changed

Filename Overview
openmeter/billing/discount.go Adds required correlation-ID validation and ownership conversion helpers, but the stricter validator blocks legacy charge updates before adapter repair.
openmeter/billing/charges/flatfee/service/create.go Clones intent slices and safely assigns normalized percentage discounts before validation.
openmeter/billing/charges/usagebased/service/create.go Clones intent slices and safely normalizes usage-based discounts before validation.
openmeter/billing/httpdriver/invoiceline.go Materializes billing-owned discounts on creates and preserves existing correlation IDs during merges.
api/v3/handlers/billinginvoices/convert.go Keeps request discounts as product-catalog values until invoice-line ownership is established.
openmeter/billing/httpdriver/discounts.go Converts API discounts into identity-free product-catalog values in accordance with the new ownership boundary.

Sequence Diagram

sequenceDiagram
  participant Worker as Charge worker/sync
  participant SM as Charge state machine
  participant Adapter as Charge adapter
  participant Validator as Discount validator
  Worker->>SM: Advance or patch legacy charge
  SM->>Adapter: UpdateCharge
  Adapter->>Validator: Validate empty correlation ID
  Validator-->>Adapter: correlation ID is required
  Note over Adapter: Defensive upsert is never reached
Loading

Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
### Issue 1
openmeter/billing/discount.go:39-41
**Legacy charge repair is blocked**

If persisted flat-fee or usage-based charges have discounts without correlation IDs, the new validation rejects them during `UpdateCharge` before the adapters reach their defensive backfill, causing charge advancement and patch operations to fail with `correlation ID is required`.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "fix: validate billing discount correlati..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Context used:

@turip
turip requested a review from a team as a code owner August 9, 2026 16:54
@turip turip added kind/bug Something isn't working release-note/bug-fix Release note: Bug Fixes area/billing labels Aug 9, 2026
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Product-catalog discounts now remain separate from billing discounts until conversion. Billing conversion assigns and validates correlation IDs, preserves them during replacements, and applies the flow to customer charges and invoice lines.

Changes

Discount identity and conversion

Layer / File(s) Summary
Billing discount contracts and conversions
openmeter/billing/discount.go, openmeter/billing/discount_test.go, openmeter/billing/README.md
Billing discounts validate non-empty correlation IDs. Product-catalog conversion generates IDs, replacement preserves matching IDs, and round-trip conversion clones discount data.
Product-catalog discount mapping
openmeter/billing/httpdriver/*, api/v3/handlers/billinginvoices/*
HTTP and API mappers retain productcatalog.Discounts until line creation. New lines convert discounts, while updates replace existing discounts.
Customer charge discount creation
api/v3/handlers/customers/charges/convert.go, openmeter/billing/charges/*/service/create.go, openmeter/billing/charges/service/featureid_test.go
Charge conversion uses product-catalog discount types. Charge creation clones intents and assigns correlation IDs before validation and processing.
Invoice line replacement integration
openmeter/billing/httpdriver/invoiceline.go, openmeter/billing/httpdriver/invoiceline_test.go
Standard, gathering, simulation, and replacement lines convert product-catalog discounts. Merge updates preserve existing correlation IDs.

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

Sequence Diagram(s)

sequenceDiagram
  participant API
  participant HTTPDriver
  participant BillingDiscounts
  participant InvoiceLine
  API->>HTTPDriver: provide product-catalog discounts
  HTTPDriver->>BillingDiscounts: convert or replace discounts
  BillingDiscounts->>InvoiceLine: apply billing discounts with correlation IDs
Loading

Possibly related PRs

Suggested reviewers: chrisgacsal, tothandras, borosr

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: validating correlation IDs for billing discounts.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 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-discount-validation

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.

@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/charges/service/featureid_test.go`:
- Around line 298-299: Update the persisted-charge assertions in
openmeter/billing/charges/service/featureid_test.go at lines 298-299 and
344-345: after the fetched charge is loaded, assert that
fetched.Intent.GetEffectiveIntent().PercentageDiscounts and
fetched.Intent.GetEffectiveIntent().Discounts.Usage are non-nil and each has a
non-empty CorrelationID, rather than checking only the originally returned
charge.
🪄 Autofix

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 Plus

Run ID: 140b0b55-eec3-4556-a69d-455ab034777c

📥 Commits

Reviewing files that changed from the base of the PR and between 42574e1 and 6cfd307.

📒 Files selected for processing (13)
  • api/v3/handlers/billinginvoices/convert.go
  • api/v3/handlers/billinginvoices/convert_test.go
  • api/v3/handlers/customers/charges/convert.go
  • openmeter/billing/README.md
  • openmeter/billing/charges/flatfee/service/create.go
  • openmeter/billing/charges/service/featureid_test.go
  • openmeter/billing/charges/usagebased/service/create.go
  • openmeter/billing/discount.go
  • openmeter/billing/discount_test.go
  • openmeter/billing/httpdriver/deprecations.go
  • openmeter/billing/httpdriver/discounts.go
  • openmeter/billing/httpdriver/invoiceline.go
  • openmeter/billing/httpdriver/invoiceline_test.go

Comment on lines +298 to +299
s.Require().NotNil(flatFeeCharge.Intent.GetEffectiveIntent().PercentageDiscounts)
s.NotEmpty(flatFeeCharge.Intent.GetEffectiveIntent().PercentageDiscounts.CorrelationID)

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert correlation IDs after the persisted-charge fetch.

The current assertions only inspect the returned charge. A persistence mapping that drops a correlation ID can still pass these tests.

  • openmeter/billing/charges/service/featureid_test.go#L298-L299: Assert that fetched.Intent.GetEffectiveIntent().PercentageDiscounts is non-nil and has a non-empty CorrelationID.
  • openmeter/billing/charges/service/featureid_test.go#L344-L345: Assert that fetched.Intent.GetEffectiveIntent().Discounts.Usage is non-nil and has a non-empty CorrelationID.

As per coding guidelines, “Suggest missing tests only when the PR introduces behavior without meaningful coverage, or when the current tests would pass despite a concrete regression in the changed code.”

📍 Affects 1 file
  • openmeter/billing/charges/service/featureid_test.go#L298-L299 (this comment)
  • openmeter/billing/charges/service/featureid_test.go#L344-L345
🤖 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/featureid_test.go` around lines 298 - 299,
Update the persisted-charge assertions in
openmeter/billing/charges/service/featureid_test.go at lines 298-299 and
344-345: after the fetched charge is loaded, assert that
fetched.Intent.GetEffectiveIntent().PercentageDiscounts and
fetched.Intent.GetEffectiveIntent().Discounts.Usage are non-nil and each has a
non-empty CorrelationID, rather than checking only the originally returned
charge.

Source: Coding guidelines

Comment on lines +39 to +41
if d.CorrelationID == "" {
errs = append(errs, errors.New("correlation ID is required"))
}

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.

P1 Legacy charge repair is blocked

If persisted flat-fee or usage-based charges have discounts without correlation IDs, the new validation rejects them during UpdateCharge before the adapters reach their defensive backfill, causing charge advancement and patch operations to fail with correlation ID is required.

Knowledge Base Used: Billing: Invoices, Charges, Tax, and Currency

Prompt To Fix With AI
This is a comment left during a code review.
Path: openmeter/billing/discount.go
Line: 39-41

Comment:
**Legacy charge repair is blocked**

If persisted flat-fee or usage-based charges have discounts without correlation IDs, the new validation rejects them during `UpdateCharge` before the adapters reach their defensive backfill, causing charge advancement and patch operations to fail with `correlation ID is required`.

**Knowledge Base Used:** [Billing: Invoices, Charges, Tax, and Currency](https://app.greptile.com/openmeter/-/custom-context/knowledge-base/openmeterio/openmeter/-/docs/billing.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex

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

Labels

area/billing kind/bug Something isn't working release-note/bug-fix Release note: Bug Fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants