# `PushX.Response`
[🔗](https://github.com/cignosystems/pushx/blob/v0.11.0/lib/push_x/response.ex#L1)

A struct representing the response from a push notification request.

## Fields

  * `:provider` - The provider used (`:apns` or `:fcm`)
  * `:status` - The result status (see below)
  * `:id` - Provider-specific message ID (if available)
  * `:reason` - Error reason string (if failed)
  * `:raw` - Raw response body (for debugging)

## Status Values

  * `:sent` - Message was successfully sent
  * `:invalid_token` - Device token is invalid or expired
  * `:expired_token` - Device token has expired
  * `:unregistered` - Device is no longer registered
  * `:payload_too_large` - Payload exceeds size limit
  * `:rate_limited` - Too many requests, try again later
  * `:server_error` - Provider server error
  * `:connection_error` - Network/connection failure
  * `:circuit_open` - Circuit breaker is open, provider temporarily blocked
  * `:invalid_request` - Missing or invalid request parameters (e.g., no `:topic` for APNS)
  * `:auth_error` - Authentication failure (e.g., invalid private key, JWT generation failed)
  * `:unknown_error` - Unrecognized error

# `status`

```elixir
@type status() ::
  :sent
  | :invalid_token
  | :expired_token
  | :unregistered
  | :payload_too_large
  | :rate_limited
  | :server_error
  | :connection_error
  | :circuit_open
  | :provider_disabled
  | :invalid_request
  | :auth_error
  | :unknown_error
```

# `t`

```elixir
@type t() :: %PushX.Response{
  id: String.t() | nil,
  provider: :apns | :fcm | :unknown,
  raw: any(),
  reason: String.t() | nil,
  retry_after: non_neg_integer() | nil,
  status: status()
}
```

# `apns_reason_to_status`

```elixir
@spec apns_reason_to_status(String.t()) :: status()
```

Maps an APNS error reason to a status atom.

See: https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/handling_notification_responses_from_apns

# `error`

```elixir
@spec error(
  provider :: :apns | :fcm | :unknown,
  status :: status(),
  reason :: String.t() | nil
) :: t()
```

Creates an error response.

# `error`

```elixir
@spec error(
  provider :: :apns | :fcm | :unknown,
  status :: status(),
  reason :: String.t() | nil,
  raw :: any()
) :: t()
```

Creates an error response with raw data.

# `error`

```elixir
@spec error(
  provider :: :apns | :fcm | :unknown,
  status :: status(),
  reason :: String.t() | nil,
  raw :: any(),
  retry_after :: non_neg_integer() | nil
) :: t()
```

Creates an error response with raw data and retry_after value.

# `extract_fcm_error_code`

```elixir
@spec extract_fcm_error_code(map()) :: String.t() | nil
```

Extracts the FCM-specific error code from a decoded error response body.

The FCM v1 API wraps the real error code (e.g., `"UNREGISTERED"`) inside the
`details` array with `@type` `"type.googleapis.com/google.firebase.fcm.v1.FcmError"`,
while the top-level `"status"` contains a generic gRPC code (e.g., `"NOT_FOUND"`).

Returns `nil` if no FCM-specific error code is found in the details.

## Examples

    iex> body = %{"error" => %{"status" => "NOT_FOUND", "details" => [%{"@type" => "type.googleapis.com/google.firebase.fcm.v1.FcmError", "errorCode" => "UNREGISTERED"}]}}
    iex> PushX.Response.extract_fcm_error_code(body)
    "UNREGISTERED"

    iex> PushX.Response.extract_fcm_error_code(%{"error" => %{"status" => "INTERNAL"}})
    nil

# `fcm_error_to_status`

```elixir
@spec fcm_error_to_status(String.t()) :: status()
```

Maps an FCM error code to a status atom.

See: https://firebase.google.com/docs/reference/fcm/rest/v1/ErrorCode

# `retryable?`

```elixir
@spec retryable?(t()) :: boolean()
```

Returns true if the error is retryable.

Retryable errors:
- `:connection_error` - Network/connection failure
- `:rate_limited` - Too many requests (with backoff)
- `:server_error` - Provider server error (5xx)

## Examples

    iex> PushX.Response.error(:fcm, :server_error, "Internal") |> PushX.Response.retryable?()
    true

    iex> PushX.Response.error(:apns, :invalid_token, "bad") |> PushX.Response.retryable?()
    false

# `should_remove_token?`

```elixir
@spec should_remove_token?(t()) :: boolean()
```

Returns true if the token should be removed from the database.

## Examples

    iex> PushX.Response.error(:apns, :invalid_token, "BadDeviceToken") |> PushX.Response.should_remove_token?()
    true

    iex> PushX.Response.error(:fcm, :server_error, "Internal") |> PushX.Response.should_remove_token?()
    false

# `success`

```elixir
@spec success(provider :: :apns | :fcm | :unknown, id :: String.t() | nil) :: t()
```

Creates a successful response.

# `success?`

```elixir
@spec success?(t()) :: boolean()
```

Returns true if the response indicates success.

## Examples

    iex> PushX.Response.success(:apns, "id-123") |> PushX.Response.success?()
    true

    iex> PushX.Response.error(:fcm, :invalid_token, "bad") |> PushX.Response.success?()
    false

---

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