[Traits](https://oods-foundry.com/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

- [list](https://oods-foundry.com/contexts/list)

  `BillingSummaryBadge`

- [detail](https://oods-foundry.com/contexts/detail)

  `CycleProgressCard`, `PaymentTimeline`

- [form](https://oods-foundry.com/contexts/form)

  `BillingIntervalSelector`, `BillingAmountInput`

- [timeline](https://oods-foundry.com/contexts/timeline)

  `PaymentEventTimeline`

- [card](https://oods-foundry.com/contexts/card)

  `BillingCardMeta`

## Parameters

- `defaultCurrency` `string`

  ISO 4217 currency code used when a value is not explicitly provided.

- `billingIntervals` `string[]`

  Allowed billing interval values that downstream objects may select.

- `minorUnits` `number`

  Number of fractional units that constitute a single major currency unit (e.g., 100 for cents).

- `paymentTiming` `string`

  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.

- `supportProration` `boolean`

  Whether mid-cycle plan changes generate prorated charges or credits. When true, proration_amount is computed on plan upgrades/downgrades.

- `prorationBehavior` `string`

  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.

- `cycleAnchorDay` `number`

  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

- [Subscription](https://oods-foundry.com/objects/subscription)

## 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"
```
