@alexasomba/better-auth-paystack 1.1.0 → 1.1.2

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/dist/index.mjs CHANGED
@@ -99,6 +99,10 @@ function getPaystackOps(paystackClient) {
99
99
  productDelete: (idOrCode) => {
100
100
  if (paystackClient?.product_delete !== void 0) return paystackClient.product_delete({ params: { path: { id_or_code: idOrCode } } });
101
101
  return paystackClient?.product?.delete?.(idOrCode);
102
+ },
103
+ planList: () => {
104
+ if (paystackClient?.plan_list !== void 0) return paystackClient.plan_list();
105
+ return paystackClient?.plan?.list?.();
102
106
  }
103
107
  };
104
108
  }
@@ -175,7 +179,7 @@ async function syncProductQuantityFromPaystack(ctx, productName, paystackClient)
175
179
  value: productName.toLowerCase().replace(/\s+/g, "-")
176
180
  }]
177
181
  });
178
- if (!localProduct?.paystackId) {
182
+ if (localProduct?.paystackId === void 0 || localProduct?.paystackId === null || localProduct?.paystackId === "") {
179
183
  if (localProduct && localProduct.unlimited !== true && localProduct.quantity !== void 0 && localProduct.quantity > 0) await ctx.context.adapter.update({
180
184
  model: "paystackProduct",
181
185
  update: {
@@ -364,10 +368,11 @@ const paystackWebhook = (options) => {
364
368
  }
365
369
  if (options.subscription?.enabled === true) try {
366
370
  if (eventName === "subscription.create") {
367
- const subscriptionCode = data?.subscription_code ?? data?.subscription?.subscription_code ?? data?.code;
368
- const customerCode = data?.customer?.customer_code ?? data?.customer_code ?? data?.customer?.code;
369
- const planCode = data?.plan?.plan_code ?? data?.plan_code ?? data?.plan;
370
- let metadata = data?.metadata;
371
+ const payloadData = data;
372
+ const subscriptionCode = payloadData?.subscription_code ?? payloadData?.subscription?.subscription_code ?? payloadData?.code;
373
+ const customerCode = payloadData?.customer?.customer_code ?? payloadData?.customer_code ?? payloadData?.customer?.code;
374
+ const planCode = payloadData?.plan?.plan_code ?? payloadData?.plan_code ?? payloadData?.plan;
375
+ let metadata = payloadData?.metadata;
371
376
  if (typeof metadata === "string") try {
372
377
  metadata = JSON.parse(metadata);
373
378
  } catch {}
@@ -405,7 +410,7 @@ const paystackWebhook = (options) => {
405
410
  paystackSubscriptionCode: subscriptionCode,
406
411
  status: "active",
407
412
  updatedAt: /* @__PURE__ */ new Date(),
408
- periodEnd: data?.next_payment_date !== void 0 && data?.next_payment_date !== null && data?.next_payment_date !== "" ? new Date(data.next_payment_date) : void 0
413
+ 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
414
  },
410
415
  where: [{
411
416
  field: "id",
@@ -438,7 +443,8 @@ const paystackWebhook = (options) => {
438
443
  }
439
444
  }
440
445
  if (eventName === "subscription.disable" || eventName === "subscription.not_renew") {
441
- const subscriptionCode = data?.subscription_code ?? data?.subscription?.subscription_code ?? data?.code;
446
+ const payloadData = data;
447
+ const subscriptionCode = payloadData?.subscription_code ?? payloadData?.subscription?.subscription_code ?? payloadData?.code;
442
448
  if (subscriptionCode !== void 0 && subscriptionCode !== null && subscriptionCode !== "") {
443
449
  const existing = await ctx.context.adapter.findOne({
444
450
  model: "subscription",
@@ -448,11 +454,15 @@ const paystackWebhook = (options) => {
448
454
  }]
449
455
  });
450
456
  let newStatus = "canceled";
451
- if (existing?.cancelAtPeriodEnd === true && existing.periodEnd !== void 0 && existing.periodEnd !== null && new Date(existing.periodEnd) > /* @__PURE__ */ new Date()) newStatus = "active";
457
+ const nextPaymentDate = data?.next_payment_date;
458
+ const periodEnd = nextPaymentDate ? new Date(nextPaymentDate) : existing?.periodEnd ? new Date(existing.periodEnd) : void 0;
459
+ if (periodEnd && periodEnd > /* @__PURE__ */ new Date()) newStatus = "active";
452
460
  await ctx.context.adapter.update({
453
461
  model: "subscription",
454
462
  update: {
455
463
  status: newStatus,
464
+ cancelAtPeriodEnd: true,
465
+ ...periodEnd ? { periodEnd } : {},
456
466
  updatedAt: /* @__PURE__ */ new Date()
457
467
  },
458
468
  where: [{
@@ -530,13 +540,39 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
530
540
  if (planName !== void 0 && planName !== null && planName !== "") {
531
541
  if (subscriptionOptions?.enabled !== true) throw new APIError("BAD_REQUEST", { message: "Subscriptions are not enabled." });
532
542
  plan = await getPlanByName(options, planName) ?? void 0;
543
+ if (!plan) {
544
+ const nativePlan = await ctx.context.adapter.findOne({
545
+ model: "paystackPlan",
546
+ where: [{
547
+ field: "name",
548
+ value: planName
549
+ }]
550
+ });
551
+ if (nativePlan) plan = nativePlan;
552
+ else plan = await ctx.context.adapter.findOne({
553
+ model: "paystackPlan",
554
+ where: [{
555
+ field: "planCode",
556
+ value: planName
557
+ }]
558
+ }) ?? void 0;
559
+ }
533
560
  if (!plan) throw new APIError("BAD_REQUEST", {
534
561
  code: "SUBSCRIPTION_PLAN_NOT_FOUND",
535
562
  message: PAYSTACK_ERROR_CODES.SUBSCRIPTION_PLAN_NOT_FOUND,
536
563
  status: 400
537
564
  });
538
565
  } else if (productName !== void 0 && productName !== null && productName !== "") {
539
- if (typeof productName === "string") product = await getProductByName(options, productName) ?? void 0;
566
+ if (typeof productName === "string") {
567
+ product ??= await getProductByName(options, productName) ?? void 0;
568
+ product ??= await ctx.context.adapter.findOne({
569
+ model: "paystackProduct",
570
+ where: [{
571
+ field: "name",
572
+ value: productName
573
+ }]
574
+ }) ?? void 0;
575
+ }
540
576
  if (!product) throw new APIError("BAD_REQUEST", {
541
577
  message: `Product '${productName}' not found.`,
542
578
  status: 400
@@ -846,7 +882,17 @@ const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
846
882
  if (planCodeFromPaystack === void 0 || planCodeFromPaystack === null || planCodeFromPaystack === "") paystackSubscriptionCode = `LOC_${reference}`;
847
883
  else paystackSubscriptionCode = (data?.subscription)?.subscription_code;
848
884
  }
849
- const updatedSubscription = await ctx.context.adapter.update({
885
+ const existingSubs = await ctx.context.adapter.findMany({
886
+ model: "subscription",
887
+ where: [{
888
+ field: "paystackTransactionReference",
889
+ value: reference
890
+ }]
891
+ });
892
+ let targetSub;
893
+ if (existingSubs && existingSubs.length > 0) targetSub = existingSubs.find((s) => !(referenceId !== void 0 && referenceId !== null && referenceId !== "") || s.referenceId === referenceId);
894
+ let updatedSubscription = null;
895
+ if (targetSub !== void 0 && targetSub !== null) updatedSubscription = await ctx.context.adapter.update({
850
896
  model: "subscription",
851
897
  update: {
852
898
  status: isTrial === true ? "trialing" : "active",
@@ -861,12 +907,9 @@ const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
861
907
  ...authorizationCode !== void 0 && authorizationCode !== null && authorizationCode !== "" ? { paystackAuthorizationCode: authorizationCode } : {}
862
908
  },
863
909
  where: [{
864
- field: "paystackTransactionReference",
865
- value: reference
866
- }, ...referenceId !== void 0 && referenceId !== null && referenceId !== "" ? [{
867
- field: "referenceId",
868
- value: referenceId
869
- }] : []]
910
+ field: "id",
911
+ value: targetSub.id
912
+ }]
870
913
  });
871
914
  if (updatedSubscription && subscriptionOptions?.enabled === true && "onSubscriptionComplete" in subscriptionOptions && typeof subscriptionOptions.onSubscriptionComplete === "function") {
872
915
  const plan = (await getPlans(subscriptionOptions)).find((p) => p.name.toLowerCase() === updatedSubscription.plan.toLowerCase());
@@ -1115,28 +1158,35 @@ const enablePaystackSubscription = (options, path = "/paystack/enable-subscripti
1115
1158
  }
1116
1159
  });
1117
1160
  };
1118
- const getSubscriptionManageLink = (options) => {
1119
- return createAuthEndpoint("/paystack/get-subscription-manage-link", {
1120
- method: "GET",
1121
- query: z.object({ subscriptionCode: z.string() }),
1122
- use: options.subscription?.enabled === true ? [
1123
- sessionMiddleware,
1124
- originCheck,
1125
- referenceMiddleware(options, "get-subscription-manage-link")
1126
- ] : [sessionMiddleware, originCheck]
1127
- }, async (ctx) => {
1161
+ const getSubscriptionManageLink = (options, path = "/paystack/get-subscription-manage-link") => {
1162
+ const manageLinkQuerySchema = z.object({ subscriptionCode: z.string() });
1163
+ const useMiddlewares = options.subscription?.enabled === true ? [
1164
+ sessionMiddleware,
1165
+ originCheck,
1166
+ referenceMiddleware(options, "get-subscription-manage-link")
1167
+ ] : [sessionMiddleware, originCheck];
1168
+ const handler = async (ctx) => {
1128
1169
  const { subscriptionCode } = ctx.query;
1170
+ if (subscriptionCode.startsWith("LOC_") || subscriptionCode.startsWith("sub_local_")) return ctx.json({
1171
+ link: null,
1172
+ message: "Local subscriptions cannot be managed on Paystack"
1173
+ });
1129
1174
  const paystack = getPaystackOps(options.paystackClient);
1130
1175
  try {
1131
1176
  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;
1177
+ 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
1178
  const link = typeof data === "string" ? data : data?.link;
1134
1179
  return ctx.json({ link });
1135
1180
  } catch (error) {
1136
1181
  ctx.context.logger.error("Failed to get subscription manage link", error);
1137
1182
  throw new APIError("BAD_REQUEST", { message: error?.message ?? "Failed to get subscription manage link" });
1138
1183
  }
1139
- });
1184
+ };
1185
+ return createAuthEndpoint(path, {
1186
+ method: "GET",
1187
+ query: manageLinkQuerySchema,
1188
+ use: useMiddlewares
1189
+ }, handler);
1140
1190
  };
1141
1191
  const syncProducts = (options) => {
1142
1192
  return createAuthEndpoint("/paystack/sync-products", {
@@ -1201,6 +1251,91 @@ const syncProducts = (options) => {
1201
1251
  }
1202
1252
  });
1203
1253
  };
1254
+ const listProducts = (_options) => {
1255
+ return createAuthEndpoint("/paystack/list-products", {
1256
+ method: "GET",
1257
+ metadata: { openapi: { operationId: "listPaystackProducts" } }
1258
+ }, async (ctx) => {
1259
+ const sorted = (await ctx.context.adapter.findMany({ model: "paystackProduct" })).sort((a, b) => a.name.localeCompare(b.name));
1260
+ return ctx.json({ products: sorted });
1261
+ });
1262
+ };
1263
+ const syncPlans = (options) => {
1264
+ return createAuthEndpoint("/paystack/sync-plans", {
1265
+ method: "POST",
1266
+ metadata: { ...HIDE_METADATA },
1267
+ disableBody: true,
1268
+ use: [sessionMiddleware]
1269
+ }, async (ctx) => {
1270
+ const paystack = getPaystackOps(options.paystackClient);
1271
+ try {
1272
+ const res = unwrapSdkResult(await paystack.planList());
1273
+ const plansData = res !== null && typeof res === "object" && "status" in res && "data" in res ? res.data : res?.data ?? res;
1274
+ if (!Array.isArray(plansData)) return ctx.json({
1275
+ status: "success",
1276
+ count: 0
1277
+ });
1278
+ for (const plan of plansData) {
1279
+ const paystackId = String(plan.id);
1280
+ const existing = await ctx.context.adapter.findOne({
1281
+ model: "paystackPlan",
1282
+ where: [{
1283
+ field: "paystackId",
1284
+ value: paystackId
1285
+ }]
1286
+ });
1287
+ const planData = {
1288
+ name: plan.name,
1289
+ description: plan.description,
1290
+ amount: plan.amount,
1291
+ currency: plan.currency,
1292
+ interval: plan.interval,
1293
+ planCode: plan.plan_code,
1294
+ paystackId,
1295
+ metadata: plan.metadata ? JSON.stringify(plan.metadata) : void 0,
1296
+ updatedAt: /* @__PURE__ */ new Date()
1297
+ };
1298
+ if (existing) await ctx.context.adapter.update({
1299
+ model: "paystackPlan",
1300
+ update: planData,
1301
+ where: [{
1302
+ field: "id",
1303
+ value: existing.id
1304
+ }]
1305
+ });
1306
+ else await ctx.context.adapter.create({
1307
+ model: "paystackPlan",
1308
+ data: {
1309
+ ...planData,
1310
+ createdAt: /* @__PURE__ */ new Date()
1311
+ }
1312
+ });
1313
+ }
1314
+ return ctx.json({
1315
+ status: "success",
1316
+ count: plansData.length
1317
+ });
1318
+ } catch (error) {
1319
+ ctx.context.logger.error("Failed to sync plans", error);
1320
+ throw new APIError("BAD_REQUEST", { message: error?.message ?? "Failed to sync plans" });
1321
+ }
1322
+ });
1323
+ };
1324
+ const listPlans = (_options) => {
1325
+ return createAuthEndpoint("/paystack/list-plans", {
1326
+ method: "GET",
1327
+ metadata: { ...HIDE_METADATA },
1328
+ use: [sessionMiddleware]
1329
+ }, async (ctx) => {
1330
+ try {
1331
+ const plans = await ctx.context.adapter.findMany({ model: "paystackPlan" });
1332
+ return ctx.json({ plans });
1333
+ } catch (error) {
1334
+ ctx.context.logger.error("Failed to list plans", error);
1335
+ throw new APIError("BAD_REQUEST", { message: error?.message ?? "Failed to list plans" });
1336
+ }
1337
+ });
1338
+ };
1204
1339
  const getConfig = (options) => {
1205
1340
  return createAuthEndpoint("/paystack/get-config", {
1206
1341
  method: "GET",
@@ -1501,18 +1636,64 @@ const products = { paystackProduct: { fields: {
1501
1636
  required: true
1502
1637
  }
1503
1638
  } } };
1639
+ const plans = { paystackPlan: { fields: {
1640
+ name: {
1641
+ type: "string",
1642
+ required: true
1643
+ },
1644
+ description: {
1645
+ type: "string",
1646
+ required: false
1647
+ },
1648
+ amount: {
1649
+ type: "number",
1650
+ required: true
1651
+ },
1652
+ currency: {
1653
+ type: "string",
1654
+ required: true
1655
+ },
1656
+ interval: {
1657
+ type: "string",
1658
+ required: true
1659
+ },
1660
+ planCode: {
1661
+ type: "string",
1662
+ required: true,
1663
+ unique: true
1664
+ },
1665
+ paystackId: {
1666
+ type: "string",
1667
+ required: true,
1668
+ unique: true
1669
+ },
1670
+ metadata: {
1671
+ type: "string",
1672
+ required: false
1673
+ },
1674
+ createdAt: {
1675
+ type: "date",
1676
+ required: true
1677
+ },
1678
+ updatedAt: {
1679
+ type: "date",
1680
+ required: true
1681
+ }
1682
+ } } };
1504
1683
  const getSchema = (options) => {
1505
1684
  let baseSchema;
1506
1685
  if (options.subscription?.enabled === true) baseSchema = {
1507
1686
  ...subscriptions,
1508
1687
  ...transactions,
1509
1688
  ...user,
1510
- ...products
1689
+ ...products,
1690
+ ...plans
1511
1691
  };
1512
1692
  else baseSchema = {
1513
1693
  ...user,
1514
1694
  ...transactions,
1515
- ...products
1695
+ ...products,
1696
+ ...plans
1516
1697
  };
1517
1698
  if (options.organization?.enabled === true) baseSchema = {
1518
1699
  ...baseSchema,
@@ -1564,24 +1745,29 @@ const checkTeamLimit = async (ctx, organizationId, maxTeams) => {
1564
1745
  //#region src/index.ts
1565
1746
  const INTERNAL_ERROR_CODES = defineErrorCodes({ ...PAYSTACK_ERROR_CODES });
1566
1747
  const paystack = (options) => {
1748
+ const routeOptions = options;
1567
1749
  return {
1568
1750
  id: "paystack",
1569
1751
  endpoints: {
1570
- initializeTransaction: initializeTransaction(options),
1571
- verifyTransaction: verifyTransaction(options),
1572
- listSubscriptions: listSubscriptions(options),
1573
- paystackWebhook: paystackWebhook(options),
1574
- listTransactions: listTransactions(options),
1575
- getConfig: getConfig(options),
1576
- disableSubscription: disablePaystackSubscription(options),
1577
- enableSubscription: enablePaystackSubscription(options),
1578
- getSubscriptionManageLink: getSubscriptionManageLink(options),
1579
- createSubscription: createSubscription(options),
1580
- upgradeSubscription: upgradeSubscription(options),
1581
- cancelSubscription: cancelSubscription(options),
1582
- restoreSubscription: restoreSubscription(options),
1583
- chargeRecurringSubscription: chargeRecurringSubscription(options),
1584
- syncProducts: syncProducts(options)
1752
+ initializeTransaction: initializeTransaction(routeOptions),
1753
+ verifyTransaction: verifyTransaction(routeOptions),
1754
+ listSubscriptions: listSubscriptions(routeOptions),
1755
+ paystackWebhook: paystackWebhook(routeOptions),
1756
+ listTransactions: listTransactions(routeOptions),
1757
+ getConfig: getConfig(routeOptions),
1758
+ disableSubscription: disablePaystackSubscription(routeOptions),
1759
+ enableSubscription: enablePaystackSubscription(routeOptions),
1760
+ getSubscriptionManageLink: getSubscriptionManageLink(routeOptions),
1761
+ subscriptionManageLink: getSubscriptionManageLink(routeOptions, "/paystack/subscription/manage-link"),
1762
+ createSubscription: createSubscription(routeOptions),
1763
+ upgradeSubscription: upgradeSubscription(routeOptions),
1764
+ cancelSubscription: cancelSubscription(routeOptions),
1765
+ restoreSubscription: restoreSubscription(routeOptions),
1766
+ chargeRecurringSubscription: chargeRecurringSubscription(routeOptions),
1767
+ syncProducts: syncProducts(routeOptions),
1768
+ listProducts: listProducts(routeOptions),
1769
+ syncPlans: syncPlans(routeOptions),
1770
+ listPlans: listPlans(routeOptions)
1585
1771
  },
1586
1772
  schema: getSchema(options),
1587
1773
  init: (ctx) => {