Skip to content

feat: add flat fee credit-then-invoice lifecycle#4351

Merged
turip merged 7 commits into
mainfrom
feat/move-flat-fee-to-state-machine
May 13, 2026
Merged

feat: add flat fee credit-then-invoice lifecycle#4351
turip merged 7 commits into
mainfrom
feat/move-flat-fee-to-state-machine

Conversation

@turip
Copy link
Copy Markdown
Member

@turip turip commented May 12, 2026

Overview

Adds flat-fee credit-then-invoice behavior so flat-fee charges begin in a created state, become active at the start of their service period, and produce invoiceable flat-fee lines at the configured invoice time.

Available customer credits are applied before any remaining fiat amount is invoiced. Fully credited charges can complete without payment settlement, while partially credited or non-credited charges remain active until invoice payment is settled.

Deleting flat-fee charges preserves billing history according to invoice mutability: pending gathering lines and mutable standard lines are removed with draft credit effects corrected, while immutable invoice lines remain in place and keep their booked ledger effects.

Notes for reviewer

Shrink and extend behavior remains out of scope for this change.

Summary by CodeRabbit

  • New Features

    • Full credit-then-invoice lifecycle for flat-fee charges with a dedicated state machine
    • Assign invoice lines to ongoing flat-fee realization runs
  • Improvements

    • New active-realization and awaiting-payment statuses; service-period-aware scheduling (advance-after)
    • State-machine-driven handling for invoicing, collection, payment, deletion, mutable-line cleanup, and auto-advance timing
    • Line engine now orchestrates most flat-fee lifecycle events; clearer separation of lifecycle callbacks
  • Documentation

    • Added rules for flat-fee credit-then-invoice lifecycle and line-engine behavior
  • Tests

    • Extensive new and updated tests covering credit-then-invoice flows, transitions, deletions, and timing behaviors

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

coderabbitai Bot commented May 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Implements a state-machine-driven lifecycle for flat-fee "credit_then_invoice" charges: per-line state machines owned by the flat-fee line engine, adapter support to assign invoice lines to realization runs, AdvanceAfter timing for gated advancement, new realization/payment statuses, and expanded tests for creation, deletion, invoicing, and payments.

Changes

Flat-fee Credit-Then-Invoice Settlement Mode

Layer / File(s) Summary
Docs, contracts, and statuses
.agents/skills/charges/SKILL.md, openmeter/billing/charges/flatfee/adapter.go, openmeter/billing/charges/flatfee/statemachine.go
Adds lifecycle rules for line-engine ownership, AssignCurrentRunInvoiceLineInput and validation, InitialAdvanceAfter on intents, new dot-delimited statuses, and status parsing/validation.
Adapter & charge meta creation
openmeter/billing/charges/flatfee/adapter/realizationrun.go, openmeter/billing/charges/flatfee/adapter/charge.go
Implements adapter method to assign invoice/line IDs to current runs and persists AdvanceAfter into charge meta creation.
Create flow
openmeter/billing/charges/flatfee/service/create.go
Compute initial status per settlement mode and populate InitialAdvanceAfter from service-period start when appropriate.
State-machine foundation
openmeter/billing/charges/flatfee/service/statemachine.go, openmeter/billing/charges/flatfee/service/triggers.go
Threads Service into state-machine config, adds service-period helpers to set/clear AdvanceAfter, validates config, and updates factory plumbing to support credit-then-invoice machines.
Credits-only flow updates
openmeter/billing/charges/flatfee/service/creditsonly.go, openmeter/billing/charges/flatfee/handler.go
Gates activation on service-period, replaces SetAdvanceAfterInvoiceAt with AdvanceAfterInvoiceAt, consolidates credit allocation+clear-advance at final, and clarifies handler timing comment.
Credit-then-invoice state machine
openmeter/billing/charges/flatfee/service/creditheninvoice.go
New CreditThenInvoiceStateMachine with full lifecycle: configureStates, DeleteCharge (includes invoice-line deletion), StartRealization (provision/assign runs, apply credits), AccrueInvoiceUsage, and AreAllPaymentsSettled.
Line engine orchestration
openmeter/billing/charges/flatfee/service/lineengine.go
Refactors line engine to drive per-line state machines for invoice collection/issuance/payment events, adds helpers for per-line machine creation and mutable-line deletion cleanup with credit correction.
Invoice accrual & handlers
openmeter/billing/charges/flatfee/service/invoice.go, openmeter/billing/charges/flatfee/service/payment.go
Privatizes invoice/payment handlers, replaces PostInvoiceIssued with accrueInvoiceUsage helper, records LineID on credit allocations, and removes charge-finalization from settlement handler.
Factory, triggers & advance control
openmeter/billing/charges/flatfee/service/triggers.go, openmeter/billing/charges/service/create.go, openmeter/billing/charges/service/advance.go
Factory now constructs credit-then-invoice machines and passes Service into machine configs; auto-advance is gated by isAdvanceDue, and AdvanceCharges processes flat-fee charges without prior settlement-mode filtering. Standard-invoice processors are set to noop where the state machine owns lifecycle.
Tests and helpers
openmeter/billing/charges/service/handlers_test.go, openmeter/billing/charges/service/invoicable_test.go, test/credits/credit_then_invoice_test.go, test/credits/sanity_test.go
Adds countedCreditAllocationCallback helper, refactors tests to drive line engine, updates wording to "promotional credits", adds many flat-fee credit-then-invoice tests and helpers, and updates sanity expectations for awaiting-payment-settlement status.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 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 directly and accurately summarizes the main feature addition: implementing a credit-then-invoice lifecycle for flat-fee charges, which is the primary objective of this extensive changeset.
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/move-flat-fee-to-state-machine

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.

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

🧹 Nitpick comments (7)
openmeter/billing/charges/flatfee/service/creditheninvoice.go (1)

162-166: 💤 Low value

Heads up: StartRealization mutates the caller's input.Line in place.

Setting input.Line.CreditsApplied here mutates the standard line that the caller passed in (since input.Line is a pointer). It happens to be what LineEngine.OnStandardInvoiceCreated wants (the returned chargesByID and later persistDetailedLines flow depend on it), but this side effect on a method parameter is easy to miss for future callers. A short comment on StartRealization would help signal the contract.

🤖 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/creditheninvoice.go` around lines
162 - 166, StartRealization mutates the caller's input.Line (a pointer) by
setting input.Line.CreditsApplied; add a clear doc comment to the
StartRealization function explaining this side-effect and that callers (e.g.,
LineEngine.OnStandardInvoiceCreated, chargesByID flow, persistDetailedLines)
rely on input.Line being updated, so future callers know the contract;
optionally note that callers should pass a copy if they want to avoid mutation.
openmeter/billing/charges/flatfee/service/lineengine.go (4)

149-196: 💤 Low value

OnMutableStandardLinesDeleted deletion guard is one-shot — second offending line is silently skipped.

The dedup via cleanedChargeIDs runs after the accrued-usage / payment guards, which is intentional for the credit-correction work. But notice: the guards at lines 170-176 only check on the first stdLine for a given charge.ID. If the same charge has multiple deleted standard lines, the second one short-circuits at the cleanedChargeIDs check above… which is fine. The subtler issue: if input.Lines is ordered such that a "safe" line for charge X appears before another line where charge X would have accrued usage/payment (shouldn't normally happen since the guards are per-charge), the guard still fires correctly because we look up charge from chargesByID, not from the line. So this is actually OK 👍 — but a short comment explaining "guards/cleanup are per-charge, not per-line" would make the intent obvious.

Also, consider extracting the loop body into a helper (cleanChargeForDeletedLine) — the 8-step procedure inside the loop is getting hefty.

🤖 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/lineengine.go` around lines 149 -
196, The loop in OnMutableStandardLinesDeleted currently applies per-charge
guards and cleanup but lacks an explanatory comment and is large; add a short
comment above the loop clarifying “guards/cleanup are per-charge (deduped via
cleanedChargeIDs) not per-line” and refactor the loop body into a helper
function (e.g., cleanChargeForDeletedLine(ctx, stdLine, charge, e) or a method
on LineEngine) that performs: validation against CurrentRun
AccruedUsage/Payment, obtaining currency calculator, calling
e.service.realizations.CorrectAllCredits, and
e.service.adapter.UpsertDetailedLines, while preserving the cleanedChargeIDs
dedup logic and existing error messages.

244-251: 💤 Low value

The type-assertion safety net is good — small nit on the error message.

The *CreditThenInvoiceStateMachine cast is properly guarded with !ok, which is great. One small thing: newStateMachineForStandardLine is the only path that builds the machine after asserting credit_then_invoice settlement mode (line 227), so the cast should always succeed in practice. If it ever fails, we've got a wiring bug — consider making the message scream a bit louder (e.g. "BUG: ...") so it's easy to spot in logs.

🤖 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/lineengine.go` around lines 244 -
251, The type-assertion failure in newStateMachineForStandardLine currently
returns a normal error when stateMachine cannot be cast to
*CreditThenInvoiceStateMachine; change the error text to make this a
loud/localized wiring bug by prefixing the message with "BUG:" (e.g. "BUG: flat
fee charge[%s]: expected credit_then_invoice state machine, got %T") so that any
unexpected cast failure in newStateMachineForStandardLine /
CreditThenInvoiceStateMachine is obvious in logs and easier to triage; keep the
same check (ok) and return style but update the error string to include the
"BUG:" marker and include charge.ID and the actual type as currently done.

70-92: ⚖️ Poor tradeoff

N+1 charge lookups per standard-line lifecycle event.

newStateMachineForStandardLine runs a GetByID per stdLine, and the same pattern is repeated across OnCollectionCompleted, OnInvoiceIssued, OnPaymentAuthorized, and OnPaymentSettled. For invoices with a single flat-fee line this is a non-issue, but if subscriptions ever batch many flat-fee charges onto one invoice it'll multiply DB roundtrips on every lifecycle hook. A batch fetch (à la getChargesForMutableStandardLineDelete using GetByIDs) followed by per-charge state machine construction would scale better. Not blocking — just flagging now while the engine wiring is fresh.

🤖 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/lineengine.go` around lines 70 -
92, The code performs an N+1 DB lookup by calling newStateMachineForStandardLine
(which internally does GetByID) for each stdLine in handlers like
OnCollectionCompleted, OnInvoiceIssued, OnPaymentAuthorized and
OnPaymentSettled; fix this by batching the charge fetch first (use the existing
pattern getChargesForMutableStandardLineDelete/GetByIDs to retrieve all charge
records for the set of stdLine IDs) and then construct each state machine from
those in-memory charge objects instead of calling newStateMachineForStandardLine
per line; adapt the callers to accept a map of pre-fetched charges (or add a
helper newStateMachineFromCharge) and then reuse stateMachine
(AdvanceUntilStateStable, FireAndActivate, GetCharge) logic unchanged.

191-191: 💤 Low value

Passing nil to UpsertDetailedLines to delete all lines could use a comment for clarity.

The nil parameter does delete all lines (the implementation loops over the lines to build a keep-list, so empty list = delete everything), but it's not obvious from reading the call site. A one-line comment here explaining the convention would help, or if the adapter grows an explicit DeleteDetailedLines(ctx, chargeID) method down the road, that'd be even clearer. Low priority, but worth flagging.

🤖 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/lineengine.go` at line 191, The
call in lineengine.go that does e.service.adapter.UpsertDetailedLines(ctx,
charge.GetChargeID(), nil) relies on a convention where passing nil removes all
lines; add a one-line inline comment at that call site clarifying that nil means
"delete all detailed lines" (or alternatively add/use a dedicated adapter method
like DeleteDetailedLines(ctx, chargeID) and call that instead) so readers of
UpsertDetailedLines and callers like e.service.adapter can immediately
understand the intent.
test/credits/credit_then_invoice_test.go (2)

1900-1991: 💤 Low value

Tiny gap: this test doesn't run charge advancement before deleting.

TestFlatFeeCreditThenInvoiceDeleteActiveChargeBeforeStandardInvoiceDeletesGatheringLine advances the charge into StatusActive (line 1969) and then deletes — perfect. One small thing that would harden the test: it doesn't assert the gathering line's ChargeID matches flatFeeChargeID.ID like sibling tests do (e.g. line 601). Easy add if you want symmetry across the suite.

🤖 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 `@test/credits/credit_then_invoice_test.go` around lines 1900 - 1991, Add an
assertion that the returned gathering line(s) belong to the deleted charge by
checking the gathering line's ChargeID equals flatFeeChargeID.ID; specifically,
in
TestFlatFeeCreditThenInvoiceDeleteActiveChargeBeforeStandardInvoiceDeletesGatheringLine
after calling s.mustGatheringLinesForCharge(...) (the activeLines/allLines
checks) add an assert like s.Equal(flatFeeChargeID.ID, allLines[0].ChargeID) (or
similar for activeLines when non-empty) so the gathering line is verified to
reference the expected flatFeeChargeID from the test.

961-1092: 💤 Low value

Mixing t.Run and s.Run for subtests inside the same testify suite.

The earlier flat-fee tests (lines 526, 644, 747, 1094, 1264) consistently use s.Run("…", func() { … }), but these later ones flip to t.Run("…", func(t *testing.T) { … }). Both work, but they aren't equivalent inside a testify suite:

  • s.Run keeps s.T() pointing at the subtest's *testing.T, so suite helpers using s.T().Helper() and s.T().Context() line up with the right t.
  • Plain t.Run shadows the outer t inside the closure, but s.T() still returns the parent's T, which can mismatch ownership/cancellation if s.T().Context() is used inside.

For consistency (and to keep t.Context() semantics matching the active subtest), it'd be nice to standardize on s.Run here. Not a defect — just a friendlier-to-read mid-file inconsistency.

Also applies to: 1300-1495, 1497-1610, 1612-1754, 1756-1898, 1900-1991

🤖 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 `@test/credits/credit_then_invoice_test.go` around lines 961 - 1092, Several
subtests in this suite use t.Run with a local *testing.T (e.g. the subtests
titled "given a partially credited flat fee charge", "when the pending line is
collected into a mutable draft invoice", "when the charge delete patch removes
the mutable standard line", and "then deleting the mutable line restores the
initial ledger state") which breaks testify suite semantics; replace each
t.Run("...", func(t *testing.T) { ... }) with s.Run("...", func() { ... }) so
the subtest uses the suite's T, remove the inner t parameter from the closure,
and update any inside references to t to use s.T() or suite helpers (or remove
if unused) to keep context/cancellation and helper behavior consistent with
other tests like the earlier flat-fee cases.
🤖 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/flatfee/service/creditheninvoice.go`:
- Around line 188-202: The AreAllPaymentsSettled method on
CreditThenInvoiceStateMachine currently returns true when
Charge.Realizations.CurrentRun is nil, which can prematurely advance a charge to
final state; update the logic so a nil CurrentRun does not imply all payments
are settled—either return false or surface/log an error instead of true. Locate
the AreAllPaymentsSettled function and change the early-return for
s.Charge.Realizations.CurrentRun == nil to a safer behavior (e.g., return false
or call a logging/guard routine), ensuring the check aligns with state
transitions such as StatusActiveAwaitingPaymentSettlement and still returns
payment.StatusSettled only when a non-nil CurrentRun.Payment is settled.
- Line 183: accrueInvoiceUsage currently sets s.Charge.State.AdvanceAfter = nil
in-memory but does not persist it, creating a race if the charge is refetched
before the state machine later runs OnActive(s.ClearAdvanceAfter) during the
StatusFinal transition; update accrueInvoiceUsage to persist the cleared value
immediately (e.g., call the adapter method that writes charge state) after
setting s.Charge.State.AdvanceAfter = nil, or if you intentionally defer
persistence, add a clear comment in accrueInvoiceUsage referencing the
OnActive(s.ClearAdvanceAfter) hook and StatusFinal to explain the guaranteed
state-machine flow; locate the change around the accrueInvoiceUsage function and
the s.Charge.State.AdvanceAfter assignment and either add the adapter
persistence call or the explanatory comment accordingly.

---

Nitpick comments:
In `@openmeter/billing/charges/flatfee/service/creditheninvoice.go`:
- Around line 162-166: StartRealization mutates the caller's input.Line (a
pointer) by setting input.Line.CreditsApplied; add a clear doc comment to the
StartRealization function explaining this side-effect and that callers (e.g.,
LineEngine.OnStandardInvoiceCreated, chargesByID flow, persistDetailedLines)
rely on input.Line being updated, so future callers know the contract;
optionally note that callers should pass a copy if they want to avoid mutation.

In `@openmeter/billing/charges/flatfee/service/lineengine.go`:
- Around line 149-196: The loop in OnMutableStandardLinesDeleted currently
applies per-charge guards and cleanup but lacks an explanatory comment and is
large; add a short comment above the loop clarifying “guards/cleanup are
per-charge (deduped via cleanedChargeIDs) not per-line” and refactor the loop
body into a helper function (e.g., cleanChargeForDeletedLine(ctx, stdLine,
charge, e) or a method on LineEngine) that performs: validation against
CurrentRun AccruedUsage/Payment, obtaining currency calculator, calling
e.service.realizations.CorrectAllCredits, and
e.service.adapter.UpsertDetailedLines, while preserving the cleanedChargeIDs
dedup logic and existing error messages.
- Around line 244-251: The type-assertion failure in
newStateMachineForStandardLine currently returns a normal error when
stateMachine cannot be cast to *CreditThenInvoiceStateMachine; change the error
text to make this a loud/localized wiring bug by prefixing the message with
"BUG:" (e.g. "BUG: flat fee charge[%s]: expected credit_then_invoice state
machine, got %T") so that any unexpected cast failure in
newStateMachineForStandardLine / CreditThenInvoiceStateMachine is obvious in
logs and easier to triage; keep the same check (ok) and return style but update
the error string to include the "BUG:" marker and include charge.ID and the
actual type as currently done.
- Around line 70-92: The code performs an N+1 DB lookup by calling
newStateMachineForStandardLine (which internally does GetByID) for each stdLine
in handlers like OnCollectionCompleted, OnInvoiceIssued, OnPaymentAuthorized and
OnPaymentSettled; fix this by batching the charge fetch first (use the existing
pattern getChargesForMutableStandardLineDelete/GetByIDs to retrieve all charge
records for the set of stdLine IDs) and then construct each state machine from
those in-memory charge objects instead of calling newStateMachineForStandardLine
per line; adapt the callers to accept a map of pre-fetched charges (or add a
helper newStateMachineFromCharge) and then reuse stateMachine
(AdvanceUntilStateStable, FireAndActivate, GetCharge) logic unchanged.
- Line 191: The call in lineengine.go that does
e.service.adapter.UpsertDetailedLines(ctx, charge.GetChargeID(), nil) relies on
a convention where passing nil removes all lines; add a one-line inline comment
at that call site clarifying that nil means "delete all detailed lines" (or
alternatively add/use a dedicated adapter method like DeleteDetailedLines(ctx,
chargeID) and call that instead) so readers of UpsertDetailedLines and callers
like e.service.adapter can immediately understand the intent.

In `@test/credits/credit_then_invoice_test.go`:
- Around line 1900-1991: Add an assertion that the returned gathering line(s)
belong to the deleted charge by checking the gathering line's ChargeID equals
flatFeeChargeID.ID; specifically, in
TestFlatFeeCreditThenInvoiceDeleteActiveChargeBeforeStandardInvoiceDeletesGatheringLine
after calling s.mustGatheringLinesForCharge(...) (the activeLines/allLines
checks) add an assert like s.Equal(flatFeeChargeID.ID, allLines[0].ChargeID) (or
similar for activeLines when non-empty) so the gathering line is verified to
reference the expected flatFeeChargeID from the test.
- Around line 961-1092: Several subtests in this suite use t.Run with a local
*testing.T (e.g. the subtests titled "given a partially credited flat fee
charge", "when the pending line is collected into a mutable draft invoice",
"when the charge delete patch removes the mutable standard line", and "then
deleting the mutable line restores the initial ledger state") which breaks
testify suite semantics; replace each t.Run("...", func(t *testing.T) { ... })
with s.Run("...", func() { ... }) so the subtest uses the suite's T, remove the
inner t parameter from the closure, and update any inside references to t to use
s.T() or suite helpers (or remove if unused) to keep context/cancellation and
helper behavior consistent with other tests like the earlier flat-fee cases.
🪄 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: 9ee5d94c-7561-4fd8-b89d-202b2d7841cc

📥 Commits

Reviewing files that changed from the base of the PR and between 47d4dc2 and 448b47a.

⛔ Files ignored due to path filters (2)
  • openmeter/ent/db/chargeflatfee/chargeflatfee.go is excluded by !**/ent/db/**
  • openmeter/ent/db/migrate/schema.go is excluded by !**/ent/db/**
📒 Files selected for processing (22)
  • .agents/skills/charges/SKILL.md
  • openmeter/billing/charges/flatfee/adapter.go
  • openmeter/billing/charges/flatfee/adapter/charge.go
  • openmeter/billing/charges/flatfee/adapter/realizationrun.go
  • openmeter/billing/charges/flatfee/handler.go
  • openmeter/billing/charges/flatfee/service.go
  • openmeter/billing/charges/flatfee/service/create.go
  • openmeter/billing/charges/flatfee/service/creditheninvoice.go
  • openmeter/billing/charges/flatfee/service/creditsonly.go
  • openmeter/billing/charges/flatfee/service/invoice.go
  • openmeter/billing/charges/flatfee/service/lineengine.go
  • openmeter/billing/charges/flatfee/service/payment.go
  • openmeter/billing/charges/flatfee/service/statemachine.go
  • openmeter/billing/charges/flatfee/service/triggers.go
  • openmeter/billing/charges/flatfee/statemachine.go
  • openmeter/billing/charges/service/advance.go
  • openmeter/billing/charges/service/create.go
  • openmeter/billing/charges/service/handlers_test.go
  • openmeter/billing/charges/service/invoicable_test.go
  • openmeter/billing/charges/service/invoice.go
  • test/credits/credit_then_invoice_test.go
  • test/credits/sanity_test.go
💤 Files with no reviewable changes (2)
  • openmeter/billing/charges/service/advance.go
  • openmeter/billing/charges/flatfee/service.go

Comment thread openmeter/billing/charges/flatfee/service/creditheninvoice.go
Comment thread openmeter/billing/charges/flatfee/service/creditheninvoice.go
@turip turip added area/billing release-note/feature Release note: Exciting New Features labels May 12, 2026
@turip turip changed the title Feat/move flat fee to state machine Add flat fee credit-then-invoice lifecycle May 12, 2026
@turip turip changed the title Add flat fee credit-then-invoice lifecycle feat: add flat fee credit-then-invoice lifecycle May 12, 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

🤖 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/advance_test.go`:
- Line 38: Replace usages of context.Background() in the tests in
advance_test.go with the test-bound context from the suite's testing.T (e.g.,
use ctx := s.T().Context() or directly s.T().Context()) so the context lifecycle
is tied to the test harness; update each test that currently calls
context.Background() (the three occurrences in this file) to use s.T().Context()
instead, ensuring any variables named ctx are assigned from s.T().Context() and
passed through the same functions as before.
🪄 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: 0668adda-aa2f-40ab-aea8-a036db841e01

📥 Commits

Reviewing files that changed from the base of the PR and between 51a6ecc and 0abcf43.

📒 Files selected for processing (3)
  • openmeter/billing/charges/flatfee/service/triggers.go
  • openmeter/billing/charges/service/advance_test.go
  • openmeter/billing/charges/service/invoicable_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • openmeter/billing/charges/flatfee/service/triggers.go

Comment thread openmeter/billing/charges/service/advance_test.go Outdated
@turip turip force-pushed the feat/move-flat-fee-to-state-machine branch from 0abcf43 to 1de7f56 Compare May 13, 2026 07:46
Comment on lines +57 to +59
if len(split) == 0 {
return meta.ChargeStatusCreated, fmt.Errorf("invalid status: %s", s)
}
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.

status Validate is already called, so this is a noop

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but I would keep it if you don't mind. It's just there so that I don't have to think about what Validate does.

Comment thread openmeter/billing/charges/flatfee/service/creditheninvoice.go Outdated
@turip turip requested a review from a team May 13, 2026 09:10
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 (2)
openmeter/billing/charges/flatfee/service/statemachine.go (2)

95-108: ⚡ Quick win

Remove unused context parameters.

All three of these helper methods accept context.Context but never use it. According to the coding guidelines: "Propagate caller's context through the full call path or remove unused context parameters."

Since these methods just perform simple field mutations (meta.NormalizeTimestamp doesn't need context), the context parameters should be removed unless they're required by an interface contract.

Also worth noting: all three methods return error but always return nil. If these aren't implementing a specific interface, consider returning void to simplify the signatures.

♻️ Suggested simplification
-func (s *stateMachine) AdvanceAfterServicePeriodFrom(ctx context.Context) error {
+func (s *stateMachine) AdvanceAfterServicePeriodFrom() {
 	s.Charge.State.AdvanceAfter = lo.ToPtr(meta.NormalizeTimestamp(s.Charge.Intent.ServicePeriod.From))
-	return nil
 }
 
-func (s *stateMachine) AdvanceAfterServicePeriodTo(ctx context.Context) error {
+func (s *stateMachine) AdvanceAfterServicePeriodTo() {
 	s.Charge.State.AdvanceAfter = lo.ToPtr(meta.NormalizeTimestamp(s.Charge.Intent.ServicePeriod.To))
-	return nil
 }
 
-func (s *stateMachine) ClearAdvanceAfter(ctx context.Context) error {
+func (s *stateMachine) ClearAdvanceAfter() {
 	s.Charge.State.AdvanceAfter = nil
-	return nil
 }

As per coding guidelines: "Propagate caller's context through the full call path or remove unused context parameters."

🤖 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/statemachine.go` around lines 95 -
108, The three methods AdvanceAfterServicePeriodFrom,
AdvanceAfterServicePeriodTo and ClearAdvanceAfter accept a context.Context and
return error but neither use ctx nor ever return non-nil; if they are not
required by an interface, remove the context parameter and change the return
type to void (no error), update bodies to just set s.Charge.State.AdvanceAfter
via meta.NormalizeTimestamp(s.Charge.Intent.ServicePeriod.From/To) or nil for
ClearAdvanceAfter; if they are required by an interface, keep the signatures but
add a comment explaining why ctx/error are unused or consider returning early
with nil as currently implemented.

91-93: 💤 Low value

Consider adding a context parameter for consistency.

This method doesn't accept context.Context, while the three helper methods below do. While clock.Now() and the comparison don't strictly need context, having a consistent signature across all these service-period helpers would make the API cleaner and more predictable.

🤖 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/statemachine.go` around lines 91 -
93, Update the IsInsideServicePeriod method to accept a context.Context for API
consistency with the three helper methods below: change the signature of
stateMachine.IsInsideServicePeriod to take (ctx context.Context) and update all
call sites and tests to pass the incoming context; keep the implementation using
clock.Now() unchanged (you don't need to use ctx inside), but add the context
import if missing so the package compiles and the method signature matches the
other helpers.
🤖 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/flatfee/service/statemachine.go`:
- Around line 95-108: The three methods AdvanceAfterServicePeriodFrom,
AdvanceAfterServicePeriodTo and ClearAdvanceAfter accept a context.Context and
return error but neither use ctx nor ever return non-nil; if they are not
required by an interface, remove the context parameter and change the return
type to void (no error), update bodies to just set s.Charge.State.AdvanceAfter
via meta.NormalizeTimestamp(s.Charge.Intent.ServicePeriod.From/To) or nil for
ClearAdvanceAfter; if they are required by an interface, keep the signatures but
add a comment explaining why ctx/error are unused or consider returning early
with nil as currently implemented.
- Around line 91-93: Update the IsInsideServicePeriod method to accept a
context.Context for API consistency with the three helper methods below: change
the signature of stateMachine.IsInsideServicePeriod to take (ctx
context.Context) and update all call sites and tests to pass the incoming
context; keep the implementation using clock.Now() unchanged (you don't need to
use ctx inside), but add the context import if missing so the package compiles
and the method signature matches the other helpers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b6802250-932c-415e-af12-699972616342

📥 Commits

Reviewing files that changed from the base of the PR and between c78a0db and 8a7c222.

📒 Files selected for processing (2)
  • openmeter/billing/charges/flatfee/service/creditheninvoice.go
  • openmeter/billing/charges/flatfee/service/statemachine.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • openmeter/billing/charges/flatfee/service/creditheninvoice.go

@turip turip requested a review from tothandras May 13, 2026 09:22
@turip turip merged commit 1057658 into main May 13, 2026
28 checks passed
@turip turip deleted the feat/move-flat-fee-to-state-machine branch May 13, 2026 09:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/billing release-note/feature Release note: Exciting New Features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants