paykitjs 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/dist/_virtual/_rolldown/runtime.js +13 -0
  3. package/dist/api/define-route.d.ts +94 -0
  4. package/dist/api/define-route.js +153 -0
  5. package/dist/api/methods.d.ts +422 -0
  6. package/dist/api/methods.js +67 -0
  7. package/dist/cli/commands/check.js +92 -0
  8. package/dist/cli/commands/init.js +264 -0
  9. package/dist/cli/commands/push.js +73 -0
  10. package/dist/cli/commands/telemetry.js +16 -0
  11. package/dist/cli/index.d.ts +1 -0
  12. package/dist/cli/index.js +21 -0
  13. package/dist/cli/templates/index.js +64 -0
  14. package/dist/cli/utils/detect.js +67 -0
  15. package/dist/cli/utils/format.js +58 -0
  16. package/dist/cli/utils/get-config.js +117 -0
  17. package/dist/cli/utils/telemetry.js +103 -0
  18. package/dist/client/index.d.ts +25 -0
  19. package/dist/client/index.js +27 -0
  20. package/dist/core/context.d.ts +17 -0
  21. package/dist/core/context.js +23 -0
  22. package/dist/core/create-paykit.d.ts +7 -0
  23. package/dist/core/create-paykit.js +52 -0
  24. package/dist/core/error-codes.d.ts +12 -0
  25. package/dist/core/error-codes.js +10 -0
  26. package/dist/core/errors.d.ts +41 -0
  27. package/dist/core/errors.js +47 -0
  28. package/dist/core/logger.d.ts +11 -0
  29. package/dist/core/logger.js +51 -0
  30. package/dist/core/utils.js +21 -0
  31. package/dist/customer/customer.api.js +47 -0
  32. package/dist/customer/customer.service.js +342 -0
  33. package/dist/customer/customer.types.d.ts +31 -0
  34. package/dist/database/index.d.ts +8 -0
  35. package/dist/database/index.js +32 -0
  36. package/dist/database/migrations/0000_init.sql +157 -0
  37. package/dist/database/migrations/meta/0000_snapshot.json +1222 -0
  38. package/dist/database/migrations/meta/_journal.json +13 -0
  39. package/dist/database/schema.d.ts +1767 -0
  40. package/dist/database/schema.js +150 -0
  41. package/dist/entitlement/entitlement.api.js +33 -0
  42. package/dist/entitlement/entitlement.service.d.ts +17 -0
  43. package/dist/entitlement/entitlement.service.js +123 -0
  44. package/dist/handlers/next.d.ts +9 -0
  45. package/dist/handlers/next.js +9 -0
  46. package/dist/index.d.ts +14 -0
  47. package/dist/index.js +6 -0
  48. package/dist/invoice/invoice.service.js +54 -0
  49. package/dist/payment/payment.service.js +49 -0
  50. package/dist/payment-method/payment-method.service.js +78 -0
  51. package/dist/product/product-sync.service.js +111 -0
  52. package/dist/product/product.service.js +127 -0
  53. package/dist/providers/provider.d.ts +159 -0
  54. package/dist/providers/stripe.js +547 -0
  55. package/dist/subscription/subscription.api.js +24 -0
  56. package/dist/subscription/subscription.service.js +896 -0
  57. package/dist/subscription/subscription.types.d.ts +18 -0
  58. package/dist/subscription/subscription.types.js +11 -0
  59. package/dist/testing/testing.api.js +29 -0
  60. package/dist/testing/testing.service.js +49 -0
  61. package/dist/types/events.d.ts +181 -0
  62. package/dist/types/instance.d.ts +88 -0
  63. package/dist/types/models.d.ts +11 -0
  64. package/dist/types/options.d.ts +32 -0
  65. package/dist/types/plugin.d.ts +11 -0
  66. package/dist/types/schema.d.ts +99 -0
  67. package/dist/types/schema.js +192 -0
  68. package/dist/webhook/webhook.api.js +29 -0
  69. package/dist/webhook/webhook.service.js +143 -0
  70. package/package.json +72 -0
@@ -0,0 +1,47 @@
1
+ import { PAYKIT_ERROR_CODES, PayKitError } from "../core/errors.js";
2
+ import { getCustomerWithDetails, getProviderCustomerIdForCustomer, hardDeleteCustomer, listCustomers, syncCustomerWithDefaults } from "./customer.service.js";
3
+ import { definePayKitMethod, returnUrl } from "../api/define-route.js";
4
+ import * as z from "zod";
5
+ //#region src/customer/customer.api.ts
6
+ const upsertCustomerSchema = z.object({
7
+ id: z.string(),
8
+ email: z.string().optional(),
9
+ name: z.string().optional(),
10
+ metadata: z.record(z.string(), z.string()).optional()
11
+ });
12
+ const customerIdSchema = z.object({ id: z.string() });
13
+ const listCustomersSchema = z.object({
14
+ limit: z.number().int().positive().optional(),
15
+ offset: z.number().int().min(0).optional(),
16
+ planIds: z.array(z.string()).optional()
17
+ }).optional();
18
+ const upsertCustomer = definePayKitMethod({ input: upsertCustomerSchema }, async (ctx) => syncCustomerWithDefaults(ctx.paykit, ctx.input));
19
+ const getCustomer = definePayKitMethod({ input: customerIdSchema }, async (ctx) => getCustomerWithDetails(ctx.paykit, ctx.input.id));
20
+ const deleteCustomer = definePayKitMethod({ input: customerIdSchema }, async (ctx) => {
21
+ await hardDeleteCustomer(ctx.paykit, ctx.input.id);
22
+ return { success: true };
23
+ });
24
+ const listCustomersMethod = definePayKitMethod({ input: listCustomersSchema }, async (ctx) => listCustomers(ctx.paykit, ctx.input));
25
+ /** Opens the provider customer portal for the resolved customer. */
26
+ const customerPortal = definePayKitMethod({
27
+ input: z.object({ returnUrl: returnUrl() }),
28
+ requireCustomer: true,
29
+ route: {
30
+ client: true,
31
+ method: "POST",
32
+ path: "/customer-portal"
33
+ }
34
+ }, async (ctx) => {
35
+ const providerCustomerId = await getProviderCustomerIdForCustomer(ctx.paykit.database, {
36
+ customerId: ctx.customer.id,
37
+ providerId: ctx.paykit.provider.id
38
+ });
39
+ if (!providerCustomerId) throw PayKitError.from("NOT_FOUND", PAYKIT_ERROR_CODES.PROVIDER_CUSTOMER_NOT_FOUND);
40
+ const { url } = await ctx.paykit.stripe.createPortalSession({
41
+ providerCustomerId,
42
+ returnUrl: ctx.input.returnUrl
43
+ });
44
+ return { url };
45
+ });
46
+ //#endregion
47
+ export { customerPortal, deleteCustomer, getCustomer, listCustomersMethod, upsertCustomer };
@@ -0,0 +1,342 @@
1
+ import { PAYKIT_ERROR_CODES, PayKitError } from "../core/errors.js";
2
+ import { customer, entitlement, invoice, paymentMethod, product, subscription } from "../database/schema.js";
3
+ import { getLatestProduct } from "../product/product.service.js";
4
+ import { getActiveSubscriptionInGroup, getCurrentSubscriptions, getScheduledSubscriptionsInGroup, insertSubscriptionRecord } from "../subscription/subscription.service.js";
5
+ import { and, count, countDistinct, desc, eq, inArray, isNull, or, sql } from "drizzle-orm";
6
+ //#region src/customer/customer.service.ts
7
+ async function syncCustomer(database, input) {
8
+ const now = /* @__PURE__ */ new Date();
9
+ const existing = await database.query.customer.findFirst({ where: eq(customer.id, input.id) });
10
+ if (existing) {
11
+ const row = (await database.update(customer).set({
12
+ email: input.email ?? existing.email ?? null,
13
+ name: input.name ?? existing.name ?? null,
14
+ metadata: input.metadata ?? existing.metadata ?? null,
15
+ deletedAt: null,
16
+ updatedAt: now
17
+ }).where(eq(customer.id, existing.id)).returning())[0];
18
+ if (!row) throw PayKitError.from("INTERNAL_SERVER_ERROR", PAYKIT_ERROR_CODES.CUSTOMER_UPDATE_FAILED);
19
+ return row;
20
+ }
21
+ const row = (await database.insert(customer).values({
22
+ id: input.id,
23
+ email: input.email ?? null,
24
+ name: input.name ?? null,
25
+ metadata: input.metadata ?? null,
26
+ deletedAt: null
27
+ }).returning())[0];
28
+ if (!row) throw PayKitError.from("INTERNAL_SERVER_ERROR", PAYKIT_ERROR_CODES.CUSTOMER_CREATE_FAILED);
29
+ return row;
30
+ }
31
+ async function ensureDefaultPlansForCustomer(ctx, customerId) {
32
+ const defaultPlans = ctx.plans.plans.filter((plan) => plan.isDefault);
33
+ if (defaultPlans.length === 0) return;
34
+ for (const defaultPlan of defaultPlans) {
35
+ if (!defaultPlan.group) continue;
36
+ if (await getActiveSubscriptionInGroup(ctx.database, {
37
+ customerId,
38
+ group: defaultPlan.group
39
+ })) continue;
40
+ if ((await getScheduledSubscriptionsInGroup(ctx.database, {
41
+ customerId,
42
+ group: defaultPlan.group
43
+ })).length > 0) continue;
44
+ const storedPlan = await getLatestProduct(ctx.database, defaultPlan.id);
45
+ if (!storedPlan) continue;
46
+ if (storedPlan.priceAmount !== null) {
47
+ ctx.logger.warn({
48
+ planId: defaultPlan.id,
49
+ customerId
50
+ }, "skipping default plan: paid default plans are not auto-attached yet");
51
+ continue;
52
+ }
53
+ await insertSubscriptionRecord(ctx.database, {
54
+ customerId,
55
+ planFeatures: defaultPlan.includes,
56
+ productInternalId: storedPlan.internalId,
57
+ startedAt: /* @__PURE__ */ new Date(),
58
+ status: "active"
59
+ });
60
+ }
61
+ }
62
+ async function syncCustomerWithDefaults(ctx, input) {
63
+ const syncedCustomer = await syncCustomer(ctx.database, input);
64
+ await ensureDefaultPlansForCustomer(ctx, syncedCustomer.id);
65
+ const providerCustomer = await ensureTestingProviderCustomer(ctx, syncedCustomer.id);
66
+ if (!providerCustomer) return syncedCustomer;
67
+ return {
68
+ ...syncedCustomer,
69
+ provider: {
70
+ ...syncedCustomer.provider ?? {},
71
+ [ctx.provider.id]: providerCustomer
72
+ }
73
+ };
74
+ }
75
+ async function applyCustomerWebhookAction(database, action) {
76
+ if (action.type === "customer.upsert") {
77
+ await syncCustomer(database, action.data);
78
+ return action.data.id;
79
+ }
80
+ await deleteCustomerFromDatabase(database, action.data.id);
81
+ return action.data.id;
82
+ }
83
+ async function getCustomerById(database, customerId) {
84
+ return await database.query.customer.findFirst({ where: and(eq(customer.id, customerId), isNull(customer.deletedAt)) }) ?? null;
85
+ }
86
+ async function getCustomerByIdOrThrow(database, customerId) {
87
+ const existingCustomer = await getCustomerById(database, customerId);
88
+ if (!existingCustomer) throw PayKitError.from("NOT_FOUND", PAYKIT_ERROR_CODES.CUSTOMER_NOT_FOUND);
89
+ return existingCustomer;
90
+ }
91
+ async function getCustomerWithDetails(ctx, customerId) {
92
+ const customerRow = await getCustomerById(ctx.database, customerId);
93
+ if (!customerRow) return null;
94
+ const subRows = await ctx.database.select({
95
+ planGroup: product.group,
96
+ planId: product.id,
97
+ status: subscription.status,
98
+ cancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
99
+ currentPeriodStart: subscription.currentPeriodStartAt,
100
+ currentPeriodEnd: subscription.currentPeriodEndAt
101
+ }).from(subscription).innerJoin(product, eq(product.internalId, subscription.productInternalId)).where(and(eq(subscription.customerId, customerId), inArray(subscription.status, [
102
+ "active",
103
+ "trialing",
104
+ "past_due",
105
+ "scheduled"
106
+ ]), or(isNull(subscription.endedAt), sql`${subscription.endedAt} > now()`))).orderBy(desc(subscription.createdAt));
107
+ const subscriptionsByGroup = /* @__PURE__ */ new Map();
108
+ for (const row of subRows) {
109
+ const currentGroup = subscriptionsByGroup.get(row.planGroup) ?? [];
110
+ currentGroup.push(row.planId);
111
+ subscriptionsByGroup.set(row.planGroup, currentGroup);
112
+ }
113
+ for (const [group, planIds] of subscriptionsByGroup) {
114
+ if (planIds.length < 2) continue;
115
+ ctx.logger.warn({
116
+ customerId,
117
+ group,
118
+ planIds
119
+ }, "multiple active subscriptions detected while reading customer details");
120
+ }
121
+ const entRows = await ctx.database.select({
122
+ featureId: entitlement.featureId,
123
+ balance: entitlement.balance,
124
+ limit: entitlement.limit,
125
+ nextResetAt: entitlement.nextResetAt
126
+ }).from(entitlement).innerJoin(subscription, eq(subscription.id, entitlement.subscriptionId)).where(and(eq(entitlement.customerId, customerId), inArray(subscription.status, [
127
+ "active",
128
+ "trialing",
129
+ "past_due"
130
+ ]), or(isNull(subscription.endedAt), sql`${subscription.endedAt} > now()`)));
131
+ const entitlements = {};
132
+ for (const row of entRows) {
133
+ const isUnlimited = row.limit === null;
134
+ entitlements[row.featureId] = {
135
+ featureId: row.featureId,
136
+ balance: row.balance ?? 0,
137
+ limit: row.limit ?? 0,
138
+ usage: isUnlimited ? 0 : (row.limit ?? 0) - (row.balance ?? 0),
139
+ unlimited: isUnlimited,
140
+ nextResetAt: row.nextResetAt
141
+ };
142
+ }
143
+ return {
144
+ ...customerRow,
145
+ subscriptions: subRows.map((row) => ({
146
+ planId: row.planId,
147
+ status: row.status,
148
+ cancelAtPeriodEnd: row.cancelAtPeriodEnd,
149
+ currentPeriodStart: row.currentPeriodStart,
150
+ currentPeriodEnd: row.currentPeriodEnd
151
+ })),
152
+ entitlements
153
+ };
154
+ }
155
+ function getProviderCustomer(customerRow, providerId) {
156
+ return (customerRow.provider ?? {})[providerId] ?? null;
157
+ }
158
+ async function setProviderCustomer(database, input) {
159
+ const providerMap = (await getCustomerByIdOrThrow(database, input.customerId)).provider ?? {};
160
+ providerMap[input.providerId] = input.providerCustomer;
161
+ await database.update(customer).set({
162
+ provider: providerMap,
163
+ updatedAt: /* @__PURE__ */ new Date()
164
+ }).where(eq(customer.id, input.customerId));
165
+ }
166
+ function getProviderCustomerId(customerRow, providerId) {
167
+ return getProviderCustomer(customerRow, providerId)?.id ?? null;
168
+ }
169
+ async function getProviderCustomerIdForCustomer(database, input) {
170
+ const row = await database.query.customer.findFirst({ where: eq(customer.id, input.customerId) });
171
+ if (!row) return null;
172
+ return getProviderCustomerId(row, input.providerId);
173
+ }
174
+ async function findCustomerByProviderCustomerId(database, input) {
175
+ return await database.query.customer.findFirst({ where: sql`${customer.provider}->${input.providerId}->>'id' = ${input.providerCustomerId}` }) ?? null;
176
+ }
177
+ async function upsertProviderCustomer(ctx, input) {
178
+ const providerId = ctx.provider.id;
179
+ const existingCustomer = await getCustomerByIdOrThrow(ctx.database, input.customerId);
180
+ const existingProviderCustomer = getProviderCustomer(existingCustomer, providerId);
181
+ const existingProviderCustomerId = existingProviderCustomer?.id ?? null;
182
+ if (existingProviderCustomerId) return {
183
+ customerId: input.customerId,
184
+ providerCustomer: existingProviderCustomer,
185
+ providerCustomerId: existingProviderCustomerId
186
+ };
187
+ const { providerCustomer } = await ctx.stripe.upsertCustomer({
188
+ createTestClock: ctx.options.testing?.enabled === true,
189
+ id: existingCustomer.id,
190
+ email: existingCustomer.email ?? void 0,
191
+ name: existingCustomer.name ?? void 0,
192
+ metadata: existingCustomer.metadata ?? void 0
193
+ });
194
+ const providerCustomerId = providerCustomer.id;
195
+ await setProviderCustomer(ctx.database, {
196
+ customerId: input.customerId,
197
+ providerCustomer,
198
+ providerId
199
+ });
200
+ return {
201
+ customerId: input.customerId,
202
+ providerCustomer,
203
+ providerCustomerId
204
+ };
205
+ }
206
+ async function ensureTestingProviderCustomer(ctx, customerId) {
207
+ if (ctx.options.testing?.enabled !== true) return null;
208
+ const { providerCustomer } = await upsertProviderCustomer(ctx, { customerId });
209
+ return providerCustomer;
210
+ }
211
+ async function deleteCustomerFromDatabase(database, customerId) {
212
+ const sIds = (await database.select({ id: subscription.id }).from(subscription).where(eq(subscription.customerId, customerId))).map((row) => row.id);
213
+ if (sIds.length > 0) await database.delete(entitlement).where(inArray(entitlement.subscriptionId, sIds));
214
+ await database.delete(subscription).where(eq(subscription.customerId, customerId));
215
+ await database.delete(invoice).where(eq(invoice.customerId, customerId));
216
+ await database.delete(paymentMethod).where(eq(paymentMethod.customerId, customerId));
217
+ await database.delete(customer).where(eq(customer.id, customerId));
218
+ }
219
+ async function hardDeleteCustomer(ctx, customerId) {
220
+ const providerCustomerId = getProviderCustomerId(await getCustomerByIdOrThrow(ctx.database, customerId), ctx.provider.id);
221
+ if (providerCustomerId) try {
222
+ const activeSubscriptions = await ctx.stripe.listActiveSubscriptions({ providerCustomerId });
223
+ for (const sub of activeSubscriptions) await ctx.stripe.cancelSubscription({ providerSubscriptionId: sub.providerSubscriptionId });
224
+ await ctx.stripe.deleteCustomer({ providerCustomerId });
225
+ } catch (error) {
226
+ ctx.logger.error({
227
+ providerCustomerId,
228
+ err: error
229
+ }, "failed to clean up Stripe customer");
230
+ }
231
+ await deleteCustomerFromDatabase(ctx.database, customerId);
232
+ }
233
+ async function emitCustomerUpdated(ctx, customerId) {
234
+ const payload = {
235
+ customerId,
236
+ subscriptions: await getCurrentSubscriptions(ctx.database, customerId)
237
+ };
238
+ try {
239
+ await ctx.options.on?.["customer.updated"]?.({
240
+ name: "customer.updated",
241
+ payload
242
+ });
243
+ await ctx.options.on?.["*"]?.({ event: {
244
+ name: "customer.updated",
245
+ payload
246
+ } });
247
+ } catch (error) {
248
+ ctx.logger.error({ err: error }, "error in customer.updated event handler");
249
+ }
250
+ }
251
+ async function listCustomers(ctx, input) {
252
+ const limit = input?.limit ?? 50;
253
+ const offset = input?.offset ?? 0;
254
+ const planIds = input?.planIds;
255
+ let customerRows;
256
+ let total;
257
+ if (planIds && planIds.length > 0) {
258
+ const planFilter = and(inArray(product.id, planIds), inArray(subscription.status, [
259
+ "active",
260
+ "trialing",
261
+ "past_due"
262
+ ]), or(isNull(subscription.endedAt), sql`${subscription.endedAt} > now()`), isNull(customer.deletedAt));
263
+ customerRows = (await ctx.database.selectDistinct({ customer }).from(customer).innerJoin(subscription, eq(subscription.customerId, customer.id)).innerJoin(product, eq(product.internalId, subscription.productInternalId)).where(planFilter).orderBy(desc(customer.createdAt)).limit(limit).offset(offset)).map((r) => r.customer);
264
+ total = (await ctx.database.select({ count: countDistinct(customer.id) }).from(customer).innerJoin(subscription, eq(subscription.customerId, customer.id)).innerJoin(product, eq(product.internalId, subscription.productInternalId)).where(planFilter))[0]?.count ?? 0;
265
+ } else {
266
+ total = (await ctx.database.select({ count: count() }).from(customer).where(isNull(customer.deletedAt)))[0]?.count ?? 0;
267
+ customerRows = await ctx.database.query.customer.findMany({
268
+ where: isNull(customer.deletedAt),
269
+ orderBy: (c, { desc }) => [desc(c.createdAt)],
270
+ limit,
271
+ offset
272
+ });
273
+ }
274
+ const customerIds = customerRows.map((c) => c.id);
275
+ const data = [];
276
+ if (customerIds.length > 0) {
277
+ const subRows = await ctx.database.select({
278
+ customerId: subscription.customerId,
279
+ planId: product.id,
280
+ status: subscription.status,
281
+ cancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
282
+ currentPeriodStart: subscription.currentPeriodStartAt,
283
+ currentPeriodEnd: subscription.currentPeriodEndAt
284
+ }).from(subscription).innerJoin(product, eq(product.internalId, subscription.productInternalId)).where(and(inArray(subscription.customerId, customerIds), inArray(subscription.status, [
285
+ "active",
286
+ "trialing",
287
+ "past_due",
288
+ "scheduled"
289
+ ]), or(isNull(subscription.endedAt), sql`${subscription.endedAt} > now()`))).orderBy(desc(subscription.createdAt));
290
+ const entRows = await ctx.database.select({
291
+ customerId: entitlement.customerId,
292
+ featureId: entitlement.featureId,
293
+ balance: entitlement.balance,
294
+ limit: entitlement.limit,
295
+ nextResetAt: entitlement.nextResetAt
296
+ }).from(entitlement).innerJoin(subscription, eq(subscription.id, entitlement.subscriptionId)).where(and(inArray(entitlement.customerId, customerIds), inArray(subscription.status, [
297
+ "active",
298
+ "trialing",
299
+ "past_due"
300
+ ]), or(isNull(subscription.endedAt), sql`${subscription.endedAt} > now()`)));
301
+ const subscriptionsByCustomer = /* @__PURE__ */ new Map();
302
+ for (const row of subRows) {
303
+ const list = subscriptionsByCustomer.get(row.customerId) ?? [];
304
+ list.push({
305
+ planId: row.planId,
306
+ status: row.status,
307
+ cancelAtPeriodEnd: row.cancelAtPeriodEnd,
308
+ currentPeriodStart: row.currentPeriodStart,
309
+ currentPeriodEnd: row.currentPeriodEnd
310
+ });
311
+ subscriptionsByCustomer.set(row.customerId, list);
312
+ }
313
+ const entitlementsByCustomer = /* @__PURE__ */ new Map();
314
+ for (const row of entRows) {
315
+ const map = entitlementsByCustomer.get(row.customerId) ?? {};
316
+ const isUnlimited = row.limit === null;
317
+ map[row.featureId] = {
318
+ featureId: row.featureId,
319
+ balance: row.balance ?? 0,
320
+ limit: row.limit ?? 0,
321
+ usage: isUnlimited ? 0 : (row.limit ?? 0) - (row.balance ?? 0),
322
+ unlimited: isUnlimited,
323
+ nextResetAt: row.nextResetAt
324
+ };
325
+ entitlementsByCustomer.set(row.customerId, map);
326
+ }
327
+ for (const c of customerRows) data.push({
328
+ ...c,
329
+ subscriptions: subscriptionsByCustomer.get(c.id) ?? [],
330
+ entitlements: entitlementsByCustomer.get(c.id) ?? {}
331
+ });
332
+ }
333
+ return {
334
+ data,
335
+ total,
336
+ hasMore: offset + limit < total,
337
+ limit,
338
+ offset
339
+ };
340
+ }
341
+ //#endregion
342
+ export { applyCustomerWebhookAction, emitCustomerUpdated, findCustomerByProviderCustomerId, getCustomerByIdOrThrow, getCustomerWithDetails, getProviderCustomer, getProviderCustomerIdForCustomer, hardDeleteCustomer, listCustomers, setProviderCustomer, syncCustomerWithDefaults, upsertProviderCustomer };
@@ -0,0 +1,31 @@
1
+ import { Customer } from "../types/models.js";
2
+
3
+ //#region src/customer/customer.types.d.ts
4
+ interface CustomerSubscription {
5
+ planId: string;
6
+ status: string;
7
+ cancelAtPeriodEnd: boolean;
8
+ currentPeriodStart: Date | null;
9
+ currentPeriodEnd: Date | null;
10
+ }
11
+ interface CustomerEntitlement {
12
+ featureId: string;
13
+ balance: number;
14
+ limit: number;
15
+ usage: number;
16
+ unlimited: boolean;
17
+ nextResetAt: Date | null;
18
+ }
19
+ interface CustomerWithDetails extends Customer {
20
+ subscriptions: CustomerSubscription[];
21
+ entitlements: Record<string, CustomerEntitlement>;
22
+ }
23
+ interface ListCustomersResult {
24
+ data: CustomerWithDetails[];
25
+ total: number;
26
+ hasMore: boolean;
27
+ limit: number;
28
+ offset: number;
29
+ }
30
+ //#endregion
31
+ export { CustomerEntitlement, CustomerSubscription, CustomerWithDetails, ListCustomersResult };
@@ -0,0 +1,8 @@
1
+ import { schema_d_exports } from "./schema.js";
2
+ import { Pool } from "pg";
3
+ import { NodePgDatabase } from "drizzle-orm/node-postgres";
4
+
5
+ //#region src/database/index.d.ts
6
+ type PayKitDatabase = NodePgDatabase<typeof schema_d_exports>;
7
+ //#endregion
8
+ export { PayKitDatabase };
@@ -0,0 +1,32 @@
1
+ import { schema_exports } from "./schema.js";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { drizzle } from "drizzle-orm/node-postgres";
6
+ import { migrate } from "drizzle-orm/node-postgres/migrator";
7
+ //#region src/database/index.ts
8
+ const migrationsSchema = "public";
9
+ const migrationsTable = "paykit_migrations";
10
+ const migrationsFolder = path.join(path.dirname(fileURLToPath(import.meta.url)), "migrations");
11
+ async function createDatabase(database) {
12
+ return drizzle(database, { schema: schema_exports });
13
+ }
14
+ async function migrateDatabase(database) {
15
+ await migrate(drizzle(database, { schema: schema_exports }), {
16
+ migrationsFolder,
17
+ migrationsSchema,
18
+ migrationsTable
19
+ });
20
+ }
21
+ async function getPendingMigrationCount(database) {
22
+ const journalPath = path.join(migrationsFolder, "meta", "_journal.json");
23
+ const totalMigrations = JSON.parse(fs.readFileSync(journalPath, "utf-8")).entries.length;
24
+ try {
25
+ const appliedCount = (await database.query(`SELECT count(*)::int AS count FROM ${migrationsSchema}.${migrationsTable}`)).rows[0]?.count ?? 0;
26
+ return Math.max(0, totalMigrations - appliedCount);
27
+ } catch {
28
+ return totalMigrations;
29
+ }
30
+ }
31
+ //#endregion
32
+ export { createDatabase, getPendingMigrationCount, migrateDatabase };
@@ -0,0 +1,157 @@
1
+ CREATE TABLE "paykit_customer" (
2
+ "id" text PRIMARY KEY NOT NULL,
3
+ "email" text,
4
+ "name" text,
5
+ "metadata" jsonb,
6
+ "provider" jsonb DEFAULT '{}'::jsonb NOT NULL,
7
+ "deleted_at" timestamp,
8
+ "created_at" timestamp NOT NULL,
9
+ "updated_at" timestamp NOT NULL
10
+ );
11
+ --> statement-breakpoint
12
+ CREATE TABLE "paykit_entitlement" (
13
+ "id" text PRIMARY KEY NOT NULL,
14
+ "subscription_id" text,
15
+ "customer_id" text NOT NULL,
16
+ "feature_id" text NOT NULL,
17
+ "limit" integer,
18
+ "balance" integer,
19
+ "next_reset_at" timestamp,
20
+ "created_at" timestamp NOT NULL,
21
+ "updated_at" timestamp NOT NULL
22
+ );
23
+ --> statement-breakpoint
24
+ CREATE TABLE "paykit_feature" (
25
+ "id" text PRIMARY KEY NOT NULL,
26
+ "type" text NOT NULL,
27
+ "created_at" timestamp NOT NULL,
28
+ "updated_at" timestamp NOT NULL
29
+ );
30
+ --> statement-breakpoint
31
+ CREATE TABLE "paykit_invoice" (
32
+ "id" text PRIMARY KEY NOT NULL,
33
+ "customer_id" text NOT NULL,
34
+ "subscription_id" text,
35
+ "type" text NOT NULL,
36
+ "status" text NOT NULL,
37
+ "amount" integer NOT NULL,
38
+ "currency" text NOT NULL,
39
+ "description" text,
40
+ "hosted_url" text,
41
+ "provider_id" text NOT NULL,
42
+ "provider_data" jsonb NOT NULL,
43
+ "period_start_at" timestamp,
44
+ "period_end_at" timestamp,
45
+ "created_at" timestamp NOT NULL,
46
+ "updated_at" timestamp NOT NULL
47
+ );
48
+ --> statement-breakpoint
49
+ CREATE TABLE "paykit_metadata" (
50
+ "id" text PRIMARY KEY NOT NULL,
51
+ "provider_id" text NOT NULL,
52
+ "type" text NOT NULL,
53
+ "data" jsonb NOT NULL,
54
+ "provider_checkout_session_id" text,
55
+ "expires_at" timestamp,
56
+ "created_at" timestamp NOT NULL
57
+ );
58
+ --> statement-breakpoint
59
+ CREATE TABLE "paykit_payment_method" (
60
+ "id" text PRIMARY KEY NOT NULL,
61
+ "customer_id" text NOT NULL,
62
+ "provider_id" text NOT NULL,
63
+ "provider_data" jsonb NOT NULL,
64
+ "is_default" boolean DEFAULT false NOT NULL,
65
+ "deleted_at" timestamp,
66
+ "created_at" timestamp NOT NULL,
67
+ "updated_at" timestamp NOT NULL
68
+ );
69
+ --> statement-breakpoint
70
+ CREATE TABLE "paykit_product" (
71
+ "internal_id" text PRIMARY KEY NOT NULL,
72
+ "id" text NOT NULL,
73
+ "version" integer DEFAULT 1 NOT NULL,
74
+ "name" text NOT NULL,
75
+ "group" text DEFAULT '' NOT NULL,
76
+ "is_default" boolean DEFAULT false NOT NULL,
77
+ "price_amount" integer,
78
+ "price_interval" text,
79
+ "hash" text,
80
+ "provider" jsonb DEFAULT '{}'::jsonb NOT NULL,
81
+ "created_at" timestamp NOT NULL,
82
+ "updated_at" timestamp NOT NULL
83
+ );
84
+ --> statement-breakpoint
85
+ CREATE TABLE "paykit_product_feature" (
86
+ "product_internal_id" text NOT NULL,
87
+ "feature_id" text NOT NULL,
88
+ "limit" integer,
89
+ "reset_interval" text,
90
+ "config" jsonb,
91
+ "created_at" timestamp NOT NULL,
92
+ "updated_at" timestamp NOT NULL,
93
+ CONSTRAINT "paykit_product_feature_product_internal_id_feature_id_pk" PRIMARY KEY("product_internal_id","feature_id")
94
+ );
95
+ --> statement-breakpoint
96
+ CREATE TABLE "paykit_subscription" (
97
+ "id" text PRIMARY KEY NOT NULL,
98
+ "customer_id" text NOT NULL,
99
+ "product_internal_id" text NOT NULL,
100
+ "provider_id" text,
101
+ "provider_data" jsonb,
102
+ "status" text NOT NULL,
103
+ "canceled" boolean DEFAULT false NOT NULL,
104
+ "cancel_at_period_end" boolean DEFAULT false NOT NULL,
105
+ "started_at" timestamp,
106
+ "trial_ends_at" timestamp,
107
+ "current_period_start_at" timestamp,
108
+ "current_period_end_at" timestamp,
109
+ "canceled_at" timestamp,
110
+ "ended_at" timestamp,
111
+ "scheduled_product_id" text,
112
+ "quantity" integer DEFAULT 1 NOT NULL,
113
+ "created_at" timestamp NOT NULL,
114
+ "updated_at" timestamp NOT NULL
115
+ );
116
+ --> statement-breakpoint
117
+ CREATE TABLE "paykit_webhook_event" (
118
+ "id" text PRIMARY KEY NOT NULL,
119
+ "provider_id" text NOT NULL,
120
+ "provider_event_id" text NOT NULL,
121
+ "type" text NOT NULL,
122
+ "payload" jsonb NOT NULL,
123
+ "status" text NOT NULL,
124
+ "error" text,
125
+ "trace_id" text,
126
+ "received_at" timestamp NOT NULL,
127
+ "processed_at" timestamp
128
+ );
129
+ --> statement-breakpoint
130
+ ALTER TABLE "paykit_entitlement" ADD CONSTRAINT "paykit_entitlement_subscription_id_paykit_subscription_id_fk" FOREIGN KEY ("subscription_id") REFERENCES "public"."paykit_subscription"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
131
+ ALTER TABLE "paykit_entitlement" ADD CONSTRAINT "paykit_entitlement_customer_id_paykit_customer_id_fk" FOREIGN KEY ("customer_id") REFERENCES "public"."paykit_customer"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
132
+ ALTER TABLE "paykit_entitlement" ADD CONSTRAINT "paykit_entitlement_feature_id_paykit_feature_id_fk" FOREIGN KEY ("feature_id") REFERENCES "public"."paykit_feature"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
133
+ ALTER TABLE "paykit_invoice" ADD CONSTRAINT "paykit_invoice_customer_id_paykit_customer_id_fk" FOREIGN KEY ("customer_id") REFERENCES "public"."paykit_customer"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
134
+ ALTER TABLE "paykit_invoice" ADD CONSTRAINT "paykit_invoice_subscription_id_paykit_subscription_id_fk" FOREIGN KEY ("subscription_id") REFERENCES "public"."paykit_subscription"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
135
+ ALTER TABLE "paykit_payment_method" ADD CONSTRAINT "paykit_payment_method_customer_id_paykit_customer_id_fk" FOREIGN KEY ("customer_id") REFERENCES "public"."paykit_customer"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
136
+ ALTER TABLE "paykit_product_feature" ADD CONSTRAINT "paykit_product_feature_product_internal_id_paykit_product_internal_id_fk" FOREIGN KEY ("product_internal_id") REFERENCES "public"."paykit_product"("internal_id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
137
+ ALTER TABLE "paykit_product_feature" ADD CONSTRAINT "paykit_product_feature_feature_id_paykit_feature_id_fk" FOREIGN KEY ("feature_id") REFERENCES "public"."paykit_feature"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
138
+ ALTER TABLE "paykit_subscription" ADD CONSTRAINT "paykit_subscription_customer_id_paykit_customer_id_fk" FOREIGN KEY ("customer_id") REFERENCES "public"."paykit_customer"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
139
+ ALTER TABLE "paykit_subscription" ADD CONSTRAINT "paykit_subscription_product_internal_id_paykit_product_internal_id_fk" FOREIGN KEY ("product_internal_id") REFERENCES "public"."paykit_product"("internal_id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
140
+ CREATE INDEX "paykit_customer_deleted_at_idx" ON "paykit_customer" USING btree ("deleted_at");--> statement-breakpoint
141
+ CREATE INDEX "paykit_entitlement_subscription_idx" ON "paykit_entitlement" USING btree ("subscription_id");--> statement-breakpoint
142
+ CREATE INDEX "paykit_entitlement_customer_feature_idx" ON "paykit_entitlement" USING btree ("customer_id","feature_id");--> statement-breakpoint
143
+ CREATE INDEX "paykit_entitlement_next_reset_idx" ON "paykit_entitlement" USING btree ("next_reset_at");--> statement-breakpoint
144
+ CREATE INDEX "paykit_invoice_customer_idx" ON "paykit_invoice" USING btree ("customer_id","created_at");--> statement-breakpoint
145
+ CREATE INDEX "paykit_invoice_subscription_idx" ON "paykit_invoice" USING btree ("subscription_id");--> statement-breakpoint
146
+ CREATE INDEX "paykit_invoice_provider_idx" ON "paykit_invoice" USING btree ("provider_id");--> statement-breakpoint
147
+ CREATE UNIQUE INDEX "paykit_metadata_checkout_session_unique" ON "paykit_metadata" USING btree ("provider_id","provider_checkout_session_id");--> statement-breakpoint
148
+ CREATE INDEX "paykit_payment_method_customer_idx" ON "paykit_payment_method" USING btree ("customer_id","deleted_at");--> statement-breakpoint
149
+ CREATE INDEX "paykit_payment_method_provider_idx" ON "paykit_payment_method" USING btree ("provider_id");--> statement-breakpoint
150
+ CREATE UNIQUE INDEX "paykit_product_id_version_unique" ON "paykit_product" USING btree ("id","version");--> statement-breakpoint
151
+ CREATE INDEX "paykit_product_default_idx" ON "paykit_product" USING btree ("is_default");--> statement-breakpoint
152
+ CREATE INDEX "paykit_product_feature_feature_idx" ON "paykit_product_feature" USING btree ("feature_id");--> statement-breakpoint
153
+ CREATE INDEX "paykit_subscription_customer_status_idx" ON "paykit_subscription" USING btree ("customer_id","status","ended_at");--> statement-breakpoint
154
+ CREATE INDEX "paykit_subscription_product_idx" ON "paykit_subscription" USING btree ("product_internal_id");--> statement-breakpoint
155
+ CREATE INDEX "paykit_subscription_provider_idx" ON "paykit_subscription" USING btree ("provider_id");--> statement-breakpoint
156
+ CREATE UNIQUE INDEX "paykit_webhook_event_provider_unique" ON "paykit_webhook_event" USING btree ("provider_id","provider_event_id");--> statement-breakpoint
157
+ CREATE INDEX "paykit_webhook_event_status_idx" ON "paykit_webhook_event" USING btree ("provider_id","status");