Traits / Billable
Billable
Recurring billing mechanics trait that normalizes cycle tracking, payment timing, and period management for subscription-based entities. Billable owns the billing CYCLE — when payments occur, how periods progress, and what the payment state is. DISTINCT from Priceable — Priceable handles PRICING (amount, currency, models). Billable handles RECURRENCE (cycles, periods, payment timing, proration). A Subscription composes both: Priceable for "how much" and Billable for "when/how". Core concepts: - Billing Cycle: current_period_start → current_period_end with progress tracking - Payment Timing: prepaid (charge at period start) vs postpaid (charge at period end) - Proration: fractional billing when mid-cycle changes occur, with preview/commit timing (proration_date), a charge-timing policy (prorationBehavior), and billing-anchor-reset disclosure - Payment Status: tracks the outcome of the most recent collection attempt - Cycle Anchoring: fixed day-of-month for consistent renewal dates PRORATION SCOPE (trait-split gap): proration semantics live ONLY on this CORE financial/Billable trait. The saas-billing domain trait SaaSBillingBillable (domains/saas-billing/traits/billable.trait.yaml) does NOT expose proration, so a Subscription composing only SaaSBillingBillable silently CANNOT prorate. To enable proration for a SaaS subscription, compose financial/Billable (as the core Subscription object does, alias SubscriptionBilling).
Generated from @oods/foundry 0.10.1
- Group
- financial
- Maturity
- experimental
- Contexts
- card, detail, form, list, timeline
Fields it adds
| Field | Type | Required | Description |
|---|---|---|---|
amount | integer | yes | Recurring price expressed in minor currency units (e.g., cents). |
currency | string | yes | ISO 4217 currency code for the billing amount. |
billing_interval | string | yes | Recurrence cadence label selected from the billingIntervals parameter. |
payment_status | string (pending, succeeded, failed, retrying, refunded) | no | Outcome of the most recent collection attempt. Distinct from subscription lifecycle status — a subscription can be "active" with payment_status "retrying". |
payment_method_type | string (card, ach, wire, invoice, other) | no | Payment instrument category used for collection. |
proration_amount | integer | no | Prorated credit or charge in minor currency units generated by a mid-cycle plan change. Positive values are charges; negative values are credits. Only populated when the supportProration parameter is true. |
proration_date | integer | no | Unix timestamp (epoch SECONDS) at which proration is calculated. Pinned when an upcoming-invoice PREVIEW is requested and MUST be passed identically on commit so the committed charge equals the previewed amount. The preview is read-only and does NOT mutate the subscription. Stored as an integer (not a datetime) so the value round-trips byte-identically between preview and commit — an ISO string normalization could shift it and desync the previewed vs charged amount. Only populated when supportProration is true. Source: docs.stripe.com/billing/subscriptions/prorations. |
last_payment_at | datetime | no | Timestamp of the most recent successful payment. |
next_payment_due_at | datetime | no | Timestamp when the next payment attempt is scheduled. |
current_period_start | datetime | no | Beginning timestamp of the active billing cycle. |
current_period_end | datetime | no | Ending timestamp of the active billing cycle. |
current_period_progress | number | no | Decimal (0.0–1.0) indicating progress through the active billing cycle. |
What it shows in each context
Parameters
defaultCurrencystring- ISO 4217 currency code used when a value is not explicitly provided.
billingIntervalsstring[]- Allowed billing interval values that downstream objects may select.
minorUnitsnumber- Number of fractional units that constitute a single major currency unit (e.g., 100 for cents).
paymentTimingstring- Whether the billing entity charges at the start of the period (prepaid) or at the end (postpaid). Affects cycle progress interpretation and dunning trigger points.
supportProrationboolean- Whether mid-cycle plan changes generate prorated charges or credits. When true, proration_amount is computed on plan upgrades/downgrades.
prorationBehaviorstring- Default policy for how mid-cycle plan changes are billed, mirroring Stripe's proration_behavior on subscription updates. Drives charge TIMING, not the proration math, and is only meaningful when supportProration is true: - create_prorations: generate prorated line items, billed on the next invoice (default) - always_invoice: generate prorations AND invoice them immediately - none: apply the change with no proration line items / no immediate adjustment Source: docs.stripe.com/billing/subscriptions/prorations.
cycleAnchorDaynumber- Day of the month used to anchor renewal dates. When set, all billing periods align to this day regardless of subscription start date. Value of 0 means no anchoring (period starts from subscription creation). BILLING-ANCHOR-RESET DISCLOSURE: changing the billing interval — or otherwise resetting the billing cycle anchor (e.g. Stripe billing_cycle_anchor=now) — RESETS the anchor and starts a new billing period immediately. This interacts with proration: the reset can itself trigger proration for the current period and shifts the renewal date. Surface this consequence to the user BEFORE applying an interval change. Source: docs.stripe.com/billing/subscriptions/prorations.
Objects that use it
The trait file
traits/financial/Billable.trait.yaml
trait:
name: Billable
version: 2.1.0
description: |
Recurring billing mechanics trait that normalizes cycle tracking, payment timing,
and period management for subscription-based entities. Billable owns the billing
CYCLE — when payments occur, how periods progress, and what the payment state is.
DISTINCT from Priceable — Priceable handles PRICING (amount, currency, models).
Billable handles RECURRENCE (cycles, periods, payment timing, proration).
A Subscription composes both: Priceable for "how much" and Billable for "when/how".
Core concepts:
- Billing Cycle: current_period_start → current_period_end with progress tracking
- Payment Timing: prepaid (charge at period start) vs postpaid (charge at period end)
- Proration: fractional billing when mid-cycle changes occur, with preview/commit
timing (proration_date), a charge-timing policy (prorationBehavior), and
billing-anchor-reset disclosure
- Payment Status: tracks the outcome of the most recent collection attempt
- Cycle Anchoring: fixed day-of-month for consistent renewal dates
PRORATION SCOPE (trait-split gap): proration semantics live ONLY on this CORE
financial/Billable trait. The saas-billing domain trait SaaSBillingBillable
(domains/saas-billing/traits/billable.trait.yaml) does NOT expose proration, so a
Subscription composing only SaaSBillingBillable silently CANNOT prorate. To enable
proration for a SaaS subscription, compose financial/Billable (as the core
Subscription object does, alias SubscriptionBilling).
category: financial
tags:
- billing
- recurring
- financial
- cycle
- payment
- subscription
parameters:
- name: defaultCurrency
type: string
required: false
description: ISO 4217 currency code used when a value is not explicitly provided.
default: usd
validation:
pattern: "^[a-zA-Z]{3}$"
- name: billingIntervals
type: string[]
required: false
description: Allowed billing interval values that downstream objects may select.
default:
- monthly
- quarterly
- annual
- name: minorUnits
type: number
required: false
description: Number of fractional units that constitute a single major currency unit (e.g., 100 for cents).
default: 100
validation:
minimum: 1
- name: paymentTiming
type: string
required: false
description: |
Whether the billing entity charges at the start of the period (prepaid)
or at the end (postpaid). Affects cycle progress interpretation and
dunning trigger points.
default: prepaid
validation:
enum:
- prepaid
- postpaid
- name: supportProration
type: boolean
required: false
description: |
Whether mid-cycle plan changes generate prorated charges or credits.
When true, proration_amount is computed on plan upgrades/downgrades.
default: false
- name: prorationBehavior
type: string
required: false
description: |
Default policy for how mid-cycle plan changes are billed, mirroring Stripe's
proration_behavior on subscription updates. Drives charge TIMING, not the
proration math, and is only meaningful when supportProration is true:
- create_prorations: generate prorated line items, billed on the next invoice (default)
- always_invoice: generate prorations AND invoice them immediately
- none: apply the change with no proration line items / no immediate adjustment
Source: docs.stripe.com/billing/subscriptions/prorations.
default: create_prorations
validation:
enum:
- create_prorations
- always_invoice
- none
- name: cycleAnchorDay
type: number
required: false
description: |
Day of the month used to anchor renewal dates. When set, all billing
periods align to this day regardless of subscription start date.
Value of 0 means no anchoring (period starts from subscription creation).
BILLING-ANCHOR-RESET DISCLOSURE: changing the billing interval — or otherwise
resetting the billing cycle anchor (e.g. Stripe billing_cycle_anchor=now) — RESETS
the anchor and starts a new billing period immediately. This interacts with
proration: the reset can itself trigger proration for the current period and shifts
the renewal date. Surface this consequence to the user BEFORE applying an interval
change. Source: docs.stripe.com/billing/subscriptions/prorations.
default: 0
validation:
minimum: 0
maximum: 31
schema:
amount:
type: integer
required: true
description: Recurring price expressed in minor currency units (e.g., cents).
validation:
minimum: 0
currency:
type: string
required: true
description: ISO 4217 currency code for the billing amount.
validation:
pattern: "^[a-zA-Z]{3}$"
billing_interval:
type: string
required: true
description: Recurrence cadence label selected from the billingIntervals parameter.
validation:
enumFromParameter: billingIntervals
payment_status:
type: string
required: false
description: |
Outcome of the most recent collection attempt. Distinct from subscription
lifecycle status — a subscription can be "active" with payment_status "retrying".
validation:
enum:
- pending
- succeeded
- failed
- retrying
- refunded
default: pending
payment_method_type:
type: string
required: false
description: Payment instrument category used for collection.
validation:
enum:
- card
- ach
- wire
- invoice
- other
proration_amount:
type: integer
required: false
description: |
Prorated credit or charge in minor currency units generated by a mid-cycle
plan change. Positive values are charges; negative values are credits.
Only populated when the supportProration parameter is true.
default: 0
proration_date:
type: integer
required: false
description: |
Unix timestamp (epoch SECONDS) at which proration is calculated. Pinned when an
upcoming-invoice PREVIEW is requested and MUST be passed identically on commit so the
committed charge equals the previewed amount. The preview is read-only and does NOT
mutate the subscription. Stored as an integer (not a datetime) so the value round-trips
byte-identically between preview and commit — an ISO string normalization could shift it
and desync the previewed vs charged amount. Only populated when supportProration is true.
Source: docs.stripe.com/billing/subscriptions/prorations.
validation:
minimum: 0
last_payment_at:
type: datetime
required: false
description: Timestamp of the most recent successful payment.
next_payment_due_at:
type: datetime
required: false
description: Timestamp when the next payment attempt is scheduled.
current_period_start:
type: datetime
required: false
description: Beginning timestamp of the active billing cycle.
current_period_end:
type: datetime
required: false
description: Ending timestamp of the active billing cycle.
current_period_progress:
type: number
required: false
description: Decimal (0.0–1.0) indicating progress through the active billing cycle.
validation:
minimum: 0
maximum: 1
semantics:
amount:
semantic_type: billing.cycle.amount_minor
token_mapping: tokenMap(billing.amount.*)
ui_hints:
component: CurrencyAmount
currencyField: currency
minorUnitsParameter: minorUnits
currency:
semantic_type: billing.cycle.currency
token_mapping: tokenMap(billing.currency.*)
ui_hints:
component: CurrencyCode
billing_interval:
semantic_type: billing.cycle.interval
token_mapping: tokenMap(billing.interval.*)
ui_hints:
component: BillingIntervalBadge
parameterSource: billingIntervals
payment_status:
semantic_type: billing.cycle.payment_status
token_mapping: tokenMap(billing.payment.status.*)
ui_hints:
component: PaymentStatusBadge
payment_method_type:
semantic_type: billing.cycle.payment_method
token_mapping: tokenMap(billing.payment.method.*)
ui_hints:
component: PaymentMethodIcon
proration_amount:
semantic_type: billing.cycle.proration
token_mapping: tokenMap(billing.proration.*)
ui_hints:
minorUnitsParameter: minorUnits
component: CurrencyAmount
currencyField: currency
signIndicator: true
proration_date:
# `timestamp` semantic: this field is a Unix-seconds point-in-time, so it groups with
# the temporal/date fields (slot-expander categorizes on the `timestamp` token) rather
# than landing uncategorized in an unrelated slot. ui_hints render it as a timestamp.
semantic_type: billing.cycle.proration_timestamp
token_mapping: tokenMap(billing.timestamp.proration)
ui_hints:
component: Timestamp
unixSeconds: true
last_payment_at:
semantic_type: billing.cycle.last_payment_at
token_mapping: tokenMap(billing.timestamp.last_payment)
ui_hints:
component: RelativeTimestamp
next_payment_due_at:
semantic_type: billing.cycle.next_payment_due_at
token_mapping: tokenMap(billing.timestamp.next_payment)
ui_hints:
component: RelativeTimestamp
urgencyThresholdDays: 3
current_period_start:
semantic_type: billing.cycle.period_start
token_mapping: tokenMap(billing.timestamp.period_start)
ui_hints:
component: Timestamp
current_period_end:
semantic_type: billing.cycle.period_end
token_mapping: tokenMap(billing.timestamp.period_end)
ui_hints:
component: Timestamp
current_period_progress:
semantic_type: billing.cycle.period_progress
token_mapping: tokenMap(billing.cycle.progress)
ui_hints:
component: CycleProgressBar
view_extensions:
list:
- component: BillingSummaryBadge
position: after
props:
amountField: amount
currencyField: currency
intervalField: billing_interval
minorUnitsParameter: minorUnits
detail:
- component: CycleProgressCard
position: main
priority: 70
props:
progressField: current_period_progress
periodStartField: current_period_start
periodEndField: current_period_end
intervalField: billing_interval
- component: PaymentTimeline
position: main
priority: 60
props:
lastPaymentField: last_payment_at
nextPaymentField: next_payment_due_at
paymentStatusField: payment_status
paymentMethodField: payment_method_type
amountField: amount
currencyField: currency
form:
- component: BillingIntervalSelector
position: main
priority: 80
props:
intervalField: billing_interval
intervalsParameter: billingIntervals
- component: BillingAmountInput
position: main
priority: 75
props:
amountField: amount
currencyField: currency
minorUnitsParameter: minorUnits
timeline:
- component: PaymentEventTimeline
props:
lastPaymentField: last_payment_at
nextPaymentField: next_payment_due_at
paymentStatusField: payment_status
amountField: amount
currencyField: currency
card:
- component: BillingCardMeta
position: after
props:
amountField: amount
currencyField: currency
intervalField: billing_interval
minorUnitsParameter: minorUnits
tokens:
# Amount display tokens
cmp.billing.amount.text: "var(--sys-text-strong)"
cmp.billing.amount.currency: "var(--sys-text-subtle)"
cmp.billing.amount.interval: "var(--sys-text-subtle)"
# Cycle progress tokens
cmp.billing.cycle.progress.track: "var(--sys-surface-neutral)"
cmp.billing.cycle.progress.fill: "var(--sys-surface-accent)"
cmp.billing.cycle.progress.text: "var(--sys-text-default)"
cmp.billing.cycle.progress.height: "var(--space-1)"
cmp.billing.cycle.progress.radius: "var(--radius-full)"
# Payment status tokens
cmp.billing.payment.succeeded.bg: "var(--sys-surface-success)"
cmp.billing.payment.succeeded.text: "var(--sys-text-success)"
cmp.billing.payment.failed.bg: "var(--sys-surface-critical)"
cmp.billing.payment.failed.text: "var(--sys-text-critical)"
cmp.billing.payment.retrying.bg: "var(--sys-surface-warning)"
cmp.billing.payment.retrying.text: "var(--sys-text-warning)"
cmp.billing.payment.pending.bg: "var(--sys-surface-info)"
cmp.billing.payment.pending.text: "var(--sys-text-info)"
# Timeline connector tokens
cmp.billing.timeline.connector: "var(--sys-border-default)"
cmp.billing.timeline.dot: "var(--sys-surface-accent)"
cmp.billing.timeline.text: "var(--sys-text-default)"
dependencies: []
# Recommended companions (not required):
# - Priceable: provides pricing metadata (unit amount, pricing model, tax behavior).
# Billable provides billing cycle mechanics. A Subscription composes both.
# - Timestampable: enriches payment timestamps with relative formatting.
metadata:
created: "2025-10-16"
updated: "2026-06-24"
owners:
- billing@oods.systems
- finance@oods.systems
maturity: experimental
accessibility:
keyboard: |
BillingIntervalSelector is keyboard navigable via arrow keys.
BillingAmountInput supports standard numeric input patterns.
screenreader: |
BillingSummaryBadge announces amount, currency, and interval as a single phrase.
CycleProgressCard announces progress percentage and remaining days.
PaymentStatusBadge announces payment outcome with tone-appropriate urgency.
regionsUsed:
- list
- detail
- form
- timeline
- card
examples:
- Subscription
- Plan
references:
- "objects/provenance.v1.json#/references/ref-0d9aaa77f987 — BillableViewData, BillingSummary, PastDueAction"
- "objects/provenance.v1.json#/references/ref-374e980524b1 — CanonicalSubscription, BillingInterval, BillingAccount"
- "domains/saas-billing/traits/billable.trait.yaml — SaaS domain extension"
- "objects/core/Subscription.object.yaml — Subscription object composition"
- "objects/provenance.v1.json#/references/ref-be33a98177f7 — billing domain status tokens"