Skip to content

Feat: record externally calculated charges as receivables for later invoicing #1252

Description

@a1x-an

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:

  1. claim each eligible unbilled item for exactly one invoice;
  2. apply available prepaid credit exactly once and persist that allocation;
  3. place only the uncovered amount into invoice_amount_due;
  4. 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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions