@alexasomba/better-auth-paystack 1.2.1 → 2.2.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 +232 -59
- package/dist/client.d.mts +133 -305
- package/dist/client.d.mts.map +1 -1
- package/dist/client.mjs +90 -94
- package/dist/client.mjs.map +1 -1
- package/dist/index.d.mts +423 -2
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +818 -687
- package/dist/index.mjs.map +1 -1
- package/dist/types-B5ZnlFrq.d.mts +258 -0
- package/dist/types-B5ZnlFrq.d.mts.map +1 -0
- 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,127 +3,83 @@ 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
|
|
48
|
+
function getPlanSeatAmount(plan) {
|
|
49
|
+
if (plan.seatAmount !== void 0) {
|
|
50
|
+
if (typeof plan.seatAmount === "number" && Number.isFinite(plan.seatAmount)) return plan.seatAmount;
|
|
51
|
+
throw new Error(`Invalid seatAmount for plan '${plan.name}'. Expected a finite number.`);
|
|
52
|
+
}
|
|
53
|
+
if (plan.seatPriceId === void 0 || plan.seatPriceId === null || plan.seatPriceId === "") return;
|
|
54
|
+
const parsed = typeof plan.seatPriceId === "string" ? Number(plan.seatPriceId) : plan.seatPriceId;
|
|
55
|
+
if (typeof parsed === "number" && Number.isFinite(parsed)) return parsed;
|
|
56
|
+
throw new Error(`Invalid seatPriceId for plan '${plan.name}'. Expected a numeric amount in the smallest currency unit.`);
|
|
57
|
+
}
|
|
58
|
+
function calculatePlanAmount(plan, quantity) {
|
|
59
|
+
return (plan.amount ?? 0) + quantity * (getPlanSeatAmount(plan) ?? 0);
|
|
60
|
+
}
|
|
61
|
+
function isLocalSubscriptionCode(subscriptionCode) {
|
|
62
|
+
return typeof subscriptionCode === "string" && (subscriptionCode.startsWith("LOC_") || subscriptionCode.startsWith("sub_local_"));
|
|
63
|
+
}
|
|
64
|
+
function isLocallyManagedSubscription(subscription) {
|
|
65
|
+
if (isLocalSubscriptionCode(subscription.paystackSubscriptionCode)) return true;
|
|
66
|
+
if (typeof subscription.paystackSubscriptionCode === "string" && subscription.paystackSubscriptionCode !== "") return false;
|
|
67
|
+
return subscription.paystackPlanCode === void 0 || subscription.paystackPlanCode === null || subscription.paystackPlanCode === "";
|
|
68
|
+
}
|
|
69
|
+
function assertLocallyManagedSubscription(subscription, action) {
|
|
70
|
+
if (!isLocallyManagedSubscription(subscription)) throw new Error(`Paystack-managed subscriptions do not support ${action}. Use local billing for seat-based or prorated subscription changes.`);
|
|
71
|
+
}
|
|
121
72
|
async function getPlans(subscriptionOptions) {
|
|
122
73
|
if (subscriptionOptions?.enabled === true) return typeof subscriptionOptions.plans === "function" ? subscriptionOptions.plans() : subscriptionOptions.plans;
|
|
123
74
|
throw new Error("Subscriptions are not enabled in the Paystack options.");
|
|
124
75
|
}
|
|
125
76
|
async function getPlanByName(options, name) {
|
|
126
|
-
if (
|
|
77
|
+
if (typeof name !== "string" || name.trim() === "") return null;
|
|
78
|
+
if (options.subscription?.enabled === true) {
|
|
79
|
+
const plans = await getPlans(options.subscription);
|
|
80
|
+
const normalizedName = name.toLowerCase();
|
|
81
|
+
return plans.find((plan) => typeof plan.name === "string" && plan.name.toLowerCase() === normalizedName) ?? null;
|
|
82
|
+
}
|
|
127
83
|
return null;
|
|
128
84
|
}
|
|
129
85
|
async function getProducts(productOptions) {
|
|
@@ -131,7 +87,7 @@ async function getProducts(productOptions) {
|
|
|
131
87
|
return [];
|
|
132
88
|
}
|
|
133
89
|
async function getProductByName(options, name) {
|
|
134
|
-
return await getProducts(options.products).then((products) => products
|
|
90
|
+
return await getProducts(options.products).then((products) => products !== void 0 && products !== null ? products.find((product) => product.name.toLowerCase() === name.toLowerCase()) ?? null : null);
|
|
135
91
|
}
|
|
136
92
|
function getNextPeriodEnd(startDate, interval) {
|
|
137
93
|
const date = new Date(startDate);
|
|
@@ -189,7 +145,7 @@ async function syncProductQuantityFromPaystack(ctx, productName, paystackClient)
|
|
|
189
145
|
}]
|
|
190
146
|
});
|
|
191
147
|
if (localProduct?.paystackId === void 0 || localProduct.paystackId === null || localProduct.paystackId === "") {
|
|
192
|
-
if (localProduct !==
|
|
148
|
+
if (localProduct?.id !== void 0 && localProduct.unlimited !== true && typeof localProduct.quantity === "number" && localProduct.quantity > 0) await ctx.context.adapter.update({
|
|
193
149
|
model: "paystackProduct",
|
|
194
150
|
update: {
|
|
195
151
|
quantity: localProduct.quantity - 1,
|
|
@@ -203,8 +159,10 @@ async function syncProductQuantityFromPaystack(ctx, productName, paystackClient)
|
|
|
203
159
|
return;
|
|
204
160
|
}
|
|
205
161
|
try {
|
|
206
|
-
const
|
|
207
|
-
if (
|
|
162
|
+
const paystackProductId = Number(localProduct.paystackId);
|
|
163
|
+
if (!Number.isFinite(paystackProductId)) return;
|
|
164
|
+
const remoteQuantity = unwrapSdkResult(await paystackClient.product?.fetch(paystackProductId))?.quantity;
|
|
165
|
+
if (remoteQuantity !== void 0 && localProduct.id !== void 0) await ctx.context.adapter.update({
|
|
208
166
|
model: "paystackProduct",
|
|
209
167
|
update: {
|
|
210
168
|
quantity: remoteQuantity,
|
|
@@ -216,7 +174,7 @@ async function syncProductQuantityFromPaystack(ctx, productName, paystackClient)
|
|
|
216
174
|
}]
|
|
217
175
|
});
|
|
218
176
|
} catch {
|
|
219
|
-
if (localProduct !==
|
|
177
|
+
if (localProduct?.id !== void 0 && localProduct.unlimited !== true && typeof localProduct.quantity === "number" && localProduct.quantity > 0) await ctx.context.adapter.update({
|
|
220
178
|
model: "paystackProduct",
|
|
221
179
|
update: {
|
|
222
180
|
quantity: localProduct.quantity - 1,
|
|
@@ -240,9 +198,10 @@ async function syncSubscriptionSeats(ctx, organizationId, options) {
|
|
|
240
198
|
}]
|
|
241
199
|
});
|
|
242
200
|
if (subscription?.paystackSubscriptionCode === void 0 || subscription.paystackSubscriptionCode === null || subscription.paystackSubscriptionCode === "") return;
|
|
201
|
+
if (subscription === null || subscription === void 0) return;
|
|
243
202
|
const plan = await getPlanByName(options, subscription.plan);
|
|
244
|
-
if (plan === null) return;
|
|
245
|
-
if (plan
|
|
203
|
+
if (plan === null || plan === void 0) return;
|
|
204
|
+
if (getPlanSeatAmount(plan) === void 0) return;
|
|
246
205
|
const quantity = (await adapter.findMany({
|
|
247
206
|
model: "member",
|
|
248
207
|
where: [{
|
|
@@ -250,14 +209,8 @@ async function syncSubscriptionSeats(ctx, organizationId, options) {
|
|
|
250
209
|
value: organizationId
|
|
251
210
|
}]
|
|
252
211
|
})).length;
|
|
253
|
-
let totalAmount = plan.amount ?? 0;
|
|
254
|
-
if (plan.seatAmount !== void 0 && plan.seatAmount !== null && typeof plan.seatAmount === "number") totalAmount += quantity * plan.seatAmount;
|
|
255
|
-
const ops = getPaystackOps(options.paystackClient);
|
|
256
212
|
try {
|
|
257
|
-
|
|
258
|
-
code: subscription.paystackSubscriptionCode,
|
|
259
|
-
amount: totalAmount
|
|
260
|
-
});
|
|
213
|
+
assertLocallyManagedSubscription(subscription, "automatic seat sync");
|
|
261
214
|
await adapter.update({
|
|
262
215
|
model: "subscription",
|
|
263
216
|
where: [{
|
|
@@ -271,7 +224,7 @@ async function syncSubscriptionSeats(ctx, organizationId, options) {
|
|
|
271
224
|
});
|
|
272
225
|
} catch (e) {
|
|
273
226
|
const log = ctx.context.logger;
|
|
274
|
-
if (log !== void 0 && log !== null) log.error("Failed to sync subscription seats
|
|
227
|
+
if (log !== void 0 && log !== null) log.error("Failed to sync subscription seats", e);
|
|
275
228
|
}
|
|
276
229
|
}
|
|
277
230
|
//#endregion
|
|
@@ -284,7 +237,7 @@ const referenceMiddleware = (options, action) => createAuthMiddleware(async (ctx
|
|
|
284
237
|
const referenceId = body.referenceId ?? query.referenceId ?? session.user.id;
|
|
285
238
|
const subscriptionOptions = options.subscription;
|
|
286
239
|
if (referenceId === session.user.id) return { referenceId };
|
|
287
|
-
if (subscriptionOptions?.enabled === true && "authorizeReference" in subscriptionOptions && subscriptionOptions.authorizeReference) {
|
|
240
|
+
if (subscriptionOptions?.enabled === true && "authorizeReference" in subscriptionOptions && typeof subscriptionOptions.authorizeReference === "function") {
|
|
288
241
|
if (await subscriptionOptions.authorizeReference({
|
|
289
242
|
user: session.user,
|
|
290
243
|
session: session.session,
|
|
@@ -325,7 +278,7 @@ const getOrganizationSubscription = async (ctx, organizationId) => {
|
|
|
325
278
|
};
|
|
326
279
|
const checkSeatLimit = async (ctx, organizationId, seatsToAdd = 1) => {
|
|
327
280
|
const subscription = await getOrganizationSubscription(ctx, organizationId);
|
|
328
|
-
if (subscription?.seats ===
|
|
281
|
+
if (subscription?.seats === null) return true;
|
|
329
282
|
const members = await ctx.context.adapter.findMany({
|
|
330
283
|
model: "member",
|
|
331
284
|
where: [{
|
|
@@ -333,6 +286,7 @@ const checkSeatLimit = async (ctx, organizationId, seatsToAdd = 1) => {
|
|
|
333
286
|
value: organizationId
|
|
334
287
|
}]
|
|
335
288
|
});
|
|
289
|
+
if (!subscription) return true;
|
|
336
290
|
if (members.length + seatsToAdd > subscription.seats) throw new APIError("FORBIDDEN", { message: `Organization member limit reached. Used: ${members.length}, Max: ${subscription.seats}` });
|
|
337
291
|
return true;
|
|
338
292
|
};
|
|
@@ -356,8 +310,17 @@ const PAYSTACK_ERROR_CODES = defineErrorCodes({
|
|
|
356
310
|
FAILED_TO_VERIFY_TRANSACTION: "Failed to verify transaction",
|
|
357
311
|
FAILED_TO_DISABLE_SUBSCRIPTION: "Failed to disable subscription",
|
|
358
312
|
FAILED_TO_ENABLE_SUBSCRIPTION: "Failed to enable subscription",
|
|
359
|
-
EMAIL_VERIFICATION_REQUIRED: "Email verification is required before you can subscribe to a plan"
|
|
313
|
+
EMAIL_VERIFICATION_REQUIRED: "Email verification is required before you can subscribe to a plan",
|
|
314
|
+
SUBSCRIPTION_PAYMENT_CHANNEL_NOT_ALLOWED: "This subscription only supports specific payment channels"
|
|
360
315
|
});
|
|
316
|
+
function getAllowedSubscriptionChannels(options) {
|
|
317
|
+
const channels = options.subscription?.allowedPaymentChannels;
|
|
318
|
+
return Array.isArray(channels) && channels.length > 0 ? channels : void 0;
|
|
319
|
+
}
|
|
320
|
+
function isAllowedSubscriptionChannel(channel, allowedChannels) {
|
|
321
|
+
if (allowedChannels === void 0) return true;
|
|
322
|
+
return channel !== void 0 && channel !== null && allowedChannels.includes(channel);
|
|
323
|
+
}
|
|
361
324
|
async function hmacSha512Hex(secret, message) {
|
|
362
325
|
const encoder = new TextEncoder();
|
|
363
326
|
const keyData = encoder.encode(secret);
|
|
@@ -375,8 +338,8 @@ async function hmacSha512Hex(secret, message) {
|
|
|
375
338
|
const { createHmac } = await import("node:crypto");
|
|
376
339
|
return createHmac("sha512", secret).update(message).digest("hex");
|
|
377
340
|
}
|
|
378
|
-
const paystackWebhook = (options) => {
|
|
379
|
-
return createAuthEndpoint(
|
|
341
|
+
const paystackWebhook = (options, path = "/webhook") => {
|
|
342
|
+
return createAuthEndpoint(path, {
|
|
380
343
|
method: "POST",
|
|
381
344
|
metadata: {
|
|
382
345
|
...HIDE_METADATA,
|
|
@@ -388,12 +351,25 @@ const paystackWebhook = (options) => {
|
|
|
388
351
|
const request = ctx.requestClone ?? ctx.request;
|
|
389
352
|
if (request === void 0 || request === null) throw new APIError("BAD_REQUEST", { message: "Request object is missing from context" });
|
|
390
353
|
const payload = await request.text();
|
|
391
|
-
const
|
|
354
|
+
const headers = ctx.headers ?? ctx.request?.headers;
|
|
355
|
+
const signature = headers?.get("x-paystack-signature");
|
|
356
|
+
if (options.webhook?.verifyIP === true) {
|
|
357
|
+
const trustedIPs = options.webhook.trustedIPs ?? [
|
|
358
|
+
"52.31.139.75",
|
|
359
|
+
"52.49.173.169",
|
|
360
|
+
"52.214.14.220"
|
|
361
|
+
];
|
|
362
|
+
const clientIP = headers?.get("x-forwarded-for")?.split(",")[0]?.trim() ?? headers?.get("x-real-ip") ?? ctx.request.ip;
|
|
363
|
+
if (clientIP !== void 0 && clientIP !== null && trustedIPs.includes(clientIP) === false) throw new APIError("UNAUTHORIZED", {
|
|
364
|
+
message: `Forbidden IP: ${clientIP}`,
|
|
365
|
+
status: 401
|
|
366
|
+
});
|
|
367
|
+
}
|
|
392
368
|
if (signature === void 0 || signature === null || signature === "") throw new APIError("UNAUTHORIZED", {
|
|
393
369
|
message: "Missing x-paystack-signature header",
|
|
394
370
|
status: 401
|
|
395
371
|
});
|
|
396
|
-
if (await hmacSha512Hex(options.paystackWebhookSecret, payload) !== signature) throw new APIError("UNAUTHORIZED", {
|
|
372
|
+
if (await hmacSha512Hex(options.webhook?.secret ?? options.paystackWebhookSecret ?? options.secretKey, payload) !== signature) throw new APIError("UNAUTHORIZED", {
|
|
397
373
|
message: "Invalid Paystack webhook signature",
|
|
398
374
|
status: 401
|
|
399
375
|
});
|
|
@@ -402,7 +378,8 @@ const paystackWebhook = (options) => {
|
|
|
402
378
|
const data = event.data;
|
|
403
379
|
if (eventName === "charge.success") {
|
|
404
380
|
const reference = data?.reference;
|
|
405
|
-
const
|
|
381
|
+
const paystackIdRaw = data?.id;
|
|
382
|
+
const paystackId = paystackIdRaw !== void 0 && paystackIdRaw !== null ? String(paystackIdRaw) : void 0;
|
|
406
383
|
if (reference !== void 0 && reference !== null && reference !== "") {
|
|
407
384
|
try {
|
|
408
385
|
await ctx.context.adapter.update({
|
|
@@ -428,7 +405,9 @@ const paystackWebhook = (options) => {
|
|
|
428
405
|
value: reference
|
|
429
406
|
}]
|
|
430
407
|
});
|
|
431
|
-
if (transaction
|
|
408
|
+
if (transaction !== void 0 && transaction !== null && transaction.product !== void 0 && transaction.product !== null && transaction.product !== "") {
|
|
409
|
+
if (options.paystackClient !== void 0 && options.paystackClient !== null) await syncProductQuantityFromPaystack(ctx, transaction.product, options.paystackClient);
|
|
410
|
+
}
|
|
432
411
|
} catch (e) {
|
|
433
412
|
ctx.context.logger.warn("Failed to sync product quantity", e);
|
|
434
413
|
}
|
|
@@ -454,19 +433,20 @@ const paystackWebhook = (options) => {
|
|
|
454
433
|
}
|
|
455
434
|
if (options.subscription?.enabled === true) try {
|
|
456
435
|
if (eventName === "subscription.create") {
|
|
457
|
-
const
|
|
458
|
-
const subscriptionCode =
|
|
459
|
-
const customerCode =
|
|
460
|
-
const planCode =
|
|
461
|
-
let metadata =
|
|
436
|
+
const subscriptionData = data;
|
|
437
|
+
const subscriptionCode = subscriptionData.subscription_code ?? "";
|
|
438
|
+
const customerCode = subscriptionData.customer?.customer_code;
|
|
439
|
+
const planCode = subscriptionData.plan?.plan_code;
|
|
440
|
+
let metadata = subscriptionData.metadata;
|
|
462
441
|
if (typeof metadata === "string") try {
|
|
463
442
|
metadata = JSON.parse(metadata);
|
|
464
443
|
} catch {}
|
|
465
|
-
const
|
|
466
|
-
|
|
444
|
+
const metadataObj = metadata !== void 0 && metadata !== null && typeof metadata === "object" ? metadata : {};
|
|
445
|
+
const referenceIdFromMetadata = typeof metadataObj.referenceId === "string" ? metadataObj.referenceId : void 0;
|
|
446
|
+
let planNameFromMetadata = typeof metadataObj.plan === "string" ? metadataObj.plan : void 0;
|
|
467
447
|
if (typeof planNameFromMetadata === "string") planNameFromMetadata = planNameFromMetadata.toLowerCase();
|
|
468
448
|
const plans = await getPlans(options.subscription);
|
|
469
|
-
const planFromCode = planCode !== void 0 && planCode !== null && planCode !== "" ? plans.find((p) => p.planCode
|
|
449
|
+
const planFromCode = planCode !== void 0 && planCode !== null && planCode !== "" ? plans.find((p) => p.planCode === planCode) : void 0;
|
|
470
450
|
const planPart = planFromCode?.name ?? planNameFromMetadata;
|
|
471
451
|
const planName = planPart !== void 0 && planPart !== null && planPart !== "" ? planPart.toLowerCase() : void 0;
|
|
472
452
|
if (subscriptionCode !== void 0 && subscriptionCode !== null && subscriptionCode !== "") {
|
|
@@ -495,7 +475,7 @@ const paystackWebhook = (options) => {
|
|
|
495
475
|
paystackSubscriptionCode: subscriptionCode,
|
|
496
476
|
status: "active",
|
|
497
477
|
updatedAt: /* @__PURE__ */ new Date(),
|
|
498
|
-
periodEnd:
|
|
478
|
+
periodEnd: subscriptionData.next_payment_date !== void 0 && subscriptionData.next_payment_date !== null ? new Date(subscriptionData.next_payment_date) : void 0
|
|
499
479
|
},
|
|
500
480
|
where: [{
|
|
501
481
|
field: "id",
|
|
@@ -528,9 +508,9 @@ const paystackWebhook = (options) => {
|
|
|
528
508
|
}
|
|
529
509
|
}
|
|
530
510
|
if (eventName === "subscription.disable" || eventName === "subscription.not_renew") {
|
|
531
|
-
const
|
|
532
|
-
const subscriptionCode =
|
|
533
|
-
if (subscriptionCode !==
|
|
511
|
+
const subscriptionData = data;
|
|
512
|
+
const subscriptionCode = subscriptionData.subscription_code ?? "";
|
|
513
|
+
if (subscriptionCode !== "") {
|
|
534
514
|
const existing = await ctx.context.adapter.findOne({
|
|
535
515
|
model: "subscription",
|
|
536
516
|
where: [{
|
|
@@ -539,9 +519,9 @@ const paystackWebhook = (options) => {
|
|
|
539
519
|
}]
|
|
540
520
|
});
|
|
541
521
|
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 >
|
|
522
|
+
const nextPaymentDate = subscriptionData.next_payment_date;
|
|
523
|
+
const periodEnd = nextPaymentDate !== void 0 && nextPaymentDate !== null && nextPaymentDate !== "" ? new Date(nextPaymentDate) : existing?.periodEnd !== void 0 && existing.periodEnd !== null ? new Date(existing.periodEnd) : void 0;
|
|
524
|
+
if (periodEnd !== void 0 && periodEnd.getTime() > Date.now()) newStatus = "active";
|
|
545
525
|
await ctx.context.adapter.update({
|
|
546
526
|
model: "subscription",
|
|
547
527
|
update: {
|
|
@@ -555,7 +535,7 @@ const paystackWebhook = (options) => {
|
|
|
555
535
|
value: subscriptionCode
|
|
556
536
|
}]
|
|
557
537
|
});
|
|
558
|
-
if (existing !==
|
|
538
|
+
if (existing !== null && existing !== void 0) await options.subscription.onSubscriptionCancel?.({
|
|
559
539
|
event,
|
|
560
540
|
subscription: {
|
|
561
541
|
...existing,
|
|
@@ -565,9 +545,9 @@ const paystackWebhook = (options) => {
|
|
|
565
545
|
}
|
|
566
546
|
}
|
|
567
547
|
if (eventName === "charge.success" || eventName === "invoice.update") {
|
|
568
|
-
const
|
|
569
|
-
const subscriptionCode =
|
|
570
|
-
if (subscriptionCode !== void 0
|
|
548
|
+
const subscriptionCodeRaw = (data?.subscription)?.subscription_code ?? data?.subscription_code;
|
|
549
|
+
const subscriptionCode = subscriptionCodeRaw !== void 0 && subscriptionCodeRaw !== null && subscriptionCodeRaw !== "" ? subscriptionCodeRaw : void 0;
|
|
550
|
+
if (subscriptionCode !== void 0) {
|
|
571
551
|
const existingSub = await ctx.context.adapter.findOne({
|
|
572
552
|
model: "subscription",
|
|
573
553
|
where: [{
|
|
@@ -575,7 +555,7 @@ const paystackWebhook = (options) => {
|
|
|
575
555
|
value: subscriptionCode
|
|
576
556
|
}]
|
|
577
557
|
});
|
|
578
|
-
if (existingSub
|
|
558
|
+
if (existingSub !== void 0 && existingSub !== null && existingSub.pendingPlan !== void 0 && existingSub.pendingPlan !== null && existingSub.pendingPlan !== "") await ctx.context.adapter.update({
|
|
579
559
|
model: "subscription",
|
|
580
560
|
update: {
|
|
581
561
|
plan: existingSub.pendingPlan,
|
|
@@ -610,7 +590,7 @@ const initializeTransactionBodySchema = z.object({
|
|
|
610
590
|
cancelAtPeriodEnd: z.boolean().optional(),
|
|
611
591
|
prorateAndCharge: z.boolean().optional()
|
|
612
592
|
});
|
|
613
|
-
const initializeTransaction = (options, path = "/
|
|
593
|
+
const initializeTransaction = (options, path = "/initialize-transaction") => {
|
|
614
594
|
const subscriptionOptions = options.subscription;
|
|
615
595
|
return createAuthEndpoint(path, {
|
|
616
596
|
method: "POST",
|
|
@@ -626,23 +606,22 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
626
606
|
if (callbackURL !== void 0 && callbackURL !== null && callbackURL !== "") {
|
|
627
607
|
const checkTrusted = () => {
|
|
628
608
|
try {
|
|
629
|
-
if (callbackURL ===
|
|
630
|
-
if (callbackURL.startsWith("/")) return true;
|
|
609
|
+
if (callbackURL?.startsWith("/") === true) return true;
|
|
631
610
|
const baseUrl = ctx.context?.baseURL ?? ctx.request?.url ?? "";
|
|
632
|
-
if (
|
|
611
|
+
if (baseUrl === "") return false;
|
|
633
612
|
const baseOrigin = new URL(baseUrl).origin;
|
|
634
613
|
return new URL(callbackURL).origin === baseOrigin;
|
|
635
614
|
} catch {
|
|
636
615
|
return false;
|
|
637
616
|
}
|
|
638
617
|
};
|
|
639
|
-
if (checkTrusted()
|
|
618
|
+
if (checkTrusted() === false) throw new APIError("FORBIDDEN", {
|
|
640
619
|
message: "callbackURL is not a trusted origin.",
|
|
641
620
|
status: 403
|
|
642
621
|
});
|
|
643
622
|
}
|
|
644
623
|
const session = await getSessionFromCtx(ctx);
|
|
645
|
-
if (
|
|
624
|
+
if (session === void 0 || session === null) throw new APIError("UNAUTHORIZED");
|
|
646
625
|
const user = session.user;
|
|
647
626
|
if (subscriptionOptions?.enabled === true && subscriptionOptions.requireEmailVerification === true && user.emailVerified !== true) throw new APIError("BAD_REQUEST", {
|
|
648
627
|
code: "EMAIL_VERIFICATION_REQUIRED",
|
|
@@ -653,7 +632,7 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
653
632
|
if (planName !== void 0 && planName !== null && planName !== "") {
|
|
654
633
|
if (subscriptionOptions?.enabled !== true) throw new APIError("BAD_REQUEST", { message: "Subscriptions are not enabled." });
|
|
655
634
|
plan = await getPlanByName(options, planName) ?? void 0;
|
|
656
|
-
if (plan ===
|
|
635
|
+
if (plan === void 0 || plan === null) try {
|
|
657
636
|
const nativePlan = await ctx.context.adapter.findOne({
|
|
658
637
|
model: "paystackPlan",
|
|
659
638
|
where: [{
|
|
@@ -669,15 +648,17 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
669
648
|
value: planName
|
|
670
649
|
}]
|
|
671
650
|
}) ?? void 0;
|
|
651
|
+
} catch {
|
|
652
|
+
plan = void 0;
|
|
672
653
|
}
|
|
673
|
-
if (plan ===
|
|
654
|
+
if (plan === void 0 || plan === null) throw new APIError("BAD_REQUEST", {
|
|
674
655
|
code: "SUBSCRIPTION_PLAN_NOT_FOUND",
|
|
675
656
|
message: PAYSTACK_ERROR_CODES.SUBSCRIPTION_PLAN_NOT_FOUND.message,
|
|
676
657
|
status: 400
|
|
677
658
|
});
|
|
678
659
|
} else if (productName !== void 0 && productName !== null && productName !== "") {
|
|
679
660
|
if (typeof productName === "string") {
|
|
680
|
-
product
|
|
661
|
+
product = await getProductByName(options, productName) ?? void 0;
|
|
681
662
|
product ??= await ctx.context.adapter.findOne({
|
|
682
663
|
model: "paystackProduct",
|
|
683
664
|
where: [{
|
|
@@ -686,19 +667,19 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
686
667
|
}]
|
|
687
668
|
}) ?? void 0;
|
|
688
669
|
}
|
|
689
|
-
if (product ===
|
|
670
|
+
if (product === void 0 || product === null) throw new APIError("BAD_REQUEST", {
|
|
690
671
|
message: `Product '${productName}' not found.`,
|
|
691
672
|
status: 400
|
|
692
673
|
});
|
|
693
|
-
} else if (bodyAmount === void 0 || bodyAmount === null
|
|
674
|
+
} else if (bodyAmount === void 0 || bodyAmount === null) throw new APIError("BAD_REQUEST", {
|
|
694
675
|
message: "Either 'plan', 'product', or 'amount' is required to initialize a transaction.",
|
|
695
676
|
status: 400
|
|
696
677
|
});
|
|
697
|
-
let amount = bodyAmount ?? product?.price;
|
|
698
|
-
const finalCurrency = currency ?? product?.currency ?? plan?.currency ?? "NGN";
|
|
678
|
+
let amount = bodyAmount ?? product?.price ?? product?.amount;
|
|
679
|
+
const finalCurrency = currency ?? product?.currency ?? product?.currency ?? plan?.currency ?? "NGN";
|
|
699
680
|
const referenceIdFromCtx = ctx.context.referenceId;
|
|
700
|
-
const referenceId = ctx.body.referenceId
|
|
701
|
-
if (plan && scheduleAtPeriodEnd === true) {
|
|
681
|
+
const referenceId = ctx.body.referenceId ?? referenceIdFromCtx ?? session.user.id;
|
|
682
|
+
if (plan !== void 0 && scheduleAtPeriodEnd === true) {
|
|
702
683
|
const existingSub = await getOrganizationSubscription(ctx, referenceId);
|
|
703
684
|
if (existingSub?.status === "active") {
|
|
704
685
|
await ctx.context.adapter.update({
|
|
@@ -740,40 +721,45 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
740
721
|
});
|
|
741
722
|
}
|
|
742
723
|
}
|
|
743
|
-
if (plan !==
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
724
|
+
if (plan !== void 0) try {
|
|
725
|
+
if (getPlanSeatAmount(plan) !== void 0) {
|
|
726
|
+
const members = await ctx.context.adapter.findMany({
|
|
727
|
+
model: "member",
|
|
728
|
+
where: [{
|
|
729
|
+
field: "organizationId",
|
|
730
|
+
value: referenceId
|
|
731
|
+
}]
|
|
732
|
+
});
|
|
733
|
+
const seatCount = members.length > 0 ? members.length : 1;
|
|
734
|
+
amount = calculatePlanAmount(plan, quantity ?? seatCount);
|
|
735
|
+
}
|
|
736
|
+
} catch (error) {
|
|
737
|
+
throw new APIError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Invalid seat configuration for plan." });
|
|
754
738
|
}
|
|
755
739
|
let url;
|
|
756
740
|
let reference;
|
|
757
741
|
let accessCode;
|
|
758
742
|
let trialStart;
|
|
759
743
|
let trialEnd;
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
744
|
+
const requestedTrialDays = plan?.freeTrial?.days !== void 0 && plan.freeTrial.days > 0 ? plan.freeTrial.days : 0;
|
|
745
|
+
const trialRequested = requestedTrialDays > 0;
|
|
746
|
+
let trialGranted = false;
|
|
747
|
+
let trialDeniedReason;
|
|
748
|
+
if (trialRequested) if ((await ctx.context.adapter.findMany({
|
|
749
|
+
model: "subscription",
|
|
750
|
+
where: [{
|
|
751
|
+
field: "referenceId",
|
|
752
|
+
value: referenceId
|
|
753
|
+
}]
|
|
754
|
+
}))?.some((sub) => sub.trialStart !== void 0 && sub.trialStart !== null || sub.trialEnd !== void 0 && sub.trialEnd !== null || sub.status === "trialing") === false) {
|
|
755
|
+
trialStart = /* @__PURE__ */ new Date();
|
|
756
|
+
trialEnd = /* @__PURE__ */ new Date();
|
|
757
|
+
trialEnd.setDate(trialEnd.getDate() + requestedTrialDays);
|
|
758
|
+
trialGranted = true;
|
|
759
|
+
} else trialDeniedReason = "already_used";
|
|
773
760
|
try {
|
|
774
|
-
let targetEmail = email
|
|
775
|
-
|
|
776
|
-
if (options.organization?.enabled === true && referenceId !== void 0 && referenceId !== null && referenceId !== "" && referenceId !== user.id) {
|
|
761
|
+
let targetEmail = email ?? user.email;
|
|
762
|
+
if (options.organization?.enabled === true && referenceId !== void 0 && referenceId !== null && referenceId !== user.id) {
|
|
777
763
|
const org = await ctx.context.adapter.findOne({
|
|
778
764
|
model: "organization",
|
|
779
765
|
where: [{
|
|
@@ -782,8 +768,8 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
782
768
|
}]
|
|
783
769
|
});
|
|
784
770
|
if (org !== void 0 && org !== null) {
|
|
785
|
-
|
|
786
|
-
if (
|
|
771
|
+
const orgWithEmail = org;
|
|
772
|
+
if (orgWithEmail.email !== void 0 && orgWithEmail.email !== null && orgWithEmail.email !== "") targetEmail = orgWithEmail.email;
|
|
787
773
|
else {
|
|
788
774
|
const ownerMember = await ctx.context.adapter.findOne({
|
|
789
775
|
model: "member",
|
|
@@ -803,110 +789,177 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
803
789
|
value: ownerMember.userId
|
|
804
790
|
}]
|
|
805
791
|
});
|
|
806
|
-
if (ownerUser
|
|
792
|
+
if (ownerUser !== void 0 && ownerUser !== null && ownerUser.email !== void 0 && ownerUser.email !== null && ownerUser.email !== "") targetEmail = ownerUser.email;
|
|
807
793
|
}
|
|
808
794
|
}
|
|
809
795
|
}
|
|
810
796
|
}
|
|
797
|
+
const allowedSubscriptionChannels = plan ? getAllowedSubscriptionChannels(options) : void 0;
|
|
811
798
|
const metadata = JSON.stringify({
|
|
812
799
|
referenceId,
|
|
813
800
|
userId: user.id,
|
|
814
|
-
plan: plan
|
|
815
|
-
product: product
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
801
|
+
plan: plan !== void 0 ? plan.name.toLowerCase() : void 0,
|
|
802
|
+
product: product !== void 0 ? product.name.toLowerCase() : void 0,
|
|
803
|
+
...extraMetadata,
|
|
804
|
+
isTrial: trialStart !== void 0,
|
|
805
|
+
trialRequested,
|
|
806
|
+
trialGranted,
|
|
807
|
+
trialDeniedReason,
|
|
808
|
+
trialEnd: trialEnd !== void 0 ? trialEnd.toISOString() : void 0
|
|
819
809
|
});
|
|
820
810
|
const initBody = {
|
|
821
811
|
email: targetEmail,
|
|
822
|
-
callback_url: callbackURL,
|
|
812
|
+
callback_url: callbackURL ?? void 0,
|
|
823
813
|
metadata,
|
|
824
814
|
currency: finalCurrency,
|
|
825
815
|
quantity
|
|
826
816
|
};
|
|
827
|
-
if (
|
|
828
|
-
|
|
829
|
-
if (initBody.email !== void 0 && initBody.email !== null && initBody.email !== "") await ops.customerUpdate(paystackCustomerCode, { email: initBody.email });
|
|
830
|
-
} catch (_e) {}
|
|
831
|
-
if (plan !== void 0 && plan !== null && prorateAndCharge === true) {
|
|
817
|
+
if (allowedSubscriptionChannels !== void 0) initBody.channels = allowedSubscriptionChannels;
|
|
818
|
+
if (plan !== void 0 && prorateAndCharge === true) {
|
|
832
819
|
const existingSub = await getOrganizationSubscription(ctx, referenceId);
|
|
833
|
-
if (existingSub?.status === "active" && existingSub.
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
820
|
+
if (existingSub?.status === "active" && existingSub.paystackSubscriptionCode !== void 0 && existingSub.paystackSubscriptionCode !== null && existingSub.paystackSubscriptionCode !== "") {
|
|
821
|
+
if (existingSub.periodEnd !== void 0 && existingSub.periodEnd !== null && existingSub.periodStart !== void 0 && existingSub.periodStart !== null) {
|
|
822
|
+
const now = /* @__PURE__ */ new Date();
|
|
823
|
+
const periodEndLocal = new Date(existingSub.periodEnd);
|
|
824
|
+
const periodStartLocal = new Date(existingSub.periodStart);
|
|
825
|
+
const totalDays = Math.max(1, Math.ceil((periodEndLocal.getTime() - periodStartLocal.getTime()) / (1e3 * 60 * 60 * 24)));
|
|
826
|
+
const remainingDays = Math.max(0, Math.ceil((periodEndLocal.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24)));
|
|
827
|
+
let oldAmount = 0;
|
|
828
|
+
if (existingSub.plan !== "") {
|
|
829
|
+
const oldPlan = await getPlanByName(options, existingSub.plan) ?? await ctx.context.adapter.findOne({
|
|
830
|
+
model: "paystackPlan",
|
|
831
|
+
where: [{
|
|
832
|
+
field: "name",
|
|
833
|
+
value: existingSub.plan
|
|
834
|
+
}]
|
|
835
|
+
}) ?? void 0;
|
|
836
|
+
if (oldPlan !== void 0 && oldPlan !== null) {
|
|
837
|
+
const oldSeatCount = existingSub.seats;
|
|
838
|
+
oldAmount = calculatePlanAmount(oldPlan, oldSeatCount);
|
|
839
|
+
}
|
|
851
840
|
}
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
841
|
+
let membersCount = 1;
|
|
842
|
+
let newSeatCount = quantity ?? existingSub.seats ?? membersCount;
|
|
843
|
+
let newAmount;
|
|
844
|
+
try {
|
|
845
|
+
assertLocallyManagedSubscription(existingSub, "plan or seat changes");
|
|
846
|
+
if (getPlanSeatAmount(plan) !== void 0) {
|
|
847
|
+
const members = await ctx.context.adapter.findMany({
|
|
848
|
+
model: "member",
|
|
849
|
+
where: [{
|
|
850
|
+
field: "organizationId",
|
|
851
|
+
value: referenceId
|
|
852
|
+
}]
|
|
853
|
+
});
|
|
854
|
+
membersCount = members.length > 0 ? members.length : 1;
|
|
855
|
+
}
|
|
856
|
+
newSeatCount = quantity ?? existingSub.seats ?? membersCount;
|
|
857
|
+
newAmount = calculatePlanAmount(plan, newSeatCount);
|
|
858
|
+
} catch (error) {
|
|
859
|
+
throw new APIError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Invalid seat configuration for plan." });
|
|
860
|
+
}
|
|
861
|
+
const costDifference = newAmount - oldAmount;
|
|
862
|
+
const prorationMetadata = {
|
|
863
|
+
type: "proration",
|
|
864
|
+
subscriptionId: existingSub.id,
|
|
865
|
+
referenceId,
|
|
866
|
+
newPlan: plan.name.toLowerCase(),
|
|
867
|
+
oldPlan: existingSub.plan,
|
|
868
|
+
newSeatCount,
|
|
869
|
+
remainingDays
|
|
870
|
+
};
|
|
871
|
+
let completedProrationReference;
|
|
872
|
+
if (costDifference > 0 && remainingDays > 0) {
|
|
873
|
+
const proratedAmount = Math.round(costDifference / totalDays * remainingDays);
|
|
874
|
+
if (proratedAmount < 5e3) throw new APIError("BAD_REQUEST", {
|
|
875
|
+
message: "Prorated upgrade amount is below Paystack's minimum charge. Schedule the change for period end instead.",
|
|
876
|
+
status: 400
|
|
877
|
+
});
|
|
878
|
+
const ops = getPaystackOps(options.paystackClient);
|
|
879
|
+
if (ops === void 0 || ops === null) {
|
|
880
|
+
ctx.context.logger.error("Paystack client not configured for proration charge");
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
if (existingSub.paystackAuthorizationCode !== void 0 && existingSub.paystackAuthorizationCode !== null && existingSub.paystackAuthorizationCode !== "") {
|
|
884
|
+
const sdkRes = unwrapSdkResult(await ops.transaction?.chargeAuthorization({ body: {
|
|
885
|
+
email: targetEmail,
|
|
886
|
+
amount: proratedAmount,
|
|
887
|
+
authorization_code: existingSub.paystackAuthorizationCode,
|
|
888
|
+
reference: `upg_${existingSub.id}_${Date.now()}_${Math.random().toString(36).substring(7)}`,
|
|
889
|
+
metadata: JSON.stringify(prorationMetadata)
|
|
890
|
+
} }));
|
|
891
|
+
if (sdkRes?.status !== "success") throw new APIError("BAD_REQUEST", { message: "Failed to process prorated charge via saved authorization." });
|
|
892
|
+
await ctx.context.adapter.create({
|
|
893
|
+
model: "paystackTransaction",
|
|
894
|
+
data: {
|
|
895
|
+
reference: sdkRes.reference ?? "",
|
|
896
|
+
paystackId: sdkRes.id !== void 0 && sdkRes.id !== null ? String(sdkRes.id) : void 0,
|
|
897
|
+
referenceId,
|
|
898
|
+
userId: user.id,
|
|
899
|
+
amount: sdkRes.amount ?? proratedAmount,
|
|
900
|
+
currency: sdkRes.currency ?? finalCurrency,
|
|
901
|
+
status: "success",
|
|
902
|
+
plan: plan.name.toLowerCase(),
|
|
903
|
+
metadata: JSON.stringify(prorationMetadata),
|
|
904
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
905
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
completedProrationReference = sdkRes.reference ?? void 0;
|
|
909
|
+
} else {
|
|
910
|
+
const initRes = unwrapSdkResult(await ops.transaction?.initialize({ body: {
|
|
911
|
+
email: targetEmail,
|
|
912
|
+
amount: proratedAmount,
|
|
913
|
+
currency: finalCurrency,
|
|
914
|
+
callback_url: callbackURL ?? void 0,
|
|
915
|
+
metadata: JSON.stringify(prorationMetadata),
|
|
916
|
+
...allowedSubscriptionChannels !== void 0 ? { channels: allowedSubscriptionChannels } : {}
|
|
917
|
+
} }));
|
|
918
|
+
await ctx.context.adapter.create({
|
|
919
|
+
model: "paystackTransaction",
|
|
920
|
+
data: {
|
|
921
|
+
reference: initRes?.reference ?? "",
|
|
922
|
+
referenceId,
|
|
923
|
+
userId: user.id,
|
|
924
|
+
amount: proratedAmount,
|
|
925
|
+
currency: finalCurrency,
|
|
926
|
+
status: "pending",
|
|
927
|
+
plan: plan.name.toLowerCase(),
|
|
928
|
+
metadata: JSON.stringify(prorationMetadata),
|
|
929
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
930
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
return ctx.json({
|
|
934
|
+
url: initRes?.authorization_url,
|
|
935
|
+
reference: initRes?.reference,
|
|
936
|
+
accessCode: initRes?.access_code,
|
|
937
|
+
redirect: true
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
await ctx.context.adapter.update({
|
|
942
|
+
model: "subscription",
|
|
857
943
|
where: [{
|
|
858
|
-
field: "
|
|
859
|
-
value:
|
|
860
|
-
}]
|
|
944
|
+
field: "id",
|
|
945
|
+
value: existingSub.id
|
|
946
|
+
}],
|
|
947
|
+
update: {
|
|
948
|
+
plan: plan.name,
|
|
949
|
+
seats: newSeatCount,
|
|
950
|
+
...completedProrationReference !== void 0 ? { paystackTransactionReference: completedProrationReference } : {},
|
|
951
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
return ctx.json({
|
|
955
|
+
status: "success",
|
|
956
|
+
message: "Subscription successfully upgraded with prorated charge.",
|
|
957
|
+
prorated: true
|
|
861
958
|
});
|
|
862
|
-
membersCount = members.length > 0 ? members.length : 1;
|
|
863
|
-
}
|
|
864
|
-
const newSeatCount = quantity ?? existingSub.seats ?? membersCount;
|
|
865
|
-
const newAmount = (plan.amount ?? 0) + newSeatCount * (plan.seatAmount ?? plan.seatPriceId ?? 0);
|
|
866
|
-
const costDifference = newAmount - oldAmount;
|
|
867
|
-
if (costDifference > 0 && remainingDays > 0) {
|
|
868
|
-
const proratedAmount = Math.round(costDifference / totalDays * remainingDays);
|
|
869
|
-
if (proratedAmount >= 5e3) {
|
|
870
|
-
if (unwrapSdkResult(await getPaystackOps(options.paystackClient).transactionChargeAuthorization({
|
|
871
|
-
email: targetEmail,
|
|
872
|
-
amount: proratedAmount,
|
|
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
|
|
881
|
-
}
|
|
882
|
-
}))?.status !== "success") throw new APIError("BAD_REQUEST", { message: "Failed to process prorated charge via saved authorization." });
|
|
883
|
-
}
|
|
884
959
|
}
|
|
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
960
|
}
|
|
908
961
|
}
|
|
909
|
-
if (plan !== void 0
|
|
962
|
+
if (plan !== void 0) if (trialStart !== void 0) initBody.amount = 5e3;
|
|
910
963
|
else {
|
|
911
964
|
initBody.plan = plan.planCode;
|
|
912
965
|
initBody.invoice_limit = plan.invoiceLimit;
|
|
@@ -918,37 +971,37 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
918
971
|
initBody.amount = Math.max(Math.round(finalAmount), 5e3);
|
|
919
972
|
}
|
|
920
973
|
else {
|
|
921
|
-
if (amount === void 0 || amount === null
|
|
974
|
+
if (amount === void 0 || amount === null) throw new APIError("BAD_REQUEST", { message: "Amount is required for one-time payments" });
|
|
922
975
|
initBody.amount = Math.round(amount);
|
|
923
976
|
}
|
|
924
|
-
const sdkRes = unwrapSdkResult(await paystack
|
|
925
|
-
url = sdkRes?.authorization_url
|
|
926
|
-
reference = sdkRes?.reference
|
|
927
|
-
accessCode = sdkRes?.access_code
|
|
977
|
+
const sdkRes = unwrapSdkResult(await paystack?.transaction?.initialize({ body: initBody }));
|
|
978
|
+
url = sdkRes?.authorization_url;
|
|
979
|
+
reference = sdkRes?.reference;
|
|
980
|
+
accessCode = sdkRes?.access_code;
|
|
928
981
|
} catch (error) {
|
|
929
982
|
ctx.context.logger.error("Failed to initialize Paystack transaction", error);
|
|
930
983
|
throw new APIError("BAD_REQUEST", {
|
|
931
984
|
code: "FAILED_TO_INITIALIZE_TRANSACTION",
|
|
932
|
-
message: error
|
|
985
|
+
message: error instanceof Error ? error.message : PAYSTACK_ERROR_CODES.FAILED_TO_INITIALIZE_TRANSACTION.message
|
|
933
986
|
});
|
|
934
987
|
}
|
|
935
988
|
await ctx.context.adapter.create({
|
|
936
989
|
model: "paystackTransaction",
|
|
937
990
|
data: {
|
|
938
|
-
reference,
|
|
991
|
+
reference: reference ?? "",
|
|
939
992
|
referenceId,
|
|
940
993
|
userId: user.id,
|
|
941
994
|
amount: amount ?? 0,
|
|
942
995
|
currency: plan?.currency ?? currency ?? "NGN",
|
|
943
996
|
status: "pending",
|
|
944
|
-
plan: plan
|
|
945
|
-
product: product
|
|
946
|
-
metadata: extraMetadata !== void 0 &&
|
|
997
|
+
plan: plan !== void 0 ? plan.name.toLowerCase() : void 0,
|
|
998
|
+
product: product !== void 0 ? product.name.toLowerCase() : void 0,
|
|
999
|
+
metadata: extraMetadata !== void 0 && Object.keys(extraMetadata).length > 0 ? JSON.stringify(extraMetadata) : void 0,
|
|
947
1000
|
createdAt: /* @__PURE__ */ new Date(),
|
|
948
1001
|
updatedAt: /* @__PURE__ */ new Date()
|
|
949
1002
|
}
|
|
950
1003
|
});
|
|
951
|
-
if (plan !== void 0
|
|
1004
|
+
if (plan !== void 0) {
|
|
952
1005
|
let storedCustomerCode = user.paystackCustomerCode;
|
|
953
1006
|
if (options.organization?.enabled === true && referenceId !== user.id) {
|
|
954
1007
|
const org = await ctx.context.adapter.findOne({
|
|
@@ -958,22 +1011,34 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
958
1011
|
value: referenceId
|
|
959
1012
|
}]
|
|
960
1013
|
});
|
|
961
|
-
if (org
|
|
1014
|
+
if (org !== void 0 && org !== null) {
|
|
1015
|
+
const paystackOrg = org;
|
|
1016
|
+
if (paystackOrg.paystackCustomerCode !== void 0 && paystackOrg.paystackCustomerCode !== null && paystackOrg.paystackCustomerCode !== "") storedCustomerCode = paystackOrg.paystackCustomerCode;
|
|
1017
|
+
}
|
|
962
1018
|
}
|
|
963
1019
|
const newSubscription = await ctx.context.adapter.create({
|
|
964
1020
|
model: "subscription",
|
|
965
1021
|
data: {
|
|
966
1022
|
plan: plan.name.toLowerCase(),
|
|
967
1023
|
referenceId,
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1024
|
+
userId: user.id,
|
|
1025
|
+
paystackCustomerCode: storedCustomerCode ?? "",
|
|
1026
|
+
paystackSubscriptionCode: "",
|
|
1027
|
+
paystackPlanCode: plan.planCode,
|
|
1028
|
+
paystackAuthorizationCode: "",
|
|
1029
|
+
paystackTransactionReference: reference ?? "",
|
|
1030
|
+
status: trialStart !== void 0 ? "trialing" : "incomplete",
|
|
1031
|
+
seats: quantity ?? 1,
|
|
1032
|
+
periodStart: /* @__PURE__ */ new Date(),
|
|
1033
|
+
periodEnd: new Date(Date.now() + 720 * 60 * 60 * 1e3),
|
|
1034
|
+
cancelAtPeriodEnd: false,
|
|
972
1035
|
trialStart,
|
|
973
|
-
trialEnd
|
|
1036
|
+
trialEnd,
|
|
1037
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
1038
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
974
1039
|
}
|
|
975
1040
|
});
|
|
976
|
-
if (trialStart !== void 0 &&
|
|
1041
|
+
if (trialStart !== void 0 && newSubscription !== void 0 && newSubscription !== null && plan.freeTrial?.onTrialStart !== void 0) await plan.freeTrial.onTrialStart(newSubscription);
|
|
977
1042
|
}
|
|
978
1043
|
return ctx.json({
|
|
979
1044
|
url,
|
|
@@ -983,15 +1048,11 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
983
1048
|
});
|
|
984
1049
|
});
|
|
985
1050
|
};
|
|
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") => {
|
|
1051
|
+
const createSubscription = (options, path = "/create-subscription") => initializeTransaction(options, path);
|
|
1052
|
+
const upgradeSubscription = (options, path = "/upgrade-subscription") => initializeTransaction(options, path);
|
|
1053
|
+
const cancelSubscription = (options, path = "/cancel-subscription") => disablePaystackSubscription(options, path);
|
|
1054
|
+
const restoreSubscription = (options, path = "/restore-subscription") => enablePaystackSubscription(options, path);
|
|
1055
|
+
const verifyTransaction = (options, path = "/verify-transaction") => {
|
|
995
1056
|
const verifyBodySchema = z.object({ reference: z.string() });
|
|
996
1057
|
const subscriptionOptions = options.subscription;
|
|
997
1058
|
return createAuthEndpoint(path, {
|
|
@@ -1004,56 +1065,76 @@ const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
|
|
|
1004
1065
|
] : [sessionMiddleware, originCheck]
|
|
1005
1066
|
}, async (ctx) => {
|
|
1006
1067
|
const paystack = getPaystackOps(options.paystackClient);
|
|
1007
|
-
let
|
|
1068
|
+
let data;
|
|
1008
1069
|
try {
|
|
1009
|
-
|
|
1070
|
+
data = unwrapSdkResult(await paystack?.transaction?.verify(ctx.body.reference));
|
|
1010
1071
|
} catch (error) {
|
|
1011
1072
|
ctx.context.logger.error("Failed to verify Paystack transaction", error);
|
|
1012
1073
|
throw new APIError("BAD_REQUEST", {
|
|
1013
1074
|
code: "FAILED_TO_VERIFY_TRANSACTION",
|
|
1014
|
-
message: error
|
|
1075
|
+
message: error instanceof Error ? error.message : PAYSTACK_ERROR_CODES.FAILED_TO_VERIFY_TRANSACTION.message
|
|
1015
1076
|
});
|
|
1016
1077
|
}
|
|
1017
|
-
|
|
1018
|
-
const
|
|
1019
|
-
const
|
|
1020
|
-
const
|
|
1021
|
-
const paystackId =
|
|
1022
|
-
const authorizationCode =
|
|
1078
|
+
if (data === void 0 || data === null) throw new APIError("BAD_REQUEST", { message: "Failed to fetch transaction data from Paystack." });
|
|
1079
|
+
const status = data.status ?? "failed";
|
|
1080
|
+
const reference = data.reference ?? ctx.body.reference;
|
|
1081
|
+
const paystackIdRaw = data.id;
|
|
1082
|
+
const paystackId = paystackIdRaw !== void 0 && paystackIdRaw !== null ? String(paystackIdRaw) : void 0;
|
|
1083
|
+
const authorizationCode = data.authorization?.authorization_code;
|
|
1084
|
+
const allowedSubscriptionChannels = getAllowedSubscriptionChannels(options);
|
|
1023
1085
|
if (status === "success") {
|
|
1024
1086
|
const session = await getSessionFromCtx(ctx);
|
|
1025
|
-
const
|
|
1087
|
+
const txRecord = await ctx.context.adapter.findOne({
|
|
1026
1088
|
model: "paystackTransaction",
|
|
1027
1089
|
where: [{
|
|
1028
1090
|
field: "reference",
|
|
1029
1091
|
value: reference
|
|
1030
1092
|
}]
|
|
1031
|
-
})
|
|
1032
|
-
|
|
1093
|
+
});
|
|
1094
|
+
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;
|
|
1095
|
+
if ((txRecord?.plan !== void 0 && txRecord.plan !== null && txRecord.plan !== "" || Boolean(data.plan)) && isAllowedSubscriptionChannel(data.channel ?? void 0, allowedSubscriptionChannels) === false) {
|
|
1096
|
+
await ctx.context.adapter.update({
|
|
1097
|
+
model: "paystackTransaction",
|
|
1098
|
+
update: {
|
|
1099
|
+
status: "failed",
|
|
1100
|
+
paystackId,
|
|
1101
|
+
amount: data.amount,
|
|
1102
|
+
currency: data.currency,
|
|
1103
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1104
|
+
},
|
|
1105
|
+
where: [{
|
|
1106
|
+
field: "reference",
|
|
1107
|
+
value: reference
|
|
1108
|
+
}]
|
|
1109
|
+
});
|
|
1110
|
+
throw new APIError("BAD_REQUEST", {
|
|
1111
|
+
code: "SUBSCRIPTION_PAYMENT_CHANNEL_NOT_ALLOWED",
|
|
1112
|
+
message: `This subscription requires one of: ${allowedSubscriptionChannels?.join(", ") ?? "allowed channels"}.`
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
if (session !== void 0 && session !== null && referenceId !== void 0 && referenceId !== null && referenceId !== "" && referenceId !== session.user.id) {
|
|
1033
1116
|
const authRef = subscriptionOptions?.authorizeReference;
|
|
1034
1117
|
let authorized = false;
|
|
1035
|
-
if (authRef !== void 0) authorized = await authRef({
|
|
1118
|
+
if (authRef !== void 0 && authRef !== null) authorized = await authRef({
|
|
1036
1119
|
user: session.user,
|
|
1037
|
-
session,
|
|
1120
|
+
session: session.session,
|
|
1038
1121
|
referenceId,
|
|
1039
1122
|
action: "verify-transaction"
|
|
1040
1123
|
}, 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
|
-
}
|
|
1124
|
+
if (authorized === false && options.organization?.enabled === true) {
|
|
1125
|
+
const member = await ctx.context.adapter.findOne({
|
|
1126
|
+
model: "member",
|
|
1127
|
+
where: [{
|
|
1128
|
+
field: "userId",
|
|
1129
|
+
value: session.user.id
|
|
1130
|
+
}, {
|
|
1131
|
+
field: "organizationId",
|
|
1132
|
+
value: referenceId
|
|
1133
|
+
}]
|
|
1134
|
+
});
|
|
1135
|
+
if (member !== void 0 && member !== null) authorized = true;
|
|
1055
1136
|
}
|
|
1056
|
-
if (
|
|
1137
|
+
if (authorized === false) throw new APIError("UNAUTHORIZED");
|
|
1057
1138
|
}
|
|
1058
1139
|
try {
|
|
1059
1140
|
await ctx.context.adapter.update({
|
|
@@ -1061,8 +1142,8 @@ const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
|
|
|
1061
1142
|
update: {
|
|
1062
1143
|
status: "success",
|
|
1063
1144
|
paystackId,
|
|
1064
|
-
|
|
1065
|
-
|
|
1145
|
+
amount: data.amount,
|
|
1146
|
+
currency: data.currency,
|
|
1066
1147
|
updatedAt: /* @__PURE__ */ new Date()
|
|
1067
1148
|
},
|
|
1068
1149
|
where: [{
|
|
@@ -1070,11 +1151,10 @@ const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
|
|
|
1070
1151
|
value: reference
|
|
1071
1152
|
}]
|
|
1072
1153
|
});
|
|
1073
|
-
const
|
|
1074
|
-
const paystackCustomerCodeFromPaystack = customer !== void 0 && customer !== null && typeof customer === "object" ? customer.customer_code : void 0;
|
|
1154
|
+
const paystackCustomerCodeFromPaystack = data.customer?.customer_code;
|
|
1075
1155
|
if (paystackCustomerCodeFromPaystack !== void 0 && paystackCustomerCodeFromPaystack !== null && paystackCustomerCodeFromPaystack !== "" && referenceId !== void 0 && referenceId !== null && referenceId !== "") {
|
|
1076
1156
|
let isOrg = options.organization?.enabled === true && typeof referenceId === "string" && referenceId.startsWith("org_");
|
|
1077
|
-
if (
|
|
1157
|
+
if (isOrg === false && options.organization?.enabled === true) {
|
|
1078
1158
|
const org = await ctx.context.adapter.findOne({
|
|
1079
1159
|
model: "organization",
|
|
1080
1160
|
where: [{
|
|
@@ -1082,9 +1162,9 @@ const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
|
|
|
1082
1162
|
value: referenceId
|
|
1083
1163
|
}]
|
|
1084
1164
|
});
|
|
1085
|
-
isOrg = org !==
|
|
1165
|
+
isOrg = org !== void 0 && org !== null;
|
|
1086
1166
|
}
|
|
1087
|
-
if (isOrg
|
|
1167
|
+
if (isOrg) await ctx.context.adapter.update({
|
|
1088
1168
|
model: "organization",
|
|
1089
1169
|
update: { paystackCustomerCode: paystackCustomerCodeFromPaystack },
|
|
1090
1170
|
where: [{
|
|
@@ -1108,45 +1188,64 @@ const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
|
|
|
1108
1188
|
value: reference
|
|
1109
1189
|
}]
|
|
1110
1190
|
});
|
|
1111
|
-
if (transaction
|
|
1191
|
+
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
1192
|
let isTrial = false;
|
|
1113
1193
|
let trialEnd;
|
|
1114
1194
|
let targetPlan;
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
isTrial =
|
|
1119
|
-
trialEnd =
|
|
1120
|
-
targetPlan =
|
|
1195
|
+
let metadataObj = {};
|
|
1196
|
+
if (data.metadata !== void 0 && data.metadata !== null && data.metadata !== "") {
|
|
1197
|
+
metadataObj = typeof data.metadata === "string" ? JSON.parse(data.metadata) : data.metadata;
|
|
1198
|
+
isTrial = metadataObj.isTrial === true || metadataObj.isTrial === "true";
|
|
1199
|
+
trialEnd = metadataObj.trialEnd;
|
|
1200
|
+
targetPlan = metadataObj.plan;
|
|
1201
|
+
}
|
|
1202
|
+
if (metadataObj.type === "proration") {
|
|
1203
|
+
const subscriptionId = metadataObj.subscriptionId;
|
|
1204
|
+
const newPlan = metadataObj.newPlan;
|
|
1205
|
+
const newSeatCount = metadataObj.newSeatCount;
|
|
1206
|
+
if (subscriptionId !== void 0 && subscriptionId !== "" && newPlan !== void 0 && newPlan !== "") await ctx.context.adapter.update({
|
|
1207
|
+
model: "subscription",
|
|
1208
|
+
update: {
|
|
1209
|
+
plan: newPlan,
|
|
1210
|
+
...typeof newSeatCount === "number" ? { seats: newSeatCount } : {},
|
|
1211
|
+
paystackTransactionReference: reference,
|
|
1212
|
+
...authorizationCode !== void 0 && authorizationCode !== null ? { paystackAuthorizationCode: authorizationCode } : {},
|
|
1213
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1214
|
+
},
|
|
1215
|
+
where: [{
|
|
1216
|
+
field: "id",
|
|
1217
|
+
value: subscriptionId
|
|
1218
|
+
}]
|
|
1219
|
+
});
|
|
1220
|
+
return ctx.json({
|
|
1221
|
+
status,
|
|
1222
|
+
reference,
|
|
1223
|
+
data
|
|
1224
|
+
});
|
|
1121
1225
|
}
|
|
1122
1226
|
let paystackSubscriptionCode;
|
|
1123
|
-
if (isTrial
|
|
1227
|
+
if (isTrial && targetPlan !== void 0 && trialEnd !== void 0) {
|
|
1124
1228
|
const email = data.customer?.email;
|
|
1125
1229
|
const planConfig = (await getPlans(subscriptionOptions)).find((p) => p.name.toLowerCase() === targetPlan?.toLowerCase());
|
|
1126
1230
|
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;
|
|
1231
|
+
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: {
|
|
1232
|
+
customer: email,
|
|
1233
|
+
plan: planConfig.planCode,
|
|
1234
|
+
authorization: authorizationCode,
|
|
1235
|
+
start_date: trialEnd
|
|
1236
|
+
} }))?.subscription_code;
|
|
1237
|
+
} else if (isTrial === false) {
|
|
1238
|
+
const planCodeFromPaystack = data.plan?.plan_code;
|
|
1138
1239
|
if (planCodeFromPaystack === void 0 || planCodeFromPaystack === null || planCodeFromPaystack === "") paystackSubscriptionCode = `LOC_${reference}`;
|
|
1139
|
-
else paystackSubscriptionCode =
|
|
1240
|
+
else paystackSubscriptionCode = data.subscription?.subscription_code ?? void 0;
|
|
1140
1241
|
}
|
|
1141
|
-
const
|
|
1242
|
+
const targetSub = (await ctx.context.adapter.findMany({
|
|
1142
1243
|
model: "subscription",
|
|
1143
1244
|
where: [{
|
|
1144
1245
|
field: "paystackTransactionReference",
|
|
1145
1246
|
value: reference
|
|
1146
1247
|
}]
|
|
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);
|
|
1248
|
+
}))?.find((s) => referenceId === void 0 || referenceId === null || referenceId === "" || s.referenceId === referenceId);
|
|
1150
1249
|
let updatedSubscription = null;
|
|
1151
1250
|
if (targetSub !== void 0 && targetSub !== null) updatedSubscription = await ctx.context.adapter.update({
|
|
1152
1251
|
model: "subscription",
|
|
@@ -1154,22 +1253,22 @@ const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
|
|
|
1154
1253
|
status: isTrial ? "trialing" : "active",
|
|
1155
1254
|
periodStart: /* @__PURE__ */ new Date(),
|
|
1156
1255
|
updatedAt: /* @__PURE__ */ new Date(),
|
|
1157
|
-
...isTrial
|
|
1256
|
+
...isTrial && trialEnd !== void 0 ? {
|
|
1158
1257
|
trialStart: /* @__PURE__ */ new Date(),
|
|
1159
1258
|
trialEnd: new Date(trialEnd),
|
|
1160
1259
|
periodEnd: new Date(trialEnd)
|
|
1161
1260
|
} : {},
|
|
1162
|
-
...paystackSubscriptionCode !== void 0
|
|
1163
|
-
...authorizationCode !== void 0 && authorizationCode !== null
|
|
1261
|
+
...paystackSubscriptionCode !== void 0 ? { paystackSubscriptionCode } : {},
|
|
1262
|
+
...authorizationCode !== void 0 && authorizationCode !== null ? { paystackAuthorizationCode: authorizationCode } : {}
|
|
1164
1263
|
},
|
|
1165
1264
|
where: [{
|
|
1166
1265
|
field: "id",
|
|
1167
1266
|
value: targetSub.id
|
|
1168
1267
|
}]
|
|
1169
1268
|
});
|
|
1170
|
-
if (updatedSubscription
|
|
1269
|
+
if (updatedSubscription !== void 0 && updatedSubscription !== null && subscriptionOptions?.onSubscriptionComplete !== void 0) {
|
|
1171
1270
|
const plan = (await getPlans(subscriptionOptions)).find((p) => p.name.toLowerCase() === updatedSubscription.plan.toLowerCase());
|
|
1172
|
-
if (plan) await subscriptionOptions.onSubscriptionComplete({
|
|
1271
|
+
if (plan !== void 0) await subscriptionOptions.onSubscriptionComplete({
|
|
1173
1272
|
event: data,
|
|
1174
1273
|
subscription: updatedSubscription,
|
|
1175
1274
|
plan
|
|
@@ -1186,10 +1285,10 @@ const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
|
|
|
1186
1285
|
});
|
|
1187
1286
|
});
|
|
1188
1287
|
};
|
|
1189
|
-
const listSubscriptions = (options) => {
|
|
1288
|
+
const listSubscriptions = (options, path = "/list-subscriptions") => {
|
|
1190
1289
|
const listQuerySchema = z.object({ referenceId: z.string().optional() });
|
|
1191
1290
|
const subscriptionOptions = options.subscription;
|
|
1192
|
-
return createAuthEndpoint(
|
|
1291
|
+
return createAuthEndpoint(path, {
|
|
1193
1292
|
method: "GET",
|
|
1194
1293
|
query: listQuerySchema,
|
|
1195
1294
|
use: subscriptionOptions?.enabled === true ? [
|
|
@@ -1200,7 +1299,7 @@ const listSubscriptions = (options) => {
|
|
|
1200
1299
|
}, async (ctx) => {
|
|
1201
1300
|
if (subscriptionOptions?.enabled !== true) throw new APIError("BAD_REQUEST", { message: "Subscriptions are not enabled in the Paystack options." });
|
|
1202
1301
|
const session = await getSessionFromCtx(ctx);
|
|
1203
|
-
if (
|
|
1302
|
+
if (session === void 0 || session === null) throw new APIError("UNAUTHORIZED");
|
|
1204
1303
|
const referenceIdPart = ctx.context.referenceId;
|
|
1205
1304
|
const queryRefId = ctx.query?.referenceId;
|
|
1206
1305
|
const referenceId = referenceIdPart ?? queryRefId ?? session.user.id;
|
|
@@ -1214,7 +1313,7 @@ const listSubscriptions = (options) => {
|
|
|
1214
1313
|
return ctx.json({ subscriptions: res });
|
|
1215
1314
|
});
|
|
1216
1315
|
};
|
|
1217
|
-
const listTransactions = (options, path = "/
|
|
1316
|
+
const listTransactions = (options, path = "/list-transactions") => {
|
|
1218
1317
|
return createAuthEndpoint(path, {
|
|
1219
1318
|
method: "GET",
|
|
1220
1319
|
query: z.object({ referenceId: z.string().optional() }),
|
|
@@ -1225,15 +1324,17 @@ const listTransactions = (options, path = "/paystack/list-transactions") => {
|
|
|
1225
1324
|
] : [sessionMiddleware, originCheck]
|
|
1226
1325
|
}, async (ctx) => {
|
|
1227
1326
|
const session = await getSessionFromCtx(ctx);
|
|
1228
|
-
if (
|
|
1229
|
-
const
|
|
1327
|
+
if (session === void 0 || session === null) throw new APIError("UNAUTHORIZED");
|
|
1328
|
+
const referenceIdPart = ctx.context.referenceId;
|
|
1329
|
+
const queryRefId = ctx.query?.referenceId;
|
|
1330
|
+
const referenceId = referenceIdPart ?? queryRefId ?? session.user.id;
|
|
1230
1331
|
const sorted = (await ctx.context.adapter.findMany({
|
|
1231
1332
|
model: "paystackTransaction",
|
|
1232
1333
|
where: [{
|
|
1233
1334
|
field: "referenceId",
|
|
1234
1335
|
value: referenceId
|
|
1235
1336
|
}]
|
|
1236
|
-
})).sort((a, b) =>
|
|
1337
|
+
})).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
1237
1338
|
return ctx.json({ transactions: sorted });
|
|
1238
1339
|
});
|
|
1239
1340
|
};
|
|
@@ -1246,8 +1347,10 @@ const enableDisableBodySchema = z.object({
|
|
|
1246
1347
|
function decodeBase64UrlToString(value) {
|
|
1247
1348
|
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
1248
1349
|
const padded = normalized + "===".slice((normalized.length + 3) % 4);
|
|
1249
|
-
|
|
1250
|
-
|
|
1350
|
+
const binaryString = atob(padded);
|
|
1351
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
1352
|
+
for (let i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i);
|
|
1353
|
+
return new TextDecoder().decode(bytes);
|
|
1251
1354
|
}
|
|
1252
1355
|
function tryGetEmailTokenFromSubscriptionManageLink(link) {
|
|
1253
1356
|
try {
|
|
@@ -1257,12 +1360,12 @@ function tryGetEmailTokenFromSubscriptionManageLink(link) {
|
|
|
1257
1360
|
if (parts.length < 2) return void 0;
|
|
1258
1361
|
const payloadJson = decodeBase64UrlToString(parts[1]);
|
|
1259
1362
|
const payload = JSON.parse(payloadJson);
|
|
1260
|
-
return typeof payload
|
|
1363
|
+
return typeof payload.email_token === "string" ? payload.email_token : void 0;
|
|
1261
1364
|
} catch {
|
|
1262
1365
|
return;
|
|
1263
1366
|
}
|
|
1264
1367
|
}
|
|
1265
|
-
const disablePaystackSubscription = (options, path = "/
|
|
1368
|
+
const disablePaystackSubscription = (options, path = "/disable-subscription") => {
|
|
1266
1369
|
return createAuthEndpoint(path, {
|
|
1267
1370
|
method: "POST",
|
|
1268
1371
|
body: enableDisableBodySchema,
|
|
@@ -1275,7 +1378,7 @@ const disablePaystackSubscription = (options, path = "/paystack/disable-subscrip
|
|
|
1275
1378
|
const { subscriptionCode, atPeriodEnd } = ctx.body;
|
|
1276
1379
|
const paystack = getPaystackOps(options.paystackClient);
|
|
1277
1380
|
try {
|
|
1278
|
-
if (subscriptionCode
|
|
1381
|
+
if (isLocalSubscriptionCode(subscriptionCode)) {
|
|
1279
1382
|
const sub = await ctx.context.adapter.findOne({
|
|
1280
1383
|
model: "subscription",
|
|
1281
1384
|
where: [{
|
|
@@ -1303,22 +1406,21 @@ const disablePaystackSubscription = (options, path = "/paystack/disable-subscrip
|
|
|
1303
1406
|
let emailToken = ctx.body.emailToken;
|
|
1304
1407
|
let nextPaymentDate;
|
|
1305
1408
|
try {
|
|
1306
|
-
const fetchRes = unwrapSdkResult(await paystack
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1409
|
+
const fetchRes = unwrapSdkResult(await paystack?.subscription?.fetch(subscriptionCode));
|
|
1410
|
+
if (fetchRes !== void 0 && fetchRes !== null) {
|
|
1411
|
+
emailToken ??= fetchRes.email_token ?? void 0;
|
|
1412
|
+
nextPaymentDate = fetchRes.next_payment_date ?? void 0;
|
|
1413
|
+
}
|
|
1310
1414
|
} catch {}
|
|
1311
1415
|
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);
|
|
1416
|
+
const link = unwrapSdkResult(await paystack?.subscription?.manageLink(subscriptionCode))?.link;
|
|
1417
|
+
if (link !== void 0 && link !== null && link !== "") emailToken = tryGetEmailTokenFromSubscriptionManageLink(link);
|
|
1316
1418
|
} catch {}
|
|
1317
1419
|
if (emailToken === void 0 || emailToken === null || emailToken === "") throw new Error("Could not retrieve email_token for subscription disable.");
|
|
1318
|
-
await paystack
|
|
1420
|
+
await paystack?.subscription?.disable({ body: {
|
|
1319
1421
|
code: subscriptionCode,
|
|
1320
1422
|
token: emailToken
|
|
1321
|
-
});
|
|
1423
|
+
} });
|
|
1322
1424
|
const periodEnd = nextPaymentDate !== void 0 && nextPaymentDate !== null && nextPaymentDate !== "" ? new Date(nextPaymentDate) : void 0;
|
|
1323
1425
|
const sub = await ctx.context.adapter.findOne({
|
|
1324
1426
|
model: "subscription",
|
|
@@ -1346,12 +1448,12 @@ const disablePaystackSubscription = (options, path = "/paystack/disable-subscrip
|
|
|
1346
1448
|
ctx.context.logger.error("Failed to disable subscription", error);
|
|
1347
1449
|
throw new APIError("BAD_REQUEST", {
|
|
1348
1450
|
code: "FAILED_TO_DISABLE_SUBSCRIPTION",
|
|
1349
|
-
message: error
|
|
1451
|
+
message: error instanceof Error ? error.message : PAYSTACK_ERROR_CODES.FAILED_TO_DISABLE_SUBSCRIPTION.message
|
|
1350
1452
|
});
|
|
1351
1453
|
}
|
|
1352
1454
|
});
|
|
1353
1455
|
};
|
|
1354
|
-
const enablePaystackSubscription = (options, path = "/
|
|
1456
|
+
const enablePaystackSubscription = (options, path = "/enable-subscription") => {
|
|
1355
1457
|
return createAuthEndpoint(path, {
|
|
1356
1458
|
method: "POST",
|
|
1357
1459
|
body: enableDisableBodySchema,
|
|
@@ -1366,20 +1468,18 @@ const enablePaystackSubscription = (options, path = "/paystack/enable-subscripti
|
|
|
1366
1468
|
try {
|
|
1367
1469
|
let emailToken = ctx.body.emailToken;
|
|
1368
1470
|
if (emailToken === void 0 || emailToken === null || emailToken === "") try {
|
|
1369
|
-
const fetchRes = unwrapSdkResult(await paystack
|
|
1370
|
-
emailToken =
|
|
1471
|
+
const fetchRes = unwrapSdkResult(await paystack?.subscription?.fetch(subscriptionCode));
|
|
1472
|
+
if (fetchRes !== void 0 && fetchRes !== null) emailToken = fetchRes.email_token ?? void 0;
|
|
1371
1473
|
} catch {}
|
|
1372
1474
|
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);
|
|
1475
|
+
const link = unwrapSdkResult(await paystack?.subscription?.manageLink(subscriptionCode))?.link;
|
|
1476
|
+
if (link !== void 0 && link !== null && link !== "") emailToken = tryGetEmailTokenFromSubscriptionManageLink(link);
|
|
1377
1477
|
} catch {}
|
|
1378
1478
|
if (emailToken === void 0 || emailToken === null || emailToken === "") throw new APIError("BAD_REQUEST", { message: "Could not retrieve email_token for subscription enable." });
|
|
1379
|
-
await paystack
|
|
1479
|
+
await paystack?.subscription?.enable({ body: {
|
|
1380
1480
|
code: subscriptionCode,
|
|
1381
1481
|
token: emailToken
|
|
1382
|
-
});
|
|
1482
|
+
} });
|
|
1383
1483
|
await ctx.context.adapter.update({
|
|
1384
1484
|
model: "subscription",
|
|
1385
1485
|
update: {
|
|
@@ -1396,12 +1496,12 @@ const enablePaystackSubscription = (options, path = "/paystack/enable-subscripti
|
|
|
1396
1496
|
ctx.context.logger.error("Failed to enable subscription", error);
|
|
1397
1497
|
throw new APIError("BAD_REQUEST", {
|
|
1398
1498
|
code: "FAILED_TO_ENABLE_SUBSCRIPTION",
|
|
1399
|
-
message: error
|
|
1499
|
+
message: error instanceof Error ? error.message : PAYSTACK_ERROR_CODES.FAILED_TO_ENABLE_SUBSCRIPTION.message
|
|
1400
1500
|
});
|
|
1401
1501
|
}
|
|
1402
1502
|
});
|
|
1403
1503
|
};
|
|
1404
|
-
const getSubscriptionManageLink = (options, path = "/
|
|
1504
|
+
const getSubscriptionManageLink = (options, path = "/subscription-manage-link") => {
|
|
1405
1505
|
const manageLinkQuerySchema = z.object({ subscriptionCode: z.string() });
|
|
1406
1506
|
const useMiddlewares = options.subscription?.enabled === true ? [
|
|
1407
1507
|
sessionMiddleware,
|
|
@@ -1410,19 +1510,17 @@ const getSubscriptionManageLink = (options, path = "/paystack/get-subscription-m
|
|
|
1410
1510
|
] : [sessionMiddleware, originCheck];
|
|
1411
1511
|
const handler = async (ctx) => {
|
|
1412
1512
|
const { subscriptionCode } = ctx.query;
|
|
1413
|
-
if (
|
|
1513
|
+
if (isLocalSubscriptionCode(subscriptionCode)) return ctx.json({
|
|
1414
1514
|
link: null,
|
|
1415
1515
|
message: "Local subscriptions cannot be managed on Paystack"
|
|
1416
1516
|
});
|
|
1417
1517
|
const paystack = getPaystackOps(options.paystackClient);
|
|
1418
1518
|
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 });
|
|
1519
|
+
const res = unwrapSdkResult(await paystack?.subscription?.manageLink(subscriptionCode));
|
|
1520
|
+
return ctx.json({ link: res?.link || null });
|
|
1423
1521
|
} catch (error) {
|
|
1424
1522
|
ctx.context.logger.error("Failed to get subscription manage link", error);
|
|
1425
|
-
throw new APIError("BAD_REQUEST", { message: error
|
|
1523
|
+
throw new APIError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Failed to get subscription manage link" });
|
|
1426
1524
|
}
|
|
1427
1525
|
};
|
|
1428
1526
|
return createAuthEndpoint(path, {
|
|
@@ -1431,69 +1529,8 @@ const getSubscriptionManageLink = (options, path = "/paystack/get-subscription-m
|
|
|
1431
1529
|
use: useMiddlewares
|
|
1432
1530
|
}, handler);
|
|
1433
1531
|
};
|
|
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", {
|
|
1532
|
+
const listProducts = (_options, path = "/list-products") => {
|
|
1533
|
+
return createAuthEndpoint(path, {
|
|
1497
1534
|
method: "GET",
|
|
1498
1535
|
metadata: { openapi: { operationId: "listPaystackProducts" } }
|
|
1499
1536
|
}, async (ctx) => {
|
|
@@ -1501,69 +1538,8 @@ const listProducts = (_options) => {
|
|
|
1501
1538
|
return ctx.json({ products: sorted });
|
|
1502
1539
|
});
|
|
1503
1540
|
};
|
|
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", {
|
|
1541
|
+
const listPlans = (_options, path = "/list-plans") => {
|
|
1542
|
+
return createAuthEndpoint(path, {
|
|
1567
1543
|
method: "GET",
|
|
1568
1544
|
metadata: { ...HIDE_METADATA },
|
|
1569
1545
|
use: [sessionMiddleware]
|
|
@@ -1573,12 +1549,12 @@ const listPlans = (_options) => {
|
|
|
1573
1549
|
return ctx.json({ plans });
|
|
1574
1550
|
} catch (error) {
|
|
1575
1551
|
ctx.context.logger.error("Failed to list plans", error);
|
|
1576
|
-
throw new APIError("BAD_REQUEST", { message: error
|
|
1552
|
+
throw new APIError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Failed to list plans" });
|
|
1577
1553
|
}
|
|
1578
1554
|
});
|
|
1579
1555
|
};
|
|
1580
|
-
const getConfig = (options) => {
|
|
1581
|
-
return createAuthEndpoint(
|
|
1556
|
+
const getConfig = (options, path = "/get-config") => {
|
|
1557
|
+
return createAuthEndpoint(path, {
|
|
1582
1558
|
method: "GET",
|
|
1583
1559
|
metadata: { openapi: { operationId: "getPaystackConfig" } }
|
|
1584
1560
|
}, async (ctx) => {
|
|
@@ -1590,103 +1566,6 @@ const getConfig = (options) => {
|
|
|
1590
1566
|
});
|
|
1591
1567
|
});
|
|
1592
1568
|
};
|
|
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
1569
|
//#endregion
|
|
1691
1570
|
//#region src/schema.ts
|
|
1692
1571
|
const transactions = { paystackTransaction: { fields: {
|
|
@@ -1950,32 +1829,259 @@ const getSchema = (options) => {
|
|
|
1950
1829
|
return mergeSchema(baseSchema, options.schema);
|
|
1951
1830
|
};
|
|
1952
1831
|
//#endregion
|
|
1832
|
+
//#region src/operations.ts
|
|
1833
|
+
async function syncPaystackProducts(ctx, options) {
|
|
1834
|
+
const paystack = getPaystackOps(options.paystackClient);
|
|
1835
|
+
try {
|
|
1836
|
+
const productsData = unwrapSdkResult(await paystack?.product?.list({}));
|
|
1837
|
+
if (!Array.isArray(productsData)) return {
|
|
1838
|
+
status: "success",
|
|
1839
|
+
count: 0
|
|
1840
|
+
};
|
|
1841
|
+
for (const product of productsData) {
|
|
1842
|
+
const paystackId = String(product.id);
|
|
1843
|
+
const existing = await ctx.context.adapter.findOne({
|
|
1844
|
+
model: "paystackProduct",
|
|
1845
|
+
where: [{
|
|
1846
|
+
field: "paystackId",
|
|
1847
|
+
value: paystackId
|
|
1848
|
+
}]
|
|
1849
|
+
});
|
|
1850
|
+
const productFields = {
|
|
1851
|
+
name: product.name ?? "",
|
|
1852
|
+
description: product.description ?? "",
|
|
1853
|
+
price: product.price ?? 0,
|
|
1854
|
+
currency: product.currency ?? "",
|
|
1855
|
+
quantity: product.quantity ?? 0,
|
|
1856
|
+
unlimited: product.unlimited !== void 0 && product.unlimited !== null && product.unlimited !== false,
|
|
1857
|
+
paystackId,
|
|
1858
|
+
slug: product.slug ?? product.name?.toLowerCase().replace(/\s+/g, "-") ?? "",
|
|
1859
|
+
metadata: product.metadata !== void 0 && product.metadata !== null ? JSON.stringify(product.metadata) : void 0,
|
|
1860
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1861
|
+
};
|
|
1862
|
+
if (existing !== void 0 && existing !== null) await ctx.context.adapter.update({
|
|
1863
|
+
model: "paystackProduct",
|
|
1864
|
+
update: productFields,
|
|
1865
|
+
where: [{
|
|
1866
|
+
field: "id",
|
|
1867
|
+
value: String(existing.id)
|
|
1868
|
+
}]
|
|
1869
|
+
});
|
|
1870
|
+
else await ctx.context.adapter.create({
|
|
1871
|
+
model: "paystackProduct",
|
|
1872
|
+
data: {
|
|
1873
|
+
...productFields,
|
|
1874
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
1875
|
+
}
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
return {
|
|
1879
|
+
status: "success",
|
|
1880
|
+
count: productsData.length
|
|
1881
|
+
};
|
|
1882
|
+
} catch (error) {
|
|
1883
|
+
ctx.context.logger.error("Failed to sync products", error);
|
|
1884
|
+
throw new APIError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Failed to sync products" });
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
async function syncPaystackPlans(ctx, options) {
|
|
1888
|
+
const paystack = getPaystackOps(options.paystackClient);
|
|
1889
|
+
try {
|
|
1890
|
+
const plansData = unwrapSdkResult(await paystack?.plan?.list());
|
|
1891
|
+
if (!Array.isArray(plansData)) return {
|
|
1892
|
+
status: "success",
|
|
1893
|
+
count: 0
|
|
1894
|
+
};
|
|
1895
|
+
for (const plan of plansData) {
|
|
1896
|
+
const paystackId = String(plan.id);
|
|
1897
|
+
const existing = await ctx.context.adapter.findOne({
|
|
1898
|
+
model: "paystackPlan",
|
|
1899
|
+
where: [{
|
|
1900
|
+
field: "paystackId",
|
|
1901
|
+
value: paystackId
|
|
1902
|
+
}]
|
|
1903
|
+
});
|
|
1904
|
+
const planData = {
|
|
1905
|
+
name: plan.name ?? "",
|
|
1906
|
+
description: plan.description ?? "",
|
|
1907
|
+
amount: plan.amount ?? 0,
|
|
1908
|
+
currency: plan.currency ?? "",
|
|
1909
|
+
interval: plan.interval ?? "",
|
|
1910
|
+
planCode: plan.plan_code ?? "",
|
|
1911
|
+
paystackId,
|
|
1912
|
+
metadata: plan.metadata !== void 0 && plan.metadata !== null ? JSON.stringify(plan.metadata) : void 0,
|
|
1913
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1914
|
+
};
|
|
1915
|
+
if (existing !== void 0 && existing !== null) await ctx.context.adapter.update({
|
|
1916
|
+
model: "paystackPlan",
|
|
1917
|
+
update: planData,
|
|
1918
|
+
where: [{
|
|
1919
|
+
field: "id",
|
|
1920
|
+
value: existing.id
|
|
1921
|
+
}]
|
|
1922
|
+
});
|
|
1923
|
+
else await ctx.context.adapter.create({
|
|
1924
|
+
model: "paystackPlan",
|
|
1925
|
+
data: {
|
|
1926
|
+
...planData,
|
|
1927
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
1928
|
+
}
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
return {
|
|
1932
|
+
status: "success",
|
|
1933
|
+
count: plansData.length
|
|
1934
|
+
};
|
|
1935
|
+
} catch (error) {
|
|
1936
|
+
ctx.context.logger.error("Failed to sync plans", error);
|
|
1937
|
+
throw new APIError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Failed to sync plans" });
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
async function chargeSubscriptionRenewal(ctx, options, input) {
|
|
1941
|
+
const { subscriptionId, amount: bodyAmount } = input;
|
|
1942
|
+
const subscription = await ctx.context.adapter.findOne({
|
|
1943
|
+
model: "subscription",
|
|
1944
|
+
where: [{
|
|
1945
|
+
field: "id",
|
|
1946
|
+
value: subscriptionId
|
|
1947
|
+
}]
|
|
1948
|
+
});
|
|
1949
|
+
if (subscription === void 0 || subscription === null) throw new APIError("NOT_FOUND", { message: "Subscription not found" });
|
|
1950
|
+
if (subscription.paystackAuthorizationCode === void 0 || subscription.paystackAuthorizationCode === null || subscription.paystackAuthorizationCode === "") throw new APIError("BAD_REQUEST", { message: "No authorization code found for this subscription" });
|
|
1951
|
+
const plan = (await getPlans(options.subscription)).find((candidate) => candidate.name.toLowerCase() === subscription.plan.toLowerCase());
|
|
1952
|
+
if (plan === void 0 || plan === null) throw new APIError("NOT_FOUND", { message: "Plan not found" });
|
|
1953
|
+
const amount = bodyAmount ?? plan.amount;
|
|
1954
|
+
if (amount === void 0 || amount === null) throw new APIError("BAD_REQUEST", { message: "Plan amount is not defined" });
|
|
1955
|
+
let email;
|
|
1956
|
+
let billingUserId = subscription.userId;
|
|
1957
|
+
const referenceId = subscription.referenceId;
|
|
1958
|
+
if (referenceId !== void 0 && referenceId !== null && referenceId !== "") {
|
|
1959
|
+
const user = await ctx.context.adapter.findOne({
|
|
1960
|
+
model: "user",
|
|
1961
|
+
where: [{
|
|
1962
|
+
field: "id",
|
|
1963
|
+
value: referenceId
|
|
1964
|
+
}]
|
|
1965
|
+
});
|
|
1966
|
+
if (user !== void 0 && user !== null) {
|
|
1967
|
+
email = user.email;
|
|
1968
|
+
billingUserId = user.id;
|
|
1969
|
+
} else if (options.organization?.enabled === true) {
|
|
1970
|
+
const ownerMember = await ctx.context.adapter.findOne({
|
|
1971
|
+
model: "member",
|
|
1972
|
+
where: [{
|
|
1973
|
+
field: "organizationId",
|
|
1974
|
+
value: referenceId
|
|
1975
|
+
}, {
|
|
1976
|
+
field: "role",
|
|
1977
|
+
value: "owner"
|
|
1978
|
+
}]
|
|
1979
|
+
});
|
|
1980
|
+
if (ownerMember !== void 0 && ownerMember !== null) {
|
|
1981
|
+
const ownerUser = await ctx.context.adapter.findOne({
|
|
1982
|
+
model: "user",
|
|
1983
|
+
where: [{
|
|
1984
|
+
field: "id",
|
|
1985
|
+
value: ownerMember.userId
|
|
1986
|
+
}]
|
|
1987
|
+
});
|
|
1988
|
+
email = ownerUser?.email;
|
|
1989
|
+
billingUserId = ownerUser?.id ?? ownerMember.userId;
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
if (email === void 0 || email === null || email === "") throw new APIError("NOT_FOUND", { message: "User email not found" });
|
|
1994
|
+
const finalCurrency = plan.currency ?? "NGN";
|
|
1995
|
+
if (!validateMinAmount(amount, finalCurrency)) throw new APIError("BAD_REQUEST", {
|
|
1996
|
+
message: `Amount ${amount} is less than the minimum required for ${finalCurrency}.`,
|
|
1997
|
+
status: 400
|
|
1998
|
+
});
|
|
1999
|
+
const chargeData = unwrapSdkResult(await getPaystackOps(options.paystackClient)?.transaction?.chargeAuthorization({ body: {
|
|
2000
|
+
email,
|
|
2001
|
+
amount,
|
|
2002
|
+
authorization_code: subscription.paystackAuthorizationCode,
|
|
2003
|
+
reference: `rec_${subscription.id}_${Date.now()}`,
|
|
2004
|
+
metadata: JSON.stringify({
|
|
2005
|
+
subscriptionId,
|
|
2006
|
+
referenceId
|
|
2007
|
+
})
|
|
2008
|
+
} }));
|
|
2009
|
+
if (chargeData?.status === "success" && chargeData.reference !== void 0) {
|
|
2010
|
+
const now = /* @__PURE__ */ new Date();
|
|
2011
|
+
const nextPeriodEnd = getNextPeriodEnd(now, plan.interval ?? "monthly");
|
|
2012
|
+
await ctx.context.adapter.create({
|
|
2013
|
+
model: "paystackTransaction",
|
|
2014
|
+
data: {
|
|
2015
|
+
reference: chargeData.reference,
|
|
2016
|
+
paystackId: chargeData.id !== void 0 && chargeData.id !== null ? String(chargeData.id) : void 0,
|
|
2017
|
+
referenceId,
|
|
2018
|
+
userId: billingUserId,
|
|
2019
|
+
amount: chargeData.amount,
|
|
2020
|
+
currency: chargeData.currency,
|
|
2021
|
+
status: "success",
|
|
2022
|
+
plan: plan.name.toLowerCase(),
|
|
2023
|
+
metadata: JSON.stringify({
|
|
2024
|
+
type: "renewal",
|
|
2025
|
+
subscriptionId,
|
|
2026
|
+
referenceId
|
|
2027
|
+
}),
|
|
2028
|
+
createdAt: now,
|
|
2029
|
+
updatedAt: now
|
|
2030
|
+
}
|
|
2031
|
+
});
|
|
2032
|
+
await ctx.context.adapter.update({
|
|
2033
|
+
model: "subscription",
|
|
2034
|
+
update: {
|
|
2035
|
+
periodStart: now,
|
|
2036
|
+
periodEnd: nextPeriodEnd,
|
|
2037
|
+
updatedAt: now,
|
|
2038
|
+
paystackTransactionReference: chargeData.reference
|
|
2039
|
+
},
|
|
2040
|
+
where: [{
|
|
2041
|
+
field: "id",
|
|
2042
|
+
value: subscription.id
|
|
2043
|
+
}]
|
|
2044
|
+
});
|
|
2045
|
+
return {
|
|
2046
|
+
status: "success",
|
|
2047
|
+
data: chargeData
|
|
2048
|
+
};
|
|
2049
|
+
}
|
|
2050
|
+
return {
|
|
2051
|
+
status: "failed",
|
|
2052
|
+
data: chargeData
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
//#endregion
|
|
1953
2056
|
//#region src/index.ts
|
|
1954
|
-
const INTERNAL_ERROR_CODES = defineErrorCodes(
|
|
2057
|
+
const INTERNAL_ERROR_CODES = defineErrorCodes(Object.fromEntries(Object.entries(PAYSTACK_ERROR_CODES).map(([key, value]) => [key, typeof value === "string" ? value : value.message])));
|
|
1955
2058
|
const paystack = (options) => {
|
|
1956
|
-
const routeOptions =
|
|
2059
|
+
const routeOptions = {
|
|
2060
|
+
...options,
|
|
2061
|
+
webhook: {
|
|
2062
|
+
...options.webhook,
|
|
2063
|
+
secret: options.webhook?.secret ?? options.paystackWebhookSecret
|
|
2064
|
+
}
|
|
2065
|
+
};
|
|
1957
2066
|
return {
|
|
1958
2067
|
id: "paystack",
|
|
1959
2068
|
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),
|
|
2069
|
+
initializeTransaction: initializeTransaction(routeOptions, "/paystack/initialize-transaction"),
|
|
2070
|
+
verifyTransaction: verifyTransaction(routeOptions, "/paystack/verify-transaction"),
|
|
2071
|
+
listSubscriptions: listSubscriptions(routeOptions, "/paystack/list-subscriptions"),
|
|
2072
|
+
paystackWebhook: paystackWebhook(routeOptions, "/paystack/webhook"),
|
|
2073
|
+
listTransactions: listTransactions(routeOptions, "/paystack/list-transactions"),
|
|
2074
|
+
getConfig: getConfig(routeOptions, "/paystack/config"),
|
|
2075
|
+
disableSubscription: disablePaystackSubscription(routeOptions, "/paystack/disable-subscription"),
|
|
2076
|
+
enableSubscription: enablePaystackSubscription(routeOptions, "/paystack/enable-subscription"),
|
|
2077
|
+
getSubscriptionManageLink: getSubscriptionManageLink(routeOptions, "/paystack/subscription-manage-link"),
|
|
1969
2078
|
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)
|
|
2079
|
+
createSubscription: createSubscription(routeOptions, "/paystack/create-subscription"),
|
|
2080
|
+
upgradeSubscription: upgradeSubscription(routeOptions, "/paystack/upgrade-subscription"),
|
|
2081
|
+
cancelSubscription: cancelSubscription(routeOptions, "/paystack/cancel-subscription"),
|
|
2082
|
+
restoreSubscription: restoreSubscription(routeOptions, "/paystack/restore-subscription"),
|
|
2083
|
+
listProducts: listProducts(routeOptions, "/paystack/list-products"),
|
|
2084
|
+
listPlans: listPlans(routeOptions, "/paystack/list-plans")
|
|
1979
2085
|
},
|
|
1980
2086
|
schema: getSchema(options),
|
|
1981
2087
|
init: (ctx) => {
|
|
@@ -1983,25 +2089,39 @@ const paystack = (options) => {
|
|
|
1983
2089
|
databaseHooks: {
|
|
1984
2090
|
user: { create: { async after(user, hookCtx) {
|
|
1985
2091
|
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
|
-
|
|
2092
|
+
try {
|
|
2093
|
+
const paystackOps = getPaystackOps(options.paystackClient);
|
|
2094
|
+
if (!paystackOps) return;
|
|
2095
|
+
const sdkRes = unwrapSdkResult(await paystackOps.customer?.create({ body: {
|
|
2096
|
+
email: user.email,
|
|
2097
|
+
first_name: user.name ?? void 0,
|
|
2098
|
+
metadata: JSON.stringify({ userId: user.id })
|
|
2099
|
+
} }) ?? await Promise.reject(/* @__PURE__ */ new Error("Paystack client missing customer ops")));
|
|
2100
|
+
const customerCode = sdkRes?.customer_code;
|
|
2101
|
+
if (customerCode !== void 0 && customerCode !== null && customerCode !== "") {
|
|
2102
|
+
await ctx.adapter.update({
|
|
2103
|
+
model: "user",
|
|
2104
|
+
where: [{
|
|
2105
|
+
field: "id",
|
|
2106
|
+
value: user.id
|
|
2107
|
+
}],
|
|
2108
|
+
update: { paystackCustomerCode: customerCode }
|
|
2109
|
+
});
|
|
2110
|
+
if (typeof options.onCustomerCreate === "function") await options.onCustomerCreate({
|
|
2111
|
+
paystackCustomer: sdkRes,
|
|
2112
|
+
user: {
|
|
2113
|
+
...user,
|
|
2114
|
+
paystackCustomerCode: customerCode
|
|
2115
|
+
}
|
|
2116
|
+
}, hookCtx);
|
|
2117
|
+
}
|
|
2118
|
+
} catch (error) {
|
|
2119
|
+
ctx.logger.error("Failed to create Paystack customer for user", error);
|
|
2120
|
+
}
|
|
2001
2121
|
} } },
|
|
2002
2122
|
organization: options.organization?.enabled === true ? { create: { async after(org, hookCtx) {
|
|
2003
2123
|
try {
|
|
2004
|
-
const extraCreateParams = options.organization?.getCustomerCreateParams ? await options.organization.getCustomerCreateParams(org, hookCtx) : {};
|
|
2124
|
+
const extraCreateParams = typeof options.organization?.getCustomerCreateParams === "function" ? await options.organization.getCustomerCreateParams(org, hookCtx) : {};
|
|
2005
2125
|
let targetEmail = org.email;
|
|
2006
2126
|
if (targetEmail === void 0 || targetEmail === null) {
|
|
2007
2127
|
const ownerMember = await ctx.adapter.findOne({
|
|
@@ -2014,7 +2134,7 @@ const paystack = (options) => {
|
|
|
2014
2134
|
value: "owner"
|
|
2015
2135
|
}]
|
|
2016
2136
|
});
|
|
2017
|
-
if (ownerMember) targetEmail = (await ctx.adapter.findOne({
|
|
2137
|
+
if (ownerMember !== null && ownerMember !== void 0) targetEmail = (await ctx.adapter.findOne({
|
|
2018
2138
|
model: "user",
|
|
2019
2139
|
where: [{
|
|
2020
2140
|
field: "id",
|
|
@@ -2026,19 +2146,29 @@ const paystack = (options) => {
|
|
|
2026
2146
|
const params = defu({
|
|
2027
2147
|
email: targetEmail,
|
|
2028
2148
|
first_name: org.name,
|
|
2029
|
-
metadata: { organizationId: org.id }
|
|
2149
|
+
metadata: JSON.stringify({ organizationId: org.id })
|
|
2030
2150
|
}, extraCreateParams);
|
|
2031
|
-
const
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2151
|
+
const paystackOps = getPaystackOps(options.paystackClient);
|
|
2152
|
+
if (!paystackOps) return;
|
|
2153
|
+
const sdkRes = unwrapSdkResult(await paystackOps.customer?.create({ body: params }) ?? await Promise.reject(/* @__PURE__ */ new Error("Paystack client missing customer ops")));
|
|
2154
|
+
const customerCode = sdkRes?.customer_code;
|
|
2155
|
+
if (customerCode !== void 0 && customerCode !== null && customerCode !== "" && sdkRes !== void 0 && sdkRes !== null) {
|
|
2156
|
+
await ctx.adapter.update({
|
|
2157
|
+
model: "organization",
|
|
2158
|
+
where: [{
|
|
2159
|
+
field: "id",
|
|
2160
|
+
value: org.id
|
|
2161
|
+
}],
|
|
2162
|
+
update: { paystackCustomerCode: customerCode }
|
|
2163
|
+
});
|
|
2164
|
+
if (typeof options.organization?.onCustomerCreate === "function") await options.organization.onCustomerCreate({
|
|
2165
|
+
paystackCustomer: sdkRes,
|
|
2166
|
+
organization: {
|
|
2167
|
+
...org,
|
|
2168
|
+
paystackCustomerCode: customerCode
|
|
2169
|
+
}
|
|
2170
|
+
}, hookCtx);
|
|
2171
|
+
}
|
|
2042
2172
|
} catch (error) {
|
|
2043
2173
|
ctx.logger.error("Failed to create Paystack customer for organization", error);
|
|
2044
2174
|
}
|
|
@@ -2073,7 +2203,7 @@ const paystack = (options) => {
|
|
|
2073
2203
|
team: { create: { before: async (team, ctx) => {
|
|
2074
2204
|
if (options.subscription?.enabled === true && team.organizationId && ctx) {
|
|
2075
2205
|
const subscription = await getOrganizationSubscription(ctx, team.organizationId);
|
|
2076
|
-
if (subscription) {
|
|
2206
|
+
if (subscription !== null && subscription !== void 0) {
|
|
2077
2207
|
const maxTeams = ((await getPlanByName(routeOptions, subscription.plan))?.limits)?.teams;
|
|
2078
2208
|
if (typeof maxTeams === "number") await checkTeamLimit(ctx, team.organizationId, maxTeams);
|
|
2079
2209
|
}
|
|
@@ -2081,10 +2211,11 @@ const paystack = (options) => {
|
|
|
2081
2211
|
} } }
|
|
2082
2212
|
} };
|
|
2083
2213
|
},
|
|
2084
|
-
$ERROR_CODES: INTERNAL_ERROR_CODES
|
|
2214
|
+
$ERROR_CODES: INTERNAL_ERROR_CODES,
|
|
2215
|
+
options
|
|
2085
2216
|
};
|
|
2086
2217
|
};
|
|
2087
2218
|
//#endregion
|
|
2088
|
-
export { paystack };
|
|
2219
|
+
export { chargeSubscriptionRenewal, paystack, syncPaystackPlans, syncPaystackProducts };
|
|
2089
2220
|
|
|
2090
2221
|
//# sourceMappingURL=index.mjs.map
|