Payment catalog, purchase ledger, and entitlements.
Provider-specific integrations validate or create transactions, but this context remains the source of truth for what a user owns inside the game.
Summary
Functions
Counts list_catalog/2's entries.
Counts list_user_entitlements/2's entitlements; takes :include_inactive.
Whether the user has EVER held key, active or not. What a once-per-account
grant (a trial) checks, since the row outlives its end.
The user's key row, active or not, or nil.
Grant an entitlement without a purchase: a trial, a contributor's reward,
a support gesture. Upserts the one (user, key) row.
Whether the user holds key right now: an active row with no end, or an end
still ahead.
Active catalog entries, optionally for one provider. Pass :page and
:page_size for one page; without them, every entry.
The user's entitlements, by key: active ones only unless
include_inactive: true. Pass :page and :page_size for one page.
Stamp a provider event as fully handled. Only then does a retry of the same event id count as a duplicate.
Functions
See Gamend.Payments.StripeEvents.cancel_stripe_subscription_at_period_end/2.
@spec count_catalog(String.t() | nil) :: non_neg_integer()
Counts list_catalog/2's entries.
@spec count_user_entitlements(Ecto.UUID.t(), keyword()) :: non_neg_integer()
Counts list_user_entitlements/2's entitlements; takes :include_inactive.
@spec create_product(map()) :: {:ok, Gamend.Payments.Product.t()} | {:error, Ecto.Changeset.t()}
@spec create_provider_product(map()) :: {:ok, Gamend.Payments.ProviderProduct.t()} | {:error, Ecto.Changeset.t()}
@spec create_purchase( Gamend.Accounts.User.t(), Gamend.Payments.ProviderProduct.t(), map() ) :: {:ok, Gamend.Payments.Purchase.t()} | {:error, Ecto.Changeset.t()}
@spec create_steam_checkout(Gamend.Accounts.User.t(), map()) :: {:ok, %{ purchase: Gamend.Payments.Purchase.t(), provider_transaction_id: String.t() | nil, steam_url: String.t() | nil }} | {:error, term()}
See Gamend.Payments.StripeEvents.create_stripe_billing_portal/2.
@spec entitlement_ever?(Ecto.UUID.t(), String.t()) :: boolean()
Whether the user has EVER held key, active or not. What a once-per-account
grant (a trial) checks, since the row outlives its end.
@spec finalize_steam_purchase(Gamend.Accounts.User.t(), map()) :: {:ok, %{purchase: Gamend.Payments.Purchase.t()}} | {:error, term()}
@spec fulfill_purchase(Gamend.Payments.Purchase.t(), map()) :: {:ok, Gamend.Payments.Purchase.t()} | {:error, term()}
@spec get_product(Ecto.UUID.t()) :: Gamend.Payments.Product.t() | nil
@spec get_product_by_sku(String.t()) :: Gamend.Payments.Product.t() | nil
@spec get_provider_product(Ecto.UUID.t()) :: Gamend.Payments.ProviderProduct.t() | nil
@spec get_provider_product(String.t(), String.t()) :: Gamend.Payments.ProviderProduct.t() | nil
@spec get_purchase(Ecto.UUID.t()) :: Gamend.Payments.Purchase.t() | nil
@spec get_purchase_by_order_id(String.t()) :: Gamend.Payments.Purchase.t() | nil
@spec get_purchase_by_provider_original_transaction(String.t(), String.t()) :: Gamend.Payments.Purchase.t() | nil
@spec get_purchase_by_provider_transaction(String.t(), String.t()) :: Gamend.Payments.Purchase.t() | nil
@spec get_user_entitlement_by_key(Ecto.UUID.t(), String.t()) :: Gamend.Payments.Entitlement.t() | nil
The user's key row, active or not, or nil.
@spec grant_entitlement(Ecto.UUID.t(), String.t(), keyword()) :: {:ok, Gamend.Payments.Entitlement.t()} | {:error, term()}
Grant an entitlement without a purchase: a trial, a contributor's reward,
a support gesture. Upserts the one (user, key) row.
Never shortens what the user already has: an active row with no end (a
lifetime purchase) keeps no end, and an active row ending later than
:expires_at keeps its later end. A row a purchase created keeps its
source_purchase_id, so its provider sync still finds it.
Options: :expires_at (a DateTime, nil for no end), :metadata (a map
merged into the row's, e.g. %{"source" => "trial", "granted_by" => id}).
@spec has_entitlement?(Ecto.UUID.t(), String.t()) :: boolean()
Whether the user holds key right now: an active row with no end, or an end
still ahead.
Answered from entitlement_rows/1, so a page asking about several keys (a
paid plan and its trial, on every render) costs one query between changes.
@spec list_catalog(String.t() | nil, keyword()) :: [ Gamend.Payments.ProviderProduct.t() ]
Active catalog entries, optionally for one provider. Pass :page and
:page_size for one page; without them, every entry.
@spec list_products(keyword()) :: [Gamend.Payments.Product.t()]
@spec list_user_entitlements(Ecto.UUID.t(), keyword()) :: [ Gamend.Payments.Entitlement.t() ]
The user's entitlements, by key: active ones only unless
include_inactive: true. Pass :page and :page_size for one page.
@spec list_user_purchases(Ecto.UUID.t(), keyword()) :: [Gamend.Payments.Purchase.t()]
@spec mark_event_processed(Gamend.Payments.ProviderEvent.t()) :: {:ok, Gamend.Payments.ProviderEvent.t()} | {:error, Ecto.Changeset.t()}
Stamp a provider event as fully handled. Only then does a retry of the same event id count as a duplicate.
@spec product_entitlement_key(Gamend.Payments.Product.t()) :: String.t()
See Gamend.Payments.StripeEvents.reconcile_stripe_purchase/1.
@spec record_provider_event(String.t(), String.t(), String.t(), map(), map()) :: {:ok, Gamend.Payments.ProviderEvent.t(), boolean()} | {:error, Ecto.Changeset.t()}
@spec revoke_purchase(Gamend.Payments.Purchase.t(), map()) :: {:ok, Gamend.Payments.Purchase.t()} | {:error, term()}
@spec update_product(Gamend.Payments.Product.t(), map()) :: {:ok, Gamend.Payments.Product.t()} | {:error, Ecto.Changeset.t()}
@spec update_provider_product(Gamend.Payments.ProviderProduct.t(), map()) :: {:ok, Gamend.Payments.ProviderProduct.t()} | {:error, Ecto.Changeset.t()}
@spec validate_store_purchase(Gamend.Accounts.User.t(), String.t(), map()) :: {:ok, %{purchase: Gamend.Payments.Purchase.t(), seen_before: boolean()}} | {:error, term()}