Packages
phoenix_kit
1.7.49
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/modules/billing/schemas/payment_method.ex
defmodule PhoenixKit.Modules.Billing.PaymentMethod do
@moduledoc """
Schema for saved payment methods (cards, bank accounts, wallets).
Payment methods are saved via provider setup sessions and can be
used for recurring payments without requiring user interaction.
## Provider Integration
Each provider stores payment method tokens:
- **Stripe**: `pm_*` payment method IDs + `cus_*` customer IDs
- **PayPal**: Billing agreement IDs
- **Razorpay**: Token IDs + customer IDs
## Security
- No raw card data is ever stored
- Only tokenized references from providers
- Tokens are provider-specific and non-transferable
## Lifecycle
- Created via setup session (hosted checkout for saving card)
- Can be set as default for user
- Can be used for subscription renewals
- Can be removed (deletes token from provider)
- Automatically marked expired based on exp_month/exp_year
"""
use Ecto.Schema
import Ecto.Changeset
@types ~w(card bank_account wallet paypal)
@statuses ~w(active expired removed failed)
@primary_key {:uuid, UUIDv7, autogenerate: true}
schema "phoenix_kit_payment_methods" do
field :id, :integer, read_after_writes: true
field :provider, :string
field :provider_payment_method_id, :string
field :provider_customer_id, :string
# Type and display info
field :type, :string, default: "card"
field :brand, :string
field :last4, :string
field :exp_month, :integer
field :exp_year, :integer
# Status
field :is_default, :boolean, default: false
field :status, :string, default: "active"
# Metadata
field :label, :string
field :metadata, :map, default: %{}
# Association
# legacy
field :user_id, :integer
belongs_to :user, PhoenixKit.Users.Auth.User,
foreign_key: :user_uuid,
references: :uuid,
type: UUIDv7
timestamps(type: :utc_datetime)
end
@doc """
Creates a changeset for a payment method.
"""
def changeset(payment_method, attrs) do
payment_method
|> cast(attrs, [
:provider,
:provider_payment_method_id,
:provider_customer_id,
:type,
:brand,
:last4,
:exp_month,
:exp_year,
:is_default,
:status,
:label,
:metadata,
:user_id,
:user_uuid
])
|> validate_required([:provider, :provider_payment_method_id, :user_uuid])
|> validate_inclusion(:type, @types)
|> validate_inclusion(:status, @statuses)
|> validate_number(:exp_month, greater_than: 0, less_than_or_equal_to: 12)
|> validate_number(:exp_year, greater_than_or_equal_to: 2020)
|> foreign_key_constraint(:user_uuid)
|> unique_constraint([:provider, :provider_payment_method_id],
name: :phoenix_kit_payment_methods_provider_pm_id_index
)
end
@doc """
Changeset for setting as default payment method.
"""
def set_default_changeset(payment_method) do
payment_method
|> change(%{is_default: true})
end
@doc """
Changeset for marking as removed.
"""
def remove_changeset(payment_method) do
payment_method
|> change(%{status: "removed"})
end
@doc """
Changeset for marking as expired.
"""
def expire_changeset(payment_method) do
payment_method
|> change(%{status: "expired"})
end
# ============================================
# Status Helpers
# ============================================
@doc """
Returns true if the payment method is usable for charges.
"""
def usable?(%__MODULE__{status: "active"} = pm) do
not expired?(pm)
end
def usable?(_), do: false
@doc """
Returns true if the card has expired based on exp_month/exp_year.
"""
def expired?(%__MODULE__{exp_month: nil}), do: false
def expired?(%__MODULE__{exp_year: nil}), do: false
def expired?(%__MODULE__{exp_month: month, exp_year: year}) do
now = Date.utc_today()
current_year = now.year
current_month = now.month
year < current_year or (year == current_year and month < current_month)
end
@doc """
Returns a display string for the payment method (e.g., "Visa **** 4242").
"""
def display_name(%__MODULE__{type: "card", brand: brand, last4: last4})
when not is_nil(brand) and not is_nil(last4) do
brand_name = String.capitalize(brand || "Card")
"#{brand_name} **** #{last4}"
end
def display_name(%__MODULE__{type: "paypal"}) do
"PayPal"
end
def display_name(%__MODULE__{type: "bank_account", last4: last4}) when not is_nil(last4) do
"Bank Account **** #{last4}"
end
def display_name(%__MODULE__{type: _type, label: label}) when not is_nil(label) do
label
end
def display_name(%__MODULE__{type: type}) do
String.capitalize(type)
end
@doc """
Returns expiration string (e.g., "12/25").
"""
def expiration_string(%__MODULE__{exp_month: nil}), do: nil
def expiration_string(%__MODULE__{exp_year: nil}), do: nil
def expiration_string(%__MODULE__{exp_month: month, exp_year: year}) do
month_str = String.pad_leading(to_string(month), 2, "0")
year_str = String.slice(to_string(year), -2, 2)
"#{month_str}/#{year_str}"
end
@doc """
Returns the icon class for the card brand (for UI display).
"""
def brand_icon(%__MODULE__{brand: brand}) do
case String.downcase(brand || "") do
"visa" -> "fa-cc-visa"
"mastercard" -> "fa-cc-mastercard"
"amex" -> "fa-cc-amex"
"discover" -> "fa-cc-discover"
"diners" -> "fa-cc-diners-club"
"jcb" -> "fa-cc-jcb"
_ -> "fa-credit-card"
end
end
end