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

# Cancellable

Adds lifecycle-aware cancellation workflows with policy guardrails and semantic token mappings.

Generated from @oods/foundry 0.10.1

- Group

  lifecycle

- Maturity

  stable

- Contexts

  card, detail, form

## Fields it adds

| Field | Type | Required | Description |
| - | - | - | - |
| `cancel_at_period_end` | `boolean` | yes | Whether cancellation occurs at the natural period end instead of immediately. When true the entity is in a REVERSIBLE pending-cancellation state: the schedule can be undone (set back to false) any time before period end, returning the entity to active. This is distinct from a terminal cancellation (a \`terminated\`/canceled subscription), which is irreversible and non-reactivatable. Mirrors Stripe's cancel_at_period_end flag (docs.stripe.com/billing/subscriptions/cancel). |
| `cancellation_reason` | `string` | no | Free-form detail describing why cancellation occurred. |
| `cancellation_reason_code` | `string` | no | Structured reason code chosen from the allowedReasons parameter. |
| `cancellation_requested_at` | `datetime` | no | Timestamp capturing when the cancellation workflow was initiated. |

## What it shows in each context

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

  `CancellationSummary`

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

  `CancellationForm`

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

  `CancellationBadge`

## Parameters

- `allowCancellationAfterStart` `boolean`

  Whether cancellation may occur after the entity transitions to an active state.

- `cancellationWindowHours` `number`

  Number of hours after activation where cancellation is permitted.

- `requireReason` `boolean`

  Whether a structured cancellation reason must be supplied by the actor.

- `allowedReasons` `string[]`

  Allow list of valid cancellation reason codes when requireReason is true.

## Objects that use it

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

## The trait file

`traits/lifecycle/Cancellable.trait.yaml`

```
trait:
  name: Cancellable
  version: 1.0.0
  description: |
    Adds lifecycle-aware cancellation workflows with policy guardrails and semantic token mappings.
  category: lifecycle
  tags:
    - lifecycle
    - cancellation
    - retention
    - workflow

parameters:
  - name: allowCancellationAfterStart
    type: boolean
    required: false
    description: Whether cancellation may occur after the entity transitions to an active state.
    default: true
  - name: cancellationWindowHours
    type: number
    required: false
    description: Number of hours after activation where cancellation is permitted.
    validation:
      minimum: 0
      maximum: 720
  - name: requireReason
    type: boolean
    required: false
    description: Whether a structured cancellation reason must be supplied by the actor.
    default: false
  - name: allowedReasons
    type: string[]
    required: false
    description: Allow list of valid cancellation reason codes when requireReason is true.
    validation:
      minItems: 1
      uniqueItems: true
      items:
        pattern: "^[a-z0-9_\\-]+$"
        minLength: 1
        maxLength: 64

schema:
  cancel_at_period_end:
    type: boolean
    required: true
    description: >-
      Whether cancellation occurs at the natural period end instead of immediately.
      When true the entity is in a REVERSIBLE pending-cancellation state: the schedule
      can be undone (set back to false) any time before period end, returning the entity
      to active. This is distinct from a terminal cancellation (a `terminated`/canceled
      subscription), which is irreversible and non-reactivatable. Mirrors Stripe's
      cancel_at_period_end flag (docs.stripe.com/billing/subscriptions/cancel).
    default: false
  cancellation_reason:
    type: string
    required: false
    description: Free-form detail describing why cancellation occurred.
    validation:
      maxLength: 160
  cancellation_reason_code:
    type: string
    required: false
    description: Structured reason code chosen from the allowedReasons parameter.
    validation:
      enumFromParameter: allowedReasons
  cancellation_requested_at:
    type: datetime
    required: false
    description: Timestamp capturing when the cancellation workflow was initiated.

semantics:
  cancel_at_period_end:
    semantic_type: status.cancel.deferred
    token_mapping: tokenMap(status.cancel.*)
    ui_hints:
      component: CancellationToggle
      emphasizeWhenTrue: true
  cancellation_reason:
    semantic_type: status.cancel.reason
    token_mapping: tokenMap(status.cancel.reason)
    ui_hints:
      component: CancellationReason
      multiline: true
      maxLength: 160
  cancellation_reason_code:
    semantic_type: status.cancel.reason_code
    token_mapping: tokenMap(status.cancel.code.*)
    ui_hints:
      component: ReasonPicker
      parameterSource: allowedReasons
  cancellation_requested_at:
    semantic_type: status.cancel.timestamp
    token_mapping: tokenMap(status.cancel.timestamp)
    ui_hints:
      component: TimelineTimestamp

view_extensions:
  # s224-m01 (#2542 ruling 6): a record with no cancellation scheduled or recorded shows no card; "Cancel at period end:
  # No" only states the default.
  detail:
    - component: CancellationSummary
      position: top
      props:
        hideWhenDefault: true
        cancelAtPeriodEndField: cancel_at_period_end
        reasonField: cancellation_reason
        codeField: cancellation_reason_code
        requestedAtField: cancellation_requested_at
  # s222-m03: no timeline recipe. A timeline's rail lists the cancellation request (recordCollectionEvents).
  form:
    - component: CancellationForm
      position: top
      props:
        reasonField: cancellation_reason
        codeField: cancellation_reason_code
        requireReasonParameter: requireReason
        allowedReasonsParameter: allowedReasons
        windowParameter: cancellationWindowHours
  # s223-m01 (#2527 ruling 7): a scheduled cancellation shows on the card; "No cancellation scheduled" only states the default.
  card:
    - component: CancellationBadge
      position: before
      props:
        field: cancel_at_period_end
        hideWhenFalse: true

tokens:
  status.cancel.primary.bg: "var(--status-cancel-bg)"
  status.cancel.primary.text: "var(--status-cancel-text)"
  status.cancel.reason.text: "var(--text-default)"
  status.cancel.timestamp.text: "var(--text-subtle)"

dependencies:
  - Stateful

metadata:
  created: "2025-10-12"
  owners:
    - lifecycle@oods.systems
    - support@oods.systems
  maturity: stable
  accessibility:
    keyboard: "Supports toggling of cancellation switches."
    screenreader: "Announces cancellation state and reason."
  regionsUsed:
    - detail
    - form
    - card
  examples:
    - Subscription
    - Reservation
  references:
    - "Trait Engine Spec v0.1 §2"
```
