Skip to content

feat(charges): add backend customer charge overrides - #4884

Closed
turip wants to merge 2 commits into
mainfrom
feat/charges-override-backend-api
Closed

feat(charges): add backend customer charge overrides#4884
turip wants to merge 2 commits into
mainfrom
feat/charges-override-backend-api

Conversation

@turip

@turip turip commented Aug 7, 2026

Copy link
Copy Markdown
Member

Summary

  • add backend customer-charge deletion for flat-fee and usage-based charges using a manual override layer
  • add backend operations for setting a complete mutable override snapshot and clearing the override
  • preserve the subscription-owned base intent while subscription sync continues reconciling it
  • reject credit-purchase overrides, which remain out of scope
  • expose PaymentAdjustmentNone and map it to the existing no-credit-refund/no-invoice-refund deletion policy

Lifecycle behavior

  • setting an override replaces the existing override layer rather than stacking layers
  • clearing a live-base override passes through settlement-specific active.clear_override cleanup and returns in the restored created, active, or final state
  • clearing an override whose hidden base was deleted passes through deleted.clear_override and returns in the normal deleted state
  • flat-fee credit-then-invoice clear cancels and detaches any mutable current realization before rebuilding billing work for the restored base
  • deletion with PaymentAdjustmentNone preserves already-realized credit, invoice, and payment history

Coverage

  • state-machine coverage for flat-fee and usage-based, in both credit-only and credit-then-invoice settlement modes
  • facade validation, ownership, payment-adjustment mapping, unsupported credit-purchase, and idempotent-clear coverage
  • subscription-sync coverage proving overrides survive reconciliation without duplicate charges and clearing restores the latest reconciled base
  • paid-invoice coverage proving immutable history survives delete, sync, and clear

Validation

  • go test ./openmeter/billing/charges ./openmeter/billing/charges/meta ./openmeter/billing/charges/statemachine ./openmeter/billing/charges/flatfee/service ./openmeter/billing/charges/usagebased/service -count=1
  • POSTGRES_HOST=127.0.0.1 go test ./openmeter/billing/charges/service -run 'TestCustomerChargeAPI' -count=1
  • focused credits-only and credit-then-invoice subscription-sync integration tests

Follow-up: patch advancement sequencing

The implementation currently makes the shared ApplyPatch call AdvanceUntilStateStable before the charge service applies accumulated invoice patches. The review discussion identified this as a real sequencing regression rather than only stale test expectations:

  • lifecycle state can advance before the corresponding invoice projection exists
  • a mutable zero-fiat flat-fee realization can advance to final, marking its run immutable and breaking the existing shrink-then-extend workflow
  • immutable proration can move from created to active before replacement gathering work has been applied

The likely follow-up design is a service-owned advancement loop that treats every transition as a complete step:

  1. fire and persist one transition
  2. drain and apply invoice patches
  3. refetch the charge because billing callbacks may have changed it
  4. only then evaluate and fire the next transition
  5. apply any invoice patches produced by that transition

A separate policy decision remains: whether every patch requests synchronous stabilization or only operations such as clear override. If mutable shrink-then-extend must keep using the same realization, ordinary shrink cannot be advanced to stability synchronously. This follow-up is intentionally left for a dedicated change.

Known broader-suite failures related to this follow-up are currently in the immutable-proration and custom-currency zero-fiat shrink/extend scenarios in invoicable_test.go.

Greptile Summary

The PR adds customer-managed override snapshots and deletion operations for flat-fee and usage-based charges, preserving subscription-owned base intent.

  • Adds set, clear, and delete facade operations with ownership and payment-adjustment handling.
  • Adds transient clear-override lifecycle states and settlement-specific cleanup.
  • Persists override layers and adds subscription-sync and billing-history coverage.
  • The new persisted detailed statuses are missing their required database migration.

Confidence Score: 4/5

The PR should not merge until a migration allows the new clear-override detailed status values in existing databases.

Override clearing persists active.clear_override or deleted.clear_override, but the committed migration history does not update the database constraints to accept those values.

Files Needing Attention: openmeter/ent/db/migrate/schema.go and tools/migrate/migrations/

Important Files Changed

Filename Overview
openmeter/billing/charges/service/api.go Adds customer-scoped delete and override facade operations with downstream ownership validation.
openmeter/billing/charges/statemachine/machine.go Adds synchronous post-patch stabilization; its known invoice-patch sequencing regression is explicitly deferred in the PR.
openmeter/billing/charges/flatfee/service/creditheninvoice.go Adds override transitions, realization cleanup, and restored-base invoice reconciliation.
openmeter/billing/charges/usagebased/service/creditsonly.go Adds override rerating and clear behavior across usage-based lifecycle states.
openmeter/ent/db/migrate/schema.go Adds persisted transient enum values without a corresponding SQL migration for deployed databases.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  API[Customer clear override] --> Patch[ClearOverride patch]
  Patch --> State{Hidden base state}
  State -->|Live| ActiveClear[active.clear_override]
  State -->|Deleted| DeletedClear[deleted.clear_override]
  ActiveClear --> Persist[Persist status_detailed]
  DeletedClear --> Persist
  Persist --> DB[(PostgreSQL)]
  DB -->|Migration lacks new values| Error[Constraint rejection]
Loading

Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
### Issue 1
openmeter/ent/db/migrate/schema.go:2207
**Missing detailed-status migration**

When clearing an override transitions a flat-fee or usage-based charge through `active.clear_override` or `deleted.clear_override`, the adapters persist a value that the committed database migration history does not add to the existing `status_detailed` constraint, causing deployed databases to reject the update and the clear operation to fail.

---

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

Reviews (1): Last reviewed commit: "feat(charges): add backend charge overri..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Context used:

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ebd090e6-dcf8-4304-82de-68b7a210f10e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

{Name: "amount_before_proration", Type: field.TypeOther, SchemaType: map[string]string{"postgres": "numeric"}},
{Name: "amount_after_proration", Type: field.TypeOther, SchemaType: map[string]string{"postgres": "numeric"}},
{Name: "status_detailed", Type: field.TypeEnum, Enums: []string{"created", "active", "active.realization.started", "active.realization.waiting_for_collection", "active.realization.processing", "active.realization.issuing", "active.realization.zero_fiat_amount_overage_completed", "active.realization.completed", "active.awaiting_payment_settlement", "final", "deleted"}},
{Name: "status_detailed", Type: field.TypeEnum, Enums: []string{"created", "active", "active.clear_override", "active.realization.started", "active.realization.waiting_for_collection", "active.realization.processing", "active.realization.issuing", "active.realization.zero_fiat_amount_overage_completed", "active.realization.completed", "active.awaiting_payment_settlement", "final", "deleted", "deleted.clear_override"}},

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 Missing detailed-status migration

When clearing an override transitions a flat-fee or usage-based charge through active.clear_override or deleted.clear_override, the adapters persist a value that the committed database migration history does not add to the existing status_detailed constraint, causing deployed databases to reject the update and the clear operation to fail.

Knowledge Base Used: Data layer: ent schema, migrations, and generated client

Prompt To Fix With AI
This is a comment left during a code review.
Path: openmeter/ent/db/migrate/schema.go
Line: 2207

Comment:
**Missing detailed-status migration**

When clearing an override transitions a flat-fee or usage-based charge through `active.clear_override` or `deleted.clear_override`, the adapters persist a value that the committed database migration history does not add to the existing `status_detailed` constraint, causing deployed databases to reject the update and the clear operation to fail.

**Knowledge Base Used:** [Data layer: ent schema, migrations, and generated client](https://app.greptile.com/openmeter/-/custom-context/knowledge-base/openmeterio/openmeter/-/docs/data-layer.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

@turip turip closed this Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant