# `LatticeStripe.TestHelpers.TestClock`
[🔗](https://github.com/szTheory/lattice_stripe/blob/v2.2.2/lib/lattice_stripe/test_helpers/test_clock.ex#L1)

Operations on Stripe [Test Clock](https://docs.stripe.com/api/test_clocks)
objects.

Test Clocks let you simulate the passage of time in Stripe test mode —
useful for exercising subscription renewals, invoice cycles, and billing
lifecycle events without waiting real-world time. This module is the
low-level SDK wrapper over `POST /v1/test_helpers/test_clocks` and sibling
endpoints; for an ergonomic ExUnit helper built on top of it, see
`LatticeStripe.Testing.TestClock`.

## Account limit

Stripe enforces a hard limit of **100 test clocks per account**. If you
hit the limit, `create/2` returns an error. The
`LatticeStripe.Testing.TestClock` user-facing helper registers every
created clock with a per-test ExUnit owner that cleans up on test exit,
and `mix lattice_stripe.test_clock.cleanup` backstops SIGKILL/crash
scenarios.

## Metadata support (A-13g)

Verified against the Stripe OpenAPI spec (spec3.sdk.json) and
`stripe/stripe-mock:latest` on 2026-04-11: `POST /v1/test_helpers/test_clocks`
does **NOT** accept a `metadata` parameter, and the `test_helpers.test_clock`
object schema has no `metadata` field. The request body schema exposes
only `expand`, `frozen_time`, and `name`; the object schema exposes
`created`, `deletes_after`, `frozen_time`, `id`, `livemode`, `name`,
`object`, `status`, and `status_details`.

Consequence: this struct intentionally omits `:metadata`.
`LatticeStripe.Testing.TestClock` (Plan 13-05) falls back to
Owner-only tracking plus an age-based Mix task for cleanup rather than
tagging clocks with a metadata marker.

## Status values

- `:ready` — clock is at its current `frozen_time` and idle
- `:advancing` — an `advance/4` call is in progress server-side
- `:internal_failure` — a server-side advancement failed; the clock is
  unusable and should be deleted

Unknown server-returned status strings are passed through as raw strings
(`String.t()`) for forward compatibility; tests should pattern-match
against the known atoms or use `to_string/1` for display.

## Deletion cascades

Deleting a test clock **cascades**: every Customer attached to the clock
is deleted, every Subscription attached is canceled. Do not attach
production-critical fixtures to a clock you intend to delete.

## Typical usage

    frozen_time = System.system_time(:second)

    {:ok, clock} = LatticeStripe.TestHelpers.TestClock.create(client, %{frozen_time: frozen_time})

    {:ok, ready} =
      LatticeStripe.TestHelpers.TestClock.advance_and_wait(
        client,
        clock.id,
        frozen_time + 86_400 * 30
      )

    assert ready.status == :ready

    {:ok, _} = LatticeStripe.TestHelpers.TestClock.delete(client, clock.id)

For a high-level ExUnit experience (automatic cleanup, setup callbacks,
customer linkage), use `LatticeStripe.Testing.TestClock` instead.

## Operations not supported by the Stripe API

- **update** — Stripe Test Clocks are immutable after creation. To
  advance their time, use `advance/4` (lands in Plan 13-04). To change
  metadata or name, delete and re-create.
- **search** — Stripe's Test Clock API does not expose a `/search`
  endpoint. Use `list/3` with client-side filtering if needed.

# `t`

```elixir
@type t() :: %LatticeStripe.TestHelpers.TestClock{
  created: integer() | nil,
  deleted: boolean(),
  deletes_after: integer() | nil,
  extra: map(),
  frozen_time: integer() | nil,
  id: String.t() | nil,
  livemode: boolean() | nil,
  name: String.t() | nil,
  object: String.t(),
  status: :ready | :advancing | :internal_failure | String.t() | nil,
  status_details: map() | nil
}
```

A Stripe Test Clock object.

See the [Stripe Test Clock API](https://docs.stripe.com/api/test_clocks/object)
for field definitions.

# `advance`

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

Advances a Test Clock to a new `frozen_time`.

Sends `POST /v1/test_helpers/test_clocks/:id/advance`. The returned
clock will typically have `status: :advancing` — the server processes
the advancement asynchronously. Use `advance_and_wait/4` (or the bang
variant) if you need to block until the clock reaches `:ready`.

Stripe enforces a maximum advancement of roughly two billing intervals
of the shortest attached subscription. Advancing further in a single
call returns a 400 error; advance in chunks instead.

## Parameters

- `client` — A `%LatticeStripe.Client{}` struct
- `id` — Test Clock id (`"clock_..."`)
- `frozen_time` — Unix timestamp (integer) to advance TO
- `opts` — Per-request overrides

## Returns

- `{:ok, %TestClock{status: :advancing}}` (usually) on success
- `{:error, %LatticeStripe.Error{}}` on failure

# `advance!`

```elixir
@spec advance!(LatticeStripe.Client.t(), String.t(), integer(), keyword()) ::
  t() | no_return()
```

Bang variant of `advance/4`. Returns `%TestClock{}` on success, raises
`LatticeStripe.Error` on failure.

# `advance_and_wait`

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

Advances a Test Clock and polls until it reaches `status: :ready`.

This is the differentiating helper for Stripe test-clock workflows: you
almost never want to call `advance/4` directly in a test, because the
clock returns `status: :advancing` and you have to poll for completion
yourself. `advance_and_wait/4` does the advance + the polling + the
terminal-failure detection + a well-behaved timeout, returning either
a ready clock or a typed `%LatticeStripe.Error{}`.

## Polling strategy

- **First poll has zero delay** — catches already-ready clocks and
  stripe-mock's instant fixture without waiting 500ms.
- **Exponential backoff with full jitter, floored at 500ms.** Subsequent
  sleeps are `max(500, :rand.uniform(delay))` where `delay` starts at
  500ms, multiplies by 1.5 each iteration, and caps at 5000ms. Stripe's
  docs warn about tight-loop rate limits on test clocks; the 500ms floor
  is non-negotiable.
- **Monotonic deadline.** The timeout uses `System.monotonic_time/1`, not
  system time — NTP adjustments during long test runs do not cause
  premature timeouts.
- **Default timeout: 60 seconds.** Override via `opts[:timeout]`
  (milliseconds).

## Errors

- **Timeout** — returns `{:error, %LatticeStripe.Error{type: :test_clock_timeout,
  raw_body: %{"clock_id" => _, "last_status" => _, "attempts" => _, "elapsed_ms" => _}}}`
- **Internal failure** — returns `{:error, %LatticeStripe.Error{type: :test_clock_failed,
  raw_body: %{"clock_id" => _, "last_status" => "internal_failure", "attempts" => _}}}` —
  Stripe entered a terminal failure state, retrying will not help.
- **HTTP failure during poll** — the underlying `retrieve/3` error propagates unchanged.

## Telemetry

Emits `[:lattice_stripe, :test_clock, :advance_and_wait, :start]` and
`[..., :stop]` via `:telemetry.span/3`. Stop metadata includes
`%{clock_id:, status:, attempts:, outcome: :ok | :error}`. Gated by
`client.telemetry_enabled`.

## Options

- `:timeout` — total deadline in ms (default 60_000)
- `:initial_interval` — first non-zero sleep in ms (default 500, clamped to 500 floor)
- `:max_interval` — maximum sleep in ms (default 5_000)
- `:multiplier` — per-iteration growth factor (default 1.5)

## Example

    {:ok, ready} =
      LatticeStripe.TestHelpers.TestClock.advance_and_wait(
        client,
        clock.id,
        System.system_time(:second) + 86_400 * 30
      )

    assert ready.status == :ready

# `advance_and_wait!`

```elixir
@spec advance_and_wait!(LatticeStripe.Client.t(), String.t(), integer(), keyword()) ::
  t() | no_return()
```

Bang variant of `advance_and_wait/4`. Returns `%TestClock{}` on success,
raises `LatticeStripe.Error` on failure (timeout or internal_failure).

# `cleanup_tagged`

```elixir
@spec cleanup_tagged(
  LatticeStripe.Client.t(),
  keyword()
) :: {:ok, term()}
```

Lists and optionally deletes test clocks older than a threshold.

Despite its historical name, `cleanup_tagged/2` does not inspect tags or
metadata. Stripe test clocks do not support metadata, so candidates are
selected by age and, when provided, a clock-name prefix.

This is the shared deletion core used by both
the `Owner` GenServer's cleanup callback (per-test) and
`mix lattice_stripe.test_clock.cleanup` (backstop). See those callers
for the user-facing entry points.

## Metadata limitation

Stripe's Test Clock API does **not** support `metadata`, so this
function cannot filter by a LatticeStripe-specific marker. Without
`:name_prefix`, the Mix task cannot distinguish LatticeStripe-managed clocks
from user-created ones. The primary cleanup path (`Owner` + `on_exit`) is
unaffected.

## Options

- `:older_than_ms` -- only consider clocks older than N milliseconds
  (default: `3_600_000` = 1 hour)
- `:delete` -- `true` to actually delete, `false` to return candidates
  only (default: `false`)
- `:name_prefix` -- optional string prefix filter on clock name
  (e.g., `"lattice_stripe_test"`)

## Returns

- `delete: false` -- `{:ok, [%TestClock{}, ...]}`
- `delete: true` -- `{:ok, %{deleted: n, failed: n, total_matched: n}}`

# `create`

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

Creates a new Stripe Test Clock.

Sends `POST /v1/test_helpers/test_clocks` with the given params and returns
the new clock as a typed `t()`.

## Parameters

- `client` — A `%LatticeStripe.Client{}` struct.
- `params` — Map of Stripe API params. Required: `:frozen_time` (integer unix
  timestamp). Optional: `:name` (string). Note: `:metadata` is NOT accepted
  by the Stripe Test Clock API (see "Metadata support" above).
- `opts` — Per-request overrides (e.g., `:idempotency_key`).

## Example

    {:ok, clock} = LatticeStripe.TestHelpers.TestClock.create(client, %{
      frozen_time: System.system_time(:second),
      name: "renewal-test"
    })

# `create!`

# `delete`

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

Deletes a Test Clock by id. DELETE /v1/test_helpers/test_clocks/:id.

**This cascades**: every Customer attached to the clock is deleted, every
Subscription canceled. See module docs.

# `delete!`

# `from_map`

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

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

Maps all known Stripe test clock fields. Any unrecognized fields are
collected into the `extra` map so no data is silently lost.

Per D-03, the `status` field is atomized via a whitelist: `"ready"` →
`:ready`, `"advancing"` → `:advancing`, `"internal_failure"` →
`:internal_failure`. Unknown values pass through as raw strings for
forward compatibility with future Stripe enum additions.

# `list`

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

Lists Test Clocks with optional filters. GET /v1/test_helpers/test_clocks.

# `list!`

# `retrieve`

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

Retrieves a Test Clock by id. GET /v1/test_helpers/test_clocks/:id.

# `retrieve!`

# `stream!`

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

Streams Test Clocks lazily via cursor pagination.

Returns a `Stream` that yields `%TestClock{}` items. Matches the
Phase 12 resource pattern (`LatticeStripe.List.stream!/2` + `Stream.map`).

---

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