Current section
Files
Jump to
Current section
Files
lib/phoenix_kit/modules/billing/providers/providers.ex
defmodule PhoenixKit.Modules.Billing.Providers do
@moduledoc """
Provider registry and helper functions for payment providers.
This module serves as the central point for working with payment providers.
It handles provider lookup, availability checking, and configuration.
## Available Providers
- `:stripe` - Stripe payments (cards, wallets)
- `:paypal` - PayPal payments
- `:razorpay` - Razorpay payments (India)
## Usage
# Get a provider module
provider = Providers.get_provider(:stripe)
provider.create_checkout_session(invoice, opts)
# List available providers
Providers.list_available_providers()
#=> [:stripe, :paypal]
# Check if provider is available
Providers.provider_enabled?(:stripe)
#=> true
"""
alias PhoenixKit.Modules.Billing.Providers.Provider
alias PhoenixKit.Modules.Billing.Providers.Types.ProviderInfo
alias PhoenixKit.Settings
@providers %{
stripe: PhoenixKit.Modules.Billing.Providers.Stripe,
paypal: PhoenixKit.Modules.Billing.Providers.PayPal,
razorpay: PhoenixKit.Modules.Billing.Providers.Razorpay
}
@provider_names Map.keys(@providers)
@doc """
Returns the provider module for the given provider name.
## Parameters
- `name` - Provider name as atom or string
## Returns
- Provider module if found
- `nil` if provider not found
## Examples
iex> Providers.get_provider(:stripe)
PhoenixKit.Modules.Billing.Providers.Stripe
iex> Providers.get_provider("paypal")
PhoenixKit.Modules.Billing.Providers.PayPal
iex> Providers.get_provider(:unknown)
nil
"""
@spec get_provider(atom() | String.t()) :: module() | nil
def get_provider(name) when is_atom(name), do: @providers[name]
def get_provider(name) when is_binary(name), do: @providers[String.to_existing_atom(name)]
@doc """
Returns a list of all provider names.
## Examples
iex> Providers.all_providers()
[:stripe, :paypal, :razorpay]
"""
@spec all_providers() :: [atom()]
def all_providers, do: @provider_names
@doc """
Returns a list of available (enabled and configured) provider names.
Checks each provider's `available?/0` callback to determine availability.
## Examples
iex> Providers.list_available_providers()
[:stripe, :paypal]
"""
@spec list_available_providers() :: [atom()]
def list_available_providers do
@providers
|> Enum.filter(fn {_name, module} ->
Code.ensure_loaded?(module) && function_exported?(module, :available?, 0) &&
module.available?()
end)
|> Enum.map(fn {name, _module} -> name end)
end
@doc """
Checks if a provider is enabled and available.
## Parameters
- `name` - Provider name as atom or string
## Returns
- `true` if provider is available
- `false` if provider is not available or not found
## Examples
iex> Providers.provider_enabled?(:stripe)
true
iex> Providers.provider_enabled?(:unknown)
false
"""
@spec provider_enabled?(atom() | String.t()) :: boolean()
def provider_enabled?(name) do
case get_provider(name) do
nil -> false
module -> Code.ensure_loaded?(module) && module.available?()
end
end
@doc """
Checks if a provider exists (regardless of availability).
## Examples
iex> Providers.provider_exists?(:stripe)
true
iex> Providers.provider_exists?(:bitcoin)
false
"""
@spec provider_exists?(atom() | String.t()) :: boolean()
def provider_exists?(name) when is_atom(name), do: Map.has_key?(@providers, name)
def provider_exists?(name) when is_binary(name) do
provider_exists?(String.to_existing_atom(name))
rescue
ArgumentError -> false
end
@doc """
Gets the setting key for a provider's enabled status.
## Examples
iex> Providers.enabled_setting_key(:stripe)
"billing_stripe_enabled"
"""
@spec enabled_setting_key(atom()) :: String.t()
def enabled_setting_key(provider) do
"billing_#{provider}_enabled"
end
@doc """
Checks if a provider is enabled in settings.
This is a lower-level check that only looks at the setting,
not whether the provider is fully configured.
## Examples
iex> Providers.setting_enabled?(:stripe)
true
"""
@spec setting_enabled?(atom()) :: boolean()
def setting_enabled?(provider) do
Settings.get_setting(enabled_setting_key(provider), "false") == "true"
end
@doc """
Creates a checkout session using the specified provider.
Convenience function that looks up the provider and calls
`create_checkout_session/2`.
## Parameters
- `provider` - Provider name
- `invoice` - Invoice to pay
- `opts` - Options passed to provider
## Returns
- `{:ok, checkout_session}` - Session created
- `{:error, :provider_not_found}` - Provider doesn't exist
- `{:error, :provider_not_available}` - Provider not configured
- `{:error, reason}` - Provider-specific error
"""
@spec create_checkout_session(atom() | String.t(), map(), keyword()) ::
{:ok, Provider.checkout_session()} | {:error, term()}
def create_checkout_session(provider, invoice, opts \\ []) do
with {:ok, module} <- get_available_provider(provider) do
module.create_checkout_session(invoice, opts)
end
end
@doc """
Creates a setup session using the specified provider.
## Parameters
- `provider` - Provider name
- `user` - User to save payment method for
- `opts` - Options passed to provider
## Returns
- `{:ok, setup_session}` - Session created
- `{:error, reason}` - Failed
"""
@spec create_setup_session(atom() | String.t(), map(), keyword()) ::
{:ok, Provider.setup_session()} | {:error, term()}
def create_setup_session(provider, user, opts \\ []) do
with {:ok, module} <- get_available_provider(provider) do
module.create_setup_session(user, opts)
end
end
@doc """
Charges a saved payment method using the appropriate provider.
## Parameters
- `payment_method` - Saved payment method record (must include :provider)
- `amount` - Amount to charge
- `opts` - Options passed to provider
## Returns
- `{:ok, charge_result}` - Charge successful
- `{:error, reason}` - Charge failed
"""
@spec charge_payment_method(map(), Decimal.t(), keyword()) ::
{:ok, Provider.charge_result()} | {:error, term()}
def charge_payment_method(%{provider: provider} = payment_method, amount, opts \\ []) do
with {:ok, module} <- get_available_provider(provider) do
module.charge_payment_method(payment_method, amount, opts)
end
end
@doc """
Verifies a webhook signature for the specified provider.
## Parameters
- `provider` - Provider name
- `payload` - Raw request body
- `signature` - Signature from headers
- `secret` - Webhook secret
## Returns
- `:ok` - Signature valid
- `{:error, :invalid_signature}` - Signature invalid
- `{:error, :provider_not_found}` - Provider doesn't exist
"""
@spec verify_webhook_signature(atom() | String.t(), binary(), String.t(), String.t()) ::
:ok | {:error, term()}
def verify_webhook_signature(provider, payload, signature, secret) do
case get_provider(provider) do
nil -> {:error, :provider_not_found}
module -> module.verify_webhook_signature(payload, signature, secret)
end
end
@doc """
Handles a webhook event for the specified provider.
## Parameters
- `provider` - Provider name
- `payload` - Decoded JSON payload
## Returns
- `{:ok, webhook_event}` - Event parsed
- `{:error, reason}` - Failed to parse
"""
@spec handle_webhook_event(atom() | String.t(), map()) ::
{:ok, Provider.webhook_event()} | {:error, term()}
def handle_webhook_event(provider, payload) do
case get_provider(provider) do
nil -> {:error, :provider_not_found}
module -> module.handle_webhook_event(payload)
end
end
@doc """
Creates a refund using the appropriate provider.
## Parameters
- `provider` - Provider name
- `provider_transaction_id` - Provider's transaction ID
- `amount` - Amount to refund (nil for full refund)
- `opts` - Options
## Returns
- `{:ok, refund_result}` - Refund created
- `{:error, reason}` - Refund failed
"""
@spec create_refund(atom() | String.t(), String.t(), Decimal.t() | nil, keyword()) ::
{:ok, Provider.refund_result()} | {:error, term()}
def create_refund(provider, provider_transaction_id, amount, opts \\ []) do
with {:ok, module} <- get_available_provider(provider) do
module.create_refund(provider_transaction_id, amount, opts)
end
end
@doc """
Returns display information for a provider.
## Examples
iex> Providers.provider_info(:stripe)
%{name: "Stripe", icon: "stripe", color: "#635BFF"}
"""
@spec provider_info(atom()) :: ProviderInfo.t()
def provider_info(:stripe) do
%ProviderInfo{
name: "Stripe",
icon: "stripe",
color: "#635BFF",
description: "Accept cards, wallets, and more"
}
end
def provider_info(:paypal) do
%ProviderInfo{
name: "PayPal",
icon: "paypal",
color: "#003087",
description: "PayPal and credit/debit cards"
}
end
def provider_info(:razorpay) do
%ProviderInfo{
name: "Razorpay",
icon: "razorpay",
color: "#072654",
description: "Popular payment gateway in India"
}
end
def provider_info(_) do
%ProviderInfo{name: "Unknown", icon: "credit-card", color: "#6B7280"}
end
# Private helpers
defp get_available_provider(provider) do
case get_provider(provider) do
nil ->
{:error, :provider_not_found}
module ->
if Code.ensure_loaded?(module) && function_exported?(module, :available?, 0) &&
module.available?() do
{:ok, module}
else
{:error, :provider_not_available}
end
end
end
end