# `Accrue.Billing.MeterEvents`
[🔗](https://github.com/szTheory/accrue/blob/accrue-v1.0.0/lib/accrue/billing/meter_events.ex#L1)

Helper for asynchronous meter-event state transitions driven by
Stripe webhooks.

Kept separate from `Accrue.Billing.MeterEventActions` so the webhook
path (`Accrue.Webhook.DefaultHandler`) doesn't pull the outbox/
NimbleOptions surface into its dependency graph.

Centralizes **pending → failed** transitions with guarded updates
and `[:accrue, :ops, :meter_reporting_failed]` so retries and duplicate
deliveries do not inflate ops counters.

# `failure_source`

```elixir
@type failure_source() :: :sync | :reconciler | :webhook
```

Origin of a terminal failure for `meter_reporting_failed` metadata.

* `:sync` — synchronous `report_usage/3` processor error
* `:reconciler` — `MeterEventsReconciler` retry exhausted path
* `:webhook` — Stripe `billing.meter.error_report_triggered` (or v1) path

# `mark_failed_by_identifier`

```elixir
@spec mark_failed_by_identifier(String.t() | nil, map(), String.t() | nil) ::
  {:ok, Accrue.Billing.MeterEvent.t()} | {:error, :not_found}
```

Looks up the meter-event row by `identifier` and flips it to `failed`
with the Stripe error-report object sanitized into `stripe_error`.

Uses the same guarded transition as the sync path so duplicate webhook
deliveries do not emit a second `meter_reporting_failed` for an already
terminal row.

`webhook_event_id` is optional metadata for ops telemetry (attach the
Stripe event id when known).

# `mark_failed_with_telemetry`

```elixir
@spec mark_failed_with_telemetry(
  Accrue.Billing.MeterEvent.t(),
  term(),
  failure_source(),
  keyword()
) ::
  {:ok, :transitioned, Accrue.Billing.MeterEvent.t()}
  | {:ok, :noop, Accrue.Billing.MeterEvent.t()}
  | {:error, :not_found}
```

Atomically flips a **single** meter-event row whose `stripe_status` is in
`:from_statuses` (default `["pending"]`) to `failed`, persisting a sanitized
`stripe_error` derived from `err`.

Emits `[:accrue, :ops, :meter_reporting_failed]` **only** when the guarded
update affects one row (`count == 1`). If no row matched (already terminal,
wrong status, or deleted), **no telemetry** is emitted and `{:ok, :noop, row}`
returns the current row when it still exists.

## Returns

  * `{:ok, :transitioned, %MeterEvent{}}` — this invocation performed the
    durable `pending` → `failed` transition (telemetry fired).
  * `{:ok, :noop, %MeterEvent{}}` — no qualifying row was updated; `row` is
    the latest state for the same primary key (e.g. idempotent replay or race).
  * `{:error, :not_found}` — the row id no longer exists.

`source` is attached to ops telemetry metadata (`:sync`, `:reconciler`, or `:webhook`).

## Options

  * `:from_statuses` — list of `stripe_status` values that may transition to
    `failed` in this update (default `["pending"]` for sync/reconciler).
    Stripe meter error webhooks use `["pending", "reported"]` because Stripe
    may reject usage after an initial `reported` acknowledgement.

---

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