# `Accrue.Billing.PromotionCode`
[🔗](https://github.com/szTheory/accrue/blob/accrue-v1.0.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`. The admin 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`.

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. Skips required-field validation so out-of-order
webhook events can settle partial state. The processor (Stripe) is
canonical for promotion code state.

---

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