@alexasomba/better-auth-paystack 2.1.0 → 2.4.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 +84 -14
- package/dist/client.d.mts +136 -155
- package/dist/client.d.mts.map +1 -1
- package/dist/client.mjs +101 -74
- package/dist/client.mjs.map +1 -1
- package/dist/index.d.mts +394 -2
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +447 -206
- package/dist/index.mjs.map +1 -1
- package/dist/types-CNI2ur0p.d.mts +271 -0
- package/dist/types-CNI2ur0p.d.mts.map +1 -0
- package/dist/version-C_50YiuM.mjs +6 -0
- package/dist/version-C_50YiuM.mjs.map +1 -0
- package/package.json +21 -22
- package/dist/index-Dwbeddkr.d.mts +0 -711
- package/dist/index-Dwbeddkr.d.mts.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { t as PACKAGE_VERSION } from "./version-C_50YiuM.mjs";
|
|
2
|
+
import { HIDE_METADATA, defineErrorCodes, logger } from "better-auth";
|
|
2
3
|
import { defu } from "defu";
|
|
3
|
-
import { createAuthEndpoint, createAuthMiddleware } from "
|
|
4
|
-
import { HIDE_METADATA, logger } from "better-auth";
|
|
5
|
-
import { APIError, getSessionFromCtx, originCheck, sessionMiddleware } from "better-auth/api";
|
|
4
|
+
import { APIError, createAuthEndpoint, createAuthMiddleware, getSessionFromCtx, originCheck, sessionMiddleware } from "better-auth/api";
|
|
6
5
|
import { z } from "zod";
|
|
7
6
|
import { PaystackResponse } from "@alexasomba/paystack-node";
|
|
8
7
|
import { mergeSchema } from "better-auth/db";
|
|
@@ -45,6 +44,30 @@ function getPaystackOps(client) {
|
|
|
45
44
|
}
|
|
46
45
|
//#endregion
|
|
47
46
|
//#region src/utils.ts
|
|
47
|
+
function getPlanSeatAmount(plan) {
|
|
48
|
+
if (plan.seatAmount !== void 0) {
|
|
49
|
+
if (typeof plan.seatAmount === "number" && Number.isFinite(plan.seatAmount)) return plan.seatAmount;
|
|
50
|
+
throw new Error(`Invalid seatAmount for plan '${plan.name}'. Expected a finite number.`);
|
|
51
|
+
}
|
|
52
|
+
if (plan.seatPriceId === void 0 || plan.seatPriceId === null || plan.seatPriceId === "") return;
|
|
53
|
+
const parsed = typeof plan.seatPriceId === "string" ? Number(plan.seatPriceId) : plan.seatPriceId;
|
|
54
|
+
if (typeof parsed === "number" && Number.isFinite(parsed)) return parsed;
|
|
55
|
+
throw new Error(`Invalid seatPriceId for plan '${plan.name}'. Expected a numeric amount in the smallest currency unit.`);
|
|
56
|
+
}
|
|
57
|
+
function calculatePlanAmount(plan, quantity) {
|
|
58
|
+
return (plan.amount ?? 0) + quantity * (getPlanSeatAmount(plan) ?? 0);
|
|
59
|
+
}
|
|
60
|
+
function isLocalSubscriptionCode(subscriptionCode) {
|
|
61
|
+
return typeof subscriptionCode === "string" && (subscriptionCode.startsWith("LOC_") || subscriptionCode.startsWith("sub_local_"));
|
|
62
|
+
}
|
|
63
|
+
function isLocallyManagedSubscription(subscription) {
|
|
64
|
+
if (isLocalSubscriptionCode(subscription.paystackSubscriptionCode)) return true;
|
|
65
|
+
if (typeof subscription.paystackSubscriptionCode === "string" && subscription.paystackSubscriptionCode !== "") return false;
|
|
66
|
+
return subscription.paystackPlanCode === void 0 || subscription.paystackPlanCode === null || subscription.paystackPlanCode === "";
|
|
67
|
+
}
|
|
68
|
+
function assertLocallyManagedSubscription(subscription, action) {
|
|
69
|
+
if (!isLocallyManagedSubscription(subscription)) throw new Error(`Paystack-managed subscriptions do not support ${action}. Use local billing for seat-based or prorated subscription changes.`);
|
|
70
|
+
}
|
|
48
71
|
async function getPlans(subscriptionOptions) {
|
|
49
72
|
if (subscriptionOptions?.enabled === true) return typeof subscriptionOptions.plans === "function" ? subscriptionOptions.plans() : subscriptionOptions.plans;
|
|
50
73
|
throw new Error("Subscriptions are not enabled in the Paystack options.");
|
|
@@ -135,7 +158,9 @@ async function syncProductQuantityFromPaystack(ctx, productName, paystackClient)
|
|
|
135
158
|
return;
|
|
136
159
|
}
|
|
137
160
|
try {
|
|
138
|
-
const
|
|
161
|
+
const paystackProductId = Number(localProduct.paystackId);
|
|
162
|
+
if (!Number.isFinite(paystackProductId)) return;
|
|
163
|
+
const remoteQuantity = unwrapSdkResult(await paystackClient.product?.fetch(paystackProductId))?.quantity;
|
|
139
164
|
if (remoteQuantity !== void 0 && localProduct.id !== void 0) await ctx.context.adapter.update({
|
|
140
165
|
model: "paystackProduct",
|
|
141
166
|
update: {
|
|
@@ -175,7 +200,7 @@ async function syncSubscriptionSeats(ctx, organizationId, options) {
|
|
|
175
200
|
if (subscription === null || subscription === void 0) return;
|
|
176
201
|
const plan = await getPlanByName(options, subscription.plan);
|
|
177
202
|
if (plan === null || plan === void 0) return;
|
|
178
|
-
if (plan
|
|
203
|
+
if (getPlanSeatAmount(plan) === void 0) return;
|
|
179
204
|
const quantity = (await adapter.findMany({
|
|
180
205
|
model: "member",
|
|
181
206
|
where: [{
|
|
@@ -183,12 +208,8 @@ async function syncSubscriptionSeats(ctx, organizationId, options) {
|
|
|
183
208
|
value: organizationId
|
|
184
209
|
}]
|
|
185
210
|
})).length;
|
|
186
|
-
let totalAmount = plan.amount ?? 0;
|
|
187
|
-
if (plan.seatAmount !== void 0 && plan.seatAmount !== null && typeof plan.seatAmount === "number") totalAmount += quantity * plan.seatAmount;
|
|
188
211
|
try {
|
|
189
|
-
|
|
190
|
-
if (client === void 0 || client === null) return;
|
|
191
|
-
unwrapSdkResult(await client.subscription?.update(subscription.paystackSubscriptionCode, { body: { amount: totalAmount } }));
|
|
212
|
+
assertLocallyManagedSubscription(subscription, "automatic seat sync");
|
|
192
213
|
await adapter.update({
|
|
193
214
|
model: "subscription",
|
|
194
215
|
where: [{
|
|
@@ -202,26 +223,39 @@ async function syncSubscriptionSeats(ctx, organizationId, options) {
|
|
|
202
223
|
});
|
|
203
224
|
} catch (e) {
|
|
204
225
|
const log = ctx.context.logger;
|
|
205
|
-
if (log !== void 0 && log !== null) log.error("Failed to sync subscription seats
|
|
226
|
+
if (log !== void 0 && log !== null) log.error("Failed to sync subscription seats", e);
|
|
206
227
|
}
|
|
207
228
|
}
|
|
208
229
|
//#endregion
|
|
209
230
|
//#region src/middleware.ts
|
|
231
|
+
const BILLING_ORG_ROLES = new Set(["owner", "admin"]);
|
|
232
|
+
function hasBillingRole(role) {
|
|
233
|
+
if (Array.isArray(role)) return role.some((value) => hasBillingRole(value));
|
|
234
|
+
if (typeof role !== "string") return false;
|
|
235
|
+
return role.split(",").map((value) => value.trim()).some((value) => BILLING_ORG_ROLES.has(value));
|
|
236
|
+
}
|
|
210
237
|
const referenceMiddleware = (options, action) => createAuthMiddleware(async (ctx) => {
|
|
211
238
|
const session = ctx.context.session;
|
|
212
239
|
if (session === null || session === void 0) throw new APIError("UNAUTHORIZED");
|
|
213
240
|
const body = ctx.body ?? {};
|
|
214
241
|
const query = ctx.query ?? {};
|
|
215
|
-
const
|
|
242
|
+
const requestQueryReferenceId = typeof ctx.request?.url === "string" ? new URL(ctx.request.url).searchParams.get("referenceId") ?? void 0 : void 0;
|
|
243
|
+
const referenceId = body.referenceId ?? query.referenceId ?? requestQueryReferenceId ?? session.user.id;
|
|
216
244
|
const subscriptionOptions = options.subscription;
|
|
217
|
-
if (referenceId === session.user.id) return {
|
|
245
|
+
if (referenceId === session.user.id) return { context: {
|
|
246
|
+
...ctx.context,
|
|
247
|
+
referenceId
|
|
248
|
+
} };
|
|
218
249
|
if (subscriptionOptions?.enabled === true && "authorizeReference" in subscriptionOptions && typeof subscriptionOptions.authorizeReference === "function") {
|
|
219
250
|
if (await subscriptionOptions.authorizeReference({
|
|
220
251
|
user: session.user,
|
|
221
252
|
session: session.session,
|
|
222
253
|
referenceId,
|
|
223
254
|
action
|
|
224
|
-
}, ctx) === true) return {
|
|
255
|
+
}, ctx) === true) return { context: {
|
|
256
|
+
...ctx.context,
|
|
257
|
+
referenceId
|
|
258
|
+
} };
|
|
225
259
|
throw new APIError("UNAUTHORIZED");
|
|
226
260
|
}
|
|
227
261
|
if (options.organization?.enabled === true) {
|
|
@@ -235,13 +269,13 @@ const referenceMiddleware = (options, action) => createAuthMiddleware(async (ctx
|
|
|
235
269
|
value: referenceId
|
|
236
270
|
}]
|
|
237
271
|
});
|
|
238
|
-
if (member !== null && member !== void 0) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
272
|
+
if (member !== null && member !== void 0 && hasBillingRole(member.role)) return { context: {
|
|
273
|
+
...ctx.context,
|
|
274
|
+
referenceId
|
|
275
|
+
} };
|
|
242
276
|
}
|
|
243
|
-
logger.error(`Passing referenceId into a subscription action isn't allowed
|
|
244
|
-
throw new APIError("BAD_REQUEST", { message: "Passing referenceId isn't allowed without subscription.authorizeReference or
|
|
277
|
+
logger.error(`Passing referenceId into a subscription action isn't allowed unless subscription.authorizeReference allows it or the session user is an organization owner/admin.`);
|
|
278
|
+
throw new APIError("BAD_REQUEST", { message: "Passing referenceId isn't allowed without subscription.authorizeReference or organization owner/admin membership." });
|
|
245
279
|
});
|
|
246
280
|
//#endregion
|
|
247
281
|
//#region src/limits.ts
|
|
@@ -288,8 +322,43 @@ const PAYSTACK_ERROR_CODES = defineErrorCodes({
|
|
|
288
322
|
FAILED_TO_VERIFY_TRANSACTION: "Failed to verify transaction",
|
|
289
323
|
FAILED_TO_DISABLE_SUBSCRIPTION: "Failed to disable subscription",
|
|
290
324
|
FAILED_TO_ENABLE_SUBSCRIPTION: "Failed to enable subscription",
|
|
291
|
-
EMAIL_VERIFICATION_REQUIRED: "Email verification is required before you can subscribe to a plan"
|
|
325
|
+
EMAIL_VERIFICATION_REQUIRED: "Email verification is required before you can subscribe to a plan",
|
|
326
|
+
SUBSCRIPTION_PAYMENT_CHANNEL_NOT_ALLOWED: "This subscription only supports specific payment channels"
|
|
292
327
|
});
|
|
328
|
+
function getAllowedSubscriptionChannels(options) {
|
|
329
|
+
const channels = options.subscription?.allowedPaymentChannels;
|
|
330
|
+
return Array.isArray(channels) && channels.length > 0 ? channels : void 0;
|
|
331
|
+
}
|
|
332
|
+
function isAllowedSubscriptionChannel(channel, allowedChannels) {
|
|
333
|
+
if (allowedChannels === void 0) return true;
|
|
334
|
+
return channel !== void 0 && channel !== null && allowedChannels.includes(channel);
|
|
335
|
+
}
|
|
336
|
+
async function assertReferenceAccess(ctx, options, data) {
|
|
337
|
+
if (data.referenceId === data.user.id) return;
|
|
338
|
+
if (options.subscription?.enabled === true && typeof options.subscription.authorizeReference === "function") {
|
|
339
|
+
if (await options.subscription.authorizeReference({
|
|
340
|
+
user: data.user,
|
|
341
|
+
session: data.session,
|
|
342
|
+
referenceId: data.referenceId,
|
|
343
|
+
action: data.action
|
|
344
|
+
}, ctx) === true) return;
|
|
345
|
+
throw new APIError("UNAUTHORIZED");
|
|
346
|
+
}
|
|
347
|
+
if (options.organization?.enabled === true) {
|
|
348
|
+
const member = await ctx.context.adapter.findOne({
|
|
349
|
+
model: "member",
|
|
350
|
+
where: [{
|
|
351
|
+
field: "userId",
|
|
352
|
+
value: data.user.id
|
|
353
|
+
}, {
|
|
354
|
+
field: "organizationId",
|
|
355
|
+
value: data.referenceId
|
|
356
|
+
}]
|
|
357
|
+
});
|
|
358
|
+
if (member !== null && member !== void 0 && hasBillingRole(member.role)) return;
|
|
359
|
+
}
|
|
360
|
+
throw new APIError("UNAUTHORIZED");
|
|
361
|
+
}
|
|
293
362
|
async function hmacSha512Hex(secret, message) {
|
|
294
363
|
const encoder = new TextEncoder();
|
|
295
364
|
const keyData = encoder.encode(secret);
|
|
@@ -338,7 +407,7 @@ const paystackWebhook = (options, path = "/webhook") => {
|
|
|
338
407
|
message: "Missing x-paystack-signature header",
|
|
339
408
|
status: 401
|
|
340
409
|
});
|
|
341
|
-
if (await hmacSha512Hex(options.webhook?.secret ?? options.secretKey, payload) !== signature) throw new APIError("UNAUTHORIZED", {
|
|
410
|
+
if (await hmacSha512Hex(options.webhook?.secret ?? options.paystackWebhookSecret ?? options.secretKey, payload) !== signature) throw new APIError("UNAUTHORIZED", {
|
|
342
411
|
message: "Invalid Paystack webhook signature",
|
|
343
412
|
status: 401
|
|
344
413
|
});
|
|
@@ -504,7 +573,7 @@ const paystackWebhook = (options, path = "/webhook") => {
|
|
|
504
573
|
value: subscriptionCode
|
|
505
574
|
}]
|
|
506
575
|
});
|
|
507
|
-
if (existing) await options.subscription.onSubscriptionCancel?.({
|
|
576
|
+
if (existing !== null && existing !== void 0) await options.subscription.onSubscriptionCancel?.({
|
|
508
577
|
event,
|
|
509
578
|
subscription: {
|
|
510
579
|
...existing,
|
|
@@ -575,7 +644,7 @@ const initializeTransaction = (options, path = "/initialize-transaction") => {
|
|
|
575
644
|
if (callbackURL !== void 0 && callbackURL !== null && callbackURL !== "") {
|
|
576
645
|
const checkTrusted = () => {
|
|
577
646
|
try {
|
|
578
|
-
if (callbackURL
|
|
647
|
+
if (callbackURL?.startsWith("/") === true) return true;
|
|
579
648
|
const baseUrl = ctx.context?.baseURL ?? ctx.request?.url ?? "";
|
|
580
649
|
if (baseUrl === "") return false;
|
|
581
650
|
const baseOrigin = new URL(baseUrl).origin;
|
|
@@ -690,39 +759,44 @@ const initializeTransaction = (options, path = "/initialize-transaction") => {
|
|
|
690
759
|
});
|
|
691
760
|
}
|
|
692
761
|
}
|
|
693
|
-
if (plan !== void 0
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
762
|
+
if (plan !== void 0) try {
|
|
763
|
+
if (getPlanSeatAmount(plan) !== void 0) {
|
|
764
|
+
const members = await ctx.context.adapter.findMany({
|
|
765
|
+
model: "member",
|
|
766
|
+
where: [{
|
|
767
|
+
field: "organizationId",
|
|
768
|
+
value: referenceId
|
|
769
|
+
}]
|
|
770
|
+
});
|
|
771
|
+
const seatCount = members.length > 0 ? members.length : 1;
|
|
772
|
+
amount = calculatePlanAmount(plan, quantity ?? seatCount);
|
|
773
|
+
}
|
|
774
|
+
} catch (error) {
|
|
775
|
+
throw new APIError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Invalid seat configuration for plan." });
|
|
704
776
|
}
|
|
705
777
|
let url;
|
|
706
778
|
let reference;
|
|
707
779
|
let accessCode;
|
|
708
780
|
let trialStart;
|
|
709
781
|
let trialEnd;
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
782
|
+
const requestedTrialDays = plan?.freeTrial?.days !== void 0 && plan.freeTrial.days > 0 ? plan.freeTrial.days : 0;
|
|
783
|
+
const trialRequested = requestedTrialDays > 0;
|
|
784
|
+
let trialGranted = false;
|
|
785
|
+
let trialDeniedReason;
|
|
786
|
+
if (trialRequested) if ((await ctx.context.adapter.findMany({
|
|
787
|
+
model: "subscription",
|
|
788
|
+
where: [{
|
|
789
|
+
field: "referenceId",
|
|
790
|
+
value: referenceId
|
|
791
|
+
}]
|
|
792
|
+
}))?.some((sub) => sub.trialStart !== void 0 && sub.trialStart !== null || sub.trialEnd !== void 0 && sub.trialEnd !== null || sub.status === "trialing") === false) {
|
|
793
|
+
trialStart = /* @__PURE__ */ new Date();
|
|
794
|
+
trialEnd = /* @__PURE__ */ new Date();
|
|
795
|
+
trialEnd.setDate(trialEnd.getDate() + requestedTrialDays);
|
|
796
|
+
trialGranted = true;
|
|
797
|
+
} else trialDeniedReason = "already_used";
|
|
723
798
|
try {
|
|
724
799
|
let targetEmail = email ?? user.email;
|
|
725
|
-
let paystackCustomerCode = user.paystackCustomerCode;
|
|
726
800
|
if (options.organization?.enabled === true && referenceId !== void 0 && referenceId !== null && referenceId !== user.id) {
|
|
727
801
|
const org = await ctx.context.adapter.findOne({
|
|
728
802
|
model: "organization",
|
|
@@ -732,8 +806,6 @@ const initializeTransaction = (options, path = "/initialize-transaction") => {
|
|
|
732
806
|
}]
|
|
733
807
|
});
|
|
734
808
|
if (org !== void 0 && org !== null) {
|
|
735
|
-
const paystackOrg = org;
|
|
736
|
-
if (paystackOrg.paystackCustomerCode !== void 0 && paystackOrg.paystackCustomerCode !== null && paystackOrg.paystackCustomerCode !== "") paystackCustomerCode = paystackOrg.paystackCustomerCode;
|
|
737
809
|
const orgWithEmail = org;
|
|
738
810
|
if (orgWithEmail.email !== void 0 && orgWithEmail.email !== null && orgWithEmail.email !== "") targetEmail = orgWithEmail.email;
|
|
739
811
|
else {
|
|
@@ -760,14 +832,18 @@ const initializeTransaction = (options, path = "/initialize-transaction") => {
|
|
|
760
832
|
}
|
|
761
833
|
}
|
|
762
834
|
}
|
|
835
|
+
const allowedSubscriptionChannels = plan ? getAllowedSubscriptionChannels(options) : void 0;
|
|
763
836
|
const metadata = JSON.stringify({
|
|
764
837
|
referenceId,
|
|
765
838
|
userId: user.id,
|
|
766
839
|
plan: plan !== void 0 ? plan.name.toLowerCase() : void 0,
|
|
767
840
|
product: product !== void 0 ? product.name.toLowerCase() : void 0,
|
|
841
|
+
...extraMetadata,
|
|
768
842
|
isTrial: trialStart !== void 0,
|
|
769
|
-
|
|
770
|
-
|
|
843
|
+
trialRequested,
|
|
844
|
+
trialGranted,
|
|
845
|
+
trialDeniedReason,
|
|
846
|
+
trialEnd: trialEnd !== void 0 ? trialEnd.toISOString() : void 0
|
|
771
847
|
});
|
|
772
848
|
const initBody = {
|
|
773
849
|
email: targetEmail,
|
|
@@ -776,13 +852,10 @@ const initializeTransaction = (options, path = "/initialize-transaction") => {
|
|
|
776
852
|
currency: finalCurrency,
|
|
777
853
|
quantity
|
|
778
854
|
};
|
|
779
|
-
if (
|
|
780
|
-
const ops = getPaystackOps(options.paystackClient);
|
|
781
|
-
if (ops !== void 0 && ops !== null && initBody.email !== "") await ops.customer?.update(paystackCustomerCode, { body: { email: initBody.email } });
|
|
782
|
-
} catch (_e) {}
|
|
855
|
+
if (allowedSubscriptionChannels !== void 0) initBody.channels = allowedSubscriptionChannels;
|
|
783
856
|
if (plan !== void 0 && prorateAndCharge === true) {
|
|
784
857
|
const existingSub = await getOrganizationSubscription(ctx, referenceId);
|
|
785
|
-
if (existingSub?.status === "active" && existingSub.
|
|
858
|
+
if (existingSub?.status === "active" && existingSub.paystackSubscriptionCode !== void 0 && existingSub.paystackSubscriptionCode !== null && existingSub.paystackSubscriptionCode !== "") {
|
|
786
859
|
if (existingSub.periodEnd !== void 0 && existingSub.periodEnd !== null && existingSub.periodStart !== void 0 && existingSub.periodStart !== null) {
|
|
787
860
|
const now = /* @__PURE__ */ new Date();
|
|
788
861
|
const periodEndLocal = new Date(existingSub.periodEnd);
|
|
@@ -800,51 +873,109 @@ const initializeTransaction = (options, path = "/initialize-transaction") => {
|
|
|
800
873
|
}) ?? void 0;
|
|
801
874
|
if (oldPlan !== void 0 && oldPlan !== null) {
|
|
802
875
|
const oldSeatCount = existingSub.seats;
|
|
803
|
-
oldAmount = (oldPlan
|
|
876
|
+
oldAmount = calculatePlanAmount(oldPlan, oldSeatCount);
|
|
804
877
|
}
|
|
805
878
|
}
|
|
806
879
|
let membersCount = 1;
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
880
|
+
let newSeatCount = quantity ?? existingSub.seats ?? membersCount;
|
|
881
|
+
let newAmount;
|
|
882
|
+
try {
|
|
883
|
+
assertLocallyManagedSubscription(existingSub, "plan or seat changes");
|
|
884
|
+
if (getPlanSeatAmount(plan) !== void 0) {
|
|
885
|
+
const members = await ctx.context.adapter.findMany({
|
|
886
|
+
model: "member",
|
|
887
|
+
where: [{
|
|
888
|
+
field: "organizationId",
|
|
889
|
+
value: referenceId
|
|
890
|
+
}]
|
|
891
|
+
});
|
|
892
|
+
membersCount = members.length > 0 ? members.length : 1;
|
|
893
|
+
}
|
|
894
|
+
newSeatCount = quantity ?? existingSub.seats ?? membersCount;
|
|
895
|
+
newAmount = calculatePlanAmount(plan, newSeatCount);
|
|
896
|
+
} catch (error) {
|
|
897
|
+
throw new APIError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Invalid seat configuration for plan." });
|
|
816
898
|
}
|
|
817
|
-
const newSeatCount = quantity ?? existingSub.seats ?? membersCount;
|
|
818
|
-
const newAmount = (plan.amount ?? 0) + newSeatCount * (plan.seatAmount ?? plan.seatPriceId ?? 0);
|
|
819
899
|
const costDifference = newAmount - oldAmount;
|
|
900
|
+
const prorationMetadata = {
|
|
901
|
+
type: "proration",
|
|
902
|
+
subscriptionId: existingSub.id,
|
|
903
|
+
referenceId,
|
|
904
|
+
newPlan: plan.name.toLowerCase(),
|
|
905
|
+
oldPlan: existingSub.plan,
|
|
906
|
+
newSeatCount,
|
|
907
|
+
remainingDays
|
|
908
|
+
};
|
|
909
|
+
let completedProrationReference;
|
|
820
910
|
if (costDifference > 0 && remainingDays > 0) {
|
|
821
911
|
const proratedAmount = Math.round(costDifference / totalDays * remainingDays);
|
|
822
|
-
if (proratedAmount
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
912
|
+
if (proratedAmount < 5e3) throw new APIError("BAD_REQUEST", {
|
|
913
|
+
message: "Prorated upgrade amount is below Paystack's minimum charge. Schedule the change for period end instead.",
|
|
914
|
+
status: 400
|
|
915
|
+
});
|
|
916
|
+
const ops = getPaystackOps(options.paystackClient);
|
|
917
|
+
if (ops === void 0 || ops === null) {
|
|
918
|
+
ctx.context.logger.error("Paystack client not configured for proration charge");
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
if (existingSub.paystackAuthorizationCode !== void 0 && existingSub.paystackAuthorizationCode !== null && existingSub.paystackAuthorizationCode !== "") {
|
|
922
|
+
const sdkRes = unwrapSdkResult(await ops.transaction?.chargeAuthorization({ body: {
|
|
829
923
|
email: targetEmail,
|
|
830
924
|
amount: proratedAmount,
|
|
831
925
|
authorization_code: existingSub.paystackAuthorizationCode,
|
|
832
926
|
reference: `upg_${existingSub.id}_${Date.now()}_${Math.random().toString(36).substring(7)}`,
|
|
833
|
-
metadata:
|
|
834
|
-
|
|
927
|
+
metadata: JSON.stringify(prorationMetadata)
|
|
928
|
+
} }));
|
|
929
|
+
if (sdkRes?.status !== "success") throw new APIError("BAD_REQUEST", { message: "Failed to process prorated charge via saved authorization." });
|
|
930
|
+
await ctx.context.adapter.create({
|
|
931
|
+
model: "paystackTransaction",
|
|
932
|
+
data: {
|
|
933
|
+
reference: sdkRes.reference ?? "",
|
|
934
|
+
paystackId: sdkRes.id !== void 0 && sdkRes.id !== null ? String(sdkRes.id) : void 0,
|
|
835
935
|
referenceId,
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
936
|
+
userId: user.id,
|
|
937
|
+
amount: sdkRes.amount ?? proratedAmount,
|
|
938
|
+
currency: sdkRes.currency ?? finalCurrency,
|
|
939
|
+
status: "success",
|
|
940
|
+
plan: plan.name.toLowerCase(),
|
|
941
|
+
metadata: JSON.stringify(prorationMetadata),
|
|
942
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
943
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
839
944
|
}
|
|
840
|
-
}
|
|
945
|
+
});
|
|
946
|
+
completedProrationReference = sdkRes.reference ?? void 0;
|
|
947
|
+
} else {
|
|
948
|
+
const initRes = unwrapSdkResult(await ops.transaction?.initialize({ body: {
|
|
949
|
+
email: targetEmail,
|
|
950
|
+
amount: proratedAmount,
|
|
951
|
+
currency: finalCurrency,
|
|
952
|
+
callback_url: callbackURL ?? void 0,
|
|
953
|
+
metadata: JSON.stringify(prorationMetadata),
|
|
954
|
+
...allowedSubscriptionChannels !== void 0 ? { channels: allowedSubscriptionChannels } : {}
|
|
955
|
+
} }));
|
|
956
|
+
await ctx.context.adapter.create({
|
|
957
|
+
model: "paystackTransaction",
|
|
958
|
+
data: {
|
|
959
|
+
reference: initRes?.reference ?? "",
|
|
960
|
+
referenceId,
|
|
961
|
+
userId: user.id,
|
|
962
|
+
amount: proratedAmount,
|
|
963
|
+
currency: finalCurrency,
|
|
964
|
+
status: "pending",
|
|
965
|
+
plan: plan.name.toLowerCase(),
|
|
966
|
+
metadata: JSON.stringify(prorationMetadata),
|
|
967
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
968
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
return ctx.json({
|
|
972
|
+
url: initRes?.authorization_url,
|
|
973
|
+
reference: initRes?.reference,
|
|
974
|
+
accessCode: initRes?.access_code,
|
|
975
|
+
redirect: true
|
|
976
|
+
});
|
|
841
977
|
}
|
|
842
978
|
}
|
|
843
|
-
const ops = getPaystackOps(options.paystackClient);
|
|
844
|
-
if (ops !== void 0 && ops !== null) await ops.subscription?.update(existingSub.paystackSubscriptionCode, { body: {
|
|
845
|
-
amount: newAmount,
|
|
846
|
-
plan: plan.planCode
|
|
847
|
-
} });
|
|
848
979
|
await ctx.context.adapter.update({
|
|
849
980
|
model: "subscription",
|
|
850
981
|
where: [{
|
|
@@ -854,6 +985,7 @@ const initializeTransaction = (options, path = "/initialize-transaction") => {
|
|
|
854
985
|
update: {
|
|
855
986
|
plan: plan.name,
|
|
856
987
|
seats: newSeatCount,
|
|
988
|
+
...completedProrationReference !== void 0 ? { paystackTransactionReference: completedProrationReference } : {},
|
|
857
989
|
updatedAt: /* @__PURE__ */ new Date()
|
|
858
990
|
}
|
|
859
991
|
});
|
|
@@ -987,6 +1119,7 @@ const verifyTransaction = (options, path = "/verify-transaction") => {
|
|
|
987
1119
|
const paystackIdRaw = data.id;
|
|
988
1120
|
const paystackId = paystackIdRaw !== void 0 && paystackIdRaw !== null ? String(paystackIdRaw) : void 0;
|
|
989
1121
|
const authorizationCode = data.authorization?.authorization_code;
|
|
1122
|
+
const allowedSubscriptionChannels = getAllowedSubscriptionChannels(options);
|
|
990
1123
|
if (status === "success") {
|
|
991
1124
|
const session = await getSessionFromCtx(ctx);
|
|
992
1125
|
const txRecord = await ctx.context.adapter.findOne({
|
|
@@ -997,6 +1130,26 @@ const verifyTransaction = (options, path = "/verify-transaction") => {
|
|
|
997
1130
|
}]
|
|
998
1131
|
});
|
|
999
1132
|
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;
|
|
1133
|
+
if ((txRecord?.plan !== void 0 && txRecord.plan !== null && txRecord.plan !== "" || Boolean(data.plan)) && isAllowedSubscriptionChannel(data.channel ?? void 0, allowedSubscriptionChannels) === false) {
|
|
1134
|
+
await ctx.context.adapter.update({
|
|
1135
|
+
model: "paystackTransaction",
|
|
1136
|
+
update: {
|
|
1137
|
+
status: "failed",
|
|
1138
|
+
paystackId,
|
|
1139
|
+
amount: data.amount,
|
|
1140
|
+
currency: data.currency,
|
|
1141
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1142
|
+
},
|
|
1143
|
+
where: [{
|
|
1144
|
+
field: "reference",
|
|
1145
|
+
value: reference
|
|
1146
|
+
}]
|
|
1147
|
+
});
|
|
1148
|
+
throw new APIError("BAD_REQUEST", {
|
|
1149
|
+
code: "SUBSCRIPTION_PAYMENT_CHANNEL_NOT_ALLOWED",
|
|
1150
|
+
message: `This subscription requires one of: ${allowedSubscriptionChannels?.join(", ") ?? "allowed channels"}.`
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1000
1153
|
if (session !== void 0 && session !== null && referenceId !== void 0 && referenceId !== null && referenceId !== "" && referenceId !== session.user.id) {
|
|
1001
1154
|
const authRef = subscriptionOptions?.authorizeReference;
|
|
1002
1155
|
let authorized = false;
|
|
@@ -1077,11 +1230,36 @@ const verifyTransaction = (options, path = "/verify-transaction") => {
|
|
|
1077
1230
|
let isTrial = false;
|
|
1078
1231
|
let trialEnd;
|
|
1079
1232
|
let targetPlan;
|
|
1233
|
+
let metadataObj = {};
|
|
1080
1234
|
if (data.metadata !== void 0 && data.metadata !== null && data.metadata !== "") {
|
|
1081
|
-
|
|
1082
|
-
isTrial =
|
|
1083
|
-
trialEnd =
|
|
1084
|
-
targetPlan =
|
|
1235
|
+
metadataObj = typeof data.metadata === "string" ? JSON.parse(data.metadata) : data.metadata;
|
|
1236
|
+
isTrial = metadataObj.isTrial === true || metadataObj.isTrial === "true";
|
|
1237
|
+
trialEnd = metadataObj.trialEnd;
|
|
1238
|
+
targetPlan = metadataObj.plan;
|
|
1239
|
+
}
|
|
1240
|
+
if (metadataObj.type === "proration") {
|
|
1241
|
+
const subscriptionId = metadataObj.subscriptionId;
|
|
1242
|
+
const newPlan = metadataObj.newPlan;
|
|
1243
|
+
const newSeatCount = metadataObj.newSeatCount;
|
|
1244
|
+
if (subscriptionId !== void 0 && subscriptionId !== "" && newPlan !== void 0 && newPlan !== "") await ctx.context.adapter.update({
|
|
1245
|
+
model: "subscription",
|
|
1246
|
+
update: {
|
|
1247
|
+
plan: newPlan,
|
|
1248
|
+
...typeof newSeatCount === "number" ? { seats: newSeatCount } : {},
|
|
1249
|
+
paystackTransactionReference: reference,
|
|
1250
|
+
...authorizationCode !== void 0 && authorizationCode !== null ? { paystackAuthorizationCode: authorizationCode } : {},
|
|
1251
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1252
|
+
},
|
|
1253
|
+
where: [{
|
|
1254
|
+
field: "id",
|
|
1255
|
+
value: subscriptionId
|
|
1256
|
+
}]
|
|
1257
|
+
});
|
|
1258
|
+
return ctx.json({
|
|
1259
|
+
status,
|
|
1260
|
+
reference,
|
|
1261
|
+
data
|
|
1262
|
+
});
|
|
1085
1263
|
}
|
|
1086
1264
|
let paystackSubscriptionCode;
|
|
1087
1265
|
if (isTrial && targetPlan !== void 0 && trialEnd !== void 0) {
|
|
@@ -1161,8 +1339,15 @@ const listSubscriptions = (options, path = "/list-subscriptions") => {
|
|
|
1161
1339
|
const session = await getSessionFromCtx(ctx);
|
|
1162
1340
|
if (session === void 0 || session === null) throw new APIError("UNAUTHORIZED");
|
|
1163
1341
|
const referenceIdPart = ctx.context.referenceId;
|
|
1164
|
-
const queryRefId = ctx.query?.referenceId;
|
|
1165
|
-
const
|
|
1342
|
+
const queryRefId = ctx.query?.referenceId ?? (typeof ctx.request?.url === "string" ? new URL(ctx.request.url).searchParams.get("referenceId") ?? void 0 : void 0);
|
|
1343
|
+
const userId = session.user.id;
|
|
1344
|
+
if (queryRefId !== void 0 && queryRefId !== userId && referenceIdPart !== queryRefId) await assertReferenceAccess(ctx, options, {
|
|
1345
|
+
user: session.user,
|
|
1346
|
+
session: session.session,
|
|
1347
|
+
referenceId: queryRefId,
|
|
1348
|
+
action: "list-subscriptions"
|
|
1349
|
+
});
|
|
1350
|
+
const referenceId = queryRefId ?? referenceIdPart ?? userId;
|
|
1166
1351
|
const res = await ctx.context.adapter.findMany({
|
|
1167
1352
|
model: "subscription",
|
|
1168
1353
|
where: [{
|
|
@@ -1186,8 +1371,15 @@ const listTransactions = (options, path = "/list-transactions") => {
|
|
|
1186
1371
|
const session = await getSessionFromCtx(ctx);
|
|
1187
1372
|
if (session === void 0 || session === null) throw new APIError("UNAUTHORIZED");
|
|
1188
1373
|
const referenceIdPart = ctx.context.referenceId;
|
|
1189
|
-
const queryRefId = ctx.query?.referenceId;
|
|
1190
|
-
const
|
|
1374
|
+
const queryRefId = ctx.query?.referenceId ?? (typeof ctx.request?.url === "string" ? new URL(ctx.request.url).searchParams.get("referenceId") ?? void 0 : void 0);
|
|
1375
|
+
const userId = session.user.id;
|
|
1376
|
+
if (queryRefId !== void 0 && queryRefId !== userId && referenceIdPart !== queryRefId) await assertReferenceAccess(ctx, options, {
|
|
1377
|
+
user: session.user,
|
|
1378
|
+
session: session.session,
|
|
1379
|
+
referenceId: queryRefId,
|
|
1380
|
+
action: "list-transactions"
|
|
1381
|
+
});
|
|
1382
|
+
const referenceId = queryRefId ?? referenceIdPart ?? userId;
|
|
1191
1383
|
const sorted = (await ctx.context.adapter.findMany({
|
|
1192
1384
|
model: "paystackTransaction",
|
|
1193
1385
|
where: [{
|
|
@@ -1238,7 +1430,7 @@ const disablePaystackSubscription = (options, path = "/disable-subscription") =>
|
|
|
1238
1430
|
const { subscriptionCode, atPeriodEnd } = ctx.body;
|
|
1239
1431
|
const paystack = getPaystackOps(options.paystackClient);
|
|
1240
1432
|
try {
|
|
1241
|
-
if (
|
|
1433
|
+
if (isLocalSubscriptionCode(subscriptionCode)) {
|
|
1242
1434
|
const sub = await ctx.context.adapter.findOne({
|
|
1243
1435
|
model: "subscription",
|
|
1244
1436
|
where: [{
|
|
@@ -1246,7 +1438,7 @@ const disablePaystackSubscription = (options, path = "/disable-subscription") =>
|
|
|
1246
1438
|
value: subscriptionCode
|
|
1247
1439
|
}]
|
|
1248
1440
|
});
|
|
1249
|
-
if (sub) {
|
|
1441
|
+
if (sub !== null && sub !== void 0) {
|
|
1250
1442
|
await ctx.context.adapter.update({
|
|
1251
1443
|
model: "subscription",
|
|
1252
1444
|
update: {
|
|
@@ -1370,7 +1562,7 @@ const getSubscriptionManageLink = (options, path = "/subscription-manage-link")
|
|
|
1370
1562
|
] : [sessionMiddleware, originCheck];
|
|
1371
1563
|
const handler = async (ctx) => {
|
|
1372
1564
|
const { subscriptionCode } = ctx.query;
|
|
1373
|
-
if (
|
|
1565
|
+
if (isLocalSubscriptionCode(subscriptionCode)) return ctx.json({
|
|
1374
1566
|
link: null,
|
|
1375
1567
|
message: "Local subscriptions cannot be managed on Paystack"
|
|
1376
1568
|
});
|
|
@@ -1426,8 +1618,6 @@ const getConfig = (options, path = "/get-config") => {
|
|
|
1426
1618
|
});
|
|
1427
1619
|
});
|
|
1428
1620
|
};
|
|
1429
|
-
//#endregion
|
|
1430
|
-
//#region src/schema.ts
|
|
1431
1621
|
const transactions = { paystackTransaction: { fields: {
|
|
1432
1622
|
reference: {
|
|
1433
1623
|
type: "string",
|
|
@@ -1663,8 +1853,17 @@ const plans = { paystackPlan: { fields: {
|
|
|
1663
1853
|
required: true
|
|
1664
1854
|
}
|
|
1665
1855
|
} } };
|
|
1856
|
+
({
|
|
1857
|
+
...subscriptions,
|
|
1858
|
+
...transactions,
|
|
1859
|
+
...user,
|
|
1860
|
+
...organization,
|
|
1861
|
+
...products,
|
|
1862
|
+
...plans
|
|
1863
|
+
});
|
|
1666
1864
|
const getSchema = (options) => {
|
|
1667
1865
|
let baseSchema;
|
|
1866
|
+
const optionSchema = options.schema;
|
|
1668
1867
|
if (options.subscription?.enabled === true) baseSchema = {
|
|
1669
1868
|
...subscriptions,
|
|
1670
1869
|
...transactions,
|
|
@@ -1683,10 +1882,10 @@ const getSchema = (options) => {
|
|
|
1683
1882
|
...organization
|
|
1684
1883
|
};
|
|
1685
1884
|
if (options.schema !== void 0 && options.subscription?.enabled !== true && "subscription" in options.schema) {
|
|
1686
|
-
const { subscription: _subscription, ...restSchema } =
|
|
1885
|
+
const { subscription: _subscription, ...restSchema } = optionSchema ?? {};
|
|
1687
1886
|
return mergeSchema(baseSchema, restSchema);
|
|
1688
1887
|
}
|
|
1689
|
-
return mergeSchema(baseSchema,
|
|
1888
|
+
return mergeSchema(baseSchema, optionSchema);
|
|
1690
1889
|
};
|
|
1691
1890
|
//#endregion
|
|
1692
1891
|
//#region src/operations.ts
|
|
@@ -1813,6 +2012,7 @@ async function chargeSubscriptionRenewal(ctx, options, input) {
|
|
|
1813
2012
|
const amount = bodyAmount ?? plan.amount;
|
|
1814
2013
|
if (amount === void 0 || amount === null) throw new APIError("BAD_REQUEST", { message: "Plan amount is not defined" });
|
|
1815
2014
|
let email;
|
|
2015
|
+
let billingUserId = subscription.userId;
|
|
1816
2016
|
const referenceId = subscription.referenceId;
|
|
1817
2017
|
if (referenceId !== void 0 && referenceId !== null && referenceId !== "") {
|
|
1818
2018
|
const user = await ctx.context.adapter.findOne({
|
|
@@ -1822,8 +2022,10 @@ async function chargeSubscriptionRenewal(ctx, options, input) {
|
|
|
1822
2022
|
value: referenceId
|
|
1823
2023
|
}]
|
|
1824
2024
|
});
|
|
1825
|
-
if (user !== void 0 && user !== null)
|
|
1826
|
-
|
|
2025
|
+
if (user !== void 0 && user !== null) {
|
|
2026
|
+
email = user.email;
|
|
2027
|
+
billingUserId = user.id;
|
|
2028
|
+
} else if (options.organization?.enabled === true) {
|
|
1827
2029
|
const ownerMember = await ctx.context.adapter.findOne({
|
|
1828
2030
|
model: "member",
|
|
1829
2031
|
where: [{
|
|
@@ -1834,13 +2036,17 @@ async function chargeSubscriptionRenewal(ctx, options, input) {
|
|
|
1834
2036
|
value: "owner"
|
|
1835
2037
|
}]
|
|
1836
2038
|
});
|
|
1837
|
-
if (ownerMember !== void 0 && ownerMember !== null)
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
2039
|
+
if (ownerMember !== void 0 && ownerMember !== null) {
|
|
2040
|
+
const ownerUser = await ctx.context.adapter.findOne({
|
|
2041
|
+
model: "user",
|
|
2042
|
+
where: [{
|
|
2043
|
+
field: "id",
|
|
2044
|
+
value: ownerMember.userId
|
|
2045
|
+
}]
|
|
2046
|
+
});
|
|
2047
|
+
email = ownerUser?.email;
|
|
2048
|
+
billingUserId = ownerUser?.id ?? ownerMember.userId;
|
|
2049
|
+
}
|
|
1844
2050
|
}
|
|
1845
2051
|
}
|
|
1846
2052
|
if (email === void 0 || email === null || email === "") throw new APIError("NOT_FOUND", { message: "User email not found" });
|
|
@@ -1854,14 +2060,34 @@ async function chargeSubscriptionRenewal(ctx, options, input) {
|
|
|
1854
2060
|
amount,
|
|
1855
2061
|
authorization_code: subscription.paystackAuthorizationCode,
|
|
1856
2062
|
reference: `rec_${subscription.id}_${Date.now()}`,
|
|
1857
|
-
metadata: {
|
|
2063
|
+
metadata: JSON.stringify({
|
|
1858
2064
|
subscriptionId,
|
|
1859
2065
|
referenceId
|
|
1860
|
-
}
|
|
2066
|
+
})
|
|
1861
2067
|
} }));
|
|
1862
2068
|
if (chargeData?.status === "success" && chargeData.reference !== void 0) {
|
|
1863
2069
|
const now = /* @__PURE__ */ new Date();
|
|
1864
2070
|
const nextPeriodEnd = getNextPeriodEnd(now, plan.interval ?? "monthly");
|
|
2071
|
+
await ctx.context.adapter.create({
|
|
2072
|
+
model: "paystackTransaction",
|
|
2073
|
+
data: {
|
|
2074
|
+
reference: chargeData.reference,
|
|
2075
|
+
paystackId: chargeData.id !== void 0 && chargeData.id !== null ? String(chargeData.id) : void 0,
|
|
2076
|
+
referenceId,
|
|
2077
|
+
userId: billingUserId,
|
|
2078
|
+
amount: chargeData.amount,
|
|
2079
|
+
currency: chargeData.currency,
|
|
2080
|
+
status: "success",
|
|
2081
|
+
plan: plan.name.toLowerCase(),
|
|
2082
|
+
metadata: JSON.stringify({
|
|
2083
|
+
type: "renewal",
|
|
2084
|
+
subscriptionId,
|
|
2085
|
+
referenceId
|
|
2086
|
+
}),
|
|
2087
|
+
createdAt: now,
|
|
2088
|
+
updatedAt: now
|
|
2089
|
+
}
|
|
2090
|
+
});
|
|
1865
2091
|
await ctx.context.adapter.update({
|
|
1866
2092
|
model: "subscription",
|
|
1867
2093
|
update: {
|
|
@@ -1888,10 +2114,17 @@ async function chargeSubscriptionRenewal(ctx, options, input) {
|
|
|
1888
2114
|
//#endregion
|
|
1889
2115
|
//#region src/index.ts
|
|
1890
2116
|
const INTERNAL_ERROR_CODES = defineErrorCodes(Object.fromEntries(Object.entries(PAYSTACK_ERROR_CODES).map(([key, value]) => [key, typeof value === "string" ? value : value.message])));
|
|
1891
|
-
const
|
|
1892
|
-
const routeOptions =
|
|
2117
|
+
const createPaystackPlugin = (options) => {
|
|
2118
|
+
const routeOptions = {
|
|
2119
|
+
...options,
|
|
2120
|
+
webhook: {
|
|
2121
|
+
...options.webhook,
|
|
2122
|
+
secret: options.webhook?.secret ?? options.paystackWebhookSecret
|
|
2123
|
+
}
|
|
2124
|
+
};
|
|
1893
2125
|
return {
|
|
1894
2126
|
id: "paystack",
|
|
2127
|
+
version: PACKAGE_VERSION,
|
|
1895
2128
|
endpoints: {
|
|
1896
2129
|
initializeTransaction: initializeTransaction(routeOptions, "/paystack/initialize-transaction"),
|
|
1897
2130
|
verifyTransaction: verifyTransaction(routeOptions, "/paystack/verify-transaction"),
|
|
@@ -1911,90 +2144,97 @@ const paystack = (options) => {
|
|
|
1911
2144
|
listPlans: listPlans(routeOptions, "/paystack/list-plans")
|
|
1912
2145
|
},
|
|
1913
2146
|
schema: getSchema(options),
|
|
1914
|
-
init: (ctx) => {
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
}
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
}
|
|
1944
|
-
}
|
|
1945
|
-
} catch (error) {
|
|
1946
|
-
ctx.logger.error("Failed to create Paystack customer for user", error);
|
|
2147
|
+
init: ((ctx) => {
|
|
2148
|
+
const organizationPluginAvailable = ctx.hasPlugin("organization");
|
|
2149
|
+
if (options.organization?.enabled === true && !organizationPluginAvailable) ctx.logger.error("Paystack organization billing is enabled, but the Better Auth organization plugin was not found. Organization billing hooks will be skipped.");
|
|
2150
|
+
return { options: { databaseHooks: {
|
|
2151
|
+
user: { create: { async after(user, hookCtx) {
|
|
2152
|
+
if (!hookCtx || options.createCustomerOnSignUp !== true || user.email === null || user.email === void 0 || user.email === "") return;
|
|
2153
|
+
try {
|
|
2154
|
+
const paystackOps = getPaystackOps(options.paystackClient);
|
|
2155
|
+
if (!paystackOps) return;
|
|
2156
|
+
const sdkRes = unwrapSdkResult(await paystackOps.customer?.create({ body: {
|
|
2157
|
+
email: user.email,
|
|
2158
|
+
first_name: user.name ?? void 0,
|
|
2159
|
+
metadata: JSON.stringify({ userId: user.id })
|
|
2160
|
+
} }) ?? await Promise.reject(/* @__PURE__ */ new Error("Paystack client missing customer ops")));
|
|
2161
|
+
const customerCode = sdkRes?.customer_code;
|
|
2162
|
+
if (customerCode !== void 0 && customerCode !== null && customerCode !== "") {
|
|
2163
|
+
await ctx.adapter.update({
|
|
2164
|
+
model: "user",
|
|
2165
|
+
where: [{
|
|
2166
|
+
field: "id",
|
|
2167
|
+
value: user.id
|
|
2168
|
+
}],
|
|
2169
|
+
update: { paystackCustomerCode: customerCode }
|
|
2170
|
+
});
|
|
2171
|
+
if (typeof options.onCustomerCreate === "function") await options.onCustomerCreate({
|
|
2172
|
+
paystackCustomer: sdkRes,
|
|
2173
|
+
user: {
|
|
2174
|
+
...user,
|
|
2175
|
+
paystackCustomerCode: customerCode
|
|
2176
|
+
}
|
|
2177
|
+
}, hookCtx);
|
|
1947
2178
|
}
|
|
1948
|
-
}
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
email: targetEmail,
|
|
1975
|
-
first_name: org.name,
|
|
1976
|
-
metadata: { organizationId: org.id }
|
|
1977
|
-
}, extraCreateParams);
|
|
1978
|
-
const paystackOps = getPaystackOps(options.paystackClient);
|
|
1979
|
-
if (!paystackOps) return;
|
|
1980
|
-
const sdkRes = unwrapSdkResult(await paystackOps.customer?.create({ body: params }) ?? await Promise.reject(/* @__PURE__ */ new Error("Paystack client missing customer ops")));
|
|
1981
|
-
const customerCode = sdkRes?.customer_code;
|
|
1982
|
-
if (customerCode !== void 0 && customerCode !== null && customerCode !== "" && sdkRes !== void 0 && sdkRes !== null) {
|
|
1983
|
-
await ctx.internalAdapter.updateOrganization(org.id, { paystackCustomerCode: customerCode });
|
|
1984
|
-
if (typeof options.organization?.onCustomerCreate === "function") await options.organization.onCustomerCreate({
|
|
1985
|
-
paystackCustomer: sdkRes,
|
|
1986
|
-
organization: {
|
|
1987
|
-
...org,
|
|
1988
|
-
paystackCustomerCode: customerCode
|
|
1989
|
-
}
|
|
1990
|
-
}, hookCtx);
|
|
1991
|
-
}
|
|
1992
|
-
} catch (error) {
|
|
1993
|
-
ctx.logger.error("Failed to create Paystack customer for organization", error);
|
|
2179
|
+
} catch (error) {
|
|
2180
|
+
ctx.logger.error("Failed to create Paystack customer for user", error);
|
|
2181
|
+
}
|
|
2182
|
+
} } },
|
|
2183
|
+
organization: options.organization?.enabled === true && organizationPluginAvailable ? { create: { async after(org, hookCtx) {
|
|
2184
|
+
try {
|
|
2185
|
+
const extraCreateParams = typeof options.organization?.getCustomerCreateParams === "function" ? await options.organization.getCustomerCreateParams(org, hookCtx) : {};
|
|
2186
|
+
let targetEmail = org.email;
|
|
2187
|
+
if (targetEmail === void 0 || targetEmail === null) {
|
|
2188
|
+
const ownerMember = await ctx.adapter.findOne({
|
|
2189
|
+
model: "member",
|
|
2190
|
+
where: [{
|
|
2191
|
+
field: "organizationId",
|
|
2192
|
+
value: org.id
|
|
2193
|
+
}, {
|
|
2194
|
+
field: "role",
|
|
2195
|
+
value: "owner"
|
|
2196
|
+
}]
|
|
2197
|
+
});
|
|
2198
|
+
if (ownerMember !== null && ownerMember !== void 0) targetEmail = (await ctx.adapter.findOne({
|
|
2199
|
+
model: "user",
|
|
2200
|
+
where: [{
|
|
2201
|
+
field: "id",
|
|
2202
|
+
value: ownerMember.userId
|
|
2203
|
+
}]
|
|
2204
|
+
}))?.email;
|
|
1994
2205
|
}
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
2206
|
+
if (targetEmail === void 0 || targetEmail === null) return;
|
|
2207
|
+
const params = defu({
|
|
2208
|
+
email: targetEmail,
|
|
2209
|
+
first_name: org.name,
|
|
2210
|
+
metadata: JSON.stringify({ organizationId: org.id })
|
|
2211
|
+
}, extraCreateParams);
|
|
2212
|
+
const paystackOps = getPaystackOps(options.paystackClient);
|
|
2213
|
+
if (!paystackOps) return;
|
|
2214
|
+
const sdkRes = unwrapSdkResult(await paystackOps.customer?.create({ body: params }) ?? await Promise.reject(/* @__PURE__ */ new Error("Paystack client missing customer ops")));
|
|
2215
|
+
const customerCode = sdkRes?.customer_code;
|
|
2216
|
+
if (customerCode !== void 0 && customerCode !== null && customerCode !== "" && sdkRes !== void 0 && sdkRes !== null) {
|
|
2217
|
+
await ctx.adapter.update({
|
|
2218
|
+
model: "organization",
|
|
2219
|
+
where: [{
|
|
2220
|
+
field: "id",
|
|
2221
|
+
value: org.id
|
|
2222
|
+
}],
|
|
2223
|
+
update: { paystackCustomerCode: customerCode }
|
|
2224
|
+
});
|
|
2225
|
+
if (typeof options.organization?.onCustomerCreate === "function") await options.organization.onCustomerCreate({
|
|
2226
|
+
paystackCustomer: sdkRes,
|
|
2227
|
+
organization: {
|
|
2228
|
+
...org,
|
|
2229
|
+
paystackCustomerCode: customerCode
|
|
2230
|
+
}
|
|
2231
|
+
}, hookCtx);
|
|
2232
|
+
}
|
|
2233
|
+
} catch (error) {
|
|
2234
|
+
ctx.logger.error("Failed to create Paystack customer for organization", error);
|
|
2235
|
+
}
|
|
2236
|
+
} } } : void 0,
|
|
2237
|
+
member: organizationPluginAvailable ? {
|
|
1998
2238
|
create: {
|
|
1999
2239
|
before: async (member, ctx) => {
|
|
2000
2240
|
if (options.subscription?.enabled === true && member.organizationId && ctx !== null && ctx !== void 0) await checkSeatLimit(ctx, member.organizationId);
|
|
@@ -2006,8 +2246,8 @@ const paystack = (options) => {
|
|
|
2006
2246
|
delete: { after: async (member, ctx) => {
|
|
2007
2247
|
if (options.subscription?.enabled === true && typeof member?.organizationId === "string" && ctx) await syncSubscriptionSeats(ctx, member.organizationId, routeOptions);
|
|
2008
2248
|
} }
|
|
2009
|
-
},
|
|
2010
|
-
invitation: {
|
|
2249
|
+
} : void 0,
|
|
2250
|
+
invitation: organizationPluginAvailable ? {
|
|
2011
2251
|
create: {
|
|
2012
2252
|
before: async (invitation, ctx) => {
|
|
2013
2253
|
if (options.subscription?.enabled === true && invitation.organizationId && ctx !== null && ctx !== void 0) await checkSeatLimit(ctx, invitation.organizationId);
|
|
@@ -2019,8 +2259,8 @@ const paystack = (options) => {
|
|
|
2019
2259
|
delete: { after: async (invitation, ctx) => {
|
|
2020
2260
|
if (options.subscription?.enabled === true && typeof invitation?.organizationId === "string" && ctx) await syncSubscriptionSeats(ctx, invitation.organizationId, routeOptions);
|
|
2021
2261
|
} }
|
|
2022
|
-
},
|
|
2023
|
-
team: { create: { before: async (team, ctx) => {
|
|
2262
|
+
} : void 0,
|
|
2263
|
+
team: organizationPluginAvailable ? { create: { before: async (team, ctx) => {
|
|
2024
2264
|
if (options.subscription?.enabled === true && team.organizationId && ctx) {
|
|
2025
2265
|
const subscription = await getOrganizationSubscription(ctx, team.organizationId);
|
|
2026
2266
|
if (subscription !== null && subscription !== void 0) {
|
|
@@ -2028,13 +2268,14 @@ const paystack = (options) => {
|
|
|
2028
2268
|
if (typeof maxTeams === "number") await checkTeamLimit(ctx, team.organizationId, maxTeams);
|
|
2029
2269
|
}
|
|
2030
2270
|
}
|
|
2031
|
-
} } }
|
|
2032
|
-
} };
|
|
2033
|
-
},
|
|
2271
|
+
} } } : void 0
|
|
2272
|
+
} } };
|
|
2273
|
+
}),
|
|
2034
2274
|
$ERROR_CODES: INTERNAL_ERROR_CODES,
|
|
2035
2275
|
options
|
|
2036
2276
|
};
|
|
2037
2277
|
};
|
|
2278
|
+
const paystack = createPaystackPlugin;
|
|
2038
2279
|
//#endregion
|
|
2039
2280
|
export { chargeSubscriptionRenewal, paystack, syncPaystackPlans, syncPaystackProducts };
|
|
2040
2281
|
|