payable payable Docs
Coming soon

Domain Model

The domain model is the set of TypeScript interfaces in src/domain/entities/. Entities are plain, fully readonly data contracts: they hold no methods and no behavior. Behavior lives in value objects, state machines, and the application layer. Persisted shapes and provider identifiers are part of the entity; invariants and transitions are enforced elsewhere.

Every entity field is declared readonly, so an entity instance is never mutated in place; state changes produce new records.

Shared building blocks

These mixins live in src/domain/entities/common.ts and are composed into the entities below.

TypeFieldsPurpose
TimestampscreatedAt: Date, updatedAt: DateCreation and last-update instants.
TenantScopedtenantId: string | nullMulti-tenant scoping. null means the record is not bound to a tenant.
StoredMoneyamount: number, currency: CurrencyCodePersisted money shape (minor units + currency code). See Value Objects for the Money behavior.
RecurringInterval'day' | 'week' | 'month' | 'year'Billing interval unit for recurring prices.
MetadataRecord<string, string>Free-form string key/value bag.

Monetary amounts on entities (total, amountPaid, amountDue, amount, unitAmount, refundedAmount) are plain number values expressed in minor units (cents for two-decimal currencies). They are never floats representing major units. The currency: CurrencyCode field on the same entity tells you how to interpret them. See Value Objects for the no-floats rule and the Money helper that wraps these stored amounts.

Entity reference diagram

erDiagram
  CUSTOMER ||--o{ SUBSCRIPTION : has
  CUSTOMER ||--o{ INVOICE : billed
  CUSTOMER ||--o{ PAYMENT : pays
  SUBSCRIPTION ||--o{ SUBSCRIPTION_ITEM : contains
  SUBSCRIPTION ||--o{ INVOICE : generates
  SUBSCRIPTION }o--|| PRICE : "priced by"
  SUBSCRIPTION_ITEM }o--|| PRICE : "priced by"
  PRODUCT ||--o{ PRICE : offers
  PAYMENT ||--o{ REFUND : "refunded by"

  CUSTOMER {
    string id PK
    string provider
    string providerCustomerId
    string billableType
    string billableId
    string email
    string name
    string tenantId
  }
  SUBSCRIPTION {
    string id PK
    string customerId FK
    string status
    string priceId FK
    number quantity
    date trialEndsAt
    date endsAt
  }
  SUBSCRIPTION_ITEM {
    string id PK
    string subscriptionId FK
    string priceId FK
    number quantity
  }
  INVOICE {
    string id PK
    string customerId FK
    string subscriptionId FK
    string status
    string currency
    number total
    number amountPaid
    number amountDue
  }
  PAYMENT {
    string id PK
    string customerId FK
    string status
    string currency
    number amount
    number refundedAmount
  }
  REFUND {
    string id PK
    string paymentId FK
    string status
    string currency
    number amount
  }
  PRODUCT {
    string id PK
    string name
    boolean active
  }
  PRICE {
    string id PK
    string productId FK
    string currency
    number unitAmount
    string interval
    boolean active
  }

Relationships are expressed by foreign-key string fields (customerId, subscriptionId, priceId, productId, paymentId). There are no embedded references; entities only carry the id of related records.

Customer

src/domain/entities/customer.entity.ts. Extends TenantScoped, Timestamps.

Purpose: links a host-application billable record (the thing being charged, identified by billableType + billableId) to a billing provider customer.

FieldTypeNotes
idstringLocal identifier.
providerstringBilling provider (e.g. stripe, paddle).
providerCustomerIdstring | nullCustomer id on the provider; null before provisioning.
billableTypestringHost-side type discriminator.
billableIdstringHost-side record id.
emailstringCustomer email.
namestring | nullOptional display name.
metadataMetadata | nullOptional string key/value bag.

Relationships: owns many Subscription, Invoice, and Payment records (each references customerId). On Payment the link is customerId: string | null, so a payment can exist without a customer.

Invariants (enforced outside the entity): the (billableType, billableId) pair identifies the host billable; providerCustomerId is populated once the customer is provisioned with the provider.

Subscription

src/domain/entities/subscription.entity.ts. Extends TenantScoped, Timestamps.

Purpose: a recurring billing agreement for a customer against a price.

FieldTypeNotes
idstringLocal identifier.
customerIdstringOwning customer.
namestringSubscription name/type.
providerstringBilling provider.
providerSubscriptionIdstring | nullSubscription id on the provider.
statusSubscriptionStatusOne of the values in subscription-status.
priceIdstring | nullPrimary price reference.
quantitynumberSeat/unit count.
trialEndsAtDate | nullTrial end instant.
endsAtDate | nullCancellation/grace-period end instant.
currentPeriodStartDate | nullCurrent billing period start.
currentPeriodEndDate | nullCurrent billing period end.

Relationships: belongs to one Customer; contains many SubscriptionItem; may generate Invoice records (Invoice.subscriptionId); references a Price via priceId.

Lifecycle: status is governed by the Subscription state machine. The date fields (trialEndsAt, endsAt) drive the lifecycle predicates below.

Subscription state predicates

src/domain/entities/subscription-state.ts. Three pure functions read a Subscription plus an explicit now: Date and return a boolean. They compare epoch milliseconds via getTime().

PredicateReturns true whenExact logic
onTrial(subscription, now)The trial is still running.trialEndsAt !== null && trialEndsAt.getTime() > now.getTime()
onGracePeriod(subscription, now)The subscription has a future end date (canceled but not yet expired).endsAt !== null && endsAt.getTime() > now.getTime()
subscriptionEnded(subscription, now)The end date has passed (or is exactly now).endsAt !== null && endsAt.getTime() <= now.getTime()

Notes:

  • onTrial uses a strict > comparison, so the exact trialEndsAt instant is no longer “on trial”.
  • onGracePeriod and subscriptionEnded are complementary across endsAt: with a non-null endsAt, exactly one is true for any given now (the boundary instant counts as ended, not grace).
  • All three return false when the relevant date is null.

Subscription Item

src/domain/entities/subscription-item.entity.ts. Extends Timestamps only (not tenant-scoped; it inherits tenancy through its parent subscription).

Purpose: a single priced line on a subscription, enabling multi-price subscriptions.

FieldTypeNotes
idstringLocal identifier.
subscriptionIdstringOwning subscription.
priceIdstringPrice for this line.
providerItemIdstring | nullItem id on the provider.
quantitynumberUnit count for this line.

Relationships: belongs to one Subscription; references one Price.

Invoice

src/domain/entities/invoice.entity.ts. Extends TenantScoped, Timestamps.

Purpose: a billing document for a customer, optionally tied to a subscription.

FieldTypeNotes
idstringLocal identifier.
customerIdstringBilled customer.
subscriptionIdstring | nullSource subscription, if any.
providerstringBilling provider.
providerInvoiceIdstring | nullInvoice id on the provider.
statusInvoiceStatusOne of the values in invoice-status.
currencyCurrencyCodeCurrency of the amounts below.
totalnumberInvoice total, minor units.
amountPaidnumberAmount paid so far, minor units.
amountDuenumberOutstanding amount, minor units.
numberstring | nullHuman-facing invoice number.
hostedInvoiceUrlstring | nullProvider-hosted invoice URL.
invoicePdfstring | nullPDF URL.

Relationships: belongs to one Customer; optionally belongs to one Subscription.

Lifecycle: status is governed by the Invoice state machine. Amount fields are minor-unit integers interpreted by currency.

Payment

src/domain/entities/payment.entity.ts. Extends TenantScoped, Timestamps.

Purpose: a charge against a provider, optionally attributed to a customer.

FieldTypeNotes
idstringLocal identifier.
customerIdstring | nullCustomer, if known.
providerstringBilling provider.
providerPaymentIdstring | nullPayment id on the provider.
statusPaymentStatusOne of the values in payment-status.
currencyCurrencyCodeCurrency of the amounts below.
amountnumberCharge amount, minor units.
refundedAmountnumberTotal refunded so far, minor units.
referencestring | nullExternal reference.
descriptionstring | nullFree-text description.

Relationships: optionally belongs to one Customer; refunded by many Refund records (each references paymentId).

Lifecycle: status is governed by the Payment state machine. refundedAmount tracks cumulative refunds; the partially_refunded and refunded payment states correspond to partial vs. full refunds.

Refund

src/domain/entities/refund.entity.ts. Extends TenantScoped, Timestamps.

Purpose: a refund issued against a payment.

FieldTypeNotes
idstringLocal identifier.
paymentIdstringPayment being refunded.
providerstringBilling provider.
providerRefundIdstring | nullRefund id on the provider.
statusRefundStatusOne of the values in refund-status.
currencyCurrencyCodeCurrency of amount.
amountnumberRefund amount, minor units.
reasonstring | nullOptional reason.

Relationships: belongs to one Payment (required paymentId).

Lifecycle: status is governed by the Refund state machine.

Product

src/domain/entities/product.entity.ts. Extends TenantScoped, Timestamps.

Purpose: a sellable product that prices attach to.

FieldTypeNotes
idstringLocal identifier.
providerstringBilling provider.
providerProductIdstring | nullProduct id on the provider.
namestringProduct name.
descriptionstring | nullOptional description.
activebooleanWhether the product is sellable.
metadataMetadata | nullOptional string key/value bag.

Relationships: offers many Price records (each references productId).

Price

src/domain/entities/price.entity.ts. Extends TenantScoped, Timestamps.

Purpose: a specific price (one-off or recurring) for a product.

FieldTypeNotes
idstringLocal identifier.
providerstringBilling provider.
providerPriceIdstring | nullPrice id on the provider.
productIdstringOwning product.
currencyCurrencyCodeCurrency of unitAmount.
unitAmountnumberUnit price, minor units.
intervalRecurringInterval | nullBilling interval; null for one-off prices.
intervalCountnumber | nullNumber of intervals per billing cycle.
activebooleanWhether the price is usable.

Relationships: belongs to one Product; referenced by Subscription.priceId and SubscriptionItem.priceId.

Notes: a recurring price sets both interval and intervalCount; a one-off price leaves both null.

Webhook Event

src/domain/entities/webhook-event.entity.ts. Extends TenantScoped (note: not Timestamps - it carries its own receivedAt/processedAt fields).

Purpose: a received provider webhook, persisted for idempotent processing and reconciliation.

WebhookEventStatus = 'pending' | 'processed' | 'failed'.

FieldTypeNotes
idstringLocal identifier.
providerstringSource provider.
providerEventIdstringEvent id on the provider (used for idempotency).
typestringRaw provider event type.
normalizedTypestring | nullCanonical event type, once mapped.
payloadstringRaw payload string.
dataRecord<string, unknown>Parsed payload.
headersRecord<string, string>Request headers.
statusWebhookEventStatusProcessing status.
correlationIdstringCorrelation id for tracing.
receivedAtDateReceipt instant.
processedAtDate | nullProcessing instant; null until processed.

Invariants (enforced outside the entity): (provider, providerEventId) uniquely identifies an event, supporting idempotent webhook handling. See Value Objects for IdempotencyKey.forWebhook.

Audit Log

src/domain/entities/audit-log.entity.ts. Extends TenantScoped (carries its own createdAt, not Timestamps).

Purpose: an immutable record of a mutation to a domain resource, for audit and traceability.

FieldTypeNotes
idstringLocal identifier.
correlationIdstringCorrelation id linking related actions.
actorTypestring | nullWho acted (type).
actorIdstring | nullWho acted (id).
actionstringAction performed.
resourceTypestringAffected resource type.
resourceIdstringAffected resource id.
beforeRecord<string, unknown> | nullState before the change.
afterRecord<string, unknown> | nullState after the change.
metadataRecord<string, unknown> | nullExtra context.
ipAddressstring | nullOrigin IP.
userAgentstring | nullOrigin user agent.
createdAtDateWhen the entry was written.

Notes: before/after capture the diff of the audited mutation; correlationId ties the entry to the request and to related webhook events.