# `LatticeStripe.Product`
[🔗](https://github.com/szTheory/lattice_stripe/blob/v2.2.2/lib/lattice_stripe/product.ex#L1)

Operations on Stripe Product objects.

A Product in Stripe represents a good or service that you sell. Products are
paired with Prices to define what customers pay. Products power subscriptions,
checkout sessions, invoices, and one-off charges.

## Usage

    client = LatticeStripe.Client.new!(api_key: "sk_live_...", finch: MyApp.Finch)

    # Create a product
    {:ok, product} = LatticeStripe.Product.create(client, %{
      "name" => "Premium Subscription",
      "type" => "service"
    })

    # Retrieve a product
    {:ok, product} = LatticeStripe.Product.retrieve(client, product.id)

    # List products
    {:ok, resp} = LatticeStripe.Product.list(client, %{"limit" => "10"})

    # Search products
    {:ok, resp} = LatticeStripe.Product.search(client, "active:'true' AND name~'shirt'")

    # Stream all products lazily (auto-pagination)
    client
    |> LatticeStripe.Product.stream!()
    |> Stream.take(100)
    |> Enum.each(&process_product/1)

## Operations not supported by the Stripe API

- **delete** — Stripe's Products API does not expose a delete endpoint. To stop
  a Product from being used, archive it with `update/4`:

      LatticeStripe.Product.update(client, product_id, %{"active" => "false"})

## Marketing display copy and entitlement attachments

The raw `features` and `marketing_features` fields are pricing-table display copy
from different Stripe API versions. They remain raw string-keyed maps for backwards
compatibility and are not entitlement attachments.

An access-bearing attachment is instead a `LatticeStripe.Product.Feature` with its
own `prodft_` id, connecting this Product's `prod_` id to an
`LatticeStripe.Entitlements.Feature` definition's `feat_` id. Read those attachments
from the dedicated endpoint with `LatticeStripe.Product.Feature.list/4` or
`LatticeStripe.Product.Feature.stream!/4`; do not infer access from Product marketing
copy.

## Stripe API Reference

See the [Stripe Product API](https://docs.stripe.com/api/products) for the full
object reference and available parameters.

# `t`

```elixir
@type t() :: %LatticeStripe.Product{
  active: boolean() | nil,
  attributes: [String.t()] | nil,
  caption: String.t() | nil,
  created: integer() | nil,
  default_price: String.t() | nil,
  deleted: boolean(),
  description: String.t() | nil,
  extra: map(),
  features: [map()] | nil,
  id: String.t() | nil,
  images: [String.t()] | nil,
  livemode: boolean() | nil,
  marketing_features: [map()] | nil,
  metadata: map() | nil,
  name: String.t() | nil,
  object: String.t(),
  package_dimensions: map() | nil,
  shippable: boolean() | nil,
  statement_descriptor: String.t() | nil,
  tax_code: String.t() | nil,
  type: :good | :service | String.t() | nil,
  unit_label: String.t() | nil,
  updated: integer() | nil,
  url: String.t() | nil
}
```

A Stripe Product object.

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

# `create`

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

Creates a new Product.

Sends `POST /v1/products` with the given params and returns `{:ok, %Product{}}`.

## Parameters

- `client` - A `%LatticeStripe.Client{}` struct
- `params` - Map of product attributes (e.g., `%{"name" => "...", "type" => "service"}`)
- `opts` - Per-request overrides (e.g., `[idempotency_key: "..."]`)

## Returns

- `{:ok, %Product{}}` on success
- `{:error, %LatticeStripe.Error{}}` on failure

## Example

    {:ok, product} = LatticeStripe.Product.create(client, %{
      "name" => "Premium Plan",
      "type" => "service"
    })

# `create!`

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

Like `create/3` but raises `LatticeStripe.Error` on failure.

# `from_map`

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

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

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

Per D-03, the `type` field is atomized via a whitelist: `"good"` → `:good`,
`"service"` → `:service`. 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 Products with optional filters.

Sends `GET /v1/products` and returns `{:ok, %Response{data: %List{}}}` with
typed `%Product{}` items.

## Parameters

- `client` - A `%LatticeStripe.Client{}` struct
- `params` - Filter params (e.g., `%{"limit" => "10", "active" => "true"}`)
- `opts` - Per-request overrides

## Returns

- `{:ok, %Response{data: %List{data: [%Product{}, ...]}}}` on success
- `{:error, %LatticeStripe.Error{}}` on failure

## Example

    {:ok, resp} = LatticeStripe.Product.list(client, %{"limit" => "20"})
    Enum.each(resp.data.data, &IO.inspect/1)

# `list!`

```elixir
@spec list!(LatticeStripe.Client.t(), map(), keyword()) :: LatticeStripe.Response.t()
```

Like `list/3` but raises `LatticeStripe.Error` on failure.

# `retrieve`

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

Retrieves a Product by ID.

Sends `GET /v1/products/:id` and returns `{:ok, %Product{}}`.

## Parameters

- `client` - A `%LatticeStripe.Client{}` struct
- `id` - The product ID string (e.g., `"prod_123"`)
- `opts` - Per-request overrides

## Returns

- `{:ok, %Product{}}` on success
- `{:error, %LatticeStripe.Error{}}` on failure

# `retrieve!`

```elixir
@spec retrieve!(LatticeStripe.Client.t(), String.t(), keyword()) :: t()
```

Like `retrieve/3` but raises `LatticeStripe.Error` on failure.

# `search`

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

Searches Products using Stripe's search query language.

Sends `GET /v1/products/search` with the query string and returns typed results.

## Searchable fields

`active`, `description`, `metadata`, `name`, `shippable`, `url`.

## Parameters

- `client` — A `%LatticeStripe.Client{}` struct
- `query` — Stripe search query string (e.g., `"active:'true' AND name~'shirt'"`)
- `opts` — Per-request overrides

## Returns

- `{:ok, %Response{data: %List{data: [%Product{}, ...]}}}` on success
- `{:error, %LatticeStripe.Error{}}` on failure

## Eventual consistency

Search results have eventual consistency. Under normal operating conditions,
newly created or updated objects appear in search results within ~1 minute.
During Stripe outages, propagation may be slower. Do not use `search/3` in
read-after-write flows where strict consistency is necessary. See
https://docs.stripe.com/search#data-freshness.

## Example

    {:ok, resp} = LatticeStripe.Product.search(client, "active:'true'")

# `search!`

```elixir
@spec search!(LatticeStripe.Client.t(), String.t(), keyword()) ::
  LatticeStripe.Response.t()
```

Like `search/3` but raises `LatticeStripe.Error` on failure.

# `search_stream!`

```elixir
@spec search_stream!(LatticeStripe.Client.t(), String.t(), keyword()) ::
  Enumerable.t()
```

Returns a lazy stream of all Products matching the search query (auto-pagination).

Emits individual `%Product{}` structs, fetching additional search pages as needed.
Raises `LatticeStripe.Error` if any page fetch fails.

## Parameters

- `client` - A `%LatticeStripe.Client{}` struct
- `query` - Stripe search query string
- `opts` - Per-request overrides

## Returns

An `Enumerable.t()` of `%Product{}` structs.

## Eventual consistency

Search results have eventual consistency. Under normal operating conditions,
newly created or updated objects appear in search results within ~1 minute.
During Stripe outages, propagation may be slower. Do not use `search_stream!/3`
in read-after-write flows where strict consistency is necessary. See
https://docs.stripe.com/search#data-freshness.

# `stream!`

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

Returns a lazy stream of all Products matching the given params (auto-pagination).

Emits individual `%Product{}` structs, fetching additional pages as needed.
Raises `LatticeStripe.Error` if any page fetch fails.

## Parameters

- `client` - A `%LatticeStripe.Client{}` struct
- `params` - Filter params (e.g., `%{"limit" => "100"}`)
- `opts` - Per-request overrides

## Returns

An `Enumerable.t()` of `%Product{}` structs.

## Example

    client
    |> LatticeStripe.Product.stream!()
    |> Stream.take(500)
    |> Enum.to_list()

# `update`

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

Updates a Product by ID.

Sends `POST /v1/products/:id` with the given params and returns `{:ok, %Product{}}`.

To archive a product (Stripe has no delete endpoint), pass `%{"active" => "false"}`.

## Parameters

- `client` - A `%LatticeStripe.Client{}` struct
- `id` - The product ID string
- `params` - Map of fields to update
- `opts` - Per-request overrides

## Returns

- `{:ok, %Product{}}` on success
- `{:error, %LatticeStripe.Error{}}` on failure

# `update!`

```elixir
@spec update!(LatticeStripe.Client.t(), String.t(), map(), keyword()) :: t()
```

Like `update/4` but raises `LatticeStripe.Error` on failure.

---

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