Skip to content

feat: credit purchase invoice#3961

Merged
turip merged 5 commits intomainfrom
feat/credit-purchase-invoice
Mar 19, 2026
Merged

feat: credit purchase invoice#3961
turip merged 5 commits intomainfrom
feat/credit-purchase-invoice

Conversation

@turip
Copy link
Member

@turip turip commented Mar 17, 2026

Overview

Add schema and basic testcase for creditpurchase via standard invoice codepath.

Notes for reviewer

Summary by CodeRabbit

  • New Features

    • Credit purchase charges now support invoiced payments and capture invoice settlement details.
    • Payment records include a required invoice identifier for stronger tracking and validation.
  • Tests

    • Added a test for standard invoice credit purchase flows (currently marked to skip).
  • Chores

    • Database migration added to persist invoiced payment records and invoice IDs.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 17, 2026

📝 Walkthrough

Walkthrough

Adds invoiced-payment support for credit-purchase charges: new invoiced payment ent/schema, DB migration adding invoice_id, model and service wiring to populate InvoiceID, a new InvoiceSettlement field on charge state, and a skipped integration test exercising the invoice-driven flow.

Changes

Cohort / File(s) Summary
Data model
openmeter/billing/charges/models/payment/invoiced.go
Added InvoiceID to InvoicedCreate and Invoiced, required by validation; added fluent SetInvoiceID() and InvoicedGetter.GetInvoiceID(), and mapped DB -> struct.
Service updates
openmeter/billing/charges/creditpurchase/chargecreditpurchase.go, openmeter/billing/charges/flatfee/service/payment.go
State gains InvoiceSettlement *payment.Invoiced. PostPaymentAuthorized now sets InvoiceID on InvoicedCreate from invoice line header.
Entity schema
openmeter/ent/schema/chargescreditpurchase.go, openmeter/ent/schema/billing.go
Added ChargeCreditPurchaseInvoicedPayment schema (uses payment.InvoicedMixin) and edges; refactored external payment schema; added cascade edge from BillingInvoiceLine to invoiced payments.
Migrations
tools/migrate/migrations/..._charge-creditpurchase-invoiced-payment.up.sql, ...down.sql
Up adds invoice_id columns to existing payment tables and creates charge_credit_purchase_invoiced_payments table with FKs, indexes, and constraints; down removes indexes, drops table, and drops invoice_id columns.
Tests
openmeter/billing/charges/service/creditpurchase_test.go
Added TestStandardInvoiceCreditPurchase (skipped) which sets up custom invoicing and walks initiated→authorized→settled invoice/payment lifecycle.

Sequence Diagram

sequenceDiagram
    participant Test as Test Harness
    participant CPS as CreditPurchase Service
    participant BIS as Billing Invoice Service
    participant CIS as CustomInvoicing Service
    participant DB as Database

    Test->>CPS: Create credit purchase with InvoiceSettlement
    CPS->>DB: Persist credit purchase (state: initiated)

    Test->>CPS: Post payment authorized callback
    CPS->>DB: Update state -> authorized, attach InvoiceSettlement (InvoiceID)

    Test->>BIS: Approve standard invoice
    BIS->>DB: Mark invoice approved

    Test->>CIS: Trigger payment processing (TriggerPaid)
    CIS->>DB: Read invoiced payment by invoice/line -> update authorized/settled timestamps/status
    CIS->>CPS: Notify/trigger charge settlement callbacks

    CPS->>DB: Read final state
    DB-->>Test: Confirm InvoiceSettlement present and charge settled
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

release-note/feature

Suggested reviewers

  • chrisgacsal
  • tothandras
🚥 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
Title check ✅ Passed The title 'feat: credit purchase invoice' clearly and directly summarizes the main change—adding credit purchase via standard invoice, which aligns with schema additions, model updates, and the new test case.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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/credit-purchase-invoice
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@turip turip force-pushed the feat/credit-purchase-invoice branch from 5cafbf2 to 7a684c9 Compare March 19, 2026 11:51
@turip turip marked this pull request as ready for review March 19, 2026 11:51
@turip turip requested a review from a team as a code owner March 19, 2026 11:51
@turip turip added release-note/misc Miscellaneous changes area/billing labels Mar 19, 2026
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: 3

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/creditpurchase/chargecreditpurchase.go (1)

90-112: ⚠️ Potential issue | 🟡 Minor

Missing validation for InvoiceSettlement in State.Validate().

The new InvoiceSettlement field isn't validated, but ExternalPaymentSettlement is. For consistency and to catch invalid data early, you'll want to add the same validation pattern:

🛡️ Proposed fix to add validation
 func (s State) Validate() error {
 	var errs []error
 
 	if s.CreditGrantRealization != nil {
 		if err := s.CreditGrantRealization.Validate(); err != nil {
 			errs = append(errs, fmt.Errorf("credit grant realization: %w", err))
 		}
 	}
 
 	if s.ExternalPaymentSettlement != nil {
 		if err := s.ExternalPaymentSettlement.Validate(); err != nil {
 			errs = append(errs, fmt.Errorf("external payment settlement: %w", err))
 		}
 	}
 
+	if s.InvoiceSettlement != nil {
+		if err := s.InvoiceSettlement.Validate(); err != nil {
+			errs = append(errs, fmt.Errorf("invoice settlement: %w", err))
+		}
+	}
+
 	return errors.Join(errs...)
 }
🤖 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 90 - 112, State.Validate is missing validation for the InvoiceSettlement
field; add a nil-check like the one for ExternalPaymentSettlement that calls
s.InvoiceSettlement.Validate(), and if it returns an error append it to errs
with fmt.Errorf("invoice settlement: %w", err) so it gets included in the final
errors.Join(errs...); this mirrors the pattern used for
CreditGrantRealization.Validate() and ExternalPaymentSettlement.Validate().
🧹 Nitpick comments (1)
tools/migrate/migrations/20260319114832_charge-creditpurchase-invoiced-payment.up.sql (1)

34-35: Minor: Unique index on id is redundant with PRIMARY KEY.

Line 35 creates chargecreditpurchaseinvoicedpayment_id as a unique index on id, but id is already the primary key (line 24). This is likely auto-generated by Ent and harmless, just a small overhead.

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

In
`@tools/migrate/migrations/20260319114832_charge-creditpurchase-invoiced-payment.up.sql`
around lines 34 - 35, The migration adds a redundant unique index
"chargecreditpurchaseinvoicedpayment_id" on column "id" of table
"charge_credit_purchase_invoiced_payments" which duplicates the PRIMARY KEY;
remove that CREATE UNIQUE INDEX statement (or skip generating it) so only the
primary key enforces uniqueness. Locate the statement creating
"chargecreditpurchaseinvoicedpayment_id" in the migration and delete it (or
adjust the Ent schema/migration generator to avoid emitting a unique index for
"id") so only the existing primary key remains.
🤖 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/creditpurchase_test.go`:
- Line 517: The test is asserting a charge-status constant against res.Status,
but HandlePaymentTrigger returns an invoice status; update the assertion to
compare res.Status to the invoice status constant (e.g.,
billing.StandardInvoiceStatusPaid) instead of meta.ChargeStatusFinal, and ensure
the billing package symbol is referenced/imported where the test asserts the
result from HandlePaymentTrigger.
- Around line 408-411: The assertion in the test callback for
CreditPurchaseTestHandler.onCreditPurchaseInitiated is checking the wrong
settlement type; update the assert.Equal that compares
charge.Intent.Settlement.Type() to use creditpurchase.SettlementTypeInvoice
(instead of SettlementTypeExternal) so it matches the InvoiceSettlement intent
set earlier; locate the initatedCallback.Handler invocation in
creditpurchase_test.go where charge.Intent.Settlement.Type() is asserted and
change the expected value to SettlementTypeInvoice.
- Around line 492-500: The assertions in the onCreditPurchasePaymentSettled
handler are checking for external settlement types/fields but this test path
uses an invoice settlement; update the checks in the callback
(onCreditPurchasePaymentSettled) to assert SettlementTypeInvoice instead of
SettlementTypeExternal and to reference charge.State.InvoicePaymentSettlement
(and its Authorized.TransactionGroupID) rather than ExternalPaymentSettlement,
keeping the rest of the expectations (payment.StatusAuthorized and
meta.ChargeStatusActive) unchanged.

---

Outside diff comments:
In `@openmeter/billing/charges/creditpurchase/chargecreditpurchase.go`:
- Around line 90-112: State.Validate is missing validation for the
InvoiceSettlement field; add a nil-check like the one for
ExternalPaymentSettlement that calls s.InvoiceSettlement.Validate(), and if it
returns an error append it to errs with fmt.Errorf("invoice settlement: %w",
err) so it gets included in the final errors.Join(errs...); this mirrors the
pattern used for CreditGrantRealization.Validate() and
ExternalPaymentSettlement.Validate().

---

Nitpick comments:
In
`@tools/migrate/migrations/20260319114832_charge-creditpurchase-invoiced-payment.up.sql`:
- Around line 34-35: The migration adds a redundant unique index
"chargecreditpurchaseinvoicedpayment_id" on column "id" of table
"charge_credit_purchase_invoiced_payments" which duplicates the PRIMARY KEY;
remove that CREATE UNIQUE INDEX statement (or skip generating it) so only the
primary key enforces uniqueness. Locate the statement creating
"chargecreditpurchaseinvoicedpayment_id" in the migration and delete it (or
adjust the Ent schema/migration generator to avoid emitting a unique index for
"id") so only the existing primary key remains.
🪄 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: f3492d99-e6c1-4f00-98c2-a6a05bf0eda5

📥 Commits

Reviewing files that changed from the base of the PR and between 1b64e43 and 7a684c9.

⛔ Files ignored due to path filters (41)
  • openmeter/ent/db/billinginvoiceline.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoiceline/billinginvoiceline.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoiceline/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoiceline_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoiceline_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/billinginvoiceline_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargecreditpurchase.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargecreditpurchase/chargecreditpurchase.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargecreditpurchase/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargecreditpurchase_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargecreditpurchase_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargecreditpurchase_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargecreditpurchaseinvoicedpayment.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargecreditpurchaseinvoicedpayment/chargecreditpurchaseinvoicedpayment.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargecreditpurchaseinvoicedpayment/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargecreditpurchaseinvoicedpayment_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargecreditpurchaseinvoicedpayment_delete.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargecreditpurchaseinvoicedpayment_query.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargecreditpurchaseinvoicedpayment_update.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeflatfeepayment.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeflatfeepayment/chargeflatfeepayment.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeflatfeepayment/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeflatfeepayment_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedrunpayment.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedrunpayment/chargeusagebasedrunpayment.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedrunpayment/where.go is excluded by !**/ent/db/**
  • openmeter/ent/db/chargeusagebasedrunpayment_create.go is excluded by !**/ent/db/**
  • openmeter/ent/db/client.go is excluded by !**/ent/db/**
  • openmeter/ent/db/cursor.go is excluded by !**/ent/db/**
  • openmeter/ent/db/ent.go is excluded by !**/ent/db/**
  • openmeter/ent/db/entmixinaccessor.go is excluded by !**/ent/db/**
  • openmeter/ent/db/expose.go is excluded by !**/ent/db/**
  • openmeter/ent/db/hook/hook.go is excluded by !**/ent/db/**
  • openmeter/ent/db/migrate/schema.go is excluded by !**/ent/db/**
  • openmeter/ent/db/mutation.go is excluded by !**/ent/db/**
  • openmeter/ent/db/paginate.go is excluded by !**/ent/db/**
  • openmeter/ent/db/predicate/predicate.go is excluded by !**/ent/db/**
  • openmeter/ent/db/runtime.go is excluded by !**/ent/db/**
  • openmeter/ent/db/setorclear.go is excluded by !**/ent/db/**
  • openmeter/ent/db/tx.go is excluded by !**/ent/db/**
  • tools/migrate/migrations/atlas.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (8)
  • openmeter/billing/charges/creditpurchase/chargecreditpurchase.go
  • openmeter/billing/charges/flatfee/service/payment.go
  • openmeter/billing/charges/models/payment/invoiced.go
  • openmeter/billing/charges/service/creditpurchase_test.go
  • openmeter/ent/schema/billing.go
  • openmeter/ent/schema/chargescreditpurchase.go
  • tools/migrate/migrations/20260319114832_charge-creditpurchase-invoiced-payment.down.sql
  • tools/migrate/migrations/20260319114832_charge-creditpurchase-invoiced-payment.up.sql

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.

🧹 Nitpick comments (2)
tools/migrate/migrations/20260319114832_charge-creditpurchase-invoiced-payment.up.sql (2)

36-37: Redundant unique index on primary key column

The id column is already defined as PRIMARY KEY ("id") on line 26, which automatically creates a unique index. This additional unique index on id is redundant and just adds storage overhead and write cost without any benefit.

🧹 Suggested removal
--- create index "chargecreditpurchaseinvoicedpayment_id" to table: "charge_credit_purchase_invoiced_payments"
-CREATE UNIQUE INDEX "chargecreditpurchaseinvoicedpayment_id" ON "charge_credit_purchase_invoiced_payments" ("id");

If this migration is generated from an Ent schema, the fix would be removing the redundant index annotation there.

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

In
`@tools/migrate/migrations/20260319114832_charge-creditpurchase-invoiced-payment.up.sql`
around lines 36 - 37, Remove the redundant unique index creation for the primary
key: delete the CREATE UNIQUE INDEX statement that creates
"chargecreditpurchaseinvoicedpayment_id" on table
"charge_credit_purchase_invoiced_payments" (the "id" column is already declared
PRIMARY KEY and has an implicit unique index), and if this migration was
generated from an Ent schema, remove the redundant index annotation from the
schema so future migrations do not reintroduce
"chargecreditpurchaseinvoicedpayment_id".

30-31: Redundant unique constraints on charge_id

There are two unique indexes involving charge_id:

  • Line 31: UNIQUE INDEX ... ("charge_id") — globally unique
  • Line 41: UNIQUE INDEX ... ("namespace", "charge_id") — unique per namespace

If charge_id is globally unique (line 31), then (namespace, charge_id) is automatically unique too, making line 41 redundant.

If the intent is to have charge_id unique only within a namespace, then line 31 is overly restrictive and should be a non-unique index (for lookup performance) while line 41 provides the actual uniqueness constraint.

Could you clarify the intended constraint? My guess is you want per-namespace uniqueness, which would mean changing line 31 to a regular index.

Also applies to: 40-41

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

In
`@tools/migrate/migrations/20260319114832_charge-creditpurchase-invoiced-payment.up.sql`
around lines 30 - 31, The migration currently creates two conflicting uniqueness
constraints: a globally unique index
charge_credit_purchase_invoiced_payments_charge_id_key on charge_id and a
per-namespace unique index
charge_credit_purchase_invoiced_payments_namespace_charge_id_key on (namespace,
charge_id); if the intended semantics are per-namespace uniqueness, change the
first statement in the diff to create a non-unique index (CREATE INDEX
charge_credit_purchase_invoiced_payments_charge_id_key ON
charge_credit_purchase_invoiced_payments (charge_id)) instead of CREATE UNIQUE
INDEX, so the per-namespace UNIQUE on (namespace, charge_id) remains the actual
constraint and the single-column index still provides lookup performance.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@tools/migrate/migrations/20260319114832_charge-creditpurchase-invoiced-payment.up.sql`:
- Around line 36-37: Remove the redundant unique index creation for the primary
key: delete the CREATE UNIQUE INDEX statement that creates
"chargecreditpurchaseinvoicedpayment_id" on table
"charge_credit_purchase_invoiced_payments" (the "id" column is already declared
PRIMARY KEY and has an implicit unique index), and if this migration was
generated from an Ent schema, remove the redundant index annotation from the
schema so future migrations do not reintroduce
"chargecreditpurchaseinvoicedpayment_id".
- Around line 30-31: The migration currently creates two conflicting uniqueness
constraints: a globally unique index
charge_credit_purchase_invoiced_payments_charge_id_key on charge_id and a
per-namespace unique index
charge_credit_purchase_invoiced_payments_namespace_charge_id_key on (namespace,
charge_id); if the intended semantics are per-namespace uniqueness, change the
first statement in the diff to create a non-unique index (CREATE INDEX
charge_credit_purchase_invoiced_payments_charge_id_key ON
charge_credit_purchase_invoiced_payments (charge_id)) instead of CREATE UNIQUE
INDEX, so the per-namespace UNIQUE on (namespace, charge_id) remains the actual
constraint and the single-column index still provides lookup performance.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 46da1a89-fd18-4c6f-9d7e-09050b6dcf37

📥 Commits

Reviewing files that changed from the base of the PR and between 7a684c9 and 9fcb8da.

⛔ Files ignored due to path filters (1)
  • tools/migrate/migrations/atlas.sum is excluded by !**/*.sum, !**/*.sum
📒 Files selected for processing (1)
  • tools/migrate/migrations/20260319114832_charge-creditpurchase-invoiced-payment.up.sql

@turip turip enabled auto-merge (squash) March 19, 2026 12:10
@turip turip merged commit 6da3779 into main Mar 19, 2026
24 of 25 checks passed
@turip turip deleted the feat/credit-purchase-invoice branch March 19, 2026 12:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants