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.
- package/LICENSE +21 -0
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/api/define-route.d.ts +94 -0
- package/dist/api/define-route.js +153 -0
- package/dist/api/methods.d.ts +422 -0
- package/dist/api/methods.js +67 -0
- package/dist/cli/commands/check.js +92 -0
- package/dist/cli/commands/init.js +264 -0
- package/dist/cli/commands/push.js +73 -0
- package/dist/cli/commands/telemetry.js +16 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +21 -0
- package/dist/cli/templates/index.js +64 -0
- package/dist/cli/utils/detect.js +67 -0
- package/dist/cli/utils/format.js +58 -0
- package/dist/cli/utils/get-config.js +117 -0
- package/dist/cli/utils/telemetry.js +103 -0
- package/dist/client/index.d.ts +25 -0
- package/dist/client/index.js +27 -0
- package/dist/core/context.d.ts +17 -0
- package/dist/core/context.js +23 -0
- package/dist/core/create-paykit.d.ts +7 -0
- package/dist/core/create-paykit.js +52 -0
- package/dist/core/error-codes.d.ts +12 -0
- package/dist/core/error-codes.js +10 -0
- package/dist/core/errors.d.ts +41 -0
- package/dist/core/errors.js +47 -0
- package/dist/core/logger.d.ts +11 -0
- package/dist/core/logger.js +51 -0
- package/dist/core/utils.js +21 -0
- package/dist/customer/customer.api.js +47 -0
- package/dist/customer/customer.service.js +342 -0
- package/dist/customer/customer.types.d.ts +31 -0
- package/dist/database/index.d.ts +8 -0
- package/dist/database/index.js +32 -0
- package/dist/database/migrations/0000_init.sql +157 -0
- package/dist/database/migrations/meta/0000_snapshot.json +1222 -0
- package/dist/database/migrations/meta/_journal.json +13 -0
- package/dist/database/schema.d.ts +1767 -0
- package/dist/database/schema.js +150 -0
- package/dist/entitlement/entitlement.api.js +33 -0
- package/dist/entitlement/entitlement.service.d.ts +17 -0
- package/dist/entitlement/entitlement.service.js +123 -0
- package/dist/handlers/next.d.ts +9 -0
- package/dist/handlers/next.js +9 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +6 -0
- package/dist/invoice/invoice.service.js +54 -0
- package/dist/payment/payment.service.js +49 -0
- package/dist/payment-method/payment-method.service.js +78 -0
- package/dist/product/product-sync.service.js +111 -0
- package/dist/product/product.service.js +127 -0
- package/dist/providers/provider.d.ts +159 -0
- package/dist/providers/stripe.js +547 -0
- package/dist/subscription/subscription.api.js +24 -0
- package/dist/subscription/subscription.service.js +896 -0
- package/dist/subscription/subscription.types.d.ts +18 -0
- package/dist/subscription/subscription.types.js +11 -0
- package/dist/testing/testing.api.js +29 -0
- package/dist/testing/testing.service.js +49 -0
- package/dist/types/events.d.ts +181 -0
- package/dist/types/instance.d.ts +88 -0
- package/dist/types/models.d.ts +11 -0
- package/dist/types/options.d.ts +32 -0
- package/dist/types/plugin.d.ts +11 -0
- package/dist/types/schema.d.ts +99 -0
- package/dist/types/schema.js +192 -0
- package/dist/webhook/webhook.api.js +29 -0
- package/dist/webhook/webhook.service.js +143 -0
- 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 };
|
package/dist/index.d.ts
ADDED
|
@@ -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 };
|