@alexasomba/better-auth-paystack 1.0.4 → 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
@@ -6,6 +6,108 @@ import { APIError, getSessionFromCtx, originCheck, sessionMiddleware } from "bet
6
6
  import * as z from "zod/v4";
7
7
  import { mergeSchema } from "better-auth/db";
8
8
 
9
+ //#region src/paystack-sdk.ts
10
+ function isOpenApiFetchResponse(value) {
11
+ return value !== null && value !== void 0 && typeof value === "object" && ("data" in value || "error" in value || "response" in value);
12
+ }
13
+ function unwrapSdkResult(result) {
14
+ if (isOpenApiFetchResponse(result)) {
15
+ if (result.error !== void 0 && result.error !== null) throw new Error(typeof result.error === "string" ? result.error : JSON.stringify(result.error));
16
+ return result.data ?? result;
17
+ }
18
+ if (result !== null && result !== void 0 && typeof result === "object" && "data" in result) {
19
+ const data = result.data;
20
+ if (data !== null && typeof data === "object" && "data" in data) return data.data;
21
+ return data;
22
+ }
23
+ return result;
24
+ }
25
+ function getPaystackOps(paystackClient) {
26
+ return {
27
+ customerCreate: (params) => {
28
+ if (paystackClient?.customer_create !== void 0) return paystackClient.customer_create({ body: params });
29
+ return paystackClient?.customer?.create?.(params);
30
+ },
31
+ customerUpdate: (code, params) => {
32
+ if (paystackClient?.customer_update !== void 0) return paystackClient.customer_update({
33
+ params: { path: { code } },
34
+ body: params
35
+ });
36
+ return paystackClient?.customer?.update?.(code, params);
37
+ },
38
+ transactionInitialize: (body) => {
39
+ if (paystackClient?.transaction_initialize !== void 0) return paystackClient.transaction_initialize({ body });
40
+ return paystackClient?.transaction?.initialize?.(body);
41
+ },
42
+ transactionVerify: (reference) => {
43
+ if (paystackClient?.transaction_verify !== void 0) return paystackClient.transaction_verify({ params: { path: { reference } } });
44
+ return paystackClient?.transaction?.verify?.(reference);
45
+ },
46
+ subscriptionCreate: (body) => {
47
+ if (paystackClient?.subscription_create !== void 0) return paystackClient.subscription_create({ body });
48
+ return paystackClient?.subscription?.create?.(body);
49
+ },
50
+ subscriptionDisable: (body) => {
51
+ if (paystackClient?.subscription_disable !== void 0) return paystackClient.subscription_disable({ body });
52
+ return paystackClient?.subscription?.disable?.(body);
53
+ },
54
+ subscriptionEnable: (body) => {
55
+ if (paystackClient?.subscription_enable !== void 0) return paystackClient.subscription_enable({ body });
56
+ return paystackClient?.subscription?.enable?.(body);
57
+ },
58
+ subscriptionFetch: async (idOrCode) => {
59
+ if (paystackClient?.subscription_fetch !== void 0) try {
60
+ return await paystackClient.subscription_fetch({ params: { path: { code: idOrCode } } });
61
+ } catch {
62
+ const compatFetch = paystackClient.subscription_fetch;
63
+ return compatFetch({ params: { path: { id_or_code: idOrCode } } });
64
+ }
65
+ return paystackClient?.subscription?.fetch?.(idOrCode);
66
+ },
67
+ subscriptionManageLink: (code) => {
68
+ if (paystackClient?.subscription_manageLink !== void 0) return paystackClient.subscription_manageLink({ params: { path: { code } } });
69
+ if (paystackClient?.subscription_manage_link !== void 0) return paystackClient.subscription_manage_link({ params: { path: { code } } });
70
+ return paystackClient?.subscription?.manage?.link?.(code);
71
+ },
72
+ subscriptionManageEmail: (code, email) => {
73
+ if (paystackClient?.subscription_manageEmail !== void 0) return paystackClient.subscription_manageEmail({ params: { path: { code } } });
74
+ return paystackClient?.subscription?.manage?.email?.(code, email);
75
+ },
76
+ transactionChargeAuthorization: (body) => {
77
+ if (paystackClient?.transaction_chargeAuthorization !== void 0) return paystackClient.transaction_chargeAuthorization({ body });
78
+ return paystackClient?.transaction?.chargeAuthorization?.(body);
79
+ },
80
+ productList: () => {
81
+ if (paystackClient?.product_list !== void 0) return paystackClient.product_list();
82
+ return paystackClient?.product?.list?.();
83
+ },
84
+ productFetch: (idOrCode) => {
85
+ if (paystackClient?.product_fetch !== void 0) return paystackClient.product_fetch({ params: { path: { id_or_code: idOrCode } } });
86
+ return paystackClient?.product?.fetch?.(idOrCode);
87
+ },
88
+ productCreate: (params) => {
89
+ if (paystackClient?.product_create !== void 0) return paystackClient.product_create({ body: params });
90
+ return paystackClient?.product?.create?.(params);
91
+ },
92
+ productUpdate: (idOrCode, params) => {
93
+ if (paystackClient?.product_update !== void 0) return paystackClient.product_update({
94
+ params: { path: { id_or_code: idOrCode } },
95
+ body: params
96
+ });
97
+ return paystackClient?.product?.update?.(idOrCode, params);
98
+ },
99
+ productDelete: (idOrCode) => {
100
+ if (paystackClient?.product_delete !== void 0) return paystackClient.product_delete({ params: { path: { id_or_code: idOrCode } } });
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?.();
106
+ }
107
+ };
108
+ }
109
+
110
+ //#endregion
9
111
  //#region src/utils.ts
10
112
  async function getPlans(subscriptionOptions) {
11
113
  if (subscriptionOptions?.enabled === true) return typeof subscriptionOptions.plans === "function" ? subscriptionOptions.plans() : subscriptionOptions.plans;
@@ -20,7 +122,7 @@ async function getProducts(productOptions) {
20
122
  return [];
21
123
  }
22
124
  async function getProductByName(options, name) {
23
- return await getProducts(options.products).then((products) => products?.find((product) => product.name.toLowerCase() === name.toLowerCase()));
125
+ return await getProducts(options.products).then((products) => products?.find((product) => product.name.toLowerCase() === name.toLowerCase()) ?? null);
24
126
  }
25
127
  function getNextPeriodEnd(startDate, interval) {
26
128
  const date = new Date(startDate);
@@ -62,6 +164,62 @@ function validateMinAmount(amount, currency) {
62
164
  }[currency.toUpperCase()];
63
165
  return min !== void 0 ? amount >= min : true;
64
166
  }
167
+ async function syncProductQuantityFromPaystack(ctx, productName, paystackClient) {
168
+ let localProduct = await ctx.context.adapter.findOne({
169
+ model: "paystackProduct",
170
+ where: [{
171
+ field: "name",
172
+ value: productName
173
+ }]
174
+ });
175
+ localProduct ??= await ctx.context.adapter.findOne({
176
+ model: "paystackProduct",
177
+ where: [{
178
+ field: "slug",
179
+ value: productName.toLowerCase().replace(/\s+/g, "-")
180
+ }]
181
+ });
182
+ if (localProduct?.paystackId === void 0 || localProduct?.paystackId === null || localProduct?.paystackId === "") {
183
+ if (localProduct && localProduct.unlimited !== true && localProduct.quantity !== void 0 && localProduct.quantity > 0) await ctx.context.adapter.update({
184
+ model: "paystackProduct",
185
+ update: {
186
+ quantity: localProduct.quantity - 1,
187
+ updatedAt: /* @__PURE__ */ new Date()
188
+ },
189
+ where: [{
190
+ field: "id",
191
+ value: localProduct.id
192
+ }]
193
+ });
194
+ return;
195
+ }
196
+ try {
197
+ const remoteQuantity = unwrapSdkResult(await getPaystackOps(paystackClient).productFetch(localProduct.paystackId))?.quantity;
198
+ if (remoteQuantity !== void 0) await ctx.context.adapter.update({
199
+ model: "paystackProduct",
200
+ update: {
201
+ quantity: remoteQuantity,
202
+ updatedAt: /* @__PURE__ */ new Date()
203
+ },
204
+ where: [{
205
+ field: "id",
206
+ value: localProduct.id
207
+ }]
208
+ });
209
+ } catch {
210
+ if (localProduct.unlimited !== true && localProduct.quantity !== void 0 && localProduct.quantity > 0) await ctx.context.adapter.update({
211
+ model: "paystackProduct",
212
+ update: {
213
+ quantity: localProduct.quantity - 1,
214
+ updatedAt: /* @__PURE__ */ new Date()
215
+ },
216
+ where: [{
217
+ field: "id",
218
+ value: localProduct.id
219
+ }]
220
+ });
221
+ }
222
+ }
65
223
 
66
224
  //#endregion
67
225
  //#region src/middleware.ts
@@ -102,96 +260,6 @@ const referenceMiddleware = (options, action) => createAuthMiddleware(async (ctx
102
260
  throw new APIError("BAD_REQUEST", { message: "Passing referenceId isn't allowed without subscription.authorizeReference or valid organization membership." });
103
261
  });
104
262
 
105
- //#endregion
106
- //#region src/paystack-sdk.ts
107
- function isOpenApiFetchResponse(value) {
108
- return value !== null && value !== void 0 && typeof value === "object" && ("data" in value || "error" in value || "response" in value);
109
- }
110
- function unwrapSdkResult(result) {
111
- if (isOpenApiFetchResponse(result)) {
112
- if (result.error !== void 0 && result.error !== null) throw new Error(typeof result.error === "string" ? result.error : JSON.stringify(result.error));
113
- return result.data;
114
- }
115
- if (result !== null && result !== void 0 && typeof result === "object" && "data" in result) return result.data ?? result;
116
- return result;
117
- }
118
- const normalizeMetadata = (value) => {
119
- if (value === void 0 || value === null || value === "") return void 0;
120
- return typeof value === "string" ? value : JSON.stringify(value);
121
- };
122
- const normalizeMetadataBody = (body) => {
123
- const { metadata, ...rest } = body;
124
- const normalized = normalizeMetadata(metadata);
125
- if (normalized === void 0) return rest;
126
- return {
127
- ...rest,
128
- metadata: normalized
129
- };
130
- };
131
- function getPaystackOps(paystackClient) {
132
- return {
133
- customerCreate: (params) => {
134
- if (paystackClient?.customer_create !== void 0) {
135
- const body = normalizeMetadataBody(params);
136
- return paystackClient.customer_create({ body });
137
- }
138
- return paystackClient?.customer?.create?.(params);
139
- },
140
- customerUpdate: (code, params) => {
141
- if (paystackClient?.customer_update !== void 0) {
142
- const body = normalizeMetadataBody(params);
143
- return paystackClient.customer_update({
144
- params: { path: { code } },
145
- body
146
- });
147
- }
148
- return paystackClient?.customer?.update?.(code, params);
149
- },
150
- transactionInitialize: (body) => {
151
- if (paystackClient?.transaction_initialize !== void 0) return paystackClient.transaction_initialize({ body });
152
- return paystackClient?.transaction?.initialize?.(body);
153
- },
154
- transactionVerify: (reference) => {
155
- if (paystackClient?.transaction_verify !== void 0) return paystackClient.transaction_verify({ params: { path: { reference } } });
156
- return paystackClient?.transaction?.verify?.(reference);
157
- },
158
- subscriptionCreate: (body) => {
159
- if (paystackClient?.subscription_create !== void 0) return paystackClient.subscription_create({ body });
160
- return paystackClient?.subscription?.create?.(body);
161
- },
162
- subscriptionDisable: (body) => {
163
- if (paystackClient?.subscription_disable !== void 0) return paystackClient.subscription_disable({ body });
164
- return paystackClient?.subscription?.disable?.(body);
165
- },
166
- subscriptionEnable: (body) => {
167
- if (paystackClient?.subscription_enable !== void 0) return paystackClient.subscription_enable({ body });
168
- return paystackClient?.subscription?.enable?.(body);
169
- },
170
- subscriptionFetch: async (idOrCode) => {
171
- if (paystackClient?.subscription_fetch !== void 0) try {
172
- return await paystackClient.subscription_fetch({ params: { path: { code: idOrCode } } });
173
- } catch {
174
- const compatFetch = paystackClient.subscription_fetch;
175
- return compatFetch({ params: { path: { id_or_code: idOrCode } } });
176
- }
177
- return paystackClient?.subscription?.fetch?.(idOrCode);
178
- },
179
- subscriptionManageLink: (code) => {
180
- if (paystackClient?.subscription_manageLink !== void 0) return paystackClient.subscription_manageLink({ params: { path: { code } } });
181
- if (paystackClient?.subscription_manage_link !== void 0) return paystackClient.subscription_manage_link({ params: { path: { code } } });
182
- return paystackClient?.subscription?.manage?.link?.(code);
183
- },
184
- subscriptionManageEmail: (code, email) => {
185
- if (paystackClient?.subscription_manageEmail !== void 0) return paystackClient.subscription_manageEmail({ params: { path: { code } } });
186
- return paystackClient?.subscription?.manage?.email?.(code, email);
187
- },
188
- transactionChargeAuthorization: (body) => {
189
- if (paystackClient?.transaction_chargeAuthorization !== void 0) return paystackClient.transaction_chargeAuthorization({ body });
190
- return paystackClient?.transaction?.chargeAuthorization?.(body);
191
- }
192
- };
193
- }
194
-
195
263
  //#endregion
196
264
  //#region src/routes.ts
197
265
  const PAYSTACK_ERROR_CODES = defineErrorCodes({
@@ -244,14 +312,14 @@ const paystackWebhook = (options) => {
244
312
  status: 401
245
313
  });
246
314
  const event = JSON.parse(payload);
247
- if (options.subscription?.enabled === true) {
248
- const eventName = String(event?.event ?? "");
249
- const data = event?.data;
250
- try {
251
- if (eventName === "charge.success") {
252
- const reference = data?.reference;
253
- const paystackId = data?.id !== void 0 && data?.id !== null ? String(data.id) : void 0;
254
- if (reference !== void 0 && reference !== null && reference !== "") await ctx.context.adapter.update({
315
+ const eventName = event.event;
316
+ const data = event.data;
317
+ if (eventName === "charge.success") {
318
+ const reference = data?.reference;
319
+ const paystackId = data?.id !== void 0 && data?.id !== null ? String(data.id) : void 0;
320
+ if (reference !== void 0 && reference !== null && reference !== "") {
321
+ try {
322
+ await ctx.context.adapter.update({
255
323
  model: "paystackTransaction",
256
324
  update: {
257
325
  status: "success",
@@ -263,134 +331,156 @@ const paystackWebhook = (options) => {
263
331
  value: reference
264
332
  }]
265
333
  });
334
+ } catch (e) {
335
+ ctx.context.logger.warn("Failed to update transaction status for charge.success", e);
266
336
  }
267
- if (eventName === "charge.failure") {
268
- const reference = data?.reference;
269
- if (reference !== void 0 && reference !== null && reference !== "") try {
270
- await ctx.context.adapter.update({
271
- model: "paystackTransaction",
272
- update: {
273
- status: "failed",
274
- updatedAt: /* @__PURE__ */ new Date()
275
- },
276
- where: [{
277
- field: "reference",
278
- value: reference
279
- }]
280
- });
281
- } catch (e) {
282
- ctx.context.logger.warn("Failed to update transaction status for charge.failure", e);
283
- }
337
+ try {
338
+ const transaction = await ctx.context.adapter.findOne({
339
+ model: "paystackTransaction",
340
+ where: [{
341
+ field: "reference",
342
+ value: reference
343
+ }]
344
+ });
345
+ if (transaction?.product) await syncProductQuantityFromPaystack(ctx, transaction.product, options.paystackClient);
346
+ } catch (e) {
347
+ ctx.context.logger.warn("Failed to sync product quantity", e);
284
348
  }
285
- if (eventName === "subscription.create") {
286
- const subscriptionCode = data?.subscription_code ?? data?.subscription?.subscription_code ?? data?.code;
287
- const customerCode = data?.customer?.customer_code ?? data?.customer_code ?? data?.customer?.code;
288
- const planCode = data?.plan?.plan_code ?? data?.plan_code ?? data?.plan;
289
- let metadata = data?.metadata;
290
- if (typeof metadata === "string") try {
291
- metadata = JSON.parse(metadata);
292
- } catch {}
293
- const referenceIdFromMetadata = typeof metadata === "object" && metadata !== null ? metadata.referenceId : void 0;
294
- let planNameFromMetadata = typeof metadata === "object" && metadata !== null ? metadata.plan : void 0;
295
- if (typeof planNameFromMetadata === "string") planNameFromMetadata = planNameFromMetadata.toLowerCase();
296
- const plans = await getPlans(options.subscription);
297
- const planFromCode = planCode !== void 0 && planCode !== null && planCode !== "" ? plans.find((p) => p.planCode !== void 0 && p.planCode !== null && p.planCode === planCode) : void 0;
298
- const planPart = planFromCode?.name ?? planNameFromMetadata;
299
- const planName = planPart !== void 0 && planPart !== null && planPart !== "" ? planPart.toLowerCase() : void 0;
300
- if (subscriptionCode !== void 0 && subscriptionCode !== null && subscriptionCode !== "") {
301
- const where = [];
302
- if (referenceIdFromMetadata !== void 0 && referenceIdFromMetadata !== null && referenceIdFromMetadata !== "") where.push({
303
- field: "referenceId",
304
- value: referenceIdFromMetadata
305
- });
306
- else if (customerCode !== void 0 && customerCode !== null && customerCode !== "") where.push({
307
- field: "paystackCustomerCode",
308
- value: customerCode
309
- });
310
- if (planName !== void 0 && planName !== null && planName !== "") where.push({
311
- field: "plan",
312
- value: planName
349
+ }
350
+ }
351
+ if (eventName === "charge.failure") {
352
+ const reference = data?.reference;
353
+ if (reference !== void 0 && reference !== null && reference !== "") try {
354
+ await ctx.context.adapter.update({
355
+ model: "paystackTransaction",
356
+ update: {
357
+ status: "failed",
358
+ updatedAt: /* @__PURE__ */ new Date()
359
+ },
360
+ where: [{
361
+ field: "reference",
362
+ value: reference
363
+ }]
364
+ });
365
+ } catch (e) {
366
+ ctx.context.logger.warn("Failed to update transaction status for charge.failure", e);
367
+ }
368
+ }
369
+ if (options.subscription?.enabled === true) try {
370
+ if (eventName === "subscription.create") {
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;
376
+ if (typeof metadata === "string") try {
377
+ metadata = JSON.parse(metadata);
378
+ } catch {}
379
+ const referenceIdFromMetadata = typeof metadata === "object" && metadata !== null ? metadata.referenceId : void 0;
380
+ let planNameFromMetadata = typeof metadata === "object" && metadata !== null ? metadata.plan : void 0;
381
+ if (typeof planNameFromMetadata === "string") planNameFromMetadata = planNameFromMetadata.toLowerCase();
382
+ const plans = await getPlans(options.subscription);
383
+ const planFromCode = planCode !== void 0 && planCode !== null && planCode !== "" ? plans.find((p) => p.planCode !== void 0 && p.planCode !== null && p.planCode === planCode) : void 0;
384
+ const planPart = planFromCode?.name ?? planNameFromMetadata;
385
+ const planName = planPart !== void 0 && planPart !== null && planPart !== "" ? planPart.toLowerCase() : void 0;
386
+ if (subscriptionCode !== void 0 && subscriptionCode !== null && subscriptionCode !== "") {
387
+ const where = [];
388
+ if (referenceIdFromMetadata !== void 0 && referenceIdFromMetadata !== null && referenceIdFromMetadata !== "") where.push({
389
+ field: "referenceId",
390
+ value: referenceIdFromMetadata
391
+ });
392
+ else if (customerCode !== void 0 && customerCode !== null && customerCode !== "") where.push({
393
+ field: "paystackCustomerCode",
394
+ value: customerCode
395
+ });
396
+ if (planName !== void 0 && planName !== null && planName !== "") where.push({
397
+ field: "plan",
398
+ value: planName
399
+ });
400
+ if (where.length > 0) {
401
+ const matches = await ctx.context.adapter.findMany({
402
+ model: "subscription",
403
+ where
313
404
  });
314
- if (where.length > 0) {
315
- const matches = await ctx.context.adapter.findMany({
405
+ const subscription = matches !== void 0 && matches !== null ? matches[0] : void 0;
406
+ if (subscription !== void 0 && subscription !== null) {
407
+ await ctx.context.adapter.update({
316
408
  model: "subscription",
317
- where
409
+ update: {
410
+ paystackSubscriptionCode: subscriptionCode,
411
+ status: "active",
412
+ updatedAt: /* @__PURE__ */ new Date(),
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
414
+ },
415
+ where: [{
416
+ field: "id",
417
+ value: subscription.id
418
+ }]
318
419
  });
319
- const subscription = matches !== void 0 && matches !== null ? matches[0] : void 0;
320
- if (subscription !== void 0 && subscription !== null) {
321
- await ctx.context.adapter.update({
322
- model: "subscription",
323
- update: {
420
+ const plan = planFromCode ?? (planName !== void 0 && planName !== null && planName !== "" ? await getPlanByName(options, planName) : void 0);
421
+ if (plan !== void 0 && plan !== null) {
422
+ await options.subscription.onSubscriptionComplete?.({
423
+ event,
424
+ subscription: {
425
+ ...subscription,
324
426
  paystackSubscriptionCode: subscriptionCode,
325
- status: "active",
326
- updatedAt: /* @__PURE__ */ new Date(),
327
- periodEnd: data?.next_payment_date !== void 0 && data?.next_payment_date !== null && data?.next_payment_date !== "" ? new Date(data.next_payment_date) : void 0
427
+ status: "active"
328
428
  },
329
- where: [{
330
- field: "id",
331
- value: subscription.id
332
- }]
333
- });
334
- const plan = planFromCode ?? (planName !== void 0 && planName !== null && planName !== "" ? await getPlanByName(options, planName) : void 0);
335
- if (plan !== void 0 && plan !== null) {
336
- await options.subscription.onSubscriptionComplete?.({
337
- event,
338
- subscription: {
339
- ...subscription,
340
- paystackSubscriptionCode: subscriptionCode,
341
- status: "active"
342
- },
343
- plan
344
- }, ctx);
345
- await options.subscription.onSubscriptionCreated?.({
346
- event,
347
- subscription: {
348
- ...subscription,
349
- paystackSubscriptionCode: subscriptionCode,
350
- status: "active"
351
- },
352
- plan
353
- }, ctx);
354
- }
429
+ plan
430
+ }, ctx);
431
+ await options.subscription.onSubscriptionCreated?.({
432
+ event,
433
+ subscription: {
434
+ ...subscription,
435
+ paystackSubscriptionCode: subscriptionCode,
436
+ status: "active"
437
+ },
438
+ plan
439
+ }, ctx);
355
440
  }
356
441
  }
357
442
  }
358
443
  }
359
- if (eventName === "subscription.disable" || eventName === "subscription.not_renew") {
360
- const subscriptionCode = data?.subscription_code ?? data?.subscription?.subscription_code ?? data?.code;
361
- if (subscriptionCode !== void 0 && subscriptionCode !== null && subscriptionCode !== "") {
362
- const existing = await ctx.context.adapter.findOne({
363
- model: "subscription",
364
- where: [{
365
- field: "paystackSubscriptionCode",
366
- value: subscriptionCode
367
- }]
368
- });
369
- let newStatus = "canceled";
370
- if (existing?.cancelAtPeriodEnd === true && existing.periodEnd !== void 0 && existing.periodEnd !== null && new Date(existing.periodEnd) > /* @__PURE__ */ new Date()) newStatus = "active";
371
- await ctx.context.adapter.update({
372
- model: "subscription",
373
- update: {
374
- status: newStatus,
375
- updatedAt: /* @__PURE__ */ new Date()
376
- },
377
- where: [{
378
- field: "paystackSubscriptionCode",
379
- value: subscriptionCode
380
- }]
381
- });
382
- if (existing) await options.subscription.onSubscriptionCancel?.({
383
- event,
384
- subscription: {
385
- ...existing,
386
- status: "canceled"
387
- }
388
- }, ctx);
389
- }
444
+ }
445
+ if (eventName === "subscription.disable" || eventName === "subscription.not_renew") {
446
+ const payloadData = data;
447
+ const subscriptionCode = payloadData?.subscription_code ?? payloadData?.subscription?.subscription_code ?? payloadData?.code;
448
+ if (subscriptionCode !== void 0 && subscriptionCode !== null && subscriptionCode !== "") {
449
+ const existing = await ctx.context.adapter.findOne({
450
+ model: "subscription",
451
+ where: [{
452
+ field: "paystackSubscriptionCode",
453
+ value: subscriptionCode
454
+ }]
455
+ });
456
+ let newStatus = "canceled";
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";
460
+ await ctx.context.adapter.update({
461
+ model: "subscription",
462
+ update: {
463
+ status: newStatus,
464
+ cancelAtPeriodEnd: true,
465
+ ...periodEnd ? { periodEnd } : {},
466
+ updatedAt: /* @__PURE__ */ new Date()
467
+ },
468
+ where: [{
469
+ field: "paystackSubscriptionCode",
470
+ value: subscriptionCode
471
+ }]
472
+ });
473
+ if (existing) await options.subscription.onSubscriptionCancel?.({
474
+ event,
475
+ subscription: {
476
+ ...existing,
477
+ status: "canceled"
478
+ }
479
+ }, ctx);
390
480
  }
391
- } catch (_e) {
392
- ctx.context.logger.error("Failed to sync Paystack webhook event", _e);
393
481
  }
482
+ } catch (_e) {
483
+ ctx.context.logger.error("Failed to sync Paystack webhook event", _e);
394
484
  }
395
485
  await options.onEvent?.(event);
396
486
  return ctx.json({ received: true });
@@ -449,14 +539,40 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
449
539
  let product;
450
540
  if (planName !== void 0 && planName !== null && planName !== "") {
451
541
  if (subscriptionOptions?.enabled !== true) throw new APIError("BAD_REQUEST", { message: "Subscriptions are not enabled." });
452
- plan = await getPlanByName(options, planName);
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
+ }
453
560
  if (!plan) throw new APIError("BAD_REQUEST", {
454
561
  code: "SUBSCRIPTION_PLAN_NOT_FOUND",
455
562
  message: PAYSTACK_ERROR_CODES.SUBSCRIPTION_PLAN_NOT_FOUND,
456
563
  status: 400
457
564
  });
458
565
  } else if (productName !== void 0 && productName !== null && productName !== "") {
459
- if (typeof productName === "string") product = await getProductByName(options, productName);
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
+ }
460
576
  if (!product) throw new APIError("BAD_REQUEST", {
461
577
  message: `Product '${productName}' not found.`,
462
578
  status: 400
@@ -465,7 +581,7 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
465
581
  message: "Either 'plan', 'product', or 'amount' is required to initialize a transaction.",
466
582
  status: 400
467
583
  });
468
- const amount = bodyAmount ?? product?.amount;
584
+ const amount = bodyAmount ?? product?.price;
469
585
  const finalCurrency = currency ?? product?.currency ?? plan?.currency ?? "NGN";
470
586
  let url;
471
587
  let reference;
@@ -580,6 +696,7 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
580
696
  currency: plan?.currency ?? currency ?? "NGN",
581
697
  status: "pending",
582
698
  plan: plan?.name.toLowerCase(),
699
+ product: product?.name.toLowerCase(),
583
700
  metadata: extraMetadata !== void 0 && extraMetadata !== null ? JSON.stringify(extraMetadata) : void 0,
584
701
  createdAt: /* @__PURE__ */ new Date(),
585
702
  updatedAt: /* @__PURE__ */ new Date()
@@ -651,13 +768,12 @@ const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
651
768
  message: error?.message ?? PAYSTACK_ERROR_CODES.FAILED_TO_VERIFY_TRANSACTION
652
769
  });
653
770
  }
654
- let data = verifyRes !== null && verifyRes !== void 0 && typeof verifyRes === "object" && "status" in verifyRes && "data" in verifyRes ? verifyRes.data : verifyRes?.data !== void 0 ? verifyRes.data : verifyRes;
655
- if (data !== null && data !== void 0 && typeof data === "object" && "status" in data && "data" in data) data = data.data;
771
+ const data = unwrapSdkResult(verifyRes);
656
772
  const status = data?.status;
657
773
  const reference = data?.reference ?? ctx.body.reference;
658
774
  const paystackId = data?.id !== void 0 && data?.id !== null ? String(data.id) : void 0;
659
775
  const authorizationCode = (data?.authorization)?.authorization_code;
660
- if (status === "success") try {
776
+ if (status === "success") {
661
777
  const session = await getSessionFromCtx(ctx);
662
778
  const referenceId = (await ctx.context.adapter.findOne({
663
779
  model: "paystackTransaction",
@@ -690,107 +806,123 @@ const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
690
806
  }
691
807
  if (!authorized) throw new APIError("UNAUTHORIZED");
692
808
  }
693
- await ctx.context.adapter.update({
694
- model: "paystackTransaction",
695
- update: {
696
- status: "success",
697
- paystackId,
698
- ...data?.amount !== void 0 && data?.amount !== null ? { amount: data.amount } : {},
699
- ...data?.currency !== void 0 && data?.currency !== null ? { currency: data.currency } : {},
700
- updatedAt: /* @__PURE__ */ new Date()
701
- },
702
- where: [{
703
- field: "reference",
704
- value: reference
705
- }]
706
- });
707
- const customer = data?.customer;
708
- const paystackCustomerCodeFromPaystack = customer !== void 0 && customer !== null && typeof customer === "object" ? customer.customer_code : void 0;
709
- if (paystackCustomerCodeFromPaystack !== void 0 && paystackCustomerCodeFromPaystack !== null && paystackCustomerCodeFromPaystack !== "" && referenceId !== void 0 && referenceId !== null && referenceId !== "") if ((options.organization?.enabled === true && (referenceId.startsWith("org_") || await ctx.context.adapter.findOne({
710
- model: "organization",
711
- where: [{
712
- field: "id",
713
- value: referenceId
714
- }]
715
- }) !== null)) === true) await ctx.context.adapter.update({
716
- model: "organization",
717
- update: { paystackCustomerCode: paystackCustomerCodeFromPaystack },
718
- where: [{
719
- field: "id",
720
- value: referenceId
721
- }]
722
- });
723
- else await ctx.context.adapter.update({
724
- model: "user",
725
- update: { paystackCustomerCode: paystackCustomerCodeFromPaystack },
726
- where: [{
727
- field: "id",
728
- value: referenceId
729
- }]
730
- });
731
- let isTrial = false;
732
- let trialEnd;
733
- let targetPlan;
734
- if (data?.metadata !== void 0 && data?.metadata !== null) {
735
- const metaRaw = data.metadata;
736
- const meta = typeof metaRaw === "string" ? JSON.parse(metaRaw) : metaRaw;
737
- isTrial = meta.isTrial === true || meta.isTrial === "true";
738
- trialEnd = meta.trialEnd;
739
- targetPlan = meta.plan;
740
- }
741
- let paystackSubscriptionCode;
742
- if (isTrial === true && targetPlan !== void 0 && targetPlan !== null && targetPlan !== "" && trialEnd !== void 0 && trialEnd !== null && trialEnd !== "") {
743
- const email = (data?.customer)?.email;
744
- const planConfig = (await getPlans(subscriptionOptions)).find((p) => p.name.toLowerCase() === targetPlan?.toLowerCase());
745
- if (planConfig !== void 0 && (planConfig.planCode === void 0 || planConfig.planCode === null || planConfig.planCode === "")) paystackSubscriptionCode = `LOC_${reference}`;
746
- if (authorizationCode !== void 0 && authorizationCode !== null && authorizationCode !== "" && email !== void 0 && email !== null && email !== "" && planConfig?.planCode !== void 0 && planConfig?.planCode !== null && planConfig?.planCode !== "") {
747
- const subData = unwrapSdkResult(await paystack.subscriptionCreate({
748
- customer: email,
749
- plan: planConfig.planCode,
750
- authorization: authorizationCode,
751
- start_date: trialEnd
752
- }));
753
- paystackSubscriptionCode = (subData?.data ?? subData)?.subscription_code;
809
+ try {
810
+ await ctx.context.adapter.update({
811
+ model: "paystackTransaction",
812
+ update: {
813
+ status: "success",
814
+ paystackId,
815
+ ...data?.amount !== void 0 && data?.amount !== null ? { amount: data.amount } : {},
816
+ ...data?.currency !== void 0 && data?.currency !== null ? { currency: data.currency } : {},
817
+ updatedAt: /* @__PURE__ */ new Date()
818
+ },
819
+ where: [{
820
+ field: "reference",
821
+ value: reference
822
+ }]
823
+ });
824
+ const customer = data?.customer;
825
+ const paystackCustomerCodeFromPaystack = customer !== void 0 && customer !== null && typeof customer === "object" ? customer.customer_code : void 0;
826
+ if (paystackCustomerCodeFromPaystack !== void 0 && paystackCustomerCodeFromPaystack !== null && paystackCustomerCodeFromPaystack !== "" && referenceId !== void 0 && referenceId !== null && referenceId !== "") if ((options.organization?.enabled === true && (referenceId.startsWith("org_") || await ctx.context.adapter.findOne({
827
+ model: "organization",
828
+ where: [{
829
+ field: "id",
830
+ value: referenceId
831
+ }]
832
+ }) !== null)) === true) await ctx.context.adapter.update({
833
+ model: "organization",
834
+ update: { paystackCustomerCode: paystackCustomerCodeFromPaystack },
835
+ where: [{
836
+ field: "id",
837
+ value: referenceId
838
+ }]
839
+ });
840
+ else await ctx.context.adapter.update({
841
+ model: "user",
842
+ update: { paystackCustomerCode: paystackCustomerCodeFromPaystack },
843
+ where: [{
844
+ field: "id",
845
+ value: referenceId
846
+ }]
847
+ });
848
+ const transaction = await ctx.context.adapter.findOne({
849
+ model: "paystackTransaction",
850
+ where: [{
851
+ field: "reference",
852
+ value: reference
853
+ }]
854
+ });
855
+ if (transaction?.product) await syncProductQuantityFromPaystack(ctx, transaction.product, options.paystackClient);
856
+ let isTrial = false;
857
+ let trialEnd;
858
+ let targetPlan;
859
+ if (data?.metadata !== void 0 && data?.metadata !== null) {
860
+ const metaRaw = data.metadata;
861
+ const meta = typeof metaRaw === "string" ? JSON.parse(metaRaw) : metaRaw;
862
+ isTrial = meta.isTrial === true || meta.isTrial === "true";
863
+ trialEnd = meta.trialEnd;
864
+ targetPlan = meta.plan;
754
865
  }
755
- } else if (isTrial !== true) {
756
- const planCodeFromPaystack = (data?.plan)?.plan_code;
757
- if (planCodeFromPaystack === void 0 || planCodeFromPaystack === null || planCodeFromPaystack === "") paystackSubscriptionCode = `LOC_${reference}`;
758
- else paystackSubscriptionCode = (data?.subscription)?.subscription_code;
759
- }
760
- const updatedSubscription = await ctx.context.adapter.update({
761
- model: "subscription",
762
- update: {
763
- status: isTrial === true ? "trialing" : "active",
764
- periodStart: /* @__PURE__ */ new Date(),
765
- updatedAt: /* @__PURE__ */ new Date(),
766
- ...isTrial === true && trialEnd !== void 0 && trialEnd !== null && trialEnd !== "" ? {
767
- trialStart: /* @__PURE__ */ new Date(),
768
- trialEnd: new Date(trialEnd),
769
- periodEnd: new Date(trialEnd)
770
- } : {},
771
- ...paystackSubscriptionCode !== void 0 && paystackSubscriptionCode !== null && paystackSubscriptionCode !== "" ? { paystackSubscriptionCode } : {},
772
- ...authorizationCode !== void 0 && authorizationCode !== null && authorizationCode !== "" ? { paystackAuthorizationCode: authorizationCode } : {}
773
- },
774
- where: [{
775
- field: "paystackTransactionReference",
776
- value: reference
777
- }, ...referenceId !== void 0 && referenceId !== null && referenceId !== "" ? [{
778
- field: "referenceId",
779
- value: referenceId
780
- }] : []]
781
- });
782
- if (updatedSubscription && subscriptionOptions?.enabled === true && "onSubscriptionComplete" in subscriptionOptions && typeof subscriptionOptions.onSubscriptionComplete === "function") {
783
- const plan = (await getPlans(subscriptionOptions)).find((p) => p.name.toLowerCase() === updatedSubscription.plan.toLowerCase());
784
- if (plan) await subscriptionOptions.onSubscriptionComplete({
785
- event: data,
786
- subscription: updatedSubscription,
787
- plan
788
- }, ctx);
866
+ let paystackSubscriptionCode;
867
+ if (isTrial === true && targetPlan !== void 0 && targetPlan !== null && targetPlan !== "" && trialEnd !== void 0 && trialEnd !== null && trialEnd !== "") {
868
+ const email = (data?.customer)?.email;
869
+ const planConfig = (await getPlans(subscriptionOptions)).find((p) => p.name.toLowerCase() === targetPlan?.toLowerCase());
870
+ if (planConfig !== void 0 && (planConfig.planCode === void 0 || planConfig.planCode === null || planConfig.planCode === "")) paystackSubscriptionCode = `LOC_${reference}`;
871
+ if (authorizationCode !== void 0 && authorizationCode !== null && authorizationCode !== "" && email !== void 0 && email !== null && email !== "" && planConfig?.planCode !== void 0 && planConfig?.planCode !== null && planConfig?.planCode !== "") {
872
+ const subData = unwrapSdkResult(await paystack.subscriptionCreate({
873
+ customer: email,
874
+ plan: planConfig.planCode,
875
+ authorization: authorizationCode,
876
+ start_date: trialEnd
877
+ }));
878
+ paystackSubscriptionCode = (subData?.data ?? subData)?.subscription_code;
879
+ }
880
+ } else if (isTrial !== true) {
881
+ const planCodeFromPaystack = (data?.plan)?.plan_code;
882
+ if (planCodeFromPaystack === void 0 || planCodeFromPaystack === null || planCodeFromPaystack === "") paystackSubscriptionCode = `LOC_${reference}`;
883
+ else paystackSubscriptionCode = (data?.subscription)?.subscription_code;
884
+ }
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({
896
+ model: "subscription",
897
+ update: {
898
+ status: isTrial === true ? "trialing" : "active",
899
+ periodStart: /* @__PURE__ */ new Date(),
900
+ updatedAt: /* @__PURE__ */ new Date(),
901
+ ...isTrial === true && trialEnd !== void 0 && trialEnd !== null && trialEnd !== "" ? {
902
+ trialStart: /* @__PURE__ */ new Date(),
903
+ trialEnd: new Date(trialEnd),
904
+ periodEnd: new Date(trialEnd)
905
+ } : {},
906
+ ...paystackSubscriptionCode !== void 0 && paystackSubscriptionCode !== null && paystackSubscriptionCode !== "" ? { paystackSubscriptionCode } : {},
907
+ ...authorizationCode !== void 0 && authorizationCode !== null && authorizationCode !== "" ? { paystackAuthorizationCode: authorizationCode } : {}
908
+ },
909
+ where: [{
910
+ field: "id",
911
+ value: targetSub.id
912
+ }]
913
+ });
914
+ if (updatedSubscription && subscriptionOptions?.enabled === true && "onSubscriptionComplete" in subscriptionOptions && typeof subscriptionOptions.onSubscriptionComplete === "function") {
915
+ const plan = (await getPlans(subscriptionOptions)).find((p) => p.name.toLowerCase() === updatedSubscription.plan.toLowerCase());
916
+ if (plan) await subscriptionOptions.onSubscriptionComplete({
917
+ event: data,
918
+ subscription: updatedSubscription,
919
+ plan
920
+ }, ctx);
921
+ }
922
+ } catch (e) {
923
+ ctx.context.logger.error("Failed to update transaction/subscription after verification", e);
789
924
  }
790
- } catch (e) {
791
- ctx.context.logger.error("Failed to update transaction/subscription after verification", e);
792
- }
793
- else if (status === "failed" || status === "abandoned") try {
925
+ } else if (status === "failed" || status === "abandoned") try {
794
926
  await ctx.context.adapter.update({
795
927
  model: "paystackTransaction",
796
928
  update: {
@@ -1026,27 +1158,182 @@ const enablePaystackSubscription = (options, path = "/paystack/enable-subscripti
1026
1158
  }
1027
1159
  });
1028
1160
  };
1029
- const getSubscriptionManageLink = (options) => {
1030
- return createAuthEndpoint("/paystack/get-subscription-manage-link", {
1031
- method: "GET",
1032
- query: z.object({ subscriptionCode: z.string() }),
1033
- use: options.subscription?.enabled === true ? [
1034
- sessionMiddleware,
1035
- originCheck,
1036
- referenceMiddleware(options, "get-subscription-manage-link")
1037
- ] : [sessionMiddleware, originCheck]
1038
- }, 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) => {
1039
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
+ });
1040
1174
  const paystack = getPaystackOps(options.paystackClient);
1041
1175
  try {
1042
1176
  const res = unwrapSdkResult(await paystack.subscriptionManageLink(subscriptionCode));
1043
- 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;
1044
1178
  const link = typeof data === "string" ? data : data?.link;
1045
1179
  return ctx.json({ link });
1046
1180
  } catch (error) {
1047
1181
  ctx.context.logger.error("Failed to get subscription manage link", error);
1048
1182
  throw new APIError("BAD_REQUEST", { message: error?.message ?? "Failed to get subscription manage link" });
1049
1183
  }
1184
+ };
1185
+ return createAuthEndpoint(path, {
1186
+ method: "GET",
1187
+ query: manageLinkQuerySchema,
1188
+ use: useMiddlewares
1189
+ }, handler);
1190
+ };
1191
+ const syncProducts = (options) => {
1192
+ return createAuthEndpoint("/paystack/sync-products", {
1193
+ method: "POST",
1194
+ metadata: { ...HIDE_METADATA },
1195
+ disableBody: true,
1196
+ use: [sessionMiddleware]
1197
+ }, async (ctx) => {
1198
+ console.error("DEBUG: syncProducts endpoint hit!");
1199
+ const paystack = getPaystackOps(options.paystackClient);
1200
+ try {
1201
+ const res = unwrapSdkResult(await paystack.productList());
1202
+ const productsData = res !== null && typeof res === "object" && "status" in res && "data" in res ? res.data : res?.data ?? res;
1203
+ if (!Array.isArray(productsData)) return ctx.json({
1204
+ status: "success",
1205
+ count: 0
1206
+ });
1207
+ for (const product of productsData) {
1208
+ const paystackId = String(product.id);
1209
+ const existing = await ctx.context.adapter.findOne({
1210
+ model: "paystackProduct",
1211
+ where: [{
1212
+ field: "paystackId",
1213
+ value: paystackId
1214
+ }]
1215
+ });
1216
+ const productData = {
1217
+ name: product.name,
1218
+ description: product.description,
1219
+ price: product.price,
1220
+ currency: product.currency,
1221
+ quantity: product.quantity,
1222
+ unlimited: product.unlimited,
1223
+ paystackId,
1224
+ slug: product.slug ?? product.name.toLowerCase().replace(/\s+/g, "-"),
1225
+ metadata: product.metadata ? JSON.stringify(product.metadata) : void 0,
1226
+ updatedAt: /* @__PURE__ */ new Date()
1227
+ };
1228
+ if (existing) await ctx.context.adapter.update({
1229
+ model: "paystackProduct",
1230
+ update: productData,
1231
+ where: [{
1232
+ field: "id",
1233
+ value: existing.id
1234
+ }]
1235
+ });
1236
+ else await ctx.context.adapter.create({
1237
+ model: "paystackProduct",
1238
+ data: {
1239
+ ...productData,
1240
+ createdAt: /* @__PURE__ */ new Date()
1241
+ }
1242
+ });
1243
+ }
1244
+ return ctx.json({
1245
+ status: "success",
1246
+ count: productsData.length
1247
+ });
1248
+ } catch (error) {
1249
+ ctx.context.logger.error("Failed to sync products", error);
1250
+ throw new APIError("BAD_REQUEST", { message: error?.message ?? "Failed to sync products" });
1251
+ }
1252
+ });
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
+ }
1050
1337
  });
1051
1338
  };
1052
1339
  const getConfig = (options) => {
@@ -1115,7 +1402,11 @@ const chargeRecurringSubscription = (options) => {
1115
1402
  }
1116
1403
  }
1117
1404
  if (email === void 0 || email === null || email === "") throw new APIError("NOT_FOUND", { message: "User email not found" });
1118
- if (!validateMinAmount(amount, plan.currency ?? "NGN")) throw new APIError("BAD_REQUEST", { message: `Amount ${amount} is below minimum for ${plan.currency ?? "NGN"}` });
1405
+ const finalCurrency = plan.currency ?? "NGN";
1406
+ if (!validateMinAmount(amount, finalCurrency)) throw new APIError("BAD_REQUEST", {
1407
+ message: `Amount ${amount} is less than the minimum required for ${finalCurrency}.`,
1408
+ status: 400
1409
+ });
1119
1410
  const data = unwrapSdkResult(await getPaystackOps(options.paystackClient).transactionChargeAuthorization({
1120
1411
  email,
1121
1412
  amount,
@@ -1161,7 +1452,8 @@ const chargeRecurringSubscription = (options) => {
1161
1452
  const transactions = { paystackTransaction: { fields: {
1162
1453
  reference: {
1163
1454
  type: "string",
1164
- required: true
1455
+ required: true,
1456
+ unique: true
1165
1457
  },
1166
1458
  paystackId: {
1167
1459
  type: "string",
@@ -1169,11 +1461,13 @@ const transactions = { paystackTransaction: { fields: {
1169
1461
  },
1170
1462
  referenceId: {
1171
1463
  type: "string",
1172
- required: true
1464
+ required: true,
1465
+ index: true
1173
1466
  },
1174
1467
  userId: {
1175
1468
  type: "string",
1176
- required: true
1469
+ required: true,
1470
+ index: true
1177
1471
  },
1178
1472
  amount: {
1179
1473
  type: "number",
@@ -1191,6 +1485,10 @@ const transactions = { paystackTransaction: { fields: {
1191
1485
  type: "string",
1192
1486
  required: false
1193
1487
  },
1488
+ product: {
1489
+ type: "string",
1490
+ required: false
1491
+ },
1194
1492
  metadata: {
1195
1493
  type: "string",
1196
1494
  required: false
@@ -1207,23 +1505,28 @@ const transactions = { paystackTransaction: { fields: {
1207
1505
  const subscriptions = { subscription: { fields: {
1208
1506
  plan: {
1209
1507
  type: "string",
1210
- required: true
1508
+ required: true,
1509
+ index: true
1211
1510
  },
1212
1511
  referenceId: {
1213
1512
  type: "string",
1214
- required: true
1513
+ required: true,
1514
+ index: true
1215
1515
  },
1216
1516
  paystackCustomerCode: {
1217
1517
  type: "string",
1218
- required: false
1518
+ required: false,
1519
+ index: true
1219
1520
  },
1220
1521
  paystackSubscriptionCode: {
1221
1522
  type: "string",
1222
- required: false
1523
+ required: false,
1524
+ unique: true
1223
1525
  },
1224
1526
  paystackTransactionReference: {
1225
1527
  type: "string",
1226
- required: false
1528
+ required: false,
1529
+ index: true
1227
1530
  },
1228
1531
  paystackAuthorizationCode: {
1229
1532
  type: "string",
@@ -1269,28 +1572,128 @@ const subscriptions = { subscription: { fields: {
1269
1572
  } } };
1270
1573
  const user = { user: { fields: { paystackCustomerCode: {
1271
1574
  type: "string",
1272
- required: false
1575
+ required: false,
1576
+ index: true
1273
1577
  } } } };
1274
1578
  const organization = { organization: { fields: {
1275
1579
  paystackCustomerCode: {
1276
1580
  type: "string",
1277
- required: false
1581
+ required: false,
1582
+ index: true
1278
1583
  },
1279
1584
  email: {
1280
1585
  type: "string",
1281
1586
  required: false
1282
1587
  }
1283
1588
  } } };
1589
+ const products = { paystackProduct: { fields: {
1590
+ name: {
1591
+ type: "string",
1592
+ required: true
1593
+ },
1594
+ description: {
1595
+ type: "string",
1596
+ required: false
1597
+ },
1598
+ price: {
1599
+ type: "number",
1600
+ required: true
1601
+ },
1602
+ currency: {
1603
+ type: "string",
1604
+ required: true
1605
+ },
1606
+ quantity: {
1607
+ type: "number",
1608
+ required: false,
1609
+ defaultValue: 0
1610
+ },
1611
+ unlimited: {
1612
+ type: "boolean",
1613
+ required: false,
1614
+ defaultValue: true
1615
+ },
1616
+ paystackId: {
1617
+ type: "string",
1618
+ required: false,
1619
+ unique: true
1620
+ },
1621
+ slug: {
1622
+ type: "string",
1623
+ required: true,
1624
+ unique: true
1625
+ },
1626
+ metadata: {
1627
+ type: "string",
1628
+ required: false
1629
+ },
1630
+ createdAt: {
1631
+ type: "date",
1632
+ required: true
1633
+ },
1634
+ updatedAt: {
1635
+ type: "date",
1636
+ required: true
1637
+ }
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
+ } } };
1284
1683
  const getSchema = (options) => {
1285
1684
  let baseSchema;
1286
1685
  if (options.subscription?.enabled === true) baseSchema = {
1287
1686
  ...subscriptions,
1288
1687
  ...transactions,
1289
- ...user
1688
+ ...user,
1689
+ ...products,
1690
+ ...plans
1290
1691
  };
1291
1692
  else baseSchema = {
1292
1693
  ...user,
1293
- ...transactions
1694
+ ...transactions,
1695
+ ...products,
1696
+ ...plans
1294
1697
  };
1295
1698
  if (options.organization?.enabled === true) baseSchema = {
1296
1699
  ...baseSchema,
@@ -1342,23 +1745,29 @@ const checkTeamLimit = async (ctx, organizationId, maxTeams) => {
1342
1745
  //#region src/index.ts
1343
1746
  const INTERNAL_ERROR_CODES = defineErrorCodes({ ...PAYSTACK_ERROR_CODES });
1344
1747
  const paystack = (options) => {
1748
+ const routeOptions = options;
1345
1749
  return {
1346
1750
  id: "paystack",
1347
1751
  endpoints: {
1348
- initializeTransaction: initializeTransaction(options),
1349
- verifyTransaction: verifyTransaction(options),
1350
- listSubscriptions: listSubscriptions(options),
1351
- paystackWebhook: paystackWebhook(options),
1352
- listTransactions: listTransactions(options),
1353
- getConfig: getConfig(options),
1354
- disableSubscription: disablePaystackSubscription(options),
1355
- enableSubscription: enablePaystackSubscription(options),
1356
- getSubscriptionManageLink: getSubscriptionManageLink(options),
1357
- createSubscription: createSubscription(options),
1358
- upgradeSubscription: upgradeSubscription(options),
1359
- cancelSubscription: cancelSubscription(options),
1360
- restoreSubscription: restoreSubscription(options),
1361
- chargeRecurringSubscription: chargeRecurringSubscription(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)
1362
1771
  },
1363
1772
  schema: getSchema(options),
1364
1773
  init: (ctx) => {
@@ -1366,12 +1775,12 @@ const paystack = (options) => {
1366
1775
  databaseHooks: {
1367
1776
  user: { create: { async after(user, hookCtx) {
1368
1777
  if (hookCtx === void 0 || hookCtx === null || options.createCustomerOnSignUp !== true) return;
1369
- const data = unwrapSdkResult(await getPaystackOps(options.paystackClient).customerCreate({
1778
+ const sdkRes = unwrapSdkResult(await getPaystackOps(options.paystackClient).customerCreate({
1370
1779
  email: user.email,
1371
1780
  first_name: user.name ?? void 0,
1372
1781
  metadata: { userId: user.id }
1373
1782
  }));
1374
- const customerCode = data?.customer_code ?? (data?.data)?.customer_code;
1783
+ const customerCode = sdkRes?.customer_code ?? (sdkRes?.data)?.customer_code;
1375
1784
  if (customerCode === void 0 || customerCode === null) return;
1376
1785
  await ctx.adapter.update({
1377
1786
  model: "user",
@@ -1412,12 +1821,11 @@ const paystack = (options) => {
1412
1821
  metadata: { organizationId: org.id }
1413
1822
  }, extraCreateParams);
1414
1823
  const sdkRes = unwrapSdkResult(await getPaystackOps(options.paystackClient).customerCreate(params));
1415
- const paystackCustomer = sdkRes !== null && typeof sdkRes === "object" && "status" in sdkRes && "data" in sdkRes ? sdkRes.data : sdkRes?.data ?? sdkRes;
1416
- const customerCode = paystackCustomer?.customer_code;
1824
+ const customerCode = sdkRes?.customer_code ?? (sdkRes?.data)?.customer_code;
1417
1825
  if (customerCode === void 0 || customerCode === null) return;
1418
1826
  await ctx.internalAdapter.updateOrganization(org.id, { paystackCustomerCode: customerCode });
1419
1827
  await options.organization?.onCustomerCreate?.({
1420
- paystackCustomer,
1828
+ paystackCustomer: sdkRes,
1421
1829
  organization: {
1422
1830
  ...org,
1423
1831
  paystackCustomerCode: customerCode