Skip to content

feat(backend): implement invoice settlement mode for credits#3976

Open
mark-vass-konghq wants to merge 1 commit intomainfrom
feat/invoice-settlement-mode-for-credits
Open

feat(backend): implement invoice settlement mode for credits#3976
mark-vass-konghq wants to merge 1 commit intomainfrom
feat/invoice-settlement-mode-for-credits

Conversation

@mark-vass-konghq
Copy link
Contributor

@mark-vass-konghq mark-vass-konghq commented Mar 19, 2026

First implementation for invoice settlement mode for credits

Summary by CodeRabbit

  • New Features

    • Full invoice settlement support for credit purchases: create/update invoiced payments, link invoices to charges, and deferred invoice-line handling post‑creation.
    • Credit-purchase creation now returns an optional gathering line for subsequent invoice creation.
    • Credit-purchase charges are processed across invoice lifecycle events (authorized → settled).
  • Bug Fixes

    • Improved loading/validation and idempotency checks for invoice settlement state.
  • Tests

    • Re-enabled and expanded tests covering end-to-end invoice credit‑purchase workflows.

@mark-vass-konghq mark-vass-konghq self-assigned this Mar 19, 2026
@mark-vass-konghq mark-vass-konghq requested a review from a team as a code owner March 19, 2026 19:27
@mark-vass-konghq mark-vass-konghq added the release-note/feature Release note: Exciting New Features label Mar 19, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 19, 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

Adds full invoiced-payment support for credit-purchase charges: adapter methods for invoiced payments, DB mapping and validation for InvoiceSettlement, service-side lifecycle (creation, link, authorize, settle), changed charge creation to emit optional gathering lines for deferred invoicing, and invoice integration updates.

Changes

Cohort / File(s) Summary
Adapter & Payment persistence
openmeter/billing/charges/creditpurchase/adapter.go, openmeter/billing/charges/creditpurchase/adapter/payment.go
Extended Adapter with CreateInvoicedPayment/UpdateInvoicedPayment; implemented transactional create/update flows for ChargeCreditPurchaseInvoicedPayment with validation and namespace scoping.
Mapper & Charge loading
openmeter/billing/charges/creditpurchase/adapter/mapper.go, openmeter/billing/charges/creditpurchase/adapter/charge.go
Mapper now loads InvoicedPayment edge when realizations expanded and maps it into State.InvoiceSettlement; UpdateCharge also populates State.InvoiceSettlement from DB state.
Domain types & validation
openmeter/billing/charges/creditpurchase/chargecreditpurchase.go, openmeter/billing/charges/creditpurchase/service.go
State validation now validates InvoiceSettlement when present; service interface extended with InvoicePaymentLifecycle; Create returns a richer ChargeWithGatheringLine.
Credit-purchase service flows
openmeter/billing/charges/creditpurchase/service/create.go, openmeter/billing/charges/creditpurchase/service/invoice.go
Create now builds optional gathering line for invoice-settled purchases and emits pending work; new lifecycle handlers implement linking invoiced payments, authorization and settlement handling with idempotency and timed-group refs.
Orchestration & post-create processing
openmeter/billing/charges/service/create.go, openmeter/billing/charges/service/invoice.go
Top-level Create is refactored to collect pending invoice-credit purchases, run post-transaction processing to create invoice lines and link invoiced payments, and dispatch invoice-status events to credit-purchase handlers.
Invoice / gathering integration
openmeter/billing/charges/service/invoicependinglines.go, openmeter/billing/stdinvoice.go, openmeter/billing/service/gatheringinvoicependinglines.go
Passed optional invoice Description through invoice creation, added credit-purchase handling in allocation (no-op), and threaded Description into adapter invoice input.
Tests
openmeter/billing/charges/service/creditpurchase_test.go
Unskipped and expanded TestStandardInvoiceCreditPurchase with concrete assertions across initiated → authorized → settled lifecycle and invoice/transaction-group checks.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Service as ChargeService
    participant CP as CreditPurchaseService
    participant Adapter
    participant Handler
    participant DB as Database

    Client->>Service: Create(intent with Invoice settlement)
    Service->>CP: Create(ctx, intent)
    CP->>Adapter: CreateCharge(...)
    Adapter->>DB: Insert charge
    Adapter-->>CP: Return charge
    CP->>Handler: OnCreditPurchaseInitiated()
    Handler-->>CP: ledger timed-group ref
    CP->>Adapter: UpdateCharge(charge with CreditGrantRealization)
    Adapter->>DB: Update charge
    CP-->>Service: Return ChargeWithGatheringLine (pending)
    Note over Service,CP: Post-transaction
    Service->>CP: LinkInvoicedPayment(ctx, charge, invoiceLine)
    CP->>Adapter: CreateInvoicedPayment(chargeID, paymentCreate)
    Adapter->>DB: Insert invoiced payment
    Adapter-->>CP: Return invoiced payment
    CP->>Adapter: UpdateCharge(charge with InvoiceSettlement)
    Adapter->>DB: Update charge
    Adapter-->>CP: Return updated charge
Loading
sequenceDiagram
    participant Billing
    participant Service as ChargeService
    participant CP as CreditPurchaseService
    participant Adapter
    participant Handler
    participant DB as Database

    Billing->>Service: Invoice status change (Authorized)
    Service->>CP: HandleInvoicePaymentAuthorized(ctx, charge, lineWithHeader)
    CP->>Adapter: Ensure/Create or Update invoiced payment (Authorized)
    Adapter->>DB: Upsert payment authorized info
    Adapter-->>CP: Return payment
    CP->>Handler: OnCreditPurchasePaymentAuthorized(...)
    Handler-->>CP: ack
    CP->>Adapter: UpdateCharge(charge with updated InvoiceSettlement)
    Adapter->>DB: Update charge

    Billing->>Service: Invoice status change (Paid)
    Service->>CP: HandleInvoicePaymentSettled(ctx, charge, lineWithHeader)
    CP->>Handler: OnCreditPurchasePaymentSettled(...)
    Handler-->>CP: ack
    CP->>Adapter: UpdateInvoicedPayment(payment with Settled)
    Adapter->>DB: Update payment
    CP->>Adapter: UpdateCharge(charge status Final)
    Adapter->>DB: Update charge
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

area/billing

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(backend): implement invoice settlement mode for credits' clearly and concisely describes the main change—adding invoice settlement functionality for credit purchases throughout the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/invoice-settlement-mode-for-credits

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.

@mark-vass-konghq
Copy link
Contributor Author

@CodeRabbit review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 19, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
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: 6

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

363-565: Could we add a case for the deferred-invoicing branch too?

This test covers the happy path where the gathering line is turned into a standard invoice during Create(). The new flow in handleInvoiceCreditPurchasePostCreate() also has a createdLine.InvoiceAt.After(clock.Now()) branch, and that’s the lifecycle edge most likely to regress because InvoiceSettlement stays unset until later.

As per coding guidelines **/*_test.go: Make sure the tests are comprehensive and cover the changes. Keep a strong focus on unit tests and in-code integration tests.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/service/creditpurchase_test.go` around lines 363 -
565, Add a new sub-test to TestStandardInvoiceCreditPurchase that exercises the
deferred-invoicing branch (createdLine.InvoiceAt.After(clock.Now())) in
handleInvoiceCreditPurchasePostCreate: create a credit purchase intent whose
invoice date is in the future (so InvoiceSettlement remains nil at Create()),
assert in the "initiated" callback that charge.State.InvoiceSettlement is nil,
then simulate the passage to invoice creation (invoke whatever path
creates/archives the invoice or call the same CustomInvoicingService trigger
used later) and finally assert that after the invoice is created and approved
the charge's State.InvoiceSettlement is set and populated (check InvoiceID,
Authorized/Settled group IDs and final charge status). Reference
TestStandardInvoiceCreditPurchase, handleInvoiceCreditPurchasePostCreate,
createdLine.InvoiceAt.After(clock.Now()), and InvoiceSettlement to locate where
to add and what to assert.
🤖 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/creditpurchase/chargecreditpurchase.go`:
- Around line 99-123: The validateOptionalValidator function currently checks v
== nil but fails to handle typed-nil values stored in the validator interface
(causing panics when calling v.Validate()); update validateOptionalValidator to
detect an underlying nil pointer before calling Validate by either using a type
switch on validator to check each pointer concrete type for nil or by using
reflection (reflect.ValueOf(v).Kind()==Ptr && reflect.ValueOf(v).IsNil()) to
return nil early; ensure you still call v.Validate() only when the underlying
value is non-nil so functions like validateOptionalValidator,
validator.Validate, and callers (e.g., CreditGrantRealization,
ExternalPaymentSettlement, InvoiceSettlement) no longer panic.

In `@openmeter/billing/charges/creditpurchase/service/invoice.go`:
- Around line 49-61: Currently LinkInvoicedPayment seeds the
newPaymentSettlement (payment.InvoicedCreate) with Authorized and
StatusAuthorized using charge.State.CreditGrantRealization which makes the
settlement appear already authorized; change this to create the InvoicedCreate
without setting Authorized and leave Status as a pre-authorization state (e.g.,
payment.StatusPending or nil), and move the assignment of Authorized and the
transition to payment.StatusAuthorized into HandleInvoicePaymentAuthorized where
the actual authorization is handled, updating the same fields (Authorized and
Status) on the existing settlement record.

In `@openmeter/billing/charges/service/create.go`:
- Around line 261-297: The code only links invoiced payments when
createdLine.InvoiceAt is past/now; for future-dated lines you must record the
pending linkage so later invoice callbacks can complete it. When
createdLine.InvoiceAt is in the future, set the charge's invoice-settlement
information (e.g., populate charge.State.InvoiceSettlement or the equivalent
field with the pending line ID createdLine.ID and any needed invoice/customer
info) and persist the charge immediately using the same persistence path used
elsewhere in this flow so that later handlers can call
s.creditPurchaseService.LinkInvoicedPayment(charge, ...) and finish linking;
ensure the persistence uses the existing charge save/update method used in this
service so state is durable.
- Around line 160-169: The loop calling handleInvoiceCreditPurchasePostCreate()
can surface a hard error after the charge is already committed; change the flow
so Create() does not return a hard error post-commit: mark pending
invoice-credit purchases durable (e.g., set a processed/pending flag tied to the
charge ID) inside the main transaction, then perform
handleInvoiceCreditPurchasePostCreate() as a best-effort background/async step
(or catch errors, log them, and attach a non-fatal warning/state to the returned
charges) instead of returning fmt.Errorf; also make
handleInvoiceCreditPurchasePostCreate() idempotent by deduping on the
charge/transaction ID so retries/background processing cannot double-apply
credits (references: Create, result.pendingInvoiceCreditPurchases,
handleInvoiceCreditPurchasePostCreate, out, charges.NewCharge).
- Around line 23-26: The file fails the repository's Go formatting/import
ordering (gci) check for the createResult declaration; run gofmt and the
project's import sorter (gci or goimports as configured) on the file containing
the createResult struct so the struct definition (type createResult and its
fields charges.Charges and pendingInvoiceCreditPurchases of
charges.WithIndex[*creditPurchaseWithPendingGatheringLine]) is formatted and
imports reordered to match repo style, then stage the resulting changes.

In `@openmeter/billing/charges/service/invoice.go`:
- Around line 33-47: The credit-purchase authorization can be executed twice
because this file routes credit purchases to
creditPurchaseService.HandleInvoicePaymentAuthorized on
StandardInvoiceStatusPaymentProcessingPending while the credit-purchase service
already links the settlement in payment.StatusAuthorized; to fix, either stop
routing here (make the creditPurchase entry a no-op like in the Issued branch)
or make the called handler idempotent by short-circuiting when the settlement is
already authorized — implement the chosen fix by updating the processorByType
mapping in handleChargeEvent (the creditPurchase function assigned here) or by
adding an early return in creditpurchase.Service.HandleInvoicePaymentAuthorized
that checks the settlement/payment status and returns nil if already authorized.

---

Nitpick comments:
In `@openmeter/billing/charges/service/creditpurchase_test.go`:
- Around line 363-565: Add a new sub-test to TestStandardInvoiceCreditPurchase
that exercises the deferred-invoicing branch
(createdLine.InvoiceAt.After(clock.Now())) in
handleInvoiceCreditPurchasePostCreate: create a credit purchase intent whose
invoice date is in the future (so InvoiceSettlement remains nil at Create()),
assert in the "initiated" callback that charge.State.InvoiceSettlement is nil,
then simulate the passage to invoice creation (invoke whatever path
creates/archives the invoice or call the same CustomInvoicingService trigger
used later) and finally assert that after the invoice is created and approved
the charge's State.InvoiceSettlement is set and populated (check InvoiceID,
Authorized/Settled group IDs and final charge status). Reference
TestStandardInvoiceCreditPurchase, handleInvoiceCreditPurchasePostCreate,
createdLine.InvoiceAt.After(clock.Now()), and InvoiceSettlement to locate where
to add and what to assert.
🪄 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: cd5a7b49-62cf-4812-a0e6-f047b09bd57a

📥 Commits

Reviewing files that changed from the base of the PR and between 4971c89 and b7ed0eb.

📒 Files selected for processing (12)
  • openmeter/billing/charges/creditpurchase/adapter.go
  • openmeter/billing/charges/creditpurchase/adapter/charge.go
  • openmeter/billing/charges/creditpurchase/adapter/mapper.go
  • openmeter/billing/charges/creditpurchase/adapter/payment.go
  • openmeter/billing/charges/creditpurchase/chargecreditpurchase.go
  • openmeter/billing/charges/creditpurchase/service.go
  • openmeter/billing/charges/creditpurchase/service/create.go
  • openmeter/billing/charges/creditpurchase/service/invoice.go
  • openmeter/billing/charges/service/create.go
  • openmeter/billing/charges/service/creditpurchase_test.go
  • openmeter/billing/charges/service/invoice.go
  • openmeter/billing/charges/service/invoicependinglines.go

Copy link
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 (4)
openmeter/billing/charges/creditpurchase/chargecreditpurchase.go (1)

118-131: Nice fix for the typed-nil interface issue!

This correctly handles the tricky Go semantics where a nil pointer wrapped in an interface isn't == nil. Small optimization: you're calling reflect.ValueOf(v) twice.

♻️ Optional: cache the reflect.Value
 func validateOptionalValidator(v validator, name string) error {
 	// Handle typed-nil values: when a nil pointer is stored in an interface,
 	// the interface itself is not nil, but the underlying value is.
 	if v == nil {
 		return nil
 	}
-	if reflect.ValueOf(v).Kind() == reflect.Ptr && reflect.ValueOf(v).IsNil() {
+	rv := reflect.ValueOf(v)
+	if rv.Kind() == reflect.Ptr && rv.IsNil() {
 		return nil
 	}
 	if err := v.Validate(); err != nil {
 		return fmt.Errorf("%s: %w", name, err)
 	}
 	return nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/creditpurchase/chargecreditpurchase.go` around
lines 118 - 131, In validateOptionalValidator, avoid calling reflect.ValueOf(v)
twice by assigning rv := reflect.ValueOf(v) once and using rv.Kind() and
rv.IsNil() for the nil-pointer checks; keep the same behavior and error wrapping
using v.Validate() and fmt.Errorf with the provided name.
openmeter/billing/charges/creditpurchase/service/invoice.go (1)

86-93: Idempotency check is clever but warrants a comment clarification.

The logic is: if InvoiceSettlement.Authorized exists AND has a different transaction group ID than CreditGrantRealization, then this handler already ran (since LinkInvoicedPayment copies the same ID, but this handler generates a new one). This is subtle - consider adding a brief inline explanation for future readers.

📝 Clearer comment
 	// Idempotency check: if already authorized via invoice approval (not just initial credit grant), skip processing.
-	// LinkInvoicedPayment sets Authorized to CreditGrantRealization, so we check if it's been updated
-	// by this handler (which would have a different transaction group ID).
+	// LinkInvoicedPayment initially copies CreditGrantRealization's TransactionGroupID into Authorized.
+	// This handler generates a NEW TransactionGroupID when processing. So if Authorized.TransactionGroupID
+	// differs from CreditGrantRealization's, this handler already ran and we skip to avoid double-processing.
 	if charge.State.InvoiceSettlement.Authorized != nil &&
 		charge.State.CreditGrantRealization != nil &&
 		charge.State.InvoiceSettlement.Authorized.TransactionGroupID != charge.State.CreditGrantRealization.GroupReference.TransactionGroupID {
 		return nil
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/creditpurchase/service/invoice.go` around lines 86
- 93, Clarify the idempotency check in invoice.go by adding a concise inline
comment next to the conditional that explains the logic: state when
InvoiceSettlement.Authorized exists and its TransactionGroupID differs from
CreditGrantRealization.GroupReference.TransactionGroupID means this handler
already ran (because LinkInvoicedPayment copies the original group ID while this
handler generates a new one), so we should return; reference the symbols
InvoiceSettlement.Authorized, CreditGrantRealization, LinkInvoicedPayment, and
TransactionGroupID in the comment to make the intent obvious to future readers.
openmeter/billing/charges/service/create.go (2)

160-174: Silent error swallowing is intentional but could use observability.

The comment explains the rationale well - charges are already committed, and this is best-effort. However, silently dropping errors makes debugging tricky in production. Consider logging these failures so operators can spot patterns or manually intervene.

💡 Add structured logging for visibility
 lo.ForEach(result.pendingInvoiceCreditPurchases, func(pending charges.WithIndex[*creditPurchaseWithPendingGatheringLine], _ int) {
 	updatedCharge, err := s.handleInvoiceCreditPurchasePostCreate(ctx, pending.Value)
 	if err != nil {
 		// Skip this charge on error - it has already been committed and can be
 		// processed later. The charge will not have InvoiceSettlement set.
+		s.logger.Warn("post-create handling failed for invoice credit purchase",
+			"charge_id", pending.Value.charge.ID,
+			"customer_id", pending.Value.customerID.ID,
+			"error", err,
+		)
 		return
 	}

 	out[pending.Index] = charges.NewCharge(updatedCharge)
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/service/create.go` around lines 160 - 174, The
post-create loop over result.pendingInvoiceCreditPurchases currently swallows
errors; update it to log structured error details when
s.handleInvoiceCreditPurchasePostCreate(ctx, pending.Value) returns an error
while preserving the current behavior of skipping the charge. Specifically, add
a processLogger (or existing logger) call that includes the charge id or
pending.Value identifier, pending.Index, and the error returned from
handleInvoiceCreditPurchasePostCreate so operators can monitor failures, but
still return/continue without altering out[pending.Index] on error.

259-274: Acknowledged: duplicate gathering lines on retry are a known gap.

The comment at lines 260-262 clearly documents this limitation. For future hardening, you could query by ChargeID before creating, or persist a partial InvoiceSettlement with the gathering line ID immediately after creation. Not blocking for this PR since it's documented, but worth tracking.

Want me to open an issue to track implementing deduplication for gathering line creation on retries?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/service/create.go` around lines 259 - 274, The
current code calling s.billingService.CreatePendingInvoiceLines with
pending.gatheringLine can create duplicate gathering lines on retries; before
creating, call a billing-side lookup (e.g., implement and use a method like
billingService.FindGatheringLinesByChargeID or ListPendingInvoiceLines filtered
by pending.chargeID) and if a matching gathering line already exists skip
CreatePendingInvoiceLines, otherwise call CreatePendingInvoiceLines and
immediately persist a partial InvoiceSettlement containing the new gathering
line ID (or record the created line ID on the pending object) so retries detect
the existing line and no duplicate is created; update the logic around
CreatePendingInvoiceLines and ensure checks use pending.chargeID and
pending.gatheringLine identifiers.
🤖 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/creditpurchase/service/invoice.go`:
- Line 15: The function name has a typo: rename onInvoceSettlementPurchase to
onInvoiceSettlementPurchase and update all references/call sites and any tests
or interface implementations that use onInvoceSettlementPurchase to the
corrected name; ensure any exported/internal usages, documentation, and mocks
reflect the new identifier so builds and references remain consistent.

In `@openmeter/billing/charges/service/create.go`:
- Around line 290-292: The check that only inspects invoices[0] is brittle;
iterate all returned invoices from InvoicePendingLines (the invoices slice) and
search each invoice's Lines.OrEmpty() for the matching ChargeID rather than
assuming invoices[0] contains the standard line; if no matching line is found
across all invoices return the existing error ("no standard invoice created for
credit purchase" or "standard line not found") so downstream logic (the later
loop that uses ChargeID) will always have the correct line regardless of which
invoice it landed on.

---

Nitpick comments:
In `@openmeter/billing/charges/creditpurchase/chargecreditpurchase.go`:
- Around line 118-131: In validateOptionalValidator, avoid calling
reflect.ValueOf(v) twice by assigning rv := reflect.ValueOf(v) once and using
rv.Kind() and rv.IsNil() for the nil-pointer checks; keep the same behavior and
error wrapping using v.Validate() and fmt.Errorf with the provided name.

In `@openmeter/billing/charges/creditpurchase/service/invoice.go`:
- Around line 86-93: Clarify the idempotency check in invoice.go by adding a
concise inline comment next to the conditional that explains the logic: state
when InvoiceSettlement.Authorized exists and its TransactionGroupID differs from
CreditGrantRealization.GroupReference.TransactionGroupID means this handler
already ran (because LinkInvoicedPayment copies the original group ID while this
handler generates a new one), so we should return; reference the symbols
InvoiceSettlement.Authorized, CreditGrantRealization, LinkInvoicedPayment, and
TransactionGroupID in the comment to make the intent obvious to future readers.

In `@openmeter/billing/charges/service/create.go`:
- Around line 160-174: The post-create loop over
result.pendingInvoiceCreditPurchases currently swallows errors; update it to log
structured error details when s.handleInvoiceCreditPurchasePostCreate(ctx,
pending.Value) returns an error while preserving the current behavior of
skipping the charge. Specifically, add a processLogger (or existing logger) call
that includes the charge id or pending.Value identifier, pending.Index, and the
error returned from handleInvoiceCreditPurchasePostCreate so operators can
monitor failures, but still return/continue without altering out[pending.Index]
on error.
- Around line 259-274: The current code calling
s.billingService.CreatePendingInvoiceLines with pending.gatheringLine can create
duplicate gathering lines on retries; before creating, call a billing-side
lookup (e.g., implement and use a method like
billingService.FindGatheringLinesByChargeID or ListPendingInvoiceLines filtered
by pending.chargeID) and if a matching gathering line already exists skip
CreatePendingInvoiceLines, otherwise call CreatePendingInvoiceLines and
immediately persist a partial InvoiceSettlement containing the new gathering
line ID (or record the created line ID on the pending object) so retries detect
the existing line and no duplicate is created; update the logic around
CreatePendingInvoiceLines and ensure checks use pending.chargeID and
pending.gatheringLine identifiers.
🪄 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: 13a8615a-b91a-421a-8c80-8bd7d135049b

📥 Commits

Reviewing files that changed from the base of the PR and between b7ed0eb and 4218be2.

📒 Files selected for processing (3)
  • openmeter/billing/charges/creditpurchase/chargecreditpurchase.go
  • openmeter/billing/charges/creditpurchase/service/invoice.go
  • openmeter/billing/charges/service/create.go

@mark-vass-konghq mark-vass-konghq force-pushed the feat/invoice-settlement-mode-for-credits branch from 4218be2 to 33bf9dc Compare March 20, 2026 12:51
Copy link
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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
openmeter/billing/charges/service/creditpurchase_test.go (1)

380-397: ⚠️ Potential issue | 🟠 Major

Please add coverage for the deferred invoice path too.

With ServicePeriod.From fixed to January 1, 2026, this test only exercises the immediate InvoiceAt <= now branch. The new deferred path—pending line created first, InvoiceSettlement linked later by invoice callbacks—is still uncovered, and that’s the branch most likely to regress here. A companion integration test with a future service period would make this change a lot safer.

As per coding guidelines, "Make sure the tests are comprehensive and cover the changes. Keep a strong focus on unit tests and in-code integration tests. When appropriate, recommend e2e tests for critical changes."

Also applies to: 403-565

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/service/creditpurchase_test.go` around lines 380 -
397, The test only exercises the immediate invoice path because
ServicePeriod.From is set to 2026-01-01 (making InvoiceAt <= now); add a
companion test variant that sets the service period
(createCreditPurchaseIntentInput.ServicePeriod.From) to a future date so the
flow creates a pending line first and later links an InvoiceSettlement via the
invoice callback path (exercise
creditpurchase.NewSettlement/creditpurchase.InvoiceSettlement, InvoiceAt branch,
and the pending-to-invoiced transition). In that new test, call
CreateCreditPurchaseIntent with a future ServicePeriod.From, assert that the
initial state is a pending line (no invoice created), then simulate the invoice
callback that attaches the InvoiceSettlement and assert the invoice is
created/linked and balances update accordingly; reuse existing helpers around
CreateCreditPurchaseIntent and assertions to mirror the immediate-path test.
♻️ Duplicate comments (1)
openmeter/billing/charges/service/create.go (1)

245-267: ⚠️ Potential issue | 🟠 Major

The future-dated branch still isn’t safely retryable.

On the InvoiceAt > now path, InvoiceSettlement stays nil, so this guard never flips on a replay. Any retry/background rerun of this helper can call CreatePendingInvoiceLines() again for the same charge and materialize duplicate pending invoice lines. This needs a durable “line already created” marker, or a lookup by ChargeID before creating another line.

Also applies to: 316-320

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/service/create.go` around lines 245 - 267,
handleInvoiceCreditPurchasePostCreate can create duplicate pending invoice lines
for future-dated charges because InvoiceSettlement stays nil; ensure idempotency
by checking for an existing gathering line tied to the charge before calling
billingService.CreatePendingInvoiceLines or by persisting a partial
InvoiceSettlement (including the created gathering line ID) immediately after
creating the gathering line. Concretely: in
handleInvoiceCreditPurchasePostCreate, before calling CreatePendingInvoiceLines,
query the billing side for existing gathering lines filtered by ChargeID (or use
an existing lookup method) and return early if one exists; if you must create a
line, write back a durable marker (e.g., set Charge.State.InvoiceSettlement with
gatheringLine ID or persist a minimal settlement record) so subsequent retries
see InvoiceSettlement non-nil and skip creation. Reference:
handleInvoiceCreditPurchasePostCreate, Charge.State.InvoiceSettlement,
pending.gatheringLine, billingService.CreatePendingInvoiceLines, and the
alternative LinkInvoicedPayment / HandleInvoicePaymentAuthorized hooks mentioned
in comments.
🧹 Nitpick comments (2)
openmeter/billing/charges/service/creditpurchase_test.go (1)

516-522: These DEBUG probes should probably become assertions or go away.

The extra fetches are fine if we want to guard the transition, but right now most of the value is just log noise. If the intermediate invoice state matters, I’d assert it directly; otherwise I’d drop the debug scaffolding before merge.

Also applies to: 539-552

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/service/creditpurchase_test.go` around lines 516 -
522, The two intermediate "DEBUG" fetches in the test use
BillingService.GetStandardInvoiceById (variables betweenInvoice, betweenErr and
the later equivalent) and currently only log results; either remove these extra
fetches entirely or convert them into explicit assertions about the intermediate
invoice state (e.g., assert no error and assert betweenInvoice.Status equals the
expected status) so they provide test value instead of noise—apply the same
change to the similar block referenced around the later case (lines 539-552)
that uses the same GetStandardInvoiceById calls.
openmeter/billing/charges/service/create.go (1)

165-170: Please emit some telemetry when this best-effort step fails.

Since the error is intentionally swallowed here, a stuck invoice-settlement charge becomes invisible to operators. Even a structured log and metric with the charge/customer context would make recovery a lot easier.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/service/create.go` around lines 165 - 170, In the
loop over result.pendingInvoiceCreditPurchases where
s.handleInvoiceCreditPurchasePostCreate(ctx, pending.Value) currently swallows
errors, emit telemetry when err != nil: log a structured error including
identifying fields from pending.Value (e.g. charge ID, customer ID, invoice ID)
and the error, and increment a failure metric/counter (e.g.
invoice_settlement_post_create_failures) tagged with those identifiers; use the
service's existing logger/telemetry client (the same object owning
handleInvoiceCreditPurchasePostCreate, e.g. s.logger or s.metrics) so operators
can detect and filter these best-effort failures even though the code continues
to return and skip processing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@openmeter/billing/charges/service/creditpurchase_test.go`:
- Around line 380-397: The test only exercises the immediate invoice path
because ServicePeriod.From is set to 2026-01-01 (making InvoiceAt <= now); add a
companion test variant that sets the service period
(createCreditPurchaseIntentInput.ServicePeriod.From) to a future date so the
flow creates a pending line first and later links an InvoiceSettlement via the
invoice callback path (exercise
creditpurchase.NewSettlement/creditpurchase.InvoiceSettlement, InvoiceAt branch,
and the pending-to-invoiced transition). In that new test, call
CreateCreditPurchaseIntent with a future ServicePeriod.From, assert that the
initial state is a pending line (no invoice created), then simulate the invoice
callback that attaches the InvoiceSettlement and assert the invoice is
created/linked and balances update accordingly; reuse existing helpers around
CreateCreditPurchaseIntent and assertions to mirror the immediate-path test.

---

Duplicate comments:
In `@openmeter/billing/charges/service/create.go`:
- Around line 245-267: handleInvoiceCreditPurchasePostCreate can create
duplicate pending invoice lines for future-dated charges because
InvoiceSettlement stays nil; ensure idempotency by checking for an existing
gathering line tied to the charge before calling
billingService.CreatePendingInvoiceLines or by persisting a partial
InvoiceSettlement (including the created gathering line ID) immediately after
creating the gathering line. Concretely: in
handleInvoiceCreditPurchasePostCreate, before calling CreatePendingInvoiceLines,
query the billing side for existing gathering lines filtered by ChargeID (or use
an existing lookup method) and return early if one exists; if you must create a
line, write back a durable marker (e.g., set Charge.State.InvoiceSettlement with
gatheringLine ID or persist a minimal settlement record) so subsequent retries
see InvoiceSettlement non-nil and skip creation. Reference:
handleInvoiceCreditPurchasePostCreate, Charge.State.InvoiceSettlement,
pending.gatheringLine, billingService.CreatePendingInvoiceLines, and the
alternative LinkInvoicedPayment / HandleInvoicePaymentAuthorized hooks mentioned
in comments.

---

Nitpick comments:
In `@openmeter/billing/charges/service/create.go`:
- Around line 165-170: In the loop over result.pendingInvoiceCreditPurchases
where s.handleInvoiceCreditPurchasePostCreate(ctx, pending.Value) currently
swallows errors, emit telemetry when err != nil: log a structured error
including identifying fields from pending.Value (e.g. charge ID, customer ID,
invoice ID) and the error, and increment a failure metric/counter (e.g.
invoice_settlement_post_create_failures) tagged with those identifiers; use the
service's existing logger/telemetry client (the same object owning
handleInvoiceCreditPurchasePostCreate, e.g. s.logger or s.metrics) so operators
can detect and filter these best-effort failures even though the code continues
to return and skip processing.

In `@openmeter/billing/charges/service/creditpurchase_test.go`:
- Around line 516-522: The two intermediate "DEBUG" fetches in the test use
BillingService.GetStandardInvoiceById (variables betweenInvoice, betweenErr and
the later equivalent) and currently only log results; either remove these extra
fetches entirely or convert them into explicit assertions about the intermediate
invoice state (e.g., assert no error and assert betweenInvoice.Status equals the
expected status) so they provide test value instead of noise—apply the same
change to the similar block referenced around the later case (lines 539-552)
that uses the same GetStandardInvoiceById calls.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b289c8c6-1441-453d-92e4-afc7b5d5a175

📥 Commits

Reviewing files that changed from the base of the PR and between 4218be2 and 33bf9dc.

📒 Files selected for processing (14)
  • openmeter/billing/adapter/invoiceapp.go
  • openmeter/billing/charges/creditpurchase/adapter.go
  • openmeter/billing/charges/creditpurchase/adapter/charge.go
  • openmeter/billing/charges/creditpurchase/adapter/mapper.go
  • openmeter/billing/charges/creditpurchase/adapter/payment.go
  • openmeter/billing/charges/creditpurchase/chargecreditpurchase.go
  • openmeter/billing/charges/creditpurchase/service.go
  • openmeter/billing/charges/creditpurchase/service/create.go
  • openmeter/billing/charges/creditpurchase/service/invoice.go
  • openmeter/billing/charges/service/create.go
  • openmeter/billing/charges/service/creditpurchase_test.go
  • openmeter/billing/charges/service/invoice.go
  • openmeter/billing/charges/service/invoicependinglines.go
  • openmeter/billing/stdinvoice.go
🚧 Files skipped from review as they are similar to previous changes (9)
  • openmeter/billing/charges/creditpurchase/service/create.go
  • openmeter/billing/charges/creditpurchase/adapter/payment.go
  • openmeter/billing/charges/creditpurchase/adapter/charge.go
  • openmeter/billing/charges/service/invoice.go
  • openmeter/billing/charges/service/invoicependinglines.go
  • openmeter/billing/charges/creditpurchase/chargecreditpurchase.go
  • openmeter/billing/charges/creditpurchase/service.go
  • openmeter/billing/charges/creditpurchase/adapter/mapper.go
  • openmeter/billing/charges/creditpurchase/service/invoice.go

Copy link
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.

♻️ Duplicate comments (2)
openmeter/billing/charges/service/create.go (2)

161-171: ⚠️ Potential issue | 🟠 Major

Don't let request cancellation kill the post-commit settlement work.

After the transaction commits, this still reuses the caller ctx. If the request times out or the client disconnects, handleInvoiceCreditPurchasePostCreate() can fail right away, and that failure is dropped here, so the charge looks created even though the invoice-side settlement never happened. This follow-up needs an owned context with its own timeout, or a durable async handoff.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/service/create.go` around lines 161 - 171, The
post-commit loop uses the caller ctx which can be canceled by the request;
instead create an independent context with its own timeout or use a durable
async handoff before calling handleInvoiceCreditPurchasePostCreate so request
cancellation won't abort settlement. In the lo.ForEach over
result.pendingInvoiceCreditPurchases, replace passing the incoming ctx to
handleInvoiceCreditPurchasePostCreate with a created background-derived context
(e.g., context.WithTimeout(context.Background(), <shortDuration>)) and ensure
you call cancel() after the call; alternatively enqueue the pending item to a
background worker/queue owned by the service so
handleInvoiceCreditPurchasePostCreate runs independently of the request
lifetime.

205-223: ⚠️ Potential issue | 🔴 Critical

This still isn't idempotent after the first billing write.

InvoiceSettlement != nil only protects the fully linked case. If CreatePendingInvoiceLines() succeeds and the function exits before LinkInvoicedPayment() persists state — or createdLine.InvoiceAt is still in the future — the next retry will create another pending line for the same charge. On a billing flow, retrying a non-idempotent write like this can duplicate invoice artifacts for one credit purchase. Please persist a durable "pending settlement / created line id" marker before returning, or dedupe the pending-line creation by ChargeID.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/service/create.go` around lines 205 - 223, The
current idempotency check only looks at charge.State.InvoiceSettlement and does
not prevent duplicate billing writes if CreatePendingInvoiceLines succeeds but
state isn't persisted; modify the flow in the processing function so that after
receiving result from s.billingService.CreatePendingInvoiceLines (the created
gathering line), you persist a durable marker (e.g., set
charge.State.InvoiceSettlement with the created line ID / a "pendingSettlement"
token) before any other return points and before calling LinkInvoicedPayment, or
alternatively query/dedupe existing gathering lines by ChargeID via the billing
service before creating; update the code paths around
s.billingService.CreatePendingInvoiceLines, pending.gatheringLine, and
LinkInvoicedPayment to ensure the created line ID is stored atomically on the
charge to prevent duplicate pending-line creation on retries.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@openmeter/billing/charges/service/create.go`:
- Around line 161-171: The post-commit loop uses the caller ctx which can be
canceled by the request; instead create an independent context with its own
timeout or use a durable async handoff before calling
handleInvoiceCreditPurchasePostCreate so request cancellation won't abort
settlement. In the lo.ForEach over result.pendingInvoiceCreditPurchases, replace
passing the incoming ctx to handleInvoiceCreditPurchasePostCreate with a created
background-derived context (e.g., context.WithTimeout(context.Background(),
<shortDuration>)) and ensure you call cancel() after the call; alternatively
enqueue the pending item to a background worker/queue owned by the service so
handleInvoiceCreditPurchasePostCreate runs independently of the request
lifetime.
- Around line 205-223: The current idempotency check only looks at
charge.State.InvoiceSettlement and does not prevent duplicate billing writes if
CreatePendingInvoiceLines succeeds but state isn't persisted; modify the flow in
the processing function so that after receiving result from
s.billingService.CreatePendingInvoiceLines (the created gathering line), you
persist a durable marker (e.g., set charge.State.InvoiceSettlement with the
created line ID / a "pendingSettlement" token) before any other return points
and before calling LinkInvoicedPayment, or alternatively query/dedupe existing
gathering lines by ChargeID via the billing service before creating; update the
code paths around s.billingService.CreatePendingInvoiceLines,
pending.gatheringLine, and LinkInvoicedPayment to ensure the created line ID is
stored atomically on the charge to prevent duplicate pending-line creation on
retries.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8d09af33-ca60-495c-972a-11b00ded1251

📥 Commits

Reviewing files that changed from the base of the PR and between 33bf9dc and e4d672f.

📒 Files selected for processing (7)
  • openmeter/billing/charges/creditpurchase/chargecreditpurchase.go
  • openmeter/billing/charges/creditpurchase/service.go
  • openmeter/billing/charges/creditpurchase/service/create.go
  • openmeter/billing/charges/service/create.go
  • openmeter/billing/charges/service/invoicependinglines.go
  • openmeter/billing/service/gatheringinvoicependinglines.go
  • openmeter/billing/stdinvoice.go
✅ Files skipped from review due to trivial changes (1)
  • openmeter/billing/service/gatheringinvoicependinglines.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • openmeter/billing/charges/creditpurchase/chargecreditpurchase.go
  • openmeter/billing/stdinvoice.go
  • openmeter/billing/charges/creditpurchase/service/create.go
  • openmeter/billing/charges/creditpurchase/service.go
  • openmeter/billing/charges/service/invoicependinglines.go

Copy link
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

♻️ Duplicate comments (2)
openmeter/billing/charges/service/create.go (1)

166-172: ⚠️ Potential issue | 🟠 Major

Create() can still fail after the charge commit.

By the time this loop runs, transaction.Run() has already succeeded. If handleInvoiceCreditPurchasePostCreate() errors after creating pending lines or even invoicing them, the API returns a hard failure for an already-active charge, and a retry is not replay-safe because the helper only short-circuits once InvoiceSettlement exists. This post-step needs a durable/idempotent retry path, or it should be treated as best-effort instead of failing Create() here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/service/create.go` around lines 166 - 172, Create()
currently fails after transaction.Run() if
handleInvoiceCreditPurchasePostCreate() errors, which can leave an active charge
and make retries unsafe; change this to a best-effort post-commit handling:
after transaction.Run() succeeds, call handleInvoiceCreditPurchasePostCreate()
but do not return its error to the caller—log the error and enqueue a durable
background retry (or mark a retryable work item) so the operation can be
replayed safely; also make handleInvoiceCreditPurchasePostCreate() and its
helpers idempotent by short-circuiting when InvoiceSettlement already exists
(and by checking/creating pending lines atomically) so background retries are
safe.
openmeter/billing/charges/creditpurchase/service/invoice.go (1)

48-60: ⚠️ Potential issue | 🟠 Major

Linking shouldn’t mark the invoice payment as authorized yet.

LinkInvoicedPayment() runs right after the standard line is created, while the invoice can still be draft/manual-approval, but this record is seeded with Authorized and StatusAuthorized. That makes the charge look invoice-authorized before the authorize hook on Line 80 actually runs. Please start this settlement in a pre-authorization state and let HandleInvoicePaymentAuthorized() own the real transition.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openmeter/billing/charges/creditpurchase/service/invoice.go` around lines 48
- 60, LinkInvoicedPayment is seeding a payment.InvoicedCreate with Authorized
and payment.StatusAuthorized too early; remove the Authorized assignment (set
Base.Authorized to nil) and set Base.Status to the pre-authorization state
constant (e.g. payment.StatusPreAuthorized or payment.StatusPending if that’s
the repo's pre-auth constant) so the record is created as pre-authorized; let
HandleInvoicePaymentAuthorized() perform the actual authorization and set
Authorized + payment.StatusAuthorized later.
🤖 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/service/create.go`:
- Around line 144-147: The code calls s.createGatheringLines(ctx,
gatheringLinesToCreate) a second time, but gatheringLinesToCreate was already
flushed earlier (and invoice-settlement credit purchases are not appended to
it), causing duplicate creation of the same pending lines; remove the redundant
second call and instead ensure any invoice-settlement credit purchase lines are
appended to the slice before the first flush or handled by a separate slice and
call; update the logic around the gatheringLinesToCreate usage (and the
createGatheringLines invocation) so each intended line is created exactly once
(refer to gatheringLinesToCreate and s.createGatheringLines).

---

Duplicate comments:
In `@openmeter/billing/charges/creditpurchase/service/invoice.go`:
- Around line 48-60: LinkInvoicedPayment is seeding a payment.InvoicedCreate
with Authorized and payment.StatusAuthorized too early; remove the Authorized
assignment (set Base.Authorized to nil) and set Base.Status to the
pre-authorization state constant (e.g. payment.StatusPreAuthorized or
payment.StatusPending if that’s the repo's pre-auth constant) so the record is
created as pre-authorized; let HandleInvoicePaymentAuthorized() perform the
actual authorization and set Authorized + payment.StatusAuthorized later.

In `@openmeter/billing/charges/service/create.go`:
- Around line 166-172: Create() currently fails after transaction.Run() if
handleInvoiceCreditPurchasePostCreate() errors, which can leave an active charge
and make retries unsafe; change this to a best-effort post-commit handling:
after transaction.Run() succeeds, call handleInvoiceCreditPurchasePostCreate()
but do not return its error to the caller—log the error and enqueue a durable
background retry (or mark a retryable work item) so the operation can be
replayed safely; also make handleInvoiceCreditPurchasePostCreate() and its
helpers idempotent by short-circuiting when InvoiceSettlement already exists
(and by checking/creating pending lines atomically) so background retries are
safe.
🪄 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: 365c1e5a-7848-4e37-ad46-3b03028fab0b

📥 Commits

Reviewing files that changed from the base of the PR and between e4d672f and 3d77246.

📒 Files selected for processing (14)
  • openmeter/billing/charges/creditpurchase/adapter.go
  • openmeter/billing/charges/creditpurchase/adapter/charge.go
  • openmeter/billing/charges/creditpurchase/adapter/mapper.go
  • openmeter/billing/charges/creditpurchase/adapter/payment.go
  • openmeter/billing/charges/creditpurchase/chargecreditpurchase.go
  • openmeter/billing/charges/creditpurchase/service.go
  • openmeter/billing/charges/creditpurchase/service/create.go
  • openmeter/billing/charges/creditpurchase/service/invoice.go
  • openmeter/billing/charges/service/create.go
  • openmeter/billing/charges/service/creditpurchase_test.go
  • openmeter/billing/charges/service/invoice.go
  • openmeter/billing/charges/service/invoicependinglines.go
  • openmeter/billing/service/gatheringinvoicependinglines.go
  • openmeter/billing/stdinvoice.go
✅ Files skipped from review due to trivial changes (1)
  • openmeter/billing/charges/creditpurchase/adapter/payment.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • openmeter/billing/charges/creditpurchase/chargecreditpurchase.go
  • openmeter/billing/charges/creditpurchase/adapter/charge.go
  • openmeter/billing/service/gatheringinvoicependinglines.go
  • openmeter/billing/charges/service/invoicependinglines.go
  • openmeter/billing/charges/creditpurchase/adapter/mapper.go
  • openmeter/billing/charges/creditpurchase/adapter.go
  • openmeter/billing/charges/service/creditpurchase_test.go

@mark-vass-konghq mark-vass-konghq force-pushed the feat/invoice-settlement-mode-for-credits branch from 3d77246 to 25e77ef Compare March 23, 2026 07:13
@turip
Copy link
Member

turip commented Mar 23, 2026

Please consider this coderabbit test coverage issue:

openmeter/billing/charges/service/creditpurchase_test.go (1)
380-397: ⚠️ Potential issue | 🟠 Major

Please add coverage for the deferred invoice path too.

With ServicePeriod.From fixed to January 1, 2026, this test only exercises the immediate InvoiceAt <= now branch. The new deferred path—pending line created first, InvoiceSettlement linked later by invoice callbacks—is still uncovered, and that’s the branch most likely to regress here. A companion integration test with a future service period would make this change a lot safer.

As per coding guidelines, "Make sure the tests are comprehensive and cover the changes. Keep a strong focus on unit tests and in-code integration tests. When appropriate, recommend e2e tests for critical changes."

Also applies to: 403-565

@mark-vass-konghq mark-vass-konghq force-pushed the feat/invoice-settlement-mode-for-credits branch from 25e77ef to 4c13c48 Compare March 23, 2026 10:18
@mark-vass-konghq mark-vass-konghq force-pushed the feat/invoice-settlement-mode-for-credits branch from 4c13c48 to c2b7b7d Compare March 23, 2026 10:46
// The charge should be active and ready for when the invoice date arrives
assert.Equal(s.T(), meta.ChargeStatusActive, creditPurchaseCharge.Status)
})

Copy link
Member

Choose a reason for hiding this comment

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

This is not entirely true. Please check the clock pakage, we use that for backdating.

But the justficiation is fine for not continuing the testcase.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-note/feature Release note: Exciting New Features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants