paykitjs 0.1.0-alpha.2

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,150 @@
1
+ import { __exportAll } from "../_virtual/_rolldown/runtime.js";
2
+ import { boolean, index, integer, jsonb, pgTableCreator, primaryKey, text, timestamp, uniqueIndex } from "drizzle-orm/pg-core";
3
+ //#region src/database/schema.ts
4
+ var schema_exports = /* @__PURE__ */ __exportAll({
5
+ customer: () => customer,
6
+ entitlement: () => entitlement,
7
+ feature: () => feature,
8
+ invoice: () => invoice,
9
+ metadata: () => metadata,
10
+ paymentMethod: () => paymentMethod,
11
+ product: () => product,
12
+ productFeature: () => productFeature,
13
+ subscription: () => subscription,
14
+ webhookEvent: () => webhookEvent
15
+ });
16
+ const pgTable = pgTableCreator((name) => `paykit_${name}`);
17
+ const createdAt = timestamp("created_at").notNull().$defaultFn(() => /* @__PURE__ */ new Date());
18
+ const updatedAt = timestamp("updated_at").notNull().$defaultFn(() => /* @__PURE__ */ new Date()).$onUpdateFn(() => /* @__PURE__ */ new Date());
19
+ const customer = pgTable("customer", {
20
+ id: text("id").primaryKey(),
21
+ email: text("email"),
22
+ name: text("name"),
23
+ metadata: jsonb("metadata").$type(),
24
+ provider: jsonb("provider").$type().notNull().default({}),
25
+ deletedAt: timestamp("deleted_at"),
26
+ createdAt,
27
+ updatedAt
28
+ }, (table) => [index("paykit_customer_deleted_at_idx").on(table.deletedAt)]);
29
+ const paymentMethod = pgTable("payment_method", {
30
+ id: text("id").primaryKey(),
31
+ customerId: text("customer_id").notNull().references(() => customer.id),
32
+ providerId: text("provider_id").notNull(),
33
+ providerData: jsonb("provider_data").$type().notNull(),
34
+ isDefault: boolean("is_default").notNull().default(false),
35
+ deletedAt: timestamp("deleted_at"),
36
+ createdAt,
37
+ updatedAt
38
+ }, (table) => [index("paykit_payment_method_customer_idx").on(table.customerId, table.deletedAt), index("paykit_payment_method_provider_idx").on(table.providerId)]);
39
+ const feature = pgTable("feature", {
40
+ id: text("id").primaryKey(),
41
+ type: text("type").notNull(),
42
+ createdAt,
43
+ updatedAt
44
+ });
45
+ const product = pgTable("product", {
46
+ internalId: text("internal_id").primaryKey(),
47
+ id: text("id").notNull(),
48
+ version: integer("version").notNull().default(1),
49
+ name: text("name").notNull(),
50
+ group: text("group").notNull().default(""),
51
+ isDefault: boolean("is_default").notNull().default(false),
52
+ priceAmount: integer("price_amount"),
53
+ priceInterval: text("price_interval"),
54
+ hash: text("hash"),
55
+ provider: jsonb("provider").$type().notNull().default({}),
56
+ createdAt,
57
+ updatedAt
58
+ }, (table) => [uniqueIndex("paykit_product_id_version_unique").on(table.id, table.version), index("paykit_product_default_idx").on(table.isDefault)]);
59
+ const productFeature = pgTable("product_feature", {
60
+ productInternalId: text("product_internal_id").notNull().references(() => product.internalId),
61
+ featureId: text("feature_id").notNull().references(() => feature.id),
62
+ limit: integer("limit"),
63
+ resetInterval: text("reset_interval"),
64
+ config: jsonb("config").$type(),
65
+ createdAt,
66
+ updatedAt
67
+ }, (table) => [primaryKey({ columns: [table.productInternalId, table.featureId] }), index("paykit_product_feature_feature_idx").on(table.featureId)]);
68
+ const subscription = pgTable("subscription", {
69
+ id: text("id").primaryKey(),
70
+ customerId: text("customer_id").notNull().references(() => customer.id),
71
+ productInternalId: text("product_internal_id").notNull().references(() => product.internalId),
72
+ providerId: text("provider_id"),
73
+ providerData: jsonb("provider_data").$type(),
74
+ status: text("status").notNull(),
75
+ canceled: boolean("canceled").notNull().default(false),
76
+ cancelAtPeriodEnd: boolean("cancel_at_period_end").notNull().default(false),
77
+ startedAt: timestamp("started_at"),
78
+ trialEndsAt: timestamp("trial_ends_at"),
79
+ currentPeriodStartAt: timestamp("current_period_start_at"),
80
+ currentPeriodEndAt: timestamp("current_period_end_at"),
81
+ canceledAt: timestamp("canceled_at"),
82
+ endedAt: timestamp("ended_at"),
83
+ scheduledProductId: text("scheduled_product_id"),
84
+ quantity: integer("quantity").notNull().default(1),
85
+ createdAt,
86
+ updatedAt
87
+ }, (table) => [
88
+ index("paykit_subscription_customer_status_idx").on(table.customerId, table.status, table.endedAt),
89
+ index("paykit_subscription_product_idx").on(table.productInternalId),
90
+ index("paykit_subscription_provider_idx").on(table.providerId)
91
+ ]);
92
+ const entitlement = pgTable("entitlement", {
93
+ id: text("id").primaryKey(),
94
+ subscriptionId: text("subscription_id").references(() => subscription.id),
95
+ customerId: text("customer_id").notNull().references(() => customer.id),
96
+ featureId: text("feature_id").notNull().references(() => feature.id),
97
+ limit: integer("limit"),
98
+ balance: integer("balance"),
99
+ nextResetAt: timestamp("next_reset_at"),
100
+ createdAt,
101
+ updatedAt
102
+ }, (table) => [
103
+ index("paykit_entitlement_subscription_idx").on(table.subscriptionId),
104
+ index("paykit_entitlement_customer_feature_idx").on(table.customerId, table.featureId),
105
+ index("paykit_entitlement_next_reset_idx").on(table.nextResetAt)
106
+ ]);
107
+ const invoice = pgTable("invoice", {
108
+ id: text("id").primaryKey(),
109
+ customerId: text("customer_id").notNull().references(() => customer.id),
110
+ subscriptionId: text("subscription_id").references(() => subscription.id),
111
+ type: text("type").notNull(),
112
+ status: text("status").notNull(),
113
+ amount: integer("amount").notNull(),
114
+ currency: text("currency").notNull(),
115
+ description: text("description"),
116
+ hostedUrl: text("hosted_url"),
117
+ providerId: text("provider_id").notNull(),
118
+ providerData: jsonb("provider_data").$type().notNull(),
119
+ periodStartAt: timestamp("period_start_at"),
120
+ periodEndAt: timestamp("period_end_at"),
121
+ createdAt,
122
+ updatedAt
123
+ }, (table) => [
124
+ index("paykit_invoice_customer_idx").on(table.customerId, table.createdAt),
125
+ index("paykit_invoice_subscription_idx").on(table.subscriptionId),
126
+ index("paykit_invoice_provider_idx").on(table.providerId)
127
+ ]);
128
+ const metadata = pgTable("metadata", {
129
+ id: text("id").primaryKey(),
130
+ providerId: text("provider_id").notNull(),
131
+ type: text("type").notNull(),
132
+ data: jsonb("data").$type().notNull(),
133
+ providerCheckoutSessionId: text("provider_checkout_session_id"),
134
+ expiresAt: timestamp("expires_at"),
135
+ createdAt
136
+ }, (table) => [uniqueIndex("paykit_metadata_checkout_session_unique").on(table.providerId, table.providerCheckoutSessionId)]);
137
+ const webhookEvent = pgTable("webhook_event", {
138
+ id: text("id").primaryKey(),
139
+ providerId: text("provider_id").notNull(),
140
+ providerEventId: text("provider_event_id").notNull(),
141
+ type: text("type").notNull(),
142
+ payload: jsonb("payload").$type().notNull(),
143
+ status: text("status").notNull(),
144
+ error: text("error"),
145
+ traceId: text("trace_id"),
146
+ receivedAt: timestamp("received_at").notNull(),
147
+ processedAt: timestamp("processed_at")
148
+ }, (table) => [uniqueIndex("paykit_webhook_event_provider_unique").on(table.providerId, table.providerEventId), index("paykit_webhook_event_status_idx").on(table.providerId, table.status)]);
149
+ //#endregion
150
+ export { customer, entitlement, feature, invoice, paymentMethod, product, productFeature, schema_exports, subscription, webhookEvent };
@@ -0,0 +1,33 @@
1
+ import { definePayKitMethod } from "../api/define-route.js";
2
+ import { getCustomerCurrentTime } from "../testing/testing.service.js";
3
+ import { checkEntitlement, reportEntitlement } from "./entitlement.service.js";
4
+ import * as z from "zod";
5
+ //#region src/entitlement/entitlement.api.ts
6
+ const entitlementCheckSchema = z.object({
7
+ featureId: z.string(),
8
+ required: z.number().positive().optional()
9
+ });
10
+ const entitlementReportSchema = z.object({
11
+ featureId: z.string(),
12
+ amount: z.number().positive().optional()
13
+ });
14
+ const check = definePayKitMethod({
15
+ input: entitlementCheckSchema,
16
+ requireCustomer: true
17
+ }, async (ctx) => checkEntitlement(ctx.paykit.database, {
18
+ customerId: ctx.customer.id,
19
+ featureId: ctx.input.featureId,
20
+ now: getCustomerCurrentTime(ctx.paykit, ctx.customer),
21
+ required: ctx.input.required
22
+ }));
23
+ const report = definePayKitMethod({
24
+ input: entitlementReportSchema,
25
+ requireCustomer: true
26
+ }, async (ctx) => reportEntitlement(ctx.paykit.database, {
27
+ amount: ctx.input.amount,
28
+ customerId: ctx.customer.id,
29
+ featureId: ctx.input.featureId,
30
+ now: getCustomerCurrentTime(ctx.paykit, ctx.customer)
31
+ }));
32
+ //#endregion
33
+ export { check, report };
@@ -0,0 +1,17 @@
1
+ //#region src/entitlement/entitlement.service.d.ts
2
+ interface EntitlementBalance {
3
+ limit: number;
4
+ remaining: number;
5
+ resetAt: Date | null;
6
+ unlimited: boolean;
7
+ }
8
+ interface CheckResult {
9
+ allowed: boolean;
10
+ balance: EntitlementBalance | null;
11
+ }
12
+ interface ReportResult {
13
+ balance: EntitlementBalance | null;
14
+ success: boolean;
15
+ }
16
+ //#endregion
17
+ export { CheckResult, EntitlementBalance, ReportResult };
@@ -0,0 +1,123 @@
1
+ import { entitlement, productFeature, subscription } from "../database/schema.js";
2
+ import { and, eq, inArray, isNull, lte, or, sql } from "drizzle-orm";
3
+ //#region src/entitlement/entitlement.service.ts
4
+ function addResetInterval(date, resetInterval) {
5
+ const next = new Date(date);
6
+ if (resetInterval === "day") next.setUTCDate(next.getUTCDate() + 1);
7
+ if (resetInterval === "week") next.setUTCDate(next.getUTCDate() + 7);
8
+ if (resetInterval === "month") {
9
+ const day = next.getUTCDate();
10
+ next.setUTCMonth(next.getUTCMonth() + 1);
11
+ if (next.getUTCDate() !== day) next.setUTCDate(0);
12
+ }
13
+ if (resetInterval === "year") {
14
+ const day = next.getUTCDate();
15
+ next.setUTCFullYear(next.getUTCFullYear() + 1);
16
+ if (next.getUTCDate() !== day) next.setUTCDate(0);
17
+ }
18
+ return next;
19
+ }
20
+ function getNextResetAt(currentResetAt, now, resetInterval) {
21
+ let nextResetAt = new Date(currentResetAt);
22
+ while (nextResetAt <= now) nextResetAt = addResetInterval(nextResetAt, resetInterval);
23
+ return nextResetAt;
24
+ }
25
+ function aggregateBalance(rows) {
26
+ if (rows.length === 0) return null;
27
+ if (rows.some((row) => row.originalLimit === null)) return {
28
+ limit: 0,
29
+ remaining: 0,
30
+ resetAt: null,
31
+ unlimited: true
32
+ };
33
+ let remaining = 0;
34
+ let limit = 0;
35
+ let resetAt = null;
36
+ for (const row of rows) {
37
+ remaining += row.balance;
38
+ limit += row.originalLimit;
39
+ if (row.nextResetAt) {
40
+ if (!resetAt || row.nextResetAt < resetAt) resetAt = row.nextResetAt;
41
+ }
42
+ }
43
+ return {
44
+ limit,
45
+ remaining,
46
+ resetAt,
47
+ unlimited: false
48
+ };
49
+ }
50
+ /** Fetch all active entitlements for a customer+feature, with product feature metadata. */
51
+ async function getActiveEntitlements(db, customerId, featureId) {
52
+ return await db.select({
53
+ id: entitlement.id,
54
+ balance: entitlement.balance,
55
+ nextResetAt: entitlement.nextResetAt,
56
+ originalLimit: productFeature.limit,
57
+ resetInterval: productFeature.resetInterval
58
+ }).from(entitlement).innerJoin(subscription, eq(entitlement.subscriptionId, subscription.id)).innerJoin(productFeature, and(eq(productFeature.productInternalId, subscription.productInternalId), eq(productFeature.featureId, entitlement.featureId))).where(and(eq(entitlement.customerId, customerId), eq(entitlement.featureId, featureId), inArray(subscription.status, ["active", "trialing"]), or(isNull(subscription.endedAt), sql`${subscription.endedAt} > now()`)));
59
+ }
60
+ /** Lazy-reset any stale entitlements and return the refreshed rows. */
61
+ async function resetStaleEntitlements(db, rows, now) {
62
+ for (const row of rows) if (row.nextResetAt && row.nextResetAt <= now && row.resetInterval && row.originalLimit != null) {
63
+ const nextReset = getNextResetAt(row.nextResetAt, now, row.resetInterval);
64
+ await db.update(entitlement).set({
65
+ balance: row.originalLimit,
66
+ nextResetAt: nextReset,
67
+ updatedAt: now
68
+ }).where(and(eq(entitlement.id, row.id), lte(entitlement.nextResetAt, now)));
69
+ row.balance = row.originalLimit;
70
+ row.nextResetAt = nextReset;
71
+ }
72
+ return rows;
73
+ }
74
+ async function checkEntitlement(database, input) {
75
+ const required = input.required ?? 1;
76
+ const rows = await getActiveEntitlements(database, input.customerId, input.featureId);
77
+ await resetStaleEntitlements(database, rows, input.now ?? /* @__PURE__ */ new Date());
78
+ const balance = aggregateBalance(rows);
79
+ if (!balance) return {
80
+ allowed: false,
81
+ balance: null
82
+ };
83
+ if (balance.unlimited) return {
84
+ allowed: true,
85
+ balance
86
+ };
87
+ return {
88
+ allowed: balance.remaining >= required,
89
+ balance
90
+ };
91
+ }
92
+ async function reportEntitlement(database, input) {
93
+ const amount = input.amount ?? 1;
94
+ const rows = await getActiveEntitlements(database, input.customerId, input.featureId);
95
+ await resetStaleEntitlements(database, rows, input.now ?? /* @__PURE__ */ new Date());
96
+ if (rows.length === 0) return {
97
+ balance: null,
98
+ success: false
99
+ };
100
+ if (rows.some((row) => row.originalLimit === null)) return {
101
+ balance: aggregateBalance(rows),
102
+ success: true
103
+ };
104
+ let deducted = false;
105
+ for (const row of rows) {
106
+ if (row.originalLimit === null || row.balance < amount) continue;
107
+ const result = await database.update(entitlement).set({
108
+ balance: sql`${entitlement.balance} - ${amount}`,
109
+ updatedAt: /* @__PURE__ */ new Date()
110
+ }).where(and(eq(entitlement.id, row.id), sql`${entitlement.balance} >= ${amount}`)).returning({ balance: entitlement.balance });
111
+ if (result.length > 0) {
112
+ row.balance = result[0].balance;
113
+ deducted = true;
114
+ break;
115
+ }
116
+ }
117
+ return {
118
+ balance: aggregateBalance(rows),
119
+ success: deducted
120
+ };
121
+ }
122
+ //#endregion
123
+ export { checkEntitlement, reportEntitlement };
@@ -0,0 +1,9 @@
1
+ import { PayKitInstance } from "../types/instance.js";
2
+
3
+ //#region src/handlers/next.d.ts
4
+ declare function paykitHandler(paykit: Pick<PayKitInstance, "handler">): {
5
+ GET: (request: Request) => Promise<Response>;
6
+ POST: (request: Request) => Promise<Response>;
7
+ };
8
+ //#endregion
9
+ export { paykitHandler };
@@ -0,0 +1,9 @@
1
+ //#region src/handlers/next.ts
2
+ function paykitHandler(paykit) {
3
+ return {
4
+ GET: paykit.handler,
5
+ POST: paykit.handler
6
+ };
7
+ }
8
+ //#endregion
9
+ export { paykitHandler };
@@ -0,0 +1,14 @@
1
+ import { NormalizedInvoice, NormalizedPayment, NormalizedPaymentMethod, NormalizedSubscription, NormalizedWebhookEvent, NormalizedWebhookEventMap, NormalizedWebhookEventName, PayKitEventHandlers, PayKitEventMap, PayKitEventName, WebhookApplyAction } from "./types/events.js";
2
+ import { PayKitProvider, ProviderCustomer, ProviderCustomerMap, ProviderTestClock, StripeProviderConfig, StripeProviderOptions } from "./providers/provider.js";
3
+ import { PayKitPlugin } from "./types/plugin.js";
4
+ import { FeatureType, MeteredFeatureConfig, MeteredResetInterval, NormalizedPlan, NormalizedPlanFeature, NormalizedSchema, PayKitFeature, PayKitFeatureDefinition, PayKitFeatureInclude, PayKitPlan, PayKitPlanConfig, PayKitPlansModule, PlanPrice, PriceInterval, feature, plan } from "./types/schema.js";
5
+ import { PayKitLoggingOptions, PayKitOptions, PayKitTestingOptions } from "./types/options.js";
6
+ import { Customer, StoredFeature, StoredInvoice, StoredProduct, StoredProductFeature, StoredSubscription } from "./types/models.js";
7
+ import { createPayKitEndpoint, definePayKitMethod, returnUrl } from "./api/define-route.js";
8
+ import { CustomerEntitlement, CustomerSubscription, CustomerWithDetails, ListCustomersResult } from "./customer/customer.types.js";
9
+ import { PayKitAdvanceTestClockInput, PayKitCheckInput, PayKitClientAdvanceTestClockInput, PayKitClientCustomerPortalInput, PayKitClientGetTestClockInput, PayKitClientSubscribeInput, PayKitCustomerPortalInput, PayKitGetTestClockInput, PayKitInstance, PayKitReportInput, PayKitSubscribeInput, PayKitSubscribeResult } from "./types/instance.js";
10
+ import { createPayKit } from "./core/create-paykit.js";
11
+ import { CheckResult, EntitlementBalance, ReportResult } from "./entitlement/entitlement.service.js";
12
+ import { RawError, defineErrorCodes } from "./core/error-codes.js";
13
+ import { PAYKIT_ERROR_CODES, PayKitError, PayKitErrorCode } from "./core/errors.js";
14
+ export { type CheckResult, type Customer, type CustomerEntitlement, type CustomerSubscription, type CustomerWithDetails, type EntitlementBalance, type FeatureType, type ListCustomersResult, type MeteredFeatureConfig, type MeteredResetInterval, type NormalizedInvoice, type NormalizedPayment, type NormalizedPaymentMethod, type NormalizedPlan, type NormalizedPlanFeature, type NormalizedSchema, type NormalizedSubscription, type NormalizedWebhookEvent, type NormalizedWebhookEventMap, type NormalizedWebhookEventName, PAYKIT_ERROR_CODES, type PayKitAdvanceTestClockInput, type PayKitCheckInput, type PayKitClientAdvanceTestClockInput, type PayKitClientCustomerPortalInput, type PayKitClientGetTestClockInput, type PayKitClientSubscribeInput, type PayKitCustomerPortalInput, PayKitError, type PayKitErrorCode, type PayKitEventHandlers, type PayKitEventMap, type PayKitEventName, type PayKitFeature, type PayKitFeatureDefinition, type PayKitFeatureInclude, type PayKitGetTestClockInput, type PayKitInstance, type PayKitLoggingOptions, type PayKitOptions, type PayKitPlan, type PayKitPlanConfig, type PayKitPlansModule, type PayKitPlugin, type PayKitProvider, type PayKitReportInput, type PayKitSubscribeInput, type PayKitSubscribeResult, type PayKitTestingOptions, type PlanPrice, type PriceInterval, type ProviderCustomer, type ProviderCustomerMap, type ProviderTestClock, type RawError, type ReportResult, type StoredFeature, type StoredInvoice, type StoredProduct, type StoredProductFeature, type StoredSubscription, type StripeProviderConfig, type StripeProviderOptions, type WebhookApplyAction, createPayKit, createPayKitEndpoint, defineErrorCodes, definePayKitMethod, feature, plan, returnUrl };
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ import { defineErrorCodes } from "./core/error-codes.js";
2
+ import { PAYKIT_ERROR_CODES, PayKitError } from "./core/errors.js";
3
+ import { createPayKitEndpoint, definePayKitMethod, returnUrl } from "./api/define-route.js";
4
+ import { feature, plan } from "./types/schema.js";
5
+ import { createPayKit } from "./core/create-paykit.js";
6
+ export { PAYKIT_ERROR_CODES, PayKitError, createPayKit, createPayKitEndpoint, defineErrorCodes, definePayKitMethod, feature, plan, returnUrl };
@@ -0,0 +1,54 @@
1
+ import { PAYKIT_ERROR_CODES, PayKitError } from "../core/errors.js";
2
+ import { invoice, subscription } from "../database/schema.js";
3
+ import { generateId } from "../core/utils.js";
4
+ import { findCustomerByProviderCustomerId } from "../customer/customer.service.js";
5
+ import { and, eq, sql } from "drizzle-orm";
6
+ //#region src/invoice/invoice.service.ts
7
+ async function upsertInvoiceRecord(database, input) {
8
+ const now = /* @__PURE__ */ new Date();
9
+ const providerData = { invoiceId: input.invoice.providerInvoiceId };
10
+ const existing = await database.query.invoice.findFirst({ where: and(eq(invoice.providerId, input.providerId), sql`${invoice.providerData}->>'invoiceId' = ${input.invoice.providerInvoiceId}`) });
11
+ const values = {
12
+ amount: input.invoice.totalAmount,
13
+ currency: input.invoice.currency,
14
+ customerId: input.customerId,
15
+ description: null,
16
+ hostedUrl: input.invoice.hostedUrl ?? null,
17
+ periodEndAt: input.invoice.periodEndAt ?? null,
18
+ periodStartAt: input.invoice.periodStartAt ?? null,
19
+ providerData,
20
+ providerId: input.providerId,
21
+ status: input.invoice.status ?? "open",
22
+ subscriptionId: input.subscriptionId ?? null,
23
+ type: "subscription",
24
+ updatedAt: now
25
+ };
26
+ if (existing) {
27
+ const row = (await database.update(invoice).set(values).where(eq(invoice.id, existing.id)).returning())[0];
28
+ if (!row) throw PayKitError.from("INTERNAL_SERVER_ERROR", PAYKIT_ERROR_CODES.INVOICE_UPSERT_FAILED);
29
+ return row;
30
+ }
31
+ const row = (await database.insert(invoice).values({
32
+ ...values,
33
+ id: generateId("inv")
34
+ }).returning())[0];
35
+ if (!row) throw PayKitError.from("INTERNAL_SERVER_ERROR", PAYKIT_ERROR_CODES.INVOICE_UPSERT_FAILED);
36
+ return row;
37
+ }
38
+ async function applyInvoiceWebhookAction(ctx, action) {
39
+ const customerRow = await findCustomerByProviderCustomerId(ctx.database, {
40
+ providerCustomerId: action.data.providerCustomerId,
41
+ providerId: ctx.provider.id
42
+ });
43
+ if (!customerRow) return null;
44
+ const subscriptionRecord = action.data.providerSubscriptionId ? await ctx.database.query.subscription.findFirst({ where: and(eq(subscription.providerId, ctx.provider.id), sql`${subscription.providerData}->>'subscriptionId' = ${action.data.providerSubscriptionId}`) }) : null;
45
+ await upsertInvoiceRecord(ctx.database, {
46
+ customerId: customerRow.id,
47
+ invoice: action.data.invoice,
48
+ providerId: ctx.provider.id,
49
+ subscriptionId: subscriptionRecord?.id ?? null
50
+ });
51
+ return customerRow.id;
52
+ }
53
+ //#endregion
54
+ export { applyInvoiceWebhookAction, upsertInvoiceRecord };
@@ -0,0 +1,49 @@
1
+ import { invoice } from "../database/schema.js";
2
+ import { generateId } from "../core/utils.js";
3
+ import { findCustomerByProviderCustomerId } from "../customer/customer.service.js";
4
+ import { and, eq, sql } from "drizzle-orm";
5
+ //#region src/payment/payment.service.ts
6
+ async function syncPaymentByProviderCustomer(database, input) {
7
+ const customerRow = await findCustomerByProviderCustomerId(database, {
8
+ providerCustomerId: input.providerCustomerId,
9
+ providerId: input.providerId
10
+ });
11
+ if (!customerRow) return;
12
+ const providerData = {
13
+ paymentId: input.payment.providerPaymentId,
14
+ methodId: input.payment.providerMethodId ?? null
15
+ };
16
+ const existing = await database.query.invoice.findFirst({ where: and(eq(invoice.providerId, input.providerId), sql`${invoice.providerData}->>'paymentId' = ${input.payment.providerPaymentId}`) });
17
+ if (existing) {
18
+ await database.update(invoice).set({
19
+ status: input.payment.status,
20
+ amount: input.payment.amount,
21
+ updatedAt: /* @__PURE__ */ new Date()
22
+ }).where(eq(invoice.id, existing.id));
23
+ return;
24
+ }
25
+ await database.insert(invoice).values({
26
+ id: generateId("inv"),
27
+ customerId: customerRow.id,
28
+ type: "charge",
29
+ status: input.payment.status,
30
+ amount: input.payment.amount,
31
+ currency: input.payment.currency,
32
+ description: input.payment.description ?? null,
33
+ providerId: input.providerId,
34
+ providerData
35
+ });
36
+ }
37
+ async function applyPaymentWebhookAction(ctx, action) {
38
+ await syncPaymentByProviderCustomer(ctx.database, {
39
+ payment: action.data.payment,
40
+ providerCustomerId: action.data.providerCustomerId,
41
+ providerId: ctx.provider.id
42
+ });
43
+ return (await findCustomerByProviderCustomerId(ctx.database, {
44
+ providerCustomerId: action.data.providerCustomerId,
45
+ providerId: ctx.provider.id
46
+ }))?.id ?? null;
47
+ }
48
+ //#endregion
49
+ export { applyPaymentWebhookAction };
@@ -0,0 +1,78 @@
1
+ import { paymentMethod } from "../database/schema.js";
2
+ import { generateId } from "../core/utils.js";
3
+ import { findCustomerByProviderCustomerId } from "../customer/customer.service.js";
4
+ import { and, eq, isNull, sql } from "drizzle-orm";
5
+ //#region src/payment-method/payment-method.service.ts
6
+ async function getDefaultPaymentMethod(database, input) {
7
+ return await database.query.paymentMethod.findFirst({
8
+ orderBy(fields, operators) {
9
+ return [operators.desc(fields.isDefault), operators.desc(fields.createdAt)];
10
+ },
11
+ where: and(eq(paymentMethod.customerId, input.customerId), eq(paymentMethod.isDefault, true), eq(paymentMethod.providerId, input.providerId), isNull(paymentMethod.deletedAt))
12
+ }) ?? null;
13
+ }
14
+ async function syncPaymentMethodByProviderCustomer(database, input) {
15
+ const customerRow = await findCustomerByProviderCustomerId(database, {
16
+ providerCustomerId: input.providerCustomerId,
17
+ providerId: input.providerId
18
+ });
19
+ if (!customerRow) return;
20
+ const now = /* @__PURE__ */ new Date();
21
+ const providerData = {
22
+ methodId: input.paymentMethod.providerMethodId,
23
+ type: input.paymentMethod.type,
24
+ last4: input.paymentMethod.last4 ?? null,
25
+ expiryMonth: input.paymentMethod.expiryMonth ?? null,
26
+ expiryYear: input.paymentMethod.expiryYear ?? null
27
+ };
28
+ const existingRow = await database.query.paymentMethod.findFirst({ where: and(eq(paymentMethod.providerId, input.providerId), sql`${paymentMethod.providerData}->>'methodId' = ${input.paymentMethod.providerMethodId}`, isNull(paymentMethod.deletedAt)) });
29
+ if (input.paymentMethod.isDefault) await database.update(paymentMethod).set({
30
+ isDefault: false,
31
+ updatedAt: now
32
+ }).where(and(eq(paymentMethod.customerId, customerRow.id), eq(paymentMethod.providerId, input.providerId)));
33
+ if (existingRow) {
34
+ await database.update(paymentMethod).set({
35
+ customerId: customerRow.id,
36
+ deletedAt: null,
37
+ isDefault: input.paymentMethod.isDefault ?? existingRow.isDefault,
38
+ providerData,
39
+ updatedAt: now
40
+ }).where(eq(paymentMethod.id, existingRow.id));
41
+ return;
42
+ }
43
+ await database.insert(paymentMethod).values({
44
+ customerId: customerRow.id,
45
+ deletedAt: null,
46
+ id: generateId("pm"),
47
+ isDefault: input.paymentMethod.isDefault ?? false,
48
+ providerId: input.providerId,
49
+ providerData
50
+ });
51
+ }
52
+ async function deletePaymentMethodByProviderId(database, input) {
53
+ await database.update(paymentMethod).set({
54
+ deletedAt: /* @__PURE__ */ new Date(),
55
+ isDefault: false,
56
+ updatedAt: /* @__PURE__ */ new Date()
57
+ }).where(and(eq(paymentMethod.providerId, input.providerId), sql`${paymentMethod.providerData}->>'methodId' = ${input.providerMethodId}`));
58
+ }
59
+ async function applyPaymentMethodWebhookAction(ctx, action) {
60
+ if (action.type === "payment_method.upsert") {
61
+ await syncPaymentMethodByProviderCustomer(ctx.database, {
62
+ paymentMethod: action.data.paymentMethod,
63
+ providerCustomerId: action.data.providerCustomerId,
64
+ providerId: ctx.provider.id
65
+ });
66
+ return (await findCustomerByProviderCustomerId(ctx.database, {
67
+ providerCustomerId: action.data.providerCustomerId,
68
+ providerId: ctx.provider.id
69
+ }))?.id ?? null;
70
+ }
71
+ await deletePaymentMethodByProviderId(ctx.database, {
72
+ providerId: ctx.provider.id,
73
+ providerMethodId: action.data.providerMethodId
74
+ });
75
+ return null;
76
+ }
77
+ //#endregion
78
+ export { applyPaymentMethodWebhookAction, getDefaultPaymentMethod };
@@ -0,0 +1,111 @@
1
+ import { PAYKIT_ERROR_CODES, PayKitError } from "../core/errors.js";
2
+ import { getLatestProductSnapshot, getProviderProduct, insertProductVersion, replaceProductFeatures, updateProductName, upsertFeature, upsertProviderProduct } from "./product.service.js";
3
+ //#region src/product/product-sync.service.ts
4
+ function serializeFeatureConfig(config) {
5
+ return JSON.stringify(config ?? null);
6
+ }
7
+ function featuresChanged(existing, next) {
8
+ if (existing.length !== next.length) return true;
9
+ return existing.some((storedFeature, index) => {
10
+ const nextFeature = next[index];
11
+ if (!nextFeature) return true;
12
+ return storedFeature.featureId !== nextFeature.id || storedFeature.limit !== nextFeature.limit || storedFeature.resetInterval !== nextFeature.resetInterval || serializeFeatureConfig(storedFeature.config) !== serializeFeatureConfig(nextFeature.config);
13
+ });
14
+ }
15
+ function planChanged(existing, next) {
16
+ if (!existing) return true;
17
+ return existing.product.group !== next.group || existing.product.isDefault !== next.isDefault || (existing.product.priceAmount ?? null) !== next.priceAmount || (existing.product.priceInterval ?? null) !== next.priceInterval || featuresChanged(existing.features, next.includes);
18
+ }
19
+ async function dryRunSyncProducts(ctx) {
20
+ const results = [];
21
+ for (const plan of ctx.plans.plans) {
22
+ const existing = await getLatestProductSnapshot(ctx.database, plan.id);
23
+ let action = "unchanged";
24
+ if (!existing) action = "created";
25
+ else if (planChanged(existing, plan)) action = "created";
26
+ else if (existing.product.name !== plan.name) action = "updated";
27
+ results.push({
28
+ action,
29
+ id: plan.id,
30
+ version: existing ? existing.product.version : 1
31
+ });
32
+ }
33
+ return results;
34
+ }
35
+ async function syncProducts(ctx) {
36
+ const results = [];
37
+ const providerId = ctx.provider.id;
38
+ for (const schemaFeature of ctx.plans.features) await upsertFeature(ctx.database, schemaFeature);
39
+ for (const plan of ctx.plans.plans) {
40
+ const existing = await getLatestProductSnapshot(ctx.database, plan.id);
41
+ const existingProviderProduct = existing ? await getProviderProduct(ctx.database, existing.product.internalId, providerId) : null;
42
+ let storedProduct = existing?.product ?? null;
43
+ let action = "unchanged";
44
+ if (!existing) {
45
+ storedProduct = await insertProductVersion(ctx.database, {
46
+ group: plan.group,
47
+ hash: plan.hash,
48
+ id: plan.id,
49
+ isDefault: plan.isDefault,
50
+ name: plan.name,
51
+ priceAmount: plan.priceAmount,
52
+ priceInterval: plan.priceInterval,
53
+ version: 1
54
+ });
55
+ await replaceProductFeatures(ctx.database, {
56
+ features: plan.includes,
57
+ productInternalId: storedProduct.internalId
58
+ });
59
+ action = "created";
60
+ } else if (planChanged(existing, plan)) {
61
+ storedProduct = await insertProductVersion(ctx.database, {
62
+ group: plan.group,
63
+ hash: plan.hash,
64
+ id: plan.id,
65
+ isDefault: plan.isDefault,
66
+ name: plan.name,
67
+ priceAmount: plan.priceAmount,
68
+ priceInterval: plan.priceInterval,
69
+ version: existing.product.version + 1
70
+ });
71
+ await replaceProductFeatures(ctx.database, {
72
+ features: plan.includes,
73
+ productInternalId: storedProduct.internalId
74
+ });
75
+ action = "created";
76
+ } else if (existing.product.name !== plan.name) {
77
+ await updateProductName(ctx.database, existing.product.internalId, plan.name);
78
+ storedProduct = {
79
+ ...existing.product,
80
+ name: plan.name
81
+ };
82
+ action = "updated";
83
+ }
84
+ if (!storedProduct) throw PayKitError.from("INTERNAL_SERVER_ERROR", PAYKIT_ERROR_CODES.PLAN_SYNC_FAILED, `Failed to sync plan "${plan.id}"`);
85
+ if (storedProduct.priceAmount !== null && storedProduct.priceInterval !== null) {
86
+ const shouldReuseExistingPriceId = action !== "created" && existingProviderProduct?.priceId !== void 0;
87
+ const providerResult = await ctx.stripe.syncProduct({
88
+ existingProviderPriceId: shouldReuseExistingPriceId ? existingProviderProduct?.priceId ?? null : null,
89
+ existingProviderProductId: existingProviderProduct?.productId ?? null,
90
+ id: plan.id,
91
+ name: plan.name,
92
+ priceAmount: storedProduct.priceAmount,
93
+ priceInterval: storedProduct.priceInterval
94
+ });
95
+ await upsertProviderProduct(ctx.database, {
96
+ productInternalId: storedProduct.internalId,
97
+ providerId,
98
+ providerProductId: providerResult.providerProductId,
99
+ providerPriceId: providerResult.providerPriceId
100
+ });
101
+ }
102
+ results.push({
103
+ action,
104
+ id: plan.id,
105
+ version: storedProduct.version
106
+ });
107
+ }
108
+ return results;
109
+ }
110
+ //#endregion
111
+ export { dryRunSyncProducts, syncProducts };