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

Coupon and promotion-code write surface.

Thin wrappers around the processor's coupon + promotion-code
endpoints. The local
`accrue_coupons` + `accrue_promotion_codes` tables are a thin
projection — the processor is canonical and Accrue mirrors only the
fields the admin LiveView needs.

## Functions

  * `create_coupon/2` — creates a coupon via the processor, persists
    a local row, records a `"coupon.created"` event.
  * `create_promotion_code/2` — creates a promotion code via the
    processor, persists a local row FK'd to the coupon, records a
    `"promotion_code.created"` event.
  * `apply_promotion_code/3` — looks up a local promotion code by
    its customer-facing `code`, validates `active` / `expires_at` /
    `max_redemptions`, then calls
    `Processor.update_subscription(sub_id, %{coupon: coupon_id})`.
    Records a `"coupon.applied"` event on success.

All public functions follow the dual-API `foo/n` + `foo!/n` pattern.
Processor calls run inside `Repo.transact/2` here because
coupon create is not SCA-capable — there's no asynchronous 3DS leg
to keep outside the transaction.

# `apply_error`

```elixir
@type apply_error() ::
  :not_found
  | :inactive
  | :expired
  | :max_redemptions_reached
  | :coupon_missing
  | term()
```

# `apply_promotion_code`

```elixir
@spec apply_promotion_code(Accrue.Billing.Subscription.t(), String.t(), keyword()) ::
  {:ok, Accrue.Billing.Subscription.t()} | {:error, apply_error()}
```

Attaches a promotion code to a subscription by looking up the local
`%PromotionCode{}` row by customer-facing `code`, validating
applicability, then calling the processor's `update_subscription`
with `%{coupon: coupon_processor_id}`.

Returns `{:ok, %Subscription{}}` on success. On validation failure
returns `{:error, :not_found | :inactive | :expired |
:max_redemptions_reached}` before making any processor call.

A `"coupon.applied"` event is recorded inside the same
`Repo.transact/2` as the processor call on success.

# `apply_promotion_code!`

```elixir
@spec apply_promotion_code!(Accrue.Billing.Subscription.t(), String.t(), keyword()) ::
  Accrue.Billing.Subscription.t()
```

Raising variant of `apply_promotion_code/3`.

# `create_coupon`

```elixir
@spec create_coupon(
  map(),
  keyword()
) :: {:ok, Accrue.Billing.Coupon.t()} | {:error, term()}
```

Creates a coupon through the configured processor and persists a
local `%Coupon{}` row plus a `"coupon.created"` event.

`params` is a map of processor-shape attrs. Supply a caller-chosen
`:id` to pin a deterministic coupon id (required for the comp flow's
`"accrue_comp_100_forever"` seed coupon).

# `create_coupon!`

```elixir
@spec create_coupon!(
  map(),
  keyword()
) :: Accrue.Billing.Coupon.t()
```

Raising variant of `create_coupon/2`.

# `create_promotion_code`

```elixir
@spec create_promotion_code(
  map(),
  keyword()
) :: {:ok, Accrue.Billing.PromotionCode.t()} | {:error, term()}
```

Creates a promotion code through the configured processor and
persists a local `%PromotionCode{}` row FK'd to the underlying
coupon. Records a `"promotion_code.created"` event.

`params[:coupon]` MUST be the processor-side coupon id (e.g.
`"accrue_comp_100_forever"` or `"SUMMER25"`).

# `create_promotion_code!`

```elixir
@spec create_promotion_code!(
  map(),
  keyword()
) :: Accrue.Billing.PromotionCode.t()
```

Raising variant of `create_promotion_code/2`.

---

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