# `Accrue.Billing.PromotionCode`
[🔗](https://github.com/szTheory/accrue/blob/accrue-v0.3.0/lib/accrue/billing/promotion_code.ex#L1)

Ecto schema for the `accrue_promotion_codes` table.

Stores the thin local projection of a processor promotion code — the
customer-facing string (e.g. `"SUMMER25"`) that resolves to a
`Coupon`. Phase 4 (BILL-27) mirrors only the fields the admin
LiveView needs to filter/sort: `code`, `active`, `max_redemptions`,
`times_redeemed`, `expires_at`, plus the FK to `accrue_coupons`.

Per D4-01 / Claude's Discretion: full processor mirror is NOT a
goal. The canonical source of truth is the processor; Accrue
denormalizes only what the admin UI touches.

# `t`

```elixir
@type t() :: %Accrue.Billing.PromotionCode{
  __meta__: term(),
  active: term(),
  code: term(),
  coupon: term(),
  coupon_id: term(),
  data: term(),
  expires_at: term(),
  id: term(),
  inserted_at: term(),
  last_stripe_event_id: term(),
  last_stripe_event_ts: term(),
  lock_version: term(),
  max_redemptions: term(),
  metadata: term(),
  processor: term(),
  processor_id: term(),
  times_redeemed: term(),
  updated_at: term()
}
```

# `changeset`

```elixir
@spec changeset(
  %Accrue.Billing.PromotionCode{
    __meta__: term(),
    active: term(),
    code: term(),
    coupon: term(),
    coupon_id: term(),
    data: term(),
    expires_at: term(),
    id: term(),
    inserted_at: term(),
    last_stripe_event_id: term(),
    last_stripe_event_ts: term(),
    lock_version: term(),
    max_redemptions: term(),
    metadata: term(),
    processor: term(),
    processor_id: term(),
    times_redeemed: term(),
    updated_at: term()
  }
  | Ecto.Changeset.t(),
  map()
) :: Ecto.Changeset.t()
```

User-path changeset. Validates required fields plus metadata shape,
enforces uniqueness on `processor_id` and `code`, and optimistic
locking on `lock_version`.

# `force_status_changeset`

```elixir
@spec force_status_changeset(
  %Accrue.Billing.PromotionCode{
    __meta__: term(),
    active: term(),
    code: term(),
    coupon: term(),
    coupon_id: term(),
    data: term(),
    expires_at: term(),
    id: term(),
    inserted_at: term(),
    last_stripe_event_id: term(),
    last_stripe_event_ts: term(),
    lock_version: term(),
    max_redemptions: term(),
    metadata: term(),
    processor: term(),
    processor_id: term(),
    times_redeemed: term(),
    updated_at: term()
  }
  | Ecto.Changeset.t(),
  map()
) :: Ecto.Changeset.t()
```

Webhook-path changeset (D3-17). Skips required-field validation so
out-of-order webhook events can settle partial state. Processor is
canonical (D2-29).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
