# Payments

This guide walks through the complete payment lifecycle in LatticeStripe — from creating
a customer to confirming a payment to issuing refunds. For an overview of Stripe's payment
model, see the [Stripe Payments documentation](https://docs.stripe.com/payments).
For recurring billing see [Subscriptions](subscriptions.md); for marketplace or platform
charges see [Connect](connect.md).

## Creating a Customer

Customers let you associate payments, subscriptions, and payment methods with a person or
business. Creating a customer before charging is recommended — it enables features like
saving payment methods and listing past charges.

```elixir
{:ok, customer} = LatticeStripe.Customer.create(client, %{
  "email" => "alice@example.com",
  "name" => "Alice Johnson",
  "phone" => "+1-555-123-4567",
  "metadata" => %{
    "user_id" => "usr_123",
    "plan" => "pro"
  }
})

IO.puts("Created customer: #{customer.id}")
# Created customer: cus_OtVFqSomeStripeId
```

`metadata` is a hash of up to 50 key/value string pairs. Use it to link Stripe objects
back to your own data model — it shows up in the Stripe Dashboard and is returned on
every fetch.

### Retrieving and Updating Customers

```elixir
# Retrieve a customer by ID
{:ok, customer} = LatticeStripe.Customer.retrieve(client, "cus_OtVFqSomeStripeId")

# Update the customer's name and metadata
{:ok, updated} = LatticeStripe.Customer.update(client, customer.id, %{
  "name" => "Alice Smith",
  "metadata" => %{"plan" => "enterprise"}
})
```

## Creating a PaymentIntent

A [PaymentIntent](https://docs.stripe.com/api/payment_intents) represents your intent to
collect payment from a customer. It tracks the lifecycle of the payment and handles
retries, 3D Secure authentication, and more.

```elixir
{:ok, intent} = LatticeStripe.PaymentIntent.create(client, %{
  "amount" => 4999,
  "currency" => "usd",
  "customer" => customer.id,
  "description" => "Pro plan subscription",
  "metadata" => %{"order_id" => "ord_456"}
})

IO.puts("PaymentIntent #{intent.id} — status: #{intent.status}")
# PaymentIntent pi_3OzqKZ2eZvKYlo2C1FRzQc8s — status: requires_payment_method
```

**Amount is always in the smallest currency unit.** For USD, that's cents: `4999` = $49.99.
For JPY (zero-decimal currency), `4999` = ¥4,999.

### Automatic vs. Manual Confirmation

`confirmation_method` selects who is allowed to call `confirm`; it does not itself attempt
a payment. The default `"automatic"` flow has your client-side code call Stripe.js to
confirm. For a server-owned confirmation flow (for example, backend-only flows or Stripe
Connect), create the intent with `confirmation_method: "manual"`, then call `confirm/3`:

```elixir
{:ok, intent} = LatticeStripe.PaymentIntent.create(client, %{
  "amount" => 4999,
  "currency" => "usd",
  "confirmation_method" => "manual",
  "payment_method" => "pm_card_visa"
})
```

## Confirming a PaymentIntent

For a manually confirmed PaymentIntent, `confirm/3` attempts the payment. If it returns
`:requires_action`, branch on the action type: Stripe.js handles `use_stripe_sdk` with the
client secret, while redirect actions provide a URL. Do not assume every SCA flow redirects.

```elixir
case LatticeStripe.PaymentIntent.confirm(client, intent.id, %{
  "payment_method" => "pm_card_visa"
}) do
  {:ok, confirmed} ->
    case confirmed.status do
      :succeeded ->
        IO.puts("Payment succeeded!")

      :requires_action ->
        case confirmed.next_action do
          %{"type" => "use_stripe_sdk"} ->
            # Return confirmed.client_secret to your authenticated client and let
            # Stripe.js handle the required next action there.
            IO.puts("3D Secure required — handle the next action with Stripe.js")

          %{"type" => "redirect_to_url", "redirect_to_url" => %{"url" => url}} ->
            IO.puts("Customer action required — redirect to: #{url}")

          _ ->
            IO.puts("Customer action required — inspect next_action before continuing")
        end

      other ->
        IO.puts("Unexpected status: #{other}")
    end

  {:error, %LatticeStripe.Error{type: :card_error} = err} ->
    IO.puts("Card declined: #{err.message}")
    IO.puts("Decline code: #{err.decline_code}")
end
```

> **Status values:** LatticeStripe atomizes known PaymentIntent statuses on `%PaymentIntent{}` (e.g. `:succeeded`, `:requires_action`). Stripe's API reference, Dashboard, webhooks, and search queries use the wire string names below.

The PaymentIntent status machine:
- `requires_payment_method` → attach a payment method
- `requires_confirmation` → call `confirm/3`
- `requires_action` → customer must complete authentication (e.g., 3D Secure)
- `processing` → payment is being processed (async)
- `succeeded` → payment successful
- `canceled` → terminal state

## Capturing a PaymentIntent (Manual Capture)

If you need to authorize a payment now but capture funds later — for example, when
fulfillment happens after checkout — create the PaymentIntent with
`capture_method: "manual"`:

```elixir
# Step 1: Authorize (hold funds on the card, don't capture yet)
{:ok, intent} = LatticeStripe.PaymentIntent.create(client, %{
  "amount" => 4999,
  "currency" => "usd",
  "payment_method" => "pm_card_visa",
  "capture_method" => "manual",
  "confirm" => true
})

IO.puts("Authorized: #{intent.status}")
# Authorized: requires_capture

# (Later, once the order ships or service is fulfilled)

# Step 2: Capture the authorized funds
{:ok, captured} = LatticeStripe.PaymentIntent.capture(client, intent.id)
IO.puts("Captured: #{captured.status}")
# Captured: succeeded
```

You can also capture a partial amount:

```elixir
{:ok, captured} = LatticeStripe.PaymentIntent.capture(client, intent.id, %{
  "amount_to_capture" => 2500  # Capture only $25.00 instead of $49.99
})
```

Uncaptured authorizations automatically expire after 7 days (or 2 days for some card
networks). See [Stripe's capture docs](https://docs.stripe.com/payments/capture-later).

## Canceling a PaymentIntent

Cancel a PaymentIntent that hasn't succeeded or been captured yet:

```elixir
{:ok, canceled} = LatticeStripe.PaymentIntent.cancel(client, intent.id, %{
  "cancellation_reason" => "abandoned"
})

IO.puts("Status: #{canceled.status}")
# Status: canceled
```

Valid cancellation reasons: `"duplicate"`, `"fraudulent"`, `"requested_by_customer"`,
`"abandoned"`. The `canceled` status is terminal — you cannot revive a canceled
PaymentIntent.

## Listing and Searching

### Listing with Filters

```elixir
# List recent PaymentIntents for a specific customer
{:ok, resp} = LatticeStripe.PaymentIntent.list(client, %{
  "customer" => customer.id,
  "limit" => 10
})

intents = resp.data.data
IO.puts("Found #{length(intents)} PaymentIntents")
```

### Auto-Pagination with Streams

Use `list/2` when one bounded page is the result you need. For a large collection, or one whose
size is unknown, prefer `stream!/2`: it auto-paginates lazily instead of loading every item at
once.

```elixir
# Process all succeeded PaymentIntents in the last 30 days
client
|> LatticeStripe.PaymentIntent.stream!(%{"created" => %{"gte" => thirty_days_ago}})
|> Stream.filter(fn intent -> intent.status == :succeeded end)
|> Stream.map(fn intent -> intent.amount end)
|> Enum.sum()
|> then(fn total -> IO.puts("Total revenue: $#{total / 100}") end)
```

`stream!/2` fetches a page only when enumeration needs more items. Lazy transforms such as
`Stream.filter/2` preserve that memory advantage; a terminal operation such as `Enum.to_list/1`
materializes the entire collection.

A stream is not a transaction. If a later page fails, `stream!/2` raises after any earlier items
have already been yielded. Make per-item side effects idempotent and checkpoint progress, or stage
the complete result before committing when the application requires all-or-nothing behavior.

### Search

Use `search/3` for full-text search across PaymentIntents:

```elixir
{:ok, resp} =
  LatticeStripe.PaymentIntent.search(client, "metadata['order_id']:'ord_456'")

results = resp.data.data
```

> **Note:** Stripe's Search API has eventual consistency. Newly created objects may not
> appear in search results immediately. For real-time lookups, use `list/3` with filters or
> `retrieve/3` by ID. See [Stripe Search docs](https://docs.stripe.com/search).

## Charge reconciliation

When a PaymentIntent succeeds, Stripe creates a **Charge** — the settled payment result
record. New integrations should use `LatticeStripe.PaymentIntent` to accept payments;
use `LatticeStripe.Charge` to read and reconcile those result records after the fact.
`LatticeStripe.Charge.create` at arity `3` will not be added: Charge is a read/reconciliation
resource, not a payment-initiation API. For direct server-side payment initiation, use
`LatticeStripe.PaymentIntent.create/3`:

```elixir
{:ok, intent} =
  LatticeStripe.PaymentIntent.create(client, %{
    "amount" => 4_999,
    "currency" => "usd",
    "payment_method" => "pm_card_visa",
    "confirm" => true
  })
```

A successful PaymentIntent creates the resulting Charge for reconciliation. Here
`"confirm" => true` asks Stripe to make the initial confirmation on the server; do not then
call Stripe.js to confirm that same PaymentIntent. If it returns `:requires_action`, send the
client secret to the authenticated client and use Stripe.js to handle the returned
`next_action` (or redirect only when the action explicitly supplies a redirect URL).
Customer action or SCA may still be required.

| Goal | Function | Notes |
|------|----------|-------|
| Fetch one charge by id | `retrieve/3` | Expand `balance_transaction` for fee details |
| Filter charges | `list/3` | Real-time; use filters for fresh lookups |
| Auto-paginate large sets | `stream!/3` | Lazy pagination over `list/3` |
| Full-text lookup | `search/3` | Query string syntax; **eventually consistent** |
| Post-hoc metadata | `update/4` | Metadata/description only — not payment state |
| Capture uncaptured legacy charge | `capture/4` | **Legacy direct charges only** — see [Capturing a PaymentIntent](#capturing-a-paymentintent-manual-capture) for PI flows |

### Retrieve a charge

```elixir
{:ok, charge} =
  LatticeStripe.Charge.retrieve(client, "ch_3OoLqrJ...",
    expand: ["balance_transaction"]
  )
```

### List and stream charges

```elixir
{:ok, resp} = LatticeStripe.Charge.list(client, %{"customer" => customer.id, "limit" => 10})

client
|> LatticeStripe.Charge.stream!()
|> Stream.take(100)
|> Enum.each(&process_charge/1)
```

### Search charges

```elixir
{:ok, resp} =
  LatticeStripe.Charge.search(client, "status:'succeeded' AND customer:'cus_123'")
```

> **Note:** Like PaymentIntent search, Charge search is eventually consistent. Do not use
> `search/3` to confirm a payment that just succeeded — use `retrieve/3` or follow the
> PaymentIntent state instead.

### Update metadata on a settled charge

```elixir
{:ok, charge} =
  LatticeStripe.Charge.update(client, "ch_3OoLqrJ...", %{
    "metadata" => %{"support_ticket" => "TKT-789"},
    "description" => "Order #456 — support update"
  })
```

### Capture a legacy direct charge

For uncaptured **legacy direct charges** (not PaymentIntent-initiated), use `capture/4`:

```elixir
{:ok, charge} = LatticeStripe.Charge.capture(client, "ch_3OoLqrJ...")
```

For PaymentIntent manual capture, use [`PaymentIntent.capture/4`](#capturing-a-paymentintent-manual-capture) instead.

Connect platforms reconciling application fees should walk `balance_transaction.fee_details` —
see [Connect Money Movement](connect-money-movement.md) for the full fee walkthrough.

**Operator guides:** [Production Checklist](production-checklist.md) §Support and audit lookups;
[Event Debugging](event-debugging.md) §`charge.*` events.

## Refunding a Payment

To return funds to a customer, create a Refund referencing the original PaymentIntent:

### Full Refund

```elixir
{:ok, refund} = LatticeStripe.Refund.create(client, %{
  "payment_intent" => intent.id,
  "reason" => "requested_by_customer"
})

IO.puts("Refund #{refund.id} — status: #{refund.status}")
# Refund re_3OzqKZ2eZvKYlo2C1FRzQc8s — status: succeeded
```

### Partial Refund

Specify an `amount` to refund only part of the original charge:

```elixir
# Refund $10.00 of a $49.99 payment
{:ok, refund} = LatticeStripe.Refund.create(client, %{
  "payment_intent" => intent.id,
  "amount" => 1000
})
```

### Refund Reasons

Valid reasons: `"duplicate"`, `"fraudulent"`, `"requested_by_customer"`. The reason affects
how the refund appears in the Stripe Dashboard and any reporting. Omitting the reason is
also valid.

### Listing Refunds

```elixir
{:ok, resp} = LatticeStripe.Refund.list(client, %{
  "payment_intent" => intent.id
})

refunds = resp.data.data
```

## Working with Idempotency Keys

LatticeStripe generates an idempotency key for every POST and reuses it for automatic retries
inside that one library call. For durable work that may cross a process crash, job restart, or
message redelivery, pass an explicit key derived from the business operation:

```elixir
{:ok, intent} = LatticeStripe.PaymentIntent.create(client, %{
  "amount" => 4999,
  "currency" => "usd",
  "customer" => customer.id
},
  idempotency_key: "payment_intent:create:order:#{order.id}:v1"
)
```

Reuse that key only for another attempt at the same operation with the same parameters. Do not
create a fresh random key on each job attempt or delegate key generation to a magic callback:
business identity belongs with the operation. When a timed-out request has an indeterminate
outcome, reconcile through a webhook or retrieve before deciding what to do next. See
[Client Configuration](client-configuration.md#idempotency-keys-that-survive-application-retries)
for the complete policy.

## Common Pitfalls

**Amount is in the smallest currency unit (cents for USD).**
`4999` means $49.99, not $4,999. Always think in cents when working with Stripe. This is
the single most common mistake when integrating Stripe for the first time.

**PaymentIntent status machine — transitions only go one direction.**
You can't capture a canceled PaymentIntent. You can't confirm an already-succeeded one.
Always check `intent.status` before performing an action, and handle the case where the
intent is in an unexpected state.

**Idempotency keys must be unique per distinct request.**
If you want to create two different payments for the same customer on the same order, use
different keys (for example, include the operation kind or line-item identity). Reusing a key
with different parameters produces an idempotency error, not a new payment.

**Automatic confirmation vs. manual confirmation.**
With "automatic" confirmation, your frontend calls Stripe.js to confirm the payment. With
`confirmation_method: "manual"`, your backend must call `confirm/3`; setting that field
alone does not confirm anything. In either flow, a `:requires_action` response may require
Stripe.js to handle a `use_stripe_sdk` next action or a browser redirect when Stripe supplies
`redirect_to_url`.

**Search API has eventual consistency.**
Newly created objects may not appear in search results for up to a few seconds. Don't use
search for real-time workflows — use `retrieve/3` or `list/3` with filters instead. See
[Stripe's search documentation](https://docs.stripe.com/search) for consistency guarantees.

## See also

- [Checkout](checkout.md) — Stripe-hosted payment pages for the same flows
- [Subscriptions](subscriptions.md) — recurring billing on top of the payment primitives
- [Tax](tax.md) — standalone `Tax.Calculation` → `Tax.Transaction` flow when you own the cart and tax logic
- [Client Configuration](client-configuration.md) — durable idempotency and request overrides
- [Error Handling](error-handling.md) — card errors, retries, and idempotency
- [Webhooks](webhooks.md) — confirm payment completion via `payment_intent.succeeded`
