paykitjs 0.0.1-alpha.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 (74) hide show
  1. package/LICENSE +21 -0
  2. package/dist/_virtual/_rolldown/runtime.js +14 -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/init.js +264 -0
  8. package/dist/cli/commands/push.js +71 -0
  9. package/dist/cli/commands/status.js +84 -0
  10. package/dist/cli/index.d.ts +1 -0
  11. package/dist/cli/index.js +45 -0
  12. package/dist/cli/templates/index.js +64 -0
  13. package/dist/cli/utils/detect.js +73 -0
  14. package/dist/cli/utils/format.js +58 -0
  15. package/dist/cli/utils/get-config.js +117 -0
  16. package/dist/cli/utils/shared.js +81 -0
  17. package/dist/cli/utils/telemetry.js +63 -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 +67 -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/utilities/dependencies/check-dependencies.js +16 -0
  69. package/dist/utilities/dependencies/get-dependencies.js +68 -0
  70. package/dist/utilities/dependencies/index.js +8 -0
  71. package/dist/utilities/dependencies/paykit-package-list.js +8 -0
  72. package/dist/webhook/webhook.api.js +29 -0
  73. package/dist/webhook/webhook.service.js +143 -0
  74. package/package.json +76 -0
@@ -0,0 +1,896 @@
1
+ import { PAYKIT_ERROR_CODES, PayKitError } from "../core/errors.js";
2
+ import { entitlement, product, subscription } from "../database/schema.js";
3
+ import { generateId } from "../core/utils.js";
4
+ import { getDefaultProductInGroup, getLatestProduct, getProductByProviderPriceId, withProviderInfo } from "../product/product.service.js";
5
+ import { upsertInvoiceRecord } from "../invoice/invoice.service.js";
6
+ import { getDefaultPaymentMethod } from "../payment-method/payment-method.service.js";
7
+ import { findCustomerByProviderCustomerId, upsertProviderCustomer } from "../customer/customer.service.js";
8
+ import { and, desc, eq, inArray, isNull, or, sql } from "drizzle-orm";
9
+ //#region src/subscription/subscription.service.ts
10
+ /** Applies the requested plan change for a customer and returns the immediate subscribe result. */
11
+ async function subscribeToPlan(ctx, input) {
12
+ return ctx.logger.trace.run("sub", async () => {
13
+ const startTime = Date.now();
14
+ ctx.logger.info({
15
+ planId: input.planId,
16
+ customerId: input.customerId
17
+ }, "subscribe started");
18
+ const subCtx = await loadSubscribeContext(ctx, input);
19
+ let result;
20
+ if (isSamePlan(subCtx)) result = await handleSamePlanSubscribe(ctx, subCtx);
21
+ else if (!subCtx.activeSubscription) result = await handleInitialSubscribe(ctx, subCtx);
22
+ else if (!hasProviderSubscription(subCtx.activeSubscription)) result = await handleLocalPlanSwitch(ctx, subCtx);
23
+ else if (subCtx.isFreeTarget) result = await handleCancelToFree(ctx, subCtx);
24
+ else if (!subCtx.isUpgrade) result = await handleScheduledDowngrade(ctx, subCtx);
25
+ else result = await handleUpgrade(ctx, subCtx);
26
+ const duration = Date.now() - startTime;
27
+ ctx.logger.info({ duration }, "subscribe completed");
28
+ return result;
29
+ });
30
+ }
31
+ async function loadSubscribeContext(ctx, input) {
32
+ const providerId = ctx.provider.id;
33
+ const normalizedPlan = ctx.plans.planMap.get(input.planId);
34
+ const latestProduct = await getLatestProduct(ctx.database, input.planId);
35
+ const storedPlan = latestProduct ? withProviderInfo(latestProduct, providerId) : null;
36
+ if (!normalizedPlan || !storedPlan) throw PayKitError.from("NOT_FOUND", PAYKIT_ERROR_CODES.PLAN_NOT_FOUND, `Plan "${input.planId}" not found`);
37
+ if (storedPlan.hash !== normalizedPlan.hash) {
38
+ ctx.logger.error({ planId: input.planId }, `Plan "${input.planId}" is out of sync. Run \`paykitjs push\` to update.`);
39
+ throw PayKitError.from("INTERNAL_SERVER_ERROR", PAYKIT_ERROR_CODES.PLAN_NOT_SYNCED, `Plan "${input.planId}" schema has changed since last sync. Run \`paykitjs push\` to update.`);
40
+ }
41
+ const isFreeTarget = storedPlan.priceAmount === null;
42
+ const isPaidTarget = !isFreeTarget;
43
+ if (isPaidTarget && !storedPlan.providerPriceId) throw PayKitError.from("INTERNAL_SERVER_ERROR", PAYKIT_ERROR_CODES.PLAN_NOT_SYNCED, `Plan "${input.planId}" is not synced with provider`);
44
+ await warnOnDuplicateActiveSubscriptionGroups(ctx, input.customerId);
45
+ const { providerCustomerId } = await upsertProviderCustomer(ctx, { customerId: input.customerId });
46
+ const hasDefaultPaymentMethod = await getDefaultPaymentMethod(ctx.database, {
47
+ customerId: input.customerId,
48
+ providerId
49
+ }) != null;
50
+ const activeSubscription = storedPlan.group ? await getActiveSubscriptionInGroup(ctx.database, {
51
+ customerId: input.customerId,
52
+ group: storedPlan.group
53
+ }) : null;
54
+ const scheduledSubscriptions = storedPlan.group ? await getScheduledSubscriptionsInGroup(ctx.database, {
55
+ customerId: input.customerId,
56
+ group: storedPlan.group
57
+ }) : [];
58
+ const activeAmount = activeSubscription?.priceAmount ?? 0;
59
+ const targetAmount = storedPlan.priceAmount ?? 0;
60
+ const isUpgrade = activeSubscription != null && hasProviderSubscription(activeSubscription) && targetAmount > activeAmount;
61
+ return {
62
+ activeSubscription,
63
+ cancelUrl: input.cancelUrl,
64
+ customerId: input.customerId,
65
+ isFreeTarget,
66
+ isPaidTarget,
67
+ isUpgrade,
68
+ normalizedPlan,
69
+ providerCustomerId,
70
+ providerId,
71
+ scheduledSubscriptions,
72
+ shouldUseCheckout: isPaidTarget && (input.forceCheckout === true || !hasDefaultPaymentMethod),
73
+ storedPlan,
74
+ successUrl: input.successUrl
75
+ };
76
+ }
77
+ function buildSubscribeResult(input) {
78
+ return {
79
+ invoice: input.invoice ? {
80
+ currency: input.invoice.currency,
81
+ hostedUrl: input.invoice.hostedUrl ?? null,
82
+ providerInvoiceId: input.invoice.providerInvoiceId,
83
+ status: input.invoice.status,
84
+ totalAmount: input.invoice.totalAmount
85
+ } : void 0,
86
+ paymentUrl: input.paymentUrl,
87
+ requiredAction: input.requiredAction ?? null
88
+ };
89
+ }
90
+ async function cancelExistingProviderSubscriptionForCheckout(ctx, completion) {
91
+ const activeSubscriptionRef = getProviderSubscriptionRef(completion.subCtx.activeSubscription);
92
+ if (isSamePlan(completion.subCtx)) {
93
+ if (activeSubscriptionRef.subscriptionId === completion.subscription.providerSubscriptionId) return;
94
+ throw PayKitError.from("BAD_REQUEST", PAYKIT_ERROR_CODES.PROVIDER_WEBHOOK_INVALID, `Checkout completed for plan "${completion.subCtx.storedPlan.id}" after a different active subscription was already present`);
95
+ }
96
+ if (!completion.subCtx.activeSubscription || !activeSubscriptionRef.subscriptionId) return;
97
+ if (!completion.subCtx.isUpgrade) throw PayKitError.from("BAD_REQUEST", PAYKIT_ERROR_CODES.PROVIDER_WEBHOOK_INVALID, `Checkout completion is only valid for new paid subscriptions or upgrades to "${completion.subCtx.storedPlan.id}"`);
98
+ await ctx.stripe.cancelSubscription({
99
+ currentPeriodEndAt: completion.subCtx.activeSubscription.currentPeriodEndAt,
100
+ providerSubscriptionId: activeSubscriptionRef.subscriptionId,
101
+ providerSubscriptionScheduleId: activeSubscriptionRef.subscriptionScheduleId
102
+ });
103
+ }
104
+ async function applyCheckoutSubscription(ctx, completion) {
105
+ const activeSubscriptionRef = getProviderSubscriptionRef(completion.subCtx.activeSubscription);
106
+ if (isSamePlan(completion.subCtx)) {
107
+ if (activeSubscriptionRef.subscriptionId === completion.subscription.providerSubscriptionId) return;
108
+ throw PayKitError.from("BAD_REQUEST", PAYKIT_ERROR_CODES.PROVIDER_WEBHOOK_INVALID, `Checkout completed for plan "${completion.subCtx.storedPlan.id}" after a different active subscription was already present`);
109
+ }
110
+ if (completion.subCtx.activeSubscription && activeSubscriptionRef.subscriptionId) {
111
+ await deleteScheduledSubscriptionsInGroupIfNeeded(ctx.database, completion.subCtx);
112
+ await endSubscriptions(ctx.database, [completion.subCtx.activeSubscription.id], {
113
+ canceled: false,
114
+ endedAt: /* @__PURE__ */ new Date(),
115
+ status: "ended"
116
+ });
117
+ await upsertProviderBackedTargetSubscription(ctx.database, completion.subCtx, completion, { deferred: true });
118
+ return;
119
+ }
120
+ if (completion.subCtx.activeSubscription) await endSubscriptions(ctx.database, [completion.subCtx.activeSubscription.id], {
121
+ canceled: false,
122
+ endedAt: /* @__PURE__ */ new Date(),
123
+ status: "ended"
124
+ });
125
+ await upsertProviderBackedTargetSubscription(ctx.database, completion.subCtx, completion, { deferred: true });
126
+ }
127
+ async function prepareSubscribeCheckoutCompleted(ctx, event) {
128
+ if (event.payload.mode !== "subscription") return null;
129
+ if (event.payload.metadata?.paykit_intent !== "subscribe") return null;
130
+ const customerId = event.payload.metadata?.paykit_customer_id;
131
+ const planId = event.payload.metadata?.paykit_plan_id;
132
+ if (!customerId || !planId) throw PayKitError.from("BAD_REQUEST", PAYKIT_ERROR_CODES.PROVIDER_WEBHOOK_INVALID, "Subscribe checkout metadata is missing paykit_customer_id or paykit_plan_id");
133
+ const checkoutSubscription = event.payload.subscription ?? null;
134
+ if (!checkoutSubscription) throw PayKitError.from("BAD_REQUEST", PAYKIT_ERROR_CODES.PROVIDER_WEBHOOK_INVALID, "Subscribe checkout completion is missing subscription data");
135
+ const subCtx = await loadSubscribeContext(ctx, {
136
+ customerId,
137
+ planId,
138
+ successUrl: "https://paykit.invalid/checkout"
139
+ });
140
+ if (subCtx.storedPlan.providerPriceId !== checkoutSubscription.providerPriceId) throw PayKitError.from("BAD_REQUEST", PAYKIT_ERROR_CODES.PROVIDER_WEBHOOK_INVALID, `Checkout price mismatch for plan "${planId}"`);
141
+ const completion = {
142
+ customerId,
143
+ invoice: event.payload.invoice ?? null,
144
+ subCtx,
145
+ subscription: checkoutSubscription
146
+ };
147
+ await cancelExistingProviderSubscriptionForCheckout(ctx, completion);
148
+ return completion;
149
+ }
150
+ async function handleSubscribeCheckoutCompleted(ctx, completion) {
151
+ await applyCheckoutSubscription(ctx, completion);
152
+ return completion.customerId;
153
+ }
154
+ function isSamePlan(subCtx) {
155
+ return subCtx.activeSubscription?.planId === subCtx.storedPlan.id;
156
+ }
157
+ function getSubscriptionEffectiveDate(input) {
158
+ return input.currentPeriodStartAt ?? input.currentPeriodEndAt ?? /* @__PURE__ */ new Date();
159
+ }
160
+ async function ensureScheduledDefaultPlan(ctx, input) {
161
+ if ((await getScheduledSubscriptionsInGroup(ctx.database, {
162
+ customerId: input.customerId,
163
+ group: input.group
164
+ })).length > 0) return;
165
+ const defaultPlan = await getDefaultProductInGroup(ctx.database, input.group);
166
+ if (!defaultPlan || defaultPlan.priceAmount !== null) return;
167
+ const normalizedPlan = ctx.plans.planMap.get(defaultPlan.id);
168
+ if (!normalizedPlan) return;
169
+ await insertSubscriptionRecord(ctx.database, {
170
+ customerId: input.customerId,
171
+ planFeatures: normalizedPlan.includes,
172
+ productInternalId: defaultPlan.internalId,
173
+ startedAt: input.startsAt,
174
+ status: "scheduled"
175
+ });
176
+ }
177
+ async function activateScheduledSubscriptionForGroup(ctx, input) {
178
+ const activationDate = getSubscriptionEffectiveDate({
179
+ currentPeriodEndAt: input.subscriptionCurrentPeriodEndAt,
180
+ currentPeriodStartAt: input.subscriptionCurrentPeriodStartAt
181
+ });
182
+ const targetSub = (await getScheduledSubscriptionsInGroup(ctx.database, {
183
+ customerId: input.customerId,
184
+ group: input.productGroup
185
+ })).find((scheduled) => {
186
+ if (scheduled.startedAt && scheduled.startedAt > activationDate) return false;
187
+ if (!input.productInternalId) return true;
188
+ return scheduled.productInternalId === input.productInternalId;
189
+ });
190
+ if (!targetSub) return null;
191
+ const activeSub = await getActiveSubscriptionInGroup(ctx.database, {
192
+ customerId: input.customerId,
193
+ group: input.productGroup
194
+ });
195
+ if (activeSub && activeSub.id !== targetSub.id) await endSubscriptions(ctx.database, [activeSub.id], {
196
+ canceled: activeSub.canceled,
197
+ endedAt: activationDate,
198
+ status: "ended"
199
+ });
200
+ await activateScheduledSubscription(ctx.database, {
201
+ currentPeriodEndAt: input.subscriptionCurrentPeriodEndAt,
202
+ currentPeriodStartAt: input.subscriptionCurrentPeriodStartAt,
203
+ subscriptionId: targetSub.id,
204
+ startedAt: targetSub.startedAt ?? activationDate,
205
+ status: input.subscriptionStatus,
206
+ providerData: input.providerData,
207
+ providerId: input.providerId
208
+ });
209
+ return targetSub.id;
210
+ }
211
+ async function applySubscriptionWebhookAction(ctx, action) {
212
+ const customerRow = await findCustomerByProviderCustomerId(ctx.database, {
213
+ providerCustomerId: action.data.providerCustomerId,
214
+ providerId: ctx.provider.id
215
+ });
216
+ if (!customerRow) return null;
217
+ if (action.type === "subscription.delete") {
218
+ const existingSub = await getSubscriptionByProviderSubscriptionId(ctx.database, {
219
+ providerId: ctx.provider.id,
220
+ providerSubscriptionId: action.data.providerSubscriptionId
221
+ });
222
+ if (!existingSub) return customerRow.id;
223
+ const effectiveEndDate = existingSub.currentPeriodEndAt ?? /* @__PURE__ */ new Date();
224
+ await endSubscriptions(ctx.database, [existingSub.id], {
225
+ canceled: true,
226
+ endedAt: effectiveEndDate,
227
+ status: "canceled"
228
+ });
229
+ const productGroup = (await ctx.database.query.product.findFirst({ where: eq(product.internalId, existingSub.productInternalId) }))?.group ?? "";
230
+ if (productGroup) {
231
+ const effectiveDate = existingSub.currentPeriodEndAt ?? /* @__PURE__ */ new Date();
232
+ await ensureScheduledDefaultPlan(ctx, {
233
+ customerId: customerRow.id,
234
+ group: productGroup,
235
+ startsAt: effectiveDate
236
+ });
237
+ await activateScheduledSubscriptionForGroup(ctx, {
238
+ customerId: customerRow.id,
239
+ productGroup,
240
+ subscriptionCurrentPeriodEndAt: null,
241
+ subscriptionCurrentPeriodStartAt: effectiveDate,
242
+ subscriptionStatus: "active"
243
+ });
244
+ }
245
+ return customerRow.id;
246
+ }
247
+ const existingSub = await getSubscriptionByProviderSubscriptionId(ctx.database, {
248
+ providerId: ctx.provider.id,
249
+ providerSubscriptionId: action.data.subscription.providerSubscriptionId
250
+ });
251
+ const storedProduct = action.data.subscription.providerPriceId ? await getProductByProviderPriceId(ctx.database, {
252
+ providerId: ctx.provider.id,
253
+ providerPriceId: action.data.subscription.providerPriceId
254
+ }) : null;
255
+ const normalizedPlan = storedProduct ? ctx.plans.planMap.get(storedProduct.id) ?? null : null;
256
+ const providerData = { subscriptionId: action.data.subscription.providerSubscriptionId };
257
+ const targetSub = existingSub ?? (storedProduct && normalizedPlan ? await insertSubscriptionRecord(ctx.database, {
258
+ currentPeriodEndAt: action.data.subscription.currentPeriodEndAt ?? null,
259
+ currentPeriodStartAt: action.data.subscription.currentPeriodStartAt ?? null,
260
+ customerId: customerRow.id,
261
+ planFeatures: normalizedPlan.includes,
262
+ productInternalId: storedProduct.internalId,
263
+ providerData,
264
+ providerId: ctx.provider.id,
265
+ startedAt: action.data.subscription.currentPeriodStartAt ?? /* @__PURE__ */ new Date(),
266
+ status: action.data.subscription.status
267
+ }) : null);
268
+ if (!targetSub) return customerRow.id;
269
+ await syncSubscriptionFromProvider(ctx.database, {
270
+ providerSubscription: action.data.subscription,
271
+ subscriptionId: targetSub.id
272
+ });
273
+ await syncSubscriptionBillingState(ctx.database, {
274
+ providerData,
275
+ subscriptionId: targetSub.id
276
+ });
277
+ if (storedProduct && existingSub?.cancelAtPeriodEnd && !action.data.subscription.cancelAtPeriodEnd && !action.data.subscription.providerSubscriptionScheduleId) {
278
+ await deleteScheduledSubscriptionsInGroup(ctx.database, {
279
+ customerId: customerRow.id,
280
+ group: storedProduct.group
281
+ });
282
+ const activeSub = await getActiveSubscriptionInGroup(ctx.database, {
283
+ customerId: customerRow.id,
284
+ group: storedProduct.group
285
+ });
286
+ if (activeSub) await replaceSubscriptionSchedule(ctx.database, {
287
+ scheduledProductId: null,
288
+ subscriptionId: activeSub.id
289
+ });
290
+ }
291
+ if (storedProduct) {
292
+ const activeSub = await getActiveSubscriptionInGroup(ctx.database, {
293
+ customerId: customerRow.id,
294
+ group: storedProduct.group
295
+ });
296
+ const competingSubscriptions = (await getActiveSubscriptionsInGroup(ctx.database, {
297
+ customerId: customerRow.id,
298
+ group: storedProduct.group
299
+ })).filter((sub) => sub.id !== targetSub.id);
300
+ const subStatus = action.data.subscription.status;
301
+ const isTerminal = subStatus === "canceled" || subStatus === "ended" || subStatus === "unpaid";
302
+ if (action.data.subscription.cancelAtPeriodEnd && activeSub) {
303
+ await scheduleSubscriptionCancellation(ctx.database, {
304
+ canceledAt: action.data.subscription.canceledAt ?? /* @__PURE__ */ new Date(),
305
+ currentPeriodEndAt: action.data.subscription.currentPeriodEndAt ?? activeSub.currentPeriodEndAt,
306
+ subscriptionId: activeSub.id
307
+ });
308
+ await ensureScheduledDefaultPlan(ctx, {
309
+ customerId: customerRow.id,
310
+ group: storedProduct.group,
311
+ startsAt: action.data.subscription.currentPeriodEndAt ?? activeSub.currentPeriodEndAt ?? /* @__PURE__ */ new Date()
312
+ });
313
+ }
314
+ if (isTerminal && activeSub) {
315
+ const effectiveDate = action.data.subscription.currentPeriodEndAt ?? /* @__PURE__ */ new Date();
316
+ await endSubscriptions(ctx.database, [activeSub.id], {
317
+ canceled: true,
318
+ endedAt: effectiveDate,
319
+ status: "canceled"
320
+ });
321
+ await ensureScheduledDefaultPlan(ctx, {
322
+ customerId: customerRow.id,
323
+ group: storedProduct.group,
324
+ startsAt: effectiveDate
325
+ });
326
+ await activateScheduledSubscriptionForGroup(ctx, {
327
+ customerId: customerRow.id,
328
+ productGroup: storedProduct.group,
329
+ subscriptionCurrentPeriodStartAt: effectiveDate,
330
+ subscriptionStatus: "active"
331
+ });
332
+ } else {
333
+ if (competingSubscriptions.length > 0) {
334
+ const effectiveDate = action.data.subscription.currentPeriodStartAt ?? action.data.subscription.currentPeriodEndAt ?? /* @__PURE__ */ new Date();
335
+ await endSubscriptions(ctx.database, competingSubscriptions.map((sub) => sub.id), {
336
+ canceled: false,
337
+ endedAt: effectiveDate,
338
+ status: "ended"
339
+ });
340
+ }
341
+ const activatedSubId = await activateScheduledSubscriptionForGroup(ctx, {
342
+ customerId: customerRow.id,
343
+ productGroup: storedProduct.group,
344
+ productInternalId: storedProduct.internalId,
345
+ providerData,
346
+ providerId: ctx.provider.id,
347
+ subscriptionCurrentPeriodEndAt: action.data.subscription.currentPeriodEndAt,
348
+ subscriptionCurrentPeriodStartAt: action.data.subscription.currentPeriodStartAt,
349
+ subscriptionStatus: action.data.subscription.status
350
+ });
351
+ if (activatedSubId) await syncSubscriptionFromProvider(ctx.database, {
352
+ providerSubscription: action.data.subscription,
353
+ subscriptionId: activatedSubId
354
+ });
355
+ }
356
+ }
357
+ return customerRow.id;
358
+ }
359
+ function getProviderSubscriptionId(subscription) {
360
+ if (subscription?.providerData == null) return null;
361
+ return typeof subscription.providerData.subscriptionId === "string" ? subscription.providerData.subscriptionId : null;
362
+ }
363
+ function hasProviderSubscription(subscription) {
364
+ return getProviderSubscriptionId(subscription) != null;
365
+ }
366
+ function getProviderSubscriptionRef(subscription) {
367
+ if (subscription?.providerData == null) return {
368
+ subscriptionId: null,
369
+ subscriptionScheduleId: null
370
+ };
371
+ const providerData = subscription.providerData;
372
+ return {
373
+ subscriptionId: typeof providerData.subscriptionId === "string" ? providerData.subscriptionId : null,
374
+ subscriptionScheduleId: typeof providerData.subscriptionScheduleId === "string" ? providerData.subscriptionScheduleId : null
375
+ };
376
+ }
377
+ /** Returns a noop or resumes the current provider subscription. */
378
+ async function handleSamePlanSubscribe(ctx, subCtx) {
379
+ const activeSubscription = subCtx.activeSubscription;
380
+ if (!activeSubscription) throw PayKitError.from("INTERNAL_SERVER_ERROR", PAYKIT_ERROR_CODES.SUBSCRIPTION_CREATE_FAILED);
381
+ const hasPendingChange = subCtx.scheduledSubscriptions.length > 0 || activeSubscription.canceled;
382
+ if (!hasProviderSubscription(activeSubscription) || !hasPendingChange) return buildSubscribeResult({ paymentUrl: null });
383
+ const activeSubscriptionRef = getProviderSubscriptionRef(activeSubscription);
384
+ const stripeResult = await ctx.stripe.resumeSubscription({
385
+ providerSubscriptionId: activeSubscriptionRef.subscriptionId,
386
+ providerSubscriptionScheduleId: activeSubscriptionRef.subscriptionScheduleId
387
+ });
388
+ await ctx.database.transaction(async (tx) => {
389
+ await deleteScheduledSubscriptionsInGroupIfNeeded(tx, subCtx);
390
+ await syncSubscriptionFromProvider(tx, {
391
+ subscriptionId: activeSubscription.id,
392
+ providerSubscription: stripeResult.subscription ?? {
393
+ cancelAtPeriodEnd: false,
394
+ providerSubscriptionId: activeSubscriptionRef.subscriptionId,
395
+ providerSubscriptionScheduleId: null,
396
+ status: activeSubscription.status
397
+ }
398
+ });
399
+ await replaceSubscriptionSchedule(tx, {
400
+ subscriptionId: activeSubscription.id,
401
+ scheduledProductId: null
402
+ });
403
+ if (stripeResult.subscription) await syncSubscriptionBillingState(tx, {
404
+ currentPeriodEndAt: stripeResult.subscription.currentPeriodEndAt,
405
+ currentPeriodStartAt: stripeResult.subscription.currentPeriodStartAt,
406
+ providerData: {
407
+ subscriptionId: stripeResult.subscription.providerSubscriptionId,
408
+ subscriptionScheduleId: stripeResult.subscription.providerSubscriptionScheduleId ?? null
409
+ },
410
+ status: stripeResult.subscription.status,
411
+ subscriptionId: activeSubscription.id
412
+ });
413
+ });
414
+ return buildSubscribeResult({
415
+ invoice: stripeResult.invoice,
416
+ paymentUrl: stripeResult.paymentUrl,
417
+ requiredAction: stripeResult.requiredAction
418
+ });
419
+ }
420
+ /** Creates the first subscription in the product group. */
421
+ async function handleInitialSubscribe(ctx, subCtx) {
422
+ if (subCtx.isFreeTarget) {
423
+ await ctx.database.transaction(async (tx) => {
424
+ await insertLocalTargetSubscription(tx, subCtx, {
425
+ startedAt: /* @__PURE__ */ new Date(),
426
+ status: "active"
427
+ });
428
+ });
429
+ return buildSubscribeResult({ paymentUrl: null });
430
+ }
431
+ if (subCtx.shouldUseCheckout) return createCheckoutSubscribe(ctx, subCtx);
432
+ const stripeResult = await ctx.stripe.createSubscription({
433
+ providerCustomerId: subCtx.providerCustomerId,
434
+ providerPriceId: subCtx.storedPlan.providerPriceId
435
+ });
436
+ await ctx.database.transaction(async (tx) => {
437
+ if (!stripeResult.subscription) throw PayKitError.from("INTERNAL_SERVER_ERROR", PAYKIT_ERROR_CODES.SUBSCRIPTION_CREATE_FAILED);
438
+ await upsertProviderBackedTargetSubscription(tx, subCtx, {
439
+ invoice: stripeResult.invoice ?? null,
440
+ subscription: stripeResult.subscription
441
+ });
442
+ });
443
+ return buildSubscribeResult({
444
+ invoice: stripeResult.invoice,
445
+ paymentUrl: stripeResult.paymentUrl,
446
+ requiredAction: stripeResult.requiredAction
447
+ });
448
+ }
449
+ /** Replaces a local-only subscription with the requested target plan. */
450
+ async function handleLocalPlanSwitch(ctx, subCtx) {
451
+ const activeSubscription = subCtx.activeSubscription;
452
+ if (!activeSubscription) throw PayKitError.from("INTERNAL_SERVER_ERROR", PAYKIT_ERROR_CODES.SUBSCRIPTION_CREATE_FAILED);
453
+ if (subCtx.shouldUseCheckout) return createCheckoutSubscribe(ctx, subCtx);
454
+ if (subCtx.isFreeTarget) {
455
+ await ctx.database.transaction(async (tx) => {
456
+ const now = /* @__PURE__ */ new Date();
457
+ await endSubscriptions(tx, [activeSubscription.id], {
458
+ canceled: false,
459
+ endedAt: now,
460
+ status: "ended"
461
+ });
462
+ await insertLocalTargetSubscription(tx, subCtx, {
463
+ startedAt: now,
464
+ status: "active"
465
+ });
466
+ });
467
+ return buildSubscribeResult({ paymentUrl: null });
468
+ }
469
+ const stripeResult = await ctx.stripe.createSubscription({
470
+ providerCustomerId: subCtx.providerCustomerId,
471
+ providerPriceId: subCtx.storedPlan.providerPriceId
472
+ });
473
+ await ctx.database.transaction(async (tx) => {
474
+ if (!stripeResult.subscription) throw PayKitError.from("INTERNAL_SERVER_ERROR", PAYKIT_ERROR_CODES.SUBSCRIPTION_CREATE_FAILED);
475
+ await endSubscriptions(tx, [activeSubscription.id], {
476
+ canceled: false,
477
+ endedAt: /* @__PURE__ */ new Date(),
478
+ status: "ended"
479
+ });
480
+ await upsertProviderBackedTargetSubscription(tx, subCtx, {
481
+ invoice: stripeResult.invoice ?? null,
482
+ subscription: stripeResult.subscription
483
+ });
484
+ });
485
+ return buildSubscribeResult({
486
+ invoice: stripeResult.invoice,
487
+ paymentUrl: stripeResult.paymentUrl,
488
+ requiredAction: stripeResult.requiredAction
489
+ });
490
+ }
491
+ /** Cancels the paid subscription and schedules the free plan for period end. */
492
+ async function handleCancelToFree(ctx, subCtx) {
493
+ const activeSubscription = subCtx.activeSubscription;
494
+ const activeSubscriptionRef = getProviderSubscriptionRef(activeSubscription);
495
+ if (!activeSubscription || !activeSubscriptionRef.subscriptionId) throw PayKitError.from("INTERNAL_SERVER_ERROR", PAYKIT_ERROR_CODES.SUBSCRIPTION_CREATE_FAILED);
496
+ const stripeResult = await ctx.stripe.cancelSubscription({
497
+ currentPeriodEndAt: activeSubscription.currentPeriodEndAt,
498
+ providerSubscriptionId: activeSubscriptionRef.subscriptionId,
499
+ providerSubscriptionScheduleId: activeSubscriptionRef.subscriptionScheduleId
500
+ });
501
+ await ctx.database.transaction(async (tx) => {
502
+ await clearScheduledSubscriptionsInGroupIfNeeded(tx, subCtx);
503
+ await insertLocalTargetSubscription(tx, subCtx, {
504
+ startedAt: activeSubscription.currentPeriodEndAt ?? null,
505
+ status: "scheduled"
506
+ });
507
+ await scheduleSubscriptionCancellation(tx, {
508
+ canceledAt: /* @__PURE__ */ new Date(),
509
+ currentPeriodEndAt: activeSubscription.currentPeriodEndAt ?? null,
510
+ subscriptionId: activeSubscription.id
511
+ });
512
+ await replaceSubscriptionSchedule(tx, {
513
+ scheduledProductId: subCtx.storedPlan.internalId,
514
+ subscriptionId: activeSubscription.id
515
+ });
516
+ if (stripeResult.subscription) await syncSubscriptionBillingState(tx, {
517
+ currentPeriodEndAt: stripeResult.subscription.currentPeriodEndAt,
518
+ currentPeriodStartAt: stripeResult.subscription.currentPeriodStartAt,
519
+ providerData: {
520
+ subscriptionId: stripeResult.subscription.providerSubscriptionId,
521
+ subscriptionScheduleId: stripeResult.subscription.providerSubscriptionScheduleId ?? null
522
+ },
523
+ status: stripeResult.subscription.status,
524
+ subscriptionId: activeSubscription.id
525
+ });
526
+ });
527
+ return buildSubscribeResult({
528
+ invoice: stripeResult.invoice,
529
+ paymentUrl: stripeResult.paymentUrl,
530
+ requiredAction: stripeResult.requiredAction
531
+ });
532
+ }
533
+ /** Schedules a lower paid tier to start when the current billing period ends. */
534
+ async function handleScheduledDowngrade(ctx, subCtx) {
535
+ const activeSubscription = subCtx.activeSubscription;
536
+ const activeSubscriptionRef = getProviderSubscriptionRef(activeSubscription);
537
+ if (!activeSubscription || !activeSubscriptionRef.subscriptionId) throw PayKitError.from("INTERNAL_SERVER_ERROR", PAYKIT_ERROR_CODES.SUBSCRIPTION_CREATE_FAILED);
538
+ const stripeResult = await ctx.stripe.scheduleSubscriptionChange({
539
+ providerPriceId: subCtx.storedPlan.providerPriceId,
540
+ providerSubscriptionId: activeSubscriptionRef.subscriptionId,
541
+ providerSubscriptionScheduleId: activeSubscriptionRef.subscriptionScheduleId
542
+ });
543
+ await ctx.database.transaction(async (tx) => {
544
+ await clearScheduledSubscriptionsInGroupIfNeeded(tx, subCtx);
545
+ await insertLocalTargetSubscription(tx, subCtx, {
546
+ startedAt: activeSubscription.currentPeriodEndAt ?? null,
547
+ status: "scheduled"
548
+ });
549
+ await scheduleSubscriptionCancellation(tx, {
550
+ canceledAt: /* @__PURE__ */ new Date(),
551
+ currentPeriodEndAt: activeSubscription.currentPeriodEndAt ?? null,
552
+ subscriptionId: activeSubscription.id
553
+ });
554
+ await replaceSubscriptionSchedule(tx, {
555
+ scheduledProductId: subCtx.storedPlan.internalId,
556
+ subscriptionId: activeSubscription.id
557
+ });
558
+ if (stripeResult.subscription) await syncSubscriptionBillingState(tx, {
559
+ currentPeriodEndAt: stripeResult.subscription.currentPeriodEndAt,
560
+ currentPeriodStartAt: stripeResult.subscription.currentPeriodStartAt,
561
+ providerData: {
562
+ subscriptionId: stripeResult.subscription.providerSubscriptionId,
563
+ subscriptionScheduleId: stripeResult.subscription.providerSubscriptionScheduleId ?? null
564
+ },
565
+ status: stripeResult.subscription.status,
566
+ subscriptionId: activeSubscription.id
567
+ });
568
+ });
569
+ return buildSubscribeResult({
570
+ invoice: stripeResult.invoice,
571
+ paymentUrl: stripeResult.paymentUrl,
572
+ requiredAction: stripeResult.requiredAction
573
+ });
574
+ }
575
+ /** Upgrades the customer immediately or redirects them to checkout. */
576
+ async function handleUpgrade(ctx, subCtx) {
577
+ const activeSubscription = subCtx.activeSubscription;
578
+ const activeSubscriptionRef = getProviderSubscriptionRef(activeSubscription);
579
+ if (!activeSubscription || !activeSubscriptionRef.subscriptionId) throw PayKitError.from("INTERNAL_SERVER_ERROR", PAYKIT_ERROR_CODES.SUBSCRIPTION_CREATE_FAILED);
580
+ if (subCtx.shouldUseCheckout) return createCheckoutSubscribe(ctx, subCtx);
581
+ const stripeResult = await ctx.stripe.updateSubscription({
582
+ providerPriceId: subCtx.storedPlan.providerPriceId,
583
+ providerSubscriptionId: activeSubscriptionRef.subscriptionId
584
+ });
585
+ await ctx.database.transaction(async (tx) => {
586
+ if (!stripeResult.subscription) throw PayKitError.from("INTERNAL_SERVER_ERROR", PAYKIT_ERROR_CODES.SUBSCRIPTION_CREATE_FAILED);
587
+ await deleteScheduledSubscriptionsInGroupIfNeeded(tx, subCtx);
588
+ await endSubscriptions(tx, [activeSubscription.id], {
589
+ canceled: false,
590
+ endedAt: /* @__PURE__ */ new Date(),
591
+ status: "ended"
592
+ });
593
+ await upsertProviderBackedTargetSubscription(tx, subCtx, {
594
+ invoice: stripeResult.invoice ?? null,
595
+ subscription: stripeResult.subscription
596
+ });
597
+ });
598
+ return buildSubscribeResult({
599
+ invoice: stripeResult.invoice,
600
+ paymentUrl: stripeResult.paymentUrl,
601
+ requiredAction: stripeResult.requiredAction
602
+ });
603
+ }
604
+ /** Starts checkout and lets the webhook finalize the subscription later. */
605
+ async function createCheckoutSubscribe(ctx, subCtx) {
606
+ return buildSubscribeResult({ paymentUrl: (await ctx.stripe.createSubscriptionCheckout({
607
+ cancelUrl: subCtx.cancelUrl,
608
+ metadata: {
609
+ paykit_customer_id: subCtx.customerId,
610
+ paykit_intent: "subscribe",
611
+ paykit_plan_id: subCtx.storedPlan.id
612
+ },
613
+ providerCustomerId: subCtx.providerCustomerId,
614
+ providerPriceId: subCtx.storedPlan.providerPriceId,
615
+ successUrl: subCtx.successUrl
616
+ })).paymentUrl });
617
+ }
618
+ async function insertLocalTargetSubscription(database, subCtx, input) {
619
+ await insertSubscriptionRecord(database, {
620
+ customerId: subCtx.customerId,
621
+ planFeatures: subCtx.normalizedPlan.includes,
622
+ productInternalId: subCtx.storedPlan.internalId,
623
+ providerId: subCtx.providerId,
624
+ startedAt: input.startedAt,
625
+ status: input.status
626
+ });
627
+ }
628
+ async function upsertProviderBackedTargetSubscription(database, subCtx, input, options) {
629
+ const providerData = {
630
+ subscriptionId: input.subscription.providerSubscriptionId,
631
+ subscriptionScheduleId: input.subscription.providerSubscriptionScheduleId ?? null
632
+ };
633
+ let subscriptionId = null;
634
+ if (options?.deferred) {
635
+ const existingSub = await getSubscriptionByProviderSubscriptionId(database, {
636
+ providerId: subCtx.providerId,
637
+ providerSubscriptionId: input.subscription.providerSubscriptionId
638
+ });
639
+ if (existingSub) {
640
+ subscriptionId = existingSub.id;
641
+ await syncSubscriptionBillingState(database, {
642
+ currentPeriodEndAt: input.subscription.currentPeriodEndAt ?? null,
643
+ currentPeriodStartAt: input.subscription.currentPeriodStartAt ?? null,
644
+ providerData,
645
+ status: input.subscription.status,
646
+ subscriptionId: existingSub.id
647
+ });
648
+ }
649
+ }
650
+ if (!subscriptionId) subscriptionId = (await insertSubscriptionRecord(database, {
651
+ currentPeriodEndAt: input.subscription.currentPeriodEndAt ?? null,
652
+ currentPeriodStartAt: input.subscription.currentPeriodStartAt ?? null,
653
+ customerId: subCtx.customerId,
654
+ planFeatures: subCtx.normalizedPlan.includes,
655
+ productInternalId: subCtx.storedPlan.internalId,
656
+ providerId: subCtx.providerId,
657
+ providerData,
658
+ startedAt: input.subscription.currentPeriodStartAt ?? /* @__PURE__ */ new Date(),
659
+ status: input.subscription.status
660
+ })).id;
661
+ if (input.invoice) await upsertInvoiceRecord(database, {
662
+ customerId: subCtx.customerId,
663
+ invoice: input.invoice,
664
+ providerId: subCtx.providerId,
665
+ subscriptionId
666
+ });
667
+ }
668
+ async function clearScheduledSubscriptionsInGroupIfNeeded(database, subCtx) {
669
+ if (!subCtx.storedPlan.group) return;
670
+ await clearScheduledSubscriptionsInGroup(database, {
671
+ customerId: subCtx.customerId,
672
+ group: subCtx.storedPlan.group
673
+ });
674
+ }
675
+ async function deleteScheduledSubscriptionsInGroupIfNeeded(database, subCtx) {
676
+ if (!subCtx.storedPlan.group) return;
677
+ await deleteScheduledSubscriptionsInGroup(database, {
678
+ customerId: subCtx.customerId,
679
+ group: subCtx.storedPlan.group
680
+ });
681
+ }
682
+ function addResetInterval(date, resetInterval) {
683
+ const next = new Date(date);
684
+ if (resetInterval === "day") next.setUTCDate(next.getUTCDate() + 1);
685
+ if (resetInterval === "week") next.setUTCDate(next.getUTCDate() + 7);
686
+ if (resetInterval === "month") {
687
+ const day = next.getUTCDate();
688
+ next.setUTCMonth(next.getUTCMonth() + 1);
689
+ if (next.getUTCDate() !== day) next.setUTCDate(0);
690
+ }
691
+ if (resetInterval === "year") {
692
+ const day = next.getUTCDate();
693
+ next.setUTCFullYear(next.getUTCFullYear() + 1);
694
+ if (next.getUTCDate() !== day) next.setUTCDate(0);
695
+ }
696
+ return next;
697
+ }
698
+ async function warnOnDuplicateActiveSubscriptionGroups(ctx, customerId) {
699
+ const activeSubscriptions = await ctx.database.select({
700
+ group: product.group,
701
+ planId: product.id,
702
+ subscriptionId: subscription.id
703
+ }).from(subscription).innerJoin(product, eq(subscription.productInternalId, product.internalId)).where(and(eq(subscription.customerId, customerId), inArray(subscription.status, [
704
+ "active",
705
+ "trialing",
706
+ "past_due"
707
+ ]), or(isNull(subscription.endedAt), sql`${subscription.endedAt} > now()`)));
708
+ const subscriptionsByGroup = /* @__PURE__ */ new Map();
709
+ for (const activeSubscription of activeSubscriptions) {
710
+ const currentGroup = subscriptionsByGroup.get(activeSubscription.group) ?? [];
711
+ currentGroup.push({
712
+ planId: activeSubscription.planId,
713
+ subscriptionId: activeSubscription.subscriptionId
714
+ });
715
+ subscriptionsByGroup.set(activeSubscription.group, currentGroup);
716
+ }
717
+ for (const [group, subscriptionsInGroup] of subscriptionsByGroup) {
718
+ if (subscriptionsInGroup.length < 2) continue;
719
+ ctx.logger.warn({
720
+ customerId,
721
+ group,
722
+ subscriptions: subscriptionsInGroup
723
+ }, "multiple active subscriptions detected in the same group");
724
+ }
725
+ }
726
+ function mapJoinRowToSubscriptionWithCatalog(row) {
727
+ const providerMap = row.product.provider;
728
+ return {
729
+ ...row.subscription,
730
+ planGroup: row.product.group,
731
+ planId: row.product.id,
732
+ planIsDefault: row.product.isDefault,
733
+ planName: row.product.name,
734
+ priceAmount: row.product.priceAmount,
735
+ priceInterval: row.product.priceInterval,
736
+ providerPriceId: Object.values(providerMap ?? {})[0]?.priceId ?? null
737
+ };
738
+ }
739
+ async function getActiveSubscriptionInGroup(database, input) {
740
+ const row = (await database.select().from(subscription).innerJoin(product, eq(subscription.productInternalId, product.internalId)).where(and(eq(subscription.customerId, input.customerId), eq(product.group, input.group), inArray(subscription.status, [
741
+ "active",
742
+ "trialing",
743
+ "past_due"
744
+ ]), or(isNull(subscription.endedAt), sql`${subscription.endedAt} > now()`))).orderBy(desc(subscription.createdAt)).limit(1))[0];
745
+ if (!row) return null;
746
+ return mapJoinRowToSubscriptionWithCatalog(row);
747
+ }
748
+ async function getActiveSubscriptionsInGroup(database, input) {
749
+ return (await database.select().from(subscription).innerJoin(product, eq(subscription.productInternalId, product.internalId)).where(and(eq(subscription.customerId, input.customerId), eq(product.group, input.group), inArray(subscription.status, [
750
+ "active",
751
+ "trialing",
752
+ "past_due"
753
+ ]), or(isNull(subscription.endedAt), sql`${subscription.endedAt} > now()`))).orderBy(desc(subscription.createdAt))).map(mapJoinRowToSubscriptionWithCatalog);
754
+ }
755
+ async function getScheduledSubscriptionsInGroup(database, input) {
756
+ return (await database.select().from(subscription).innerJoin(product, eq(subscription.productInternalId, product.internalId)).where(and(eq(subscription.customerId, input.customerId), eq(product.group, input.group), eq(subscription.status, "scheduled"), isNull(subscription.endedAt))).orderBy(desc(subscription.createdAt))).map(mapJoinRowToSubscriptionWithCatalog);
757
+ }
758
+ async function getSubscriptionByProviderSubscriptionId(database, input) {
759
+ return await database.query.subscription.findFirst({
760
+ orderBy: (s, { desc: d }) => [d(s.createdAt)],
761
+ where: and(eq(subscription.providerId, input.providerId), sql`${subscription.providerData}->>'subscriptionId' = ${input.providerSubscriptionId}`)
762
+ }) ?? null;
763
+ }
764
+ async function getSubscriptionById(database, subscriptionId) {
765
+ return await database.query.subscription.findFirst({ where: eq(subscription.id, subscriptionId) }) ?? null;
766
+ }
767
+ async function insertSubscriptionRecord(database, input) {
768
+ const now = /* @__PURE__ */ new Date();
769
+ const row = (await database.insert(subscription).values({
770
+ canceled: false,
771
+ cancelAtPeriodEnd: false,
772
+ canceledAt: null,
773
+ currentPeriodEndAt: input.currentPeriodEndAt ?? null,
774
+ currentPeriodStartAt: input.currentPeriodStartAt ?? null,
775
+ customerId: input.customerId,
776
+ endedAt: null,
777
+ id: generateId("sub"),
778
+ productInternalId: input.productInternalId,
779
+ providerData: input.providerData ?? null,
780
+ providerId: input.providerId ?? null,
781
+ quantity: 1,
782
+ scheduledProductId: input.scheduledProductId ?? null,
783
+ startedAt: input.startedAt ?? now,
784
+ status: input.status,
785
+ trialEndsAt: input.trialEndsAt ?? null
786
+ }).returning())[0];
787
+ if (!row) throw PayKitError.from("INTERNAL_SERVER_ERROR", PAYKIT_ERROR_CODES.SUBSCRIPTION_CREATE_FAILED);
788
+ if (input.planFeatures.length > 0) for (const planFeature of input.planFeatures) {
789
+ const isBoolean = planFeature.type === "boolean";
790
+ await database.insert(entitlement).values({
791
+ balance: isBoolean ? null : planFeature.limit ?? 0,
792
+ customerId: input.customerId,
793
+ featureId: planFeature.id,
794
+ id: generateId("ent"),
795
+ limit: isBoolean ? null : planFeature.limit ?? null,
796
+ nextResetAt: planFeature.resetInterval ? addResetInterval(now, planFeature.resetInterval) : null,
797
+ subscriptionId: row.id
798
+ });
799
+ }
800
+ return row;
801
+ }
802
+ async function endSubscriptions(database, subscriptionIds, input) {
803
+ if (subscriptionIds.length === 0) return;
804
+ await database.update(subscription).set({
805
+ canceled: input.canceled ?? false,
806
+ canceledAt: input.canceledAt ?? (input.canceled ? /* @__PURE__ */ new Date() : null),
807
+ endedAt: input.endedAt ?? /* @__PURE__ */ new Date(),
808
+ status: input.status,
809
+ updatedAt: /* @__PURE__ */ new Date()
810
+ }).where(inArray(subscription.id, [...subscriptionIds]));
811
+ }
812
+ async function clearScheduledSubscriptionsInGroup(database, input) {
813
+ const scheduled = await getScheduledSubscriptionsInGroup(database, input);
814
+ if (scheduled.length === 0) return;
815
+ await endSubscriptions(database, scheduled.map((item) => item.id), {
816
+ endedAt: /* @__PURE__ */ new Date(),
817
+ status: "canceled"
818
+ });
819
+ }
820
+ async function deleteSubscriptions(database, subscriptionIds) {
821
+ if (subscriptionIds.length === 0) return;
822
+ await database.delete(entitlement).where(inArray(entitlement.subscriptionId, [...subscriptionIds]));
823
+ await database.delete(subscription).where(inArray(subscription.id, [...subscriptionIds]));
824
+ }
825
+ async function deleteScheduledSubscriptionsInGroup(database, input) {
826
+ const scheduled = await getScheduledSubscriptionsInGroup(database, input);
827
+ if (scheduled.length === 0) return;
828
+ await deleteSubscriptions(database, scheduled.map((item) => item.id));
829
+ }
830
+ async function scheduleSubscriptionCancellation(database, input) {
831
+ const existing = await getSubscriptionById(database, input.subscriptionId);
832
+ if (!existing) return;
833
+ await database.update(subscription).set({
834
+ canceled: true,
835
+ canceledAt: input.canceledAt ?? /* @__PURE__ */ new Date(),
836
+ endedAt: input.currentPeriodEndAt ?? existing.endedAt,
837
+ updatedAt: /* @__PURE__ */ new Date()
838
+ }).where(eq(subscription.id, input.subscriptionId));
839
+ }
840
+ async function replaceSubscriptionSchedule(database, input) {
841
+ await database.update(subscription).set({
842
+ scheduledProductId: input.scheduledProductId ?? null,
843
+ updatedAt: /* @__PURE__ */ new Date()
844
+ }).where(eq(subscription.id, input.subscriptionId));
845
+ }
846
+ async function activateScheduledSubscription(database, input) {
847
+ await database.update(subscription).set({
848
+ canceled: false,
849
+ canceledAt: null,
850
+ currentPeriodEndAt: input.currentPeriodEndAt ?? null,
851
+ currentPeriodStartAt: input.currentPeriodStartAt ?? null,
852
+ endedAt: null,
853
+ providerData: input.providerData ?? null,
854
+ providerId: input.providerId,
855
+ startedAt: input.startedAt ?? /* @__PURE__ */ new Date(),
856
+ status: input.status,
857
+ updatedAt: /* @__PURE__ */ new Date()
858
+ }).where(eq(subscription.id, input.subscriptionId));
859
+ }
860
+ async function syncSubscriptionFromProvider(database, input) {
861
+ const endedAt = input.providerSubscription.cancelAtPeriodEnd === false ? null : input.providerSubscription.endedAt ?? null;
862
+ const canceledAt = input.providerSubscription.cancelAtPeriodEnd === false ? null : input.providerSubscription.canceledAt ?? null;
863
+ await database.update(subscription).set({
864
+ canceled: input.providerSubscription.cancelAtPeriodEnd,
865
+ cancelAtPeriodEnd: input.providerSubscription.cancelAtPeriodEnd,
866
+ canceledAt,
867
+ currentPeriodEndAt: input.providerSubscription.currentPeriodEndAt ?? null,
868
+ currentPeriodStartAt: input.providerSubscription.currentPeriodStartAt ?? null,
869
+ endedAt,
870
+ status: input.providerSubscription.status,
871
+ updatedAt: /* @__PURE__ */ new Date()
872
+ }).where(eq(subscription.id, input.subscriptionId));
873
+ }
874
+ async function syncSubscriptionBillingState(database, input) {
875
+ const existing = await database.query.subscription.findFirst({ where: eq(subscription.id, input.subscriptionId) });
876
+ if (!existing) return;
877
+ await database.update(subscription).set({
878
+ currentPeriodEndAt: input.currentPeriodEndAt !== void 0 ? input.currentPeriodEndAt : existing.currentPeriodEndAt,
879
+ currentPeriodStartAt: input.currentPeriodStartAt !== void 0 ? input.currentPeriodStartAt : existing.currentPeriodStartAt,
880
+ providerData: input.providerData !== void 0 ? input.providerData : existing.providerData,
881
+ startedAt: input.startedAt !== void 0 ? input.startedAt : existing.startedAt,
882
+ status: input.status ?? existing.status,
883
+ updatedAt: /* @__PURE__ */ new Date()
884
+ }).where(eq(subscription.id, input.subscriptionId));
885
+ }
886
+ async function getCurrentSubscriptions(database, customerId) {
887
+ return database.select({
888
+ currentPeriodEndAt: subscription.currentPeriodEndAt,
889
+ endedAt: subscription.endedAt,
890
+ id: product.id,
891
+ startedAt: subscription.startedAt,
892
+ status: subscription.status
893
+ }).from(subscription).innerJoin(product, eq(subscription.productInternalId, product.internalId)).where(and(eq(subscription.customerId, customerId), or(isNull(subscription.endedAt), sql`${subscription.endedAt} > now()`, eq(subscription.status, "scheduled")))).orderBy(desc(subscription.createdAt));
894
+ }
895
+ //#endregion
896
+ export { applySubscriptionWebhookAction, getActiveSubscriptionInGroup, getCurrentSubscriptions, getScheduledSubscriptionsInGroup, handleSubscribeCheckoutCompleted, insertSubscriptionRecord, prepareSubscribeCheckoutCompleted, subscribeToPlan };