Skip to content

feat: invoice line persisting#4143

Merged
turip merged 2 commits intomainfrom
fix/usage-based-line-id-persisting
Apr 15, 2026
Merged

feat: invoice line persisting#4143
turip merged 2 commits intomainfrom
fix/usage-based-line-id-persisting

Conversation

@turip
Copy link
Copy Markdown
Member

@turip turip commented Apr 15, 2026

Summary

Persist usage-based realization run line_id against the billing line and move the usage-based line assignment to a post-persist invoice hook.

This splits the line-ID persistence work out from the broader invoice-accrual changes.

What changed

Database / Ent

  • Added nullable line_id to usage-based realization runs
  • Added the FK from usage-based runs to billing_invoice_line
  • Regenerated Ent
  • Added migration:
    • 20260413154216_usagebased-run-line-id

Billing lifecycle

  • Added OnStandardInvoiceCreated to the line engine contract
  • CreateStandardInvoiceFromGatheringLines now:
    1. persists the standard invoice and lines
    2. invokes OnStandardInvoiceCreated
    3. recalculates and persists again

Usage-based

  • BuildStandardInvoiceLines is now structural only
  • Usage-based run creation / line assignment moved to OnStandardInvoiceCreated
  • credit_then_invoice final realization start now receives the persisted standard line ID and stores it on the run
  • OnCollectionCompleted now reloads the state machine from the standard line and keeps working with the persisted run.LineID

Why

Previously the usage-based run could be linked too early, while the line was still in its gathering-line lifecycle. Because the FK points to the invoice line row, that timing was unstable.

This change makes the assignment happen only after the standard invoice line is persisted, so run.LineID points at the stable standard line row.

Tests

Added / updated coverage to assert that for usage-based credit_then_invoice:

  • the current realization run gets the persisted standard line ID after InvoicePendingLines
  • the same run.LineID is still present after collection completion

Validated with:

nix develop --impure .#ci -c env GOCACHE=/tmp/go-build go test -tags=dynamic -run '^$' ./openmeter/billing/... ./test/billing
nix develop --impure .#ci -c env POSTGRES_HOST=127.0.0.1 GOCACHE=/tmp/go-build go test -count=1 -tags=dynamic -run 'TestInvoicableCharges/TestUsageBasedCreditThenInvoiceLifecycle$' -v ./openmeter/billing/charges/service

Notes

  • This PR intentionally does not include the later invoice-accrual / OnInvoiceIssued behavior changes.
  • The branch stays focused on line-ID persistence and the minimal billing hook needed to support it.

Summary by CodeRabbit

  • New Features

    • Improved standard invoice creation flow to invoke per-line hooks and trigger invoice recalculation.
    • Tracking link between usage-based realization runs and invoice line items for clearer traceability.
  • Improvements

    • Added validation and lifecycle checks during invoice creation to ensure lines and realizations are consistently associated.
    • Updated invoice processing to recalculate totals and tax handling after line-level post-create processing.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 15, 2026

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: 0c01b337-1291-4d0a-9396-6ce60dd7e73c

📥 Commits

Reviewing files that changed from the base of the PR and between 31ebba1 and feec7aa.

📒 Files selected for processing (1)
  • openmeter/ent/schema/chargesusagebased.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • openmeter/ent/schema/chargesusagebased.go

📝 Walkthrough

Walkthrough

This PR adds a new OnStandardInvoiceCreated lifecycle hook to line engines, threads invoice line IDs into usage-based realization runs, updates state-machine wiring to accept LineID, invokes per-line hooks during invoice creation with recalculation, and adds DB schema + migrations to store realization run → invoice line links.

Changes

Cohort / File(s) Summary
LineEngine Interface & Core Implementations
openmeter/billing/lineengine.go, openmeter/billing/lineengine/engine.go, openmeter/billing/charges/creditpurchase/lineengine/engine.go, openmeter/billing/charges/flatfee/lineengine/engine.go
Added OnStandardInvoiceCreated to LineEngine. Core and simple charge engines provide pass-through implementations returning input.Lines.
Usage-Based LineEngine Logic
openmeter/billing/charges/usagebased/service/lineengine.go, openmeter/billing/charges/usagebased/service/creditheninvoice.go, openmeter/billing/charges/usagebased/service/stdinvoice.go
Refactored invoice-related flows: added OnStandardInvoiceCreated, centralized state-machine creation (newStateMachineForStandardLine), changed transitions to pass LineID into state machine; removed previous standard-invoice lifecycle stub methods.
Realization Run Model & Propagation
openmeter/billing/charges/usagebased/realizationrun.go, openmeter/billing/charges/usagebased/service/run/create.go, openmeter/billing/charges/usagebased/adapter/realizationrun.go, openmeter/billing/charges/usagebased/adapter/mapper.go
Added optional LineID to run input and base structs, validated non-empty when provided, propagated LineID into Ent create and mapper so DB records include invoice line association.
Invoice Service Integration & Recalc
openmeter/billing/service/gatheringinvoicependinglines.go
After invoice creation, invokes per-line OnStandardInvoiceCreated hooks, validates/replaces lines by ID, then recalculates and persists the invoice. Added invokeOnStandardInvoiceCreated and recalculateStandardInvoice.
Schema, Ent Edges & Migrations
openmeter/ent/schema/chargesusagebased.go, openmeter/ent/schema/billing.go, tools/migrate/migrations/...usagebased-run-line-id.up.sql, ...usagebased-run-line-id.down.sql
Added nullable line_id on charge_usage_based_runs, an edge to billing_invoice_lines (Unique, OnDelete SetNull), and corresponding up/down SQL migrations + unique index.
Service Interface Cleanup
openmeter/billing/charges/usagebased/service.go
Removed InvoiceLifecycleHooks embedding from the usage-based Service interface (removed lifecycle hook requirements).
Tests & Mocks
openmeter/billing/charges/service/invoicable_test.go, test/billing/lineengine_test.go
Updated tests to capture invoice-standard line IDs and assert realization runs reference them. Extended mock line engine with onStandardInvoiceCreated callback and default pass-through behavior.

Sequence Diagram

sequenceDiagram
    participant Client
    participant InvoiceService
    participant LineEngine
    participant StateMachine
    participant DB

    Client->>InvoiceService: CreateStandardInvoiceFromGatheringLines()
    InvoiceService->>DB: persist invoice + lines
    InvoiceService->>LineEngine: OnStandardInvoiceCreated(OnStandardInvoiceCreatedInput{Lines})
    LineEngine->>StateMachine: newStateMachineForStandardLine(stdLine)
    StateMachine->>DB: Get charge (ExpandRealizations)
    StateMachine->>StateMachine: Activate / Fire with invoiceCreatedInput{LineID}
    StateMachine->>DB: CreateRealizationRun(LineID)
    DB-->>StateMachine: RealizationRun record
    StateMachine-->>LineEngine: return StandardLines
    LineEngine-->>InvoiceService: updated lines
    InvoiceService->>InvoiceService: recalculate invoice
    InvoiceService->>DB: persist recalculated invoice
    InvoiceService-->>Client: return finalized invoice
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

release-note/feature

Suggested reviewers

  • tothandras
  • GAlexIHU
🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: invoice line persisting' accurately summarizes the main change—persisting usage-based realization run line IDs against billing invoice lines—and reflects the core feature introduced across multiple files.

✏️ 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 fix/usage-based-line-id-persisting

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 force-pushed the fix/usage-based-line-id-persisting branch from f780aaa to 398ce07 Compare April 15, 2026 12:24
@turip turip added release-note/bug-fix Release note: Bug Fixes area/billing labels Apr 15, 2026
@turip turip marked this pull request as ready for review April 15, 2026 12:25
@turip turip requested a review from a team as a code owner April 15, 2026 12:25
@turip turip force-pushed the fix/usage-based-line-id-persisting branch from 398ce07 to 31ebba1 Compare April 15, 2026 12:58
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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openmeter/billing/charges/usagebased/service/run/create.go`:
- Line 25: Add a non-empty check for LineID inside the
CreateRatedRunInput.Validate() method so the exported input self-validates and
rejects empty LineID before downstream calls; update
CreateRatedRunInput.Validate() to return an error when LineID is nil or empty
(same validation currently in createNewRealizationRun()), and remove or keep the
downstream guard in createNewRealizationRun() as desired to avoid duplicate late
failures after GetRatingForUsage() runs.

In `@openmeter/ent/schema/chargesusagebased.go`:
- Around line 198-201: The edge declaration edge.From("billing_invoice_line",
BillingInvoiceLine.Type).Ref("charge_usage_based_run").Field("line_id").Unique()
is missing the entsql OnDelete annotation; update that edge to include
Annotations(entsql.OnDelete(entsql.SetNull)) so the Ent schema matches the
migration's ON DELETE SET NULL behavior (ensure you add the annotation
import/usages consistent with other edges that specify delete actions).
🪄 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: 0527953c-043e-411f-b41a-9806d2470902

📥 Commits

Reviewing files that changed from the base of the PR and between 171cbc2 and 31ebba1.

⛔ Files ignored due to path filters (18)
  • openmeter/ent/db/billinginvoiceline.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoiceline/billinginvoiceline.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoiceline/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoiceline_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoiceline_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoiceline_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruns.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruns/chargeusagebasedruns.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruns/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruns_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruns_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedruns_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/client.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/runtime.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 (19)
  • openmeter/billing/charges/creditpurchase/lineengine/engine.go
  • openmeter/billing/charges/flatfee/lineengine/engine.go
  • openmeter/billing/charges/service/invoicable_test.go
  • openmeter/billing/charges/usagebased/adapter/mapper.go
  • openmeter/billing/charges/usagebased/adapter/realizationrun.go
  • openmeter/billing/charges/usagebased/realizationrun.go
  • openmeter/billing/charges/usagebased/service.go
  • openmeter/billing/charges/usagebased/service/creditheninvoice.go
  • openmeter/billing/charges/usagebased/service/lineengine.go
  • openmeter/billing/charges/usagebased/service/run/create.go
  • openmeter/billing/charges/usagebased/service/stdinvoice.go
  • openmeter/billing/lineengine.go
  • openmeter/billing/lineengine/engine.go
  • openmeter/billing/service/gatheringinvoicependinglines.go
  • openmeter/ent/schema/billing.go
  • openmeter/ent/schema/chargesusagebased.go
  • test/billing/lineengine_test.go
  • tools/migrate/migrations/20260413154216_usagebased-run-line-id.down.sql
  • tools/migrate/migrations/20260413154216_usagebased-run-line-id.up.sql
💤 Files with no reviewable changes (2)
  • openmeter/billing/charges/usagebased/service.go
  • openmeter/billing/charges/usagebased/service/stdinvoice.go

Comment thread openmeter/billing/charges/usagebased/service/run/create.go
Comment thread openmeter/ent/schema/chargesusagebased.go Outdated
@turip turip requested review from GAlexIHU and tothandras April 15, 2026 13:24
@turip turip merged commit 24a3be8 into main Apr 15, 2026
24 checks passed
@turip turip deleted the fix/usage-based-line-id-persisting branch April 15, 2026 15:26
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