@alexasomba/better-auth-paystack 2.4.0 → 2.4.3
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/README.md +11 -0
- package/dist/client.d.mts +1 -1
- package/dist/client.mjs +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +518 -491
- package/dist/index.mjs.map +1 -1
- package/dist/{types-CNI2ur0p.d.mts → types-DHZSS1K6.d.mts} +5 -4
- package/dist/types-DHZSS1K6.d.mts.map +1 -0
- package/dist/version-GQ15aLQo.mjs +6 -0
- package/dist/version-GQ15aLQo.mjs.map +1 -0
- package/package.json +6 -3
- package/skills/billing-catalog-and-limits/SKILL.md +186 -0
- package/skills/organization-billing/SKILL.md +141 -0
- package/skills/setup/SKILL.md +146 -0
- package/skills/subscriptions-and-transactions/SKILL.md +147 -0
- package/skills/tanstack-start/SKILL.md +163 -0
- package/dist/types-CNI2ur0p.d.mts.map +0 -1
- package/dist/version-C_50YiuM.mjs +0 -6
- package/dist/version-C_50YiuM.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,210 @@
|
|
|
1
|
-
import { t as PACKAGE_VERSION } from "./version-
|
|
2
|
-
import { HIDE_METADATA, defineErrorCodes
|
|
1
|
+
import { t as PACKAGE_VERSION } from "./version-GQ15aLQo.mjs";
|
|
2
|
+
import { HIDE_METADATA, defineErrorCodes } from "better-auth";
|
|
3
3
|
import { defu } from "defu";
|
|
4
4
|
import { APIError, createAuthEndpoint, createAuthMiddleware, getSessionFromCtx, originCheck, sessionMiddleware } from "better-auth/api";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { PaystackResponse } from "@alexasomba/paystack-node";
|
|
6
|
+
import { PaystackError, PaystackResponse } from "@alexasomba/paystack-node";
|
|
7
7
|
import { mergeSchema } from "better-auth/db";
|
|
8
|
+
//#region src/billing-store.ts
|
|
9
|
+
function sortSubscriptionsForCurrent(subscriptions) {
|
|
10
|
+
const statusRank = new Map([
|
|
11
|
+
["active", 0],
|
|
12
|
+
["trialing", 1],
|
|
13
|
+
["incomplete", 2],
|
|
14
|
+
["past_due", 3],
|
|
15
|
+
["canceled", 4]
|
|
16
|
+
]);
|
|
17
|
+
return [...subscriptions].sort((a, b) => {
|
|
18
|
+
const rankA = statusRank.get(a.status) ?? 99;
|
|
19
|
+
const rankB = statusRank.get(b.status) ?? 99;
|
|
20
|
+
if (rankA !== rankB) return rankA - rankB;
|
|
21
|
+
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function createBillingStore(ctx) {
|
|
25
|
+
return createBillingStoreFromAdapter(ctx.context.adapter);
|
|
26
|
+
}
|
|
27
|
+
function createBillingStoreFromAdapter(adapter) {
|
|
28
|
+
const findOne = async (model, where) => await adapter.findOne({
|
|
29
|
+
model,
|
|
30
|
+
where
|
|
31
|
+
}) ?? null;
|
|
32
|
+
const findMany = async (model, where) => await adapter.findMany({
|
|
33
|
+
model,
|
|
34
|
+
...where ? { where } : {}
|
|
35
|
+
}) ?? [];
|
|
36
|
+
return {
|
|
37
|
+
findSubscriptionById: (id) => findOne("subscription", [{
|
|
38
|
+
field: "id",
|
|
39
|
+
value: id
|
|
40
|
+
}]),
|
|
41
|
+
findSubscriptionByCode: (subscriptionCode) => findOne("subscription", [{
|
|
42
|
+
field: "paystackSubscriptionCode",
|
|
43
|
+
value: subscriptionCode
|
|
44
|
+
}]),
|
|
45
|
+
findSubscriptionsByReference: (referenceId) => findMany("subscription", [{
|
|
46
|
+
field: "referenceId",
|
|
47
|
+
value: referenceId
|
|
48
|
+
}]),
|
|
49
|
+
async findCurrentSubscription(referenceId) {
|
|
50
|
+
return sortSubscriptionsForCurrent(await this.findSubscriptionsByReference(referenceId))[0] ?? null;
|
|
51
|
+
},
|
|
52
|
+
findSubscriptionsByTransactionReference: (reference) => findMany("subscription", [{
|
|
53
|
+
field: "paystackTransactionReference",
|
|
54
|
+
value: reference
|
|
55
|
+
}]),
|
|
56
|
+
createSubscription: async (data) => await adapter.create({
|
|
57
|
+
model: "subscription",
|
|
58
|
+
data
|
|
59
|
+
}),
|
|
60
|
+
updateSubscription: (id, update) => adapter.update({
|
|
61
|
+
model: "subscription",
|
|
62
|
+
update,
|
|
63
|
+
where: [{
|
|
64
|
+
field: "id",
|
|
65
|
+
value: id
|
|
66
|
+
}]
|
|
67
|
+
}),
|
|
68
|
+
updateSubscriptionByCode: (subscriptionCode, update) => adapter.update({
|
|
69
|
+
model: "subscription",
|
|
70
|
+
update,
|
|
71
|
+
where: [{
|
|
72
|
+
field: "paystackSubscriptionCode",
|
|
73
|
+
value: subscriptionCode
|
|
74
|
+
}]
|
|
75
|
+
}),
|
|
76
|
+
createTransaction: async (data) => await adapter.create({
|
|
77
|
+
model: "paystackTransaction",
|
|
78
|
+
data
|
|
79
|
+
}),
|
|
80
|
+
findTransactionByReference: (reference) => findOne("paystackTransaction", [{
|
|
81
|
+
field: "reference",
|
|
82
|
+
value: reference
|
|
83
|
+
}]),
|
|
84
|
+
updateTransactionByReference: (reference, update) => adapter.update({
|
|
85
|
+
model: "paystackTransaction",
|
|
86
|
+
update,
|
|
87
|
+
where: [{
|
|
88
|
+
field: "reference",
|
|
89
|
+
value: reference
|
|
90
|
+
}]
|
|
91
|
+
}),
|
|
92
|
+
async listTransactions(referenceId) {
|
|
93
|
+
return (await findMany("paystackTransaction", [{
|
|
94
|
+
field: "referenceId",
|
|
95
|
+
value: referenceId
|
|
96
|
+
}])).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
97
|
+
},
|
|
98
|
+
async listProducts() {
|
|
99
|
+
return (await findMany("paystackProduct")).sort((a, b) => a.name.localeCompare(b.name));
|
|
100
|
+
},
|
|
101
|
+
findProductByName: (name) => findOne("paystackProduct", [{
|
|
102
|
+
field: "name",
|
|
103
|
+
value: name
|
|
104
|
+
}]),
|
|
105
|
+
findProductBySlug: (slug) => findOne("paystackProduct", [{
|
|
106
|
+
field: "slug",
|
|
107
|
+
value: slug
|
|
108
|
+
}]),
|
|
109
|
+
async updateProduct(id, update) {
|
|
110
|
+
await adapter.update({
|
|
111
|
+
model: "paystackProduct",
|
|
112
|
+
update,
|
|
113
|
+
where: [{
|
|
114
|
+
field: "id",
|
|
115
|
+
value: id
|
|
116
|
+
}]
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
async upsertProductByPaystackId(paystackId, data) {
|
|
120
|
+
const existing = await findOne("paystackProduct", [{
|
|
121
|
+
field: "paystackId",
|
|
122
|
+
value: paystackId
|
|
123
|
+
}]);
|
|
124
|
+
if (existing?.id !== void 0) {
|
|
125
|
+
const { createdAt: _createdAt, ...update } = data;
|
|
126
|
+
await adapter.update({
|
|
127
|
+
model: "paystackProduct",
|
|
128
|
+
update,
|
|
129
|
+
where: [{
|
|
130
|
+
field: "id",
|
|
131
|
+
value: String(existing.id)
|
|
132
|
+
}]
|
|
133
|
+
});
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
await adapter.create({
|
|
137
|
+
model: "paystackProduct",
|
|
138
|
+
data
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
listPlans: () => findMany("paystackPlan"),
|
|
142
|
+
findPlanByName: (name) => findOne("paystackPlan", [{
|
|
143
|
+
field: "name",
|
|
144
|
+
value: name
|
|
145
|
+
}]),
|
|
146
|
+
findPlanByCode: (planCode) => findOne("paystackPlan", [{
|
|
147
|
+
field: "planCode",
|
|
148
|
+
value: planCode
|
|
149
|
+
}]),
|
|
150
|
+
async upsertPlanByPaystackId(paystackId, data) {
|
|
151
|
+
const existing = await findOne("paystackPlan", [{
|
|
152
|
+
field: "paystackId",
|
|
153
|
+
value: paystackId
|
|
154
|
+
}]);
|
|
155
|
+
if (existing?.id !== void 0) {
|
|
156
|
+
const { createdAt: _createdAt, ...update } = data;
|
|
157
|
+
await adapter.update({
|
|
158
|
+
model: "paystackPlan",
|
|
159
|
+
update,
|
|
160
|
+
where: [{
|
|
161
|
+
field: "id",
|
|
162
|
+
value: existing.id
|
|
163
|
+
}]
|
|
164
|
+
});
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
await adapter.create({
|
|
168
|
+
model: "paystackPlan",
|
|
169
|
+
data
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
findUser: (id) => findOne("user", [{
|
|
173
|
+
field: "id",
|
|
174
|
+
value: id
|
|
175
|
+
}]),
|
|
176
|
+
findOrganization: (id) => findOne("organization", [{
|
|
177
|
+
field: "id",
|
|
178
|
+
value: id
|
|
179
|
+
}]),
|
|
180
|
+
findOrganizationOwner: (organizationId) => findOne("member", [{
|
|
181
|
+
field: "organizationId",
|
|
182
|
+
value: organizationId
|
|
183
|
+
}, {
|
|
184
|
+
field: "role",
|
|
185
|
+
value: "owner"
|
|
186
|
+
}]),
|
|
187
|
+
listMembers: (organizationId) => findMany("member", [{
|
|
188
|
+
field: "organizationId",
|
|
189
|
+
value: organizationId
|
|
190
|
+
}]),
|
|
191
|
+
listTeams: (organizationId) => findMany("team", [{
|
|
192
|
+
field: "organizationId",
|
|
193
|
+
value: organizationId
|
|
194
|
+
}]),
|
|
195
|
+
async saveCustomerCode(referenceId, customerCode, isOrganization) {
|
|
196
|
+
await adapter.update({
|
|
197
|
+
model: isOrganization ? "organization" : "user",
|
|
198
|
+
update: { paystackCustomerCode: customerCode },
|
|
199
|
+
where: [{
|
|
200
|
+
field: "id",
|
|
201
|
+
value: referenceId
|
|
202
|
+
}]
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
//#endregion
|
|
8
208
|
//#region src/paystack-sdk.ts
|
|
9
209
|
/**
|
|
10
210
|
* Interface for checking if a result is a PaystackResponse from the SDK v1.9.1+
|
|
@@ -20,6 +220,10 @@ function unwrapSdkResult(result) {
|
|
|
20
220
|
if (IsPaystackResponse(result)) try {
|
|
21
221
|
return result.unwrap();
|
|
22
222
|
} catch (e) {
|
|
223
|
+
if (e instanceof PaystackError) throw new APIError("BAD_REQUEST", {
|
|
224
|
+
message: e.message,
|
|
225
|
+
status: e.status
|
|
226
|
+
});
|
|
23
227
|
throw new APIError("BAD_REQUEST", { message: e?.message ?? "Paystack API error" });
|
|
24
228
|
}
|
|
25
229
|
let current = result;
|
|
@@ -42,6 +246,50 @@ function unwrapSdkResult(result) {
|
|
|
42
246
|
function getPaystackOps(client) {
|
|
43
247
|
return client;
|
|
44
248
|
}
|
|
249
|
+
function createPaystackAdapter(client) {
|
|
250
|
+
const requireClient = () => {
|
|
251
|
+
if (client === void 0 || client === null) throw new APIError("BAD_REQUEST", { message: "Paystack client is not configured" });
|
|
252
|
+
return client;
|
|
253
|
+
};
|
|
254
|
+
return {
|
|
255
|
+
async initializeTransaction(body) {
|
|
256
|
+
return unwrapSdkResult(await requireClient().transaction?.initialize({ body }));
|
|
257
|
+
},
|
|
258
|
+
async verifyTransaction(reference) {
|
|
259
|
+
return unwrapSdkResult(await requireClient().transaction?.verify(reference));
|
|
260
|
+
},
|
|
261
|
+
async chargeAuthorization(body) {
|
|
262
|
+
return unwrapSdkResult(await requireClient().transaction?.chargeAuthorization({ body }));
|
|
263
|
+
},
|
|
264
|
+
async createCustomer(body) {
|
|
265
|
+
return unwrapSdkResult(await requireClient().customer?.create({ body }));
|
|
266
|
+
},
|
|
267
|
+
async listProducts() {
|
|
268
|
+
return unwrapSdkResult(await requireClient().product?.list({}));
|
|
269
|
+
},
|
|
270
|
+
async fetchProduct(productId) {
|
|
271
|
+
return unwrapSdkResult(await requireClient().product?.fetch(productId));
|
|
272
|
+
},
|
|
273
|
+
async listPlans() {
|
|
274
|
+
return unwrapSdkResult(await requireClient().plan?.list());
|
|
275
|
+
},
|
|
276
|
+
async createSubscription(body) {
|
|
277
|
+
return unwrapSdkResult(await requireClient().subscription?.create({ body }));
|
|
278
|
+
},
|
|
279
|
+
async fetchSubscription(subscriptionCode) {
|
|
280
|
+
return unwrapSdkResult(await requireClient().subscription?.fetch(subscriptionCode));
|
|
281
|
+
},
|
|
282
|
+
async disableSubscription(body) {
|
|
283
|
+
return unwrapSdkResult(await requireClient().subscription?.disable({ body }));
|
|
284
|
+
},
|
|
285
|
+
async enableSubscription(body) {
|
|
286
|
+
return unwrapSdkResult(await requireClient().subscription?.enable({ body }));
|
|
287
|
+
},
|
|
288
|
+
async manageSubscriptionLink(subscriptionCode) {
|
|
289
|
+
return unwrapSdkResult(await requireClient().subscription?.manageLink(subscriptionCode));
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
}
|
|
45
293
|
//#endregion
|
|
46
294
|
//#region src/utils.ts
|
|
47
295
|
function getPlanSeatAmount(plan) {
|
|
@@ -129,97 +377,46 @@ function validateMinAmount(amount, currency) {
|
|
|
129
377
|
return min !== void 0 ? amount >= min : true;
|
|
130
378
|
}
|
|
131
379
|
async function syncProductQuantityFromPaystack(ctx, productName, paystackClient) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
field: "name",
|
|
136
|
-
value: productName
|
|
137
|
-
}]
|
|
138
|
-
});
|
|
139
|
-
localProduct ??= await ctx.context.adapter.findOne({
|
|
140
|
-
model: "paystackProduct",
|
|
141
|
-
where: [{
|
|
142
|
-
field: "slug",
|
|
143
|
-
value: productName.toLowerCase().replace(/\s+/g, "-")
|
|
144
|
-
}]
|
|
145
|
-
});
|
|
380
|
+
const store = createBillingStore(ctx);
|
|
381
|
+
let localProduct = await store.findProductByName(productName);
|
|
382
|
+
localProduct ??= await store.findProductBySlug(productName.toLowerCase().replace(/\s+/g, "-"));
|
|
146
383
|
if (localProduct?.paystackId === void 0 || localProduct.paystackId === null || localProduct.paystackId === "") {
|
|
147
|
-
if (localProduct?.id !== void 0 && localProduct.unlimited !== true && typeof localProduct.quantity === "number" && localProduct.quantity > 0) await
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
quantity: localProduct.quantity - 1,
|
|
151
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
152
|
-
},
|
|
153
|
-
where: [{
|
|
154
|
-
field: "id",
|
|
155
|
-
value: localProduct.id
|
|
156
|
-
}]
|
|
384
|
+
if (localProduct?.id !== void 0 && localProduct.unlimited !== true && typeof localProduct.quantity === "number" && localProduct.quantity > 0) await store.updateProduct(localProduct.id, {
|
|
385
|
+
quantity: localProduct.quantity - 1,
|
|
386
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
157
387
|
});
|
|
158
388
|
return;
|
|
159
389
|
}
|
|
160
390
|
try {
|
|
161
391
|
const paystackProductId = Number(localProduct.paystackId);
|
|
162
392
|
if (!Number.isFinite(paystackProductId)) return;
|
|
163
|
-
const remoteQuantity =
|
|
164
|
-
if (remoteQuantity !== void 0 && localProduct.id !== void 0) await
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
quantity: remoteQuantity,
|
|
168
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
169
|
-
},
|
|
170
|
-
where: [{
|
|
171
|
-
field: "id",
|
|
172
|
-
value: localProduct.id
|
|
173
|
-
}]
|
|
393
|
+
const remoteQuantity = (await createPaystackAdapter(paystackClient).fetchProduct(paystackProductId))?.quantity;
|
|
394
|
+
if (remoteQuantity !== void 0 && localProduct.id !== void 0) await store.updateProduct(localProduct.id, {
|
|
395
|
+
quantity: remoteQuantity,
|
|
396
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
174
397
|
});
|
|
175
398
|
} catch {
|
|
176
|
-
if (localProduct?.id !== void 0 && localProduct.unlimited !== true && typeof localProduct.quantity === "number" && localProduct.quantity > 0) await
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
quantity: localProduct.quantity - 1,
|
|
180
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
181
|
-
},
|
|
182
|
-
where: [{
|
|
183
|
-
field: "id",
|
|
184
|
-
value: localProduct.id
|
|
185
|
-
}]
|
|
399
|
+
if (localProduct?.id !== void 0 && localProduct.unlimited !== true && typeof localProduct.quantity === "number" && localProduct.quantity > 0) await store.updateProduct(localProduct.id, {
|
|
400
|
+
quantity: localProduct.quantity - 1,
|
|
401
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
186
402
|
});
|
|
187
403
|
}
|
|
188
404
|
}
|
|
189
405
|
async function syncSubscriptionSeats(ctx, organizationId, options) {
|
|
190
406
|
if (options.subscription?.enabled !== true) return;
|
|
191
|
-
const
|
|
192
|
-
const subscription = await
|
|
193
|
-
model: "subscription",
|
|
194
|
-
where: [{
|
|
195
|
-
field: "referenceId",
|
|
196
|
-
value: organizationId
|
|
197
|
-
}]
|
|
198
|
-
});
|
|
407
|
+
const store = createBillingStore(ctx);
|
|
408
|
+
const subscription = await store.findCurrentSubscription(organizationId);
|
|
199
409
|
if (subscription?.paystackSubscriptionCode === void 0 || subscription.paystackSubscriptionCode === null || subscription.paystackSubscriptionCode === "") return;
|
|
200
410
|
if (subscription === null || subscription === void 0) return;
|
|
201
411
|
const plan = await getPlanByName(options, subscription.plan);
|
|
202
412
|
if (plan === null || plan === void 0) return;
|
|
203
413
|
if (getPlanSeatAmount(plan) === void 0) return;
|
|
204
|
-
const quantity = (await
|
|
205
|
-
model: "member",
|
|
206
|
-
where: [{
|
|
207
|
-
field: "organizationId",
|
|
208
|
-
value: organizationId
|
|
209
|
-
}]
|
|
210
|
-
})).length;
|
|
414
|
+
const quantity = (await store.listMembers(organizationId)).length;
|
|
211
415
|
try {
|
|
212
416
|
assertLocallyManagedSubscription(subscription, "automatic seat sync");
|
|
213
|
-
await
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
field: "id",
|
|
217
|
-
value: subscription.id
|
|
218
|
-
}],
|
|
219
|
-
update: {
|
|
220
|
-
seats: quantity,
|
|
221
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
222
|
-
}
|
|
417
|
+
await store.updateSubscription(subscription.id, {
|
|
418
|
+
seats: quantity,
|
|
419
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
223
420
|
});
|
|
224
421
|
} catch (e) {
|
|
225
422
|
const log = ctx.context.logger;
|
|
@@ -227,35 +424,28 @@ async function syncSubscriptionSeats(ctx, organizationId, options) {
|
|
|
227
424
|
}
|
|
228
425
|
}
|
|
229
426
|
//#endregion
|
|
230
|
-
//#region src/
|
|
427
|
+
//#region src/reference-access.ts
|
|
231
428
|
const BILLING_ORG_ROLES = new Set(["owner", "admin"]);
|
|
232
429
|
function hasBillingRole(role) {
|
|
233
430
|
if (Array.isArray(role)) return role.some((value) => hasBillingRole(value));
|
|
234
431
|
if (typeof role !== "string") return false;
|
|
235
432
|
return role.split(",").map((value) => value.trim()).some((value) => BILLING_ORG_ROLES.has(value));
|
|
236
433
|
}
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
session: session.session,
|
|
253
|
-
referenceId,
|
|
254
|
-
action
|
|
255
|
-
}, ctx) === true) return { context: {
|
|
256
|
-
...ctx.context,
|
|
257
|
-
referenceId
|
|
258
|
-
} };
|
|
434
|
+
function resolveBillingReferenceId(input) {
|
|
435
|
+
const body = input.body ?? {};
|
|
436
|
+
const query = input.query ?? {};
|
|
437
|
+
const requestQueryReferenceId = typeof input.requestUrl === "string" ? new URL(input.requestUrl).searchParams.get("referenceId") ?? void 0 : void 0;
|
|
438
|
+
return body.referenceId ?? query.referenceId ?? requestQueryReferenceId ?? input.fallbackUserId;
|
|
439
|
+
}
|
|
440
|
+
async function authorizeBillingReference(ctx, options, data) {
|
|
441
|
+
if (data.referenceId === data.user.id) return;
|
|
442
|
+
if (options.subscription?.enabled === true && typeof options.subscription.authorizeReference === "function") {
|
|
443
|
+
if (await options.subscription.authorizeReference({
|
|
444
|
+
user: data.user,
|
|
445
|
+
session: data.session,
|
|
446
|
+
referenceId: data.referenceId,
|
|
447
|
+
action: data.action
|
|
448
|
+
}, ctx) === true) return;
|
|
259
449
|
throw new APIError("UNAUTHORIZED");
|
|
260
450
|
}
|
|
261
451
|
if (options.organization?.enabled === true) {
|
|
@@ -263,56 +453,170 @@ const referenceMiddleware = (options, action) => createAuthMiddleware(async (ctx
|
|
|
263
453
|
model: "member",
|
|
264
454
|
where: [{
|
|
265
455
|
field: "userId",
|
|
266
|
-
value:
|
|
456
|
+
value: data.user.id
|
|
267
457
|
}, {
|
|
268
458
|
field: "organizationId",
|
|
269
|
-
value: referenceId
|
|
459
|
+
value: data.referenceId
|
|
270
460
|
}]
|
|
271
461
|
});
|
|
272
|
-
if (member !== null && member !== void 0 && hasBillingRole(member.role)) return
|
|
273
|
-
...ctx.context,
|
|
274
|
-
referenceId
|
|
275
|
-
} };
|
|
462
|
+
if (member !== null && member !== void 0 && hasBillingRole(member.role)) return;
|
|
276
463
|
}
|
|
277
|
-
|
|
278
|
-
|
|
464
|
+
throw new APIError("UNAUTHORIZED");
|
|
465
|
+
}
|
|
466
|
+
//#endregion
|
|
467
|
+
//#region src/middleware.ts
|
|
468
|
+
const referenceMiddleware = (options, action) => createAuthMiddleware(async (ctx) => {
|
|
469
|
+
const session = ctx.context.session;
|
|
470
|
+
if (session === null || session === void 0) throw new APIError("UNAUTHORIZED");
|
|
471
|
+
const referenceId = resolveBillingReferenceId({
|
|
472
|
+
body: ctx.body,
|
|
473
|
+
query: ctx.query,
|
|
474
|
+
requestUrl: ctx.request?.url,
|
|
475
|
+
fallbackUserId: session.user.id
|
|
476
|
+
});
|
|
477
|
+
await authorizeBillingReference(ctx, options, {
|
|
478
|
+
user: session.user,
|
|
479
|
+
session: session.session,
|
|
480
|
+
referenceId,
|
|
481
|
+
action
|
|
482
|
+
});
|
|
483
|
+
return { context: {
|
|
484
|
+
...ctx.context,
|
|
485
|
+
referenceId
|
|
486
|
+
} };
|
|
279
487
|
});
|
|
280
488
|
//#endregion
|
|
281
489
|
//#region src/limits.ts
|
|
282
490
|
const getOrganizationSubscription = async (ctx, organizationId) => {
|
|
283
|
-
return
|
|
284
|
-
model: "subscription",
|
|
285
|
-
where: [{
|
|
286
|
-
field: "referenceId",
|
|
287
|
-
value: organizationId
|
|
288
|
-
}]
|
|
289
|
-
});
|
|
491
|
+
return createBillingStore(ctx).findCurrentSubscription(organizationId);
|
|
290
492
|
};
|
|
291
493
|
const checkSeatLimit = async (ctx, organizationId, seatsToAdd = 1) => {
|
|
292
494
|
const subscription = await getOrganizationSubscription(ctx, organizationId);
|
|
293
495
|
if (subscription?.seats === null) return true;
|
|
294
|
-
const members = await ctx.
|
|
295
|
-
model: "member",
|
|
296
|
-
where: [{
|
|
297
|
-
field: "organizationId",
|
|
298
|
-
value: organizationId
|
|
299
|
-
}]
|
|
300
|
-
});
|
|
496
|
+
const members = await createBillingStore(ctx).listMembers(organizationId);
|
|
301
497
|
if (!subscription) return true;
|
|
302
498
|
if (members.length + seatsToAdd > subscription.seats) throw new APIError("FORBIDDEN", { message: `Organization member limit reached. Used: ${members.length}, Max: ${subscription.seats}` });
|
|
303
499
|
return true;
|
|
304
500
|
};
|
|
305
501
|
const checkTeamLimit = async (ctx, organizationId, maxTeams) => {
|
|
306
|
-
if ((await ctx.
|
|
307
|
-
model: "team",
|
|
308
|
-
where: [{
|
|
309
|
-
field: "organizationId",
|
|
310
|
-
value: organizationId
|
|
311
|
-
}]
|
|
312
|
-
})).length >= maxTeams) throw new APIError("FORBIDDEN", { message: `Organization team limit reached. Max teams: ${maxTeams}` });
|
|
502
|
+
if ((await createBillingStore(ctx).listTeams(organizationId)).length >= maxTeams) throw new APIError("FORBIDDEN", { message: `Organization team limit reached. Max teams: ${maxTeams}` });
|
|
313
503
|
return true;
|
|
314
504
|
};
|
|
315
505
|
//#endregion
|
|
506
|
+
//#region src/subscription-lifecycle.ts
|
|
507
|
+
async function handleProratedUpgrade(ctx, options, input) {
|
|
508
|
+
const store = createBillingStore(ctx);
|
|
509
|
+
const existingSub = await store.findCurrentSubscription(input.referenceId);
|
|
510
|
+
if (existingSub?.status !== "active" || existingSub.paystackSubscriptionCode === void 0 || existingSub.paystackSubscriptionCode === null || existingSub.paystackSubscriptionCode === "" || existingSub.periodEnd === void 0 || existingSub.periodEnd === null || existingSub.periodStart === void 0 || existingSub.periodStart === null) return null;
|
|
511
|
+
const now = /* @__PURE__ */ new Date();
|
|
512
|
+
const periodEnd = new Date(existingSub.periodEnd);
|
|
513
|
+
const periodStart = new Date(existingSub.periodStart);
|
|
514
|
+
const totalDays = Math.max(1, Math.ceil((periodEnd.getTime() - periodStart.getTime()) / (1e3 * 60 * 60 * 24)));
|
|
515
|
+
const remainingDays = Math.max(0, Math.ceil((periodEnd.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24)));
|
|
516
|
+
let oldAmount = 0;
|
|
517
|
+
if (existingSub.plan !== "") {
|
|
518
|
+
const oldPlan = await getPlanByName(options, existingSub.plan) ?? await store.findPlanByName(existingSub.plan);
|
|
519
|
+
if (oldPlan !== void 0 && oldPlan !== null) oldAmount = calculatePlanAmount(oldPlan, existingSub.seats);
|
|
520
|
+
}
|
|
521
|
+
let membersCount = 1;
|
|
522
|
+
let newSeatCount;
|
|
523
|
+
let newAmount;
|
|
524
|
+
try {
|
|
525
|
+
assertLocallyManagedSubscription(existingSub, "plan or seat changes");
|
|
526
|
+
if (getPlanSeatAmount(input.plan) !== void 0) {
|
|
527
|
+
const members = await store.listMembers(input.referenceId);
|
|
528
|
+
membersCount = members.length > 0 ? members.length : 1;
|
|
529
|
+
}
|
|
530
|
+
newSeatCount = input.quantity ?? existingSub.seats ?? membersCount;
|
|
531
|
+
newAmount = calculatePlanAmount(input.plan, newSeatCount);
|
|
532
|
+
} catch (error) {
|
|
533
|
+
throw new APIError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Invalid seat configuration for plan." });
|
|
534
|
+
}
|
|
535
|
+
const costDifference = newAmount - oldAmount;
|
|
536
|
+
const prorationMetadata = {
|
|
537
|
+
type: "proration",
|
|
538
|
+
subscriptionId: existingSub.id,
|
|
539
|
+
referenceId: input.referenceId,
|
|
540
|
+
newPlan: input.plan.name.toLowerCase(),
|
|
541
|
+
oldPlan: existingSub.plan,
|
|
542
|
+
newSeatCount,
|
|
543
|
+
remainingDays
|
|
544
|
+
};
|
|
545
|
+
let completedProrationReference;
|
|
546
|
+
if (costDifference > 0 && remainingDays > 0) {
|
|
547
|
+
const proratedAmount = Math.round(costDifference / totalDays * remainingDays);
|
|
548
|
+
if (proratedAmount < 5e3) throw new APIError("BAD_REQUEST", {
|
|
549
|
+
message: "Prorated upgrade amount is below Paystack's minimum charge. Schedule the change for period end instead.",
|
|
550
|
+
status: 400
|
|
551
|
+
});
|
|
552
|
+
const paystack = createPaystackAdapter(options.paystackClient);
|
|
553
|
+
if (existingSub.paystackAuthorizationCode !== void 0 && existingSub.paystackAuthorizationCode !== null && existingSub.paystackAuthorizationCode !== "") {
|
|
554
|
+
const sdkRes = await paystack.chargeAuthorization({
|
|
555
|
+
email: input.targetEmail,
|
|
556
|
+
amount: proratedAmount,
|
|
557
|
+
authorization_code: existingSub.paystackAuthorizationCode,
|
|
558
|
+
reference: `upg_${existingSub.id}_${Date.now()}_${Math.random().toString(36).substring(7)}`,
|
|
559
|
+
metadata: JSON.stringify(prorationMetadata)
|
|
560
|
+
});
|
|
561
|
+
if (sdkRes?.status !== "success") throw new APIError("BAD_REQUEST", { message: "Failed to process prorated charge via saved authorization." });
|
|
562
|
+
await store.createTransaction({
|
|
563
|
+
reference: sdkRes.reference ?? "",
|
|
564
|
+
paystackId: sdkRes.id !== void 0 && sdkRes.id !== null ? String(sdkRes.id) : void 0,
|
|
565
|
+
referenceId: input.referenceId,
|
|
566
|
+
userId: input.userId,
|
|
567
|
+
amount: sdkRes.amount ?? proratedAmount,
|
|
568
|
+
currency: sdkRes.currency ?? input.finalCurrency,
|
|
569
|
+
status: "success",
|
|
570
|
+
plan: input.plan.name.toLowerCase(),
|
|
571
|
+
metadata: JSON.stringify(prorationMetadata),
|
|
572
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
573
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
574
|
+
});
|
|
575
|
+
completedProrationReference = sdkRes.reference ?? void 0;
|
|
576
|
+
} else {
|
|
577
|
+
const initRes = await paystack.initializeTransaction({
|
|
578
|
+
email: input.targetEmail,
|
|
579
|
+
amount: proratedAmount,
|
|
580
|
+
currency: input.finalCurrency,
|
|
581
|
+
callback_url: input.callbackURL ?? void 0,
|
|
582
|
+
metadata: JSON.stringify(prorationMetadata),
|
|
583
|
+
...input.allowedSubscriptionChannels !== void 0 ? { channels: input.allowedSubscriptionChannels } : {}
|
|
584
|
+
});
|
|
585
|
+
await store.createTransaction({
|
|
586
|
+
reference: initRes?.reference ?? "",
|
|
587
|
+
referenceId: input.referenceId,
|
|
588
|
+
userId: input.userId,
|
|
589
|
+
amount: proratedAmount,
|
|
590
|
+
currency: input.finalCurrency,
|
|
591
|
+
status: "pending",
|
|
592
|
+
plan: input.plan.name.toLowerCase(),
|
|
593
|
+
metadata: JSON.stringify(prorationMetadata),
|
|
594
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
595
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
596
|
+
});
|
|
597
|
+
return {
|
|
598
|
+
kind: "checkout",
|
|
599
|
+
url: initRes?.authorization_url,
|
|
600
|
+
reference: initRes?.reference,
|
|
601
|
+
accessCode: initRes?.access_code,
|
|
602
|
+
redirect: true
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
await store.updateSubscription(existingSub.id, {
|
|
607
|
+
plan: input.plan.name,
|
|
608
|
+
seats: newSeatCount,
|
|
609
|
+
...completedProrationReference !== void 0 ? { paystackTransactionReference: completedProrationReference } : {},
|
|
610
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
611
|
+
});
|
|
612
|
+
return {
|
|
613
|
+
kind: "completed",
|
|
614
|
+
status: "success",
|
|
615
|
+
message: "Subscription successfully upgraded with prorated charge.",
|
|
616
|
+
prorated: true
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
//#endregion
|
|
316
620
|
//#region src/routes.ts
|
|
317
621
|
const PAYSTACK_ERROR_CODES = defineErrorCodes({
|
|
318
622
|
SUBSCRIPTION_NOT_FOUND: "Subscription not found",
|
|
@@ -333,32 +637,6 @@ function isAllowedSubscriptionChannel(channel, allowedChannels) {
|
|
|
333
637
|
if (allowedChannels === void 0) return true;
|
|
334
638
|
return channel !== void 0 && channel !== null && allowedChannels.includes(channel);
|
|
335
639
|
}
|
|
336
|
-
async function assertReferenceAccess(ctx, options, data) {
|
|
337
|
-
if (data.referenceId === data.user.id) return;
|
|
338
|
-
if (options.subscription?.enabled === true && typeof options.subscription.authorizeReference === "function") {
|
|
339
|
-
if (await options.subscription.authorizeReference({
|
|
340
|
-
user: data.user,
|
|
341
|
-
session: data.session,
|
|
342
|
-
referenceId: data.referenceId,
|
|
343
|
-
action: data.action
|
|
344
|
-
}, ctx) === true) return;
|
|
345
|
-
throw new APIError("UNAUTHORIZED");
|
|
346
|
-
}
|
|
347
|
-
if (options.organization?.enabled === true) {
|
|
348
|
-
const member = await ctx.context.adapter.findOne({
|
|
349
|
-
model: "member",
|
|
350
|
-
where: [{
|
|
351
|
-
field: "userId",
|
|
352
|
-
value: data.user.id
|
|
353
|
-
}, {
|
|
354
|
-
field: "organizationId",
|
|
355
|
-
value: data.referenceId
|
|
356
|
-
}]
|
|
357
|
-
});
|
|
358
|
-
if (member !== null && member !== void 0 && hasBillingRole(member.role)) return;
|
|
359
|
-
}
|
|
360
|
-
throw new APIError("UNAUTHORIZED");
|
|
361
|
-
}
|
|
362
640
|
async function hmacSha512Hex(secret, message) {
|
|
363
641
|
const encoder = new TextEncoder();
|
|
364
642
|
const keyData = encoder.encode(secret);
|
|
@@ -854,148 +1132,27 @@ const initializeTransaction = (options, path = "/initialize-transaction") => {
|
|
|
854
1132
|
};
|
|
855
1133
|
if (allowedSubscriptionChannels !== void 0) initBody.channels = allowedSubscriptionChannels;
|
|
856
1134
|
if (plan !== void 0 && prorateAndCharge === true) {
|
|
857
|
-
const
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
}
|
|
879
|
-
let membersCount = 1;
|
|
880
|
-
let newSeatCount = quantity ?? existingSub.seats ?? membersCount;
|
|
881
|
-
let newAmount;
|
|
882
|
-
try {
|
|
883
|
-
assertLocallyManagedSubscription(existingSub, "plan or seat changes");
|
|
884
|
-
if (getPlanSeatAmount(plan) !== void 0) {
|
|
885
|
-
const members = await ctx.context.adapter.findMany({
|
|
886
|
-
model: "member",
|
|
887
|
-
where: [{
|
|
888
|
-
field: "organizationId",
|
|
889
|
-
value: referenceId
|
|
890
|
-
}]
|
|
891
|
-
});
|
|
892
|
-
membersCount = members.length > 0 ? members.length : 1;
|
|
893
|
-
}
|
|
894
|
-
newSeatCount = quantity ?? existingSub.seats ?? membersCount;
|
|
895
|
-
newAmount = calculatePlanAmount(plan, newSeatCount);
|
|
896
|
-
} catch (error) {
|
|
897
|
-
throw new APIError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Invalid seat configuration for plan." });
|
|
898
|
-
}
|
|
899
|
-
const costDifference = newAmount - oldAmount;
|
|
900
|
-
const prorationMetadata = {
|
|
901
|
-
type: "proration",
|
|
902
|
-
subscriptionId: existingSub.id,
|
|
903
|
-
referenceId,
|
|
904
|
-
newPlan: plan.name.toLowerCase(),
|
|
905
|
-
oldPlan: existingSub.plan,
|
|
906
|
-
newSeatCount,
|
|
907
|
-
remainingDays
|
|
908
|
-
};
|
|
909
|
-
let completedProrationReference;
|
|
910
|
-
if (costDifference > 0 && remainingDays > 0) {
|
|
911
|
-
const proratedAmount = Math.round(costDifference / totalDays * remainingDays);
|
|
912
|
-
if (proratedAmount < 5e3) throw new APIError("BAD_REQUEST", {
|
|
913
|
-
message: "Prorated upgrade amount is below Paystack's minimum charge. Schedule the change for period end instead.",
|
|
914
|
-
status: 400
|
|
915
|
-
});
|
|
916
|
-
const ops = getPaystackOps(options.paystackClient);
|
|
917
|
-
if (ops === void 0 || ops === null) {
|
|
918
|
-
ctx.context.logger.error("Paystack client not configured for proration charge");
|
|
919
|
-
return;
|
|
920
|
-
}
|
|
921
|
-
if (existingSub.paystackAuthorizationCode !== void 0 && existingSub.paystackAuthorizationCode !== null && existingSub.paystackAuthorizationCode !== "") {
|
|
922
|
-
const sdkRes = unwrapSdkResult(await ops.transaction?.chargeAuthorization({ body: {
|
|
923
|
-
email: targetEmail,
|
|
924
|
-
amount: proratedAmount,
|
|
925
|
-
authorization_code: existingSub.paystackAuthorizationCode,
|
|
926
|
-
reference: `upg_${existingSub.id}_${Date.now()}_${Math.random().toString(36).substring(7)}`,
|
|
927
|
-
metadata: JSON.stringify(prorationMetadata)
|
|
928
|
-
} }));
|
|
929
|
-
if (sdkRes?.status !== "success") throw new APIError("BAD_REQUEST", { message: "Failed to process prorated charge via saved authorization." });
|
|
930
|
-
await ctx.context.adapter.create({
|
|
931
|
-
model: "paystackTransaction",
|
|
932
|
-
data: {
|
|
933
|
-
reference: sdkRes.reference ?? "",
|
|
934
|
-
paystackId: sdkRes.id !== void 0 && sdkRes.id !== null ? String(sdkRes.id) : void 0,
|
|
935
|
-
referenceId,
|
|
936
|
-
userId: user.id,
|
|
937
|
-
amount: sdkRes.amount ?? proratedAmount,
|
|
938
|
-
currency: sdkRes.currency ?? finalCurrency,
|
|
939
|
-
status: "success",
|
|
940
|
-
plan: plan.name.toLowerCase(),
|
|
941
|
-
metadata: JSON.stringify(prorationMetadata),
|
|
942
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
943
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
944
|
-
}
|
|
945
|
-
});
|
|
946
|
-
completedProrationReference = sdkRes.reference ?? void 0;
|
|
947
|
-
} else {
|
|
948
|
-
const initRes = unwrapSdkResult(await ops.transaction?.initialize({ body: {
|
|
949
|
-
email: targetEmail,
|
|
950
|
-
amount: proratedAmount,
|
|
951
|
-
currency: finalCurrency,
|
|
952
|
-
callback_url: callbackURL ?? void 0,
|
|
953
|
-
metadata: JSON.stringify(prorationMetadata),
|
|
954
|
-
...allowedSubscriptionChannels !== void 0 ? { channels: allowedSubscriptionChannels } : {}
|
|
955
|
-
} }));
|
|
956
|
-
await ctx.context.adapter.create({
|
|
957
|
-
model: "paystackTransaction",
|
|
958
|
-
data: {
|
|
959
|
-
reference: initRes?.reference ?? "",
|
|
960
|
-
referenceId,
|
|
961
|
-
userId: user.id,
|
|
962
|
-
amount: proratedAmount,
|
|
963
|
-
currency: finalCurrency,
|
|
964
|
-
status: "pending",
|
|
965
|
-
plan: plan.name.toLowerCase(),
|
|
966
|
-
metadata: JSON.stringify(prorationMetadata),
|
|
967
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
968
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
969
|
-
}
|
|
970
|
-
});
|
|
971
|
-
return ctx.json({
|
|
972
|
-
url: initRes?.authorization_url,
|
|
973
|
-
reference: initRes?.reference,
|
|
974
|
-
accessCode: initRes?.access_code,
|
|
975
|
-
redirect: true
|
|
976
|
-
});
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
await ctx.context.adapter.update({
|
|
980
|
-
model: "subscription",
|
|
981
|
-
where: [{
|
|
982
|
-
field: "id",
|
|
983
|
-
value: existingSub.id
|
|
984
|
-
}],
|
|
985
|
-
update: {
|
|
986
|
-
plan: plan.name,
|
|
987
|
-
seats: newSeatCount,
|
|
988
|
-
...completedProrationReference !== void 0 ? { paystackTransactionReference: completedProrationReference } : {},
|
|
989
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
990
|
-
}
|
|
991
|
-
});
|
|
992
|
-
return ctx.json({
|
|
993
|
-
status: "success",
|
|
994
|
-
message: "Subscription successfully upgraded with prorated charge.",
|
|
995
|
-
prorated: true
|
|
996
|
-
});
|
|
997
|
-
}
|
|
998
|
-
}
|
|
1135
|
+
const proration = await handleProratedUpgrade(ctx, options, {
|
|
1136
|
+
plan,
|
|
1137
|
+
referenceId,
|
|
1138
|
+
quantity,
|
|
1139
|
+
targetEmail,
|
|
1140
|
+
userId: user.id,
|
|
1141
|
+
finalCurrency,
|
|
1142
|
+
callbackURL,
|
|
1143
|
+
allowedSubscriptionChannels
|
|
1144
|
+
});
|
|
1145
|
+
if (proration?.kind === "checkout") return ctx.json({
|
|
1146
|
+
url: proration.url ?? "",
|
|
1147
|
+
reference: proration.reference ?? "",
|
|
1148
|
+
accessCode: proration.accessCode ?? "",
|
|
1149
|
+
redirect: proration.redirect
|
|
1150
|
+
});
|
|
1151
|
+
if (proration?.kind === "completed") return ctx.json({
|
|
1152
|
+
status: proration.status,
|
|
1153
|
+
message: proration.message,
|
|
1154
|
+
prorated: proration.prorated
|
|
1155
|
+
});
|
|
999
1156
|
}
|
|
1000
1157
|
if (plan !== void 0) if (trialStart !== void 0) initBody.amount = 5e3;
|
|
1001
1158
|
else {
|
|
@@ -1150,30 +1307,12 @@ const verifyTransaction = (options, path = "/verify-transaction") => {
|
|
|
1150
1307
|
message: `This subscription requires one of: ${allowedSubscriptionChannels?.join(", ") ?? "allowed channels"}.`
|
|
1151
1308
|
});
|
|
1152
1309
|
}
|
|
1153
|
-
if (session !== void 0 && session !== null && referenceId !== void 0 && referenceId !== null && referenceId !== "" && referenceId !== session.user.id) {
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
referenceId,
|
|
1160
|
-
action: "verify-transaction"
|
|
1161
|
-
}, ctx);
|
|
1162
|
-
if (authorized === false && options.organization?.enabled === true) {
|
|
1163
|
-
const member = await ctx.context.adapter.findOne({
|
|
1164
|
-
model: "member",
|
|
1165
|
-
where: [{
|
|
1166
|
-
field: "userId",
|
|
1167
|
-
value: session.user.id
|
|
1168
|
-
}, {
|
|
1169
|
-
field: "organizationId",
|
|
1170
|
-
value: referenceId
|
|
1171
|
-
}]
|
|
1172
|
-
});
|
|
1173
|
-
if (member !== void 0 && member !== null) authorized = true;
|
|
1174
|
-
}
|
|
1175
|
-
if (authorized === false) throw new APIError("UNAUTHORIZED");
|
|
1176
|
-
}
|
|
1310
|
+
if (session !== void 0 && session !== null && referenceId !== void 0 && referenceId !== null && referenceId !== "" && referenceId !== session.user.id) await authorizeBillingReference(ctx, options, {
|
|
1311
|
+
user: session.user,
|
|
1312
|
+
session: session.session,
|
|
1313
|
+
referenceId,
|
|
1314
|
+
action: "verify-transaction"
|
|
1315
|
+
});
|
|
1177
1316
|
try {
|
|
1178
1317
|
await ctx.context.adapter.update({
|
|
1179
1318
|
model: "paystackTransaction",
|
|
@@ -1338,23 +1477,18 @@ const listSubscriptions = (options, path = "/list-subscriptions") => {
|
|
|
1338
1477
|
if (subscriptionOptions?.enabled !== true) throw new APIError("BAD_REQUEST", { message: "Subscriptions are not enabled in the Paystack options." });
|
|
1339
1478
|
const session = await getSessionFromCtx(ctx);
|
|
1340
1479
|
if (session === void 0 || session === null) throw new APIError("UNAUTHORIZED");
|
|
1480
|
+
const store = createBillingStore(ctx);
|
|
1341
1481
|
const referenceIdPart = ctx.context.referenceId;
|
|
1342
1482
|
const queryRefId = ctx.query?.referenceId ?? (typeof ctx.request?.url === "string" ? new URL(ctx.request.url).searchParams.get("referenceId") ?? void 0 : void 0);
|
|
1343
1483
|
const userId = session.user.id;
|
|
1344
|
-
if (queryRefId !== void 0 && queryRefId !== userId && referenceIdPart !== queryRefId) await
|
|
1484
|
+
if (queryRefId !== void 0 && queryRefId !== userId && referenceIdPart !== queryRefId) await authorizeBillingReference(ctx, options, {
|
|
1345
1485
|
user: session.user,
|
|
1346
1486
|
session: session.session,
|
|
1347
1487
|
referenceId: queryRefId,
|
|
1348
1488
|
action: "list-subscriptions"
|
|
1349
1489
|
});
|
|
1350
1490
|
const referenceId = queryRefId ?? referenceIdPart ?? userId;
|
|
1351
|
-
const res = await
|
|
1352
|
-
model: "subscription",
|
|
1353
|
-
where: [{
|
|
1354
|
-
field: "referenceId",
|
|
1355
|
-
value: referenceId
|
|
1356
|
-
}]
|
|
1357
|
-
});
|
|
1491
|
+
const res = await store.findSubscriptionsByReference(referenceId);
|
|
1358
1492
|
return ctx.json({ subscriptions: res });
|
|
1359
1493
|
});
|
|
1360
1494
|
};
|
|
@@ -1370,24 +1504,19 @@ const listTransactions = (options, path = "/list-transactions") => {
|
|
|
1370
1504
|
}, async (ctx) => {
|
|
1371
1505
|
const session = await getSessionFromCtx(ctx);
|
|
1372
1506
|
if (session === void 0 || session === null) throw new APIError("UNAUTHORIZED");
|
|
1507
|
+
const store = createBillingStore(ctx);
|
|
1373
1508
|
const referenceIdPart = ctx.context.referenceId;
|
|
1374
1509
|
const queryRefId = ctx.query?.referenceId ?? (typeof ctx.request?.url === "string" ? new URL(ctx.request.url).searchParams.get("referenceId") ?? void 0 : void 0);
|
|
1375
1510
|
const userId = session.user.id;
|
|
1376
|
-
if (queryRefId !== void 0 && queryRefId !== userId && referenceIdPart !== queryRefId) await
|
|
1511
|
+
if (queryRefId !== void 0 && queryRefId !== userId && referenceIdPart !== queryRefId) await authorizeBillingReference(ctx, options, {
|
|
1377
1512
|
user: session.user,
|
|
1378
1513
|
session: session.session,
|
|
1379
1514
|
referenceId: queryRefId,
|
|
1380
1515
|
action: "list-transactions"
|
|
1381
1516
|
});
|
|
1382
1517
|
const referenceId = queryRefId ?? referenceIdPart ?? userId;
|
|
1383
|
-
const
|
|
1384
|
-
|
|
1385
|
-
where: [{
|
|
1386
|
-
field: "referenceId",
|
|
1387
|
-
value: referenceId
|
|
1388
|
-
}]
|
|
1389
|
-
})).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
1390
|
-
return ctx.json({ transactions: sorted });
|
|
1518
|
+
const transactions = await store.listTransactions(referenceId);
|
|
1519
|
+
return ctx.json({ transactions });
|
|
1391
1520
|
});
|
|
1392
1521
|
};
|
|
1393
1522
|
const enableDisableBodySchema = z.object({
|
|
@@ -1586,8 +1715,8 @@ const listProducts = (_options, path = "/list-products") => {
|
|
|
1586
1715
|
method: "GET",
|
|
1587
1716
|
metadata: { openapi: { operationId: "listPaystackProducts" } }
|
|
1588
1717
|
}, async (ctx) => {
|
|
1589
|
-
const
|
|
1590
|
-
return ctx.json({ products
|
|
1718
|
+
const products = await createBillingStore(ctx).listProducts();
|
|
1719
|
+
return ctx.json({ products });
|
|
1591
1720
|
});
|
|
1592
1721
|
};
|
|
1593
1722
|
const listPlans = (_options, path = "/list-plans") => {
|
|
@@ -1597,7 +1726,7 @@ const listPlans = (_options, path = "/list-plans") => {
|
|
|
1597
1726
|
use: [sessionMiddleware]
|
|
1598
1727
|
}, async (ctx) => {
|
|
1599
1728
|
try {
|
|
1600
|
-
const plans = await ctx.
|
|
1729
|
+
const plans = await createBillingStore(ctx).listPlans();
|
|
1601
1730
|
return ctx.json({ plans });
|
|
1602
1731
|
} catch (error) {
|
|
1603
1732
|
ctx.context.logger.error("Failed to list plans", error);
|
|
@@ -1890,22 +2019,16 @@ const getSchema = (options) => {
|
|
|
1890
2019
|
//#endregion
|
|
1891
2020
|
//#region src/operations.ts
|
|
1892
2021
|
async function syncPaystackProducts(ctx, options) {
|
|
1893
|
-
const paystack =
|
|
2022
|
+
const paystack = createPaystackAdapter(options.paystackClient);
|
|
2023
|
+
const store = createBillingStore(ctx);
|
|
1894
2024
|
try {
|
|
1895
|
-
const productsData =
|
|
2025
|
+
const productsData = await paystack.listProducts();
|
|
1896
2026
|
if (!Array.isArray(productsData)) return {
|
|
1897
2027
|
status: "success",
|
|
1898
2028
|
count: 0
|
|
1899
2029
|
};
|
|
1900
2030
|
for (const product of productsData) {
|
|
1901
2031
|
const paystackId = String(product.id);
|
|
1902
|
-
const existing = await ctx.context.adapter.findOne({
|
|
1903
|
-
model: "paystackProduct",
|
|
1904
|
-
where: [{
|
|
1905
|
-
field: "paystackId",
|
|
1906
|
-
value: paystackId
|
|
1907
|
-
}]
|
|
1908
|
-
});
|
|
1909
2032
|
const productFields = {
|
|
1910
2033
|
name: product.name ?? "",
|
|
1911
2034
|
description: product.description ?? "",
|
|
@@ -1918,20 +2041,9 @@ async function syncPaystackProducts(ctx, options) {
|
|
|
1918
2041
|
metadata: product.metadata !== void 0 && product.metadata !== null ? JSON.stringify(product.metadata) : void 0,
|
|
1919
2042
|
updatedAt: /* @__PURE__ */ new Date()
|
|
1920
2043
|
};
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
where: [{
|
|
1925
|
-
field: "id",
|
|
1926
|
-
value: String(existing.id)
|
|
1927
|
-
}]
|
|
1928
|
-
});
|
|
1929
|
-
else await ctx.context.adapter.create({
|
|
1930
|
-
model: "paystackProduct",
|
|
1931
|
-
data: {
|
|
1932
|
-
...productFields,
|
|
1933
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
1934
|
-
}
|
|
2044
|
+
await store.upsertProductByPaystackId(paystackId, {
|
|
2045
|
+
...productFields,
|
|
2046
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
1935
2047
|
});
|
|
1936
2048
|
}
|
|
1937
2049
|
return {
|
|
@@ -1944,25 +2056,19 @@ async function syncPaystackProducts(ctx, options) {
|
|
|
1944
2056
|
}
|
|
1945
2057
|
}
|
|
1946
2058
|
async function syncPaystackPlans(ctx, options) {
|
|
1947
|
-
const paystack =
|
|
2059
|
+
const paystack = createPaystackAdapter(options.paystackClient);
|
|
2060
|
+
const store = createBillingStore(ctx);
|
|
1948
2061
|
try {
|
|
1949
|
-
const plansData =
|
|
2062
|
+
const plansData = await paystack.listPlans();
|
|
1950
2063
|
if (!Array.isArray(plansData)) return {
|
|
1951
2064
|
status: "success",
|
|
1952
2065
|
count: 0
|
|
1953
2066
|
};
|
|
1954
2067
|
for (const plan of plansData) {
|
|
1955
2068
|
const paystackId = String(plan.id);
|
|
1956
|
-
const existing = await ctx.context.adapter.findOne({
|
|
1957
|
-
model: "paystackPlan",
|
|
1958
|
-
where: [{
|
|
1959
|
-
field: "paystackId",
|
|
1960
|
-
value: paystackId
|
|
1961
|
-
}]
|
|
1962
|
-
});
|
|
1963
2069
|
const planData = {
|
|
1964
2070
|
name: plan.name ?? "",
|
|
1965
|
-
description: plan.description
|
|
2071
|
+
description: typeof plan.description === "string" ? plan.description : "",
|
|
1966
2072
|
amount: plan.amount ?? 0,
|
|
1967
2073
|
currency: plan.currency ?? "",
|
|
1968
2074
|
interval: plan.interval ?? "",
|
|
@@ -1971,20 +2077,9 @@ async function syncPaystackPlans(ctx, options) {
|
|
|
1971
2077
|
metadata: plan.metadata !== void 0 && plan.metadata !== null ? JSON.stringify(plan.metadata) : void 0,
|
|
1972
2078
|
updatedAt: /* @__PURE__ */ new Date()
|
|
1973
2079
|
};
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
where: [{
|
|
1978
|
-
field: "id",
|
|
1979
|
-
value: existing.id
|
|
1980
|
-
}]
|
|
1981
|
-
});
|
|
1982
|
-
else await ctx.context.adapter.create({
|
|
1983
|
-
model: "paystackPlan",
|
|
1984
|
-
data: {
|
|
1985
|
-
...planData,
|
|
1986
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
1987
|
-
}
|
|
2080
|
+
await store.upsertPlanByPaystackId(paystackId, {
|
|
2081
|
+
...planData,
|
|
2082
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
1988
2083
|
});
|
|
1989
2084
|
}
|
|
1990
2085
|
return {
|
|
@@ -1998,13 +2093,8 @@ async function syncPaystackPlans(ctx, options) {
|
|
|
1998
2093
|
}
|
|
1999
2094
|
async function chargeSubscriptionRenewal(ctx, options, input) {
|
|
2000
2095
|
const { subscriptionId, amount: bodyAmount } = input;
|
|
2001
|
-
const
|
|
2002
|
-
|
|
2003
|
-
where: [{
|
|
2004
|
-
field: "id",
|
|
2005
|
-
value: subscriptionId
|
|
2006
|
-
}]
|
|
2007
|
-
});
|
|
2096
|
+
const store = createBillingStore(ctx);
|
|
2097
|
+
const subscription = await store.findSubscriptionById(subscriptionId);
|
|
2008
2098
|
if (subscription === void 0 || subscription === null) throw new APIError("NOT_FOUND", { message: "Subscription not found" });
|
|
2009
2099
|
if (subscription.paystackAuthorizationCode === void 0 || subscription.paystackAuthorizationCode === null || subscription.paystackAuthorizationCode === "") throw new APIError("BAD_REQUEST", { message: "No authorization code found for this subscription" });
|
|
2010
2100
|
const plan = (await getPlans(options.subscription)).find((candidate) => candidate.name.toLowerCase() === subscription.plan.toLowerCase());
|
|
@@ -2015,35 +2105,14 @@ async function chargeSubscriptionRenewal(ctx, options, input) {
|
|
|
2015
2105
|
let billingUserId = subscription.userId;
|
|
2016
2106
|
const referenceId = subscription.referenceId;
|
|
2017
2107
|
if (referenceId !== void 0 && referenceId !== null && referenceId !== "") {
|
|
2018
|
-
const user = await
|
|
2019
|
-
model: "user",
|
|
2020
|
-
where: [{
|
|
2021
|
-
field: "id",
|
|
2022
|
-
value: referenceId
|
|
2023
|
-
}]
|
|
2024
|
-
});
|
|
2108
|
+
const user = await store.findUser(referenceId);
|
|
2025
2109
|
if (user !== void 0 && user !== null) {
|
|
2026
2110
|
email = user.email;
|
|
2027
2111
|
billingUserId = user.id;
|
|
2028
2112
|
} else if (options.organization?.enabled === true) {
|
|
2029
|
-
const ownerMember = await
|
|
2030
|
-
model: "member",
|
|
2031
|
-
where: [{
|
|
2032
|
-
field: "organizationId",
|
|
2033
|
-
value: referenceId
|
|
2034
|
-
}, {
|
|
2035
|
-
field: "role",
|
|
2036
|
-
value: "owner"
|
|
2037
|
-
}]
|
|
2038
|
-
});
|
|
2113
|
+
const ownerMember = await store.findOrganizationOwner(referenceId);
|
|
2039
2114
|
if (ownerMember !== void 0 && ownerMember !== null) {
|
|
2040
|
-
const ownerUser = await
|
|
2041
|
-
model: "user",
|
|
2042
|
-
where: [{
|
|
2043
|
-
field: "id",
|
|
2044
|
-
value: ownerMember.userId
|
|
2045
|
-
}]
|
|
2046
|
-
});
|
|
2115
|
+
const ownerUser = await store.findUser(ownerMember.userId);
|
|
2047
2116
|
email = ownerUser?.email;
|
|
2048
2117
|
billingUserId = ownerUser?.id ?? ownerMember.userId;
|
|
2049
2118
|
}
|
|
@@ -2055,7 +2124,7 @@ async function chargeSubscriptionRenewal(ctx, options, input) {
|
|
|
2055
2124
|
message: `Amount ${amount} is less than the minimum required for ${finalCurrency}.`,
|
|
2056
2125
|
status: 400
|
|
2057
2126
|
});
|
|
2058
|
-
const
|
|
2127
|
+
const typedChargeData = await createPaystackAdapter(options.paystackClient).chargeAuthorization({
|
|
2059
2128
|
email,
|
|
2060
2129
|
amount,
|
|
2061
2130
|
authorization_code: subscription.paystackAuthorizationCode,
|
|
@@ -2064,51 +2133,41 @@ async function chargeSubscriptionRenewal(ctx, options, input) {
|
|
|
2064
2133
|
subscriptionId,
|
|
2065
2134
|
referenceId
|
|
2066
2135
|
})
|
|
2067
|
-
}
|
|
2068
|
-
if (
|
|
2136
|
+
});
|
|
2137
|
+
if (typedChargeData?.status === "success" && typedChargeData.reference !== void 0) {
|
|
2069
2138
|
const now = /* @__PURE__ */ new Date();
|
|
2070
2139
|
const nextPeriodEnd = getNextPeriodEnd(now, plan.interval ?? "monthly");
|
|
2071
|
-
await
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
createdAt: now,
|
|
2088
|
-
updatedAt: now
|
|
2089
|
-
}
|
|
2140
|
+
await store.createTransaction({
|
|
2141
|
+
reference: typedChargeData.reference,
|
|
2142
|
+
paystackId: typedChargeData.id !== void 0 && typedChargeData.id !== null ? String(typedChargeData.id) : void 0,
|
|
2143
|
+
referenceId,
|
|
2144
|
+
userId: billingUserId,
|
|
2145
|
+
amount: typedChargeData.amount,
|
|
2146
|
+
currency: typedChargeData.currency,
|
|
2147
|
+
status: "success",
|
|
2148
|
+
plan: plan.name.toLowerCase(),
|
|
2149
|
+
metadata: JSON.stringify({
|
|
2150
|
+
type: "renewal",
|
|
2151
|
+
subscriptionId,
|
|
2152
|
+
referenceId
|
|
2153
|
+
}),
|
|
2154
|
+
createdAt: now,
|
|
2155
|
+
updatedAt: now
|
|
2090
2156
|
});
|
|
2091
|
-
await
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
updatedAt: now,
|
|
2097
|
-
paystackTransactionReference: chargeData.reference
|
|
2098
|
-
},
|
|
2099
|
-
where: [{
|
|
2100
|
-
field: "id",
|
|
2101
|
-
value: subscription.id
|
|
2102
|
-
}]
|
|
2157
|
+
await store.updateSubscription(subscription.id, {
|
|
2158
|
+
periodStart: now,
|
|
2159
|
+
periodEnd: nextPeriodEnd,
|
|
2160
|
+
updatedAt: now,
|
|
2161
|
+
paystackTransactionReference: typedChargeData.reference
|
|
2103
2162
|
});
|
|
2104
2163
|
return {
|
|
2105
2164
|
status: "success",
|
|
2106
|
-
data:
|
|
2165
|
+
data: typedChargeData
|
|
2107
2166
|
};
|
|
2108
2167
|
}
|
|
2109
2168
|
return {
|
|
2110
2169
|
status: "failed",
|
|
2111
|
-
data:
|
|
2170
|
+
data: typedChargeData
|
|
2112
2171
|
};
|
|
2113
2172
|
}
|
|
2114
2173
|
//#endregion
|
|
@@ -2151,23 +2210,14 @@ const createPaystackPlugin = (options) => {
|
|
|
2151
2210
|
user: { create: { async after(user, hookCtx) {
|
|
2152
2211
|
if (!hookCtx || options.createCustomerOnSignUp !== true || user.email === null || user.email === void 0 || user.email === "") return;
|
|
2153
2212
|
try {
|
|
2154
|
-
const
|
|
2155
|
-
if (!paystackOps) return;
|
|
2156
|
-
const sdkRes = unwrapSdkResult(await paystackOps.customer?.create({ body: {
|
|
2213
|
+
const sdkRes = await createPaystackAdapter(options.paystackClient).createCustomer({
|
|
2157
2214
|
email: user.email,
|
|
2158
2215
|
first_name: user.name ?? void 0,
|
|
2159
2216
|
metadata: JSON.stringify({ userId: user.id })
|
|
2160
|
-
}
|
|
2217
|
+
});
|
|
2161
2218
|
const customerCode = sdkRes?.customer_code;
|
|
2162
2219
|
if (customerCode !== void 0 && customerCode !== null && customerCode !== "") {
|
|
2163
|
-
await ctx.adapter.
|
|
2164
|
-
model: "user",
|
|
2165
|
-
where: [{
|
|
2166
|
-
field: "id",
|
|
2167
|
-
value: user.id
|
|
2168
|
-
}],
|
|
2169
|
-
update: { paystackCustomerCode: customerCode }
|
|
2170
|
-
});
|
|
2220
|
+
await createBillingStoreFromAdapter(ctx.adapter).saveCustomerCode(user.id, customerCode, false);
|
|
2171
2221
|
if (typeof options.onCustomerCreate === "function") await options.onCustomerCreate({
|
|
2172
2222
|
paystackCustomer: sdkRes,
|
|
2173
2223
|
user: {
|
|
@@ -2185,23 +2235,9 @@ const createPaystackPlugin = (options) => {
|
|
|
2185
2235
|
const extraCreateParams = typeof options.organization?.getCustomerCreateParams === "function" ? await options.organization.getCustomerCreateParams(org, hookCtx) : {};
|
|
2186
2236
|
let targetEmail = org.email;
|
|
2187
2237
|
if (targetEmail === void 0 || targetEmail === null) {
|
|
2188
|
-
const
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
field: "organizationId",
|
|
2192
|
-
value: org.id
|
|
2193
|
-
}, {
|
|
2194
|
-
field: "role",
|
|
2195
|
-
value: "owner"
|
|
2196
|
-
}]
|
|
2197
|
-
});
|
|
2198
|
-
if (ownerMember !== null && ownerMember !== void 0) targetEmail = (await ctx.adapter.findOne({
|
|
2199
|
-
model: "user",
|
|
2200
|
-
where: [{
|
|
2201
|
-
field: "id",
|
|
2202
|
-
value: ownerMember.userId
|
|
2203
|
-
}]
|
|
2204
|
-
}))?.email;
|
|
2238
|
+
const store = createBillingStoreFromAdapter(ctx.adapter);
|
|
2239
|
+
const ownerMember = await store.findOrganizationOwner(org.id);
|
|
2240
|
+
if (ownerMember !== null && ownerMember !== void 0) targetEmail = (await store.findUser(ownerMember.userId))?.email;
|
|
2205
2241
|
}
|
|
2206
2242
|
if (targetEmail === void 0 || targetEmail === null) return;
|
|
2207
2243
|
const params = defu({
|
|
@@ -2209,19 +2245,10 @@ const createPaystackPlugin = (options) => {
|
|
|
2209
2245
|
first_name: org.name,
|
|
2210
2246
|
metadata: JSON.stringify({ organizationId: org.id })
|
|
2211
2247
|
}, extraCreateParams);
|
|
2212
|
-
const
|
|
2213
|
-
if (!paystackOps) return;
|
|
2214
|
-
const sdkRes = unwrapSdkResult(await paystackOps.customer?.create({ body: params }) ?? await Promise.reject(/* @__PURE__ */ new Error("Paystack client missing customer ops")));
|
|
2248
|
+
const sdkRes = await createPaystackAdapter(options.paystackClient).createCustomer(params);
|
|
2215
2249
|
const customerCode = sdkRes?.customer_code;
|
|
2216
2250
|
if (customerCode !== void 0 && customerCode !== null && customerCode !== "" && sdkRes !== void 0 && sdkRes !== null) {
|
|
2217
|
-
await ctx.adapter.
|
|
2218
|
-
model: "organization",
|
|
2219
|
-
where: [{
|
|
2220
|
-
field: "id",
|
|
2221
|
-
value: org.id
|
|
2222
|
-
}],
|
|
2223
|
-
update: { paystackCustomerCode: customerCode }
|
|
2224
|
-
});
|
|
2251
|
+
await createBillingStoreFromAdapter(ctx.adapter).saveCustomerCode(org.id, customerCode, true);
|
|
2225
2252
|
if (typeof options.organization?.onCustomerCreate === "function") await options.organization.onCustomerCreate({
|
|
2226
2253
|
paystackCustomer: sdkRes,
|
|
2227
2254
|
organization: {
|