Skip to content

fix: credit-then-invoice discount sync#4376

Merged
turip merged 8 commits into
mainfrom
feat/fix-discount-sync
May 18, 2026
Merged

fix: credit-then-invoice discount sync#4376
turip merged 8 commits into
mainfrom
feat/fix-discount-sync

Conversation

@turip
Copy link
Copy Markdown
Member

@turip turip commented May 18, 2026

Summary

This PR tightens credit-then-invoice reconciliation for subscription-backed charges. From the billing domain perspective it covers three related consistency issues:

  • Keeps charge-backed flat-fee invoice details aligned with the credited standard invoice line, so promotional credit allocations are reflected in persisted realization-run detailed lines and run totals.
  • Repairs subscription item references on existing charges when subscription edits recreate the concrete item row for the same logical item, preserving charge identity while pointing future sync operations at the current subscription item.
  • Moves credit application mapping into shared detailed-line behavior so discount and credit calculations can be reapplied deterministically without carrying stale credit allocations across recalculation.

The PR also extends credit-then-invoice subscription sync coverage for usage-based draft and issued invoice edit flows, including ledger balance expectations, charge state expectations, immutable invoice warning behavior, and promotional credit settlement.

Validation

  • make lint-go-fast
  • targeted billing/charges/subscription-sync go test package set with dynamic tags and POSTGRES_HOST=127.0.0.1
  • focused CreditThenInvoice usage-based gathering update draft and issued invoice tests

Summary by CodeRabbit

  • New Features

    • Charges can now have their subscription item references updated in-place.
  • Improvements

    • Consistent currency rounding and recomputed detailed-line totals.
    • Refined credit allocation and application for more accurate billing.
  • Reliability

    • Subscription-sync can repair mismatched charge subscription references and supports dry-run verification.
  • Tests

    • Expanded unit and integration tests covering credit application, detailed-line behavior, and sync scenarios.

Review Change Stack

@turip turip requested a review from a team as a code owner May 18, 2026 12:19
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1d8118fb-c32f-461f-83a9-db34dea55b7f

📥 Commits

Reviewing files that changed from the base of the PR and between 8175b66 and eb10643.

⛔ Files ignored due to path filters (7)
  • 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/chargeusagebased_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebased_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/setorclear.go is excluded by !**/ent/db/**
📒 Files selected for processing (25)
  • openmeter/billing/charges/flatfee/adapter.go
  • openmeter/billing/charges/flatfee/adapter/charge.go
  • openmeter/billing/charges/flatfee/service.go
  • openmeter/billing/charges/flatfee/service/linemapper.go
  • openmeter/billing/charges/flatfee/service/realizations/credittheninvoice.go
  • openmeter/billing/charges/flatfee/service/subscription.go
  • openmeter/billing/charges/models/chargemeta/mixin.go
  • openmeter/billing/charges/service.go
  • openmeter/billing/charges/service/subscription.go
  • openmeter/billing/charges/usagebased/adapter.go
  • openmeter/billing/charges/usagebased/adapter/charge.go
  • openmeter/billing/charges/usagebased/service.go
  • openmeter/billing/charges/usagebased/service/linemapper.go
  • openmeter/billing/charges/usagebased/service/subscription.go
  • openmeter/billing/invoicedetailedline.go
  • openmeter/billing/invoicedetailedline_test.go
  • openmeter/billing/worker/subscriptionsync/service/base_test.go
  • openmeter/billing/worker/subscriptionsync/service/reconcile.go
  • openmeter/billing/worker/subscriptionsync/service/repair.go
  • openmeter/billing/worker/subscriptionsync/service/sync.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/server/server_test.go

📝 Walkthrough

Walkthrough

Adds UpdateSubscriptionItemID across adapters and services, makes subscription_item_id mutable in schemas, introduces DetailedLines credit helpers, integrates credit application into flat-fee and usage-based flows, implements subscription-sync repair for subscription-item reconciliation, and expands related tests.

Changes

Charge subscription item update and credit handling flow

Layer / File(s) Summary
Contracts (interfaces)
openmeter/billing/charges/flatfee/adapter.go, openmeter/billing/charges/flatfee/service.go, openmeter/billing/charges/usagebased/adapter.go, openmeter/billing/charges/usagebased/service.go, openmeter/billing/charges/service.go
Adds UpdateSubscriptionItemID(ctx, charge, newSubscriptionItemID) (Charge, error) signatures to charge adapter and service interfaces.
DetailedLines credit helpers & tests
openmeter/billing/invoicedetailedline.go, openmeter/billing/invoicedetailedline_test.go
Adds WithCreditsApplied and WithReversedCredits to apply/reverse credits across detailed lines with currency-aware rounding; includes unit tests and helpers verifying distribution, replacement, reversal, and error conditions.
Flat-fee rating and credit-then-invoice refactor
openmeter/billing/charges/flatfee/service/realizations/credittheninvoice.go, openmeter/billing/charges/flatfee/service/linemapper.go
Introduces rateFlatFeeLine and applyCreditsToFlatFeeLine, targets credit allocation from rounded rated totals, applies credits via DetailedLines helpers, and persists detailed lines derived from mapped (credits-applied) lines.
Usage-based detailed-line mapping
openmeter/billing/charges/usagebased/service/linemapper.go
Replaces manual credit arithmetic with mapUsageBasedDetailedLines that clones/reset run detailed lines and applies credits via WithCreditsApplied, recomputing totals from mapped detailed lines.
Schema mutations & adapter implementations
openmeter/billing/charges/models/chargemeta/mixin.go, openmeter/ent/schema/*, openmeter/billing/charges/flatfee/adapter/charge.go, openmeter/billing/charges/usagebased/adapter/charge.go
Removes Immutable() from subscription_item_id fields in Ent schemas and implements transactional UpdateSubscriptionItemID in flat-fee and usage-based adapters that update DB and remap entities.
Service-layer orchestration
openmeter/billing/charges/flatfee/service/subscription.go, openmeter/billing/charges/usagebased/service/subscription.go, openmeter/billing/charges/service/subscription.go, openmeter/server/server_test.go
Adds service-level UpdateSubscriptionItemID implementations with validation, aggregated validation errors, transaction wrapping, type dispatch, compile-time conformance assertions, and a no-op server test stub.
Subscription-sync repair
openmeter/billing/worker/subscriptionsync/service/reconcile.go, openmeter/billing/worker/subscriptionsync/service/repair.go
Adds repairChargeSubscriptionReferences to reconcile persisted charge entries by updating differing concrete subscription_item_id values via UpdateSubscriptionItemID, with dry-run support and mapping helpers between persisted items and charges.
Tests & sync wiring
openmeter/billing/worker/subscriptionsync/service/base_test.go, openmeter/billing/worker/subscriptionsync/service/sync_credittheninvoice_test.go, openmeter/billing/worker/subscriptionsync/service/sync.go
Updates price equality assertions to use decimal equality, reclassifies non-flat prices as usage-based, expands and un-skips many subscription-sync CTI tests (cancellations, progressive billing, gathering/draft/issued flows, discounts), and forwards DryRun into buildSyncPlan.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

area/billing

Suggested reviewers

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

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 'fix: credit-then-invoice discount sync' accurately captures the main objective of the PR, which involves fixing discount synchronization in credit-then-invoice reconciliation. It's concise and directly related to the core changes.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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/fix-discount-sync

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 and usage tips.

@turip turip added kind/bug Something isn't working release-note/bug-fix Release note: Bug Fixes labels May 18, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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 (3)
openmeter/billing/invoicedetailedline_test.go (1)

207-211: 💤 Low value

Consider using InexactFloat64() for decimal assertions per coding guidelines.

The helper works fine and provides a good error message, but the coding guidelines suggest using require.Equal(t, expectedFloat64, actual.InexactFloat64()) for decimal comparisons. Since you're dealing with simple integer-based test values, this would work well:

require.Equal(t, expected.InexactFloat64(), actual.InexactFloat64())

That said, your current approach is readable and the error message is clear, so this is really just a style alignment thing.

🤖 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/invoicedetailedline_test.go` around lines 207 - 211,
Replace the custom equality check in requireDecimalEqual with an assertion that
compares float64s using the Decimal.InexactFloat64() method; specifically,
inside requireDecimalEqual (and any callers), call require.Equal(t,
expected.InexactFloat64(), actual.InexactFloat64()) instead of using
expected.Equal(actual) so the test follows the coding guideline for decimal
assertions.
openmeter/billing/charges/flatfee/service/realizations/credittheninvoice.go (1)

333-367: 💤 Low value

Consider adding a brief comment explaining the two-clone pattern.

The logic here is sound - you need ratingLine with nulled split-line fields for rating, and ratedLine to preserve the original structure. But it took me a moment to understand why there are two clones. A one-liner above line 341 could help future readers:

// ratingLine is a separate clone with split-line metadata cleared for rating;
// generated detailed lines are merged back into ratedLine which keeps original structure.

Just a thought - the existing comment on lines 347-350 does explain the why for nulling the fields, but doesn't explain why a second clone is needed.

🤖 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/flatfee/service/realizations/credittheninvoice.go`
around lines 333 - 367, Add a one-line comment in rateFlatFeeLine explaining the
two-clone pattern: that we create ratedLine as the preserved copy keeping
original split-line metadata and create a separate ratingLine with
SplitLineGroupID/SplitLineHierarchy nulled so rating (GenerateDetailedLines)
runs on a sanitized version and its generated detailed lines are then merged
back into ratedLine; place this comment near where ratedLine and ratingLine are
cloned/initialized to clarify why both clones are needed.
openmeter/billing/charges/flatfee/adapter/charge.go (1)

82-85: ⚡ Quick win

Add the same managed-model validation guard used by other mutating adapter methods.

Small consistency win: UpdateCharge and DeleteCharge validate charge.ManagedModel before DB writes, but this new mutator currently skips that fast-fail check.

Suggested patch
 func (a *adapter) UpdateSubscriptionItemID(ctx context.Context, charge flatfee.Charge, newSubscriptionItemID string) (flatfee.Charge, error) {
+	if err := charge.ManagedModel.Validate(); err != nil {
+		return flatfee.Charge{}, err
+	}
+
 	if err := charge.Validate(); err != nil {
 		return flatfee.Charge{}, err
 	}
🤖 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/flatfee/adapter/charge.go` around lines 82 - 85,
Add the same managed-model validation guard to UpdateSubscriptionItemID: before
performing DB writes, check charge.ManagedModel (like in UpdateCharge and
DeleteCharge) and return an error if it’s invalid/empty to fast-fail; update the
adapter.UpdateSubscriptionItemID function to perform this validation immediately
after charge.Validate() and before any DB operations so it matches the other
mutators' behavior.
🤖 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/worker/subscriptionsync/service/repair.go`:
- Around line 38-45: Before calling lo.SliceToMap on target.Items, detect
duplicate UniqueID values and fail/handle explicitly: compute the list of
billable UniqueIDs (filtering via item.IsBillable()), use lo.Map + lo.Uniq (or
build a count map) to compare counts and if counts differ return an error/log
indicating duplicate UniqueID(s) so you don't silently overwrite; then only call
lo.SliceToMap to build targetByUniqueID once uniqueness is confirmed. Reference:
target.Items, targetstate.StateItem, UniqueID, lo.SliceToMap, and
targetByUniqueID.

---

Nitpick comments:
In `@openmeter/billing/charges/flatfee/adapter/charge.go`:
- Around line 82-85: Add the same managed-model validation guard to
UpdateSubscriptionItemID: before performing DB writes, check charge.ManagedModel
(like in UpdateCharge and DeleteCharge) and return an error if it’s
invalid/empty to fast-fail; update the adapter.UpdateSubscriptionItemID function
to perform this validation immediately after charge.Validate() and before any DB
operations so it matches the other mutators' behavior.

In `@openmeter/billing/charges/flatfee/service/realizations/credittheninvoice.go`:
- Around line 333-367: Add a one-line comment in rateFlatFeeLine explaining the
two-clone pattern: that we create ratedLine as the preserved copy keeping
original split-line metadata and create a separate ratingLine with
SplitLineGroupID/SplitLineHierarchy nulled so rating (GenerateDetailedLines)
runs on a sanitized version and its generated detailed lines are then merged
back into ratedLine; place this comment near where ratedLine and ratingLine are
cloned/initialized to clarify why both clones are needed.

In `@openmeter/billing/invoicedetailedline_test.go`:
- Around line 207-211: Replace the custom equality check in requireDecimalEqual
with an assertion that compares float64s using the Decimal.InexactFloat64()
method; specifically, inside requireDecimalEqual (and any callers), call
require.Equal(t, expected.InexactFloat64(), actual.InexactFloat64()) instead of
using expected.Equal(actual) so the test follows the coding guideline for
decimal assertions.
🪄 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: f33236c6-e217-40b2-a228-ba99b429db22

📥 Commits

Reviewing files that changed from the base of the PR and between 499cdc5 and 52d3829.

⛔ Files ignored due to path filters (7)
  • 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/chargeusagebased_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebased_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/setorclear.go is excluded by !**/ent/db/**
📒 Files selected for processing (24)
  • openmeter/billing/charges/flatfee/adapter.go
  • openmeter/billing/charges/flatfee/adapter/charge.go
  • openmeter/billing/charges/flatfee/service.go
  • openmeter/billing/charges/flatfee/service/linemapper.go
  • openmeter/billing/charges/flatfee/service/realizations/credittheninvoice.go
  • openmeter/billing/charges/flatfee/service/subscription.go
  • openmeter/billing/charges/models/chargemeta/mixin.go
  • openmeter/billing/charges/service.go
  • openmeter/billing/charges/service/subscription.go
  • openmeter/billing/charges/usagebased/adapter.go
  • openmeter/billing/charges/usagebased/adapter/charge.go
  • openmeter/billing/charges/usagebased/service.go
  • openmeter/billing/charges/usagebased/service/linemapper.go
  • openmeter/billing/charges/usagebased/service/subscription.go
  • openmeter/billing/invoicedetailedline.go
  • openmeter/billing/invoicedetailedline_test.go
  • openmeter/billing/worker/subscriptionsync/service/base_test.go
  • openmeter/billing/worker/subscriptionsync/service/reconcile.go
  • openmeter/billing/worker/subscriptionsync/service/repair.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/server/server_test.go
💤 Files with no reviewable changes (2)
  • openmeter/ent/schema/chargescreditpurchase.go
  • openmeter/ent/schema/chargesflatfee.go

Comment thread openmeter/billing/worker/subscriptionsync/service/repair.go Outdated
@turip turip changed the title Fix credit-then-invoice discount sync fix: credit-then-invoice discount sync May 18, 2026
@turip turip requested a review from tothandras May 18, 2026 15:35
@turip turip enabled auto-merge (squash) May 18, 2026 15:40
@turip turip merged commit 7aeba69 into main May 18, 2026
25 of 26 checks passed
@turip turip deleted the feat/fix-discount-sync branch May 18, 2026 15:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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