[Traits](https://oods-foundry.com/traits) / SaaSBillingPayable

# SaaSBillingPayable

Captures invoice level billing terms, status, and outstanding balance metadata for SaaS finance teams.

Generated from @oods/foundry 0.10.1

- Group

  domain

- Maturity

  beta

- Contexts

  card, list

## Fields it adds

| Field | Type | Required | Description |
| - | - | - | - |
| `invoice_number` | `string` | yes | Human readable invoice identifier presented to customers. |
| `status` | `string` | yes | Canonical invoice status after provider mapping. |
| `provider_status` | `string` | no | Original provider reported status or badge. |
| `issued_at` | `datetime` | yes | Timestamp when the invoice was finalized or posted. |
| `due_at` | `datetime` | no | Payment due timestamp derived from payment terms. |
| `paid_at` | `datetime` | no | Timestamp when full payment cleared. |
| `total_minor` | `integer` | yes | Total amount due expressed in minor currency units. |
| `balance_minor` | `integer` | no | Remaining amount outstanding in minor units. |
| `currency` | `string` | yes | ISO 4217 currency for the invoice. |
| `payment_terms` | `string` | no | Payment term label describing when payment is expected. |
| `collection_state` | `string` | no | Dunning phase or retry program detail presented to success teams. |
| `last_reminder_at` | `datetime` | no | Timestamp of most recent payment reminder issued to customer. |
| `aging_bucket_days` | `integer` | no | Number of days outstanding grouped for aging reports. |
| `attempt_count` | `integer` | no | Number of automatic payment attempts made on this invoice (Stripe smart-retries attempt_count). Increments on each retry in the dunning window. Source: docs.stripe.com/billing/revenue-recovery/smart-retries. |
| `next_payment_attempt` | `datetime` | no | When the next automatic payment retry is scheduled (Stripe smart-retries next_payment_attempt). Defines the open end of the retry window — modeled on the Refundable.refundable_until datetime-window precedent. Absent/null when no further attempts are scheduled (retries exhausted → invoice goes uncollectible, mirroring the subscription \`unpaid\` state). Source: docs.stripe.com/billing/revenue-recovery/smart-retries. |
| `memo` | `string` | no | Internal finance note or customer visible memo. |

## What it shows in each context

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

  `BillingSummaryBadge`

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

  `BillingSummaryBadge`

## Parameters

- `statusEnum` `string[]`, required

  Canonical invoice status values allowed by the domain.

- `paymentTerms` `string[]`

  Supported payment term descriptors (ex: net_15, due_upon_receipt).

## Objects that use it

- [Invoice](https://oods-foundry.com/objects/invoice)

## The trait file

`domains/saas-billing/traits/payable.trait.yaml`

```
trait:
  name: SaaSBillingPayable
  version: 1.0.0
  description: Captures invoice level billing terms, status, and outstanding balance metadata for SaaS finance teams.
  category: domain
  tags:
    - billing
    - invoice
    - domain

parameters:
  - name: statusEnum
    type: string[]
    required: true
    description: Canonical invoice status values allowed by the domain.
    default:
      - draft
      - posted
      - open
      - processing
      - past_due
      - paid
      - refunded
      - uncollectible
      - void
  - name: paymentTerms
    type: string[]
    required: false
    description: "Supported payment term descriptors (ex: net_15, due_upon_receipt)."
    default:
      - due_upon_receipt
      - net_15
      - net_30
      - net_45

schema:
  invoice_number:
    type: string
    required: true
    description: Human readable invoice identifier presented to customers.
  status:
    type: string
    required: true
    description: Canonical invoice status after provider mapping.
    validation:
      enumFromParameter: statusEnum
  provider_status:
    type: string
    required: false
    description: Original provider reported status or badge.
  issued_at:
    type: datetime
    required: true
    description: Timestamp when the invoice was finalized or posted.
  due_at:
    type: datetime
    required: false
    description: Payment due timestamp derived from payment terms.
  paid_at:
    type: datetime
    required: false
    description: Timestamp when full payment cleared.
  total_minor:
    type: integer
    required: true
    description: Total amount due expressed in minor currency units.
  balance_minor:
    type: integer
    required: false
    description: Remaining amount outstanding in minor units.
    default: 0
  currency:
    type: string
    required: true
    description: ISO 4217 currency for the invoice.
  payment_terms:
    type: string
    required: false
    description: Payment term label describing when payment is expected.
    validation:
      enumFromParameter: paymentTerms
  collection_state:
    type: string
    required: false
    description: Dunning phase or retry program detail presented to success teams.
  last_reminder_at:
    type: datetime
    required: false
    description: Timestamp of most recent payment reminder issued to customer.
  aging_bucket_days:
    type: integer
    required: false
    description: Number of days outstanding grouped for aging reports.
  attempt_count:
    type: integer
    required: false
    description: >-
      Number of automatic payment attempts made on this invoice (Stripe smart-retries
      attempt_count). Increments on each retry in the dunning window. Source:
      docs.stripe.com/billing/revenue-recovery/smart-retries.
    default: 0
    validation:
      minimum: 0
  next_payment_attempt:
    type: datetime
    required: false
    description: >-
      When the next automatic payment retry is scheduled (Stripe smart-retries
      next_payment_attempt). Defines the open end of the retry window — modeled on the
      Refundable.refundable_until datetime-window precedent. Absent/null when no further
      attempts are scheduled (retries exhausted → invoice goes uncollectible, mirroring
      the subscription `unpaid` state). Source: docs.stripe.com/billing/revenue-recovery/smart-retries.
  memo:
    type: string
    required: false
    description: Internal finance note or customer visible memo.

semantics:
  invoice_number:
    semantic_type: billing.invoice.number
    token_mapping: tokenMap(billing.invoice.number)
  status:
    semantic_type: billing.invoice.status
    token_mapping: tokenMap(billing.invoice.status.*)
  provider_status:
    semantic_type: billing.invoice.provider_status
    token_mapping: tokenMap(billing.invoice.provider_status)
  issued_at:
    semantic_type: billing.invoice.issued_at
    token_mapping: tokenMap(billing.invoice.issued_at)
  due_at:
    semantic_type: billing.invoice.due_at
    token_mapping: tokenMap(billing.invoice.due_at)
  paid_at:
    semantic_type: billing.invoice.paid_at
    token_mapping: tokenMap(billing.invoice.paid_at)
  total_minor:
    semantic_type: billing.invoice.total_minor
    token_mapping: tokenMap(billing.invoice.total_minor)
    ui_hints:
      component: CurrencyAmount
      currencyField: currency
      minorUnits: 100
  balance_minor:
    semantic_type: billing.invoice.balance_minor
    token_mapping: tokenMap(billing.invoice.balance_minor)
    ui_hints:
      component: CurrencyAmount
      currencyField: currency
      minorUnits: 100
  currency:
    semantic_type: billing.invoice.currency
    token_mapping: tokenMap(billing.invoice.currency)
  payment_terms:
    semantic_type: billing.invoice.payment_terms
    token_mapping: tokenMap(billing.invoice.payment_terms)
  collection_state:
    semantic_type: billing.invoice.collection_state
    token_mapping: tokenMap(billing.invoice.collection_state)
  last_reminder_at:
    semantic_type: billing.invoice.last_reminder_at
    token_mapping: tokenMap(billing.invoice.last_reminder_at)
  aging_bucket_days:
    semantic_type: billing.invoice.aging_bucket_days
    token_mapping: tokenMap(billing.invoice.aging_bucket_days)
  attempt_count:
    semantic_type: billing.invoice.attempt_count
    token_mapping: tokenMap(billing.invoice.attempt_count)
  next_payment_attempt:
    semantic_type: billing.invoice.next_payment_attempt
    token_mapping: tokenMap(billing.invoice.next_payment_attempt)
  memo:
    semantic_type: billing.invoice.memo
    token_mapping: tokenMap(billing.invoice.memo)

view_extensions:
  # s222-m03 (#2502 ruling 13): an invoice's row, page header and card show its total, as a financial trait's recipes do.
  list:
    - component: BillingSummaryBadge
      position: after
      props:
        amountField: total_minor
        currencyField: currency
        minorUnits: 100
        showInterval: false
  card:
    - component: BillingSummaryBadge
      position: after
      props:
        amountField: total_minor
        currencyField: currency
        minorUnits: 100
        showInterval: false

tokens:
  billing.invoice.number: "var(--cmp-text-strong)"
  billing.invoice.total_minor: "var(--cmp-text-body-strong)"
  billing.invoice.balance_minor: "var(--cmp-text-warning)"
  billing.invoice.status.past_due: "var(--cmp-status-critical-text)"
  billing.invoice.status.paid: "var(--cmp-status-success-text)"

metadata:
  owners:
    - revenue-ops@oods.systems
  maturity: beta
  regionsUsed:
    - list
    - card
  notes:
    - Canonical status options sourced from r4.5 mapping research.
    - >-
      Dunning/access reconciliation (s126-m05, scoped to the access-revocation need):
      the 9-value statusEnum is a SUPERSET of the canonical 5-state invoice model
      (draft, posted, paid, past_due, void — see docs/billing/lifecycle-states.md).
      Dunning-relevant mapping: `open`/`processing` = pre-dunning (awaiting/settling
      payment); `past_due` = overdue, retries ongoing → subscription past_due (GRACE,
      keep access); `uncollectible` = retries exhausted → mirrors subscription `unpaid`
      (REVOKE access). The retry window is carried by attempt_count + next_payment_attempt.
      The subscription-status → access decision is encoded in
      src/domain/billing/states.ts hasServiceAccess().
```
