Skip to content

fix: flat fee invoice-at lifecycle#4618

Merged
turip merged 3 commits into
mainfrom
feat/flat-fee-invoice-at-lifecycle
Jun 30, 2026
Merged

fix: flat fee invoice-at lifecycle#4618
turip merged 3 commits into
mainfrom
feat/flat-fee-invoice-at-lifecycle

Conversation

@turip

@turip turip commented Jun 30, 2026

Copy link
Copy Markdown
Member

Summary

Fixes flat-fee lifecycle scheduling when invoice_at and the service period do not start at the same time.

  • credit-then-invoice flat fees become invoiceable based on invoice_at
  • credit-only flat fees move from created to active at invoice_at, then remain open until the flat-fee ledger booked_at
  • customer balance treats flat-fee live impact as started at invoice_at
  • adds regression coverage for the invoice-at-before-service-start balance window

Root Cause

Flat-fee invoice collection was governed by invoice_at, but the charge lifecycle and customer balance gates still assumed service-period start for flat fees. That let invoice collection try to process charge lines while the charge remained created, producing unsupported final_invoice_created transitions.

For credits-only flat fees, there is an additional balance window: allocation is booked at flat-fee booked_at (service_period.from for in-advance, service_period.to for in-arrears), while invoice readiness can be earlier. The charge must remain non-final until booked_at so customer balance can continue projecting the live impact before the ledger booking is visible.

Validation

  • PASS: env POSTGRES_HOST=127.0.0.1 go test -tags=dynamic ./openmeter/ledger/customerbalance
  • PASS: env POSTGRES_HOST=127.0.0.1 go test -tags=dynamic ./openmeter/billing/charges/flatfee/service ./openmeter/billing/charges/service
  • PASS: env POSTGRES_HOST=127.0.0.1 go vet -tags=dynamic ./openmeter/ledger/customerbalance ./openmeter/billing/charges/flatfee/service ./openmeter/billing/charges/service
  • FAIL: make test-nocache currently fails in test/credits because existing flat-fee credit-then-invoice tests still expect service-period-start activation.
  • FAIL: make lint-go-fast could not run in the ambient shell because golangci-lint is not installed there.

Summary by CodeRabbit

  • Bug Fixes

    • Flat-fee charge progression, activation, and customer-balance “has started” logic now consistently follow invoice_at timing (including when invoicing occurs before service start).
    • Credit-then-invoice and credit-only flows were adjusted so state transitions and scheduled advancement use the correct invoice_at (and for later credits-only steps, booked usage timing).
  • Tests

    • Added/updated coverage for cases where invoice_at precedes service start, including verification of Live vs. Settled balance behavior and expected charge advancement.

@turip
turip requested a review from a team as a code owner June 30, 2026 16:18
@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR moves flat-fee lifecycle timing to invoice_at where invoice readiness starts. The main changes are:

  • Flat-fee creation now schedules advancement from invoice_at.
  • Credit-then-invoice flat fees now leave created based on invoice_at.
  • Credit-only flat fees now activate at invoice_at and finalize at booked usage time.
  • Customer balance now includes flat-fee live impact from invoice_at.
  • Tests cover invoice-at-before-service-start lifecycle and balance behavior.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
openmeter/billing/charges/flatfee/service/create.go Initial flat-fee charge advancement now uses invoice_at.
openmeter/billing/charges/flatfee/service/creditheninvoice.go Credit-then-invoice lifecycle gates and recreated charges now schedule from invoice_at.
openmeter/billing/charges/flatfee/service/creditsonly.go Credit-only flat fees now activate at invoice_at and finalize when usage is booked.
openmeter/billing/charges/flatfee/service/statemachine.go Shared invoice-at guard and scheduling helpers were added.
openmeter/ledger/customerbalance/service.go Flat-fee live balance impact now starts at effective invoice_at.
openmeter/billing/charges/service/invoicable_test.go Billing tests now cover flat-fee invoice-at timing before service start.
openmeter/ledger/customerbalance/service_test.go Balance tests now cover flat-fee live impact before service start.
test/credits/credit_then_invoice_test.go Credit integration tests now expect invoice-at based CTI activation.

Reviews (4): Last reviewed commit: "fix(billing): schedule recreated flat fe..." | Re-trigger Greptile

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

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

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: 39b454a9-e479-4792-83ec-defb8efdc073

📥 Commits

Reviewing files that changed from the base of the PR and between 3d37f24 and 1d7428d.

📒 Files selected for processing (3)
  • openmeter/billing/charges/flatfee/service/creditheninvoice.go
  • openmeter/ledger/customerbalance/service_test.go
  • test/credits/credit_then_invoice_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • openmeter/ledger/customerbalance/service_test.go

📝 Walkthrough

Walkthrough

Flat-fee charge timing now uses InvoiceAt for creation and state transitions, with CreditsOnly switching active progression to booked-usage timing. Customer balance start checks were updated, and tests were expanded for invoice-before-service-start and revised balance timing.

Changes

Flat-fee InvoiceAt timing changes

Layer / File(s) Summary
New InvoiceAt predicate and advancement helpers on stateMachine
openmeter/billing/charges/flatfee/service/statemachine.go
Adds IsAfterInvoiceAt, IsAfterInvoiceAtAndZeroAmount, IsAfterInvoiceAtAndNonZeroAmount, and AdvanceAfterInvoiceAt.
Transition predicates updated to InvoiceAt
openmeter/billing/charges/flatfee/service/create.go, openmeter/billing/charges/flatfee/service/creditheninvoice.go, openmeter/billing/charges/flatfee/service/creditsonly.go
create.go sets InitialAdvanceAfter from InvoiceAt; creditheninvoice.go gates created-state transitions on InvoiceAt; creditsonly.go switches created-state advancement to InvoiceAt and active-state advancement to booked-at timing.
chargeHasStarted uses InvoiceAt
openmeter/ledger/customerbalance/service.go
The flat-fee start-time check now compares against GetEffectiveInvoiceAt().
Tests for InvoiceAt-before-service-start and balance timing
openmeter/billing/charges/service/invoicable_test.go, openmeter/ledger/customerbalance/service_test.go, test/credits/credit_then_invoice_test.go
Lifecycle and balance tests cover invoice-before-service-start behavior, no activation at service-period start, and balance changes at InvoiceAt.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 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 is concise and clearly matches the main change: aligning flat-fee lifecycle behavior around invoice_at.
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.
✨ 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/flat-fee-invoice-at-lifecycle

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.

@turip turip added the release-note/bug-fix Release note: Bug Fixes label Jun 30, 2026
@turip
turip force-pushed the feat/flat-fee-invoice-at-lifecycle branch from 8078b2b to 32e395c Compare June 30, 2026 16:26

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🧹 Nitpick comments (2)
openmeter/billing/charges/service/invoicable_test.go (1)

945-973: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a pre-invoice_at assertion to lock in the new gate.

This only checks the happy path once the clock reaches invoice_at. If pending lines accidentally become invoiceable earlier, this test still passes. A quick InvoicePendingLines assertion before Line 967 expecting no invoices would pin the new timing contract directly. As per path instructions, **/*_test.go: Suggest missing tests only when the PR introduces behavior without meaningful coverage, or when the current tests would pass despite a concrete regression in the changed code.

🤖 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/service/invoicable_test.go` around lines 945 - 973,
The timing gate around invoicing pending flat-fee lines is only covered at
invoice_at, so the test in invoicable_test.go could still pass even if lines
become invoiceable too early. Add an assertion in the same test around
BillingService.InvoicePendingLines that runs before invoiceAt and verifies no
invoices are created, using the existing test setup around clock.FreezeTime,
invoiceAt, and InvoicePendingLines to lock in the pre-invoice_at contract.

Source: Path instructions

openmeter/ledger/customerbalance/service_test.go (1)

370-384: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the balance at invoice_at before advancing the charge.

Right now the key live-balance check happens only after AdvanceCharge. If chargeHasStarted regressed to a status-based check, this test would still pass once the state machine moves the charge to active. A balance assertion right after Line 370 and before Line 371 would pin the invoice_at rule directly. As per path instructions, **/*_test.go: Suggest missing tests only when the PR introduces behavior without meaningful coverage, or when the current tests would pass despite a concrete regression in the changed code.

🤖 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/ledger/customerbalance/service_test.go` around lines 370 - 384, Add
a balance assertion in customerbalance/service_test.go using the existing test
flow around AdvanceCharge: check the live balance immediately after
clock.FreezeTime(invoiceAt) and before calling flatFeeService.AdvanceCharge.
This should use the same requireBalance helper and the current
charge/servicePeriod setup so the test directly verifies the invoice_at rule
even if chargeHasStarted regresses to a status-based check.

Source: Path instructions

🤖 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/invoicable_test.go`:
- Line 967: The test in invoicable_test.go freezes time with
clock.FreezeTime(invoiceAt) but does not pair it with a same-scope cleanup; add
a defer clock.UnFreeze() immediately after the freeze in the same subtest scope.
Keep the cleanup local to the test block around the FreezeTime call so it does
not rely on outer teardown and remains stable if the test structure changes.

In `@openmeter/ledger/customerbalance/service_test.go`:
- Around line 370-403: The test uses multiple clock.FreezeTime calls in separate
phases of the customer balance scenario, but each freeze is not immediately
paired with a same-scope defer clock.UnFreeze(), which makes the cleanup
fragile. Update the test around the AdvanceCharge and requireBalance steps so
each frozen-clock phase has its own local cleanup using clock.UnFreeze(), and
keep the pairing close to the FreezeTime call to preserve isolation even if the
phases are reordered.

---

Nitpick comments:
In `@openmeter/billing/charges/service/invoicable_test.go`:
- Around line 945-973: The timing gate around invoicing pending flat-fee lines
is only covered at invoice_at, so the test in invoicable_test.go could still
pass even if lines become invoiceable too early. Add an assertion in the same
test around BillingService.InvoicePendingLines that runs before invoiceAt and
verifies no invoices are created, using the existing test setup around
clock.FreezeTime, invoiceAt, and InvoicePendingLines to lock in the
pre-invoice_at contract.

In `@openmeter/ledger/customerbalance/service_test.go`:
- Around line 370-384: Add a balance assertion in
customerbalance/service_test.go using the existing test flow around
AdvanceCharge: check the live balance immediately after
clock.FreezeTime(invoiceAt) and before calling flatFeeService.AdvanceCharge.
This should use the same requireBalance helper and the current
charge/servicePeriod setup so the test directly verifies the invoice_at rule
even if chargeHasStarted regresses to a status-based check.
🪄 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: 738b3129-0702-491b-9597-20dd6479ae66

📥 Commits

Reviewing files that changed from the base of the PR and between 8b9649e and 8078b2b.

📒 Files selected for processing (7)
  • 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/statemachine.go
  • openmeter/billing/charges/service/invoicable_test.go
  • openmeter/ledger/customerbalance/service.go
  • openmeter/ledger/customerbalance/service_test.go

Comment thread openmeter/billing/charges/service/invoicable_test.go
Comment thread openmeter/ledger/customerbalance/service_test.go

@coderabbitai coderabbitai Bot left a comment

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.

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 `@test/credits/credit_then_invoice_test.go`:
- Line 2135: The `clock.FreezeTime(...)` usage in `credit_then_invoice_test.go`
is leaking shared frozen state across subtests. Update each affected subtest in
the `Test...` flow to freeze time within that subtest’s own scope and
immediately pair it with `defer clock.UnFreeze()` in the same block. Use the
local subtest bodies around the `clock.FreezeTime(servicePeriod.To)` calls so
each case manages its own clock state independently.
🪄 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: 9961247e-dbbf-4340-9d05-caa6af004258

📥 Commits

Reviewing files that changed from the base of the PR and between 8078b2b and 32e395c.

📒 Files selected for processing (8)
  • 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/statemachine.go
  • openmeter/billing/charges/service/invoicable_test.go
  • openmeter/ledger/customerbalance/service.go
  • openmeter/ledger/customerbalance/service_test.go
  • test/credits/credit_then_invoice_test.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • openmeter/billing/charges/flatfee/service/create.go
  • openmeter/ledger/customerbalance/service.go
  • openmeter/billing/charges/flatfee/service/creditheninvoice.go
  • openmeter/ledger/customerbalance/service_test.go
  • openmeter/billing/charges/flatfee/service/statemachine.go
  • openmeter/billing/charges/service/invoicable_test.go
  • openmeter/billing/charges/flatfee/service/creditsonly.go

Comment thread test/credits/credit_then_invoice_test.go
@turip turip changed the title Fix flat fee invoice-at lifecycle fix: flat fee invoice-at lifecycle Jun 30, 2026
@turip
turip force-pushed the feat/flat-fee-invoice-at-lifecycle branch from f97ce33 to 3d37f24 Compare June 30, 2026 16:41
Comment thread openmeter/billing/charges/flatfee/service/creditheninvoice.go
GAlexIHU
GAlexIHU previously approved these changes Jun 30, 2026
@turip
turip enabled auto-merge (squash) June 30, 2026 17:20
@turip
turip merged commit 6481a29 into main Jun 30, 2026
26 checks passed
@turip
turip deleted the feat/flat-fee-invoice-at-lifecycle branch June 30, 2026 17:20
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.

3 participants