# `LatticeStripe.SubscriptionSchedule`
[🔗](https://github.com/szTheory/lattice_stripe/blob/v2.2.2/lib/lattice_stripe/subscription_schedule.ex#L1)

Operations on Stripe Subscription Schedule objects.

A Subscription Schedule lets you define a phased billing timeline — each
phase describes the prices, quantities, proration behavior, and trial
settings for a slice of time. When one phase ends, the schedule transitions
to the next and applies the new configuration to the underlying Subscription.

## Creation modes

Stripe accepts two mutually-exclusive parameter shapes on create:

### Mode 1: `from_subscription`

Convert an existing Subscription into a schedule whose first phase captures
the subscription's current state.

    LatticeStripe.SubscriptionSchedule.create(client, %{
      "from_subscription" => "sub_1234567890"
    })

### Mode 2: `customer` + `phases`

Build a new schedule from scratch with an explicit phase timeline.

    LatticeStripe.SubscriptionSchedule.create(client, %{
      "customer" => "cus_1234567890",
      "start_date" => "now",
      "end_behavior" => "release",
      "phases" => [
        %{
          "items" => [%{"price" => "price_1234567890", "quantity" => 1}],
          "iterations" => 12,
          "proration_behavior" => "create_prorations"
        }
      ]
    })

Mixing fields from both modes in a single call raises a Stripe 400 that
surfaces as `{:error, %LatticeStripe.Error{type: :invalid_request_error}}`.
LatticeStripe does not client-side-validate the mode — Stripe's own error
is already actionable.

## cancel vs release

- `cancel/4` (added in Plan 16-02) terminates **both** the schedule AND the
  underlying Subscription.
- `release/4` (added in Plan 16-02) detaches the schedule from its
  Subscription; the Subscription remains active and billable but is no
  longer governed by phases. This is irreversible.

## Search

Stripe does not expose a `search` endpoint for Subscription Schedules
(unlike Subscriptions). This module therefore has no `search/3` or
`search_stream!/3`.

## Proration guard

When a client has `require_explicit_proration: true`, `update/4` requires
`proration_behavior` at either the top level of `params` or inside any
element of `params["phases"][]`. Stripe does not accept
`proration_behavior` at `phases[].items[]`, so the guard does not walk
that deep.

Wired into `update/4` only — `create/3`, `cancel/4`, and `release/4`
bypass this guard because Stripe does not accept `proration_behavior` on
those endpoints.

## Telemetry

SubscriptionSchedule operations piggyback on the general
`[:lattice_stripe, :request, *]` events emitted by `Client.request/2`. No
schedule-specific events are emitted — state transitions belong to webhook
handlers, not the SDK layer.

## Stripe API Reference

See the [Stripe Subscription Schedules API](https://docs.stripe.com/api/subscription_schedules).

# `t`

```elixir
@type t() :: %LatticeStripe.SubscriptionSchedule{
  application: String.t() | nil,
  billing_mode: String.t() | nil,
  canceled_at: integer() | nil,
  completed_at: integer() | nil,
  created: integer() | nil,
  current_phase: LatticeStripe.SubscriptionSchedule.CurrentPhase.t() | nil,
  customer: LatticeStripe.Customer.t() | String.t() | nil,
  customer_account: String.t() | nil,
  default_settings: LatticeStripe.SubscriptionSchedule.Phase.t() | nil,
  end_behavior: atom() | String.t() | nil,
  extra: map(),
  id: String.t() | nil,
  livemode: boolean() | nil,
  metadata: map() | nil,
  object: String.t(),
  phases: [LatticeStripe.SubscriptionSchedule.Phase.t()] | nil,
  released_at: integer() | nil,
  released_subscription: String.t() | nil,
  status: atom() | String.t() | nil,
  subscription: LatticeStripe.Subscription.t() | String.t() | nil,
  test_clock: String.t() | nil
}
```

A Stripe Subscription Schedule.

See the [Stripe SubscriptionSchedule object](https://docs.stripe.com/api/subscription_schedules/object)
for field definitions.

# `cancel`

```elixir
@spec cancel(LatticeStripe.Client.t(), String.t(), map(), keyword()) ::
  {:ok, t()} | {:error, LatticeStripe.Error.t()}
```

Cancels a Subscription Schedule.

Terminates the schedule AND the underlying Subscription. Both entities move
to `canceled` status. This is irreversible.

## Wire shape

Dispatches `POST /v1/subscription_schedules/:id/cancel` — unlike
`LatticeStripe.Subscription.cancel/4` which uses DELETE. Do not mix them up.

## Params

- `"invoice_now"` (boolean) — whether to generate a final invoice
  immediately for any prorations
- `"prorate"` (boolean) — whether to prorate the cancellation

Note: the `prorate` param here is unrelated to `proration_behavior`. It
controls whether a proration invoice is generated on cancel, not which
proration strategy applies to a future item change. LatticeStripe does NOT
run `check_proration_required/2` on this call.

## Contrast with `release/4`

- `cancel/4` terminates both schedule and subscription.
- `release/4` detaches the schedule; the subscription stays active.

## Examples

    iex> LatticeStripe.SubscriptionSchedule.cancel(client, "sub_sched_123", %{"invoice_now" => true})
    {:ok, %LatticeStripe.SubscriptionSchedule{status: "canceled"}}

# `cancel!`

```elixir
@spec cancel!(LatticeStripe.Client.t(), String.t(), map(), keyword()) :: t()
```

Like `cancel/4` but raises on failure.

# `create`

```elixir
@spec create(LatticeStripe.Client.t(), map(), keyword()) ::
  {:ok, t()} | {:error, LatticeStripe.Error.t()}
```

Creates a new Subscription Schedule.

Sends `POST /v1/subscription_schedules`. See the module `@moduledoc` for the
two mutually-exclusive parameter shapes (`from_subscription` vs
`customer` + `phases`).

Stripe does not accept `proration_behavior` on create — schedules prorate
based on `start_date` mode, not an explicit field — so this function does
NOT run the proration guard.

# `create!`

```elixir
@spec create!(LatticeStripe.Client.t(), map(), keyword()) :: t()
```

Like `create/3` but raises on failure.

# `from_map`

```elixir
@spec from_map(map() | nil) :: t() | nil
```

Converts a decoded Stripe API map to a `%SubscriptionSchedule{}` struct.

Decodes nested typed structs:
- `current_phase` → `%LatticeStripe.SubscriptionSchedule.CurrentPhase{}`
- `default_settings` → `%LatticeStripe.SubscriptionSchedule.Phase{}` (reused)
- `phases` → `[%LatticeStripe.SubscriptionSchedule.Phase{}]`

Unknown top-level fields are collected into `:extra`.

# `list`

```elixir
@spec list(LatticeStripe.Client.t(), map(), keyword()) ::
  {:ok, LatticeStripe.Response.t()} | {:error, LatticeStripe.Error.t()}
```

Lists Subscription Schedules with optional filters.

Sends `GET /v1/subscription_schedules`.

# `list!`

```elixir
@spec list!(LatticeStripe.Client.t(), map(), keyword()) :: LatticeStripe.Response.t()
```

Like `list/3` but raises on failure.

# `release`

```elixir
@spec release(LatticeStripe.Client.t(), String.t(), map(), keyword()) ::
  {:ok, t()} | {:error, LatticeStripe.Error.t()}
```

Releases a Subscription Schedule.

Detaches the schedule from its Subscription. The Subscription remains
active and billable but is no longer governed by phases — subsequent
configuration changes must go through `LatticeStripe.Subscription.update/4`
directly.

**This is irreversible.** Once a schedule is released, there is no API to
re-attach it. If you need to regain phase-based control, you must create a
new schedule from the now-detached subscription (if policy allows).

## Contrast with `cancel/4`

- `release/4` detaches the schedule; the subscription continues billing
  but is no longer phase-governed.
- `cancel/4` terminates BOTH the schedule AND the underlying subscription.

Use `release/4` when you want to graduate a subscription off a phased plan
into a flat ongoing subscription. Use `cancel/4` when you want to end
billing entirely.

## Wire shape

Dispatches `POST /v1/subscription_schedules/:id/release`.

## Params

- `"preserve_cancel_date"` (boolean) — if the underlying subscription had
  a scheduled cancellation date, whether to preserve it on release

## Examples

    iex> LatticeStripe.SubscriptionSchedule.release(client, "sub_sched_123")
    {:ok, %LatticeStripe.SubscriptionSchedule{status: "released"}}

# `release!`

```elixir
@spec release!(LatticeStripe.Client.t(), String.t(), map(), keyword()) :: t()
```

Like `release/4` but raises on failure.

# `retrieve`

```elixir
@spec retrieve(LatticeStripe.Client.t(), String.t(), keyword()) ::
  {:ok, t()} | {:error, LatticeStripe.Error.t()}
```

Retrieves a Subscription Schedule by ID.

Sends `GET /v1/subscription_schedules/:id`.

# `retrieve!`

```elixir
@spec retrieve!(LatticeStripe.Client.t(), String.t(), keyword()) :: t()
```

Like `retrieve/3` but raises on failure.

# `stream!`

```elixir
@spec stream!(LatticeStripe.Client.t(), map(), keyword()) :: Enumerable.t()
```

Returns a lazy stream of all Subscription Schedules matching the given params.

Auto-paginates via `LatticeStripe.List.stream!/2`. Raises on fetch failure.

# `update`

```elixir
@spec update(LatticeStripe.Client.t(), String.t(), map(), keyword()) ::
  {:ok, t()} | {:error, LatticeStripe.Error.t()}
```

Updates a Subscription Schedule by ID.

Sends `POST /v1/subscription_schedules/:id`.

> #### Proration guard {: .info}
>
> When the client has `require_explicit_proration: true`, this function
> runs the Billing proration guard BEFORE dispatching the request. Params
> must carry `"proration_behavior"` at
> the top level OR at `phases[].proration_behavior` (but NOT
> `phases[].items[]` — Stripe does not accept it there).

# `update!`

```elixir
@spec update!(LatticeStripe.Client.t(), String.t(), map(), keyword()) :: t()
```

Like `update/4` but raises on failure.

---

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