Skip to Content
FeaturesModulesPaymentsData Model

Data Model

Two models, both in the cfg_payments app label.

Amounts are always integer minor units (cents). amount = 1000 with currency = "usd" means $10.00. This keeps the model provider-agnostic and free of floating-point drift.


Payment

One provider-neutral payment attempt. Table: cfg_payments_payment, ordered by -created_at.

Status lifecycle

Payment.Status is a TextChoices enum:

ValueLabel
pendingPending
processingProcessing
requires_actionRequires action
succeededSucceeded
failedFailed
refundedRefunded
cancelledCancelled

A payment is created pending, moves to processing once the provider intent exists, and settles into succeeded / failed / refunded via webhook or reconciliation.

Fields

FieldTypeNotes
idUUIDFieldPrimary key, uuid4, non-editable.
short_idCharField(24)Human/lookup id, auto-set to pay_XXXXXX on save. Indexed.
ownerFK → owner modelThe billable party (see ownership seam). related_name="cfg_payments".
reference_kindCharField(32)What was paid for, in your vocabulary (e.g. "order"). Blank for standalone payments.
reference_idCharField(64)The loose reference id (e.g. "ord_abc123").
created_byFK → AUTH_USER_MODELNullable; who initiated it. SET_NULL on delete.
providerCharField(32)e.g. "stripe".
external_idCharField(128)Provider intent/charge id (Stripe PaymentIntent pi_…). Indexed.
statusCharField(20)One of the status values above. Default pending.
amountBigIntegerFieldAuthoritative amount in minor units.
currencyCharField(8)ISO currency, default "usd".
amount_refundedBigIntegerFieldMinor units refunded so far. Default 0.
idempotency_keyCharField(128)Unique. Client→server dedupe for double-clicked checkout.
created_atDateTimeFieldauto_now_add.
updated_atDateTimeFieldauto_now.

Constraints & indexes

  • Unique (provider, external_id) where external_id is set — one payment row per provider intent.
  • Unique idempotency_key.
  • Indexes on (owner, status), (short_id), and (reference_kind, reference_id).

Payment inherits a truncating mixin whose save() clips every CharField to its max_length first — external provider strings can’t blow up a save with a right-truncation error.

The ownership seam

Payment.owner targets the model named by CFG_PAYMENTS_OWNER_MODEL (emitted from PaymentsConfig.owner_model), falling back to AUTH_USER_MODEL. Point it at your org model for org-level billing — and remember to set owner_resolver when it’s not the user model (see Configuration).

The reference pair

(reference_kind, reference_id) records what a payment was for without a foreign key. The engine never imports your order/plan models; you interpret the pair on your side (e.g. in the fulfillment_hook). Both are blank for standalone payments and top-ups.


PaymentEvent

Inbound provider webhook log and the idempotency guard. Table: cfg_payments_event, ordered by -received_at.

Fields

FieldTypeNotes
idUUIDFieldPrimary key.
providerCharField(32)e.g. "stripe".
event_idCharField(128)Unique. Provider event id (Stripe evt_…).
event_typeCharField(64)Provider event type.
paymentFK → PaymentNullable; linked once resolved. related_name="events".
payloadJSONFieldRaw event stored verbatim for audit/debug and replay.
received_atDateTimeFieldauto_now_add.
processed_atDateTimeFieldSet when handling completes; null until then.
errorTextFieldSet when a handler raised, kept for retry/debug.

Idempotency

The event_id unique constraint is the webhook idempotency mechanism. The handler does a get_or_create(event_id=…); a replayed delivery of the same Stripe event short-circuits instead of double-applying. The raw payload is kept so an event can be replayed after a handler bug is fixed.

An index on (provider, event_type) supports filtering the event log.

Last updated on