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/phoenix_kit/migrations/postgres/v33.ex
defmodule PhoenixKit.Migrations.Postgres.V33 do
@moduledoc """
PhoenixKit V33 Migration: Payment Providers and Subscriptions
This migration introduces payment provider integrations (Stripe, PayPal, Razorpay)
and the subscription system with internal billing control.
## Design Philosophy
PhoenixKit uses **Internal Subscription Control** - subscriptions are managed
in our database as the source of truth, not by payment providers. This approach:
- Allows using providers without subscription API support
- Gives full control over subscription lifecycle
- Enables pause, resume, proration calculations
- Supports multiple providers for the same subscription
## Changes
### Payment Methods Table (phoenix_kit_payment_methods)
- Saved payment methods (cards, wallets) for recurring billing
- Tokenized data from providers (no raw card data stored)
- Display info for user (last4, brand, expiration)
### Subscription Plans Table (phoenix_kit_subscription_plans)
- Subscription pricing plans
- Interval configuration (day/week/month/year)
- Trial period support
- Feature list (JSONB)
### Subscriptions Table (phoenix_kit_subscriptions)
- User subscriptions (master record)
- Status tracking with grace period and dunning
- Billing cycle management
- Payment method association
### Payment Provider Configs Table (phoenix_kit_payment_provider_configs)
- Provider credentials (encrypted)
- Webhook secrets
- Test/Live mode configuration
### Webhook Events Table (phoenix_kit_webhook_events)
- Webhook event logging for idempotency
- Event processing status tracking
- Retry count for failed events
### Modifications to Existing Tables
- Orders: checkout session fields
- Invoices: subscription reference
### Settings Seeds
- Provider enable/disable settings
- Subscription grace period configuration
- Dunning retry configuration
"""
use Ecto.Migration
@doc """
Run the V33 migration to add payment providers and subscriptions.
"""
def up(%{prefix: prefix} = _opts) do
# ===========================================
# 1. PAYMENT METHODS TABLE
# ===========================================
create_if_not_exists table(:phoenix_kit_payment_methods, prefix: prefix) do
add :user_id, :integer, null: false
add :provider, :string, size: 20, null: false
add :provider_payment_method_id, :string, null: false
add :provider_customer_id, :string
# Payment method type
add :type, :string, size: 20, null: false, default: "card"
add :brand, :string, size: 20
add :last4, :string, size: 4
add :exp_month, :integer
add :exp_year, :integer
# Display name for user
add :display_name, :string
# Status
add :is_default, :boolean, null: false, default: false
add :status, :string, size: 20, null: false, default: "active"
# Additional data
add :metadata, :map, null: false, default: %{}
timestamps(type: :utc_datetime_usec)
end
create_if_not_exists index(:phoenix_kit_payment_methods, [:user_id],
name: :phoenix_kit_payment_methods_user_id_idx,
prefix: prefix
)
create_if_not_exists index(:phoenix_kit_payment_methods, [:user_id, :is_default],
name: :phoenix_kit_payment_methods_user_default_idx,
prefix: prefix
)
create_if_not_exists unique_index(
:phoenix_kit_payment_methods,
[:provider, :provider_payment_method_id],
name: :phoenix_kit_payment_methods_provider_id_uidx,
prefix: prefix
)
# ===========================================
# 2. SUBSCRIPTION PLANS TABLE
# ===========================================
create_if_not_exists table(:phoenix_kit_subscription_plans, prefix: prefix) do
add :name, :string, null: false
add :slug, :string, null: false
add :description, :text
# Pricing
add :price, :decimal, precision: 15, scale: 2, null: false
add :currency, :string, size: 3, null: false, default: "EUR"
# Billing interval
add :interval, :string, size: 10, null: false, default: "month"
add :interval_count, :integer, null: false, default: 1
# Trial
add :trial_days, :integer, null: false, default: 0
# Features (JSONB array)
add :features, {:array, :map}, null: false, default: []
# Status
add :active, :boolean, null: false, default: true
add :sort_order, :integer, null: false, default: 0
# Additional data
add :metadata, :map, null: false, default: %{}
timestamps(type: :utc_datetime_usec)
end
create_if_not_exists unique_index(:phoenix_kit_subscription_plans, [:slug],
name: :phoenix_kit_subscription_plans_slug_uidx,
prefix: prefix
)
# ===========================================
# 3. SUBSCRIPTIONS TABLE
# ===========================================
create_if_not_exists table(:phoenix_kit_subscriptions, prefix: prefix) do
add :user_id, :integer, null: false
add :billing_profile_id, :integer
add :payment_method_id, :integer
add :plan_id, :integer, null: false
add :plan_name, :string, null: false
# Provider info (for tracking, not control)
add :provider, :string, size: 20
add :provider_subscription_id, :string
# Status
add :status, :string, size: 20, null: false, default: "active"
# Billing period
add :current_period_start, :utc_datetime_usec, null: false
add :current_period_end, :utc_datetime_usec, null: false
# Cancellation
add :cancel_at_period_end, :boolean, null: false, default: false
add :cancelled_at, :utc_datetime_usec
# Trial
add :trial_start, :utc_datetime_usec
add :trial_end, :utc_datetime_usec
# Dunning (failed payment handling)
add :grace_period_end, :utc_datetime_usec
add :renewal_attempts, :integer, null: false, default: 0
add :last_renewal_attempt_at, :utc_datetime_usec
add :last_renewal_error, :string
# Pricing at subscription time
add :price, :decimal, precision: 15, scale: 2, null: false
add :currency, :string, size: 3, null: false, default: "EUR"
# Additional data
add :metadata, :map, null: false, default: %{}
timestamps(type: :utc_datetime_usec)
end
create_if_not_exists index(:phoenix_kit_subscriptions, [:user_id],
name: :phoenix_kit_subscriptions_user_id_idx,
prefix: prefix
)
create_if_not_exists index(:phoenix_kit_subscriptions, [:status],
name: :phoenix_kit_subscriptions_status_idx,
prefix: prefix
)
create_if_not_exists index(:phoenix_kit_subscriptions, [:current_period_end],
name: :phoenix_kit_subscriptions_period_end_idx,
prefix: prefix
)
create_if_not_exists index(:phoenix_kit_subscriptions, [:provider, :provider_subscription_id],
name: :phoenix_kit_subscriptions_provider_idx,
prefix: prefix
)
# ===========================================
# 4. PAYMENT PROVIDER CONFIGS TABLE
# ===========================================
create_if_not_exists table(:phoenix_kit_payment_provider_configs, prefix: prefix) do
add :provider, :string, size: 20, null: false
# Status
add :enabled, :boolean, null: false, default: false
add :mode, :string, size: 10, null: false, default: "test"
# Credentials (should be encrypted in app layer)
add :api_key, :text
add :api_secret, :text
add :webhook_secret, :text
# Auto-generated webhook URL (for display)
add :webhook_url, :string
# Verification
add :last_verified_at, :utc_datetime_usec
add :verification_status, :string, size: 20, default: "pending"
add :verification_error, :text
# Additional config (JSONB)
add :config, :map, null: false, default: %{}
timestamps(type: :utc_datetime_usec)
end
create_if_not_exists unique_index(:phoenix_kit_payment_provider_configs, [:provider],
name: :phoenix_kit_payment_provider_configs_provider_uidx,
prefix: prefix
)
# ===========================================
# 5. WEBHOOK EVENTS TABLE (for idempotency)
# ===========================================
create_if_not_exists table(:phoenix_kit_webhook_events, prefix: prefix) do
add :provider, :string, size: 20, null: false
add :event_id, :string, null: false
add :event_type, :string, null: false
# Payload
add :payload, :map, null: false, default: %{}
# Processing status
add :processed, :boolean, null: false, default: false
add :processed_at, :utc_datetime_usec
add :error_message, :text
add :retry_count, :integer, null: false, default: 0
timestamps(type: :utc_datetime_usec)
end
create_if_not_exists unique_index(:phoenix_kit_webhook_events, [:provider, :event_id],
name: :phoenix_kit_webhook_events_provider_event_uidx,
prefix: prefix
)
create_if_not_exists index(:phoenix_kit_webhook_events, [:processed],
name: :phoenix_kit_webhook_events_processed_idx,
prefix: prefix
)
# ===========================================
# 6. MODIFY ORDERS TABLE - Add checkout fields
# ===========================================
alter table(:phoenix_kit_orders, prefix: prefix) do
add_if_not_exists :checkout_session_id, :string
add_if_not_exists :checkout_url, :text
add_if_not_exists :checkout_expires_at, :utc_datetime_usec
end
# Make payment_method nullable - method is determined after payment
execute """
ALTER TABLE #{prefix_table_name("phoenix_kit_orders", prefix)}
ALTER COLUMN payment_method DROP DEFAULT,
ALTER COLUMN payment_method DROP NOT NULL
"""
# ===========================================
# 7. MODIFY INVOICES TABLE - Add subscription reference
# ===========================================
alter table(:phoenix_kit_invoices, prefix: prefix) do
add_if_not_exists :subscription_id, :integer
end
create_if_not_exists index(:phoenix_kit_invoices, [:subscription_id],
name: :phoenix_kit_invoices_subscription_id_idx,
prefix: prefix
)
# ===========================================
# 8. SEED SETTINGS
# ===========================================
now = DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601()
settings = [
{"billing_stripe_enabled", "false"},
{"billing_paypal_enabled", "false"},
{"billing_razorpay_enabled", "false"},
{"billing_subscription_grace_days", "3"},
{"billing_dunning_max_attempts", "3"},
{"billing_dunning_retry_days", "3"}
]
for {key, value} <- settings do
execute """
INSERT INTO #{prefix_table_name("phoenix_kit_settings", prefix)} (key, value, module, date_added, date_updated)
VALUES ('#{key}', '#{value}', 'billing', '#{now}', '#{now}')
ON CONFLICT (key) DO NOTHING
"""
end
# ===========================================
# 9. UPDATE VERSION
# ===========================================
execute "COMMENT ON TABLE #{prefix_table_name("phoenix_kit", prefix)} IS '33'"
end
@doc """
Rollback the V33 migration.
"""
def down(%{prefix: prefix} = _opts) do
# Remove settings
settings_keys = [
"billing_stripe_enabled",
"billing_paypal_enabled",
"billing_razorpay_enabled",
"billing_subscription_grace_days",
"billing_dunning_max_attempts",
"billing_dunning_retry_days"
]
for key <- settings_keys do
execute """
DELETE FROM #{prefix_table_name("phoenix_kit_settings", prefix)}
WHERE key = '#{key}'
"""
end
# Remove index from invoices
drop_if_exists index(:phoenix_kit_invoices, [:subscription_id],
name: :phoenix_kit_invoices_subscription_id_idx,
prefix: prefix
)
# Remove columns from invoices
alter table(:phoenix_kit_invoices, prefix: prefix) do
remove_if_exists :subscription_id, :integer
end
# Restore NOT NULL and default "bank" for payment_method
execute """
UPDATE #{prefix_table_name("phoenix_kit_orders", prefix)}
SET payment_method = 'bank' WHERE payment_method IS NULL
"""
execute """
ALTER TABLE #{prefix_table_name("phoenix_kit_orders", prefix)}
ALTER COLUMN payment_method SET NOT NULL,
ALTER COLUMN payment_method SET DEFAULT 'bank'
"""
# Remove columns from orders
alter table(:phoenix_kit_orders, prefix: prefix) do
remove_if_exists :checkout_session_id, :string
remove_if_exists :checkout_url, :text
remove_if_exists :checkout_expires_at, :utc_datetime_usec
end
# Drop webhook events table
drop_if_exists table(:phoenix_kit_webhook_events, prefix: prefix)
# Drop provider configs table
drop_if_exists table(:phoenix_kit_payment_provider_configs, prefix: prefix)
# Drop subscriptions table
drop_if_exists table(:phoenix_kit_subscriptions, prefix: prefix)
# Drop subscription plans table
drop_if_exists table(:phoenix_kit_subscription_plans, prefix: prefix)
# Drop payment methods table
drop_if_exists table(:phoenix_kit_payment_methods, prefix: prefix)
# Update version
execute "COMMENT ON TABLE #{prefix_table_name("phoenix_kit", prefix)} IS '32'"
end
# Helper to build prefixed table name
defp prefix_table_name(table, nil), do: table
defp prefix_table_name(table, "public"), do: table
defp prefix_table_name(table, prefix), do: "#{prefix}.#{table}"
end