@alexasomba/better-auth-paystack 1.2.1 → 2.1.0
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 +156 -47
- package/dist/client.d.mts +75 -222
- package/dist/client.d.mts.map +1 -1
- package/dist/client.mjs +26 -46
- package/dist/client.mjs.map +1 -1
- package/dist/index-Dwbeddkr.d.mts +711 -0
- package/dist/index-Dwbeddkr.d.mts.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +602 -651
- package/dist/index.mjs.map +1 -1
- package/package.json +64 -73
- package/dist/index-DoMJ9OLF.d.mts +0 -488
- package/dist/index-DoMJ9OLF.d.mts.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -3,118 +3,45 @@ import { defu } from "defu";
|
|
|
3
3
|
import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
|
|
4
4
|
import { HIDE_METADATA, logger } from "better-auth";
|
|
5
5
|
import { APIError, getSessionFromCtx, originCheck, sessionMiddleware } from "better-auth/api";
|
|
6
|
-
import
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { PaystackResponse } from "@alexasomba/paystack-node";
|
|
7
8
|
import { mergeSchema } from "better-auth/db";
|
|
8
9
|
//#region src/paystack-sdk.ts
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Interface for checking if a result is a PaystackResponse from the SDK v1.9.1+
|
|
12
|
+
*/
|
|
13
|
+
function IsPaystackResponse(value) {
|
|
14
|
+
return value instanceof PaystackResponse;
|
|
11
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Unwraps a Paystack SDK result, extracting the data or throwing an APIError if the request failed.
|
|
18
|
+
* Leverages the native .unwrap() method in SDK v1.9.1+ if available.
|
|
19
|
+
*/
|
|
12
20
|
function unwrapSdkResult(result) {
|
|
13
|
-
if (
|
|
14
|
-
|
|
15
|
-
|
|
21
|
+
if (IsPaystackResponse(result)) try {
|
|
22
|
+
return result.unwrap();
|
|
23
|
+
} catch (e) {
|
|
24
|
+
throw new APIError("BAD_REQUEST", { message: e?.message ?? "Paystack API error" });
|
|
16
25
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
let current = result;
|
|
27
|
+
while (current !== null && current !== void 0 && typeof current === "object") {
|
|
28
|
+
const body = current;
|
|
29
|
+
if (body.status === false) throw new APIError("BAD_REQUEST", { message: body.message ?? "Paystack API error" });
|
|
30
|
+
if ("authorization_url" in body || "reference" in body || "customer_code" in body) break;
|
|
31
|
+
if ("data" in body && body.data !== void 0 && body.data !== null && typeof body.data === "object") {
|
|
32
|
+
current = body.data;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
break;
|
|
21
36
|
}
|
|
22
|
-
return
|
|
37
|
+
return current;
|
|
23
38
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
customerUpdate: (code, params) => {
|
|
31
|
-
if (paystackClient?.customer_update !== void 0) return paystackClient.customer_update({
|
|
32
|
-
params: { path: { code } },
|
|
33
|
-
body: params
|
|
34
|
-
});
|
|
35
|
-
return paystackClient?.customer?.update?.(code, params);
|
|
36
|
-
},
|
|
37
|
-
transactionInitialize: (body) => {
|
|
38
|
-
if (paystackClient?.transaction_initialize !== void 0) return paystackClient.transaction_initialize({ body });
|
|
39
|
-
return paystackClient?.transaction?.initialize?.(body);
|
|
40
|
-
},
|
|
41
|
-
transactionVerify: (reference) => {
|
|
42
|
-
if (paystackClient?.transaction_verify !== void 0) return paystackClient.transaction_verify({ params: { path: { reference } } });
|
|
43
|
-
return paystackClient?.transaction?.verify?.(reference);
|
|
44
|
-
},
|
|
45
|
-
subscriptionCreate: (body) => {
|
|
46
|
-
if (paystackClient?.subscription_create !== void 0) return paystackClient.subscription_create({ body });
|
|
47
|
-
return paystackClient?.subscription?.create?.(body);
|
|
48
|
-
},
|
|
49
|
-
subscriptionDisable: (body) => {
|
|
50
|
-
if (paystackClient?.subscription_disable !== void 0) return paystackClient.subscription_disable({ body });
|
|
51
|
-
return paystackClient?.subscription?.disable?.(body);
|
|
52
|
-
},
|
|
53
|
-
subscriptionEnable: (body) => {
|
|
54
|
-
if (paystackClient?.subscription_enable !== void 0) return paystackClient.subscription_enable({ body });
|
|
55
|
-
return paystackClient?.subscription?.enable?.(body);
|
|
56
|
-
},
|
|
57
|
-
subscriptionFetch: async (idOrCode) => {
|
|
58
|
-
if (paystackClient?.subscription_fetch !== void 0) try {
|
|
59
|
-
return await paystackClient.subscription_fetch({ params: { path: { code: idOrCode } } });
|
|
60
|
-
} catch {
|
|
61
|
-
const compatFetch = paystackClient.subscription_fetch;
|
|
62
|
-
return compatFetch({ params: { path: { id_or_code: idOrCode } } });
|
|
63
|
-
}
|
|
64
|
-
return paystackClient?.subscription?.fetch?.(idOrCode);
|
|
65
|
-
},
|
|
66
|
-
subscriptionManageLink: (code) => {
|
|
67
|
-
if (paystackClient?.subscription_manageLink !== void 0) return paystackClient.subscription_manageLink({ params: { path: { code } } });
|
|
68
|
-
if (paystackClient?.subscription_manage_link !== void 0) return paystackClient.subscription_manage_link({ params: { path: { code } } });
|
|
69
|
-
return paystackClient?.subscription?.manage?.link?.(code);
|
|
70
|
-
},
|
|
71
|
-
subscriptionManageEmail: (code, email) => {
|
|
72
|
-
if (paystackClient?.subscription_manageEmail !== void 0) return paystackClient.subscription_manageEmail({ params: { path: { code } } });
|
|
73
|
-
return paystackClient?.subscription?.manage?.email?.(code, email);
|
|
74
|
-
},
|
|
75
|
-
subscriptionUpdate: (params) => {
|
|
76
|
-
if (paystackClient?.subscription_update !== void 0) return paystackClient.subscription_update({
|
|
77
|
-
params: { path: { code: params.code } },
|
|
78
|
-
body: {
|
|
79
|
-
plan: params.plan,
|
|
80
|
-
authorization: params.authorization,
|
|
81
|
-
amount: params.amount
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
return paystackClient?.subscription?.update?.(params.code, params);
|
|
85
|
-
},
|
|
86
|
-
transactionChargeAuthorization: (body) => {
|
|
87
|
-
if (paystackClient?.transaction_chargeAuthorization !== void 0) return paystackClient.transaction_chargeAuthorization({ body });
|
|
88
|
-
return paystackClient?.transaction?.chargeAuthorization?.(body);
|
|
89
|
-
},
|
|
90
|
-
productList: () => {
|
|
91
|
-
if (paystackClient?.product_list !== void 0) return paystackClient.product_list();
|
|
92
|
-
return paystackClient?.product?.list?.();
|
|
93
|
-
},
|
|
94
|
-
productFetch: (idOrCode) => {
|
|
95
|
-
if (paystackClient?.product_fetch !== void 0) return paystackClient.product_fetch({ params: { path: { id_or_code: idOrCode } } });
|
|
96
|
-
return paystackClient?.product?.fetch?.(idOrCode);
|
|
97
|
-
},
|
|
98
|
-
productCreate: (params) => {
|
|
99
|
-
if (paystackClient?.product_create !== void 0) return paystackClient.product_create({ body: params });
|
|
100
|
-
return paystackClient?.product?.create?.(params);
|
|
101
|
-
},
|
|
102
|
-
productUpdate: (idOrCode, params) => {
|
|
103
|
-
if (paystackClient?.product_update !== void 0) return paystackClient.product_update({
|
|
104
|
-
params: { path: { id_or_code: idOrCode } },
|
|
105
|
-
body: params
|
|
106
|
-
});
|
|
107
|
-
return paystackClient?.product?.update?.(idOrCode, params);
|
|
108
|
-
},
|
|
109
|
-
productDelete: (idOrCode) => {
|
|
110
|
-
if (paystackClient?.product_delete !== void 0) return paystackClient.product_delete({ params: { path: { id_or_code: idOrCode } } });
|
|
111
|
-
return paystackClient?.product?.delete?.(idOrCode);
|
|
112
|
-
},
|
|
113
|
-
planList: () => {
|
|
114
|
-
if (paystackClient?.plan_list !== void 0) return paystackClient.plan_list();
|
|
115
|
-
return paystackClient?.plan?.list?.();
|
|
116
|
-
}
|
|
117
|
-
};
|
|
39
|
+
/**
|
|
40
|
+
* Returns the operations object from a Paystack client.
|
|
41
|
+
* For v1.9.1+, the client itself uses the grouped structure.
|
|
42
|
+
*/
|
|
43
|
+
function getPaystackOps(client) {
|
|
44
|
+
return client;
|
|
118
45
|
}
|
|
119
46
|
//#endregion
|
|
120
47
|
//#region src/utils.ts
|
|
@@ -123,7 +50,12 @@ async function getPlans(subscriptionOptions) {
|
|
|
123
50
|
throw new Error("Subscriptions are not enabled in the Paystack options.");
|
|
124
51
|
}
|
|
125
52
|
async function getPlanByName(options, name) {
|
|
126
|
-
if (
|
|
53
|
+
if (typeof name !== "string" || name.trim() === "") return null;
|
|
54
|
+
if (options.subscription?.enabled === true) {
|
|
55
|
+
const plans = await getPlans(options.subscription);
|
|
56
|
+
const normalizedName = name.toLowerCase();
|
|
57
|
+
return plans.find((plan) => typeof plan.name === "string" && plan.name.toLowerCase() === normalizedName) ?? null;
|
|
58
|
+
}
|
|
127
59
|
return null;
|
|
128
60
|
}
|
|
129
61
|
async function getProducts(productOptions) {
|
|
@@ -131,7 +63,7 @@ async function getProducts(productOptions) {
|
|
|
131
63
|
return [];
|
|
132
64
|
}
|
|
133
65
|
async function getProductByName(options, name) {
|
|
134
|
-
return await getProducts(options.products).then((products) => products
|
|
66
|
+
return await getProducts(options.products).then((products) => products !== void 0 && products !== null ? products.find((product) => product.name.toLowerCase() === name.toLowerCase()) ?? null : null);
|
|
135
67
|
}
|
|
136
68
|
function getNextPeriodEnd(startDate, interval) {
|
|
137
69
|
const date = new Date(startDate);
|
|
@@ -189,7 +121,7 @@ async function syncProductQuantityFromPaystack(ctx, productName, paystackClient)
|
|
|
189
121
|
}]
|
|
190
122
|
});
|
|
191
123
|
if (localProduct?.paystackId === void 0 || localProduct.paystackId === null || localProduct.paystackId === "") {
|
|
192
|
-
if (localProduct !==
|
|
124
|
+
if (localProduct?.id !== void 0 && localProduct.unlimited !== true && typeof localProduct.quantity === "number" && localProduct.quantity > 0) await ctx.context.adapter.update({
|
|
193
125
|
model: "paystackProduct",
|
|
194
126
|
update: {
|
|
195
127
|
quantity: localProduct.quantity - 1,
|
|
@@ -203,8 +135,8 @@ async function syncProductQuantityFromPaystack(ctx, productName, paystackClient)
|
|
|
203
135
|
return;
|
|
204
136
|
}
|
|
205
137
|
try {
|
|
206
|
-
const remoteQuantity = unwrapSdkResult(await
|
|
207
|
-
if (remoteQuantity !== void 0) await ctx.context.adapter.update({
|
|
138
|
+
const remoteQuantity = unwrapSdkResult(await paystackClient.product?.fetch(localProduct.paystackId))?.quantity;
|
|
139
|
+
if (remoteQuantity !== void 0 && localProduct.id !== void 0) await ctx.context.adapter.update({
|
|
208
140
|
model: "paystackProduct",
|
|
209
141
|
update: {
|
|
210
142
|
quantity: remoteQuantity,
|
|
@@ -216,7 +148,7 @@ async function syncProductQuantityFromPaystack(ctx, productName, paystackClient)
|
|
|
216
148
|
}]
|
|
217
149
|
});
|
|
218
150
|
} catch {
|
|
219
|
-
if (localProduct !==
|
|
151
|
+
if (localProduct?.id !== void 0 && localProduct.unlimited !== true && typeof localProduct.quantity === "number" && localProduct.quantity > 0) await ctx.context.adapter.update({
|
|
220
152
|
model: "paystackProduct",
|
|
221
153
|
update: {
|
|
222
154
|
quantity: localProduct.quantity - 1,
|
|
@@ -240,9 +172,10 @@ async function syncSubscriptionSeats(ctx, organizationId, options) {
|
|
|
240
172
|
}]
|
|
241
173
|
});
|
|
242
174
|
if (subscription?.paystackSubscriptionCode === void 0 || subscription.paystackSubscriptionCode === null || subscription.paystackSubscriptionCode === "") return;
|
|
175
|
+
if (subscription === null || subscription === void 0) return;
|
|
243
176
|
const plan = await getPlanByName(options, subscription.plan);
|
|
244
|
-
if (plan === null) return;
|
|
245
|
-
if (plan.seatAmount === void 0
|
|
177
|
+
if (plan === null || plan === void 0) return;
|
|
178
|
+
if (plan.seatAmount === void 0) return;
|
|
246
179
|
const quantity = (await adapter.findMany({
|
|
247
180
|
model: "member",
|
|
248
181
|
where: [{
|
|
@@ -252,12 +185,10 @@ async function syncSubscriptionSeats(ctx, organizationId, options) {
|
|
|
252
185
|
})).length;
|
|
253
186
|
let totalAmount = plan.amount ?? 0;
|
|
254
187
|
if (plan.seatAmount !== void 0 && plan.seatAmount !== null && typeof plan.seatAmount === "number") totalAmount += quantity * plan.seatAmount;
|
|
255
|
-
const ops = getPaystackOps(options.paystackClient);
|
|
256
188
|
try {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
});
|
|
189
|
+
const client = options.paystackClient;
|
|
190
|
+
if (client === void 0 || client === null) return;
|
|
191
|
+
unwrapSdkResult(await client.subscription?.update(subscription.paystackSubscriptionCode, { body: { amount: totalAmount } }));
|
|
261
192
|
await adapter.update({
|
|
262
193
|
model: "subscription",
|
|
263
194
|
where: [{
|
|
@@ -284,7 +215,7 @@ const referenceMiddleware = (options, action) => createAuthMiddleware(async (ctx
|
|
|
284
215
|
const referenceId = body.referenceId ?? query.referenceId ?? session.user.id;
|
|
285
216
|
const subscriptionOptions = options.subscription;
|
|
286
217
|
if (referenceId === session.user.id) return { referenceId };
|
|
287
|
-
if (subscriptionOptions?.enabled === true && "authorizeReference" in subscriptionOptions && subscriptionOptions.authorizeReference) {
|
|
218
|
+
if (subscriptionOptions?.enabled === true && "authorizeReference" in subscriptionOptions && typeof subscriptionOptions.authorizeReference === "function") {
|
|
288
219
|
if (await subscriptionOptions.authorizeReference({
|
|
289
220
|
user: session.user,
|
|
290
221
|
session: session.session,
|
|
@@ -325,7 +256,7 @@ const getOrganizationSubscription = async (ctx, organizationId) => {
|
|
|
325
256
|
};
|
|
326
257
|
const checkSeatLimit = async (ctx, organizationId, seatsToAdd = 1) => {
|
|
327
258
|
const subscription = await getOrganizationSubscription(ctx, organizationId);
|
|
328
|
-
if (subscription?.seats ===
|
|
259
|
+
if (subscription?.seats === null) return true;
|
|
329
260
|
const members = await ctx.context.adapter.findMany({
|
|
330
261
|
model: "member",
|
|
331
262
|
where: [{
|
|
@@ -333,6 +264,7 @@ const checkSeatLimit = async (ctx, organizationId, seatsToAdd = 1) => {
|
|
|
333
264
|
value: organizationId
|
|
334
265
|
}]
|
|
335
266
|
});
|
|
267
|
+
if (!subscription) return true;
|
|
336
268
|
if (members.length + seatsToAdd > subscription.seats) throw new APIError("FORBIDDEN", { message: `Organization member limit reached. Used: ${members.length}, Max: ${subscription.seats}` });
|
|
337
269
|
return true;
|
|
338
270
|
};
|
|
@@ -375,8 +307,8 @@ async function hmacSha512Hex(secret, message) {
|
|
|
375
307
|
const { createHmac } = await import("node:crypto");
|
|
376
308
|
return createHmac("sha512", secret).update(message).digest("hex");
|
|
377
309
|
}
|
|
378
|
-
const paystackWebhook = (options) => {
|
|
379
|
-
return createAuthEndpoint(
|
|
310
|
+
const paystackWebhook = (options, path = "/webhook") => {
|
|
311
|
+
return createAuthEndpoint(path, {
|
|
380
312
|
method: "POST",
|
|
381
313
|
metadata: {
|
|
382
314
|
...HIDE_METADATA,
|
|
@@ -388,12 +320,25 @@ const paystackWebhook = (options) => {
|
|
|
388
320
|
const request = ctx.requestClone ?? ctx.request;
|
|
389
321
|
if (request === void 0 || request === null) throw new APIError("BAD_REQUEST", { message: "Request object is missing from context" });
|
|
390
322
|
const payload = await request.text();
|
|
391
|
-
const
|
|
323
|
+
const headers = ctx.headers ?? ctx.request?.headers;
|
|
324
|
+
const signature = headers?.get("x-paystack-signature");
|
|
325
|
+
if (options.webhook?.verifyIP === true) {
|
|
326
|
+
const trustedIPs = options.webhook.trustedIPs ?? [
|
|
327
|
+
"52.31.139.75",
|
|
328
|
+
"52.49.173.169",
|
|
329
|
+
"52.214.14.220"
|
|
330
|
+
];
|
|
331
|
+
const clientIP = headers?.get("x-forwarded-for")?.split(",")[0]?.trim() ?? headers?.get("x-real-ip") ?? ctx.request.ip;
|
|
332
|
+
if (clientIP !== void 0 && clientIP !== null && trustedIPs.includes(clientIP) === false) throw new APIError("UNAUTHORIZED", {
|
|
333
|
+
message: `Forbidden IP: ${clientIP}`,
|
|
334
|
+
status: 401
|
|
335
|
+
});
|
|
336
|
+
}
|
|
392
337
|
if (signature === void 0 || signature === null || signature === "") throw new APIError("UNAUTHORIZED", {
|
|
393
338
|
message: "Missing x-paystack-signature header",
|
|
394
339
|
status: 401
|
|
395
340
|
});
|
|
396
|
-
if (await hmacSha512Hex(options.
|
|
341
|
+
if (await hmacSha512Hex(options.webhook?.secret ?? options.secretKey, payload) !== signature) throw new APIError("UNAUTHORIZED", {
|
|
397
342
|
message: "Invalid Paystack webhook signature",
|
|
398
343
|
status: 401
|
|
399
344
|
});
|
|
@@ -402,7 +347,8 @@ const paystackWebhook = (options) => {
|
|
|
402
347
|
const data = event.data;
|
|
403
348
|
if (eventName === "charge.success") {
|
|
404
349
|
const reference = data?.reference;
|
|
405
|
-
const
|
|
350
|
+
const paystackIdRaw = data?.id;
|
|
351
|
+
const paystackId = paystackIdRaw !== void 0 && paystackIdRaw !== null ? String(paystackIdRaw) : void 0;
|
|
406
352
|
if (reference !== void 0 && reference !== null && reference !== "") {
|
|
407
353
|
try {
|
|
408
354
|
await ctx.context.adapter.update({
|
|
@@ -428,7 +374,9 @@ const paystackWebhook = (options) => {
|
|
|
428
374
|
value: reference
|
|
429
375
|
}]
|
|
430
376
|
});
|
|
431
|
-
if (transaction
|
|
377
|
+
if (transaction !== void 0 && transaction !== null && transaction.product !== void 0 && transaction.product !== null && transaction.product !== "") {
|
|
378
|
+
if (options.paystackClient !== void 0 && options.paystackClient !== null) await syncProductQuantityFromPaystack(ctx, transaction.product, options.paystackClient);
|
|
379
|
+
}
|
|
432
380
|
} catch (e) {
|
|
433
381
|
ctx.context.logger.warn("Failed to sync product quantity", e);
|
|
434
382
|
}
|
|
@@ -454,19 +402,20 @@ const paystackWebhook = (options) => {
|
|
|
454
402
|
}
|
|
455
403
|
if (options.subscription?.enabled === true) try {
|
|
456
404
|
if (eventName === "subscription.create") {
|
|
457
|
-
const
|
|
458
|
-
const subscriptionCode =
|
|
459
|
-
const customerCode =
|
|
460
|
-
const planCode =
|
|
461
|
-
let metadata =
|
|
405
|
+
const subscriptionData = data;
|
|
406
|
+
const subscriptionCode = subscriptionData.subscription_code ?? "";
|
|
407
|
+
const customerCode = subscriptionData.customer?.customer_code;
|
|
408
|
+
const planCode = subscriptionData.plan?.plan_code;
|
|
409
|
+
let metadata = subscriptionData.metadata;
|
|
462
410
|
if (typeof metadata === "string") try {
|
|
463
411
|
metadata = JSON.parse(metadata);
|
|
464
412
|
} catch {}
|
|
465
|
-
const
|
|
466
|
-
|
|
413
|
+
const metadataObj = metadata !== void 0 && metadata !== null && typeof metadata === "object" ? metadata : {};
|
|
414
|
+
const referenceIdFromMetadata = typeof metadataObj.referenceId === "string" ? metadataObj.referenceId : void 0;
|
|
415
|
+
let planNameFromMetadata = typeof metadataObj.plan === "string" ? metadataObj.plan : void 0;
|
|
467
416
|
if (typeof planNameFromMetadata === "string") planNameFromMetadata = planNameFromMetadata.toLowerCase();
|
|
468
417
|
const plans = await getPlans(options.subscription);
|
|
469
|
-
const planFromCode = planCode !== void 0 && planCode !== null && planCode !== "" ? plans.find((p) => p.planCode
|
|
418
|
+
const planFromCode = planCode !== void 0 && planCode !== null && planCode !== "" ? plans.find((p) => p.planCode === planCode) : void 0;
|
|
470
419
|
const planPart = planFromCode?.name ?? planNameFromMetadata;
|
|
471
420
|
const planName = planPart !== void 0 && planPart !== null && planPart !== "" ? planPart.toLowerCase() : void 0;
|
|
472
421
|
if (subscriptionCode !== void 0 && subscriptionCode !== null && subscriptionCode !== "") {
|
|
@@ -495,7 +444,7 @@ const paystackWebhook = (options) => {
|
|
|
495
444
|
paystackSubscriptionCode: subscriptionCode,
|
|
496
445
|
status: "active",
|
|
497
446
|
updatedAt: /* @__PURE__ */ new Date(),
|
|
498
|
-
periodEnd:
|
|
447
|
+
periodEnd: subscriptionData.next_payment_date !== void 0 && subscriptionData.next_payment_date !== null ? new Date(subscriptionData.next_payment_date) : void 0
|
|
499
448
|
},
|
|
500
449
|
where: [{
|
|
501
450
|
field: "id",
|
|
@@ -528,9 +477,9 @@ const paystackWebhook = (options) => {
|
|
|
528
477
|
}
|
|
529
478
|
}
|
|
530
479
|
if (eventName === "subscription.disable" || eventName === "subscription.not_renew") {
|
|
531
|
-
const
|
|
532
|
-
const subscriptionCode =
|
|
533
|
-
if (subscriptionCode !==
|
|
480
|
+
const subscriptionData = data;
|
|
481
|
+
const subscriptionCode = subscriptionData.subscription_code ?? "";
|
|
482
|
+
if (subscriptionCode !== "") {
|
|
534
483
|
const existing = await ctx.context.adapter.findOne({
|
|
535
484
|
model: "subscription",
|
|
536
485
|
where: [{
|
|
@@ -539,9 +488,9 @@ const paystackWebhook = (options) => {
|
|
|
539
488
|
}]
|
|
540
489
|
});
|
|
541
490
|
let newStatus = "canceled";
|
|
542
|
-
const nextPaymentDate =
|
|
543
|
-
const periodEnd = nextPaymentDate !== void 0 && nextPaymentDate !== null && nextPaymentDate !== "" ? new Date(nextPaymentDate) : existing?.periodEnd !== void 0 ? new Date(existing.periodEnd) : void 0;
|
|
544
|
-
if (periodEnd !== void 0 && periodEnd >
|
|
491
|
+
const nextPaymentDate = subscriptionData.next_payment_date;
|
|
492
|
+
const periodEnd = nextPaymentDate !== void 0 && nextPaymentDate !== null && nextPaymentDate !== "" ? new Date(nextPaymentDate) : existing?.periodEnd !== void 0 && existing.periodEnd !== null ? new Date(existing.periodEnd) : void 0;
|
|
493
|
+
if (periodEnd !== void 0 && periodEnd.getTime() > Date.now()) newStatus = "active";
|
|
545
494
|
await ctx.context.adapter.update({
|
|
546
495
|
model: "subscription",
|
|
547
496
|
update: {
|
|
@@ -555,7 +504,7 @@ const paystackWebhook = (options) => {
|
|
|
555
504
|
value: subscriptionCode
|
|
556
505
|
}]
|
|
557
506
|
});
|
|
558
|
-
if (existing
|
|
507
|
+
if (existing) await options.subscription.onSubscriptionCancel?.({
|
|
559
508
|
event,
|
|
560
509
|
subscription: {
|
|
561
510
|
...existing,
|
|
@@ -565,9 +514,9 @@ const paystackWebhook = (options) => {
|
|
|
565
514
|
}
|
|
566
515
|
}
|
|
567
516
|
if (eventName === "charge.success" || eventName === "invoice.update") {
|
|
568
|
-
const
|
|
569
|
-
const subscriptionCode =
|
|
570
|
-
if (subscriptionCode !== void 0
|
|
517
|
+
const subscriptionCodeRaw = (data?.subscription)?.subscription_code ?? data?.subscription_code;
|
|
518
|
+
const subscriptionCode = subscriptionCodeRaw !== void 0 && subscriptionCodeRaw !== null && subscriptionCodeRaw !== "" ? subscriptionCodeRaw : void 0;
|
|
519
|
+
if (subscriptionCode !== void 0) {
|
|
571
520
|
const existingSub = await ctx.context.adapter.findOne({
|
|
572
521
|
model: "subscription",
|
|
573
522
|
where: [{
|
|
@@ -575,7 +524,7 @@ const paystackWebhook = (options) => {
|
|
|
575
524
|
value: subscriptionCode
|
|
576
525
|
}]
|
|
577
526
|
});
|
|
578
|
-
if (existingSub
|
|
527
|
+
if (existingSub !== void 0 && existingSub !== null && existingSub.pendingPlan !== void 0 && existingSub.pendingPlan !== null && existingSub.pendingPlan !== "") await ctx.context.adapter.update({
|
|
579
528
|
model: "subscription",
|
|
580
529
|
update: {
|
|
581
530
|
plan: existingSub.pendingPlan,
|
|
@@ -610,7 +559,7 @@ const initializeTransactionBodySchema = z.object({
|
|
|
610
559
|
cancelAtPeriodEnd: z.boolean().optional(),
|
|
611
560
|
prorateAndCharge: z.boolean().optional()
|
|
612
561
|
});
|
|
613
|
-
const initializeTransaction = (options, path = "/
|
|
562
|
+
const initializeTransaction = (options, path = "/initialize-transaction") => {
|
|
614
563
|
const subscriptionOptions = options.subscription;
|
|
615
564
|
return createAuthEndpoint(path, {
|
|
616
565
|
method: "POST",
|
|
@@ -626,23 +575,22 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
626
575
|
if (callbackURL !== void 0 && callbackURL !== null && callbackURL !== "") {
|
|
627
576
|
const checkTrusted = () => {
|
|
628
577
|
try {
|
|
629
|
-
if (callbackURL === void 0 || callbackURL === null || callbackURL === "") return false;
|
|
630
578
|
if (callbackURL.startsWith("/")) return true;
|
|
631
579
|
const baseUrl = ctx.context?.baseURL ?? ctx.request?.url ?? "";
|
|
632
|
-
if (
|
|
580
|
+
if (baseUrl === "") return false;
|
|
633
581
|
const baseOrigin = new URL(baseUrl).origin;
|
|
634
582
|
return new URL(callbackURL).origin === baseOrigin;
|
|
635
583
|
} catch {
|
|
636
584
|
return false;
|
|
637
585
|
}
|
|
638
586
|
};
|
|
639
|
-
if (checkTrusted()
|
|
587
|
+
if (checkTrusted() === false) throw new APIError("FORBIDDEN", {
|
|
640
588
|
message: "callbackURL is not a trusted origin.",
|
|
641
589
|
status: 403
|
|
642
590
|
});
|
|
643
591
|
}
|
|
644
592
|
const session = await getSessionFromCtx(ctx);
|
|
645
|
-
if (
|
|
593
|
+
if (session === void 0 || session === null) throw new APIError("UNAUTHORIZED");
|
|
646
594
|
const user = session.user;
|
|
647
595
|
if (subscriptionOptions?.enabled === true && subscriptionOptions.requireEmailVerification === true && user.emailVerified !== true) throw new APIError("BAD_REQUEST", {
|
|
648
596
|
code: "EMAIL_VERIFICATION_REQUIRED",
|
|
@@ -653,7 +601,7 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
653
601
|
if (planName !== void 0 && planName !== null && planName !== "") {
|
|
654
602
|
if (subscriptionOptions?.enabled !== true) throw new APIError("BAD_REQUEST", { message: "Subscriptions are not enabled." });
|
|
655
603
|
plan = await getPlanByName(options, planName) ?? void 0;
|
|
656
|
-
if (plan ===
|
|
604
|
+
if (plan === void 0 || plan === null) try {
|
|
657
605
|
const nativePlan = await ctx.context.adapter.findOne({
|
|
658
606
|
model: "paystackPlan",
|
|
659
607
|
where: [{
|
|
@@ -669,15 +617,17 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
669
617
|
value: planName
|
|
670
618
|
}]
|
|
671
619
|
}) ?? void 0;
|
|
620
|
+
} catch {
|
|
621
|
+
plan = void 0;
|
|
672
622
|
}
|
|
673
|
-
if (plan ===
|
|
623
|
+
if (plan === void 0 || plan === null) throw new APIError("BAD_REQUEST", {
|
|
674
624
|
code: "SUBSCRIPTION_PLAN_NOT_FOUND",
|
|
675
625
|
message: PAYSTACK_ERROR_CODES.SUBSCRIPTION_PLAN_NOT_FOUND.message,
|
|
676
626
|
status: 400
|
|
677
627
|
});
|
|
678
628
|
} else if (productName !== void 0 && productName !== null && productName !== "") {
|
|
679
629
|
if (typeof productName === "string") {
|
|
680
|
-
product
|
|
630
|
+
product = await getProductByName(options, productName) ?? void 0;
|
|
681
631
|
product ??= await ctx.context.adapter.findOne({
|
|
682
632
|
model: "paystackProduct",
|
|
683
633
|
where: [{
|
|
@@ -686,19 +636,19 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
686
636
|
}]
|
|
687
637
|
}) ?? void 0;
|
|
688
638
|
}
|
|
689
|
-
if (product ===
|
|
639
|
+
if (product === void 0 || product === null) throw new APIError("BAD_REQUEST", {
|
|
690
640
|
message: `Product '${productName}' not found.`,
|
|
691
641
|
status: 400
|
|
692
642
|
});
|
|
693
|
-
} else if (bodyAmount === void 0 || bodyAmount === null
|
|
643
|
+
} else if (bodyAmount === void 0 || bodyAmount === null) throw new APIError("BAD_REQUEST", {
|
|
694
644
|
message: "Either 'plan', 'product', or 'amount' is required to initialize a transaction.",
|
|
695
645
|
status: 400
|
|
696
646
|
});
|
|
697
|
-
let amount = bodyAmount ?? product?.price;
|
|
698
|
-
const finalCurrency = currency ?? product?.currency ?? plan?.currency ?? "NGN";
|
|
647
|
+
let amount = bodyAmount ?? product?.price ?? product?.amount;
|
|
648
|
+
const finalCurrency = currency ?? product?.currency ?? product?.currency ?? plan?.currency ?? "NGN";
|
|
699
649
|
const referenceIdFromCtx = ctx.context.referenceId;
|
|
700
|
-
const referenceId = ctx.body.referenceId
|
|
701
|
-
if (plan && scheduleAtPeriodEnd === true) {
|
|
650
|
+
const referenceId = ctx.body.referenceId ?? referenceIdFromCtx ?? session.user.id;
|
|
651
|
+
if (plan !== void 0 && scheduleAtPeriodEnd === true) {
|
|
702
652
|
const existingSub = await getOrganizationSubscription(ctx, referenceId);
|
|
703
653
|
if (existingSub?.status === "active") {
|
|
704
654
|
await ctx.context.adapter.update({
|
|
@@ -740,7 +690,7 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
740
690
|
});
|
|
741
691
|
}
|
|
742
692
|
}
|
|
743
|
-
if (plan !==
|
|
693
|
+
if (plan !== void 0 && (plan.seatAmount !== void 0 || plan.seatPriceId !== void 0)) {
|
|
744
694
|
const members = await ctx.context.adapter.findMany({
|
|
745
695
|
model: "member",
|
|
746
696
|
where: [{
|
|
@@ -757,23 +707,23 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
757
707
|
let accessCode;
|
|
758
708
|
let trialStart;
|
|
759
709
|
let trialEnd;
|
|
760
|
-
if (plan?.freeTrial?.days !== void 0 && plan.freeTrial.days
|
|
710
|
+
if (plan?.freeTrial?.days !== void 0 && plan.freeTrial.days > 0) {
|
|
761
711
|
if ((await ctx.context.adapter.findMany({
|
|
762
712
|
model: "subscription",
|
|
763
713
|
where: [{
|
|
764
714
|
field: "referenceId",
|
|
765
715
|
value: referenceId
|
|
766
716
|
}]
|
|
767
|
-
}))?.some((sub) => sub.trialStart !== void 0 && sub.trialStart !== null || sub.trialEnd !== void 0 && sub.trialEnd !== null || sub.status === "trialing")
|
|
717
|
+
}))?.some((sub) => sub.trialStart !== void 0 && sub.trialStart !== null || sub.trialEnd !== void 0 && sub.trialEnd !== null || sub.status === "trialing") === false) {
|
|
768
718
|
trialStart = /* @__PURE__ */ new Date();
|
|
769
719
|
trialEnd = /* @__PURE__ */ new Date();
|
|
770
720
|
trialEnd.setDate(trialEnd.getDate() + plan.freeTrial.days);
|
|
771
721
|
}
|
|
772
722
|
}
|
|
773
723
|
try {
|
|
774
|
-
let targetEmail = email
|
|
724
|
+
let targetEmail = email ?? user.email;
|
|
775
725
|
let paystackCustomerCode = user.paystackCustomerCode;
|
|
776
|
-
if (options.organization?.enabled === true && referenceId !== void 0 && referenceId !== null && referenceId !==
|
|
726
|
+
if (options.organization?.enabled === true && referenceId !== void 0 && referenceId !== null && referenceId !== user.id) {
|
|
777
727
|
const org = await ctx.context.adapter.findOne({
|
|
778
728
|
model: "organization",
|
|
779
729
|
where: [{
|
|
@@ -782,8 +732,10 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
782
732
|
}]
|
|
783
733
|
});
|
|
784
734
|
if (org !== void 0 && org !== null) {
|
|
785
|
-
|
|
786
|
-
if (
|
|
735
|
+
const paystackOrg = org;
|
|
736
|
+
if (paystackOrg.paystackCustomerCode !== void 0 && paystackOrg.paystackCustomerCode !== null && paystackOrg.paystackCustomerCode !== "") paystackCustomerCode = paystackOrg.paystackCustomerCode;
|
|
737
|
+
const orgWithEmail = org;
|
|
738
|
+
if (orgWithEmail.email !== void 0 && orgWithEmail.email !== null && orgWithEmail.email !== "") targetEmail = orgWithEmail.email;
|
|
787
739
|
else {
|
|
788
740
|
const ownerMember = await ctx.context.adapter.findOne({
|
|
789
741
|
model: "member",
|
|
@@ -803,7 +755,7 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
803
755
|
value: ownerMember.userId
|
|
804
756
|
}]
|
|
805
757
|
});
|
|
806
|
-
if (ownerUser
|
|
758
|
+
if (ownerUser !== void 0 && ownerUser !== null && ownerUser.email !== void 0 && ownerUser.email !== null && ownerUser.email !== "") targetEmail = ownerUser.email;
|
|
807
759
|
}
|
|
808
760
|
}
|
|
809
761
|
}
|
|
@@ -811,102 +763,109 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
811
763
|
const metadata = JSON.stringify({
|
|
812
764
|
referenceId,
|
|
813
765
|
userId: user.id,
|
|
814
|
-
plan: plan
|
|
815
|
-
product: product
|
|
816
|
-
isTrial: trialStart !== void 0
|
|
817
|
-
trialEnd: trialEnd
|
|
766
|
+
plan: plan !== void 0 ? plan.name.toLowerCase() : void 0,
|
|
767
|
+
product: product !== void 0 ? product.name.toLowerCase() : void 0,
|
|
768
|
+
isTrial: trialStart !== void 0,
|
|
769
|
+
trialEnd: trialEnd !== void 0 ? trialEnd.toISOString() : void 0,
|
|
818
770
|
...extraMetadata
|
|
819
771
|
});
|
|
820
772
|
const initBody = {
|
|
821
773
|
email: targetEmail,
|
|
822
|
-
callback_url: callbackURL,
|
|
774
|
+
callback_url: callbackURL ?? void 0,
|
|
823
775
|
metadata,
|
|
824
776
|
currency: finalCurrency,
|
|
825
777
|
quantity
|
|
826
778
|
};
|
|
827
779
|
if (paystackCustomerCode !== void 0 && paystackCustomerCode !== null && paystackCustomerCode !== "") try {
|
|
828
780
|
const ops = getPaystackOps(options.paystackClient);
|
|
829
|
-
if (
|
|
781
|
+
if (ops !== void 0 && ops !== null && initBody.email !== "") await ops.customer?.update(paystackCustomerCode, { body: { email: initBody.email } });
|
|
830
782
|
} catch (_e) {}
|
|
831
|
-
if (plan !== void 0 &&
|
|
783
|
+
if (plan !== void 0 && prorateAndCharge === true) {
|
|
832
784
|
const existingSub = await getOrganizationSubscription(ctx, referenceId);
|
|
833
|
-
if (existingSub?.status === "active" && existingSub.paystackAuthorizationCode !== null && existingSub.paystackAuthorizationCode !== void 0 && existingSub.paystackSubscriptionCode !== null && existingSub.paystackSubscriptionCode !==
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
785
|
+
if (existingSub?.status === "active" && existingSub.paystackAuthorizationCode !== void 0 && existingSub.paystackAuthorizationCode !== null && existingSub.paystackAuthorizationCode !== "" && existingSub.paystackSubscriptionCode !== void 0 && existingSub.paystackSubscriptionCode !== null && existingSub.paystackSubscriptionCode !== "") {
|
|
786
|
+
if (existingSub.periodEnd !== void 0 && existingSub.periodEnd !== null && existingSub.periodStart !== void 0 && existingSub.periodStart !== null) {
|
|
787
|
+
const now = /* @__PURE__ */ new Date();
|
|
788
|
+
const periodEndLocal = new Date(existingSub.periodEnd);
|
|
789
|
+
const periodStartLocal = new Date(existingSub.periodStart);
|
|
790
|
+
const totalDays = Math.max(1, Math.ceil((periodEndLocal.getTime() - periodStartLocal.getTime()) / (1e3 * 60 * 60 * 24)));
|
|
791
|
+
const remainingDays = Math.max(0, Math.ceil((periodEndLocal.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24)));
|
|
792
|
+
let oldAmount = 0;
|
|
793
|
+
if (existingSub.plan !== "") {
|
|
794
|
+
const oldPlan = await getPlanByName(options, existingSub.plan) ?? await ctx.context.adapter.findOne({
|
|
795
|
+
model: "paystackPlan",
|
|
796
|
+
where: [{
|
|
797
|
+
field: "name",
|
|
798
|
+
value: existingSub.plan
|
|
799
|
+
}]
|
|
800
|
+
}) ?? void 0;
|
|
801
|
+
if (oldPlan !== void 0 && oldPlan !== null) {
|
|
802
|
+
const oldSeatCount = existingSub.seats;
|
|
803
|
+
oldAmount = (oldPlan.amount ?? 0) + oldSeatCount * (oldPlan.seatAmount ?? oldPlan.seatPriceId ?? 0);
|
|
804
|
+
}
|
|
851
805
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
authorization_code: existingSub.paystackAuthorizationCode,
|
|
874
|
-
reference: `prorate_${Date.now()}_${Math.random().toString(36).substring(7)}`,
|
|
875
|
-
metadata: {
|
|
876
|
-
type: "proration",
|
|
877
|
-
referenceId,
|
|
878
|
-
newPlan: plan.name,
|
|
879
|
-
oldPlan: existingSub.plan,
|
|
880
|
-
remainingDays
|
|
806
|
+
let membersCount = 1;
|
|
807
|
+
if (plan.seatAmount !== void 0 || plan.seatPriceId !== void 0) {
|
|
808
|
+
const members = await ctx.context.adapter.findMany({
|
|
809
|
+
model: "member",
|
|
810
|
+
where: [{
|
|
811
|
+
field: "organizationId",
|
|
812
|
+
value: referenceId
|
|
813
|
+
}]
|
|
814
|
+
});
|
|
815
|
+
membersCount = members.length > 0 ? members.length : 1;
|
|
816
|
+
}
|
|
817
|
+
const newSeatCount = quantity ?? existingSub.seats ?? membersCount;
|
|
818
|
+
const newAmount = (plan.amount ?? 0) + newSeatCount * (plan.seatAmount ?? plan.seatPriceId ?? 0);
|
|
819
|
+
const costDifference = newAmount - oldAmount;
|
|
820
|
+
if (costDifference > 0 && remainingDays > 0) {
|
|
821
|
+
const proratedAmount = Math.round(costDifference / totalDays * remainingDays);
|
|
822
|
+
if (proratedAmount >= 5e3) {
|
|
823
|
+
const ops = getPaystackOps(options.paystackClient);
|
|
824
|
+
if (ops === void 0 || ops === null) {
|
|
825
|
+
ctx.context.logger.error("Paystack client not configured for proration charge");
|
|
826
|
+
return;
|
|
881
827
|
}
|
|
882
|
-
|
|
828
|
+
if (unwrapSdkResult(await ops.transaction?.chargeAuthorization({ body: {
|
|
829
|
+
email: targetEmail,
|
|
830
|
+
amount: proratedAmount,
|
|
831
|
+
authorization_code: existingSub.paystackAuthorizationCode,
|
|
832
|
+
reference: `upg_${existingSub.id}_${Date.now()}_${Math.random().toString(36).substring(7)}`,
|
|
833
|
+
metadata: {
|
|
834
|
+
type: "proration",
|
|
835
|
+
referenceId,
|
|
836
|
+
newPlan: plan.name,
|
|
837
|
+
oldPlan: existingSub.plan,
|
|
838
|
+
remainingDays
|
|
839
|
+
}
|
|
840
|
+
} }))?.status !== "success") throw new APIError("BAD_REQUEST", { message: "Failed to process prorated charge via saved authorization." });
|
|
841
|
+
}
|
|
883
842
|
}
|
|
843
|
+
const ops = getPaystackOps(options.paystackClient);
|
|
844
|
+
if (ops !== void 0 && ops !== null) await ops.subscription?.update(existingSub.paystackSubscriptionCode, { body: {
|
|
845
|
+
amount: newAmount,
|
|
846
|
+
plan: plan.planCode
|
|
847
|
+
} });
|
|
848
|
+
await ctx.context.adapter.update({
|
|
849
|
+
model: "subscription",
|
|
850
|
+
where: [{
|
|
851
|
+
field: "id",
|
|
852
|
+
value: existingSub.id
|
|
853
|
+
}],
|
|
854
|
+
update: {
|
|
855
|
+
plan: plan.name,
|
|
856
|
+
seats: newSeatCount,
|
|
857
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
return ctx.json({
|
|
861
|
+
status: "success",
|
|
862
|
+
message: "Subscription successfully upgraded with prorated charge.",
|
|
863
|
+
prorated: true
|
|
864
|
+
});
|
|
884
865
|
}
|
|
885
|
-
await getPaystackOps(options.paystackClient).subscriptionUpdate({
|
|
886
|
-
code: existingSub.paystackSubscriptionCode,
|
|
887
|
-
amount: newAmount,
|
|
888
|
-
plan: plan.planCode
|
|
889
|
-
});
|
|
890
|
-
await ctx.context.adapter.update({
|
|
891
|
-
model: "subscription",
|
|
892
|
-
where: [{
|
|
893
|
-
field: "id",
|
|
894
|
-
value: existingSub.id
|
|
895
|
-
}],
|
|
896
|
-
update: {
|
|
897
|
-
plan: plan.name,
|
|
898
|
-
seats: newSeatCount,
|
|
899
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
900
|
-
}
|
|
901
|
-
});
|
|
902
|
-
return ctx.json({
|
|
903
|
-
status: "success",
|
|
904
|
-
message: "Subscription successfully upgraded with prorated charge.",
|
|
905
|
-
prorated: true
|
|
906
|
-
});
|
|
907
866
|
}
|
|
908
867
|
}
|
|
909
|
-
if (plan !== void 0
|
|
868
|
+
if (plan !== void 0) if (trialStart !== void 0) initBody.amount = 5e3;
|
|
910
869
|
else {
|
|
911
870
|
initBody.plan = plan.planCode;
|
|
912
871
|
initBody.invoice_limit = plan.invoiceLimit;
|
|
@@ -918,37 +877,37 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
918
877
|
initBody.amount = Math.max(Math.round(finalAmount), 5e3);
|
|
919
878
|
}
|
|
920
879
|
else {
|
|
921
|
-
if (amount === void 0 || amount === null
|
|
880
|
+
if (amount === void 0 || amount === null) throw new APIError("BAD_REQUEST", { message: "Amount is required for one-time payments" });
|
|
922
881
|
initBody.amount = Math.round(amount);
|
|
923
882
|
}
|
|
924
|
-
const sdkRes = unwrapSdkResult(await paystack
|
|
925
|
-
url = sdkRes?.authorization_url
|
|
926
|
-
reference = sdkRes?.reference
|
|
927
|
-
accessCode = sdkRes?.access_code
|
|
883
|
+
const sdkRes = unwrapSdkResult(await paystack?.transaction?.initialize({ body: initBody }));
|
|
884
|
+
url = sdkRes?.authorization_url;
|
|
885
|
+
reference = sdkRes?.reference;
|
|
886
|
+
accessCode = sdkRes?.access_code;
|
|
928
887
|
} catch (error) {
|
|
929
888
|
ctx.context.logger.error("Failed to initialize Paystack transaction", error);
|
|
930
889
|
throw new APIError("BAD_REQUEST", {
|
|
931
890
|
code: "FAILED_TO_INITIALIZE_TRANSACTION",
|
|
932
|
-
message: error
|
|
891
|
+
message: error instanceof Error ? error.message : PAYSTACK_ERROR_CODES.FAILED_TO_INITIALIZE_TRANSACTION.message
|
|
933
892
|
});
|
|
934
893
|
}
|
|
935
894
|
await ctx.context.adapter.create({
|
|
936
895
|
model: "paystackTransaction",
|
|
937
896
|
data: {
|
|
938
|
-
reference,
|
|
897
|
+
reference: reference ?? "",
|
|
939
898
|
referenceId,
|
|
940
899
|
userId: user.id,
|
|
941
900
|
amount: amount ?? 0,
|
|
942
901
|
currency: plan?.currency ?? currency ?? "NGN",
|
|
943
902
|
status: "pending",
|
|
944
|
-
plan: plan
|
|
945
|
-
product: product
|
|
946
|
-
metadata: extraMetadata !== void 0 &&
|
|
903
|
+
plan: plan !== void 0 ? plan.name.toLowerCase() : void 0,
|
|
904
|
+
product: product !== void 0 ? product.name.toLowerCase() : void 0,
|
|
905
|
+
metadata: extraMetadata !== void 0 && Object.keys(extraMetadata).length > 0 ? JSON.stringify(extraMetadata) : void 0,
|
|
947
906
|
createdAt: /* @__PURE__ */ new Date(),
|
|
948
907
|
updatedAt: /* @__PURE__ */ new Date()
|
|
949
908
|
}
|
|
950
909
|
});
|
|
951
|
-
if (plan !== void 0
|
|
910
|
+
if (plan !== void 0) {
|
|
952
911
|
let storedCustomerCode = user.paystackCustomerCode;
|
|
953
912
|
if (options.organization?.enabled === true && referenceId !== user.id) {
|
|
954
913
|
const org = await ctx.context.adapter.findOne({
|
|
@@ -958,22 +917,34 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
958
917
|
value: referenceId
|
|
959
918
|
}]
|
|
960
919
|
});
|
|
961
|
-
if (org
|
|
920
|
+
if (org !== void 0 && org !== null) {
|
|
921
|
+
const paystackOrg = org;
|
|
922
|
+
if (paystackOrg.paystackCustomerCode !== void 0 && paystackOrg.paystackCustomerCode !== null && paystackOrg.paystackCustomerCode !== "") storedCustomerCode = paystackOrg.paystackCustomerCode;
|
|
923
|
+
}
|
|
962
924
|
}
|
|
963
925
|
const newSubscription = await ctx.context.adapter.create({
|
|
964
926
|
model: "subscription",
|
|
965
927
|
data: {
|
|
966
928
|
plan: plan.name.toLowerCase(),
|
|
967
929
|
referenceId,
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
930
|
+
userId: user.id,
|
|
931
|
+
paystackCustomerCode: storedCustomerCode ?? "",
|
|
932
|
+
paystackSubscriptionCode: "",
|
|
933
|
+
paystackPlanCode: plan.planCode,
|
|
934
|
+
paystackAuthorizationCode: "",
|
|
935
|
+
paystackTransactionReference: reference ?? "",
|
|
936
|
+
status: trialStart !== void 0 ? "trialing" : "incomplete",
|
|
937
|
+
seats: quantity ?? 1,
|
|
938
|
+
periodStart: /* @__PURE__ */ new Date(),
|
|
939
|
+
periodEnd: new Date(Date.now() + 720 * 60 * 60 * 1e3),
|
|
940
|
+
cancelAtPeriodEnd: false,
|
|
972
941
|
trialStart,
|
|
973
|
-
trialEnd
|
|
942
|
+
trialEnd,
|
|
943
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
944
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
974
945
|
}
|
|
975
946
|
});
|
|
976
|
-
if (trialStart !== void 0 &&
|
|
947
|
+
if (trialStart !== void 0 && newSubscription !== void 0 && newSubscription !== null && plan.freeTrial?.onTrialStart !== void 0) await plan.freeTrial.onTrialStart(newSubscription);
|
|
977
948
|
}
|
|
978
949
|
return ctx.json({
|
|
979
950
|
url,
|
|
@@ -983,15 +954,11 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
983
954
|
});
|
|
984
955
|
});
|
|
985
956
|
};
|
|
986
|
-
const createSubscription = (options
|
|
987
|
-
const upgradeSubscription = (options
|
|
988
|
-
const
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
const cancelSubscription = (options) => {
|
|
992
|
-
return disablePaystackSubscription(options, "/paystack/cancel-subscription");
|
|
993
|
-
};
|
|
994
|
-
const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
|
|
957
|
+
const createSubscription = (options, path = "/create-subscription") => initializeTransaction(options, path);
|
|
958
|
+
const upgradeSubscription = (options, path = "/upgrade-subscription") => initializeTransaction(options, path);
|
|
959
|
+
const cancelSubscription = (options, path = "/cancel-subscription") => disablePaystackSubscription(options, path);
|
|
960
|
+
const restoreSubscription = (options, path = "/restore-subscription") => enablePaystackSubscription(options, path);
|
|
961
|
+
const verifyTransaction = (options, path = "/verify-transaction") => {
|
|
995
962
|
const verifyBodySchema = z.object({ reference: z.string() });
|
|
996
963
|
const subscriptionOptions = options.subscription;
|
|
997
964
|
return createAuthEndpoint(path, {
|
|
@@ -1004,56 +971,55 @@ const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
|
|
|
1004
971
|
] : [sessionMiddleware, originCheck]
|
|
1005
972
|
}, async (ctx) => {
|
|
1006
973
|
const paystack = getPaystackOps(options.paystackClient);
|
|
1007
|
-
let
|
|
974
|
+
let data;
|
|
1008
975
|
try {
|
|
1009
|
-
|
|
976
|
+
data = unwrapSdkResult(await paystack?.transaction?.verify(ctx.body.reference));
|
|
1010
977
|
} catch (error) {
|
|
1011
978
|
ctx.context.logger.error("Failed to verify Paystack transaction", error);
|
|
1012
979
|
throw new APIError("BAD_REQUEST", {
|
|
1013
980
|
code: "FAILED_TO_VERIFY_TRANSACTION",
|
|
1014
|
-
message: error
|
|
981
|
+
message: error instanceof Error ? error.message : PAYSTACK_ERROR_CODES.FAILED_TO_VERIFY_TRANSACTION.message
|
|
1015
982
|
});
|
|
1016
983
|
}
|
|
1017
|
-
|
|
1018
|
-
const
|
|
1019
|
-
const
|
|
1020
|
-
const
|
|
1021
|
-
const paystackId =
|
|
1022
|
-
const authorizationCode =
|
|
984
|
+
if (data === void 0 || data === null) throw new APIError("BAD_REQUEST", { message: "Failed to fetch transaction data from Paystack." });
|
|
985
|
+
const status = data.status ?? "failed";
|
|
986
|
+
const reference = data.reference ?? ctx.body.reference;
|
|
987
|
+
const paystackIdRaw = data.id;
|
|
988
|
+
const paystackId = paystackIdRaw !== void 0 && paystackIdRaw !== null ? String(paystackIdRaw) : void 0;
|
|
989
|
+
const authorizationCode = data.authorization?.authorization_code;
|
|
1023
990
|
if (status === "success") {
|
|
1024
991
|
const session = await getSessionFromCtx(ctx);
|
|
1025
|
-
const
|
|
992
|
+
const txRecord = await ctx.context.adapter.findOne({
|
|
1026
993
|
model: "paystackTransaction",
|
|
1027
994
|
where: [{
|
|
1028
995
|
field: "reference",
|
|
1029
996
|
value: reference
|
|
1030
997
|
}]
|
|
1031
|
-
})
|
|
1032
|
-
|
|
998
|
+
});
|
|
999
|
+
const referenceId = txRecord !== void 0 && txRecord !== null && txRecord.referenceId !== void 0 && txRecord.referenceId !== null && txRecord.referenceId !== "" ? txRecord.referenceId : session !== void 0 && session !== null ? session.user.id : void 0;
|
|
1000
|
+
if (session !== void 0 && session !== null && referenceId !== void 0 && referenceId !== null && referenceId !== "" && referenceId !== session.user.id) {
|
|
1033
1001
|
const authRef = subscriptionOptions?.authorizeReference;
|
|
1034
1002
|
let authorized = false;
|
|
1035
|
-
if (authRef !== void 0) authorized = await authRef({
|
|
1003
|
+
if (authRef !== void 0 && authRef !== null) authorized = await authRef({
|
|
1036
1004
|
user: session.user,
|
|
1037
|
-
session,
|
|
1005
|
+
session: session.session,
|
|
1038
1006
|
referenceId,
|
|
1039
1007
|
action: "verify-transaction"
|
|
1040
1008
|
}, ctx);
|
|
1041
|
-
if (authorized
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
if (member !== void 0 && member !== null) authorized = true;
|
|
1054
|
-
}
|
|
1009
|
+
if (authorized === false && options.organization?.enabled === true) {
|
|
1010
|
+
const member = await ctx.context.adapter.findOne({
|
|
1011
|
+
model: "member",
|
|
1012
|
+
where: [{
|
|
1013
|
+
field: "userId",
|
|
1014
|
+
value: session.user.id
|
|
1015
|
+
}, {
|
|
1016
|
+
field: "organizationId",
|
|
1017
|
+
value: referenceId
|
|
1018
|
+
}]
|
|
1019
|
+
});
|
|
1020
|
+
if (member !== void 0 && member !== null) authorized = true;
|
|
1055
1021
|
}
|
|
1056
|
-
if (
|
|
1022
|
+
if (authorized === false) throw new APIError("UNAUTHORIZED");
|
|
1057
1023
|
}
|
|
1058
1024
|
try {
|
|
1059
1025
|
await ctx.context.adapter.update({
|
|
@@ -1061,8 +1027,8 @@ const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
|
|
|
1061
1027
|
update: {
|
|
1062
1028
|
status: "success",
|
|
1063
1029
|
paystackId,
|
|
1064
|
-
|
|
1065
|
-
|
|
1030
|
+
amount: data.amount,
|
|
1031
|
+
currency: data.currency,
|
|
1066
1032
|
updatedAt: /* @__PURE__ */ new Date()
|
|
1067
1033
|
},
|
|
1068
1034
|
where: [{
|
|
@@ -1070,11 +1036,10 @@ const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
|
|
|
1070
1036
|
value: reference
|
|
1071
1037
|
}]
|
|
1072
1038
|
});
|
|
1073
|
-
const
|
|
1074
|
-
const paystackCustomerCodeFromPaystack = customer !== void 0 && customer !== null && typeof customer === "object" ? customer.customer_code : void 0;
|
|
1039
|
+
const paystackCustomerCodeFromPaystack = data.customer?.customer_code;
|
|
1075
1040
|
if (paystackCustomerCodeFromPaystack !== void 0 && paystackCustomerCodeFromPaystack !== null && paystackCustomerCodeFromPaystack !== "" && referenceId !== void 0 && referenceId !== null && referenceId !== "") {
|
|
1076
1041
|
let isOrg = options.organization?.enabled === true && typeof referenceId === "string" && referenceId.startsWith("org_");
|
|
1077
|
-
if (
|
|
1042
|
+
if (isOrg === false && options.organization?.enabled === true) {
|
|
1078
1043
|
const org = await ctx.context.adapter.findOne({
|
|
1079
1044
|
model: "organization",
|
|
1080
1045
|
where: [{
|
|
@@ -1082,9 +1047,9 @@ const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
|
|
|
1082
1047
|
value: referenceId
|
|
1083
1048
|
}]
|
|
1084
1049
|
});
|
|
1085
|
-
isOrg = org !==
|
|
1050
|
+
isOrg = org !== void 0 && org !== null;
|
|
1086
1051
|
}
|
|
1087
|
-
if (isOrg
|
|
1052
|
+
if (isOrg) await ctx.context.adapter.update({
|
|
1088
1053
|
model: "organization",
|
|
1089
1054
|
update: { paystackCustomerCode: paystackCustomerCodeFromPaystack },
|
|
1090
1055
|
where: [{
|
|
@@ -1108,45 +1073,39 @@ const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
|
|
|
1108
1073
|
value: reference
|
|
1109
1074
|
}]
|
|
1110
1075
|
});
|
|
1111
|
-
if (transaction
|
|
1076
|
+
if (transaction !== void 0 && transaction !== null && transaction.product !== void 0 && transaction.product !== null && transaction.product !== "" && options.paystackClient !== void 0 && options.paystackClient !== null) await syncProductQuantityFromPaystack(ctx, transaction.product, options.paystackClient);
|
|
1112
1077
|
let isTrial = false;
|
|
1113
1078
|
let trialEnd;
|
|
1114
1079
|
let targetPlan;
|
|
1115
|
-
if (data
|
|
1116
|
-
const
|
|
1117
|
-
const meta = typeof metaRaw === "string" ? JSON.parse(metaRaw) : metaRaw;
|
|
1080
|
+
if (data.metadata !== void 0 && data.metadata !== null && data.metadata !== "") {
|
|
1081
|
+
const meta = typeof data.metadata === "string" ? JSON.parse(data.metadata) : data.metadata;
|
|
1118
1082
|
isTrial = meta.isTrial === true || meta.isTrial === "true";
|
|
1119
1083
|
trialEnd = meta.trialEnd;
|
|
1120
1084
|
targetPlan = meta.plan;
|
|
1121
1085
|
}
|
|
1122
1086
|
let paystackSubscriptionCode;
|
|
1123
|
-
if (isTrial
|
|
1087
|
+
if (isTrial && targetPlan !== void 0 && trialEnd !== void 0) {
|
|
1124
1088
|
const email = data.customer?.email;
|
|
1125
1089
|
const planConfig = (await getPlans(subscriptionOptions)).find((p) => p.name.toLowerCase() === targetPlan?.toLowerCase());
|
|
1126
1090
|
if (planConfig !== void 0 && planConfig !== null && (planConfig.planCode === void 0 || planConfig.planCode === null || planConfig.planCode === "")) paystackSubscriptionCode = `LOC_${reference}`;
|
|
1127
|
-
if (authorizationCode !== void 0 && authorizationCode !== null &&
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
}
|
|
1136
|
-
} else if (isTrial !== true) {
|
|
1137
|
-
const planCodeFromPaystack = (data?.plan)?.plan_code;
|
|
1091
|
+
if (authorizationCode !== void 0 && authorizationCode !== null && email !== void 0 && email !== null && email !== "" && planConfig?.planCode !== void 0 && planConfig.planCode !== null && planConfig.planCode !== "") paystackSubscriptionCode = unwrapSdkResult(await paystack?.subscription?.create({ body: {
|
|
1092
|
+
customer: email,
|
|
1093
|
+
plan: planConfig.planCode,
|
|
1094
|
+
authorization: authorizationCode,
|
|
1095
|
+
start_date: trialEnd
|
|
1096
|
+
} }))?.subscription_code;
|
|
1097
|
+
} else if (isTrial === false) {
|
|
1098
|
+
const planCodeFromPaystack = data.plan?.plan_code;
|
|
1138
1099
|
if (planCodeFromPaystack === void 0 || planCodeFromPaystack === null || planCodeFromPaystack === "") paystackSubscriptionCode = `LOC_${reference}`;
|
|
1139
|
-
else paystackSubscriptionCode =
|
|
1100
|
+
else paystackSubscriptionCode = data.subscription?.subscription_code ?? void 0;
|
|
1140
1101
|
}
|
|
1141
|
-
const
|
|
1102
|
+
const targetSub = (await ctx.context.adapter.findMany({
|
|
1142
1103
|
model: "subscription",
|
|
1143
1104
|
where: [{
|
|
1144
1105
|
field: "paystackTransactionReference",
|
|
1145
1106
|
value: reference
|
|
1146
1107
|
}]
|
|
1147
|
-
});
|
|
1148
|
-
let targetSub;
|
|
1149
|
-
if (existingSubs !== null && existingSubs !== void 0 && existingSubs.length > 0) targetSub = existingSubs.find((s) => referenceId === void 0 || referenceId === null || referenceId === "" || s.referenceId === referenceId);
|
|
1108
|
+
}))?.find((s) => referenceId === void 0 || referenceId === null || referenceId === "" || s.referenceId === referenceId);
|
|
1150
1109
|
let updatedSubscription = null;
|
|
1151
1110
|
if (targetSub !== void 0 && targetSub !== null) updatedSubscription = await ctx.context.adapter.update({
|
|
1152
1111
|
model: "subscription",
|
|
@@ -1154,22 +1113,22 @@ const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
|
|
|
1154
1113
|
status: isTrial ? "trialing" : "active",
|
|
1155
1114
|
periodStart: /* @__PURE__ */ new Date(),
|
|
1156
1115
|
updatedAt: /* @__PURE__ */ new Date(),
|
|
1157
|
-
...isTrial
|
|
1116
|
+
...isTrial && trialEnd !== void 0 ? {
|
|
1158
1117
|
trialStart: /* @__PURE__ */ new Date(),
|
|
1159
1118
|
trialEnd: new Date(trialEnd),
|
|
1160
1119
|
periodEnd: new Date(trialEnd)
|
|
1161
1120
|
} : {},
|
|
1162
|
-
...paystackSubscriptionCode !== void 0
|
|
1163
|
-
...authorizationCode !== void 0 && authorizationCode !== null
|
|
1121
|
+
...paystackSubscriptionCode !== void 0 ? { paystackSubscriptionCode } : {},
|
|
1122
|
+
...authorizationCode !== void 0 && authorizationCode !== null ? { paystackAuthorizationCode: authorizationCode } : {}
|
|
1164
1123
|
},
|
|
1165
1124
|
where: [{
|
|
1166
1125
|
field: "id",
|
|
1167
1126
|
value: targetSub.id
|
|
1168
1127
|
}]
|
|
1169
1128
|
});
|
|
1170
|
-
if (updatedSubscription
|
|
1129
|
+
if (updatedSubscription !== void 0 && updatedSubscription !== null && subscriptionOptions?.onSubscriptionComplete !== void 0) {
|
|
1171
1130
|
const plan = (await getPlans(subscriptionOptions)).find((p) => p.name.toLowerCase() === updatedSubscription.plan.toLowerCase());
|
|
1172
|
-
if (plan) await subscriptionOptions.onSubscriptionComplete({
|
|
1131
|
+
if (plan !== void 0) await subscriptionOptions.onSubscriptionComplete({
|
|
1173
1132
|
event: data,
|
|
1174
1133
|
subscription: updatedSubscription,
|
|
1175
1134
|
plan
|
|
@@ -1186,10 +1145,10 @@ const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
|
|
|
1186
1145
|
});
|
|
1187
1146
|
});
|
|
1188
1147
|
};
|
|
1189
|
-
const listSubscriptions = (options) => {
|
|
1148
|
+
const listSubscriptions = (options, path = "/list-subscriptions") => {
|
|
1190
1149
|
const listQuerySchema = z.object({ referenceId: z.string().optional() });
|
|
1191
1150
|
const subscriptionOptions = options.subscription;
|
|
1192
|
-
return createAuthEndpoint(
|
|
1151
|
+
return createAuthEndpoint(path, {
|
|
1193
1152
|
method: "GET",
|
|
1194
1153
|
query: listQuerySchema,
|
|
1195
1154
|
use: subscriptionOptions?.enabled === true ? [
|
|
@@ -1200,7 +1159,7 @@ const listSubscriptions = (options) => {
|
|
|
1200
1159
|
}, async (ctx) => {
|
|
1201
1160
|
if (subscriptionOptions?.enabled !== true) throw new APIError("BAD_REQUEST", { message: "Subscriptions are not enabled in the Paystack options." });
|
|
1202
1161
|
const session = await getSessionFromCtx(ctx);
|
|
1203
|
-
if (
|
|
1162
|
+
if (session === void 0 || session === null) throw new APIError("UNAUTHORIZED");
|
|
1204
1163
|
const referenceIdPart = ctx.context.referenceId;
|
|
1205
1164
|
const queryRefId = ctx.query?.referenceId;
|
|
1206
1165
|
const referenceId = referenceIdPart ?? queryRefId ?? session.user.id;
|
|
@@ -1214,7 +1173,7 @@ const listSubscriptions = (options) => {
|
|
|
1214
1173
|
return ctx.json({ subscriptions: res });
|
|
1215
1174
|
});
|
|
1216
1175
|
};
|
|
1217
|
-
const listTransactions = (options, path = "/
|
|
1176
|
+
const listTransactions = (options, path = "/list-transactions") => {
|
|
1218
1177
|
return createAuthEndpoint(path, {
|
|
1219
1178
|
method: "GET",
|
|
1220
1179
|
query: z.object({ referenceId: z.string().optional() }),
|
|
@@ -1225,15 +1184,17 @@ const listTransactions = (options, path = "/paystack/list-transactions") => {
|
|
|
1225
1184
|
] : [sessionMiddleware, originCheck]
|
|
1226
1185
|
}, async (ctx) => {
|
|
1227
1186
|
const session = await getSessionFromCtx(ctx);
|
|
1228
|
-
if (
|
|
1229
|
-
const
|
|
1187
|
+
if (session === void 0 || session === null) throw new APIError("UNAUTHORIZED");
|
|
1188
|
+
const referenceIdPart = ctx.context.referenceId;
|
|
1189
|
+
const queryRefId = ctx.query?.referenceId;
|
|
1190
|
+
const referenceId = referenceIdPart ?? queryRefId ?? session.user.id;
|
|
1230
1191
|
const sorted = (await ctx.context.adapter.findMany({
|
|
1231
1192
|
model: "paystackTransaction",
|
|
1232
1193
|
where: [{
|
|
1233
1194
|
field: "referenceId",
|
|
1234
1195
|
value: referenceId
|
|
1235
1196
|
}]
|
|
1236
|
-
})).sort((a, b) =>
|
|
1197
|
+
})).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
1237
1198
|
return ctx.json({ transactions: sorted });
|
|
1238
1199
|
});
|
|
1239
1200
|
};
|
|
@@ -1246,8 +1207,10 @@ const enableDisableBodySchema = z.object({
|
|
|
1246
1207
|
function decodeBase64UrlToString(value) {
|
|
1247
1208
|
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
1248
1209
|
const padded = normalized + "===".slice((normalized.length + 3) % 4);
|
|
1249
|
-
|
|
1250
|
-
|
|
1210
|
+
const binaryString = atob(padded);
|
|
1211
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
1212
|
+
for (let i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i);
|
|
1213
|
+
return new TextDecoder().decode(bytes);
|
|
1251
1214
|
}
|
|
1252
1215
|
function tryGetEmailTokenFromSubscriptionManageLink(link) {
|
|
1253
1216
|
try {
|
|
@@ -1257,12 +1220,12 @@ function tryGetEmailTokenFromSubscriptionManageLink(link) {
|
|
|
1257
1220
|
if (parts.length < 2) return void 0;
|
|
1258
1221
|
const payloadJson = decodeBase64UrlToString(parts[1]);
|
|
1259
1222
|
const payload = JSON.parse(payloadJson);
|
|
1260
|
-
return typeof payload
|
|
1223
|
+
return typeof payload.email_token === "string" ? payload.email_token : void 0;
|
|
1261
1224
|
} catch {
|
|
1262
1225
|
return;
|
|
1263
1226
|
}
|
|
1264
1227
|
}
|
|
1265
|
-
const disablePaystackSubscription = (options, path = "/
|
|
1228
|
+
const disablePaystackSubscription = (options, path = "/disable-subscription") => {
|
|
1266
1229
|
return createAuthEndpoint(path, {
|
|
1267
1230
|
method: "POST",
|
|
1268
1231
|
body: enableDisableBodySchema,
|
|
@@ -1275,7 +1238,7 @@ const disablePaystackSubscription = (options, path = "/paystack/disable-subscrip
|
|
|
1275
1238
|
const { subscriptionCode, atPeriodEnd } = ctx.body;
|
|
1276
1239
|
const paystack = getPaystackOps(options.paystackClient);
|
|
1277
1240
|
try {
|
|
1278
|
-
if (subscriptionCode.startsWith("LOC_")) {
|
|
1241
|
+
if (subscriptionCode.startsWith("LOC_") || subscriptionCode.startsWith("sub_local_")) {
|
|
1279
1242
|
const sub = await ctx.context.adapter.findOne({
|
|
1280
1243
|
model: "subscription",
|
|
1281
1244
|
where: [{
|
|
@@ -1283,7 +1246,7 @@ const disablePaystackSubscription = (options, path = "/paystack/disable-subscrip
|
|
|
1283
1246
|
value: subscriptionCode
|
|
1284
1247
|
}]
|
|
1285
1248
|
});
|
|
1286
|
-
if (sub
|
|
1249
|
+
if (sub) {
|
|
1287
1250
|
await ctx.context.adapter.update({
|
|
1288
1251
|
model: "subscription",
|
|
1289
1252
|
update: {
|
|
@@ -1303,22 +1266,21 @@ const disablePaystackSubscription = (options, path = "/paystack/disable-subscrip
|
|
|
1303
1266
|
let emailToken = ctx.body.emailToken;
|
|
1304
1267
|
let nextPaymentDate;
|
|
1305
1268
|
try {
|
|
1306
|
-
const fetchRes = unwrapSdkResult(await paystack
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1269
|
+
const fetchRes = unwrapSdkResult(await paystack?.subscription?.fetch(subscriptionCode));
|
|
1270
|
+
if (fetchRes !== void 0 && fetchRes !== null) {
|
|
1271
|
+
emailToken ??= fetchRes.email_token ?? void 0;
|
|
1272
|
+
nextPaymentDate = fetchRes.next_payment_date ?? void 0;
|
|
1273
|
+
}
|
|
1310
1274
|
} catch {}
|
|
1311
1275
|
if (emailToken === void 0 || emailToken === null || emailToken === "") try {
|
|
1312
|
-
const
|
|
1313
|
-
|
|
1314
|
-
const link = typeof data === "string" ? data : data.link;
|
|
1315
|
-
if (typeof link === "string" && link !== "") emailToken = tryGetEmailTokenFromSubscriptionManageLink(link);
|
|
1276
|
+
const link = unwrapSdkResult(await paystack?.subscription?.manageLink(subscriptionCode))?.link;
|
|
1277
|
+
if (link !== void 0 && link !== null && link !== "") emailToken = tryGetEmailTokenFromSubscriptionManageLink(link);
|
|
1316
1278
|
} catch {}
|
|
1317
1279
|
if (emailToken === void 0 || emailToken === null || emailToken === "") throw new Error("Could not retrieve email_token for subscription disable.");
|
|
1318
|
-
await paystack
|
|
1280
|
+
await paystack?.subscription?.disable({ body: {
|
|
1319
1281
|
code: subscriptionCode,
|
|
1320
1282
|
token: emailToken
|
|
1321
|
-
});
|
|
1283
|
+
} });
|
|
1322
1284
|
const periodEnd = nextPaymentDate !== void 0 && nextPaymentDate !== null && nextPaymentDate !== "" ? new Date(nextPaymentDate) : void 0;
|
|
1323
1285
|
const sub = await ctx.context.adapter.findOne({
|
|
1324
1286
|
model: "subscription",
|
|
@@ -1346,12 +1308,12 @@ const disablePaystackSubscription = (options, path = "/paystack/disable-subscrip
|
|
|
1346
1308
|
ctx.context.logger.error("Failed to disable subscription", error);
|
|
1347
1309
|
throw new APIError("BAD_REQUEST", {
|
|
1348
1310
|
code: "FAILED_TO_DISABLE_SUBSCRIPTION",
|
|
1349
|
-
message: error
|
|
1311
|
+
message: error instanceof Error ? error.message : PAYSTACK_ERROR_CODES.FAILED_TO_DISABLE_SUBSCRIPTION.message
|
|
1350
1312
|
});
|
|
1351
1313
|
}
|
|
1352
1314
|
});
|
|
1353
1315
|
};
|
|
1354
|
-
const enablePaystackSubscription = (options, path = "/
|
|
1316
|
+
const enablePaystackSubscription = (options, path = "/enable-subscription") => {
|
|
1355
1317
|
return createAuthEndpoint(path, {
|
|
1356
1318
|
method: "POST",
|
|
1357
1319
|
body: enableDisableBodySchema,
|
|
@@ -1366,20 +1328,18 @@ const enablePaystackSubscription = (options, path = "/paystack/enable-subscripti
|
|
|
1366
1328
|
try {
|
|
1367
1329
|
let emailToken = ctx.body.emailToken;
|
|
1368
1330
|
if (emailToken === void 0 || emailToken === null || emailToken === "") try {
|
|
1369
|
-
const fetchRes = unwrapSdkResult(await paystack
|
|
1370
|
-
emailToken =
|
|
1331
|
+
const fetchRes = unwrapSdkResult(await paystack?.subscription?.fetch(subscriptionCode));
|
|
1332
|
+
if (fetchRes !== void 0 && fetchRes !== null) emailToken = fetchRes.email_token ?? void 0;
|
|
1371
1333
|
} catch {}
|
|
1372
1334
|
if (emailToken === void 0 || emailToken === null || emailToken === "") try {
|
|
1373
|
-
const
|
|
1374
|
-
|
|
1375
|
-
const link = typeof data === "string" ? data : data.link;
|
|
1376
|
-
if (typeof link === "string" && link !== "") emailToken = tryGetEmailTokenFromSubscriptionManageLink(link);
|
|
1335
|
+
const link = unwrapSdkResult(await paystack?.subscription?.manageLink(subscriptionCode))?.link;
|
|
1336
|
+
if (link !== void 0 && link !== null && link !== "") emailToken = tryGetEmailTokenFromSubscriptionManageLink(link);
|
|
1377
1337
|
} catch {}
|
|
1378
1338
|
if (emailToken === void 0 || emailToken === null || emailToken === "") throw new APIError("BAD_REQUEST", { message: "Could not retrieve email_token for subscription enable." });
|
|
1379
|
-
await paystack
|
|
1339
|
+
await paystack?.subscription?.enable({ body: {
|
|
1380
1340
|
code: subscriptionCode,
|
|
1381
1341
|
token: emailToken
|
|
1382
|
-
});
|
|
1342
|
+
} });
|
|
1383
1343
|
await ctx.context.adapter.update({
|
|
1384
1344
|
model: "subscription",
|
|
1385
1345
|
update: {
|
|
@@ -1396,12 +1356,12 @@ const enablePaystackSubscription = (options, path = "/paystack/enable-subscripti
|
|
|
1396
1356
|
ctx.context.logger.error("Failed to enable subscription", error);
|
|
1397
1357
|
throw new APIError("BAD_REQUEST", {
|
|
1398
1358
|
code: "FAILED_TO_ENABLE_SUBSCRIPTION",
|
|
1399
|
-
message: error
|
|
1359
|
+
message: error instanceof Error ? error.message : PAYSTACK_ERROR_CODES.FAILED_TO_ENABLE_SUBSCRIPTION.message
|
|
1400
1360
|
});
|
|
1401
1361
|
}
|
|
1402
1362
|
});
|
|
1403
1363
|
};
|
|
1404
|
-
const getSubscriptionManageLink = (options, path = "/
|
|
1364
|
+
const getSubscriptionManageLink = (options, path = "/subscription-manage-link") => {
|
|
1405
1365
|
const manageLinkQuerySchema = z.object({ subscriptionCode: z.string() });
|
|
1406
1366
|
const useMiddlewares = options.subscription?.enabled === true ? [
|
|
1407
1367
|
sessionMiddleware,
|
|
@@ -1416,13 +1376,11 @@ const getSubscriptionManageLink = (options, path = "/paystack/get-subscription-m
|
|
|
1416
1376
|
});
|
|
1417
1377
|
const paystack = getPaystackOps(options.paystackClient);
|
|
1418
1378
|
try {
|
|
1419
|
-
const res = unwrapSdkResult(await paystack
|
|
1420
|
-
|
|
1421
|
-
const link = typeof data === "string" ? data : data.link;
|
|
1422
|
-
return ctx.json({ link: typeof link === "string" ? link : null });
|
|
1379
|
+
const res = unwrapSdkResult(await paystack?.subscription?.manageLink(subscriptionCode));
|
|
1380
|
+
return ctx.json({ link: res?.link || null });
|
|
1423
1381
|
} catch (error) {
|
|
1424
1382
|
ctx.context.logger.error("Failed to get subscription manage link", error);
|
|
1425
|
-
throw new APIError("BAD_REQUEST", { message: error
|
|
1383
|
+
throw new APIError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Failed to get subscription manage link" });
|
|
1426
1384
|
}
|
|
1427
1385
|
};
|
|
1428
1386
|
return createAuthEndpoint(path, {
|
|
@@ -1431,69 +1389,8 @@ const getSubscriptionManageLink = (options, path = "/paystack/get-subscription-m
|
|
|
1431
1389
|
use: useMiddlewares
|
|
1432
1390
|
}, handler);
|
|
1433
1391
|
};
|
|
1434
|
-
const
|
|
1435
|
-
return createAuthEndpoint(
|
|
1436
|
-
method: "POST",
|
|
1437
|
-
metadata: { ...HIDE_METADATA },
|
|
1438
|
-
disableBody: true,
|
|
1439
|
-
use: [sessionMiddleware]
|
|
1440
|
-
}, async (ctx) => {
|
|
1441
|
-
const paystack = getPaystackOps(options.paystackClient);
|
|
1442
|
-
try {
|
|
1443
|
-
const dataRaw = unwrapSdkResult(await paystack.productList());
|
|
1444
|
-
const productsDataRaw = dataRaw?.data ?? dataRaw;
|
|
1445
|
-
if (!Array.isArray(productsDataRaw)) return ctx.json({ products: [] });
|
|
1446
|
-
const productsData = productsDataRaw;
|
|
1447
|
-
for (const productRaw of productsData) {
|
|
1448
|
-
const product = productRaw;
|
|
1449
|
-
const paystackId = String(product.id);
|
|
1450
|
-
const existing = await ctx.context.adapter.findOne({
|
|
1451
|
-
model: "paystackProduct",
|
|
1452
|
-
where: [{
|
|
1453
|
-
field: "paystackId",
|
|
1454
|
-
value: paystackId
|
|
1455
|
-
}]
|
|
1456
|
-
});
|
|
1457
|
-
const productFields = {
|
|
1458
|
-
name: typeof product.name === "string" ? product.name : "",
|
|
1459
|
-
description: typeof product.description === "string" ? product.description : "",
|
|
1460
|
-
price: typeof product.price === "number" ? product.price : 0,
|
|
1461
|
-
currency: typeof product.currency === "string" ? product.currency : "",
|
|
1462
|
-
quantity: typeof product.quantity === "number" ? product.quantity : 0,
|
|
1463
|
-
unlimited: product.unlimited === true,
|
|
1464
|
-
paystackId,
|
|
1465
|
-
slug: typeof product.slug === "string" && product.slug !== "" ? product.slug : typeof product.name === "string" ? product.name.toLowerCase().replace(/\s+/g, "-") : "",
|
|
1466
|
-
metadata: product.metadata !== void 0 && product.metadata !== null ? JSON.stringify(product.metadata) : void 0,
|
|
1467
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
1468
|
-
};
|
|
1469
|
-
if (existing !== null && existing !== void 0) await ctx.context.adapter.update({
|
|
1470
|
-
model: "paystackProduct",
|
|
1471
|
-
update: productFields,
|
|
1472
|
-
where: [{
|
|
1473
|
-
field: "id",
|
|
1474
|
-
value: existing.id
|
|
1475
|
-
}]
|
|
1476
|
-
});
|
|
1477
|
-
else await ctx.context.adapter.create({
|
|
1478
|
-
model: "paystackProduct",
|
|
1479
|
-
data: {
|
|
1480
|
-
...productFields,
|
|
1481
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
1482
|
-
}
|
|
1483
|
-
});
|
|
1484
|
-
}
|
|
1485
|
-
return ctx.json({
|
|
1486
|
-
status: "success",
|
|
1487
|
-
count: productsData.length
|
|
1488
|
-
});
|
|
1489
|
-
} catch (error) {
|
|
1490
|
-
ctx.context.logger.error("Failed to sync products", error);
|
|
1491
|
-
throw new APIError("BAD_REQUEST", { message: error?.message ?? "Failed to sync products" });
|
|
1492
|
-
}
|
|
1493
|
-
});
|
|
1494
|
-
};
|
|
1495
|
-
const listProducts = (_options) => {
|
|
1496
|
-
return createAuthEndpoint("/paystack/list-products", {
|
|
1392
|
+
const listProducts = (_options, path = "/list-products") => {
|
|
1393
|
+
return createAuthEndpoint(path, {
|
|
1497
1394
|
method: "GET",
|
|
1498
1395
|
metadata: { openapi: { operationId: "listPaystackProducts" } }
|
|
1499
1396
|
}, async (ctx) => {
|
|
@@ -1501,69 +1398,8 @@ const listProducts = (_options) => {
|
|
|
1501
1398
|
return ctx.json({ products: sorted });
|
|
1502
1399
|
});
|
|
1503
1400
|
};
|
|
1504
|
-
const
|
|
1505
|
-
return createAuthEndpoint(
|
|
1506
|
-
method: "POST",
|
|
1507
|
-
metadata: { ...HIDE_METADATA },
|
|
1508
|
-
disableBody: true,
|
|
1509
|
-
use: [sessionMiddleware]
|
|
1510
|
-
}, async (ctx) => {
|
|
1511
|
-
const paystack = getPaystackOps(options.paystackClient);
|
|
1512
|
-
try {
|
|
1513
|
-
const res = unwrapSdkResult(await paystack.planList());
|
|
1514
|
-
const plansData = res?.data ?? res;
|
|
1515
|
-
if (!Array.isArray(plansData)) return ctx.json({
|
|
1516
|
-
status: "success",
|
|
1517
|
-
count: 0
|
|
1518
|
-
});
|
|
1519
|
-
for (const plan of plansData) {
|
|
1520
|
-
const paystackId = String(plan.id);
|
|
1521
|
-
const existing = await ctx.context.adapter.findOne({
|
|
1522
|
-
model: "paystackPlan",
|
|
1523
|
-
where: [{
|
|
1524
|
-
field: "paystackId",
|
|
1525
|
-
value: paystackId
|
|
1526
|
-
}]
|
|
1527
|
-
});
|
|
1528
|
-
const planData = {
|
|
1529
|
-
name: plan.name,
|
|
1530
|
-
description: plan.description,
|
|
1531
|
-
amount: plan.amount,
|
|
1532
|
-
currency: plan.currency,
|
|
1533
|
-
interval: plan.interval,
|
|
1534
|
-
planCode: plan.plan_code,
|
|
1535
|
-
paystackId,
|
|
1536
|
-
metadata: plan.metadata !== void 0 && plan.metadata !== null ? JSON.stringify(plan.metadata) : void 0,
|
|
1537
|
-
updatedAt: /* @__PURE__ */ new Date()
|
|
1538
|
-
};
|
|
1539
|
-
if (existing !== void 0 && existing !== null) await ctx.context.adapter.update({
|
|
1540
|
-
model: "paystackPlan",
|
|
1541
|
-
update: planData,
|
|
1542
|
-
where: [{
|
|
1543
|
-
field: "id",
|
|
1544
|
-
value: existing.id
|
|
1545
|
-
}]
|
|
1546
|
-
});
|
|
1547
|
-
else await ctx.context.adapter.create({
|
|
1548
|
-
model: "paystackPlan",
|
|
1549
|
-
data: {
|
|
1550
|
-
...planData,
|
|
1551
|
-
createdAt: /* @__PURE__ */ new Date()
|
|
1552
|
-
}
|
|
1553
|
-
});
|
|
1554
|
-
}
|
|
1555
|
-
return ctx.json({
|
|
1556
|
-
status: "success",
|
|
1557
|
-
count: plansData.length
|
|
1558
|
-
});
|
|
1559
|
-
} catch (error) {
|
|
1560
|
-
ctx.context.logger.error("Failed to sync plans", error);
|
|
1561
|
-
throw new APIError("BAD_REQUEST", { message: error?.message ?? "Failed to sync plans" });
|
|
1562
|
-
}
|
|
1563
|
-
});
|
|
1564
|
-
};
|
|
1565
|
-
const listPlans = (_options) => {
|
|
1566
|
-
return createAuthEndpoint("/paystack/list-plans", {
|
|
1401
|
+
const listPlans = (_options, path = "/list-plans") => {
|
|
1402
|
+
return createAuthEndpoint(path, {
|
|
1567
1403
|
method: "GET",
|
|
1568
1404
|
metadata: { ...HIDE_METADATA },
|
|
1569
1405
|
use: [sessionMiddleware]
|
|
@@ -1573,12 +1409,12 @@ const listPlans = (_options) => {
|
|
|
1573
1409
|
return ctx.json({ plans });
|
|
1574
1410
|
} catch (error) {
|
|
1575
1411
|
ctx.context.logger.error("Failed to list plans", error);
|
|
1576
|
-
throw new APIError("BAD_REQUEST", { message: error
|
|
1412
|
+
throw new APIError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Failed to list plans" });
|
|
1577
1413
|
}
|
|
1578
1414
|
});
|
|
1579
1415
|
};
|
|
1580
|
-
const getConfig = (options) => {
|
|
1581
|
-
return createAuthEndpoint(
|
|
1416
|
+
const getConfig = (options, path = "/get-config") => {
|
|
1417
|
+
return createAuthEndpoint(path, {
|
|
1582
1418
|
method: "GET",
|
|
1583
1419
|
metadata: { openapi: { operationId: "getPaystackConfig" } }
|
|
1584
1420
|
}, async (ctx) => {
|
|
@@ -1590,103 +1426,6 @@ const getConfig = (options) => {
|
|
|
1590
1426
|
});
|
|
1591
1427
|
});
|
|
1592
1428
|
};
|
|
1593
|
-
const chargeRecurringSubscription = (options) => {
|
|
1594
|
-
return createAuthEndpoint("/paystack/charge-recurring", {
|
|
1595
|
-
method: "POST",
|
|
1596
|
-
body: z.object({
|
|
1597
|
-
subscriptionId: z.string(),
|
|
1598
|
-
amount: z.number().optional()
|
|
1599
|
-
})
|
|
1600
|
-
}, async (ctx) => {
|
|
1601
|
-
const { subscriptionId, amount: bodyAmount } = ctx.body;
|
|
1602
|
-
const subscription = await ctx.context.adapter.findOne({
|
|
1603
|
-
model: "subscription",
|
|
1604
|
-
where: [{
|
|
1605
|
-
field: "id",
|
|
1606
|
-
value: subscriptionId
|
|
1607
|
-
}]
|
|
1608
|
-
});
|
|
1609
|
-
if (subscription === void 0 || subscription === null) throw new APIError("NOT_FOUND", { message: "Subscription not found" });
|
|
1610
|
-
if (subscription.paystackAuthorizationCode === void 0 || subscription.paystackAuthorizationCode === null || subscription.paystackAuthorizationCode === "") throw new APIError("BAD_REQUEST", { message: "No authorization code found for this subscription" });
|
|
1611
|
-
const plan = (await getPlans(options.subscription)).find((p) => p.name.toLowerCase() === subscription.plan.toLowerCase());
|
|
1612
|
-
if (!plan) throw new APIError("NOT_FOUND", { message: "Plan not found" });
|
|
1613
|
-
const amount = bodyAmount ?? plan.amount;
|
|
1614
|
-
if (amount === void 0 || amount === null) throw new APIError("BAD_REQUEST", { message: "Plan amount is not defined" });
|
|
1615
|
-
let email;
|
|
1616
|
-
if (subscription.referenceId !== void 0 && subscription.referenceId !== null && subscription.referenceId !== "") {
|
|
1617
|
-
const user = await ctx.context.adapter.findOne({
|
|
1618
|
-
model: "user",
|
|
1619
|
-
where: [{
|
|
1620
|
-
field: "id",
|
|
1621
|
-
value: subscription.referenceId
|
|
1622
|
-
}]
|
|
1623
|
-
});
|
|
1624
|
-
if (user !== void 0 && user !== null) email = user.email;
|
|
1625
|
-
else if (options.organization?.enabled === true) {
|
|
1626
|
-
const ownerMember = await ctx.context.adapter.findOne({
|
|
1627
|
-
model: "member",
|
|
1628
|
-
where: [{
|
|
1629
|
-
field: "organizationId",
|
|
1630
|
-
value: subscription.referenceId
|
|
1631
|
-
}, {
|
|
1632
|
-
field: "role",
|
|
1633
|
-
value: "owner"
|
|
1634
|
-
}]
|
|
1635
|
-
});
|
|
1636
|
-
if (ownerMember !== void 0 && ownerMember !== null) email = (await ctx.context.adapter.findOne({
|
|
1637
|
-
model: "user",
|
|
1638
|
-
where: [{
|
|
1639
|
-
field: "id",
|
|
1640
|
-
value: ownerMember.userId
|
|
1641
|
-
}]
|
|
1642
|
-
}))?.email;
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
|
-
if (email === void 0 || email === null || email === "") throw new APIError("NOT_FOUND", { message: "User email not found" });
|
|
1646
|
-
const finalCurrency = plan.currency ?? "NGN";
|
|
1647
|
-
if (!validateMinAmount(amount, finalCurrency)) throw new APIError("BAD_REQUEST", {
|
|
1648
|
-
message: `Amount ${amount} is less than the minimum required for ${finalCurrency}.`,
|
|
1649
|
-
status: 400
|
|
1650
|
-
});
|
|
1651
|
-
const dataRaw = unwrapSdkResult(await getPaystackOps(options.paystackClient).transactionChargeAuthorization({
|
|
1652
|
-
email,
|
|
1653
|
-
amount,
|
|
1654
|
-
authorization_code: subscription.paystackAuthorizationCode,
|
|
1655
|
-
currency: plan.currency,
|
|
1656
|
-
metadata: {
|
|
1657
|
-
subscriptionId,
|
|
1658
|
-
referenceId: subscription.referenceId,
|
|
1659
|
-
plan: plan.name
|
|
1660
|
-
}
|
|
1661
|
-
}));
|
|
1662
|
-
const chargeData = dataRaw?.data ?? dataRaw;
|
|
1663
|
-
if (chargeData?.status === "success" || dataRaw?.status === "success") {
|
|
1664
|
-
const now = /* @__PURE__ */ new Date();
|
|
1665
|
-
const nextPeriodEnd = getNextPeriodEnd(now, plan.interval ?? "monthly");
|
|
1666
|
-
await ctx.context.adapter.update({
|
|
1667
|
-
model: "subscription",
|
|
1668
|
-
update: {
|
|
1669
|
-
periodStart: now,
|
|
1670
|
-
periodEnd: nextPeriodEnd,
|
|
1671
|
-
updatedAt: now,
|
|
1672
|
-
paystackTransactionReference: chargeData?.reference ?? dataRaw?.reference
|
|
1673
|
-
},
|
|
1674
|
-
where: [{
|
|
1675
|
-
field: "id",
|
|
1676
|
-
value: subscription.id
|
|
1677
|
-
}]
|
|
1678
|
-
});
|
|
1679
|
-
return ctx.json({
|
|
1680
|
-
status: "success",
|
|
1681
|
-
data: chargeData
|
|
1682
|
-
});
|
|
1683
|
-
}
|
|
1684
|
-
return ctx.json({
|
|
1685
|
-
status: "failed",
|
|
1686
|
-
data: chargeData
|
|
1687
|
-
}, { status: 400 });
|
|
1688
|
-
});
|
|
1689
|
-
};
|
|
1690
1429
|
//#endregion
|
|
1691
1430
|
//#region src/schema.ts
|
|
1692
1431
|
const transactions = { paystackTransaction: { fields: {
|
|
@@ -1950,32 +1689,226 @@ const getSchema = (options) => {
|
|
|
1950
1689
|
return mergeSchema(baseSchema, options.schema);
|
|
1951
1690
|
};
|
|
1952
1691
|
//#endregion
|
|
1692
|
+
//#region src/operations.ts
|
|
1693
|
+
async function syncPaystackProducts(ctx, options) {
|
|
1694
|
+
const paystack = getPaystackOps(options.paystackClient);
|
|
1695
|
+
try {
|
|
1696
|
+
const productsData = unwrapSdkResult(await paystack?.product?.list({}));
|
|
1697
|
+
if (!Array.isArray(productsData)) return {
|
|
1698
|
+
status: "success",
|
|
1699
|
+
count: 0
|
|
1700
|
+
};
|
|
1701
|
+
for (const product of productsData) {
|
|
1702
|
+
const paystackId = String(product.id);
|
|
1703
|
+
const existing = await ctx.context.adapter.findOne({
|
|
1704
|
+
model: "paystackProduct",
|
|
1705
|
+
where: [{
|
|
1706
|
+
field: "paystackId",
|
|
1707
|
+
value: paystackId
|
|
1708
|
+
}]
|
|
1709
|
+
});
|
|
1710
|
+
const productFields = {
|
|
1711
|
+
name: product.name ?? "",
|
|
1712
|
+
description: product.description ?? "",
|
|
1713
|
+
price: product.price ?? 0,
|
|
1714
|
+
currency: product.currency ?? "",
|
|
1715
|
+
quantity: product.quantity ?? 0,
|
|
1716
|
+
unlimited: product.unlimited !== void 0 && product.unlimited !== null && product.unlimited !== false,
|
|
1717
|
+
paystackId,
|
|
1718
|
+
slug: product.slug ?? product.name?.toLowerCase().replace(/\s+/g, "-") ?? "",
|
|
1719
|
+
metadata: product.metadata !== void 0 && product.metadata !== null ? JSON.stringify(product.metadata) : void 0,
|
|
1720
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1721
|
+
};
|
|
1722
|
+
if (existing !== void 0 && existing !== null) await ctx.context.adapter.update({
|
|
1723
|
+
model: "paystackProduct",
|
|
1724
|
+
update: productFields,
|
|
1725
|
+
where: [{
|
|
1726
|
+
field: "id",
|
|
1727
|
+
value: String(existing.id)
|
|
1728
|
+
}]
|
|
1729
|
+
});
|
|
1730
|
+
else await ctx.context.adapter.create({
|
|
1731
|
+
model: "paystackProduct",
|
|
1732
|
+
data: {
|
|
1733
|
+
...productFields,
|
|
1734
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
1735
|
+
}
|
|
1736
|
+
});
|
|
1737
|
+
}
|
|
1738
|
+
return {
|
|
1739
|
+
status: "success",
|
|
1740
|
+
count: productsData.length
|
|
1741
|
+
};
|
|
1742
|
+
} catch (error) {
|
|
1743
|
+
ctx.context.logger.error("Failed to sync products", error);
|
|
1744
|
+
throw new APIError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Failed to sync products" });
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
async function syncPaystackPlans(ctx, options) {
|
|
1748
|
+
const paystack = getPaystackOps(options.paystackClient);
|
|
1749
|
+
try {
|
|
1750
|
+
const plansData = unwrapSdkResult(await paystack?.plan?.list());
|
|
1751
|
+
if (!Array.isArray(plansData)) return {
|
|
1752
|
+
status: "success",
|
|
1753
|
+
count: 0
|
|
1754
|
+
};
|
|
1755
|
+
for (const plan of plansData) {
|
|
1756
|
+
const paystackId = String(plan.id);
|
|
1757
|
+
const existing = await ctx.context.adapter.findOne({
|
|
1758
|
+
model: "paystackPlan",
|
|
1759
|
+
where: [{
|
|
1760
|
+
field: "paystackId",
|
|
1761
|
+
value: paystackId
|
|
1762
|
+
}]
|
|
1763
|
+
});
|
|
1764
|
+
const planData = {
|
|
1765
|
+
name: plan.name ?? "",
|
|
1766
|
+
description: plan.description ?? "",
|
|
1767
|
+
amount: plan.amount ?? 0,
|
|
1768
|
+
currency: plan.currency ?? "",
|
|
1769
|
+
interval: plan.interval ?? "",
|
|
1770
|
+
planCode: plan.plan_code ?? "",
|
|
1771
|
+
paystackId,
|
|
1772
|
+
metadata: plan.metadata !== void 0 && plan.metadata !== null ? JSON.stringify(plan.metadata) : void 0,
|
|
1773
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1774
|
+
};
|
|
1775
|
+
if (existing !== void 0 && existing !== null) await ctx.context.adapter.update({
|
|
1776
|
+
model: "paystackPlan",
|
|
1777
|
+
update: planData,
|
|
1778
|
+
where: [{
|
|
1779
|
+
field: "id",
|
|
1780
|
+
value: existing.id
|
|
1781
|
+
}]
|
|
1782
|
+
});
|
|
1783
|
+
else await ctx.context.adapter.create({
|
|
1784
|
+
model: "paystackPlan",
|
|
1785
|
+
data: {
|
|
1786
|
+
...planData,
|
|
1787
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
1788
|
+
}
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
return {
|
|
1792
|
+
status: "success",
|
|
1793
|
+
count: plansData.length
|
|
1794
|
+
};
|
|
1795
|
+
} catch (error) {
|
|
1796
|
+
ctx.context.logger.error("Failed to sync plans", error);
|
|
1797
|
+
throw new APIError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Failed to sync plans" });
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
async function chargeSubscriptionRenewal(ctx, options, input) {
|
|
1801
|
+
const { subscriptionId, amount: bodyAmount } = input;
|
|
1802
|
+
const subscription = await ctx.context.adapter.findOne({
|
|
1803
|
+
model: "subscription",
|
|
1804
|
+
where: [{
|
|
1805
|
+
field: "id",
|
|
1806
|
+
value: subscriptionId
|
|
1807
|
+
}]
|
|
1808
|
+
});
|
|
1809
|
+
if (subscription === void 0 || subscription === null) throw new APIError("NOT_FOUND", { message: "Subscription not found" });
|
|
1810
|
+
if (subscription.paystackAuthorizationCode === void 0 || subscription.paystackAuthorizationCode === null || subscription.paystackAuthorizationCode === "") throw new APIError("BAD_REQUEST", { message: "No authorization code found for this subscription" });
|
|
1811
|
+
const plan = (await getPlans(options.subscription)).find((candidate) => candidate.name.toLowerCase() === subscription.plan.toLowerCase());
|
|
1812
|
+
if (plan === void 0 || plan === null) throw new APIError("NOT_FOUND", { message: "Plan not found" });
|
|
1813
|
+
const amount = bodyAmount ?? plan.amount;
|
|
1814
|
+
if (amount === void 0 || amount === null) throw new APIError("BAD_REQUEST", { message: "Plan amount is not defined" });
|
|
1815
|
+
let email;
|
|
1816
|
+
const referenceId = subscription.referenceId;
|
|
1817
|
+
if (referenceId !== void 0 && referenceId !== null && referenceId !== "") {
|
|
1818
|
+
const user = await ctx.context.adapter.findOne({
|
|
1819
|
+
model: "user",
|
|
1820
|
+
where: [{
|
|
1821
|
+
field: "id",
|
|
1822
|
+
value: referenceId
|
|
1823
|
+
}]
|
|
1824
|
+
});
|
|
1825
|
+
if (user !== void 0 && user !== null) email = user.email;
|
|
1826
|
+
else if (options.organization?.enabled === true) {
|
|
1827
|
+
const ownerMember = await ctx.context.adapter.findOne({
|
|
1828
|
+
model: "member",
|
|
1829
|
+
where: [{
|
|
1830
|
+
field: "organizationId",
|
|
1831
|
+
value: referenceId
|
|
1832
|
+
}, {
|
|
1833
|
+
field: "role",
|
|
1834
|
+
value: "owner"
|
|
1835
|
+
}]
|
|
1836
|
+
});
|
|
1837
|
+
if (ownerMember !== void 0 && ownerMember !== null) email = (await ctx.context.adapter.findOne({
|
|
1838
|
+
model: "user",
|
|
1839
|
+
where: [{
|
|
1840
|
+
field: "id",
|
|
1841
|
+
value: ownerMember.userId
|
|
1842
|
+
}]
|
|
1843
|
+
}))?.email;
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
if (email === void 0 || email === null || email === "") throw new APIError("NOT_FOUND", { message: "User email not found" });
|
|
1847
|
+
const finalCurrency = plan.currency ?? "NGN";
|
|
1848
|
+
if (!validateMinAmount(amount, finalCurrency)) throw new APIError("BAD_REQUEST", {
|
|
1849
|
+
message: `Amount ${amount} is less than the minimum required for ${finalCurrency}.`,
|
|
1850
|
+
status: 400
|
|
1851
|
+
});
|
|
1852
|
+
const chargeData = unwrapSdkResult(await getPaystackOps(options.paystackClient)?.transaction?.chargeAuthorization({ body: {
|
|
1853
|
+
email,
|
|
1854
|
+
amount,
|
|
1855
|
+
authorization_code: subscription.paystackAuthorizationCode,
|
|
1856
|
+
reference: `rec_${subscription.id}_${Date.now()}`,
|
|
1857
|
+
metadata: {
|
|
1858
|
+
subscriptionId,
|
|
1859
|
+
referenceId
|
|
1860
|
+
}
|
|
1861
|
+
} }));
|
|
1862
|
+
if (chargeData?.status === "success" && chargeData.reference !== void 0) {
|
|
1863
|
+
const now = /* @__PURE__ */ new Date();
|
|
1864
|
+
const nextPeriodEnd = getNextPeriodEnd(now, plan.interval ?? "monthly");
|
|
1865
|
+
await ctx.context.adapter.update({
|
|
1866
|
+
model: "subscription",
|
|
1867
|
+
update: {
|
|
1868
|
+
periodStart: now,
|
|
1869
|
+
periodEnd: nextPeriodEnd,
|
|
1870
|
+
updatedAt: now,
|
|
1871
|
+
paystackTransactionReference: chargeData.reference
|
|
1872
|
+
},
|
|
1873
|
+
where: [{
|
|
1874
|
+
field: "id",
|
|
1875
|
+
value: subscription.id
|
|
1876
|
+
}]
|
|
1877
|
+
});
|
|
1878
|
+
return {
|
|
1879
|
+
status: "success",
|
|
1880
|
+
data: chargeData
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
return {
|
|
1884
|
+
status: "failed",
|
|
1885
|
+
data: chargeData
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
//#endregion
|
|
1953
1889
|
//#region src/index.ts
|
|
1954
|
-
const INTERNAL_ERROR_CODES = defineErrorCodes(
|
|
1890
|
+
const INTERNAL_ERROR_CODES = defineErrorCodes(Object.fromEntries(Object.entries(PAYSTACK_ERROR_CODES).map(([key, value]) => [key, typeof value === "string" ? value : value.message])));
|
|
1955
1891
|
const paystack = (options) => {
|
|
1956
1892
|
const routeOptions = options;
|
|
1957
1893
|
return {
|
|
1958
1894
|
id: "paystack",
|
|
1959
1895
|
endpoints: {
|
|
1960
|
-
initializeTransaction: initializeTransaction(routeOptions),
|
|
1961
|
-
verifyTransaction: verifyTransaction(routeOptions),
|
|
1962
|
-
listSubscriptions: listSubscriptions(routeOptions),
|
|
1963
|
-
paystackWebhook: paystackWebhook(routeOptions),
|
|
1964
|
-
listTransactions: listTransactions(routeOptions),
|
|
1965
|
-
getConfig: getConfig(routeOptions),
|
|
1966
|
-
disableSubscription: disablePaystackSubscription(routeOptions),
|
|
1967
|
-
enableSubscription: enablePaystackSubscription(routeOptions),
|
|
1968
|
-
getSubscriptionManageLink: getSubscriptionManageLink(routeOptions),
|
|
1896
|
+
initializeTransaction: initializeTransaction(routeOptions, "/paystack/initialize-transaction"),
|
|
1897
|
+
verifyTransaction: verifyTransaction(routeOptions, "/paystack/verify-transaction"),
|
|
1898
|
+
listSubscriptions: listSubscriptions(routeOptions, "/paystack/list-subscriptions"),
|
|
1899
|
+
paystackWebhook: paystackWebhook(routeOptions, "/paystack/webhook"),
|
|
1900
|
+
listTransactions: listTransactions(routeOptions, "/paystack/list-transactions"),
|
|
1901
|
+
getConfig: getConfig(routeOptions, "/paystack/config"),
|
|
1902
|
+
disableSubscription: disablePaystackSubscription(routeOptions, "/paystack/disable-subscription"),
|
|
1903
|
+
enableSubscription: enablePaystackSubscription(routeOptions, "/paystack/enable-subscription"),
|
|
1904
|
+
getSubscriptionManageLink: getSubscriptionManageLink(routeOptions, "/paystack/subscription-manage-link"),
|
|
1969
1905
|
subscriptionManageLink: getSubscriptionManageLink(routeOptions, "/paystack/subscription/manage-link"),
|
|
1970
|
-
createSubscription: createSubscription(routeOptions),
|
|
1971
|
-
upgradeSubscription: upgradeSubscription(routeOptions),
|
|
1972
|
-
cancelSubscription: cancelSubscription(routeOptions),
|
|
1973
|
-
restoreSubscription: restoreSubscription(routeOptions),
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
listProducts: listProducts(routeOptions),
|
|
1977
|
-
syncPlans: syncPlans(routeOptions),
|
|
1978
|
-
listPlans: listPlans(routeOptions)
|
|
1906
|
+
createSubscription: createSubscription(routeOptions, "/paystack/create-subscription"),
|
|
1907
|
+
upgradeSubscription: upgradeSubscription(routeOptions, "/paystack/upgrade-subscription"),
|
|
1908
|
+
cancelSubscription: cancelSubscription(routeOptions, "/paystack/cancel-subscription"),
|
|
1909
|
+
restoreSubscription: restoreSubscription(routeOptions, "/paystack/restore-subscription"),
|
|
1910
|
+
listProducts: listProducts(routeOptions, "/paystack/list-products"),
|
|
1911
|
+
listPlans: listPlans(routeOptions, "/paystack/list-plans")
|
|
1979
1912
|
},
|
|
1980
1913
|
schema: getSchema(options),
|
|
1981
1914
|
init: (ctx) => {
|
|
@@ -1983,25 +1916,39 @@ const paystack = (options) => {
|
|
|
1983
1916
|
databaseHooks: {
|
|
1984
1917
|
user: { create: { async after(user, hookCtx) {
|
|
1985
1918
|
if (!hookCtx || options.createCustomerOnSignUp !== true || user.email === null || user.email === void 0 || user.email === "") return;
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
1919
|
+
try {
|
|
1920
|
+
const paystackOps = getPaystackOps(options.paystackClient);
|
|
1921
|
+
if (!paystackOps) return;
|
|
1922
|
+
const sdkRes = unwrapSdkResult(await paystackOps.customer?.create({ body: {
|
|
1923
|
+
email: user.email,
|
|
1924
|
+
first_name: user.name ?? void 0,
|
|
1925
|
+
metadata: { userId: user.id }
|
|
1926
|
+
} }) ?? await Promise.reject(/* @__PURE__ */ new Error("Paystack client missing customer ops")));
|
|
1927
|
+
const customerCode = sdkRes?.customer_code;
|
|
1928
|
+
if (customerCode !== void 0 && customerCode !== null && customerCode !== "") {
|
|
1929
|
+
await ctx.adapter.update({
|
|
1930
|
+
model: "user",
|
|
1931
|
+
where: [{
|
|
1932
|
+
field: "id",
|
|
1933
|
+
value: user.id
|
|
1934
|
+
}],
|
|
1935
|
+
update: { paystackCustomerCode: customerCode }
|
|
1936
|
+
});
|
|
1937
|
+
if (typeof options.onCustomerCreate === "function") await options.onCustomerCreate({
|
|
1938
|
+
paystackCustomer: sdkRes,
|
|
1939
|
+
user: {
|
|
1940
|
+
...user,
|
|
1941
|
+
paystackCustomerCode: customerCode
|
|
1942
|
+
}
|
|
1943
|
+
}, hookCtx);
|
|
1944
|
+
}
|
|
1945
|
+
} catch (error) {
|
|
1946
|
+
ctx.logger.error("Failed to create Paystack customer for user", error);
|
|
1947
|
+
}
|
|
2001
1948
|
} } },
|
|
2002
1949
|
organization: options.organization?.enabled === true ? { create: { async after(org, hookCtx) {
|
|
2003
1950
|
try {
|
|
2004
|
-
const extraCreateParams = options.organization?.getCustomerCreateParams ? await options.organization.getCustomerCreateParams(org, hookCtx) : {};
|
|
1951
|
+
const extraCreateParams = typeof options.organization?.getCustomerCreateParams === "function" ? await options.organization.getCustomerCreateParams(org, hookCtx) : {};
|
|
2005
1952
|
let targetEmail = org.email;
|
|
2006
1953
|
if (targetEmail === void 0 || targetEmail === null) {
|
|
2007
1954
|
const ownerMember = await ctx.adapter.findOne({
|
|
@@ -2014,7 +1961,7 @@ const paystack = (options) => {
|
|
|
2014
1961
|
value: "owner"
|
|
2015
1962
|
}]
|
|
2016
1963
|
});
|
|
2017
|
-
if (ownerMember) targetEmail = (await ctx.adapter.findOne({
|
|
1964
|
+
if (ownerMember !== null && ownerMember !== void 0) targetEmail = (await ctx.adapter.findOne({
|
|
2018
1965
|
model: "user",
|
|
2019
1966
|
where: [{
|
|
2020
1967
|
field: "id",
|
|
@@ -2028,17 +1975,20 @@ const paystack = (options) => {
|
|
|
2028
1975
|
first_name: org.name,
|
|
2029
1976
|
metadata: { organizationId: org.id }
|
|
2030
1977
|
}, extraCreateParams);
|
|
2031
|
-
const
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
organization
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
1978
|
+
const paystackOps = getPaystackOps(options.paystackClient);
|
|
1979
|
+
if (!paystackOps) return;
|
|
1980
|
+
const sdkRes = unwrapSdkResult(await paystackOps.customer?.create({ body: params }) ?? await Promise.reject(/* @__PURE__ */ new Error("Paystack client missing customer ops")));
|
|
1981
|
+
const customerCode = sdkRes?.customer_code;
|
|
1982
|
+
if (customerCode !== void 0 && customerCode !== null && customerCode !== "" && sdkRes !== void 0 && sdkRes !== null) {
|
|
1983
|
+
await ctx.internalAdapter.updateOrganization(org.id, { paystackCustomerCode: customerCode });
|
|
1984
|
+
if (typeof options.organization?.onCustomerCreate === "function") await options.organization.onCustomerCreate({
|
|
1985
|
+
paystackCustomer: sdkRes,
|
|
1986
|
+
organization: {
|
|
1987
|
+
...org,
|
|
1988
|
+
paystackCustomerCode: customerCode
|
|
1989
|
+
}
|
|
1990
|
+
}, hookCtx);
|
|
1991
|
+
}
|
|
2042
1992
|
} catch (error) {
|
|
2043
1993
|
ctx.logger.error("Failed to create Paystack customer for organization", error);
|
|
2044
1994
|
}
|
|
@@ -2073,7 +2023,7 @@ const paystack = (options) => {
|
|
|
2073
2023
|
team: { create: { before: async (team, ctx) => {
|
|
2074
2024
|
if (options.subscription?.enabled === true && team.organizationId && ctx) {
|
|
2075
2025
|
const subscription = await getOrganizationSubscription(ctx, team.organizationId);
|
|
2076
|
-
if (subscription) {
|
|
2026
|
+
if (subscription !== null && subscription !== void 0) {
|
|
2077
2027
|
const maxTeams = ((await getPlanByName(routeOptions, subscription.plan))?.limits)?.teams;
|
|
2078
2028
|
if (typeof maxTeams === "number") await checkTeamLimit(ctx, team.organizationId, maxTeams);
|
|
2079
2029
|
}
|
|
@@ -2081,10 +2031,11 @@ const paystack = (options) => {
|
|
|
2081
2031
|
} } }
|
|
2082
2032
|
} };
|
|
2083
2033
|
},
|
|
2084
|
-
$ERROR_CODES: INTERNAL_ERROR_CODES
|
|
2034
|
+
$ERROR_CODES: INTERNAL_ERROR_CODES,
|
|
2035
|
+
options
|
|
2085
2036
|
};
|
|
2086
2037
|
};
|
|
2087
2038
|
//#endregion
|
|
2088
|
-
export { paystack };
|
|
2039
|
+
export { chargeSubscriptionRenewal, paystack, syncPaystackPlans, syncPaystackProducts };
|
|
2089
2040
|
|
|
2090
2041
|
//# sourceMappingURL=index.mjs.map
|