Skip to main content

Database Schema

The commerce domain uses four primary tables: orders, users_plans, payments, and shipping_notes. Each represents a distinct layer of the transaction lifecycle.


Entity Relationships


Orders

An order is the root purchase entity. It is normally created before the user pays. The subscription flow for Yuno is the exception: there, the order is created inside the recurring payment service after the user completes the Yuno checkout. The Yuno one-off (OTT) flow follows the standard sequence and creates the order before the payment call. See Yuno Checkout Flow.

Key columns:

ColumnDescription
uuidPublic identifier used in URLs and session storage
gateway_typePayment source: stripe, mercadopago, yuno, payu, manual, bulk, external, lti, saml, totalDiscountCoupon
gateway_keyGateway's subscription or charge identifier; set after payment completes
typeinternal, permission, report, internal_report, sale. Gates whether a payment row is ever created: sale and report types do, internal and permission do not
statuspending, approved, paused, cancelled, refunded, expired, error, dispute_lost
sandboxSet at creation from the gateway environment; never changes after that
shipping_informationJSON snapshot of carrier, address, and price for physical orders
amountStored as a string in the currency's main unit (not cents). See "Amount units" below

Rules:

  • An order always has at least one UserPlan. Multiple UserPlan rows represent a multi-item cart.
  • sandbox is immutable after creation. A live order cannot become a sandbox order and vice versa.
  • Duplicate gateway_key entries are blocked at the model level.
  • Pending orders older than 6 months are automatically pruned. Associated UserPlan rows are deleted first.

Historical records may carry gateway_type = 'paypal'. PayPal is a deprecated gateway; see Deprecated gateways.


User Plans (users_plans)

A UserPlan row is one line item in an order: the binding between a user, a plan or product, and the billing terms. The table name users_plans is a legacy name; the concept is "order line item."

Key columns:

ColumnDescription
order_idParent order
plan_idThe Plan being purchased
issue_idThe Issue the user gets direct access to
plan_typesingle, retail, prepaid, recurring
statuspending, approved, paused, cancelled, expired
valid_from / valid_toAccess window; valid_to is null for perpetual access
intervalmonth or annual; null for one-off types
paused_historyJSON array of pause intervals; used to calculate expected payments

Rules:

  • By convention, plan_type = single and plan_type = retail are paired with an issue_id so the line item grants access to a specific piece of content. The pairing is not enforced by a database or application constraint: a derived assigns_issue flag is true only when both conditions hold, but issue_id can be null and no save will fail.
  • plan_type = recurring is the only cancellable and pausable type. One-off types (single, prepaid, retail) are neither.
  • valid_to semantics:
    • single: always null (access is perpetual).
    • prepaid: fixed date set at creation.
    • recurring: null while active; set when the subscription ends.
  • Paused periods are tracked so that payment-count checks don't incorrectly flag a paused subscription as behind on payments.

Payments

Each row is an immutable financial event. Financial fields are never mutated after insert; the table is treated as an append-only ledger. See Payment Ledger Model for the full explanation.

Key columns:

ColumnDescription
order_idParent order
user_plan_idAssociated UserPlan; null for shipping payments
gateway_idFK to the gateways table row, distinct from gateway_type
gateway_typeString identifier of the payment source (mirrors the Order's value)
gateway_transaction_idGateway's transaction identifier
gateway_keyGateway's identifier for the operation (subscription/preapproval for recurring, payment id for one-offs)
gateway_statusRaw status string returned by the gateway, before mapping to the platform status
statuspending, approved, cancelled, refunded, error, dispute_lost
plan_typeMirrors the UserPlan type; shipping for carrier-cost rows
sale_typeretail, subscription, external, shipping
recurring_cycle1-based cycle counter; null for one-off payments
payment_payloadJSON field for additional event metadata; see the Glossary entry for the schema (refunds store the source payment id here)
deleted_atSoft delete timestamp; rows are soft-deleted, not hard-deleted

Rules:

  • Financial fields are not mutated after insert. The same gateway event delivered twice produces one row, not two.
  • status is part of the idempotency key. A pending and an approved event for the same transaction are two separate rows.
  • Refunds are new rows with status = 'refunded'; the original approved row is never modified. The id of the row being refunded lives inside payment_payload (see the Glossary entry for payment_payload).
  • user_plan_id = null with plan_type = 'shipping' marks a carrier-cost row with no plan backing. See Shipping Payment.
  • recurring_cycle on a shipping row is resolved by proximity to the companion product payment for the same transaction (matched by timestamp).
  • The table uses soft deletes. Queries that need to see deleted rows must call withTrashed().

Amount units

Amount columns on payments are integers in the currency's subunit (cents):

  • amount_in_cents, tax_in_cents, gross_sale_in_cents, gross_sale_in_usd_cents, net_sale_*_in_cents, cogs_in_cents, gateway_fee_in_cents, and similar *_in_cents fields.

In contrast, orders.amount is stored as a string in the currency's main unit. When mixing the two in a calculation, convert explicitly: a 10-USD order amount is "10.00", not 1000. Zero-decimal currencies (CLP, PYG) require special handling (see How To Add Support For A New Currency).


Shipping Notes

A shipping note represents one physical shipment. Annual subscriptions generate 12 notes over 12 months; monthly subscriptions generate 1 per cycle.

Key columns:

ColumnDescription
order_idParent order
payment_idThe shipping payment row that triggered this shipment. Nullable; rows created before the November 2025 migration may have null payment_id
carrier_idSelected carrier
statuspending, order-generated, in-transit, pickup, delivered, completed, cancelled
shipping_informationSnapshot of address and carrier details at creation time
period_lengthTotal shipments for the subscription cycle: 1 (monthly) or 12 (annual)
shipment_numberPosition in the cycle: 1 to 12

Rules:

  • Shipment #1 is created synchronously when the shipping payment is approved.
  • Shipments #2–#12 for annual subscriptions are created one per month by a background job. The job checks whether the previous shipment is at least 27 days old before creating the next.
  • shipping_information is a snapshot. Address changes after creation do not affect existing notes.
  • A notification is dispatched to the user after each note is created.

Relationship summary

FromCardinalityToNotes
orders1:N (min 1)users_plansOne per cart line item
orders1:NpaymentsAll payment events for this order
orders1:Nshipping_notesPhysical orders only
users_plansN:1plansRequired
users_plansN:0..1issuesRequired for single and retail types
users_plans1:NpaymentsOne per billing cycle
paymentsN:0..1users_plansNull for shipping payments
payments1:Nshipping_notesShipping payments only
shipping_notesN:1shipping_carriersRequired

X

Graph View