# `PaperTiger.Idempotency`
[🔗](https://github.com/EnaiaInc/paper_tiger/blob/v1.2.1/lib/paper_tiger/idempotency.ex#L1)

Implements Stripe's idempotency mechanism to prevent duplicate requests.

Stripe stores idempotency keys for 24 hours. Requests with the same
Idempotency-Key header return the cached response without re-executing.

## Usage

The `PaperTiger.Plugs.Idempotency` plug automatically handles this for POST requests.

## Implementation

- Stores responses in ETS keyed by idempotency key
- TTL: 24 hours (matches Stripe)
- Cleanup runs hourly to remove expired entries

## Examples

    # Client retries request with same key
    headers = [{"idempotency-key", "req_123"}]

    # First request executes and caches response
    Stripe.Charge.create(%{...}, headers: headers)

    # Second request returns cached response (no duplicate charge)
    Stripe.Charge.create(%{...}, headers: headers)

# `check`

```elixir
@spec check(String.t(), binary() | nil) ::
  {:cached, map()} | :new_request | :in_progress | {:conflict, binary()}
```

Checks if a request with this idempotency key has been processed.

Returns:
- `{:cached, response}` - Key exists, return cached response
- `{:conflict, old_fingerprint}` - Key exists for different payload
- `:new_request` - Key doesn't exist, proceed with request (and reserves the key atomically)
- `:in_progress` - Another request with this key is currently processing

## Race Condition Protection

Uses atomic ETS insert_new to prevent race conditions. If two requests
arrive simultaneously with the same key, only one will get `:new_request`,
the other will get `:in_progress` and should retry or wait.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `clear`

```elixir
@spec clear() :: :ok
```

Clears all idempotency keys.

Useful for test cleanup.

# `clear_namespace`

```elixir
@spec clear_namespace(pid() | :global) :: :ok
```

Clears idempotency keys for a specific namespace.

Used by `PaperTiger.Test` to clean up after each test.

# `request_fingerprint`

```elixir
@spec request_fingerprint(Plug.Conn.t()) :: binary()
```

Builds a deterministic fingerprint for idempotent request matching.

# `start_link`

```elixir
@spec start_link(keyword()) :: GenServer.on_start()
```

Starts the Idempotency GenServer.

# `store`

```elixir
@spec store(String.t(), map(), binary() | nil) :: :ok
```

Stores a response for the given idempotency key.

The response will be cached for 24 hours.

---

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