Skip to content

feat(billing): add mutable line deletion hook#4317

Merged
turip merged 2 commits into
mainfrom
chore/lineengine-mutable-line-delete-hook
May 8, 2026
Merged

feat(billing): add mutable line deletion hook#4317
turip merged 2 commits into
mainfrom
chore/lineengine-mutable-line-delete-hook

Conversation

@turip
Copy link
Copy Markdown
Member

@turip turip commented May 7, 2026

Summary

  • add a billing line-engine hook for mutable standard invoice line deletion
  • detect newly deleted mutable standard lines inside the billing invoice update transaction
  • add no-op hook implementations for existing line engines and focused billing service tests

Summary by CodeRabbit

  • New Features
    • Billing now detects invoice standard lines newly marked deleted during edits and invokes per-engine deletion handlers so each line engine processes only its lines.
  • Chores
    • Added default no-op handling for engines that don’t require special deletion logic.
    • Service interface extended to surface deletion events to registered engines.
  • Tests
    • Added tests for deletion detection, per-engine grouping, and error propagation from engine handlers.

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

coderabbitai Bot commented May 7, 2026

Review Change Stack
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: e7cdd87b-10f7-4851-af81-33a4bd5142b1

📥 Commits

Reviewing files that changed from the base of the PR and between 66ba7d0 and 36daa87.

📒 Files selected for processing (13)
  • openmeter/billing/charges/creditpurchase/lineengine/engine.go
  • openmeter/billing/charges/flatfee/service/lineengine.go
  • openmeter/billing/charges/usagebased/service/lineengine.go
  • openmeter/billing/lineengine.go
  • openmeter/billing/lineengine/engine.go
  • openmeter/billing/service.go
  • openmeter/billing/service/invoice.go
  • openmeter/billing/service/invoice_test.go
  • openmeter/billing/service/lineengine.go
  • openmeter/billing/service/lineengine_test.go
  • openmeter/billing/testutils/lineengine.go
  • openmeter/server/server_test.go
  • test/billing/lineengine_test.go
✅ Files skipped from review due to trivial changes (1)
  • openmeter/billing/service/lineengine_test.go
🚧 Files skipped from review as they are similar to previous changes (12)
  • openmeter/billing/service/lineengine.go
  • openmeter/billing/lineengine/engine.go
  • openmeter/billing/lineengine.go
  • openmeter/billing/charges/creditpurchase/lineengine/engine.go
  • openmeter/billing/service/invoice_test.go
  • test/billing/lineengine_test.go
  • openmeter/server/server_test.go
  • openmeter/billing/service.go
  • openmeter/billing/testutils/lineengine.go
  • openmeter/billing/charges/flatfee/service/lineengine.go
  • openmeter/billing/service/invoice.go
  • openmeter/billing/charges/usagebased/service/lineengine.go

📝 Walkthrough

Walkthrough

Adds a LineEngine lifecycle hook OnMutableStandardLinesDeleted; invoice edit flow snapshots pre-edit lines and detects newly deleted standard lines, the service groups deleted lines by engine type and dispatches per-engine callbacks; engines and test helpers are updated (mostly no-op implementations) and unit tests added.

Changes

Mutable Standard Lines Deletion Lifecycle Hook

Layer / File(s) Summary
Interface Contract
openmeter/billing/lineengine.go, openmeter/billing/service.go
LineEngine and LineEngineService gain OnMutableStandardLinesDeleted(ctx, OnMutableStandardLinesDeletedInput) error; exported On*Input aliases are reorganized to include the new alias.
Service-Level Orchestration
openmeter/billing/service/lineengine.go
Service.OnMutableStandardLinesDeleted validates input, groups standard lines by LineEngineType, and dispatches per-engine callbacks with error wrapping.
Deletion Detection & Integration
openmeter/billing/service/invoice.go
Adds collectNewlyDeletedStandardLines, snapshots pre-edit lines, detects transitions to deleted, clones deleted lines, and invokes s.OnMutableStandardLinesDeleted when deletions are found.
Engine Implementations
openmeter/billing/charges/creditpurchase/lineengine/engine.go, openmeter/billing/charges/flatfee/service/lineengine.go, openmeter/billing/charges/usagebased/service/lineengine.go, openmeter/billing/lineengine/engine.go, openmeter/billing/testutils/lineengine.go
Concrete engines and the base/no-op helpers add OnMutableStandardLinesDeleted implementations that accept the input and return nil (no-op).
Tests & Helpers
openmeter/billing/service/invoice_test.go, openmeter/billing/service/lineengine_test.go, test/billing/lineengine_test.go, openmeter/server/server_test.go
Adds TestCollectNewlyDeletedStandardLines; tests grouping/dispatch and engine-error propagation; extends test doubles and server stub to support/record the new hook.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

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 'feat(billing): add mutable line deletion hook' clearly and concisely summarizes the main change—adding a new billing hook for mutable standard line deletions.
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 chore/lineengine-mutable-line-delete-hook

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 release-note/misc Miscellaneous changes area/billing labels May 7, 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.

🧹 Nitpick comments (3)
openmeter/billing/charges/usagebased/service/lineengine.go (1)

231-233: Heads-up: this no-op may need revisiting for usage-based lines.

Flat fee and credit purchase engines are stateless w.r.t. line deletion, but usage-based lines can have active realization runs started during OnStandardInvoiceCreated. If a mutable usage-based standard line is deleted while a run is active, the run currently has no invoice line to land its results on — which could leave orphaned state in the charge state machine.

This is clearly out of scope for this PR (the hook foundation is the right first step!), but it's worth tracking as a follow-up so the usage-based engine can fire an appropriate trigger (e.g., cancel/void the active run) when its lines are deleted.

🤖 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/service/lineengine.go` around lines 231
- 233, The no-op OnMutableStandardLinesDeleted in LineEngine currently ignores
deletions which can orphan active usage-based realization runs started in
OnStandardInvoiceCreated; update OnMutableStandardLinesDeleted to detect if any
deleted standard lines belong to usage-based charges, look up active realization
runs in the charge state machine for those line IDs, and trigger appropriate
cleanup (e.g., cancel or void the run and persist state) so runs do not remain
orphaned — use LineEngine's existing helpers for charge lookup and state
transitions (same services used by OnStandardInvoiceCreated) to locate runs and
invoke the cancel/void transition.
openmeter/billing/service/invoice_test.go (1)

18-37: 💤 Low value

Good coverage of the key scenarios!

The four cases — newly deleted, already deleted, unchanged, and appeared-already-deleted — map cleanly to the expected behavior. One gap worth considering: there's no test case for a non-mutable (system-managed) line that transitions from non-deleted to deleted. Since the hook is named OnMutableStandardLinesDeleted, the filtering should exclude such lines, but nothing currently verifies that.

🧪 Suggested additional case
 before := billing.NewStandardInvoiceLines([]*billing.StandardLine{
     newStandardLineForLineEngineTest("newly-deleted", billing.LineEngineTypeInvoice, false),
     newStandardLineForLineEngineTest("already-deleted", billing.LineEngineTypeInvoice, true),
     newStandardLineForLineEngineTest("unchanged", billing.LineEngineTypeInvoice, false),
+    newSystemManagedLineForLineEngineTest("system-deleted", billing.LineEngineTypeInvoice, false),
 })

 after := billing.NewStandardInvoiceLines([]*billing.StandardLine{
     newStandardLineForLineEngineTest("newly-deleted", billing.LineEngineTypeInvoice, true),
     newStandardLineForLineEngineTest("already-deleted", billing.LineEngineTypeInvoice, true),
     newStandardLineForLineEngineTest("unchanged", billing.LineEngineTypeInvoice, false),
     newStandardLineForLineEngineTest("new-deleted-line", billing.LineEngineTypeInvoice, true),
+    newSystemManagedLineForLineEngineTest("system-deleted", billing.LineEngineTypeInvoice, true),
 })

(If filtering happens in the caller rather than in collectNewlyDeletedStandardLines, this might be better placed in a test for executeTriggerOnInvoice instead.)

🤖 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/service/invoice_test.go` around lines 18 - 37, Add a test
case to verify that system-managed (non-mutable) lines are excluded when they
transition from non-deleted to deleted: in TestCollectNewlyDeletedStandardLines
extend the "after" and "before" fixtures to include a line created by
newStandardLineForLineEngineTest with an identifier like "system-deleted" where
the before has DeletedAt=nil and the after has DeletedAt set, but with the line
marked as non-mutable/system-managed; call collectNewlyDeletedStandardLines and
assert that this "system-deleted" line is NOT present in deletedLines (ensuring
filtering by mutability), and if you determine filtering happens at a different
layer, add the equivalent assertion to the executeTriggerOnInvoice test instead
while referencing the OnMutableStandardLinesDeleted hook to validate behavior.
openmeter/billing/service/lineengine_test.go (1)

13-57: ⚡ Quick win

Great happy-path coverage; add one failure-path case too.

Consider adding a case where an engine hook returns an error (or engine type is unregistered), and assert the service returns the expected wrapped error. It’ll harden the new dispatch path with minimal effort.

As per coding guidelines "**/*_test.go: Make sure the tests are comprehensive and cover the changes."

🤖 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/service/lineengine_test.go` around lines 13 - 57, Add a
failure-path test alongside TestOnMutableStandardLinesDeletedGroupsLinesByEngine
that verifies OnMutableStandardLinesDeleted returns a wrapped error when an
engine fails or is missing: create a recordingLineEngine (or a stub) whose
OnMutableStandardLinesDeleted method returns a sentinel error, register only the
other engine (or omit registration for the failing engine), call
svc.OnMutableStandardLinesDeleted with lines that include the failing engine's
LineEngineType, and assert the call returns a non-nil error that wraps the
sentinel (use errors.Is or string containment); reference
TestOnMutableStandardLinesDeletedGroupsLinesByEngine,
Service.RegisterLineEngine, Service.OnMutableStandardLinesDeleted, and
recordingLineEngine to locate where to add the new test.
🤖 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/usagebased/service/lineengine.go`:
- Around line 231-233: The no-op OnMutableStandardLinesDeleted in LineEngine
currently ignores deletions which can orphan active usage-based realization runs
started in OnStandardInvoiceCreated; update OnMutableStandardLinesDeleted to
detect if any deleted standard lines belong to usage-based charges, look up
active realization runs in the charge state machine for those line IDs, and
trigger appropriate cleanup (e.g., cancel or void the run and persist state) so
runs do not remain orphaned — use LineEngine's existing helpers for charge
lookup and state transitions (same services used by OnStandardInvoiceCreated) to
locate runs and invoke the cancel/void transition.

In `@openmeter/billing/service/invoice_test.go`:
- Around line 18-37: Add a test case to verify that system-managed (non-mutable)
lines are excluded when they transition from non-deleted to deleted: in
TestCollectNewlyDeletedStandardLines extend the "after" and "before" fixtures to
include a line created by newStandardLineForLineEngineTest with an identifier
like "system-deleted" where the before has DeletedAt=nil and the after has
DeletedAt set, but with the line marked as non-mutable/system-managed; call
collectNewlyDeletedStandardLines and assert that this "system-deleted" line is
NOT present in deletedLines (ensuring filtering by mutability), and if you
determine filtering happens at a different layer, add the equivalent assertion
to the executeTriggerOnInvoice test instead while referencing the
OnMutableStandardLinesDeleted hook to validate behavior.

In `@openmeter/billing/service/lineengine_test.go`:
- Around line 13-57: Add a failure-path test alongside
TestOnMutableStandardLinesDeletedGroupsLinesByEngine that verifies
OnMutableStandardLinesDeleted returns a wrapped error when an engine fails or is
missing: create a recordingLineEngine (or a stub) whose
OnMutableStandardLinesDeleted method returns a sentinel error, register only the
other engine (or omit registration for the failing engine), call
svc.OnMutableStandardLinesDeleted with lines that include the failing engine's
LineEngineType, and assert the call returns a non-nil error that wraps the
sentinel (use errors.Is or string containment); reference
TestOnMutableStandardLinesDeletedGroupsLinesByEngine,
Service.RegisterLineEngine, Service.OnMutableStandardLinesDeleted, and
recordingLineEngine to locate where to add the new test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e7b9eab0-b5fa-4814-aad6-ff44ef2e78a0

📥 Commits

Reviewing files that changed from the base of the PR and between 8d3a5a0 and 8d39066.

📒 Files selected for processing (11)
  • openmeter/billing/charges/creditpurchase/lineengine/engine.go
  • openmeter/billing/charges/flatfee/service/lineengine.go
  • openmeter/billing/charges/usagebased/service/lineengine.go
  • openmeter/billing/lineengine.go
  • openmeter/billing/lineengine/engine.go
  • openmeter/billing/service.go
  • openmeter/billing/service/invoice.go
  • openmeter/billing/service/invoice_test.go
  • openmeter/billing/service/lineengine.go
  • openmeter/billing/service/lineengine_test.go
  • openmeter/billing/testutils/lineengine.go

@turip turip force-pushed the chore/lineengine-mutable-line-delete-hook branch from 66ba7d0 to 36daa87 Compare May 8, 2026 13:23
@turip turip enabled auto-merge (squash) May 8, 2026 14:28
@turip turip merged commit 63399c9 into main May 8, 2026
25 checks passed
@turip turip deleted the chore/lineengine-mutable-line-delete-hook branch May 8, 2026 14:30
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