Skip to content

Commit 39a5fb3

Browse files
committed
added a migration and dropped existing tables
1 parent 0d84670 commit 39a5fb3

2 files changed

Lines changed: 214 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,4 @@
121121
- Ensure subscription provider is updated when switching payment gateways.
122122
- Document how to add a new billing provider.
123123
- Add Dodo Payments and Paddle provider integrations with hosted checkout support.
124+
- Add billing schema update migration to backfill missing plan/subscription columns.
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
return static function (PDO $pdo): void {
6+
$driver = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
7+
8+
$tableExists = static function (string $table) use ($pdo, $driver): bool {
9+
if ($driver === 'sqlite') {
10+
$stmt = $pdo->prepare('SELECT name FROM sqlite_master WHERE type = "table" AND name = ?');
11+
$stmt->execute([$table]);
12+
return (bool)$stmt->fetchColumn();
13+
}
14+
$stmt = $pdo->prepare('SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ?');
15+
$stmt->execute([$table]);
16+
return (int)$stmt->fetchColumn() > 0;
17+
};
18+
19+
$columnExists = static function (string $table, string $column) use ($pdo, $driver): bool {
20+
if ($driver === 'sqlite') {
21+
$stmt = $pdo->prepare('PRAGMA table_info(' . $table . ')');
22+
$stmt->execute();
23+
$columns = $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [];
24+
foreach ($columns as $info) {
25+
if (($info['name'] ?? '') === $column) {
26+
return true;
27+
}
28+
}
29+
return false;
30+
}
31+
$stmt = $pdo->prepare(
32+
'SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ?'
33+
);
34+
$stmt->execute([$table, $column]);
35+
return (int)$stmt->fetchColumn() > 0;
36+
};
37+
38+
$addColumn = static function (string $table, string $definition) use ($pdo, $driver): void {
39+
try {
40+
$pdo->exec('ALTER TABLE ' . $table . ' ADD COLUMN ' . $definition);
41+
} catch (Throwable $e) {
42+
// Ignore if column already exists or cannot be altered.
43+
}
44+
};
45+
46+
if ($tableExists('plans')) {
47+
if (!$columnExists('plans', 'currency')) {
48+
$addColumn('plans', $driver === 'sqlite' ? "currency TEXT NOT NULL DEFAULT 'USD'" : "currency VARCHAR(10) NOT NULL DEFAULT 'USD'");
49+
}
50+
if (!$columnExists('plans', 'duration')) {
51+
$addColumn('plans', $driver === 'sqlite' ? "duration TEXT NOT NULL DEFAULT 'monthly'" : "duration VARCHAR(20) NOT NULL DEFAULT 'monthly'");
52+
}
53+
if (!$columnExists('plans', 'is_grandfathered')) {
54+
$addColumn('plans', $driver === 'sqlite' ? 'is_grandfathered INTEGER NOT NULL DEFAULT 0' : 'is_grandfathered TINYINT(1) NOT NULL DEFAULT 0');
55+
}
56+
if (!$columnExists('plans', 'disabled_at')) {
57+
$addColumn('plans', $driver === 'sqlite' ? 'disabled_at TEXT NULL' : 'disabled_at DATETIME NULL');
58+
}
59+
foreach ([
60+
'stripe_price_id' => $driver === 'sqlite' ? 'stripe_price_id TEXT NULL' : 'stripe_price_id VARCHAR(120) NULL',
61+
'razorpay_plan_id' => $driver === 'sqlite' ? 'razorpay_plan_id TEXT NULL' : 'razorpay_plan_id VARCHAR(120) NULL',
62+
'paypal_plan_id' => $driver === 'sqlite' ? 'paypal_plan_id TEXT NULL' : 'paypal_plan_id VARCHAR(120) NULL',
63+
'lemonsqueezy_variant_id' => $driver === 'sqlite' ? 'lemonsqueezy_variant_id TEXT NULL' : 'lemonsqueezy_variant_id VARCHAR(120) NULL',
64+
'dodo_price_id' => $driver === 'sqlite' ? 'dodo_price_id TEXT NULL' : 'dodo_price_id VARCHAR(120) NULL',
65+
'paddle_price_id' => $driver === 'sqlite' ? 'paddle_price_id TEXT NULL' : 'paddle_price_id VARCHAR(120) NULL',
66+
] as $column => $definition) {
67+
if (!$columnExists('plans', $column)) {
68+
$addColumn('plans', $definition);
69+
}
70+
}
71+
}
72+
73+
if ($tableExists('subscriptions')) {
74+
foreach ([
75+
'provider' => $driver === 'sqlite' ? 'provider TEXT NULL' : 'provider VARCHAR(30) NULL',
76+
'provider_customer_id' => $driver === 'sqlite' ? 'provider_customer_id TEXT NULL' : 'provider_customer_id VARCHAR(120) NULL',
77+
'provider_subscription_id' => $driver === 'sqlite' ? 'provider_subscription_id TEXT NULL' : 'provider_subscription_id VARCHAR(120) NULL',
78+
'provider_checkout_id' => $driver === 'sqlite' ? 'provider_checkout_id TEXT NULL' : 'provider_checkout_id VARCHAR(120) NULL',
79+
'provider_status' => $driver === 'sqlite' ? 'provider_status TEXT NULL' : 'provider_status VARCHAR(40) NULL',
80+
'currency' => $driver === 'sqlite' ? "currency TEXT NOT NULL DEFAULT 'USD'" : "currency VARCHAR(10) NOT NULL DEFAULT 'USD'",
81+
'type' => $driver === 'sqlite' ? "type TEXT NOT NULL DEFAULT 'recurring'" : "type VARCHAR(20) NOT NULL DEFAULT 'recurring'",
82+
'trial_ends_at' => $driver === 'sqlite' ? 'trial_ends_at TEXT NULL' : 'trial_ends_at DATETIME NULL',
83+
'current_period_start' => $driver === 'sqlite' ? 'current_period_start TEXT NULL' : 'current_period_start DATETIME NULL',
84+
'current_period_end' => $driver === 'sqlite' ? 'current_period_end TEXT NULL' : 'current_period_end DATETIME NULL',
85+
'cancel_at_period_end' => $driver === 'sqlite' ? 'cancel_at_period_end INTEGER NOT NULL DEFAULT 0' : 'cancel_at_period_end TINYINT(1) NOT NULL DEFAULT 0',
86+
'canceled_at' => $driver === 'sqlite' ? 'canceled_at TEXT NULL' : 'canceled_at DATETIME NULL',
87+
] as $column => $definition) {
88+
if (!$columnExists('subscriptions', $column)) {
89+
$addColumn('subscriptions', $definition);
90+
}
91+
}
92+
}
93+
94+
if ($tableExists('subscription_changes')) {
95+
foreach ([
96+
'provider' => $driver === 'sqlite' ? 'provider TEXT NULL' : 'provider VARCHAR(50) NULL',
97+
'provider_checkout_id' => $driver === 'sqlite' ? 'provider_checkout_id TEXT NULL' : 'provider_checkout_id VARCHAR(120) NULL',
98+
] as $column => $definition) {
99+
if (!$columnExists('subscription_changes', $column)) {
100+
$addColumn('subscription_changes', $definition);
101+
}
102+
}
103+
}
104+
105+
if (!$tableExists('app_settings')) {
106+
if ($driver === 'sqlite') {
107+
$pdo->exec(
108+
'CREATE TABLE IF NOT EXISTS app_settings (
109+
id INTEGER PRIMARY KEY AUTOINCREMENT,
110+
setting_key TEXT NOT NULL UNIQUE,
111+
setting_value TEXT NOT NULL,
112+
updated_at TEXT NOT NULL
113+
);'
114+
);
115+
} else {
116+
$pdo->exec(
117+
'CREATE TABLE IF NOT EXISTS app_settings (
118+
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
119+
setting_key VARCHAR(120) NOT NULL UNIQUE,
120+
setting_value TEXT NOT NULL,
121+
updated_at DATETIME NOT NULL
122+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;'
123+
);
124+
}
125+
}
126+
127+
if (!$tableExists('payment_gateway_settings')) {
128+
if ($driver === 'sqlite') {
129+
$pdo->exec(
130+
'CREATE TABLE IF NOT EXISTS payment_gateway_settings (
131+
id INTEGER PRIMARY KEY AUTOINCREMENT,
132+
provider TEXT NOT NULL,
133+
setting_key TEXT NOT NULL,
134+
setting_value TEXT NOT NULL,
135+
updated_at TEXT NOT NULL
136+
);'
137+
);
138+
} else {
139+
$pdo->exec(
140+
'CREATE TABLE IF NOT EXISTS payment_gateway_settings (
141+
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
142+
provider VARCHAR(30) NOT NULL,
143+
setting_key VARCHAR(120) NOT NULL,
144+
setting_value TEXT NOT NULL,
145+
updated_at DATETIME NOT NULL,
146+
UNIQUE KEY idx_gateway_setting (provider, setting_key)
147+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;'
148+
);
149+
}
150+
}
151+
152+
if (!$tableExists('payment_gateway_events')) {
153+
if ($driver === 'sqlite') {
154+
$pdo->exec(
155+
'CREATE TABLE IF NOT EXISTS payment_gateway_events (
156+
id INTEGER PRIMARY KEY AUTOINCREMENT,
157+
provider TEXT NOT NULL,
158+
subscription_id INTEGER NULL,
159+
status TEXT NOT NULL,
160+
event_type TEXT NOT NULL,
161+
created_at TEXT NOT NULL
162+
);'
163+
);
164+
} else {
165+
$pdo->exec(
166+
'CREATE TABLE IF NOT EXISTS payment_gateway_events (
167+
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
168+
provider VARCHAR(30) NOT NULL,
169+
subscription_id BIGINT UNSIGNED NULL,
170+
status VARCHAR(40) NOT NULL,
171+
event_type VARCHAR(120) NOT NULL,
172+
created_at DATETIME NOT NULL
173+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;'
174+
);
175+
}
176+
}
177+
178+
if (!$tableExists('subscription_changes')) {
179+
if ($driver === 'sqlite') {
180+
$pdo->exec(
181+
'CREATE TABLE IF NOT EXISTS subscription_changes (
182+
id INTEGER PRIMARY KEY AUTOINCREMENT,
183+
subscription_id INTEGER NOT NULL,
184+
from_plan_id INTEGER NULL,
185+
to_plan_id INTEGER NULL,
186+
change_type TEXT NOT NULL,
187+
status TEXT NOT NULL,
188+
effective_at TEXT NULL,
189+
provider TEXT NULL,
190+
provider_checkout_id TEXT NULL,
191+
created_at TEXT NOT NULL,
192+
applied_at TEXT NULL
193+
);'
194+
);
195+
} else {
196+
$pdo->exec(
197+
'CREATE TABLE IF NOT EXISTS subscription_changes (
198+
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
199+
subscription_id BIGINT UNSIGNED NOT NULL,
200+
from_plan_id BIGINT UNSIGNED NULL,
201+
to_plan_id BIGINT UNSIGNED NULL,
202+
change_type VARCHAR(40) NOT NULL,
203+
status VARCHAR(30) NOT NULL,
204+
effective_at DATETIME NULL,
205+
provider VARCHAR(50) NULL,
206+
provider_checkout_id VARCHAR(120) NULL,
207+
created_at DATETIME NOT NULL,
208+
applied_at DATETIME NULL
209+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;'
210+
);
211+
}
212+
}
213+
};

0 commit comments

Comments
 (0)