@alexasomba/better-auth-paystack 1.1.0 → 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 +33 -1
- package/dist/client.d.mts.map +1 -1
- package/dist/client.mjs +15 -0
- package/dist/client.mjs.map +1 -1
- package/dist/index.d.mts +166 -89
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +539 -108
- package/dist/index.mjs.map +1 -1
- package/dist/{types-BVSLYZGY.d.mts → types-BOpjdQrr.d.mts} +63 -16
- package/dist/types-BOpjdQrr.d.mts.map +1 -0
- package/package.json +13 -13
- package/dist/types-BVSLYZGY.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);
|
|
@@ -99,6 +110,10 @@ function getPaystackOps(paystackClient) {
|
|
|
99
110
|
productDelete: (idOrCode) => {
|
|
100
111
|
if (paystackClient?.product_delete !== void 0) return paystackClient.product_delete({ params: { path: { id_or_code: idOrCode } } });
|
|
101
112
|
return paystackClient?.product?.delete?.(idOrCode);
|
|
113
|
+
},
|
|
114
|
+
planList: () => {
|
|
115
|
+
if (paystackClient?.plan_list !== void 0) return paystackClient.plan_list();
|
|
116
|
+
return paystackClient?.plan?.list?.();
|
|
102
117
|
}
|
|
103
118
|
};
|
|
104
119
|
}
|
|
@@ -175,7 +190,7 @@ async function syncProductQuantityFromPaystack(ctx, productName, paystackClient)
|
|
|
175
190
|
value: productName.toLowerCase().replace(/\s+/g, "-")
|
|
176
191
|
}]
|
|
177
192
|
});
|
|
178
|
-
if (
|
|
193
|
+
if (localProduct?.paystackId === void 0 || localProduct?.paystackId === null || localProduct?.paystackId === "") {
|
|
179
194
|
if (localProduct && localProduct.unlimited !== true && localProduct.quantity !== void 0 && localProduct.quantity > 0) await ctx.context.adapter.update({
|
|
180
195
|
model: "paystackProduct",
|
|
181
196
|
update: {
|
|
@@ -216,6 +231,51 @@ async function syncProductQuantityFromPaystack(ctx, productName, paystackClient)
|
|
|
216
231
|
});
|
|
217
232
|
}
|
|
218
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
|
+
}
|
|
219
279
|
|
|
220
280
|
//#endregion
|
|
221
281
|
//#region src/middleware.ts
|
|
@@ -256,6 +316,41 @@ const referenceMiddleware = (options, action) => createAuthMiddleware(async (ctx
|
|
|
256
316
|
throw new APIError("BAD_REQUEST", { message: "Passing referenceId isn't allowed without subscription.authorizeReference or valid organization membership." });
|
|
257
317
|
});
|
|
258
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
|
+
|
|
259
354
|
//#endregion
|
|
260
355
|
//#region src/routes.ts
|
|
261
356
|
const PAYSTACK_ERROR_CODES = defineErrorCodes({
|
|
@@ -364,10 +459,11 @@ const paystackWebhook = (options) => {
|
|
|
364
459
|
}
|
|
365
460
|
if (options.subscription?.enabled === true) try {
|
|
366
461
|
if (eventName === "subscription.create") {
|
|
367
|
-
const
|
|
368
|
-
const
|
|
369
|
-
const
|
|
370
|
-
|
|
462
|
+
const payloadData = data;
|
|
463
|
+
const subscriptionCode = payloadData?.subscription_code ?? payloadData?.subscription?.subscription_code ?? payloadData?.code;
|
|
464
|
+
const customerCode = payloadData?.customer?.customer_code ?? payloadData?.customer_code ?? payloadData?.customer?.code;
|
|
465
|
+
const planCode = payloadData?.plan?.plan_code ?? payloadData?.plan_code ?? payloadData?.plan;
|
|
466
|
+
let metadata = payloadData?.metadata;
|
|
371
467
|
if (typeof metadata === "string") try {
|
|
372
468
|
metadata = JSON.parse(metadata);
|
|
373
469
|
} catch {}
|
|
@@ -405,7 +501,7 @@ const paystackWebhook = (options) => {
|
|
|
405
501
|
paystackSubscriptionCode: subscriptionCode,
|
|
406
502
|
status: "active",
|
|
407
503
|
updatedAt: /* @__PURE__ */ new Date(),
|
|
408
|
-
periodEnd:
|
|
504
|
+
periodEnd: payloadData?.next_payment_date !== void 0 && payloadData?.next_payment_date !== null && payloadData?.next_payment_date !== "" ? new Date(payloadData.next_payment_date) : void 0
|
|
409
505
|
},
|
|
410
506
|
where: [{
|
|
411
507
|
field: "id",
|
|
@@ -438,7 +534,8 @@ const paystackWebhook = (options) => {
|
|
|
438
534
|
}
|
|
439
535
|
}
|
|
440
536
|
if (eventName === "subscription.disable" || eventName === "subscription.not_renew") {
|
|
441
|
-
const
|
|
537
|
+
const payloadData = data;
|
|
538
|
+
const subscriptionCode = payloadData?.subscription_code ?? payloadData?.subscription?.subscription_code ?? payloadData?.code;
|
|
442
539
|
if (subscriptionCode !== void 0 && subscriptionCode !== null && subscriptionCode !== "") {
|
|
443
540
|
const existing = await ctx.context.adapter.findOne({
|
|
444
541
|
model: "subscription",
|
|
@@ -448,11 +545,15 @@ const paystackWebhook = (options) => {
|
|
|
448
545
|
}]
|
|
449
546
|
});
|
|
450
547
|
let newStatus = "canceled";
|
|
451
|
-
|
|
548
|
+
const nextPaymentDate = data?.next_payment_date;
|
|
549
|
+
const periodEnd = nextPaymentDate ? new Date(nextPaymentDate) : existing?.periodEnd ? new Date(existing.periodEnd) : void 0;
|
|
550
|
+
if (periodEnd && periodEnd > /* @__PURE__ */ new Date()) newStatus = "active";
|
|
452
551
|
await ctx.context.adapter.update({
|
|
453
552
|
model: "subscription",
|
|
454
553
|
update: {
|
|
455
554
|
status: newStatus,
|
|
555
|
+
cancelAtPeriodEnd: true,
|
|
556
|
+
...periodEnd ? { periodEnd } : {},
|
|
456
557
|
updatedAt: /* @__PURE__ */ new Date()
|
|
457
558
|
},
|
|
458
559
|
where: [{
|
|
@@ -469,6 +570,31 @@ const paystackWebhook = (options) => {
|
|
|
469
570
|
}, ctx);
|
|
470
571
|
}
|
|
471
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
|
+
}
|
|
472
598
|
} catch (_e) {
|
|
473
599
|
ctx.context.logger.error("Failed to sync Paystack webhook event", _e);
|
|
474
600
|
}
|
|
@@ -485,7 +611,10 @@ const initializeTransactionBodySchema = z.object({
|
|
|
485
611
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
486
612
|
referenceId: z.string().optional(),
|
|
487
613
|
callbackURL: z.string().optional(),
|
|
488
|
-
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()
|
|
489
618
|
});
|
|
490
619
|
const initializeTransaction = (options, path = "/paystack/initialize-transaction") => {
|
|
491
620
|
const subscriptionOptions = options.subscription;
|
|
@@ -499,7 +628,7 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
499
628
|
] : [sessionMiddleware, originCheck]
|
|
500
629
|
}, async (ctx) => {
|
|
501
630
|
const paystack = getPaystackOps(options.paystackClient);
|
|
502
|
-
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;
|
|
503
632
|
if (callbackURL !== void 0 && callbackURL !== null && callbackURL !== "") {
|
|
504
633
|
const checkTrusted = () => {
|
|
505
634
|
try {
|
|
@@ -523,20 +652,46 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
523
652
|
const user = session.user;
|
|
524
653
|
if (subscriptionOptions?.enabled === true && subscriptionOptions.requireEmailVerification === true && !user.emailVerified) throw new APIError("BAD_REQUEST", {
|
|
525
654
|
code: "EMAIL_VERIFICATION_REQUIRED",
|
|
526
|
-
message: PAYSTACK_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED
|
|
655
|
+
message: PAYSTACK_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED.message
|
|
527
656
|
});
|
|
528
657
|
let plan;
|
|
529
658
|
let product;
|
|
530
659
|
if (planName !== void 0 && planName !== null && planName !== "") {
|
|
531
660
|
if (subscriptionOptions?.enabled !== true) throw new APIError("BAD_REQUEST", { message: "Subscriptions are not enabled." });
|
|
532
661
|
plan = await getPlanByName(options, planName) ?? void 0;
|
|
662
|
+
if (!plan) {
|
|
663
|
+
const nativePlan = await ctx.context.adapter.findOne({
|
|
664
|
+
model: "paystackPlan",
|
|
665
|
+
where: [{
|
|
666
|
+
field: "name",
|
|
667
|
+
value: planName
|
|
668
|
+
}]
|
|
669
|
+
});
|
|
670
|
+
if (nativePlan) plan = nativePlan;
|
|
671
|
+
else plan = await ctx.context.adapter.findOne({
|
|
672
|
+
model: "paystackPlan",
|
|
673
|
+
where: [{
|
|
674
|
+
field: "planCode",
|
|
675
|
+
value: planName
|
|
676
|
+
}]
|
|
677
|
+
}) ?? void 0;
|
|
678
|
+
}
|
|
533
679
|
if (!plan) throw new APIError("BAD_REQUEST", {
|
|
534
680
|
code: "SUBSCRIPTION_PLAN_NOT_FOUND",
|
|
535
|
-
message: PAYSTACK_ERROR_CODES.SUBSCRIPTION_PLAN_NOT_FOUND,
|
|
681
|
+
message: PAYSTACK_ERROR_CODES.SUBSCRIPTION_PLAN_NOT_FOUND.message,
|
|
536
682
|
status: 400
|
|
537
683
|
});
|
|
538
684
|
} else if (productName !== void 0 && productName !== null && productName !== "") {
|
|
539
|
-
if (typeof productName === "string")
|
|
685
|
+
if (typeof productName === "string") {
|
|
686
|
+
product ??= await getProductByName(options, productName) ?? void 0;
|
|
687
|
+
product ??= await ctx.context.adapter.findOne({
|
|
688
|
+
model: "paystackProduct",
|
|
689
|
+
where: [{
|
|
690
|
+
field: "name",
|
|
691
|
+
value: productName
|
|
692
|
+
}]
|
|
693
|
+
}) ?? void 0;
|
|
694
|
+
}
|
|
540
695
|
if (!product) throw new APIError("BAD_REQUEST", {
|
|
541
696
|
message: `Product '${productName}' not found.`,
|
|
542
697
|
status: 400
|
|
@@ -545,13 +700,67 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
545
700
|
message: "Either 'plan', 'product', or 'amount' is required to initialize a transaction.",
|
|
546
701
|
status: 400
|
|
547
702
|
});
|
|
548
|
-
|
|
703
|
+
let amount = bodyAmount ?? product?.price;
|
|
549
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
|
+
}
|
|
550
761
|
let url;
|
|
551
762
|
let reference;
|
|
552
763
|
let accessCode;
|
|
553
|
-
const referenceIdFromCtx = ctx.context.referenceId;
|
|
554
|
-
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;
|
|
555
764
|
let trialStart;
|
|
556
765
|
let trialEnd;
|
|
557
766
|
if (plan?.freeTrial?.days !== void 0 && plan.freeTrial.days !== null && plan.freeTrial.days > 0) {
|
|
@@ -625,13 +834,95 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
625
834
|
const ops = getPaystackOps(options.paystackClient);
|
|
626
835
|
if (initBody.email !== void 0 && initBody.email !== null && initBody.email !== "") await ops.customerUpdate(paystackCustomerCode, { email: initBody.email });
|
|
627
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
|
+
}
|
|
628
916
|
if (plan) if (trialStart) initBody.amount = 5e3;
|
|
629
917
|
else {
|
|
630
918
|
initBody.plan = plan.planCode;
|
|
631
919
|
initBody.invoice_limit = plan.invoiceLimit;
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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);
|
|
635
926
|
}
|
|
636
927
|
else {
|
|
637
928
|
if (amount === void 0 || amount === null || amount === 0) throw new APIError("BAD_REQUEST", { message: "Amount is required for one-time payments" });
|
|
@@ -647,7 +938,7 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
|
|
|
647
938
|
ctx.context.logger.error("Failed to initialize Paystack transaction", error);
|
|
648
939
|
throw new APIError("BAD_REQUEST", {
|
|
649
940
|
code: "FAILED_TO_INITIALIZE_TRANSACTION",
|
|
650
|
-
message: error?.message ?? PAYSTACK_ERROR_CODES.FAILED_TO_INITIALIZE_TRANSACTION
|
|
941
|
+
message: error?.message ?? PAYSTACK_ERROR_CODES.FAILED_TO_INITIALIZE_TRANSACTION.message
|
|
651
942
|
});
|
|
652
943
|
}
|
|
653
944
|
await ctx.context.adapter.create({
|
|
@@ -729,7 +1020,7 @@ const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
|
|
|
729
1020
|
ctx.context.logger.error("Failed to verify Paystack transaction", error);
|
|
730
1021
|
throw new APIError("BAD_REQUEST", {
|
|
731
1022
|
code: "FAILED_TO_VERIFY_TRANSACTION",
|
|
732
|
-
message: error?.message ?? PAYSTACK_ERROR_CODES.FAILED_TO_VERIFY_TRANSACTION
|
|
1023
|
+
message: error?.message ?? PAYSTACK_ERROR_CODES.FAILED_TO_VERIFY_TRANSACTION.message
|
|
733
1024
|
});
|
|
734
1025
|
}
|
|
735
1026
|
const data = unwrapSdkResult(verifyRes);
|
|
@@ -846,7 +1137,17 @@ const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
|
|
|
846
1137
|
if (planCodeFromPaystack === void 0 || planCodeFromPaystack === null || planCodeFromPaystack === "") paystackSubscriptionCode = `LOC_${reference}`;
|
|
847
1138
|
else paystackSubscriptionCode = (data?.subscription)?.subscription_code;
|
|
848
1139
|
}
|
|
849
|
-
const
|
|
1140
|
+
const existingSubs = await ctx.context.adapter.findMany({
|
|
1141
|
+
model: "subscription",
|
|
1142
|
+
where: [{
|
|
1143
|
+
field: "paystackTransactionReference",
|
|
1144
|
+
value: reference
|
|
1145
|
+
}]
|
|
1146
|
+
});
|
|
1147
|
+
let targetSub;
|
|
1148
|
+
if (existingSubs && existingSubs.length > 0) targetSub = existingSubs.find((s) => !(referenceId !== void 0 && referenceId !== null && referenceId !== "") || s.referenceId === referenceId);
|
|
1149
|
+
let updatedSubscription = null;
|
|
1150
|
+
if (targetSub !== void 0 && targetSub !== null) updatedSubscription = await ctx.context.adapter.update({
|
|
850
1151
|
model: "subscription",
|
|
851
1152
|
update: {
|
|
852
1153
|
status: isTrial === true ? "trialing" : "active",
|
|
@@ -861,12 +1162,9 @@ const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
|
|
|
861
1162
|
...authorizationCode !== void 0 && authorizationCode !== null && authorizationCode !== "" ? { paystackAuthorizationCode: authorizationCode } : {}
|
|
862
1163
|
},
|
|
863
1164
|
where: [{
|
|
864
|
-
field: "
|
|
865
|
-
value:
|
|
866
|
-
}
|
|
867
|
-
field: "referenceId",
|
|
868
|
-
value: referenceId
|
|
869
|
-
}] : []]
|
|
1165
|
+
field: "id",
|
|
1166
|
+
value: targetSub.id
|
|
1167
|
+
}]
|
|
870
1168
|
});
|
|
871
1169
|
if (updatedSubscription && subscriptionOptions?.enabled === true && "onSubscriptionComplete" in subscriptionOptions && typeof subscriptionOptions.onSubscriptionComplete === "function") {
|
|
872
1170
|
const plan = (await getPlans(subscriptionOptions)).find((p) => p.name.toLowerCase() === updatedSubscription.plan.toLowerCase());
|
|
@@ -955,7 +1253,8 @@ const listTransactions = (options, path = "/paystack/list-transactions") => {
|
|
|
955
1253
|
const enableDisableBodySchema = z.object({
|
|
956
1254
|
referenceId: z.string().optional(),
|
|
957
1255
|
subscriptionCode: z.string(),
|
|
958
|
-
emailToken: z.string().optional()
|
|
1256
|
+
emailToken: z.string().optional(),
|
|
1257
|
+
atPeriodEnd: z.boolean().optional()
|
|
959
1258
|
});
|
|
960
1259
|
function decodeBase64UrlToString(value) {
|
|
961
1260
|
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
@@ -986,7 +1285,7 @@ const disablePaystackSubscription = (options, path = "/paystack/disable-subscrip
|
|
|
986
1285
|
referenceMiddleware(options, "disable-subscription")
|
|
987
1286
|
] : [sessionMiddleware, originCheck]
|
|
988
1287
|
}, async (ctx) => {
|
|
989
|
-
const { subscriptionCode } = ctx.body;
|
|
1288
|
+
const { subscriptionCode, atPeriodEnd } = ctx.body;
|
|
990
1289
|
const paystack = getPaystackOps(options.paystackClient);
|
|
991
1290
|
try {
|
|
992
1291
|
if (subscriptionCode.startsWith("LOC_")) {
|
|
@@ -1001,8 +1300,8 @@ const disablePaystackSubscription = (options, path = "/paystack/disable-subscrip
|
|
|
1001
1300
|
await ctx.context.adapter.update({
|
|
1002
1301
|
model: "subscription",
|
|
1003
1302
|
update: {
|
|
1004
|
-
status: "active",
|
|
1005
|
-
cancelAtPeriodEnd:
|
|
1303
|
+
status: atPeriodEnd === false ? "canceled" : "active",
|
|
1304
|
+
cancelAtPeriodEnd: atPeriodEnd !== false,
|
|
1006
1305
|
updatedAt: /* @__PURE__ */ new Date()
|
|
1007
1306
|
},
|
|
1008
1307
|
where: [{
|
|
@@ -1044,8 +1343,8 @@ const disablePaystackSubscription = (options, path = "/paystack/disable-subscrip
|
|
|
1044
1343
|
if (sub) await ctx.context.adapter.update({
|
|
1045
1344
|
model: "subscription",
|
|
1046
1345
|
update: {
|
|
1047
|
-
status: "active",
|
|
1048
|
-
cancelAtPeriodEnd:
|
|
1346
|
+
status: atPeriodEnd === false ? "canceled" : "active",
|
|
1347
|
+
cancelAtPeriodEnd: atPeriodEnd !== false,
|
|
1049
1348
|
periodEnd,
|
|
1050
1349
|
updatedAt: /* @__PURE__ */ new Date()
|
|
1051
1350
|
},
|
|
@@ -1060,7 +1359,7 @@ const disablePaystackSubscription = (options, path = "/paystack/disable-subscrip
|
|
|
1060
1359
|
ctx.context.logger.error("Failed to disable subscription", error);
|
|
1061
1360
|
throw new APIError("BAD_REQUEST", {
|
|
1062
1361
|
code: "FAILED_TO_DISABLE_SUBSCRIPTION",
|
|
1063
|
-
message: error?.message ?? PAYSTACK_ERROR_CODES.FAILED_TO_DISABLE_SUBSCRIPTION
|
|
1362
|
+
message: error?.message ?? PAYSTACK_ERROR_CODES.FAILED_TO_DISABLE_SUBSCRIPTION.message
|
|
1064
1363
|
});
|
|
1065
1364
|
}
|
|
1066
1365
|
});
|
|
@@ -1110,33 +1409,40 @@ const enablePaystackSubscription = (options, path = "/paystack/enable-subscripti
|
|
|
1110
1409
|
ctx.context.logger.error("Failed to enable subscription", error);
|
|
1111
1410
|
throw new APIError("BAD_REQUEST", {
|
|
1112
1411
|
code: "FAILED_TO_ENABLE_SUBSCRIPTION",
|
|
1113
|
-
message: error?.message ?? PAYSTACK_ERROR_CODES.FAILED_TO_ENABLE_SUBSCRIPTION
|
|
1412
|
+
message: error?.message ?? PAYSTACK_ERROR_CODES.FAILED_TO_ENABLE_SUBSCRIPTION.message
|
|
1114
1413
|
});
|
|
1115
1414
|
}
|
|
1116
1415
|
});
|
|
1117
1416
|
};
|
|
1118
|
-
const getSubscriptionManageLink = (options) => {
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
] : [sessionMiddleware, originCheck]
|
|
1127
|
-
}, async (ctx) => {
|
|
1417
|
+
const getSubscriptionManageLink = (options, path = "/paystack/get-subscription-manage-link") => {
|
|
1418
|
+
const manageLinkQuerySchema = z.object({ subscriptionCode: z.string() });
|
|
1419
|
+
const useMiddlewares = options.subscription?.enabled === true ? [
|
|
1420
|
+
sessionMiddleware,
|
|
1421
|
+
originCheck,
|
|
1422
|
+
referenceMiddleware(options, "get-subscription-manage-link")
|
|
1423
|
+
] : [sessionMiddleware, originCheck];
|
|
1424
|
+
const handler = async (ctx) => {
|
|
1128
1425
|
const { subscriptionCode } = ctx.query;
|
|
1426
|
+
if (subscriptionCode.startsWith("LOC_") || subscriptionCode.startsWith("sub_local_")) return ctx.json({
|
|
1427
|
+
link: null,
|
|
1428
|
+
message: "Local subscriptions cannot be managed on Paystack"
|
|
1429
|
+
});
|
|
1129
1430
|
const paystack = getPaystackOps(options.paystackClient);
|
|
1130
1431
|
try {
|
|
1131
1432
|
const res = unwrapSdkResult(await paystack.subscriptionManageLink(subscriptionCode));
|
|
1132
|
-
const data = res !== null && res !== void 0 && "status" in res && "data" in res ? res.data : res?.data !== void 0 ? res.data : res;
|
|
1433
|
+
const data = res !== null && res !== void 0 && typeof res === "object" && "status" in res && "data" in res ? res.data : res?.data !== void 0 ? res.data : res;
|
|
1133
1434
|
const link = typeof data === "string" ? data : data?.link;
|
|
1134
1435
|
return ctx.json({ link });
|
|
1135
1436
|
} catch (error) {
|
|
1136
1437
|
ctx.context.logger.error("Failed to get subscription manage link", error);
|
|
1137
1438
|
throw new APIError("BAD_REQUEST", { message: error?.message ?? "Failed to get subscription manage link" });
|
|
1138
1439
|
}
|
|
1139
|
-
}
|
|
1440
|
+
};
|
|
1441
|
+
return createAuthEndpoint(path, {
|
|
1442
|
+
method: "GET",
|
|
1443
|
+
query: manageLinkQuerySchema,
|
|
1444
|
+
use: useMiddlewares
|
|
1445
|
+
}, handler);
|
|
1140
1446
|
};
|
|
1141
1447
|
const syncProducts = (options) => {
|
|
1142
1448
|
return createAuthEndpoint("/paystack/sync-products", {
|
|
@@ -1201,6 +1507,91 @@ const syncProducts = (options) => {
|
|
|
1201
1507
|
}
|
|
1202
1508
|
});
|
|
1203
1509
|
};
|
|
1510
|
+
const listProducts = (_options) => {
|
|
1511
|
+
return createAuthEndpoint("/paystack/list-products", {
|
|
1512
|
+
method: "GET",
|
|
1513
|
+
metadata: { openapi: { operationId: "listPaystackProducts" } }
|
|
1514
|
+
}, async (ctx) => {
|
|
1515
|
+
const sorted = (await ctx.context.adapter.findMany({ model: "paystackProduct" })).sort((a, b) => a.name.localeCompare(b.name));
|
|
1516
|
+
return ctx.json({ products: sorted });
|
|
1517
|
+
});
|
|
1518
|
+
};
|
|
1519
|
+
const syncPlans = (options) => {
|
|
1520
|
+
return createAuthEndpoint("/paystack/sync-plans", {
|
|
1521
|
+
method: "POST",
|
|
1522
|
+
metadata: { ...HIDE_METADATA },
|
|
1523
|
+
disableBody: true,
|
|
1524
|
+
use: [sessionMiddleware]
|
|
1525
|
+
}, async (ctx) => {
|
|
1526
|
+
const paystack = getPaystackOps(options.paystackClient);
|
|
1527
|
+
try {
|
|
1528
|
+
const res = unwrapSdkResult(await paystack.planList());
|
|
1529
|
+
const plansData = res !== null && typeof res === "object" && "status" in res && "data" in res ? res.data : res?.data ?? res;
|
|
1530
|
+
if (!Array.isArray(plansData)) return ctx.json({
|
|
1531
|
+
status: "success",
|
|
1532
|
+
count: 0
|
|
1533
|
+
});
|
|
1534
|
+
for (const plan of plansData) {
|
|
1535
|
+
const paystackId = String(plan.id);
|
|
1536
|
+
const existing = await ctx.context.adapter.findOne({
|
|
1537
|
+
model: "paystackPlan",
|
|
1538
|
+
where: [{
|
|
1539
|
+
field: "paystackId",
|
|
1540
|
+
value: paystackId
|
|
1541
|
+
}]
|
|
1542
|
+
});
|
|
1543
|
+
const planData = {
|
|
1544
|
+
name: plan.name,
|
|
1545
|
+
description: plan.description,
|
|
1546
|
+
amount: plan.amount,
|
|
1547
|
+
currency: plan.currency,
|
|
1548
|
+
interval: plan.interval,
|
|
1549
|
+
planCode: plan.plan_code,
|
|
1550
|
+
paystackId,
|
|
1551
|
+
metadata: plan.metadata ? JSON.stringify(plan.metadata) : void 0,
|
|
1552
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1553
|
+
};
|
|
1554
|
+
if (existing) await ctx.context.adapter.update({
|
|
1555
|
+
model: "paystackPlan",
|
|
1556
|
+
update: planData,
|
|
1557
|
+
where: [{
|
|
1558
|
+
field: "id",
|
|
1559
|
+
value: existing.id
|
|
1560
|
+
}]
|
|
1561
|
+
});
|
|
1562
|
+
else await ctx.context.adapter.create({
|
|
1563
|
+
model: "paystackPlan",
|
|
1564
|
+
data: {
|
|
1565
|
+
...planData,
|
|
1566
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
1567
|
+
}
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
return ctx.json({
|
|
1571
|
+
status: "success",
|
|
1572
|
+
count: plansData.length
|
|
1573
|
+
});
|
|
1574
|
+
} catch (error) {
|
|
1575
|
+
ctx.context.logger.error("Failed to sync plans", error);
|
|
1576
|
+
throw new APIError("BAD_REQUEST", { message: error?.message ?? "Failed to sync plans" });
|
|
1577
|
+
}
|
|
1578
|
+
});
|
|
1579
|
+
};
|
|
1580
|
+
const listPlans = (_options) => {
|
|
1581
|
+
return createAuthEndpoint("/paystack/list-plans", {
|
|
1582
|
+
method: "GET",
|
|
1583
|
+
metadata: { ...HIDE_METADATA },
|
|
1584
|
+
use: [sessionMiddleware]
|
|
1585
|
+
}, async (ctx) => {
|
|
1586
|
+
try {
|
|
1587
|
+
const plans = await ctx.context.adapter.findMany({ model: "paystackPlan" });
|
|
1588
|
+
return ctx.json({ plans });
|
|
1589
|
+
} catch (error) {
|
|
1590
|
+
ctx.context.logger.error("Failed to list plans", error);
|
|
1591
|
+
throw new APIError("BAD_REQUEST", { message: error?.message ?? "Failed to list plans" });
|
|
1592
|
+
}
|
|
1593
|
+
});
|
|
1594
|
+
};
|
|
1204
1595
|
const getConfig = (options) => {
|
|
1205
1596
|
return createAuthEndpoint("/paystack/get-config", {
|
|
1206
1597
|
method: "GET",
|
|
@@ -1433,6 +1824,10 @@ const subscriptions = { subscription: { fields: {
|
|
|
1433
1824
|
seats: {
|
|
1434
1825
|
type: "number",
|
|
1435
1826
|
required: false
|
|
1827
|
+
},
|
|
1828
|
+
pendingPlan: {
|
|
1829
|
+
type: "string",
|
|
1830
|
+
required: false
|
|
1436
1831
|
}
|
|
1437
1832
|
} } };
|
|
1438
1833
|
const user = { user: { fields: { paystackCustomerCode: {
|
|
@@ -1501,18 +1896,64 @@ const products = { paystackProduct: { fields: {
|
|
|
1501
1896
|
required: true
|
|
1502
1897
|
}
|
|
1503
1898
|
} } };
|
|
1899
|
+
const plans = { paystackPlan: { fields: {
|
|
1900
|
+
name: {
|
|
1901
|
+
type: "string",
|
|
1902
|
+
required: true
|
|
1903
|
+
},
|
|
1904
|
+
description: {
|
|
1905
|
+
type: "string",
|
|
1906
|
+
required: false
|
|
1907
|
+
},
|
|
1908
|
+
amount: {
|
|
1909
|
+
type: "number",
|
|
1910
|
+
required: true
|
|
1911
|
+
},
|
|
1912
|
+
currency: {
|
|
1913
|
+
type: "string",
|
|
1914
|
+
required: true
|
|
1915
|
+
},
|
|
1916
|
+
interval: {
|
|
1917
|
+
type: "string",
|
|
1918
|
+
required: true
|
|
1919
|
+
},
|
|
1920
|
+
planCode: {
|
|
1921
|
+
type: "string",
|
|
1922
|
+
required: true,
|
|
1923
|
+
unique: true
|
|
1924
|
+
},
|
|
1925
|
+
paystackId: {
|
|
1926
|
+
type: "string",
|
|
1927
|
+
required: true,
|
|
1928
|
+
unique: true
|
|
1929
|
+
},
|
|
1930
|
+
metadata: {
|
|
1931
|
+
type: "string",
|
|
1932
|
+
required: false
|
|
1933
|
+
},
|
|
1934
|
+
createdAt: {
|
|
1935
|
+
type: "date",
|
|
1936
|
+
required: true
|
|
1937
|
+
},
|
|
1938
|
+
updatedAt: {
|
|
1939
|
+
type: "date",
|
|
1940
|
+
required: true
|
|
1941
|
+
}
|
|
1942
|
+
} } };
|
|
1504
1943
|
const getSchema = (options) => {
|
|
1505
1944
|
let baseSchema;
|
|
1506
1945
|
if (options.subscription?.enabled === true) baseSchema = {
|
|
1507
1946
|
...subscriptions,
|
|
1508
1947
|
...transactions,
|
|
1509
1948
|
...user,
|
|
1510
|
-
...products
|
|
1949
|
+
...products,
|
|
1950
|
+
...plans
|
|
1511
1951
|
};
|
|
1512
1952
|
else baseSchema = {
|
|
1513
1953
|
...user,
|
|
1514
1954
|
...transactions,
|
|
1515
|
-
...products
|
|
1955
|
+
...products,
|
|
1956
|
+
...plans
|
|
1516
1957
|
};
|
|
1517
1958
|
if (options.organization?.enabled === true) baseSchema = {
|
|
1518
1959
|
...baseSchema,
|
|
@@ -1525,63 +1966,33 @@ const getSchema = (options) => {
|
|
|
1525
1966
|
return mergeSchema(baseSchema, options.schema);
|
|
1526
1967
|
};
|
|
1527
1968
|
|
|
1528
|
-
//#endregion
|
|
1529
|
-
//#region src/limits.ts
|
|
1530
|
-
const getOrganizationSubscription = async (ctx, organizationId) => {
|
|
1531
|
-
return await ctx.context.adapter.findOne({
|
|
1532
|
-
model: "subscription",
|
|
1533
|
-
where: [{
|
|
1534
|
-
field: "referenceId",
|
|
1535
|
-
value: organizationId
|
|
1536
|
-
}]
|
|
1537
|
-
});
|
|
1538
|
-
};
|
|
1539
|
-
const checkSeatLimit = async (ctx, organizationId, seatsToAdd = 1) => {
|
|
1540
|
-
const subscription = await getOrganizationSubscription(ctx, organizationId);
|
|
1541
|
-
if (subscription?.seats === void 0 || subscription.seats === null) return true;
|
|
1542
|
-
const members = await ctx.context.adapter.findMany({
|
|
1543
|
-
model: "member",
|
|
1544
|
-
where: [{
|
|
1545
|
-
field: "organizationId",
|
|
1546
|
-
value: organizationId
|
|
1547
|
-
}]
|
|
1548
|
-
});
|
|
1549
|
-
if (members.length + seatsToAdd > subscription.seats) throw new APIError("FORBIDDEN", { message: `Organization member limit reached. Used: ${members.length}, Max: ${subscription.seats}` });
|
|
1550
|
-
return true;
|
|
1551
|
-
};
|
|
1552
|
-
const checkTeamLimit = async (ctx, organizationId, maxTeams) => {
|
|
1553
|
-
if ((await ctx.context.adapter.findMany({
|
|
1554
|
-
model: "team",
|
|
1555
|
-
where: [{
|
|
1556
|
-
field: "organizationId",
|
|
1557
|
-
value: organizationId
|
|
1558
|
-
}]
|
|
1559
|
-
})).length >= maxTeams) throw new APIError("FORBIDDEN", { message: `Organization team limit reached. Max teams: ${maxTeams}` });
|
|
1560
|
-
return true;
|
|
1561
|
-
};
|
|
1562
|
-
|
|
1563
1969
|
//#endregion
|
|
1564
1970
|
//#region src/index.ts
|
|
1565
|
-
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])) });
|
|
1566
1972
|
const paystack = (options) => {
|
|
1973
|
+
const routeOptions = options;
|
|
1567
1974
|
return {
|
|
1568
1975
|
id: "paystack",
|
|
1569
1976
|
endpoints: {
|
|
1570
|
-
initializeTransaction: initializeTransaction(
|
|
1571
|
-
verifyTransaction: verifyTransaction(
|
|
1572
|
-
listSubscriptions: listSubscriptions(
|
|
1573
|
-
paystackWebhook: paystackWebhook(
|
|
1574
|
-
listTransactions: listTransactions(
|
|
1575
|
-
getConfig: getConfig(
|
|
1576
|
-
disableSubscription: disablePaystackSubscription(
|
|
1577
|
-
enableSubscription: enablePaystackSubscription(
|
|
1578
|
-
getSubscriptionManageLink: getSubscriptionManageLink(
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1977
|
+
initializeTransaction: initializeTransaction(routeOptions),
|
|
1978
|
+
verifyTransaction: verifyTransaction(routeOptions),
|
|
1979
|
+
listSubscriptions: listSubscriptions(routeOptions),
|
|
1980
|
+
paystackWebhook: paystackWebhook(routeOptions),
|
|
1981
|
+
listTransactions: listTransactions(routeOptions),
|
|
1982
|
+
getConfig: getConfig(routeOptions),
|
|
1983
|
+
disableSubscription: disablePaystackSubscription(routeOptions),
|
|
1984
|
+
enableSubscription: enablePaystackSubscription(routeOptions),
|
|
1985
|
+
getSubscriptionManageLink: getSubscriptionManageLink(routeOptions),
|
|
1986
|
+
subscriptionManageLink: getSubscriptionManageLink(routeOptions, "/paystack/subscription/manage-link"),
|
|
1987
|
+
createSubscription: createSubscription(routeOptions),
|
|
1988
|
+
upgradeSubscription: upgradeSubscription(routeOptions),
|
|
1989
|
+
cancelSubscription: cancelSubscription(routeOptions),
|
|
1990
|
+
restoreSubscription: restoreSubscription(routeOptions),
|
|
1991
|
+
chargeRecurringSubscription: chargeRecurringSubscription(routeOptions),
|
|
1992
|
+
syncProducts: syncProducts(routeOptions),
|
|
1993
|
+
listProducts: listProducts(routeOptions),
|
|
1994
|
+
syncPlans: syncPlans(routeOptions),
|
|
1995
|
+
listPlans: listPlans(routeOptions)
|
|
1585
1996
|
},
|
|
1586
1997
|
schema: getSchema(options),
|
|
1587
1998
|
init: (ctx) => {
|
|
@@ -1650,12 +2061,32 @@ const paystack = (options) => {
|
|
|
1650
2061
|
}
|
|
1651
2062
|
} } } : void 0
|
|
1652
2063
|
},
|
|
1653
|
-
member: {
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
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
|
+
},
|
|
1659
2090
|
team: { create: { before: async (team, ctx) => {
|
|
1660
2091
|
if (options.subscription?.enabled === true && team.organizationId && ctx !== null && ctx !== void 0) {
|
|
1661
2092
|
const subscription = await getOrganizationSubscription(ctx, team.organizationId);
|