@alexasomba/better-auth-paystack 1.1.2 → 1.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 +22 -2
- package/dist/client.d.mts +23 -1
- package/dist/client.d.mts.map +1 -1
- package/dist/client.mjs.map +1 -1
- package/dist/index.d.mts +51 -87
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +308 -63
- package/dist/index.mjs.map +1 -1
- package/dist/{types-CMXvth6C.d.mts → types-BOpjdQrr.d.mts} +31 -4
- package/dist/types-BOpjdQrr.d.mts.map +1 -0
- package/package.json +8 -8
- package/dist/types-CMXvth6C.d.mts.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineErrorCodes } from "@better-auth/core/utils";
|
|
1
|
+
import { defineErrorCodes } from "@better-auth/core/utils/error-codes";
|
|
2
2
|
import { defu } from "defu";
|
|
3
3
|
import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
|
|
4
4
|
import { HIDE_METADATA, logger } from "better-auth";
|
|
@@ -73,6 +73,17 @@ function getPaystackOps(paystackClient) {
|
|
|
73
73
|
if (paystackClient?.subscription_manageEmail !== void 0) return paystackClient.subscription_manageEmail({ params: { path: { code } } });
|
|
74
74
|
return paystackClient?.subscription?.manage?.email?.(code, email);
|
|
75
75
|
},
|
|
76
|
+
subscriptionUpdate: (params) => {
|
|
77
|
+
if (paystackClient?.subscription_update !== void 0) return paystackClient.subscription_update({
|
|
78
|
+
params: { path: { code: params.code } },
|
|
79
|
+
body: {
|
|
80
|
+
plan: params.plan,
|
|
81
|
+
authorization: params.authorization,
|
|
82
|
+
amount: params.amount
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
return paystackClient?.subscription?.update?.(params.code, params);
|
|
86
|
+
},
|
|
76
87
|
transactionChargeAuthorization: (body) => {
|
|
77
88
|
if (paystackClient?.transaction_chargeAuthorization !== void 0) return paystackClient.transaction_chargeAuthorization({ body });
|
|
78
89
|
return paystackClient?.transaction?.chargeAuthorization?.(body);
|
|
@@ -220,6 +231,51 @@ async function syncProductQuantityFromPaystack(ctx, productName, paystackClient)
|
|
|
220
231
|
});
|
|
221
232
|
}
|
|
222
233
|
}
|
|
234
|
+
async function syncSubscriptionSeats(ctx, organizationId, options) {
|
|
235
|
+
if (options.subscription?.enabled !== true) return;
|
|
236
|
+
const adapter = ctx.context?.adapter ?? ctx.adapter;
|
|
237
|
+
const subscription = await adapter.findOne({
|
|
238
|
+
model: "subscription",
|
|
239
|
+
where: [{
|
|
240
|
+
field: "referenceId",
|
|
241
|
+
value: organizationId
|
|
242
|
+
}]
|
|
243
|
+
});
|
|
244
|
+
if (subscription === null || subscription.paystackSubscriptionCode === void 0 || subscription.paystackSubscriptionCode === null) return;
|
|
245
|
+
const plan = await getPlanByName(options, subscription.plan);
|
|
246
|
+
if (plan === null) return;
|
|
247
|
+
if (plan.seatAmount === void 0 && plan.seatPlanCode === void 0) return;
|
|
248
|
+
const quantity = (await adapter.findMany({
|
|
249
|
+
model: "member",
|
|
250
|
+
where: [{
|
|
251
|
+
field: "organizationId",
|
|
252
|
+
value: organizationId
|
|
253
|
+
}]
|
|
254
|
+
})).length;
|
|
255
|
+
let totalAmount = plan.amount ?? 0;
|
|
256
|
+
if (plan.seatAmount !== void 0 && plan.seatAmount !== null) totalAmount += quantity * plan.seatAmount;
|
|
257
|
+
const ops = getPaystackOps(options.paystackClient);
|
|
258
|
+
try {
|
|
259
|
+
await ops.subscriptionUpdate({
|
|
260
|
+
code: subscription.paystackSubscriptionCode,
|
|
261
|
+
amount: totalAmount
|
|
262
|
+
});
|
|
263
|
+
await adapter.update({
|
|
264
|
+
model: "subscription",
|
|
265
|
+
where: [{
|
|
266
|
+
field: "id",
|
|
267
|
+
value: subscription.id
|
|
268
|
+
}],
|
|
269
|
+
update: {
|
|
270
|
+
seats: quantity,
|
|
271
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
} catch (e) {
|
|
275
|
+
const log = ctx.context?.logger ?? ctx.logger;
|
|
276
|
+
if (log !== void 0 && log !== null) log.error("Failed to sync subscription seats with Paystack", e);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
223
279
|
|
|
224
280
|
//#endregion
|
|
225
281
|
//#region src/middleware.ts
|
|
@@ -260,6 +316,41 @@ const referenceMiddleware = (options, action) => createAuthMiddleware(async (ctx
|
|
|
260
316
|
throw new APIError("BAD_REQUEST", { message: "Passing referenceId isn't allowed without subscription.authorizeReference or valid organization membership." });
|
|
261
317
|
});
|
|
262
318
|
|
|
319
|
+
//#endregion
|
|
320
|
+
//#region src/limits.ts
|
|
321
|
+
const getOrganizationSubscription = async (ctx, organizationId) => {
|
|
322
|
+
return await ctx.context.adapter.findOne({
|
|
323
|
+
model: "subscription",
|
|
324
|
+
where: [{
|
|
325
|
+
field: "referenceId",
|
|
326
|
+
value: organizationId
|
|
327
|
+
}]
|
|
328
|
+
});
|
|
329
|
+
};
|
|
330
|
+
const checkSeatLimit = async (ctx, organizationId, seatsToAdd = 1) => {
|
|
331
|
+
const subscription = await getOrganizationSubscription(ctx, organizationId);
|
|
332
|
+
if (subscription?.seats === void 0 || subscription.seats === null) return true;
|
|
333
|
+
const members = await ctx.context.adapter.findMany({
|
|
334
|
+
model: "member",
|
|
335
|
+
where: [{
|
|
336
|
+
field: "organizationId",
|
|
337
|
+
value: organizationId
|
|
338
|
+
}]
|
|
339
|
+
});
|
|
340
|
+
if (members.length + seatsToAdd > subscription.seats) throw new APIError("FORBIDDEN", { message: `Organization member limit reached. Used: ${members.length}, Max: ${subscription.seats}` });
|
|
341
|
+
return true;
|
|
342
|
+
};
|
|
343
|
+
const checkTeamLimit = async (ctx, organizationId, maxTeams) => {
|
|
344
|
+
if ((await ctx.context.adapter.findMany({
|
|
345
|
+
model: "team",
|
|
346
|
+
where: [{
|
|
347
|
+
field: "organizationId",
|
|
348
|
+
value: organizationId
|
|
349
|
+
}]
|
|
350
|
+
})).length >= maxTeams) throw new APIError("FORBIDDEN", { message: `Organization team limit reached. Max teams: ${maxTeams}` });
|
|
351
|
+
return true;
|
|
352
|
+
};
|
|
353
|
+
|
|
263
354
|
//#endregion
|
|
264
355
|
//#region src/routes.ts
|
|
265
356
|
const PAYSTACK_ERROR_CODES = defineErrorCodes({
|
|
@@ -479,6 +570,31 @@ const paystackWebhook = (options) => {
|
|
|
479
570
|
}, ctx);
|
|
480
571
|
}
|
|
481
572
|
}
|
|
573
|
+
if (eventName === "charge.success" || eventName === "invoice.update") {
|
|
574
|
+
const payloadData = data;
|
|
575
|
+
const subscriptionCode = payloadData?.subscription?.subscription_code ?? payloadData?.subscription_code;
|
|
576
|
+
if (subscriptionCode) {
|
|
577
|
+
const existingSub = await ctx.context.adapter.findOne({
|
|
578
|
+
model: "subscription",
|
|
579
|
+
where: [{
|
|
580
|
+
field: "paystackSubscriptionCode",
|
|
581
|
+
value: subscriptionCode
|
|
582
|
+
}]
|
|
583
|
+
});
|
|
584
|
+
if (existingSub?.pendingPlan) await ctx.context.adapter.update({
|
|
585
|
+
model: "subscription",
|
|
586
|
+
update: {
|
|
587
|
+
plan: existingSub.pendingPlan,
|
|
588
|
+
pendingPlan: null,
|
|
589
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
590
|
+
},
|
|
591
|
+
where: [{
|
|
592
|
+
field: "id",
|
|
593
|
+
value: existingSub.id
|
|
594
|
+
}]
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}
|
|
482
598
|
} catch (_e) {
|
|
483
599
|
ctx.context.logger.error("Failed to sync Paystack webhook event", _e);
|
|
484
600
|
}
|
|
@@ -495,7 +611,10 @@ const initializeTransactionBodySchema = z.object({
|
|
|
495
611
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
496
612
|
referenceId: z.string().optional(),
|
|
497
613
|
callbackURL: z.string().optional(),
|
|
498
|
-
quantity: z.number().int().positive().optional()
|
|
614
|
+
quantity: z.number().int().positive().optional(),
|
|
615
|
+
scheduleAtPeriodEnd: z.boolean().optional(),
|
|
616
|
+
cancelAtPeriodEnd: z.boolean().optional(),
|
|
617
|
+
prorateAndCharge: z.boolean().optional()
|
|
499
618
|
});
|
|
500
619
|
const initializeTransaction = (options, path = "/paystack/initialize-transaction") => {
|
|
501
620
|
const subscriptionOptions = options.subscription;
|
|
@@ -509,7 +628,7 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
509
628
|
] : [sessionMiddleware, originCheck]
|
|
510
629
|
}, async (ctx) => {
|
|
511
630
|
const paystack = getPaystackOps(options.paystackClient);
|
|
512
|
-
const { plan: planName, product: productName, amount: bodyAmount, currency, email, metadata: extraMetadata, callbackURL, quantity } = ctx.body;
|
|
631
|
+
const { plan: planName, product: productName, amount: bodyAmount, currency, email, metadata: extraMetadata, callbackURL, quantity, scheduleAtPeriodEnd, cancelAtPeriodEnd, prorateAndCharge } = ctx.body;
|
|
513
632
|
if (callbackURL !== void 0 && callbackURL !== null && callbackURL !== "") {
|
|
514
633
|
const checkTrusted = () => {
|
|
515
634
|
try {
|
|
@@ -533,7 +652,7 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
533
652
|
const user = session.user;
|
|
534
653
|
if (subscriptionOptions?.enabled === true && subscriptionOptions.requireEmailVerification === true && !user.emailVerified) throw new APIError("BAD_REQUEST", {
|
|
535
654
|
code: "EMAIL_VERIFICATION_REQUIRED",
|
|
536
|
-
message: PAYSTACK_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED
|
|
655
|
+
message: PAYSTACK_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED.message
|
|
537
656
|
});
|
|
538
657
|
let plan;
|
|
539
658
|
let product;
|
|
@@ -559,7 +678,7 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
559
678
|
}
|
|
560
679
|
if (!plan) throw new APIError("BAD_REQUEST", {
|
|
561
680
|
code: "SUBSCRIPTION_PLAN_NOT_FOUND",
|
|
562
|
-
message: PAYSTACK_ERROR_CODES.SUBSCRIPTION_PLAN_NOT_FOUND,
|
|
681
|
+
message: PAYSTACK_ERROR_CODES.SUBSCRIPTION_PLAN_NOT_FOUND.message,
|
|
563
682
|
status: 400
|
|
564
683
|
});
|
|
565
684
|
} else if (productName !== void 0 && productName !== null && productName !== "") {
|
|
@@ -581,13 +700,67 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
581
700
|
message: "Either 'plan', 'product', or 'amount' is required to initialize a transaction.",
|
|
582
701
|
status: 400
|
|
583
702
|
});
|
|
584
|
-
|
|
703
|
+
let amount = bodyAmount ?? product?.price;
|
|
585
704
|
const finalCurrency = currency ?? product?.currency ?? plan?.currency ?? "NGN";
|
|
705
|
+
const referenceIdFromCtx = ctx.context.referenceId;
|
|
706
|
+
const referenceId = ctx.body.referenceId !== void 0 && ctx.body.referenceId !== null && ctx.body.referenceId !== "" ? ctx.body.referenceId : referenceIdFromCtx !== void 0 && referenceIdFromCtx !== null && referenceIdFromCtx !== "" ? referenceIdFromCtx : session.user.id;
|
|
707
|
+
if (plan && scheduleAtPeriodEnd === true) {
|
|
708
|
+
const existingSub = await getOrganizationSubscription(ctx, referenceId);
|
|
709
|
+
if (existingSub?.status === "active") {
|
|
710
|
+
await ctx.context.adapter.update({
|
|
711
|
+
model: "subscription",
|
|
712
|
+
where: [{
|
|
713
|
+
field: "id",
|
|
714
|
+
value: existingSub.id
|
|
715
|
+
}],
|
|
716
|
+
update: {
|
|
717
|
+
pendingPlan: plan.name,
|
|
718
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
return ctx.json({
|
|
722
|
+
status: "success",
|
|
723
|
+
message: "Plan change scheduled at period end.",
|
|
724
|
+
scheduled: true
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (cancelAtPeriodEnd === true) {
|
|
729
|
+
const existingSub = await getOrganizationSubscription(ctx, referenceId);
|
|
730
|
+
if (existingSub?.status === "active") {
|
|
731
|
+
await ctx.context.adapter.update({
|
|
732
|
+
model: "subscription",
|
|
733
|
+
where: [{
|
|
734
|
+
field: "id",
|
|
735
|
+
value: existingSub.id
|
|
736
|
+
}],
|
|
737
|
+
update: {
|
|
738
|
+
cancelAtPeriodEnd: true,
|
|
739
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
return ctx.json({
|
|
743
|
+
status: "success",
|
|
744
|
+
message: "Subscription cancellation scheduled at period end.",
|
|
745
|
+
scheduled: true
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (plan && (plan.seatAmount !== void 0 || plan.seatPriceId !== void 0)) {
|
|
750
|
+
const members = await ctx.context.adapter.findMany({
|
|
751
|
+
model: "member",
|
|
752
|
+
where: [{
|
|
753
|
+
field: "organizationId",
|
|
754
|
+
value: referenceId
|
|
755
|
+
}]
|
|
756
|
+
});
|
|
757
|
+
const seatCount = members.length > 0 ? members.length : 1;
|
|
758
|
+
const quantityToUse = quantity ?? seatCount;
|
|
759
|
+
amount = (plan.amount ?? 0) + quantityToUse * (plan.seatAmount ?? plan.seatPriceId ?? 0);
|
|
760
|
+
}
|
|
586
761
|
let url;
|
|
587
762
|
let reference;
|
|
588
763
|
let accessCode;
|
|
589
|
-
const referenceIdFromCtx = ctx.context.referenceId;
|
|
590
|
-
const referenceId = ctx.body.referenceId !== void 0 && ctx.body.referenceId !== null && ctx.body.referenceId !== "" ? ctx.body.referenceId : referenceIdFromCtx !== void 0 && referenceIdFromCtx !== null && referenceIdFromCtx !== "" ? referenceIdFromCtx : session.user.id;
|
|
591
764
|
let trialStart;
|
|
592
765
|
let trialEnd;
|
|
593
766
|
if (plan?.freeTrial?.days !== void 0 && plan.freeTrial.days !== null && plan.freeTrial.days > 0) {
|
|
@@ -661,13 +834,95 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
661
834
|
const ops = getPaystackOps(options.paystackClient);
|
|
662
835
|
if (initBody.email !== void 0 && initBody.email !== null && initBody.email !== "") await ops.customerUpdate(paystackCustomerCode, { email: initBody.email });
|
|
663
836
|
} catch (_e) {}
|
|
837
|
+
if (plan && prorateAndCharge === true) {
|
|
838
|
+
const existingSub = await getOrganizationSubscription(ctx, referenceId);
|
|
839
|
+
if (existingSub?.status === "active" && existingSub.paystackAuthorizationCode !== null && existingSub.paystackAuthorizationCode !== void 0 && existingSub.paystackSubscriptionCode !== null && existingSub.paystackSubscriptionCode !== void 0) {
|
|
840
|
+
const now = /* @__PURE__ */ new Date();
|
|
841
|
+
const periodEndLocal = existingSub.periodEnd ? new Date(existingSub.periodEnd) : new Date(now.getTime() + 720 * 60 * 60 * 1e3);
|
|
842
|
+
const periodStartLocal = existingSub.periodStart ? new Date(existingSub.periodStart) : now;
|
|
843
|
+
const totalDays = Math.max(1, Math.ceil((periodEndLocal.getTime() - periodStartLocal.getTime()) / (1e3 * 60 * 60 * 24)));
|
|
844
|
+
const remainingDays = Math.max(0, Math.ceil((periodEndLocal.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24)));
|
|
845
|
+
let oldAmount = 0;
|
|
846
|
+
if (existingSub.plan) {
|
|
847
|
+
const oldPlan = await getPlanByName(options, existingSub.plan) ?? await ctx.context.adapter.findOne({
|
|
848
|
+
model: "paystackPlan",
|
|
849
|
+
where: [{
|
|
850
|
+
field: "name",
|
|
851
|
+
value: existingSub.plan
|
|
852
|
+
}]
|
|
853
|
+
});
|
|
854
|
+
if (oldPlan) {
|
|
855
|
+
const oldSeatCount = existingSub.seats ?? 1;
|
|
856
|
+
oldAmount = (oldPlan.amount ?? 0) + oldSeatCount * (oldPlan.seatAmount ?? oldPlan.seatPriceId ?? 0);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
let membersCount = 1;
|
|
860
|
+
if (plan.seatAmount !== void 0 || plan.seatPriceId !== void 0) {
|
|
861
|
+
const members = await ctx.context.adapter.findMany({
|
|
862
|
+
model: "member",
|
|
863
|
+
where: [{
|
|
864
|
+
field: "organizationId",
|
|
865
|
+
value: referenceId
|
|
866
|
+
}]
|
|
867
|
+
});
|
|
868
|
+
membersCount = members.length > 0 ? members.length : 1;
|
|
869
|
+
}
|
|
870
|
+
const newSeatCount = quantity ?? existingSub.seats ?? membersCount;
|
|
871
|
+
const newAmount = (plan.amount ?? 0) + newSeatCount * (plan.seatAmount ?? plan.seatPriceId ?? 0);
|
|
872
|
+
const costDifference = newAmount - oldAmount;
|
|
873
|
+
if (costDifference > 0 && remainingDays > 0) {
|
|
874
|
+
const proratedAmount = Math.round(costDifference / totalDays * remainingDays);
|
|
875
|
+
if (proratedAmount >= 5e3) {
|
|
876
|
+
const chargeData = unwrapSdkResult(await getPaystackOps(options.paystackClient).transactionChargeAuthorization({
|
|
877
|
+
email: targetEmail,
|
|
878
|
+
amount: proratedAmount,
|
|
879
|
+
authorization_code: existingSub.paystackAuthorizationCode,
|
|
880
|
+
reference: `prorate_${Date.now()}_${Math.random().toString(36).substring(7)}`,
|
|
881
|
+
metadata: {
|
|
882
|
+
type: "proration",
|
|
883
|
+
referenceId,
|
|
884
|
+
newPlan: plan.name,
|
|
885
|
+
oldPlan: existingSub.plan,
|
|
886
|
+
remainingDays
|
|
887
|
+
}
|
|
888
|
+
}));
|
|
889
|
+
if ((chargeData?.data?.status ?? chargeData?.status) !== "success") throw new APIError("BAD_REQUEST", { message: "Failed to process prorated charge via saved authorization." });
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
await getPaystackOps(options.paystackClient).subscriptionUpdate({
|
|
893
|
+
code: existingSub.paystackSubscriptionCode,
|
|
894
|
+
amount: newAmount,
|
|
895
|
+
plan: plan.planCode
|
|
896
|
+
});
|
|
897
|
+
await ctx.context.adapter.update({
|
|
898
|
+
model: "subscription",
|
|
899
|
+
where: [{
|
|
900
|
+
field: "id",
|
|
901
|
+
value: existingSub.id
|
|
902
|
+
}],
|
|
903
|
+
update: {
|
|
904
|
+
plan: plan.name,
|
|
905
|
+
seats: newSeatCount,
|
|
906
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
return ctx.json({
|
|
910
|
+
status: "success",
|
|
911
|
+
message: "Subscription successfully upgraded with prorated charge.",
|
|
912
|
+
prorated: true
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
}
|
|
664
916
|
if (plan) if (trialStart) initBody.amount = 5e3;
|
|
665
917
|
else {
|
|
666
918
|
initBody.plan = plan.planCode;
|
|
667
919
|
initBody.invoice_limit = plan.invoiceLimit;
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
920
|
+
let finalAmount;
|
|
921
|
+
if (amount !== void 0 && amount !== null) {
|
|
922
|
+
finalAmount = amount;
|
|
923
|
+
initBody.quantity = 1;
|
|
924
|
+
} else finalAmount = (plan.amount ?? 5e4) * (quantity ?? 1);
|
|
925
|
+
initBody.amount = Math.max(Math.round(finalAmount), 5e4);
|
|
671
926
|
}
|
|
672
927
|
else {
|
|
673
928
|
if (amount === void 0 || amount === null || amount === 0) throw new APIError("BAD_REQUEST", { message: "Amount is required for one-time payments" });
|
|
@@ -683,7 +938,7 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
683
938
|
ctx.context.logger.error("Failed to initialize Paystack transaction", error);
|
|
684
939
|
throw new APIError("BAD_REQUEST", {
|
|
685
940
|
code: "FAILED_TO_INITIALIZE_TRANSACTION",
|
|
686
|
-
message: error?.message ?? PAYSTACK_ERROR_CODES.FAILED_TO_INITIALIZE_TRANSACTION
|
|
941
|
+
message: error?.message ?? PAYSTACK_ERROR_CODES.FAILED_TO_INITIALIZE_TRANSACTION.message
|
|
687
942
|
});
|
|
688
943
|
}
|
|
689
944
|
await ctx.context.adapter.create({
|
|
@@ -765,7 +1020,7 @@ const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
|
|
|
765
1020
|
ctx.context.logger.error("Failed to verify Paystack transaction", error);
|
|
766
1021
|
throw new APIError("BAD_REQUEST", {
|
|
767
1022
|
code: "FAILED_TO_VERIFY_TRANSACTION",
|
|
768
|
-
message: error?.message ?? PAYSTACK_ERROR_CODES.FAILED_TO_VERIFY_TRANSACTION
|
|
1023
|
+
message: error?.message ?? PAYSTACK_ERROR_CODES.FAILED_TO_VERIFY_TRANSACTION.message
|
|
769
1024
|
});
|
|
770
1025
|
}
|
|
771
1026
|
const data = unwrapSdkResult(verifyRes);
|
|
@@ -998,7 +1253,8 @@ const listTransactions = (options, path = "/paystack/list-transactions") => {
|
|
|
998
1253
|
const enableDisableBodySchema = z.object({
|
|
999
1254
|
referenceId: z.string().optional(),
|
|
1000
1255
|
subscriptionCode: z.string(),
|
|
1001
|
-
emailToken: z.string().optional()
|
|
1256
|
+
emailToken: z.string().optional(),
|
|
1257
|
+
atPeriodEnd: z.boolean().optional()
|
|
1002
1258
|
});
|
|
1003
1259
|
function decodeBase64UrlToString(value) {
|
|
1004
1260
|
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
@@ -1029,7 +1285,7 @@ const disablePaystackSubscription = (options, path = "/paystack/disable-subscrip
|
|
|
1029
1285
|
referenceMiddleware(options, "disable-subscription")
|
|
1030
1286
|
] : [sessionMiddleware, originCheck]
|
|
1031
1287
|
}, async (ctx) => {
|
|
1032
|
-
const { subscriptionCode } = ctx.body;
|
|
1288
|
+
const { subscriptionCode, atPeriodEnd } = ctx.body;
|
|
1033
1289
|
const paystack = getPaystackOps(options.paystackClient);
|
|
1034
1290
|
try {
|
|
1035
1291
|
if (subscriptionCode.startsWith("LOC_")) {
|
|
@@ -1044,8 +1300,8 @@ const disablePaystackSubscription = (options, path = "/paystack/disable-subscrip
|
|
|
1044
1300
|
await ctx.context.adapter.update({
|
|
1045
1301
|
model: "subscription",
|
|
1046
1302
|
update: {
|
|
1047
|
-
status: "active",
|
|
1048
|
-
cancelAtPeriodEnd:
|
|
1303
|
+
status: atPeriodEnd === false ? "canceled" : "active",
|
|
1304
|
+
cancelAtPeriodEnd: atPeriodEnd !== false,
|
|
1049
1305
|
updatedAt: /* @__PURE__ */ new Date()
|
|
1050
1306
|
},
|
|
1051
1307
|
where: [{
|
|
@@ -1087,8 +1343,8 @@ const disablePaystackSubscription = (options, path = "/paystack/disable-subscrip
|
|
|
1087
1343
|
if (sub) await ctx.context.adapter.update({
|
|
1088
1344
|
model: "subscription",
|
|
1089
1345
|
update: {
|
|
1090
|
-
status: "active",
|
|
1091
|
-
cancelAtPeriodEnd:
|
|
1346
|
+
status: atPeriodEnd === false ? "canceled" : "active",
|
|
1347
|
+
cancelAtPeriodEnd: atPeriodEnd !== false,
|
|
1092
1348
|
periodEnd,
|
|
1093
1349
|
updatedAt: /* @__PURE__ */ new Date()
|
|
1094
1350
|
},
|
|
@@ -1103,7 +1359,7 @@ const disablePaystackSubscription = (options, path = "/paystack/disable-subscrip
|
|
|
1103
1359
|
ctx.context.logger.error("Failed to disable subscription", error);
|
|
1104
1360
|
throw new APIError("BAD_REQUEST", {
|
|
1105
1361
|
code: "FAILED_TO_DISABLE_SUBSCRIPTION",
|
|
1106
|
-
message: error?.message ?? PAYSTACK_ERROR_CODES.FAILED_TO_DISABLE_SUBSCRIPTION
|
|
1362
|
+
message: error?.message ?? PAYSTACK_ERROR_CODES.FAILED_TO_DISABLE_SUBSCRIPTION.message
|
|
1107
1363
|
});
|
|
1108
1364
|
}
|
|
1109
1365
|
});
|
|
@@ -1153,7 +1409,7 @@ const enablePaystackSubscription = (options, path = "/paystack/enable-subscripti
|
|
|
1153
1409
|
ctx.context.logger.error("Failed to enable subscription", error);
|
|
1154
1410
|
throw new APIError("BAD_REQUEST", {
|
|
1155
1411
|
code: "FAILED_TO_ENABLE_SUBSCRIPTION",
|
|
1156
|
-
message: error?.message ?? PAYSTACK_ERROR_CODES.FAILED_TO_ENABLE_SUBSCRIPTION
|
|
1412
|
+
message: error?.message ?? PAYSTACK_ERROR_CODES.FAILED_TO_ENABLE_SUBSCRIPTION.message
|
|
1157
1413
|
});
|
|
1158
1414
|
}
|
|
1159
1415
|
});
|
|
@@ -1568,6 +1824,10 @@ const subscriptions = { subscription: { fields: {
|
|
|
1568
1824
|
seats: {
|
|
1569
1825
|
type: "number",
|
|
1570
1826
|
required: false
|
|
1827
|
+
},
|
|
1828
|
+
pendingPlan: {
|
|
1829
|
+
type: "string",
|
|
1830
|
+
required: false
|
|
1571
1831
|
}
|
|
1572
1832
|
} } };
|
|
1573
1833
|
const user = { user: { fields: { paystackCustomerCode: {
|
|
@@ -1706,44 +1966,9 @@ const getSchema = (options) => {
|
|
|
1706
1966
|
return mergeSchema(baseSchema, options.schema);
|
|
1707
1967
|
};
|
|
1708
1968
|
|
|
1709
|
-
//#endregion
|
|
1710
|
-
//#region src/limits.ts
|
|
1711
|
-
const getOrganizationSubscription = async (ctx, organizationId) => {
|
|
1712
|
-
return await ctx.context.adapter.findOne({
|
|
1713
|
-
model: "subscription",
|
|
1714
|
-
where: [{
|
|
1715
|
-
field: "referenceId",
|
|
1716
|
-
value: organizationId
|
|
1717
|
-
}]
|
|
1718
|
-
});
|
|
1719
|
-
};
|
|
1720
|
-
const checkSeatLimit = async (ctx, organizationId, seatsToAdd = 1) => {
|
|
1721
|
-
const subscription = await getOrganizationSubscription(ctx, organizationId);
|
|
1722
|
-
if (subscription?.seats === void 0 || subscription.seats === null) return true;
|
|
1723
|
-
const members = await ctx.context.adapter.findMany({
|
|
1724
|
-
model: "member",
|
|
1725
|
-
where: [{
|
|
1726
|
-
field: "organizationId",
|
|
1727
|
-
value: organizationId
|
|
1728
|
-
}]
|
|
1729
|
-
});
|
|
1730
|
-
if (members.length + seatsToAdd > subscription.seats) throw new APIError("FORBIDDEN", { message: `Organization member limit reached. Used: ${members.length}, Max: ${subscription.seats}` });
|
|
1731
|
-
return true;
|
|
1732
|
-
};
|
|
1733
|
-
const checkTeamLimit = async (ctx, organizationId, maxTeams) => {
|
|
1734
|
-
if ((await ctx.context.adapter.findMany({
|
|
1735
|
-
model: "team",
|
|
1736
|
-
where: [{
|
|
1737
|
-
field: "organizationId",
|
|
1738
|
-
value: organizationId
|
|
1739
|
-
}]
|
|
1740
|
-
})).length >= maxTeams) throw new APIError("FORBIDDEN", { message: `Organization team limit reached. Max teams: ${maxTeams}` });
|
|
1741
|
-
return true;
|
|
1742
|
-
};
|
|
1743
|
-
|
|
1744
1969
|
//#endregion
|
|
1745
1970
|
//#region src/index.ts
|
|
1746
|
-
const INTERNAL_ERROR_CODES = defineErrorCodes({ ...PAYSTACK_ERROR_CODES });
|
|
1971
|
+
const INTERNAL_ERROR_CODES = defineErrorCodes({ ...Object.fromEntries(Object.entries(PAYSTACK_ERROR_CODES).map(([key, value]) => [key, typeof value === "string" ? value : value.message])) });
|
|
1747
1972
|
const paystack = (options) => {
|
|
1748
1973
|
const routeOptions = options;
|
|
1749
1974
|
return {
|
|
@@ -1836,12 +2061,32 @@ const paystack = (options) => {
|
|
|
1836
2061
|
}
|
|
1837
2062
|
} } } : void 0
|
|
1838
2063
|
},
|
|
1839
|
-
member: {
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
2064
|
+
member: {
|
|
2065
|
+
create: {
|
|
2066
|
+
before: async (member, ctx) => {
|
|
2067
|
+
if (options.subscription?.enabled === true && member.organizationId && ctx !== null && ctx !== void 0) await checkSeatLimit(ctx, member.organizationId);
|
|
2068
|
+
},
|
|
2069
|
+
after: async (member, ctx) => {
|
|
2070
|
+
if (options.subscription?.enabled === true && member?.organizationId !== void 0 && member?.organizationId !== null && ctx !== void 0 && ctx !== null) await syncSubscriptionSeats(ctx, member.organizationId, options);
|
|
2071
|
+
}
|
|
2072
|
+
},
|
|
2073
|
+
delete: { after: async (member, ctx) => {
|
|
2074
|
+
if (options.subscription?.enabled === true && member?.organizationId !== void 0 && member?.organizationId !== null && ctx !== void 0 && ctx !== null) await syncSubscriptionSeats(ctx, member.organizationId, options);
|
|
2075
|
+
} }
|
|
2076
|
+
},
|
|
2077
|
+
invitation: {
|
|
2078
|
+
create: {
|
|
2079
|
+
before: async (invitation, ctx) => {
|
|
2080
|
+
if (options.subscription?.enabled === true && invitation.organizationId && ctx !== null && ctx !== void 0) await checkSeatLimit(ctx, invitation.organizationId);
|
|
2081
|
+
},
|
|
2082
|
+
after: async (invitation, ctx) => {
|
|
2083
|
+
if (options.subscription?.enabled === true && invitation?.organizationId !== void 0 && invitation?.organizationId !== null && ctx !== void 0 && ctx !== null) await syncSubscriptionSeats(ctx, invitation.organizationId, options);
|
|
2084
|
+
}
|
|
2085
|
+
},
|
|
2086
|
+
delete: { after: async (invitation, ctx) => {
|
|
2087
|
+
if (options.subscription?.enabled === true && invitation?.organizationId !== void 0 && invitation?.organizationId !== null && ctx !== void 0 && ctx !== null) await syncSubscriptionSeats(ctx, invitation.organizationId, options);
|
|
2088
|
+
} }
|
|
2089
|
+
},
|
|
1845
2090
|
team: { create: { before: async (team, ctx) => {
|
|
1846
2091
|
if (options.subscription?.enabled === true && team.organizationId && ctx !== null && ctx !== void 0) {
|
|
1847
2092
|
const subscription = await getOrganizationSubscription(ctx, team.organizationId);
|