LatticeStripe.Entitlements.Feature (LatticeStripe v2.0.0)

Copy Markdown View Source

Stripe Entitlements Feature — the definition of a feature you sell.

A feature is created once per capability in your product catalog (wire object entitlements.feature, ids prefixed feat_), given an immutable lookup_key that your own system keys on, and then attached to Products so that customers who buy those Products receive a matching LatticeStripe.Entitlements.ActiveEntitlement.

The surface is create/3, retrieve/3, update/4 and list/3 — plus stream!/3 — and that is the complete surface, not a partial one. Stripe ships no DELETE for features, so nothing here is deferred.

See Entitlements for the end-to-end story.

Archiving

Two words, one concept. The feature object carries a boolean field named active. The list endpoint takes a boolean filter named archived, whose sense is inverted: a feature with active: false is one the archived filter calls archived. Nothing in a function signature surfaces that split.

Stripe's own description of the active field states the consequence: inactive features cannot be attached to new products and will not be returned from the features list endpoint. So list/3 and stream!/3 return a filtered view by default — the sellable catalog, not the whole catalog.

Archived is not deleted

A reconciler that diffs Stripe's feature catalog against a local configuration and passes no filter will see archived features simply vanish from the response, and a naive diff reports them as deleted. Acting on that — revoking access, pruning local rows — can cut off a customer who still legitimately holds the entitlement.

When the job is catalog reconciliation rather than "what can I sell today", pass %{"archived" => true} explicitly and reconcile both views.

There is deliberately no archive/3 verb. Stripe ships no archive endpoint, and the house rule is that explicit verbs mirror explicit Stripe endpoints — a wrapper over update/4 is named after the exact wire field it sets, which here would make it set_active/4. Archiving is therefore update/4 with active: false, and unarchiving is update/4 with active: true:

{:ok, archived} =
  LatticeStripe.Entitlements.Feature.update(client, feature.id, %{"active" => false})

LatticeStripe.Price and LatticeStripe.Product use the identical active: false archive mechanic and likewise ship no archive verb.

Using lookup_key as your system identifier

Filter list/3 by lookup key:

{:ok, resp} =
  LatticeStripe.Entitlements.Feature.list(client, %{"lookup_key" => "premium_support"})

That returns a list, not a singleton, even when exactly one feature matches. Stripe defines no unique-lookup retrieval, so there is no retrieve_by_lookup_key/3 here — a helper would have to invent semantics for the zero-result and multi-result cases that Stripe does not define. You decide what those mean for your system.

The unlock is that lookup_key is immutable after create. It is absent from the update request body schema entirely, so Stripe silently ignores an attempt to change it rather than erroring. That immutability is what makes it safe to key host application configuration on the lookup key instead of on the generated feat_ id — the string you chose at create time is the string that will still be there.

Relationship to other feature surfaces

This module is not LatticeStripe.Product.Feature. This module is the entitlement feature definition — wire object entitlements.feature, ids prefixed feat_. LatticeStripe.Product.Feature is the product attachment — wire object product_feature, ids prefixed prodft_ — which records that a given Product grants a given feature. The attachment carries the full definition under its entitlement_feature field as a direct reference; that field is never a bare id string, so it always decodes to a LatticeStripe.Entitlements.Feature.

Summary

Functions

Create an entitlement feature.

Decode a Stripe-shaped string-keyed map into a %Feature{}.

List entitlement features.

Retrieve an entitlement feature by ID.

Returns a lazy stream of all entitlement features (auto-pagination).

Update an entitlement feature.

Types

t()

@type t() :: %LatticeStripe.Entitlements.Feature{
  active: boolean() | nil,
  extra: map(),
  id: String.t() | nil,
  livemode: boolean() | nil,
  lookup_key: String.t() | nil,
  metadata: map() | nil,
  name: String.t() | nil,
  object: String.t() | nil
}

Functions

create(client, params, opts \\ [])

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

Create an entitlement feature.

Requires lookup_key and name params (string keys — Stripe wire format). Both are guarded before any network call: the guard raises ArgumentError and checks key presence, not value emptiness, so a lookup_key whose value is "" or nil passes the guard and fails at Stripe instead.

params has no default. An argument-less create could only ever raise, so the arity that would allow one does not exist.

lookup_key is immutable after create — see the moduledoc section Using lookup_key as your system identifier. Choose it deliberately.

Optional params: metadata, expand.

{:ok, feature} =
  LatticeStripe.Entitlements.Feature.create(client, %{
    "lookup_key" => "premium_support",
    "name" => "Premium Support"
  })

create!(client, params, opts \\ [])

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

Bang variant of create/3. Raises LatticeStripe.Error on failure.

from_map(feature)

@spec from_map(map() | t() | nil) :: t() | nil

Decode a Stripe-shaped string-keyed map into a %Feature{}.

Idempotent: from_map/1 applied to an already-decoded %Feature{} returns it unchanged, and from_map(nil) returns nil. Unknown top-level keys land in :extra.

list(client, params \\ %{}, opts \\ [])

@spec list(LatticeStripe.Client.t(), map(), keyword()) ::
  {:ok, LatticeStripe.Response.t()} | {:error, LatticeStripe.Error.t()}

List entitlement features.

Unlike LatticeStripe.Entitlements.ActiveEntitlement.list/3, no filter is required — features are account-wide catalog objects, not customer-scoped.

Supports Stripe's archived (boolean) and lookup_key filters, plus limit (default 10, max 100), starting_after, ending_before and expand.

archived defaults to omitting archived features. A catalog reconciler that passes no filter sees a filtered view and will report archived features as deleted — read the moduledoc section Archiving before diffing this against a local catalog.

A lookup_key filter returns a list, not a singleton, even when exactly one feature matches.

list!(client, params \\ %{}, opts \\ [])

Bang variant of list/3. Raises LatticeStripe.Error on failure.

retrieve(client, id, opts \\ [])

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

Retrieve an entitlement feature by ID.

The id is the feat_-prefixed identifier returned by create/3, list/3 or stream!/3. There is no retrieval by lookup_key — Stripe defines none; filter list/3 instead.

retrieve!(client, id, opts \\ [])

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

Bang variant of retrieve/3. Raises LatticeStripe.Error on failure.

stream!(client, params \\ %{}, opts \\ [])

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

Returns a lazy stream of all entitlement features (auto-pagination).

Emits individual %Feature{} structs, following has_more and fetching each subsequent page as the stream is consumed. Raises LatticeStripe.Error if any page fetch fails, so a partial enumeration surfaces as an error rather than as a short list.

This is the entry point for catalog drift detection: limit defaults to 10, so a single list/3 call over a catalog of more than ten features silently returns a partial set. Filters pass through to every page, so pair it with %{"archived" => true} when reconciling.

client
|> LatticeStripe.Entitlements.Feature.stream!()
|> Enum.map(& &1.lookup_key)

There is no non-bang stream/3 twin — a lazy stream cannot return an error tuple at construction time for a failure that happens pages later.

update(client, id, params, opts \\ [])

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

Update an entitlement feature.

Stripe accepts active, name, metadata and expand; other keys in params are passed through for forward compatibility. lookup_key is not an accepted update param — Stripe ignores it silently rather than erroring, which is precisely what makes it a safe key for host configuration.

This is the archive operation. There is no archive/3 verb; see the moduledoc section Archiving for why.

{:ok, archived} =
  LatticeStripe.Entitlements.Feature.update(client, "feat_123", %{"active" => false})

update!(client, id, params, opts \\ [])

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

Bang variant of update/4. Raises LatticeStripe.Error on failure.