Context
Some integrations already know the final monetary amount of a completed service. They do not need Meteroid to calculate a price, they need a supported way to register that amount before invoice generation.
This is a companion design discussion to the single signed pay-as-you-go balance proposal (Issue #1251). My own integration uses that simpler model and does not require receivables or invoice aggregation. I am documenting this alternative separately because it may fit Meteroid users who retain non-negative prepaid credit while recording debt for services that have already been delivered.
In this model, prepaid credit does not become negative. A negative customer position is derived from unsettled receivables and invoices.
Possible accounting model
The model would track three distinct amounts:
- unapplied prepaid credit;
- receivables that have been recorded but not yet included in an invoice;
- finalized invoice amounts still due.
The signed customer position could be derived as:
net_customer_position =
unapplied_prepaid_credit
- unbilled_receivables
- invoice_amount_due
Recording a completed-service charge increases unbilled_receivables. Moving that charge into an invoice transfers it from one debt category to another, it must not reduce net_customer_position a second time.
Example
A customer has 100.00 in prepaid credit. A completed service produces an externally calculated charge of 130.00.
| Stage |
Prepaid credit |
Unbilled receivables |
Invoice due |
Net position |
| Before the charge |
100.00 |
0.00 |
0.00 |
+100.00 |
| Charge recorded |
100.00 |
130.00 |
0.00 |
-30.00 |
| Invoice finalized and credit applied |
0.00 |
0.00 |
30.00 |
-30.00 |
| Invoice paid |
0.00 |
0.00 |
0.00 |
0.00 |
The central invariant is visible in the middle two rows: invoice finalization changes the classification of the debt, but preserves the -30.00 net position.
Receivable ingestion
Meteroid could expose an operation such as CreateExternalBillableItem for an exact monetary charge calculated by another service.
Each item would contain:
- the customer, amount, and currency;
- a durable external charge ID scoped to the tenant;
- occurrence time, description, source metadata, and actor;
- its current settlement state;
- references to any invoice line, reversal, credit allocation, payment, or credit note.
The external ID identifies this financial item rather than the service job as a whole. Retrying the same ID with the same material payload should return the existing item. Reusing it with different material data should return a conflict. These guarantees must survive process restarts and concurrent API replicas.
The item is immutable after creation. Corrections are represented by new idempotent reversal or adjustment records that reference the original item.
A completed-service charge remains recorded in full when prepaid credit is insufficient. A credit limit may affect authorization of future service, but must not truncate or reject debt for service already delivered.
Invoice and settlement lifecycle
When an invoice is generated, Meteroid would:
- claim each eligible unbilled item for exactly one invoice;
- apply available prepaid credit exactly once and persist that allocation;
- place only the uncovered amount into
invoice_amount_due;
- preserve the same
net_customer_position throughout finalization.
A possible lifecycle is unbilled -> invoiced -> partially_paid -> settled, with explicit reversed or credited outcomes. A correction before invoicing can reverse the billable item, after invoicing it can use a credit note or invoice adjustment. Retrying generation, finalization, payment reconciliation, or correction must not duplicate any financial movement.
Credit exposure
The derived position could be returned together with a credit limit and an available amount:
available_to_spend = net_customer_position + credit_limit
Crossing zero, a warning threshold, or the credit limit could produce a queryable state and an outbox or webhook event. Decisions about future service remain in the calling application.
Invariants to preserve
- One external item can be linked to at most one invoice line.
- Applying prepaid credit is represented once and can be reconciled.
- Invoice finalization never repeats the original charge.
- Partial payment, full payment, reversal, and credit note all preserve the derived position.
- Every transition is traceable from the external charge through the invoice and settlement records.
Context
Some integrations already know the final monetary amount of a completed service. They do not need Meteroid to calculate a price, they need a supported way to register that amount before invoice generation.
This is a companion design discussion to the single signed pay-as-you-go balance proposal (Issue #1251). My own integration uses that simpler model and does not require receivables or invoice aggregation. I am documenting this alternative separately because it may fit Meteroid users who retain non-negative prepaid credit while recording debt for services that have already been delivered.
In this model, prepaid credit does not become negative. A negative customer position is derived from unsettled receivables and invoices.
Possible accounting model
The model would track three distinct amounts:
The signed customer position could be derived as:
Recording a completed-service charge increases
unbilled_receivables. Moving that charge into an invoice transfers it from one debt category to another, it must not reducenet_customer_positiona second time.Example
A customer has
100.00in prepaid credit. A completed service produces an externally calculated charge of130.00.The central invariant is visible in the middle two rows: invoice finalization changes the classification of the debt, but preserves the
-30.00net position.Receivable ingestion
Meteroid could expose an operation such as
CreateExternalBillableItemfor an exact monetary charge calculated by another service.Each item would contain:
The external ID identifies this financial item rather than the service job as a whole. Retrying the same ID with the same material payload should return the existing item. Reusing it with different material data should return a conflict. These guarantees must survive process restarts and concurrent API replicas.
The item is immutable after creation. Corrections are represented by new idempotent reversal or adjustment records that reference the original item.
A completed-service charge remains recorded in full when prepaid credit is insufficient. A credit limit may affect authorization of future service, but must not truncate or reject debt for service already delivered.
Invoice and settlement lifecycle
When an invoice is generated, Meteroid would:
invoice_amount_due;net_customer_positionthroughout finalization.A possible lifecycle is
unbilled -> invoiced -> partially_paid -> settled, with explicit reversed or credited outcomes. A correction before invoicing can reverse the billable item, after invoicing it can use a credit note or invoice adjustment. Retrying generation, finalization, payment reconciliation, or correction must not duplicate any financial movement.Credit exposure
The derived position could be returned together with a credit limit and an available amount:
Crossing zero, a warning threshold, or the credit limit could produce a queryable state and an outbox or webhook event. Decisions about future service remain in the calling application.
Invariants to preserve