@alexasomba/better-auth-paystack 1.2.1 → 2.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/dist/index.mjs CHANGED
@@ -3,127 +3,83 @@ import { defu } from "defu";
3
3
  import { createAuthEndpoint, createAuthMiddleware } from "@better-auth/core/api";
4
4
  import { HIDE_METADATA, logger } from "better-auth";
5
5
  import { APIError, getSessionFromCtx, originCheck, sessionMiddleware } from "better-auth/api";
6
- import * as z from "zod/v4";
6
+ import { z } from "zod";
7
+ import { PaystackResponse } from "@alexasomba/paystack-node";
7
8
  import { mergeSchema } from "better-auth/db";
8
9
  //#region src/paystack-sdk.ts
9
- function isOpenApiFetchResponse(value) {
10
- return value !== null && value !== void 0 && typeof value === "object" && ("data" in value || "error" in value || "response" in value);
10
+ /**
11
+ * Interface for checking if a result is a PaystackResponse from the SDK v1.9.1+
12
+ */
13
+ function IsPaystackResponse(value) {
14
+ return value instanceof PaystackResponse;
11
15
  }
16
+ /**
17
+ * Unwraps a Paystack SDK result, extracting the data or throwing an APIError if the request failed.
18
+ * Leverages the native .unwrap() method in SDK v1.9.1+ if available.
19
+ */
12
20
  function unwrapSdkResult(result) {
13
- if (isOpenApiFetchResponse(result)) {
14
- if (result.error !== void 0 && result.error !== null) throw new Error(typeof result.error === "string" ? result.error : JSON.stringify(result.error));
15
- return result.data ?? result;
21
+ if (IsPaystackResponse(result)) try {
22
+ return result.unwrap();
23
+ } catch (e) {
24
+ throw new APIError("BAD_REQUEST", { message: e?.message ?? "Paystack API error" });
16
25
  }
17
- if (result !== null && result !== void 0 && typeof result === "object" && "data" in result) {
18
- const data = result.data;
19
- if (data !== null && typeof data === "object" && "data" in data) return data.data;
20
- return data;
26
+ let current = result;
27
+ while (current !== null && current !== void 0 && typeof current === "object") {
28
+ const body = current;
29
+ if (body.status === false) throw new APIError("BAD_REQUEST", { message: body.message ?? "Paystack API error" });
30
+ if ("authorization_url" in body || "reference" in body || "customer_code" in body) break;
31
+ if ("data" in body && body.data !== void 0 && body.data !== null && typeof body.data === "object") {
32
+ current = body.data;
33
+ continue;
34
+ }
35
+ break;
21
36
  }
22
- return result;
37
+ return current;
23
38
  }
24
- function getPaystackOps(paystackClient) {
25
- return {
26
- customerCreate: (params) => {
27
- if (paystackClient?.customer_create !== void 0) return paystackClient.customer_create({ body: params });
28
- return paystackClient?.customer?.create?.(params);
29
- },
30
- customerUpdate: (code, params) => {
31
- if (paystackClient?.customer_update !== void 0) return paystackClient.customer_update({
32
- params: { path: { code } },
33
- body: params
34
- });
35
- return paystackClient?.customer?.update?.(code, params);
36
- },
37
- transactionInitialize: (body) => {
38
- if (paystackClient?.transaction_initialize !== void 0) return paystackClient.transaction_initialize({ body });
39
- return paystackClient?.transaction?.initialize?.(body);
40
- },
41
- transactionVerify: (reference) => {
42
- if (paystackClient?.transaction_verify !== void 0) return paystackClient.transaction_verify({ params: { path: { reference } } });
43
- return paystackClient?.transaction?.verify?.(reference);
44
- },
45
- subscriptionCreate: (body) => {
46
- if (paystackClient?.subscription_create !== void 0) return paystackClient.subscription_create({ body });
47
- return paystackClient?.subscription?.create?.(body);
48
- },
49
- subscriptionDisable: (body) => {
50
- if (paystackClient?.subscription_disable !== void 0) return paystackClient.subscription_disable({ body });
51
- return paystackClient?.subscription?.disable?.(body);
52
- },
53
- subscriptionEnable: (body) => {
54
- if (paystackClient?.subscription_enable !== void 0) return paystackClient.subscription_enable({ body });
55
- return paystackClient?.subscription?.enable?.(body);
56
- },
57
- subscriptionFetch: async (idOrCode) => {
58
- if (paystackClient?.subscription_fetch !== void 0) try {
59
- return await paystackClient.subscription_fetch({ params: { path: { code: idOrCode } } });
60
- } catch {
61
- const compatFetch = paystackClient.subscription_fetch;
62
- return compatFetch({ params: { path: { id_or_code: idOrCode } } });
63
- }
64
- return paystackClient?.subscription?.fetch?.(idOrCode);
65
- },
66
- subscriptionManageLink: (code) => {
67
- if (paystackClient?.subscription_manageLink !== void 0) return paystackClient.subscription_manageLink({ params: { path: { code } } });
68
- if (paystackClient?.subscription_manage_link !== void 0) return paystackClient.subscription_manage_link({ params: { path: { code } } });
69
- return paystackClient?.subscription?.manage?.link?.(code);
70
- },
71
- subscriptionManageEmail: (code, email) => {
72
- if (paystackClient?.subscription_manageEmail !== void 0) return paystackClient.subscription_manageEmail({ params: { path: { code } } });
73
- return paystackClient?.subscription?.manage?.email?.(code, email);
74
- },
75
- subscriptionUpdate: (params) => {
76
- if (paystackClient?.subscription_update !== void 0) return paystackClient.subscription_update({
77
- params: { path: { code: params.code } },
78
- body: {
79
- plan: params.plan,
80
- authorization: params.authorization,
81
- amount: params.amount
82
- }
83
- });
84
- return paystackClient?.subscription?.update?.(params.code, params);
85
- },
86
- transactionChargeAuthorization: (body) => {
87
- if (paystackClient?.transaction_chargeAuthorization !== void 0) return paystackClient.transaction_chargeAuthorization({ body });
88
- return paystackClient?.transaction?.chargeAuthorization?.(body);
89
- },
90
- productList: () => {
91
- if (paystackClient?.product_list !== void 0) return paystackClient.product_list();
92
- return paystackClient?.product?.list?.();
93
- },
94
- productFetch: (idOrCode) => {
95
- if (paystackClient?.product_fetch !== void 0) return paystackClient.product_fetch({ params: { path: { id_or_code: idOrCode } } });
96
- return paystackClient?.product?.fetch?.(idOrCode);
97
- },
98
- productCreate: (params) => {
99
- if (paystackClient?.product_create !== void 0) return paystackClient.product_create({ body: params });
100
- return paystackClient?.product?.create?.(params);
101
- },
102
- productUpdate: (idOrCode, params) => {
103
- if (paystackClient?.product_update !== void 0) return paystackClient.product_update({
104
- params: { path: { id_or_code: idOrCode } },
105
- body: params
106
- });
107
- return paystackClient?.product?.update?.(idOrCode, params);
108
- },
109
- productDelete: (idOrCode) => {
110
- if (paystackClient?.product_delete !== void 0) return paystackClient.product_delete({ params: { path: { id_or_code: idOrCode } } });
111
- return paystackClient?.product?.delete?.(idOrCode);
112
- },
113
- planList: () => {
114
- if (paystackClient?.plan_list !== void 0) return paystackClient.plan_list();
115
- return paystackClient?.plan?.list?.();
116
- }
117
- };
39
+ /**
40
+ * Returns the operations object from a Paystack client.
41
+ * For v1.9.1+, the client itself uses the grouped structure.
42
+ */
43
+ function getPaystackOps(client) {
44
+ return client;
118
45
  }
119
46
  //#endregion
120
47
  //#region src/utils.ts
48
+ function getPlanSeatAmount(plan) {
49
+ if (plan.seatAmount !== void 0) {
50
+ if (typeof plan.seatAmount === "number" && Number.isFinite(plan.seatAmount)) return plan.seatAmount;
51
+ throw new Error(`Invalid seatAmount for plan '${plan.name}'. Expected a finite number.`);
52
+ }
53
+ if (plan.seatPriceId === void 0 || plan.seatPriceId === null || plan.seatPriceId === "") return;
54
+ const parsed = typeof plan.seatPriceId === "string" ? Number(plan.seatPriceId) : plan.seatPriceId;
55
+ if (typeof parsed === "number" && Number.isFinite(parsed)) return parsed;
56
+ throw new Error(`Invalid seatPriceId for plan '${plan.name}'. Expected a numeric amount in the smallest currency unit.`);
57
+ }
58
+ function calculatePlanAmount(plan, quantity) {
59
+ return (plan.amount ?? 0) + quantity * (getPlanSeatAmount(plan) ?? 0);
60
+ }
61
+ function isLocalSubscriptionCode(subscriptionCode) {
62
+ return typeof subscriptionCode === "string" && (subscriptionCode.startsWith("LOC_") || subscriptionCode.startsWith("sub_local_"));
63
+ }
64
+ function isLocallyManagedSubscription(subscription) {
65
+ if (isLocalSubscriptionCode(subscription.paystackSubscriptionCode)) return true;
66
+ if (typeof subscription.paystackSubscriptionCode === "string" && subscription.paystackSubscriptionCode !== "") return false;
67
+ return subscription.paystackPlanCode === void 0 || subscription.paystackPlanCode === null || subscription.paystackPlanCode === "";
68
+ }
69
+ function assertLocallyManagedSubscription(subscription, action) {
70
+ if (!isLocallyManagedSubscription(subscription)) throw new Error(`Paystack-managed subscriptions do not support ${action}. Use local billing for seat-based or prorated subscription changes.`);
71
+ }
121
72
  async function getPlans(subscriptionOptions) {
122
73
  if (subscriptionOptions?.enabled === true) return typeof subscriptionOptions.plans === "function" ? subscriptionOptions.plans() : subscriptionOptions.plans;
123
74
  throw new Error("Subscriptions are not enabled in the Paystack options.");
124
75
  }
125
76
  async function getPlanByName(options, name) {
126
- if (options.subscription?.enabled === true) return (await getPlans(options.subscription)).find((plan) => plan.name.toLowerCase() === name.toLowerCase()) ?? null;
77
+ if (typeof name !== "string" || name.trim() === "") return null;
78
+ if (options.subscription?.enabled === true) {
79
+ const plans = await getPlans(options.subscription);
80
+ const normalizedName = name.toLowerCase();
81
+ return plans.find((plan) => typeof plan.name === "string" && plan.name.toLowerCase() === normalizedName) ?? null;
82
+ }
127
83
  return null;
128
84
  }
129
85
  async function getProducts(productOptions) {
@@ -131,7 +87,7 @@ async function getProducts(productOptions) {
131
87
  return [];
132
88
  }
133
89
  async function getProductByName(options, name) {
134
- return await getProducts(options.products).then((products) => products?.find((product) => product.name.toLowerCase() === name.toLowerCase()) ?? null);
90
+ return await getProducts(options.products).then((products) => products !== void 0 && products !== null ? products.find((product) => product.name.toLowerCase() === name.toLowerCase()) ?? null : null);
135
91
  }
136
92
  function getNextPeriodEnd(startDate, interval) {
137
93
  const date = new Date(startDate);
@@ -189,7 +145,7 @@ async function syncProductQuantityFromPaystack(ctx, productName, paystackClient)
189
145
  }]
190
146
  });
191
147
  if (localProduct?.paystackId === void 0 || localProduct.paystackId === null || localProduct.paystackId === "") {
192
- if (localProduct !== null && localProduct.unlimited !== true && typeof localProduct.quantity === "number" && localProduct.quantity > 0) await ctx.context.adapter.update({
148
+ if (localProduct?.id !== void 0 && localProduct.unlimited !== true && typeof localProduct.quantity === "number" && localProduct.quantity > 0) await ctx.context.adapter.update({
193
149
  model: "paystackProduct",
194
150
  update: {
195
151
  quantity: localProduct.quantity - 1,
@@ -203,8 +159,10 @@ async function syncProductQuantityFromPaystack(ctx, productName, paystackClient)
203
159
  return;
204
160
  }
205
161
  try {
206
- const remoteQuantity = unwrapSdkResult(await getPaystackOps(paystackClient).productFetch(localProduct.paystackId))?.quantity;
207
- if (remoteQuantity !== void 0) await ctx.context.adapter.update({
162
+ const paystackProductId = Number(localProduct.paystackId);
163
+ if (!Number.isFinite(paystackProductId)) return;
164
+ const remoteQuantity = unwrapSdkResult(await paystackClient.product?.fetch(paystackProductId))?.quantity;
165
+ if (remoteQuantity !== void 0 && localProduct.id !== void 0) await ctx.context.adapter.update({
208
166
  model: "paystackProduct",
209
167
  update: {
210
168
  quantity: remoteQuantity,
@@ -216,7 +174,7 @@ async function syncProductQuantityFromPaystack(ctx, productName, paystackClient)
216
174
  }]
217
175
  });
218
176
  } catch {
219
- if (localProduct !== null && localProduct.unlimited !== true && typeof localProduct.quantity === "number" && localProduct.quantity > 0) await ctx.context.adapter.update({
177
+ if (localProduct?.id !== void 0 && localProduct.unlimited !== true && typeof localProduct.quantity === "number" && localProduct.quantity > 0) await ctx.context.adapter.update({
220
178
  model: "paystackProduct",
221
179
  update: {
222
180
  quantity: localProduct.quantity - 1,
@@ -240,9 +198,10 @@ async function syncSubscriptionSeats(ctx, organizationId, options) {
240
198
  }]
241
199
  });
242
200
  if (subscription?.paystackSubscriptionCode === void 0 || subscription.paystackSubscriptionCode === null || subscription.paystackSubscriptionCode === "") return;
201
+ if (subscription === null || subscription === void 0) return;
243
202
  const plan = await getPlanByName(options, subscription.plan);
244
- if (plan === null) return;
245
- if (plan.seatAmount === void 0 && plan.seatPlanCode === void 0) return;
203
+ if (plan === null || plan === void 0) return;
204
+ if (getPlanSeatAmount(plan) === void 0) return;
246
205
  const quantity = (await adapter.findMany({
247
206
  model: "member",
248
207
  where: [{
@@ -250,14 +209,8 @@ async function syncSubscriptionSeats(ctx, organizationId, options) {
250
209
  value: organizationId
251
210
  }]
252
211
  })).length;
253
- let totalAmount = plan.amount ?? 0;
254
- if (plan.seatAmount !== void 0 && plan.seatAmount !== null && typeof plan.seatAmount === "number") totalAmount += quantity * plan.seatAmount;
255
- const ops = getPaystackOps(options.paystackClient);
256
212
  try {
257
- await ops.subscriptionUpdate({
258
- code: subscription.paystackSubscriptionCode,
259
- amount: totalAmount
260
- });
213
+ assertLocallyManagedSubscription(subscription, "automatic seat sync");
261
214
  await adapter.update({
262
215
  model: "subscription",
263
216
  where: [{
@@ -271,7 +224,7 @@ async function syncSubscriptionSeats(ctx, organizationId, options) {
271
224
  });
272
225
  } catch (e) {
273
226
  const log = ctx.context.logger;
274
- if (log !== void 0 && log !== null) log.error("Failed to sync subscription seats with Paystack", e);
227
+ if (log !== void 0 && log !== null) log.error("Failed to sync subscription seats", e);
275
228
  }
276
229
  }
277
230
  //#endregion
@@ -284,7 +237,7 @@ const referenceMiddleware = (options, action) => createAuthMiddleware(async (ctx
284
237
  const referenceId = body.referenceId ?? query.referenceId ?? session.user.id;
285
238
  const subscriptionOptions = options.subscription;
286
239
  if (referenceId === session.user.id) return { referenceId };
287
- if (subscriptionOptions?.enabled === true && "authorizeReference" in subscriptionOptions && subscriptionOptions.authorizeReference) {
240
+ if (subscriptionOptions?.enabled === true && "authorizeReference" in subscriptionOptions && typeof subscriptionOptions.authorizeReference === "function") {
288
241
  if (await subscriptionOptions.authorizeReference({
289
242
  user: session.user,
290
243
  session: session.session,
@@ -325,7 +278,7 @@ const getOrganizationSubscription = async (ctx, organizationId) => {
325
278
  };
326
279
  const checkSeatLimit = async (ctx, organizationId, seatsToAdd = 1) => {
327
280
  const subscription = await getOrganizationSubscription(ctx, organizationId);
328
- if (subscription?.seats === void 0 || subscription.seats === null) return true;
281
+ if (subscription?.seats === null) return true;
329
282
  const members = await ctx.context.adapter.findMany({
330
283
  model: "member",
331
284
  where: [{
@@ -333,6 +286,7 @@ const checkSeatLimit = async (ctx, organizationId, seatsToAdd = 1) => {
333
286
  value: organizationId
334
287
  }]
335
288
  });
289
+ if (!subscription) return true;
336
290
  if (members.length + seatsToAdd > subscription.seats) throw new APIError("FORBIDDEN", { message: `Organization member limit reached. Used: ${members.length}, Max: ${subscription.seats}` });
337
291
  return true;
338
292
  };
@@ -356,8 +310,17 @@ const PAYSTACK_ERROR_CODES = defineErrorCodes({
356
310
  FAILED_TO_VERIFY_TRANSACTION: "Failed to verify transaction",
357
311
  FAILED_TO_DISABLE_SUBSCRIPTION: "Failed to disable subscription",
358
312
  FAILED_TO_ENABLE_SUBSCRIPTION: "Failed to enable subscription",
359
- EMAIL_VERIFICATION_REQUIRED: "Email verification is required before you can subscribe to a plan"
313
+ EMAIL_VERIFICATION_REQUIRED: "Email verification is required before you can subscribe to a plan",
314
+ SUBSCRIPTION_PAYMENT_CHANNEL_NOT_ALLOWED: "This subscription only supports specific payment channels"
360
315
  });
316
+ function getAllowedSubscriptionChannels(options) {
317
+ const channels = options.subscription?.allowedPaymentChannels;
318
+ return Array.isArray(channels) && channels.length > 0 ? channels : void 0;
319
+ }
320
+ function isAllowedSubscriptionChannel(channel, allowedChannels) {
321
+ if (allowedChannels === void 0) return true;
322
+ return channel !== void 0 && channel !== null && allowedChannels.includes(channel);
323
+ }
361
324
  async function hmacSha512Hex(secret, message) {
362
325
  const encoder = new TextEncoder();
363
326
  const keyData = encoder.encode(secret);
@@ -375,8 +338,8 @@ async function hmacSha512Hex(secret, message) {
375
338
  const { createHmac } = await import("node:crypto");
376
339
  return createHmac("sha512", secret).update(message).digest("hex");
377
340
  }
378
- const paystackWebhook = (options) => {
379
- return createAuthEndpoint("/paystack/webhook", {
341
+ const paystackWebhook = (options, path = "/webhook") => {
342
+ return createAuthEndpoint(path, {
380
343
  method: "POST",
381
344
  metadata: {
382
345
  ...HIDE_METADATA,
@@ -388,12 +351,25 @@ const paystackWebhook = (options) => {
388
351
  const request = ctx.requestClone ?? ctx.request;
389
352
  if (request === void 0 || request === null) throw new APIError("BAD_REQUEST", { message: "Request object is missing from context" });
390
353
  const payload = await request.text();
391
- const signature = (ctx.headers ?? ctx.request?.headers)?.get("x-paystack-signature");
354
+ const headers = ctx.headers ?? ctx.request?.headers;
355
+ const signature = headers?.get("x-paystack-signature");
356
+ if (options.webhook?.verifyIP === true) {
357
+ const trustedIPs = options.webhook.trustedIPs ?? [
358
+ "52.31.139.75",
359
+ "52.49.173.169",
360
+ "52.214.14.220"
361
+ ];
362
+ const clientIP = headers?.get("x-forwarded-for")?.split(",")[0]?.trim() ?? headers?.get("x-real-ip") ?? ctx.request.ip;
363
+ if (clientIP !== void 0 && clientIP !== null && trustedIPs.includes(clientIP) === false) throw new APIError("UNAUTHORIZED", {
364
+ message: `Forbidden IP: ${clientIP}`,
365
+ status: 401
366
+ });
367
+ }
392
368
  if (signature === void 0 || signature === null || signature === "") throw new APIError("UNAUTHORIZED", {
393
369
  message: "Missing x-paystack-signature header",
394
370
  status: 401
395
371
  });
396
- if (await hmacSha512Hex(options.paystackWebhookSecret, payload) !== signature) throw new APIError("UNAUTHORIZED", {
372
+ if (await hmacSha512Hex(options.webhook?.secret ?? options.paystackWebhookSecret ?? options.secretKey, payload) !== signature) throw new APIError("UNAUTHORIZED", {
397
373
  message: "Invalid Paystack webhook signature",
398
374
  status: 401
399
375
  });
@@ -402,7 +378,8 @@ const paystackWebhook = (options) => {
402
378
  const data = event.data;
403
379
  if (eventName === "charge.success") {
404
380
  const reference = data?.reference;
405
- const paystackId = data?.id !== void 0 && data?.id !== null ? String(data.id) : void 0;
381
+ const paystackIdRaw = data?.id;
382
+ const paystackId = paystackIdRaw !== void 0 && paystackIdRaw !== null ? String(paystackIdRaw) : void 0;
406
383
  if (reference !== void 0 && reference !== null && reference !== "") {
407
384
  try {
408
385
  await ctx.context.adapter.update({
@@ -428,7 +405,9 @@ const paystackWebhook = (options) => {
428
405
  value: reference
429
406
  }]
430
407
  });
431
- if (transaction?.product !== void 0 && transaction.product !== null && transaction.product !== "") await syncProductQuantityFromPaystack(ctx, transaction.product, options.paystackClient);
408
+ if (transaction !== void 0 && transaction !== null && transaction.product !== void 0 && transaction.product !== null && transaction.product !== "") {
409
+ if (options.paystackClient !== void 0 && options.paystackClient !== null) await syncProductQuantityFromPaystack(ctx, transaction.product, options.paystackClient);
410
+ }
432
411
  } catch (e) {
433
412
  ctx.context.logger.warn("Failed to sync product quantity", e);
434
413
  }
@@ -454,19 +433,20 @@ const paystackWebhook = (options) => {
454
433
  }
455
434
  if (options.subscription?.enabled === true) try {
456
435
  if (eventName === "subscription.create") {
457
- const payloadData = data;
458
- const subscriptionCode = payloadData?.subscription_code ?? payloadData?.subscription?.subscription_code ?? payloadData?.code;
459
- const customerCode = payloadData?.customer?.customer_code ?? payloadData?.customer_code ?? payloadData?.customer?.code;
460
- const planCode = payloadData?.plan?.plan_code ?? payloadData?.plan_code ?? payloadData?.plan;
461
- let metadata = payloadData?.metadata;
436
+ const subscriptionData = data;
437
+ const subscriptionCode = subscriptionData.subscription_code ?? "";
438
+ const customerCode = subscriptionData.customer?.customer_code;
439
+ const planCode = subscriptionData.plan?.plan_code;
440
+ let metadata = subscriptionData.metadata;
462
441
  if (typeof metadata === "string") try {
463
442
  metadata = JSON.parse(metadata);
464
443
  } catch {}
465
- const referenceIdFromMetadata = typeof metadata === "object" && metadata !== null ? metadata.referenceId : void 0;
466
- let planNameFromMetadata = typeof metadata === "object" && metadata !== null ? metadata.plan : void 0;
444
+ const metadataObj = metadata !== void 0 && metadata !== null && typeof metadata === "object" ? metadata : {};
445
+ const referenceIdFromMetadata = typeof metadataObj.referenceId === "string" ? metadataObj.referenceId : void 0;
446
+ let planNameFromMetadata = typeof metadataObj.plan === "string" ? metadataObj.plan : void 0;
467
447
  if (typeof planNameFromMetadata === "string") planNameFromMetadata = planNameFromMetadata.toLowerCase();
468
448
  const plans = await getPlans(options.subscription);
469
- const planFromCode = planCode !== void 0 && planCode !== null && planCode !== "" ? plans.find((p) => p.planCode !== void 0 && p.planCode !== null && p.planCode === planCode) : void 0;
449
+ const planFromCode = planCode !== void 0 && planCode !== null && planCode !== "" ? plans.find((p) => p.planCode === planCode) : void 0;
470
450
  const planPart = planFromCode?.name ?? planNameFromMetadata;
471
451
  const planName = planPart !== void 0 && planPart !== null && planPart !== "" ? planPart.toLowerCase() : void 0;
472
452
  if (subscriptionCode !== void 0 && subscriptionCode !== null && subscriptionCode !== "") {
@@ -495,7 +475,7 @@ const paystackWebhook = (options) => {
495
475
  paystackSubscriptionCode: subscriptionCode,
496
476
  status: "active",
497
477
  updatedAt: /* @__PURE__ */ new Date(),
498
- periodEnd: payloadData?.next_payment_date !== void 0 && payloadData.next_payment_date !== null && payloadData.next_payment_date !== "" ? new Date(payloadData.next_payment_date) : void 0
478
+ periodEnd: subscriptionData.next_payment_date !== void 0 && subscriptionData.next_payment_date !== null ? new Date(subscriptionData.next_payment_date) : void 0
499
479
  },
500
480
  where: [{
501
481
  field: "id",
@@ -528,9 +508,9 @@ const paystackWebhook = (options) => {
528
508
  }
529
509
  }
530
510
  if (eventName === "subscription.disable" || eventName === "subscription.not_renew") {
531
- const payloadData = data;
532
- const subscriptionCode = payloadData?.subscription_code ?? payloadData?.subscription?.subscription_code ?? payloadData?.code;
533
- if (subscriptionCode !== void 0 && subscriptionCode !== null && subscriptionCode !== "") {
511
+ const subscriptionData = data;
512
+ const subscriptionCode = subscriptionData.subscription_code ?? "";
513
+ if (subscriptionCode !== "") {
534
514
  const existing = await ctx.context.adapter.findOne({
535
515
  model: "subscription",
536
516
  where: [{
@@ -539,9 +519,9 @@ const paystackWebhook = (options) => {
539
519
  }]
540
520
  });
541
521
  let newStatus = "canceled";
542
- const nextPaymentDate = data?.next_payment_date;
543
- const periodEnd = nextPaymentDate !== void 0 && nextPaymentDate !== null && nextPaymentDate !== "" ? new Date(nextPaymentDate) : existing?.periodEnd !== void 0 ? new Date(existing.periodEnd) : void 0;
544
- if (periodEnd !== void 0 && periodEnd > /* @__PURE__ */ new Date()) newStatus = "active";
522
+ const nextPaymentDate = subscriptionData.next_payment_date;
523
+ const periodEnd = nextPaymentDate !== void 0 && nextPaymentDate !== null && nextPaymentDate !== "" ? new Date(nextPaymentDate) : existing?.periodEnd !== void 0 && existing.periodEnd !== null ? new Date(existing.periodEnd) : void 0;
524
+ if (periodEnd !== void 0 && periodEnd.getTime() > Date.now()) newStatus = "active";
545
525
  await ctx.context.adapter.update({
546
526
  model: "subscription",
547
527
  update: {
@@ -555,7 +535,7 @@ const paystackWebhook = (options) => {
555
535
  value: subscriptionCode
556
536
  }]
557
537
  });
558
- if (existing !== void 0 && existing !== null) await options.subscription.onSubscriptionCancel?.({
538
+ if (existing !== null && existing !== void 0) await options.subscription.onSubscriptionCancel?.({
559
539
  event,
560
540
  subscription: {
561
541
  ...existing,
@@ -565,9 +545,9 @@ const paystackWebhook = (options) => {
565
545
  }
566
546
  }
567
547
  if (eventName === "charge.success" || eventName === "invoice.update") {
568
- const payloadData = data;
569
- const subscriptionCode = payloadData?.subscription?.subscription_code ?? payloadData?.subscription_code;
570
- if (subscriptionCode !== void 0 && subscriptionCode !== null && subscriptionCode !== "") {
548
+ const subscriptionCodeRaw = (data?.subscription)?.subscription_code ?? data?.subscription_code;
549
+ const subscriptionCode = subscriptionCodeRaw !== void 0 && subscriptionCodeRaw !== null && subscriptionCodeRaw !== "" ? subscriptionCodeRaw : void 0;
550
+ if (subscriptionCode !== void 0) {
571
551
  const existingSub = await ctx.context.adapter.findOne({
572
552
  model: "subscription",
573
553
  where: [{
@@ -575,7 +555,7 @@ const paystackWebhook = (options) => {
575
555
  value: subscriptionCode
576
556
  }]
577
557
  });
578
- if (existingSub?.pendingPlan !== void 0 && existingSub.pendingPlan !== null && existingSub.pendingPlan !== "") await ctx.context.adapter.update({
558
+ if (existingSub !== void 0 && existingSub !== null && existingSub.pendingPlan !== void 0 && existingSub.pendingPlan !== null && existingSub.pendingPlan !== "") await ctx.context.adapter.update({
579
559
  model: "subscription",
580
560
  update: {
581
561
  plan: existingSub.pendingPlan,
@@ -610,7 +590,7 @@ const initializeTransactionBodySchema = z.object({
610
590
  cancelAtPeriodEnd: z.boolean().optional(),
611
591
  prorateAndCharge: z.boolean().optional()
612
592
  });
613
- const initializeTransaction = (options, path = "/paystack/initialize-transaction") => {
593
+ const initializeTransaction = (options, path = "/initialize-transaction") => {
614
594
  const subscriptionOptions = options.subscription;
615
595
  return createAuthEndpoint(path, {
616
596
  method: "POST",
@@ -626,23 +606,22 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
626
606
  if (callbackURL !== void 0 && callbackURL !== null && callbackURL !== "") {
627
607
  const checkTrusted = () => {
628
608
  try {
629
- if (callbackURL === void 0 || callbackURL === null || callbackURL === "") return false;
630
- if (callbackURL.startsWith("/")) return true;
609
+ if (callbackURL?.startsWith("/") === true) return true;
631
610
  const baseUrl = ctx.context?.baseURL ?? ctx.request?.url ?? "";
632
- if (!baseUrl) return false;
611
+ if (baseUrl === "") return false;
633
612
  const baseOrigin = new URL(baseUrl).origin;
634
613
  return new URL(callbackURL).origin === baseOrigin;
635
614
  } catch {
636
615
  return false;
637
616
  }
638
617
  };
639
- if (checkTrusted() !== true) throw new APIError("FORBIDDEN", {
618
+ if (checkTrusted() === false) throw new APIError("FORBIDDEN", {
640
619
  message: "callbackURL is not a trusted origin.",
641
620
  status: 403
642
621
  });
643
622
  }
644
623
  const session = await getSessionFromCtx(ctx);
645
- if (!session) throw new APIError("UNAUTHORIZED");
624
+ if (session === void 0 || session === null) throw new APIError("UNAUTHORIZED");
646
625
  const user = session.user;
647
626
  if (subscriptionOptions?.enabled === true && subscriptionOptions.requireEmailVerification === true && user.emailVerified !== true) throw new APIError("BAD_REQUEST", {
648
627
  code: "EMAIL_VERIFICATION_REQUIRED",
@@ -653,7 +632,7 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
653
632
  if (planName !== void 0 && planName !== null && planName !== "") {
654
633
  if (subscriptionOptions?.enabled !== true) throw new APIError("BAD_REQUEST", { message: "Subscriptions are not enabled." });
655
634
  plan = await getPlanByName(options, planName) ?? void 0;
656
- if (plan === null || plan === void 0) {
635
+ if (plan === void 0 || plan === null) try {
657
636
  const nativePlan = await ctx.context.adapter.findOne({
658
637
  model: "paystackPlan",
659
638
  where: [{
@@ -669,15 +648,17 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
669
648
  value: planName
670
649
  }]
671
650
  }) ?? void 0;
651
+ } catch {
652
+ plan = void 0;
672
653
  }
673
- if (plan === null || plan === void 0) throw new APIError("BAD_REQUEST", {
654
+ if (plan === void 0 || plan === null) throw new APIError("BAD_REQUEST", {
674
655
  code: "SUBSCRIPTION_PLAN_NOT_FOUND",
675
656
  message: PAYSTACK_ERROR_CODES.SUBSCRIPTION_PLAN_NOT_FOUND.message,
676
657
  status: 400
677
658
  });
678
659
  } else if (productName !== void 0 && productName !== null && productName !== "") {
679
660
  if (typeof productName === "string") {
680
- product ??= await getProductByName(options, productName) ?? void 0;
661
+ product = await getProductByName(options, productName) ?? void 0;
681
662
  product ??= await ctx.context.adapter.findOne({
682
663
  model: "paystackProduct",
683
664
  where: [{
@@ -686,19 +667,19 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
686
667
  }]
687
668
  }) ?? void 0;
688
669
  }
689
- if (product === null || product === void 0) throw new APIError("BAD_REQUEST", {
670
+ if (product === void 0 || product === null) throw new APIError("BAD_REQUEST", {
690
671
  message: `Product '${productName}' not found.`,
691
672
  status: 400
692
673
  });
693
- } else if (bodyAmount === void 0 || bodyAmount === null || bodyAmount === 0) throw new APIError("BAD_REQUEST", {
674
+ } else if (bodyAmount === void 0 || bodyAmount === null) throw new APIError("BAD_REQUEST", {
694
675
  message: "Either 'plan', 'product', or 'amount' is required to initialize a transaction.",
695
676
  status: 400
696
677
  });
697
- let amount = bodyAmount ?? product?.price;
698
- const finalCurrency = currency ?? product?.currency ?? plan?.currency ?? "NGN";
678
+ let amount = bodyAmount ?? product?.price ?? product?.amount;
679
+ const finalCurrency = currency ?? product?.currency ?? product?.currency ?? plan?.currency ?? "NGN";
699
680
  const referenceIdFromCtx = ctx.context.referenceId;
700
- 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;
701
- if (plan && scheduleAtPeriodEnd === true) {
681
+ const referenceId = ctx.body.referenceId ?? referenceIdFromCtx ?? session.user.id;
682
+ if (plan !== void 0 && scheduleAtPeriodEnd === true) {
702
683
  const existingSub = await getOrganizationSubscription(ctx, referenceId);
703
684
  if (existingSub?.status === "active") {
704
685
  await ctx.context.adapter.update({
@@ -740,40 +721,45 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
740
721
  });
741
722
  }
742
723
  }
743
- if (plan !== null && plan !== void 0 && (plan.seatAmount !== void 0 || "seatPriceId" in plan)) {
744
- const members = await ctx.context.adapter.findMany({
745
- model: "member",
746
- where: [{
747
- field: "organizationId",
748
- value: referenceId
749
- }]
750
- });
751
- const seatCount = members.length > 0 ? members.length : 1;
752
- const quantityToUse = quantity ?? seatCount;
753
- amount = (plan.amount ?? 0) + quantityToUse * (plan.seatAmount ?? plan.seatPriceId ?? 0);
724
+ if (plan !== void 0) try {
725
+ if (getPlanSeatAmount(plan) !== void 0) {
726
+ const members = await ctx.context.adapter.findMany({
727
+ model: "member",
728
+ where: [{
729
+ field: "organizationId",
730
+ value: referenceId
731
+ }]
732
+ });
733
+ const seatCount = members.length > 0 ? members.length : 1;
734
+ amount = calculatePlanAmount(plan, quantity ?? seatCount);
735
+ }
736
+ } catch (error) {
737
+ throw new APIError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Invalid seat configuration for plan." });
754
738
  }
755
739
  let url;
756
740
  let reference;
757
741
  let accessCode;
758
742
  let trialStart;
759
743
  let trialEnd;
760
- if (plan?.freeTrial?.days !== void 0 && plan.freeTrial.days !== null && plan.freeTrial.days > 0) {
761
- if ((await ctx.context.adapter.findMany({
762
- model: "subscription",
763
- where: [{
764
- field: "referenceId",
765
- value: referenceId
766
- }]
767
- }))?.some((sub) => sub.trialStart !== void 0 && sub.trialStart !== null || sub.trialEnd !== void 0 && sub.trialEnd !== null || sub.status === "trialing") !== true) {
768
- trialStart = /* @__PURE__ */ new Date();
769
- trialEnd = /* @__PURE__ */ new Date();
770
- trialEnd.setDate(trialEnd.getDate() + plan.freeTrial.days);
771
- }
772
- }
744
+ const requestedTrialDays = plan?.freeTrial?.days !== void 0 && plan.freeTrial.days > 0 ? plan.freeTrial.days : 0;
745
+ const trialRequested = requestedTrialDays > 0;
746
+ let trialGranted = false;
747
+ let trialDeniedReason;
748
+ if (trialRequested) if ((await ctx.context.adapter.findMany({
749
+ model: "subscription",
750
+ where: [{
751
+ field: "referenceId",
752
+ value: referenceId
753
+ }]
754
+ }))?.some((sub) => sub.trialStart !== void 0 && sub.trialStart !== null || sub.trialEnd !== void 0 && sub.trialEnd !== null || sub.status === "trialing") === false) {
755
+ trialStart = /* @__PURE__ */ new Date();
756
+ trialEnd = /* @__PURE__ */ new Date();
757
+ trialEnd.setDate(trialEnd.getDate() + requestedTrialDays);
758
+ trialGranted = true;
759
+ } else trialDeniedReason = "already_used";
773
760
  try {
774
- let targetEmail = email !== void 0 && email !== null && email !== "" ? email : user.email;
775
- let paystackCustomerCode = user.paystackCustomerCode;
776
- if (options.organization?.enabled === true && referenceId !== void 0 && referenceId !== null && referenceId !== "" && referenceId !== user.id) {
761
+ let targetEmail = email ?? user.email;
762
+ if (options.organization?.enabled === true && referenceId !== void 0 && referenceId !== null && referenceId !== user.id) {
777
763
  const org = await ctx.context.adapter.findOne({
778
764
  model: "organization",
779
765
  where: [{
@@ -782,8 +768,8 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
782
768
  }]
783
769
  });
784
770
  if (org !== void 0 && org !== null) {
785
- if (org.paystackCustomerCode !== void 0 && org.paystackCustomerCode !== null && org.paystackCustomerCode !== "") paystackCustomerCode = org.paystackCustomerCode;
786
- if (org.email !== void 0 && org.email !== null && org.email !== "") targetEmail = org.email;
771
+ const orgWithEmail = org;
772
+ if (orgWithEmail.email !== void 0 && orgWithEmail.email !== null && orgWithEmail.email !== "") targetEmail = orgWithEmail.email;
787
773
  else {
788
774
  const ownerMember = await ctx.context.adapter.findOne({
789
775
  model: "member",
@@ -803,110 +789,177 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
803
789
  value: ownerMember.userId
804
790
  }]
805
791
  });
806
- if (ownerUser?.email !== void 0 && ownerUser?.email !== null && ownerUser?.email !== "") targetEmail = ownerUser.email;
792
+ if (ownerUser !== void 0 && ownerUser !== null && ownerUser.email !== void 0 && ownerUser.email !== null && ownerUser.email !== "") targetEmail = ownerUser.email;
807
793
  }
808
794
  }
809
795
  }
810
796
  }
797
+ const allowedSubscriptionChannels = plan ? getAllowedSubscriptionChannels(options) : void 0;
811
798
  const metadata = JSON.stringify({
812
799
  referenceId,
813
800
  userId: user.id,
814
- plan: plan?.name.toLowerCase(),
815
- product: product?.name.toLowerCase(),
816
- isTrial: trialStart !== void 0 && trialStart !== null,
817
- trialEnd: trialEnd?.toISOString(),
818
- ...extraMetadata
801
+ plan: plan !== void 0 ? plan.name.toLowerCase() : void 0,
802
+ product: product !== void 0 ? product.name.toLowerCase() : void 0,
803
+ ...extraMetadata,
804
+ isTrial: trialStart !== void 0,
805
+ trialRequested,
806
+ trialGranted,
807
+ trialDeniedReason,
808
+ trialEnd: trialEnd !== void 0 ? trialEnd.toISOString() : void 0
819
809
  });
820
810
  const initBody = {
821
811
  email: targetEmail,
822
- callback_url: callbackURL,
812
+ callback_url: callbackURL ?? void 0,
823
813
  metadata,
824
814
  currency: finalCurrency,
825
815
  quantity
826
816
  };
827
- if (paystackCustomerCode !== void 0 && paystackCustomerCode !== null && paystackCustomerCode !== "") try {
828
- const ops = getPaystackOps(options.paystackClient);
829
- if (initBody.email !== void 0 && initBody.email !== null && initBody.email !== "") await ops.customerUpdate(paystackCustomerCode, { email: initBody.email });
830
- } catch (_e) {}
831
- if (plan !== void 0 && plan !== null && prorateAndCharge === true) {
817
+ if (allowedSubscriptionChannels !== void 0) initBody.channels = allowedSubscriptionChannels;
818
+ if (plan !== void 0 && prorateAndCharge === true) {
832
819
  const existingSub = await getOrganizationSubscription(ctx, referenceId);
833
- if (existingSub?.status === "active" && existingSub.paystackAuthorizationCode !== null && existingSub.paystackAuthorizationCode !== void 0 && existingSub.paystackSubscriptionCode !== null && existingSub.paystackSubscriptionCode !== void 0) {
834
- const now = /* @__PURE__ */ new Date();
835
- const periodEndLocal = existingSub.periodEnd ? new Date(existingSub.periodEnd) : new Date(now.getTime() + 720 * 60 * 60 * 1e3);
836
- const periodStartLocal = existingSub.periodStart ? new Date(existingSub.periodStart) : now;
837
- const totalDays = Math.max(1, Math.ceil((periodEndLocal.getTime() - periodStartLocal.getTime()) / (1e3 * 60 * 60 * 24)));
838
- const remainingDays = Math.max(0, Math.ceil((periodEndLocal.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24)));
839
- let oldAmount = 0;
840
- if (existingSub.plan !== void 0 && existingSub.plan !== null && existingSub.plan !== "") {
841
- const oldPlan = await getPlanByName(options, existingSub.plan) ?? await ctx.context.adapter.findOne({
842
- model: "paystackPlan",
843
- where: [{
844
- field: "name",
845
- value: existingSub.plan
846
- }]
847
- });
848
- if (oldPlan !== null && oldPlan !== void 0) {
849
- const oldSeatCount = existingSub.seats ?? 1;
850
- oldAmount = (oldPlan.amount ?? 0) + oldSeatCount * (oldPlan.seatAmount ?? oldPlan.seatPriceId ?? 0);
820
+ if (existingSub?.status === "active" && existingSub.paystackSubscriptionCode !== void 0 && existingSub.paystackSubscriptionCode !== null && existingSub.paystackSubscriptionCode !== "") {
821
+ if (existingSub.periodEnd !== void 0 && existingSub.periodEnd !== null && existingSub.periodStart !== void 0 && existingSub.periodStart !== null) {
822
+ const now = /* @__PURE__ */ new Date();
823
+ const periodEndLocal = new Date(existingSub.periodEnd);
824
+ const periodStartLocal = new Date(existingSub.periodStart);
825
+ const totalDays = Math.max(1, Math.ceil((periodEndLocal.getTime() - periodStartLocal.getTime()) / (1e3 * 60 * 60 * 24)));
826
+ const remainingDays = Math.max(0, Math.ceil((periodEndLocal.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24)));
827
+ let oldAmount = 0;
828
+ if (existingSub.plan !== "") {
829
+ const oldPlan = await getPlanByName(options, existingSub.plan) ?? await ctx.context.adapter.findOne({
830
+ model: "paystackPlan",
831
+ where: [{
832
+ field: "name",
833
+ value: existingSub.plan
834
+ }]
835
+ }) ?? void 0;
836
+ if (oldPlan !== void 0 && oldPlan !== null) {
837
+ const oldSeatCount = existingSub.seats;
838
+ oldAmount = calculatePlanAmount(oldPlan, oldSeatCount);
839
+ }
851
840
  }
852
- }
853
- let membersCount = 1;
854
- if (plan.seatAmount !== void 0 || plan.seatPriceId !== void 0) {
855
- const members = await ctx.context.adapter.findMany({
856
- model: "member",
841
+ let membersCount = 1;
842
+ let newSeatCount = quantity ?? existingSub.seats ?? membersCount;
843
+ let newAmount;
844
+ try {
845
+ assertLocallyManagedSubscription(existingSub, "plan or seat changes");
846
+ if (getPlanSeatAmount(plan) !== void 0) {
847
+ const members = await ctx.context.adapter.findMany({
848
+ model: "member",
849
+ where: [{
850
+ field: "organizationId",
851
+ value: referenceId
852
+ }]
853
+ });
854
+ membersCount = members.length > 0 ? members.length : 1;
855
+ }
856
+ newSeatCount = quantity ?? existingSub.seats ?? membersCount;
857
+ newAmount = calculatePlanAmount(plan, newSeatCount);
858
+ } catch (error) {
859
+ throw new APIError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Invalid seat configuration for plan." });
860
+ }
861
+ const costDifference = newAmount - oldAmount;
862
+ const prorationMetadata = {
863
+ type: "proration",
864
+ subscriptionId: existingSub.id,
865
+ referenceId,
866
+ newPlan: plan.name.toLowerCase(),
867
+ oldPlan: existingSub.plan,
868
+ newSeatCount,
869
+ remainingDays
870
+ };
871
+ let completedProrationReference;
872
+ if (costDifference > 0 && remainingDays > 0) {
873
+ const proratedAmount = Math.round(costDifference / totalDays * remainingDays);
874
+ if (proratedAmount < 5e3) throw new APIError("BAD_REQUEST", {
875
+ message: "Prorated upgrade amount is below Paystack's minimum charge. Schedule the change for period end instead.",
876
+ status: 400
877
+ });
878
+ const ops = getPaystackOps(options.paystackClient);
879
+ if (ops === void 0 || ops === null) {
880
+ ctx.context.logger.error("Paystack client not configured for proration charge");
881
+ return;
882
+ }
883
+ if (existingSub.paystackAuthorizationCode !== void 0 && existingSub.paystackAuthorizationCode !== null && existingSub.paystackAuthorizationCode !== "") {
884
+ const sdkRes = unwrapSdkResult(await ops.transaction?.chargeAuthorization({ body: {
885
+ email: targetEmail,
886
+ amount: proratedAmount,
887
+ authorization_code: existingSub.paystackAuthorizationCode,
888
+ reference: `upg_${existingSub.id}_${Date.now()}_${Math.random().toString(36).substring(7)}`,
889
+ metadata: JSON.stringify(prorationMetadata)
890
+ } }));
891
+ if (sdkRes?.status !== "success") throw new APIError("BAD_REQUEST", { message: "Failed to process prorated charge via saved authorization." });
892
+ await ctx.context.adapter.create({
893
+ model: "paystackTransaction",
894
+ data: {
895
+ reference: sdkRes.reference ?? "",
896
+ paystackId: sdkRes.id !== void 0 && sdkRes.id !== null ? String(sdkRes.id) : void 0,
897
+ referenceId,
898
+ userId: user.id,
899
+ amount: sdkRes.amount ?? proratedAmount,
900
+ currency: sdkRes.currency ?? finalCurrency,
901
+ status: "success",
902
+ plan: plan.name.toLowerCase(),
903
+ metadata: JSON.stringify(prorationMetadata),
904
+ createdAt: /* @__PURE__ */ new Date(),
905
+ updatedAt: /* @__PURE__ */ new Date()
906
+ }
907
+ });
908
+ completedProrationReference = sdkRes.reference ?? void 0;
909
+ } else {
910
+ const initRes = unwrapSdkResult(await ops.transaction?.initialize({ body: {
911
+ email: targetEmail,
912
+ amount: proratedAmount,
913
+ currency: finalCurrency,
914
+ callback_url: callbackURL ?? void 0,
915
+ metadata: JSON.stringify(prorationMetadata),
916
+ ...allowedSubscriptionChannels !== void 0 ? { channels: allowedSubscriptionChannels } : {}
917
+ } }));
918
+ await ctx.context.adapter.create({
919
+ model: "paystackTransaction",
920
+ data: {
921
+ reference: initRes?.reference ?? "",
922
+ referenceId,
923
+ userId: user.id,
924
+ amount: proratedAmount,
925
+ currency: finalCurrency,
926
+ status: "pending",
927
+ plan: plan.name.toLowerCase(),
928
+ metadata: JSON.stringify(prorationMetadata),
929
+ createdAt: /* @__PURE__ */ new Date(),
930
+ updatedAt: /* @__PURE__ */ new Date()
931
+ }
932
+ });
933
+ return ctx.json({
934
+ url: initRes?.authorization_url,
935
+ reference: initRes?.reference,
936
+ accessCode: initRes?.access_code,
937
+ redirect: true
938
+ });
939
+ }
940
+ }
941
+ await ctx.context.adapter.update({
942
+ model: "subscription",
857
943
  where: [{
858
- field: "organizationId",
859
- value: referenceId
860
- }]
944
+ field: "id",
945
+ value: existingSub.id
946
+ }],
947
+ update: {
948
+ plan: plan.name,
949
+ seats: newSeatCount,
950
+ ...completedProrationReference !== void 0 ? { paystackTransactionReference: completedProrationReference } : {},
951
+ updatedAt: /* @__PURE__ */ new Date()
952
+ }
953
+ });
954
+ return ctx.json({
955
+ status: "success",
956
+ message: "Subscription successfully upgraded with prorated charge.",
957
+ prorated: true
861
958
  });
862
- membersCount = members.length > 0 ? members.length : 1;
863
- }
864
- const newSeatCount = quantity ?? existingSub.seats ?? membersCount;
865
- const newAmount = (plan.amount ?? 0) + newSeatCount * (plan.seatAmount ?? plan.seatPriceId ?? 0);
866
- const costDifference = newAmount - oldAmount;
867
- if (costDifference > 0 && remainingDays > 0) {
868
- const proratedAmount = Math.round(costDifference / totalDays * remainingDays);
869
- if (proratedAmount >= 5e3) {
870
- if (unwrapSdkResult(await getPaystackOps(options.paystackClient).transactionChargeAuthorization({
871
- email: targetEmail,
872
- amount: proratedAmount,
873
- authorization_code: existingSub.paystackAuthorizationCode,
874
- reference: `prorate_${Date.now()}_${Math.random().toString(36).substring(7)}`,
875
- metadata: {
876
- type: "proration",
877
- referenceId,
878
- newPlan: plan.name,
879
- oldPlan: existingSub.plan,
880
- remainingDays
881
- }
882
- }))?.status !== "success") throw new APIError("BAD_REQUEST", { message: "Failed to process prorated charge via saved authorization." });
883
- }
884
959
  }
885
- await getPaystackOps(options.paystackClient).subscriptionUpdate({
886
- code: existingSub.paystackSubscriptionCode,
887
- amount: newAmount,
888
- plan: plan.planCode
889
- });
890
- await ctx.context.adapter.update({
891
- model: "subscription",
892
- where: [{
893
- field: "id",
894
- value: existingSub.id
895
- }],
896
- update: {
897
- plan: plan.name,
898
- seats: newSeatCount,
899
- updatedAt: /* @__PURE__ */ new Date()
900
- }
901
- });
902
- return ctx.json({
903
- status: "success",
904
- message: "Subscription successfully upgraded with prorated charge.",
905
- prorated: true
906
- });
907
960
  }
908
961
  }
909
- if (plan !== void 0 && plan !== null) if (trialStart !== void 0 && trialStart !== null) initBody.amount = 5e3;
962
+ if (plan !== void 0) if (trialStart !== void 0) initBody.amount = 5e3;
910
963
  else {
911
964
  initBody.plan = plan.planCode;
912
965
  initBody.invoice_limit = plan.invoiceLimit;
@@ -918,37 +971,37 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
918
971
  initBody.amount = Math.max(Math.round(finalAmount), 5e3);
919
972
  }
920
973
  else {
921
- if (amount === void 0 || amount === null || amount === 0) throw new APIError("BAD_REQUEST", { message: "Amount is required for one-time payments" });
974
+ if (amount === void 0 || amount === null) throw new APIError("BAD_REQUEST", { message: "Amount is required for one-time payments" });
922
975
  initBody.amount = Math.round(amount);
923
976
  }
924
- const sdkRes = unwrapSdkResult(await paystack.transactionInitialize(initBody));
925
- url = sdkRes?.authorization_url ?? sdkRes?.data?.authorization_url;
926
- reference = sdkRes?.reference ?? sdkRes?.data?.reference;
927
- accessCode = sdkRes?.access_code ?? sdkRes?.data?.access_code;
977
+ const sdkRes = unwrapSdkResult(await paystack?.transaction?.initialize({ body: initBody }));
978
+ url = sdkRes?.authorization_url;
979
+ reference = sdkRes?.reference;
980
+ accessCode = sdkRes?.access_code;
928
981
  } catch (error) {
929
982
  ctx.context.logger.error("Failed to initialize Paystack transaction", error);
930
983
  throw new APIError("BAD_REQUEST", {
931
984
  code: "FAILED_TO_INITIALIZE_TRANSACTION",
932
- message: error?.message ?? PAYSTACK_ERROR_CODES.FAILED_TO_INITIALIZE_TRANSACTION.message
985
+ message: error instanceof Error ? error.message : PAYSTACK_ERROR_CODES.FAILED_TO_INITIALIZE_TRANSACTION.message
933
986
  });
934
987
  }
935
988
  await ctx.context.adapter.create({
936
989
  model: "paystackTransaction",
937
990
  data: {
938
- reference,
991
+ reference: reference ?? "",
939
992
  referenceId,
940
993
  userId: user.id,
941
994
  amount: amount ?? 0,
942
995
  currency: plan?.currency ?? currency ?? "NGN",
943
996
  status: "pending",
944
- plan: plan?.name.toLowerCase(),
945
- product: product?.name.toLowerCase(),
946
- metadata: extraMetadata !== void 0 && extraMetadata !== null && Object.keys(extraMetadata).length > 0 ? JSON.stringify(extraMetadata) : void 0,
997
+ plan: plan !== void 0 ? plan.name.toLowerCase() : void 0,
998
+ product: product !== void 0 ? product.name.toLowerCase() : void 0,
999
+ metadata: extraMetadata !== void 0 && Object.keys(extraMetadata).length > 0 ? JSON.stringify(extraMetadata) : void 0,
947
1000
  createdAt: /* @__PURE__ */ new Date(),
948
1001
  updatedAt: /* @__PURE__ */ new Date()
949
1002
  }
950
1003
  });
951
- if (plan !== void 0 && plan !== null) {
1004
+ if (plan !== void 0) {
952
1005
  let storedCustomerCode = user.paystackCustomerCode;
953
1006
  if (options.organization?.enabled === true && referenceId !== user.id) {
954
1007
  const org = await ctx.context.adapter.findOne({
@@ -958,22 +1011,34 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
958
1011
  value: referenceId
959
1012
  }]
960
1013
  });
961
- if (org?.paystackCustomerCode !== void 0 && org.paystackCustomerCode !== null && org.paystackCustomerCode !== "") storedCustomerCode = org.paystackCustomerCode;
1014
+ if (org !== void 0 && org !== null) {
1015
+ const paystackOrg = org;
1016
+ if (paystackOrg.paystackCustomerCode !== void 0 && paystackOrg.paystackCustomerCode !== null && paystackOrg.paystackCustomerCode !== "") storedCustomerCode = paystackOrg.paystackCustomerCode;
1017
+ }
962
1018
  }
963
1019
  const newSubscription = await ctx.context.adapter.create({
964
1020
  model: "subscription",
965
1021
  data: {
966
1022
  plan: plan.name.toLowerCase(),
967
1023
  referenceId,
968
- paystackCustomerCode: storedCustomerCode,
969
- paystackTransactionReference: reference,
970
- status: trialStart !== void 0 && trialStart !== null ? "trialing" : "incomplete",
971
- seats: quantity,
1024
+ userId: user.id,
1025
+ paystackCustomerCode: storedCustomerCode ?? "",
1026
+ paystackSubscriptionCode: "",
1027
+ paystackPlanCode: plan.planCode,
1028
+ paystackAuthorizationCode: "",
1029
+ paystackTransactionReference: reference ?? "",
1030
+ status: trialStart !== void 0 ? "trialing" : "incomplete",
1031
+ seats: quantity ?? 1,
1032
+ periodStart: /* @__PURE__ */ new Date(),
1033
+ periodEnd: new Date(Date.now() + 720 * 60 * 60 * 1e3),
1034
+ cancelAtPeriodEnd: false,
972
1035
  trialStart,
973
- trialEnd
1036
+ trialEnd,
1037
+ createdAt: /* @__PURE__ */ new Date(),
1038
+ updatedAt: /* @__PURE__ */ new Date()
974
1039
  }
975
1040
  });
976
- if (trialStart !== void 0 && trialStart !== null && newSubscription !== null && plan.freeTrial?.onTrialStart !== void 0 && plan.freeTrial?.onTrialStart !== null) await plan.freeTrial.onTrialStart(newSubscription);
1041
+ if (trialStart !== void 0 && newSubscription !== void 0 && newSubscription !== null && plan.freeTrial?.onTrialStart !== void 0) await plan.freeTrial.onTrialStart(newSubscription);
977
1042
  }
978
1043
  return ctx.json({
979
1044
  url,
@@ -983,15 +1048,11 @@ const initializeTransaction = (options, path = "/paystack/initialize-transaction
983
1048
  });
984
1049
  });
985
1050
  };
986
- const createSubscription = (options) => initializeTransaction(options, "/paystack/create-subscription");
987
- const upgradeSubscription = (options) => initializeTransaction(options, "/paystack/upgrade-subscription");
988
- const restoreSubscription = (options) => {
989
- return enablePaystackSubscription(options, "/paystack/restore-subscription");
990
- };
991
- const cancelSubscription = (options) => {
992
- return disablePaystackSubscription(options, "/paystack/cancel-subscription");
993
- };
994
- const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
1051
+ const createSubscription = (options, path = "/create-subscription") => initializeTransaction(options, path);
1052
+ const upgradeSubscription = (options, path = "/upgrade-subscription") => initializeTransaction(options, path);
1053
+ const cancelSubscription = (options, path = "/cancel-subscription") => disablePaystackSubscription(options, path);
1054
+ const restoreSubscription = (options, path = "/restore-subscription") => enablePaystackSubscription(options, path);
1055
+ const verifyTransaction = (options, path = "/verify-transaction") => {
995
1056
  const verifyBodySchema = z.object({ reference: z.string() });
996
1057
  const subscriptionOptions = options.subscription;
997
1058
  return createAuthEndpoint(path, {
@@ -1004,56 +1065,76 @@ const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
1004
1065
  ] : [sessionMiddleware, originCheck]
1005
1066
  }, async (ctx) => {
1006
1067
  const paystack = getPaystackOps(options.paystackClient);
1007
- let verifyRes;
1068
+ let data;
1008
1069
  try {
1009
- verifyRes = unwrapSdkResult(await paystack.transactionVerify(ctx.body.reference));
1070
+ data = unwrapSdkResult(await paystack?.transaction?.verify(ctx.body.reference));
1010
1071
  } catch (error) {
1011
1072
  ctx.context.logger.error("Failed to verify Paystack transaction", error);
1012
1073
  throw new APIError("BAD_REQUEST", {
1013
1074
  code: "FAILED_TO_VERIFY_TRANSACTION",
1014
- message: error?.message ?? PAYSTACK_ERROR_CODES.FAILED_TO_VERIFY_TRANSACTION.message
1075
+ message: error instanceof Error ? error.message : PAYSTACK_ERROR_CODES.FAILED_TO_VERIFY_TRANSACTION.message
1015
1076
  });
1016
1077
  }
1017
- const dataRaw = unwrapSdkResult(verifyRes);
1018
- const data = dataRaw?.data ?? dataRaw;
1019
- const status = data?.status;
1020
- const reference = data?.reference ?? ctx.body.reference;
1021
- const paystackId = data?.id !== void 0 && data?.id !== null ? String(data.id) : void 0;
1022
- const authorizationCode = (data?.authorization)?.authorization_code;
1078
+ if (data === void 0 || data === null) throw new APIError("BAD_REQUEST", { message: "Failed to fetch transaction data from Paystack." });
1079
+ const status = data.status ?? "failed";
1080
+ const reference = data.reference ?? ctx.body.reference;
1081
+ const paystackIdRaw = data.id;
1082
+ const paystackId = paystackIdRaw !== void 0 && paystackIdRaw !== null ? String(paystackIdRaw) : void 0;
1083
+ const authorizationCode = data.authorization?.authorization_code;
1084
+ const allowedSubscriptionChannels = getAllowedSubscriptionChannels(options);
1023
1085
  if (status === "success") {
1024
1086
  const session = await getSessionFromCtx(ctx);
1025
- const referenceId = (await ctx.context.adapter.findOne({
1087
+ const txRecord = await ctx.context.adapter.findOne({
1026
1088
  model: "paystackTransaction",
1027
1089
  where: [{
1028
1090
  field: "reference",
1029
1091
  value: reference
1030
1092
  }]
1031
- }))?.referenceId ?? (session?.user)?.id;
1032
- if (session !== null && session !== void 0 && referenceId !== session.user.id) {
1093
+ });
1094
+ const referenceId = txRecord !== void 0 && txRecord !== null && txRecord.referenceId !== void 0 && txRecord.referenceId !== null && txRecord.referenceId !== "" ? txRecord.referenceId : session !== void 0 && session !== null ? session.user.id : void 0;
1095
+ if ((txRecord?.plan !== void 0 && txRecord.plan !== null && txRecord.plan !== "" || Boolean(data.plan)) && isAllowedSubscriptionChannel(data.channel ?? void 0, allowedSubscriptionChannels) === false) {
1096
+ await ctx.context.adapter.update({
1097
+ model: "paystackTransaction",
1098
+ update: {
1099
+ status: "failed",
1100
+ paystackId,
1101
+ amount: data.amount,
1102
+ currency: data.currency,
1103
+ updatedAt: /* @__PURE__ */ new Date()
1104
+ },
1105
+ where: [{
1106
+ field: "reference",
1107
+ value: reference
1108
+ }]
1109
+ });
1110
+ throw new APIError("BAD_REQUEST", {
1111
+ code: "SUBSCRIPTION_PAYMENT_CHANNEL_NOT_ALLOWED",
1112
+ message: `This subscription requires one of: ${allowedSubscriptionChannels?.join(", ") ?? "allowed channels"}.`
1113
+ });
1114
+ }
1115
+ if (session !== void 0 && session !== null && referenceId !== void 0 && referenceId !== null && referenceId !== "" && referenceId !== session.user.id) {
1033
1116
  const authRef = subscriptionOptions?.authorizeReference;
1034
1117
  let authorized = false;
1035
- if (authRef !== void 0) authorized = await authRef({
1118
+ if (authRef !== void 0 && authRef !== null) authorized = await authRef({
1036
1119
  user: session.user,
1037
- session,
1120
+ session: session.session,
1038
1121
  referenceId,
1039
1122
  action: "verify-transaction"
1040
1123
  }, ctx);
1041
- if (authorized !== true) {
1042
- if (options.organization?.enabled === true) {
1043
- const member = await ctx.context.adapter.findOne({
1044
- model: "member",
1045
- where: [{
1046
- field: "userId",
1047
- value: session.user.id
1048
- }, {
1049
- field: "organizationId",
1050
- value: referenceId
1051
- }]
1052
- });
1053
- if (member !== void 0 && member !== null) authorized = true;
1054
- }
1124
+ if (authorized === false && options.organization?.enabled === true) {
1125
+ const member = await ctx.context.adapter.findOne({
1126
+ model: "member",
1127
+ where: [{
1128
+ field: "userId",
1129
+ value: session.user.id
1130
+ }, {
1131
+ field: "organizationId",
1132
+ value: referenceId
1133
+ }]
1134
+ });
1135
+ if (member !== void 0 && member !== null) authorized = true;
1055
1136
  }
1056
- if (!authorized) throw new APIError("UNAUTHORIZED");
1137
+ if (authorized === false) throw new APIError("UNAUTHORIZED");
1057
1138
  }
1058
1139
  try {
1059
1140
  await ctx.context.adapter.update({
@@ -1061,8 +1142,8 @@ const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
1061
1142
  update: {
1062
1143
  status: "success",
1063
1144
  paystackId,
1064
- ...data?.amount !== void 0 && data?.amount !== null ? { amount: data.amount } : {},
1065
- ...data?.currency !== void 0 && data?.currency !== null && data?.currency !== "" ? { currency: data.currency } : {},
1145
+ amount: data.amount,
1146
+ currency: data.currency,
1066
1147
  updatedAt: /* @__PURE__ */ new Date()
1067
1148
  },
1068
1149
  where: [{
@@ -1070,11 +1151,10 @@ const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
1070
1151
  value: reference
1071
1152
  }]
1072
1153
  });
1073
- const customer = data?.customer;
1074
- const paystackCustomerCodeFromPaystack = customer !== void 0 && customer !== null && typeof customer === "object" ? customer.customer_code : void 0;
1154
+ const paystackCustomerCodeFromPaystack = data.customer?.customer_code;
1075
1155
  if (paystackCustomerCodeFromPaystack !== void 0 && paystackCustomerCodeFromPaystack !== null && paystackCustomerCodeFromPaystack !== "" && referenceId !== void 0 && referenceId !== null && referenceId !== "") {
1076
1156
  let isOrg = options.organization?.enabled === true && typeof referenceId === "string" && referenceId.startsWith("org_");
1077
- if (!isOrg && options.organization?.enabled === true) {
1157
+ if (isOrg === false && options.organization?.enabled === true) {
1078
1158
  const org = await ctx.context.adapter.findOne({
1079
1159
  model: "organization",
1080
1160
  where: [{
@@ -1082,9 +1162,9 @@ const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
1082
1162
  value: referenceId
1083
1163
  }]
1084
1164
  });
1085
- isOrg = org !== null && org !== void 0;
1165
+ isOrg = org !== void 0 && org !== null;
1086
1166
  }
1087
- if (isOrg === true) await ctx.context.adapter.update({
1167
+ if (isOrg) await ctx.context.adapter.update({
1088
1168
  model: "organization",
1089
1169
  update: { paystackCustomerCode: paystackCustomerCodeFromPaystack },
1090
1170
  where: [{
@@ -1108,45 +1188,64 @@ const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
1108
1188
  value: reference
1109
1189
  }]
1110
1190
  });
1111
- if (transaction?.product !== void 0 && transaction?.product !== null && transaction?.product !== "") await syncProductQuantityFromPaystack(ctx, transaction.product, options.paystackClient);
1191
+ if (transaction !== void 0 && transaction !== null && transaction.product !== void 0 && transaction.product !== null && transaction.product !== "" && options.paystackClient !== void 0 && options.paystackClient !== null) await syncProductQuantityFromPaystack(ctx, transaction.product, options.paystackClient);
1112
1192
  let isTrial = false;
1113
1193
  let trialEnd;
1114
1194
  let targetPlan;
1115
- if (data?.metadata !== void 0 && data?.metadata !== null) {
1116
- const metaRaw = data.metadata;
1117
- const meta = typeof metaRaw === "string" ? JSON.parse(metaRaw) : metaRaw;
1118
- isTrial = meta.isTrial === true || meta.isTrial === "true";
1119
- trialEnd = meta.trialEnd;
1120
- targetPlan = meta.plan;
1195
+ let metadataObj = {};
1196
+ if (data.metadata !== void 0 && data.metadata !== null && data.metadata !== "") {
1197
+ metadataObj = typeof data.metadata === "string" ? JSON.parse(data.metadata) : data.metadata;
1198
+ isTrial = metadataObj.isTrial === true || metadataObj.isTrial === "true";
1199
+ trialEnd = metadataObj.trialEnd;
1200
+ targetPlan = metadataObj.plan;
1201
+ }
1202
+ if (metadataObj.type === "proration") {
1203
+ const subscriptionId = metadataObj.subscriptionId;
1204
+ const newPlan = metadataObj.newPlan;
1205
+ const newSeatCount = metadataObj.newSeatCount;
1206
+ if (subscriptionId !== void 0 && subscriptionId !== "" && newPlan !== void 0 && newPlan !== "") await ctx.context.adapter.update({
1207
+ model: "subscription",
1208
+ update: {
1209
+ plan: newPlan,
1210
+ ...typeof newSeatCount === "number" ? { seats: newSeatCount } : {},
1211
+ paystackTransactionReference: reference,
1212
+ ...authorizationCode !== void 0 && authorizationCode !== null ? { paystackAuthorizationCode: authorizationCode } : {},
1213
+ updatedAt: /* @__PURE__ */ new Date()
1214
+ },
1215
+ where: [{
1216
+ field: "id",
1217
+ value: subscriptionId
1218
+ }]
1219
+ });
1220
+ return ctx.json({
1221
+ status,
1222
+ reference,
1223
+ data
1224
+ });
1121
1225
  }
1122
1226
  let paystackSubscriptionCode;
1123
- if (isTrial === true && targetPlan !== void 0 && targetPlan !== null && targetPlan !== "" && trialEnd !== void 0 && trialEnd !== null && trialEnd !== "") {
1227
+ if (isTrial && targetPlan !== void 0 && trialEnd !== void 0) {
1124
1228
  const email = data.customer?.email;
1125
1229
  const planConfig = (await getPlans(subscriptionOptions)).find((p) => p.name.toLowerCase() === targetPlan?.toLowerCase());
1126
1230
  if (planConfig !== void 0 && planConfig !== null && (planConfig.planCode === void 0 || planConfig.planCode === null || planConfig.planCode === "")) paystackSubscriptionCode = `LOC_${reference}`;
1127
- if (authorizationCode !== void 0 && authorizationCode !== null && authorizationCode !== "" && email !== void 0 && email !== null && email !== "" && planConfig?.planCode !== void 0 && planConfig.planCode !== null && planConfig.planCode !== "") {
1128
- const subRes = unwrapSdkResult(await paystack.subscriptionCreate({
1129
- customer: email,
1130
- plan: planConfig.planCode,
1131
- authorization: authorizationCode,
1132
- start_date: trialEnd
1133
- }));
1134
- paystackSubscriptionCode = (subRes?.data ?? subRes)?.subscription_code;
1135
- }
1136
- } else if (isTrial !== true) {
1137
- const planCodeFromPaystack = (data?.plan)?.plan_code;
1231
+ if (authorizationCode !== void 0 && authorizationCode !== null && email !== void 0 && email !== null && email !== "" && planConfig?.planCode !== void 0 && planConfig.planCode !== null && planConfig.planCode !== "") paystackSubscriptionCode = unwrapSdkResult(await paystack?.subscription?.create({ body: {
1232
+ customer: email,
1233
+ plan: planConfig.planCode,
1234
+ authorization: authorizationCode,
1235
+ start_date: trialEnd
1236
+ } }))?.subscription_code;
1237
+ } else if (isTrial === false) {
1238
+ const planCodeFromPaystack = data.plan?.plan_code;
1138
1239
  if (planCodeFromPaystack === void 0 || planCodeFromPaystack === null || planCodeFromPaystack === "") paystackSubscriptionCode = `LOC_${reference}`;
1139
- else paystackSubscriptionCode = (data?.subscription)?.subscription_code;
1240
+ else paystackSubscriptionCode = data.subscription?.subscription_code ?? void 0;
1140
1241
  }
1141
- const existingSubs = await ctx.context.adapter.findMany({
1242
+ const targetSub = (await ctx.context.adapter.findMany({
1142
1243
  model: "subscription",
1143
1244
  where: [{
1144
1245
  field: "paystackTransactionReference",
1145
1246
  value: reference
1146
1247
  }]
1147
- });
1148
- let targetSub;
1149
- if (existingSubs !== null && existingSubs !== void 0 && existingSubs.length > 0) targetSub = existingSubs.find((s) => referenceId === void 0 || referenceId === null || referenceId === "" || s.referenceId === referenceId);
1248
+ }))?.find((s) => referenceId === void 0 || referenceId === null || referenceId === "" || s.referenceId === referenceId);
1150
1249
  let updatedSubscription = null;
1151
1250
  if (targetSub !== void 0 && targetSub !== null) updatedSubscription = await ctx.context.adapter.update({
1152
1251
  model: "subscription",
@@ -1154,22 +1253,22 @@ const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
1154
1253
  status: isTrial ? "trialing" : "active",
1155
1254
  periodStart: /* @__PURE__ */ new Date(),
1156
1255
  updatedAt: /* @__PURE__ */ new Date(),
1157
- ...isTrial === true && trialEnd !== void 0 && trialEnd !== null ? {
1256
+ ...isTrial && trialEnd !== void 0 ? {
1158
1257
  trialStart: /* @__PURE__ */ new Date(),
1159
1258
  trialEnd: new Date(trialEnd),
1160
1259
  periodEnd: new Date(trialEnd)
1161
1260
  } : {},
1162
- ...paystackSubscriptionCode !== void 0 && paystackSubscriptionCode !== null && paystackSubscriptionCode !== "" ? { paystackSubscriptionCode } : {},
1163
- ...authorizationCode !== void 0 && authorizationCode !== null && authorizationCode !== "" ? { paystackAuthorizationCode: authorizationCode } : {}
1261
+ ...paystackSubscriptionCode !== void 0 ? { paystackSubscriptionCode } : {},
1262
+ ...authorizationCode !== void 0 && authorizationCode !== null ? { paystackAuthorizationCode: authorizationCode } : {}
1164
1263
  },
1165
1264
  where: [{
1166
1265
  field: "id",
1167
1266
  value: targetSub.id
1168
1267
  }]
1169
1268
  });
1170
- if (updatedSubscription && subscriptionOptions?.enabled === true && "onSubscriptionComplete" in subscriptionOptions && typeof subscriptionOptions.onSubscriptionComplete === "function") {
1269
+ if (updatedSubscription !== void 0 && updatedSubscription !== null && subscriptionOptions?.onSubscriptionComplete !== void 0) {
1171
1270
  const plan = (await getPlans(subscriptionOptions)).find((p) => p.name.toLowerCase() === updatedSubscription.plan.toLowerCase());
1172
- if (plan) await subscriptionOptions.onSubscriptionComplete({
1271
+ if (plan !== void 0) await subscriptionOptions.onSubscriptionComplete({
1173
1272
  event: data,
1174
1273
  subscription: updatedSubscription,
1175
1274
  plan
@@ -1186,10 +1285,10 @@ const verifyTransaction = (options, path = "/paystack/verify-transaction") => {
1186
1285
  });
1187
1286
  });
1188
1287
  };
1189
- const listSubscriptions = (options) => {
1288
+ const listSubscriptions = (options, path = "/list-subscriptions") => {
1190
1289
  const listQuerySchema = z.object({ referenceId: z.string().optional() });
1191
1290
  const subscriptionOptions = options.subscription;
1192
- return createAuthEndpoint("/paystack/list-subscriptions", {
1291
+ return createAuthEndpoint(path, {
1193
1292
  method: "GET",
1194
1293
  query: listQuerySchema,
1195
1294
  use: subscriptionOptions?.enabled === true ? [
@@ -1200,7 +1299,7 @@ const listSubscriptions = (options) => {
1200
1299
  }, async (ctx) => {
1201
1300
  if (subscriptionOptions?.enabled !== true) throw new APIError("BAD_REQUEST", { message: "Subscriptions are not enabled in the Paystack options." });
1202
1301
  const session = await getSessionFromCtx(ctx);
1203
- if (!session) throw new APIError("UNAUTHORIZED");
1302
+ if (session === void 0 || session === null) throw new APIError("UNAUTHORIZED");
1204
1303
  const referenceIdPart = ctx.context.referenceId;
1205
1304
  const queryRefId = ctx.query?.referenceId;
1206
1305
  const referenceId = referenceIdPart ?? queryRefId ?? session.user.id;
@@ -1214,7 +1313,7 @@ const listSubscriptions = (options) => {
1214
1313
  return ctx.json({ subscriptions: res });
1215
1314
  });
1216
1315
  };
1217
- const listTransactions = (options, path = "/paystack/list-transactions") => {
1316
+ const listTransactions = (options, path = "/list-transactions") => {
1218
1317
  return createAuthEndpoint(path, {
1219
1318
  method: "GET",
1220
1319
  query: z.object({ referenceId: z.string().optional() }),
@@ -1225,15 +1324,17 @@ const listTransactions = (options, path = "/paystack/list-transactions") => {
1225
1324
  ] : [sessionMiddleware, originCheck]
1226
1325
  }, async (ctx) => {
1227
1326
  const session = await getSessionFromCtx(ctx);
1228
- if (!session) throw new APIError("UNAUTHORIZED");
1229
- const referenceId = ctx.context.referenceId ?? ctx.query?.referenceId ?? session.user.id;
1327
+ if (session === void 0 || session === null) throw new APIError("UNAUTHORIZED");
1328
+ const referenceIdPart = ctx.context.referenceId;
1329
+ const queryRefId = ctx.query?.referenceId;
1330
+ const referenceId = referenceIdPart ?? queryRefId ?? session.user.id;
1230
1331
  const sorted = (await ctx.context.adapter.findMany({
1231
1332
  model: "paystackTransaction",
1232
1333
  where: [{
1233
1334
  field: "referenceId",
1234
1335
  value: referenceId
1235
1336
  }]
1236
- })).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
1337
+ })).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
1237
1338
  return ctx.json({ transactions: sorted });
1238
1339
  });
1239
1340
  };
@@ -1246,8 +1347,10 @@ const enableDisableBodySchema = z.object({
1246
1347
  function decodeBase64UrlToString(value) {
1247
1348
  const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
1248
1349
  const padded = normalized + "===".slice((normalized.length + 3) % 4);
1249
- if (typeof globalThis.atob === "function") return globalThis.atob(padded);
1250
- return Buffer.from(padded, "base64").toString("utf8");
1350
+ const binaryString = atob(padded);
1351
+ const bytes = new Uint8Array(binaryString.length);
1352
+ for (let i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i);
1353
+ return new TextDecoder().decode(bytes);
1251
1354
  }
1252
1355
  function tryGetEmailTokenFromSubscriptionManageLink(link) {
1253
1356
  try {
@@ -1257,12 +1360,12 @@ function tryGetEmailTokenFromSubscriptionManageLink(link) {
1257
1360
  if (parts.length < 2) return void 0;
1258
1361
  const payloadJson = decodeBase64UrlToString(parts[1]);
1259
1362
  const payload = JSON.parse(payloadJson);
1260
- return typeof payload?.email_token === "string" ? payload.email_token : void 0;
1363
+ return typeof payload.email_token === "string" ? payload.email_token : void 0;
1261
1364
  } catch {
1262
1365
  return;
1263
1366
  }
1264
1367
  }
1265
- const disablePaystackSubscription = (options, path = "/paystack/disable-subscription") => {
1368
+ const disablePaystackSubscription = (options, path = "/disable-subscription") => {
1266
1369
  return createAuthEndpoint(path, {
1267
1370
  method: "POST",
1268
1371
  body: enableDisableBodySchema,
@@ -1275,7 +1378,7 @@ const disablePaystackSubscription = (options, path = "/paystack/disable-subscrip
1275
1378
  const { subscriptionCode, atPeriodEnd } = ctx.body;
1276
1379
  const paystack = getPaystackOps(options.paystackClient);
1277
1380
  try {
1278
- if (subscriptionCode.startsWith("LOC_")) {
1381
+ if (isLocalSubscriptionCode(subscriptionCode)) {
1279
1382
  const sub = await ctx.context.adapter.findOne({
1280
1383
  model: "subscription",
1281
1384
  where: [{
@@ -1303,22 +1406,21 @@ const disablePaystackSubscription = (options, path = "/paystack/disable-subscrip
1303
1406
  let emailToken = ctx.body.emailToken;
1304
1407
  let nextPaymentDate;
1305
1408
  try {
1306
- const fetchRes = unwrapSdkResult(await paystack.subscriptionFetch(subscriptionCode));
1307
- const data = fetchRes?.data ?? fetchRes;
1308
- if (emailToken === void 0 || emailToken === null || emailToken === "") emailToken = data?.email_token;
1309
- nextPaymentDate = data?.next_payment_date;
1409
+ const fetchRes = unwrapSdkResult(await paystack?.subscription?.fetch(subscriptionCode));
1410
+ if (fetchRes !== void 0 && fetchRes !== null) {
1411
+ emailToken ??= fetchRes.email_token ?? void 0;
1412
+ nextPaymentDate = fetchRes.next_payment_date ?? void 0;
1413
+ }
1310
1414
  } catch {}
1311
1415
  if (emailToken === void 0 || emailToken === null || emailToken === "") try {
1312
- const linkRes = unwrapSdkResult(await paystack.subscriptionManageLink(subscriptionCode));
1313
- const data = linkRes?.data ?? linkRes;
1314
- const link = typeof data === "string" ? data : data.link;
1315
- if (typeof link === "string" && link !== "") emailToken = tryGetEmailTokenFromSubscriptionManageLink(link);
1416
+ const link = unwrapSdkResult(await paystack?.subscription?.manageLink(subscriptionCode))?.link;
1417
+ if (link !== void 0 && link !== null && link !== "") emailToken = tryGetEmailTokenFromSubscriptionManageLink(link);
1316
1418
  } catch {}
1317
1419
  if (emailToken === void 0 || emailToken === null || emailToken === "") throw new Error("Could not retrieve email_token for subscription disable.");
1318
- await paystack.subscriptionDisable({
1420
+ await paystack?.subscription?.disable({ body: {
1319
1421
  code: subscriptionCode,
1320
1422
  token: emailToken
1321
- });
1423
+ } });
1322
1424
  const periodEnd = nextPaymentDate !== void 0 && nextPaymentDate !== null && nextPaymentDate !== "" ? new Date(nextPaymentDate) : void 0;
1323
1425
  const sub = await ctx.context.adapter.findOne({
1324
1426
  model: "subscription",
@@ -1346,12 +1448,12 @@ const disablePaystackSubscription = (options, path = "/paystack/disable-subscrip
1346
1448
  ctx.context.logger.error("Failed to disable subscription", error);
1347
1449
  throw new APIError("BAD_REQUEST", {
1348
1450
  code: "FAILED_TO_DISABLE_SUBSCRIPTION",
1349
- message: error?.message ?? PAYSTACK_ERROR_CODES.FAILED_TO_DISABLE_SUBSCRIPTION.message
1451
+ message: error instanceof Error ? error.message : PAYSTACK_ERROR_CODES.FAILED_TO_DISABLE_SUBSCRIPTION.message
1350
1452
  });
1351
1453
  }
1352
1454
  });
1353
1455
  };
1354
- const enablePaystackSubscription = (options, path = "/paystack/enable-subscription") => {
1456
+ const enablePaystackSubscription = (options, path = "/enable-subscription") => {
1355
1457
  return createAuthEndpoint(path, {
1356
1458
  method: "POST",
1357
1459
  body: enableDisableBodySchema,
@@ -1366,20 +1468,18 @@ const enablePaystackSubscription = (options, path = "/paystack/enable-subscripti
1366
1468
  try {
1367
1469
  let emailToken = ctx.body.emailToken;
1368
1470
  if (emailToken === void 0 || emailToken === null || emailToken === "") try {
1369
- const fetchRes = unwrapSdkResult(await paystack.subscriptionFetch(subscriptionCode));
1370
- emailToken = (fetchRes?.data ?? fetchRes)?.email_token;
1471
+ const fetchRes = unwrapSdkResult(await paystack?.subscription?.fetch(subscriptionCode));
1472
+ if (fetchRes !== void 0 && fetchRes !== null) emailToken = fetchRes.email_token ?? void 0;
1371
1473
  } catch {}
1372
1474
  if (emailToken === void 0 || emailToken === null || emailToken === "") try {
1373
- const linkRes = unwrapSdkResult(await paystack.subscriptionManageLink(subscriptionCode));
1374
- const data = linkRes?.data ?? linkRes;
1375
- const link = typeof data === "string" ? data : data.link;
1376
- if (typeof link === "string" && link !== "") emailToken = tryGetEmailTokenFromSubscriptionManageLink(link);
1475
+ const link = unwrapSdkResult(await paystack?.subscription?.manageLink(subscriptionCode))?.link;
1476
+ if (link !== void 0 && link !== null && link !== "") emailToken = tryGetEmailTokenFromSubscriptionManageLink(link);
1377
1477
  } catch {}
1378
1478
  if (emailToken === void 0 || emailToken === null || emailToken === "") throw new APIError("BAD_REQUEST", { message: "Could not retrieve email_token for subscription enable." });
1379
- await paystack.subscriptionEnable({
1479
+ await paystack?.subscription?.enable({ body: {
1380
1480
  code: subscriptionCode,
1381
1481
  token: emailToken
1382
- });
1482
+ } });
1383
1483
  await ctx.context.adapter.update({
1384
1484
  model: "subscription",
1385
1485
  update: {
@@ -1396,12 +1496,12 @@ const enablePaystackSubscription = (options, path = "/paystack/enable-subscripti
1396
1496
  ctx.context.logger.error("Failed to enable subscription", error);
1397
1497
  throw new APIError("BAD_REQUEST", {
1398
1498
  code: "FAILED_TO_ENABLE_SUBSCRIPTION",
1399
- message: error?.message ?? PAYSTACK_ERROR_CODES.FAILED_TO_ENABLE_SUBSCRIPTION.message
1499
+ message: error instanceof Error ? error.message : PAYSTACK_ERROR_CODES.FAILED_TO_ENABLE_SUBSCRIPTION.message
1400
1500
  });
1401
1501
  }
1402
1502
  });
1403
1503
  };
1404
- const getSubscriptionManageLink = (options, path = "/paystack/get-subscription-manage-link") => {
1504
+ const getSubscriptionManageLink = (options, path = "/subscription-manage-link") => {
1405
1505
  const manageLinkQuerySchema = z.object({ subscriptionCode: z.string() });
1406
1506
  const useMiddlewares = options.subscription?.enabled === true ? [
1407
1507
  sessionMiddleware,
@@ -1410,19 +1510,17 @@ const getSubscriptionManageLink = (options, path = "/paystack/get-subscription-m
1410
1510
  ] : [sessionMiddleware, originCheck];
1411
1511
  const handler = async (ctx) => {
1412
1512
  const { subscriptionCode } = ctx.query;
1413
- if (subscriptionCode.startsWith("LOC_") || subscriptionCode.startsWith("sub_local_")) return ctx.json({
1513
+ if (isLocalSubscriptionCode(subscriptionCode)) return ctx.json({
1414
1514
  link: null,
1415
1515
  message: "Local subscriptions cannot be managed on Paystack"
1416
1516
  });
1417
1517
  const paystack = getPaystackOps(options.paystackClient);
1418
1518
  try {
1419
- const res = unwrapSdkResult(await paystack.subscriptionManageLink(subscriptionCode));
1420
- const data = res?.data ?? res;
1421
- const link = typeof data === "string" ? data : data.link;
1422
- return ctx.json({ link: typeof link === "string" ? link : null });
1519
+ const res = unwrapSdkResult(await paystack?.subscription?.manageLink(subscriptionCode));
1520
+ return ctx.json({ link: res?.link || null });
1423
1521
  } catch (error) {
1424
1522
  ctx.context.logger.error("Failed to get subscription manage link", error);
1425
- throw new APIError("BAD_REQUEST", { message: error?.message ?? "Failed to get subscription manage link" });
1523
+ throw new APIError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Failed to get subscription manage link" });
1426
1524
  }
1427
1525
  };
1428
1526
  return createAuthEndpoint(path, {
@@ -1431,69 +1529,8 @@ const getSubscriptionManageLink = (options, path = "/paystack/get-subscription-m
1431
1529
  use: useMiddlewares
1432
1530
  }, handler);
1433
1531
  };
1434
- const syncProducts = (options) => {
1435
- return createAuthEndpoint("/paystack/sync-products", {
1436
- method: "POST",
1437
- metadata: { ...HIDE_METADATA },
1438
- disableBody: true,
1439
- use: [sessionMiddleware]
1440
- }, async (ctx) => {
1441
- const paystack = getPaystackOps(options.paystackClient);
1442
- try {
1443
- const dataRaw = unwrapSdkResult(await paystack.productList());
1444
- const productsDataRaw = dataRaw?.data ?? dataRaw;
1445
- if (!Array.isArray(productsDataRaw)) return ctx.json({ products: [] });
1446
- const productsData = productsDataRaw;
1447
- for (const productRaw of productsData) {
1448
- const product = productRaw;
1449
- const paystackId = String(product.id);
1450
- const existing = await ctx.context.adapter.findOne({
1451
- model: "paystackProduct",
1452
- where: [{
1453
- field: "paystackId",
1454
- value: paystackId
1455
- }]
1456
- });
1457
- const productFields = {
1458
- name: typeof product.name === "string" ? product.name : "",
1459
- description: typeof product.description === "string" ? product.description : "",
1460
- price: typeof product.price === "number" ? product.price : 0,
1461
- currency: typeof product.currency === "string" ? product.currency : "",
1462
- quantity: typeof product.quantity === "number" ? product.quantity : 0,
1463
- unlimited: product.unlimited === true,
1464
- paystackId,
1465
- slug: typeof product.slug === "string" && product.slug !== "" ? product.slug : typeof product.name === "string" ? product.name.toLowerCase().replace(/\s+/g, "-") : "",
1466
- metadata: product.metadata !== void 0 && product.metadata !== null ? JSON.stringify(product.metadata) : void 0,
1467
- updatedAt: /* @__PURE__ */ new Date()
1468
- };
1469
- if (existing !== null && existing !== void 0) await ctx.context.adapter.update({
1470
- model: "paystackProduct",
1471
- update: productFields,
1472
- where: [{
1473
- field: "id",
1474
- value: existing.id
1475
- }]
1476
- });
1477
- else await ctx.context.adapter.create({
1478
- model: "paystackProduct",
1479
- data: {
1480
- ...productFields,
1481
- createdAt: /* @__PURE__ */ new Date()
1482
- }
1483
- });
1484
- }
1485
- return ctx.json({
1486
- status: "success",
1487
- count: productsData.length
1488
- });
1489
- } catch (error) {
1490
- ctx.context.logger.error("Failed to sync products", error);
1491
- throw new APIError("BAD_REQUEST", { message: error?.message ?? "Failed to sync products" });
1492
- }
1493
- });
1494
- };
1495
- const listProducts = (_options) => {
1496
- return createAuthEndpoint("/paystack/list-products", {
1532
+ const listProducts = (_options, path = "/list-products") => {
1533
+ return createAuthEndpoint(path, {
1497
1534
  method: "GET",
1498
1535
  metadata: { openapi: { operationId: "listPaystackProducts" } }
1499
1536
  }, async (ctx) => {
@@ -1501,69 +1538,8 @@ const listProducts = (_options) => {
1501
1538
  return ctx.json({ products: sorted });
1502
1539
  });
1503
1540
  };
1504
- const syncPlans = (options) => {
1505
- return createAuthEndpoint("/paystack/sync-plans", {
1506
- method: "POST",
1507
- metadata: { ...HIDE_METADATA },
1508
- disableBody: true,
1509
- use: [sessionMiddleware]
1510
- }, async (ctx) => {
1511
- const paystack = getPaystackOps(options.paystackClient);
1512
- try {
1513
- const res = unwrapSdkResult(await paystack.planList());
1514
- const plansData = res?.data ?? res;
1515
- if (!Array.isArray(plansData)) return ctx.json({
1516
- status: "success",
1517
- count: 0
1518
- });
1519
- for (const plan of plansData) {
1520
- const paystackId = String(plan.id);
1521
- const existing = await ctx.context.adapter.findOne({
1522
- model: "paystackPlan",
1523
- where: [{
1524
- field: "paystackId",
1525
- value: paystackId
1526
- }]
1527
- });
1528
- const planData = {
1529
- name: plan.name,
1530
- description: plan.description,
1531
- amount: plan.amount,
1532
- currency: plan.currency,
1533
- interval: plan.interval,
1534
- planCode: plan.plan_code,
1535
- paystackId,
1536
- metadata: plan.metadata !== void 0 && plan.metadata !== null ? JSON.stringify(plan.metadata) : void 0,
1537
- updatedAt: /* @__PURE__ */ new Date()
1538
- };
1539
- if (existing !== void 0 && existing !== null) await ctx.context.adapter.update({
1540
- model: "paystackPlan",
1541
- update: planData,
1542
- where: [{
1543
- field: "id",
1544
- value: existing.id
1545
- }]
1546
- });
1547
- else await ctx.context.adapter.create({
1548
- model: "paystackPlan",
1549
- data: {
1550
- ...planData,
1551
- createdAt: /* @__PURE__ */ new Date()
1552
- }
1553
- });
1554
- }
1555
- return ctx.json({
1556
- status: "success",
1557
- count: plansData.length
1558
- });
1559
- } catch (error) {
1560
- ctx.context.logger.error("Failed to sync plans", error);
1561
- throw new APIError("BAD_REQUEST", { message: error?.message ?? "Failed to sync plans" });
1562
- }
1563
- });
1564
- };
1565
- const listPlans = (_options) => {
1566
- return createAuthEndpoint("/paystack/list-plans", {
1541
+ const listPlans = (_options, path = "/list-plans") => {
1542
+ return createAuthEndpoint(path, {
1567
1543
  method: "GET",
1568
1544
  metadata: { ...HIDE_METADATA },
1569
1545
  use: [sessionMiddleware]
@@ -1573,12 +1549,12 @@ const listPlans = (_options) => {
1573
1549
  return ctx.json({ plans });
1574
1550
  } catch (error) {
1575
1551
  ctx.context.logger.error("Failed to list plans", error);
1576
- throw new APIError("BAD_REQUEST", { message: error?.message ?? "Failed to list plans" });
1552
+ throw new APIError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Failed to list plans" });
1577
1553
  }
1578
1554
  });
1579
1555
  };
1580
- const getConfig = (options) => {
1581
- return createAuthEndpoint("/paystack/get-config", {
1556
+ const getConfig = (options, path = "/get-config") => {
1557
+ return createAuthEndpoint(path, {
1582
1558
  method: "GET",
1583
1559
  metadata: { openapi: { operationId: "getPaystackConfig" } }
1584
1560
  }, async (ctx) => {
@@ -1590,103 +1566,6 @@ const getConfig = (options) => {
1590
1566
  });
1591
1567
  });
1592
1568
  };
1593
- const chargeRecurringSubscription = (options) => {
1594
- return createAuthEndpoint("/paystack/charge-recurring", {
1595
- method: "POST",
1596
- body: z.object({
1597
- subscriptionId: z.string(),
1598
- amount: z.number().optional()
1599
- })
1600
- }, async (ctx) => {
1601
- const { subscriptionId, amount: bodyAmount } = ctx.body;
1602
- const subscription = await ctx.context.adapter.findOne({
1603
- model: "subscription",
1604
- where: [{
1605
- field: "id",
1606
- value: subscriptionId
1607
- }]
1608
- });
1609
- if (subscription === void 0 || subscription === null) throw new APIError("NOT_FOUND", { message: "Subscription not found" });
1610
- if (subscription.paystackAuthorizationCode === void 0 || subscription.paystackAuthorizationCode === null || subscription.paystackAuthorizationCode === "") throw new APIError("BAD_REQUEST", { message: "No authorization code found for this subscription" });
1611
- const plan = (await getPlans(options.subscription)).find((p) => p.name.toLowerCase() === subscription.plan.toLowerCase());
1612
- if (!plan) throw new APIError("NOT_FOUND", { message: "Plan not found" });
1613
- const amount = bodyAmount ?? plan.amount;
1614
- if (amount === void 0 || amount === null) throw new APIError("BAD_REQUEST", { message: "Plan amount is not defined" });
1615
- let email;
1616
- if (subscription.referenceId !== void 0 && subscription.referenceId !== null && subscription.referenceId !== "") {
1617
- const user = await ctx.context.adapter.findOne({
1618
- model: "user",
1619
- where: [{
1620
- field: "id",
1621
- value: subscription.referenceId
1622
- }]
1623
- });
1624
- if (user !== void 0 && user !== null) email = user.email;
1625
- else if (options.organization?.enabled === true) {
1626
- const ownerMember = await ctx.context.adapter.findOne({
1627
- model: "member",
1628
- where: [{
1629
- field: "organizationId",
1630
- value: subscription.referenceId
1631
- }, {
1632
- field: "role",
1633
- value: "owner"
1634
- }]
1635
- });
1636
- if (ownerMember !== void 0 && ownerMember !== null) email = (await ctx.context.adapter.findOne({
1637
- model: "user",
1638
- where: [{
1639
- field: "id",
1640
- value: ownerMember.userId
1641
- }]
1642
- }))?.email;
1643
- }
1644
- }
1645
- if (email === void 0 || email === null || email === "") throw new APIError("NOT_FOUND", { message: "User email not found" });
1646
- const finalCurrency = plan.currency ?? "NGN";
1647
- if (!validateMinAmount(amount, finalCurrency)) throw new APIError("BAD_REQUEST", {
1648
- message: `Amount ${amount} is less than the minimum required for ${finalCurrency}.`,
1649
- status: 400
1650
- });
1651
- const dataRaw = unwrapSdkResult(await getPaystackOps(options.paystackClient).transactionChargeAuthorization({
1652
- email,
1653
- amount,
1654
- authorization_code: subscription.paystackAuthorizationCode,
1655
- currency: plan.currency,
1656
- metadata: {
1657
- subscriptionId,
1658
- referenceId: subscription.referenceId,
1659
- plan: plan.name
1660
- }
1661
- }));
1662
- const chargeData = dataRaw?.data ?? dataRaw;
1663
- if (chargeData?.status === "success" || dataRaw?.status === "success") {
1664
- const now = /* @__PURE__ */ new Date();
1665
- const nextPeriodEnd = getNextPeriodEnd(now, plan.interval ?? "monthly");
1666
- await ctx.context.adapter.update({
1667
- model: "subscription",
1668
- update: {
1669
- periodStart: now,
1670
- periodEnd: nextPeriodEnd,
1671
- updatedAt: now,
1672
- paystackTransactionReference: chargeData?.reference ?? dataRaw?.reference
1673
- },
1674
- where: [{
1675
- field: "id",
1676
- value: subscription.id
1677
- }]
1678
- });
1679
- return ctx.json({
1680
- status: "success",
1681
- data: chargeData
1682
- });
1683
- }
1684
- return ctx.json({
1685
- status: "failed",
1686
- data: chargeData
1687
- }, { status: 400 });
1688
- });
1689
- };
1690
1569
  //#endregion
1691
1570
  //#region src/schema.ts
1692
1571
  const transactions = { paystackTransaction: { fields: {
@@ -1950,32 +1829,259 @@ const getSchema = (options) => {
1950
1829
  return mergeSchema(baseSchema, options.schema);
1951
1830
  };
1952
1831
  //#endregion
1832
+ //#region src/operations.ts
1833
+ async function syncPaystackProducts(ctx, options) {
1834
+ const paystack = getPaystackOps(options.paystackClient);
1835
+ try {
1836
+ const productsData = unwrapSdkResult(await paystack?.product?.list({}));
1837
+ if (!Array.isArray(productsData)) return {
1838
+ status: "success",
1839
+ count: 0
1840
+ };
1841
+ for (const product of productsData) {
1842
+ const paystackId = String(product.id);
1843
+ const existing = await ctx.context.adapter.findOne({
1844
+ model: "paystackProduct",
1845
+ where: [{
1846
+ field: "paystackId",
1847
+ value: paystackId
1848
+ }]
1849
+ });
1850
+ const productFields = {
1851
+ name: product.name ?? "",
1852
+ description: product.description ?? "",
1853
+ price: product.price ?? 0,
1854
+ currency: product.currency ?? "",
1855
+ quantity: product.quantity ?? 0,
1856
+ unlimited: product.unlimited !== void 0 && product.unlimited !== null && product.unlimited !== false,
1857
+ paystackId,
1858
+ slug: product.slug ?? product.name?.toLowerCase().replace(/\s+/g, "-") ?? "",
1859
+ metadata: product.metadata !== void 0 && product.metadata !== null ? JSON.stringify(product.metadata) : void 0,
1860
+ updatedAt: /* @__PURE__ */ new Date()
1861
+ };
1862
+ if (existing !== void 0 && existing !== null) await ctx.context.adapter.update({
1863
+ model: "paystackProduct",
1864
+ update: productFields,
1865
+ where: [{
1866
+ field: "id",
1867
+ value: String(existing.id)
1868
+ }]
1869
+ });
1870
+ else await ctx.context.adapter.create({
1871
+ model: "paystackProduct",
1872
+ data: {
1873
+ ...productFields,
1874
+ createdAt: /* @__PURE__ */ new Date()
1875
+ }
1876
+ });
1877
+ }
1878
+ return {
1879
+ status: "success",
1880
+ count: productsData.length
1881
+ };
1882
+ } catch (error) {
1883
+ ctx.context.logger.error("Failed to sync products", error);
1884
+ throw new APIError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Failed to sync products" });
1885
+ }
1886
+ }
1887
+ async function syncPaystackPlans(ctx, options) {
1888
+ const paystack = getPaystackOps(options.paystackClient);
1889
+ try {
1890
+ const plansData = unwrapSdkResult(await paystack?.plan?.list());
1891
+ if (!Array.isArray(plansData)) return {
1892
+ status: "success",
1893
+ count: 0
1894
+ };
1895
+ for (const plan of plansData) {
1896
+ const paystackId = String(plan.id);
1897
+ const existing = await ctx.context.adapter.findOne({
1898
+ model: "paystackPlan",
1899
+ where: [{
1900
+ field: "paystackId",
1901
+ value: paystackId
1902
+ }]
1903
+ });
1904
+ const planData = {
1905
+ name: plan.name ?? "",
1906
+ description: plan.description ?? "",
1907
+ amount: plan.amount ?? 0,
1908
+ currency: plan.currency ?? "",
1909
+ interval: plan.interval ?? "",
1910
+ planCode: plan.plan_code ?? "",
1911
+ paystackId,
1912
+ metadata: plan.metadata !== void 0 && plan.metadata !== null ? JSON.stringify(plan.metadata) : void 0,
1913
+ updatedAt: /* @__PURE__ */ new Date()
1914
+ };
1915
+ if (existing !== void 0 && existing !== null) await ctx.context.adapter.update({
1916
+ model: "paystackPlan",
1917
+ update: planData,
1918
+ where: [{
1919
+ field: "id",
1920
+ value: existing.id
1921
+ }]
1922
+ });
1923
+ else await ctx.context.adapter.create({
1924
+ model: "paystackPlan",
1925
+ data: {
1926
+ ...planData,
1927
+ createdAt: /* @__PURE__ */ new Date()
1928
+ }
1929
+ });
1930
+ }
1931
+ return {
1932
+ status: "success",
1933
+ count: plansData.length
1934
+ };
1935
+ } catch (error) {
1936
+ ctx.context.logger.error("Failed to sync plans", error);
1937
+ throw new APIError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Failed to sync plans" });
1938
+ }
1939
+ }
1940
+ async function chargeSubscriptionRenewal(ctx, options, input) {
1941
+ const { subscriptionId, amount: bodyAmount } = input;
1942
+ const subscription = await ctx.context.adapter.findOne({
1943
+ model: "subscription",
1944
+ where: [{
1945
+ field: "id",
1946
+ value: subscriptionId
1947
+ }]
1948
+ });
1949
+ if (subscription === void 0 || subscription === null) throw new APIError("NOT_FOUND", { message: "Subscription not found" });
1950
+ if (subscription.paystackAuthorizationCode === void 0 || subscription.paystackAuthorizationCode === null || subscription.paystackAuthorizationCode === "") throw new APIError("BAD_REQUEST", { message: "No authorization code found for this subscription" });
1951
+ const plan = (await getPlans(options.subscription)).find((candidate) => candidate.name.toLowerCase() === subscription.plan.toLowerCase());
1952
+ if (plan === void 0 || plan === null) throw new APIError("NOT_FOUND", { message: "Plan not found" });
1953
+ const amount = bodyAmount ?? plan.amount;
1954
+ if (amount === void 0 || amount === null) throw new APIError("BAD_REQUEST", { message: "Plan amount is not defined" });
1955
+ let email;
1956
+ let billingUserId = subscription.userId;
1957
+ const referenceId = subscription.referenceId;
1958
+ if (referenceId !== void 0 && referenceId !== null && referenceId !== "") {
1959
+ const user = await ctx.context.adapter.findOne({
1960
+ model: "user",
1961
+ where: [{
1962
+ field: "id",
1963
+ value: referenceId
1964
+ }]
1965
+ });
1966
+ if (user !== void 0 && user !== null) {
1967
+ email = user.email;
1968
+ billingUserId = user.id;
1969
+ } else if (options.organization?.enabled === true) {
1970
+ const ownerMember = await ctx.context.adapter.findOne({
1971
+ model: "member",
1972
+ where: [{
1973
+ field: "organizationId",
1974
+ value: referenceId
1975
+ }, {
1976
+ field: "role",
1977
+ value: "owner"
1978
+ }]
1979
+ });
1980
+ if (ownerMember !== void 0 && ownerMember !== null) {
1981
+ const ownerUser = await ctx.context.adapter.findOne({
1982
+ model: "user",
1983
+ where: [{
1984
+ field: "id",
1985
+ value: ownerMember.userId
1986
+ }]
1987
+ });
1988
+ email = ownerUser?.email;
1989
+ billingUserId = ownerUser?.id ?? ownerMember.userId;
1990
+ }
1991
+ }
1992
+ }
1993
+ if (email === void 0 || email === null || email === "") throw new APIError("NOT_FOUND", { message: "User email not found" });
1994
+ const finalCurrency = plan.currency ?? "NGN";
1995
+ if (!validateMinAmount(amount, finalCurrency)) throw new APIError("BAD_REQUEST", {
1996
+ message: `Amount ${amount} is less than the minimum required for ${finalCurrency}.`,
1997
+ status: 400
1998
+ });
1999
+ const chargeData = unwrapSdkResult(await getPaystackOps(options.paystackClient)?.transaction?.chargeAuthorization({ body: {
2000
+ email,
2001
+ amount,
2002
+ authorization_code: subscription.paystackAuthorizationCode,
2003
+ reference: `rec_${subscription.id}_${Date.now()}`,
2004
+ metadata: JSON.stringify({
2005
+ subscriptionId,
2006
+ referenceId
2007
+ })
2008
+ } }));
2009
+ if (chargeData?.status === "success" && chargeData.reference !== void 0) {
2010
+ const now = /* @__PURE__ */ new Date();
2011
+ const nextPeriodEnd = getNextPeriodEnd(now, plan.interval ?? "monthly");
2012
+ await ctx.context.adapter.create({
2013
+ model: "paystackTransaction",
2014
+ data: {
2015
+ reference: chargeData.reference,
2016
+ paystackId: chargeData.id !== void 0 && chargeData.id !== null ? String(chargeData.id) : void 0,
2017
+ referenceId,
2018
+ userId: billingUserId,
2019
+ amount: chargeData.amount,
2020
+ currency: chargeData.currency,
2021
+ status: "success",
2022
+ plan: plan.name.toLowerCase(),
2023
+ metadata: JSON.stringify({
2024
+ type: "renewal",
2025
+ subscriptionId,
2026
+ referenceId
2027
+ }),
2028
+ createdAt: now,
2029
+ updatedAt: now
2030
+ }
2031
+ });
2032
+ await ctx.context.adapter.update({
2033
+ model: "subscription",
2034
+ update: {
2035
+ periodStart: now,
2036
+ periodEnd: nextPeriodEnd,
2037
+ updatedAt: now,
2038
+ paystackTransactionReference: chargeData.reference
2039
+ },
2040
+ where: [{
2041
+ field: "id",
2042
+ value: subscription.id
2043
+ }]
2044
+ });
2045
+ return {
2046
+ status: "success",
2047
+ data: chargeData
2048
+ };
2049
+ }
2050
+ return {
2051
+ status: "failed",
2052
+ data: chargeData
2053
+ };
2054
+ }
2055
+ //#endregion
1953
2056
  //#region src/index.ts
1954
- const INTERNAL_ERROR_CODES = defineErrorCodes({ ...Object.fromEntries(Object.entries(PAYSTACK_ERROR_CODES).map(([key, value]) => [key, typeof value === "string" ? value : value.message])) });
2057
+ const INTERNAL_ERROR_CODES = defineErrorCodes(Object.fromEntries(Object.entries(PAYSTACK_ERROR_CODES).map(([key, value]) => [key, typeof value === "string" ? value : value.message])));
1955
2058
  const paystack = (options) => {
1956
- const routeOptions = options;
2059
+ const routeOptions = {
2060
+ ...options,
2061
+ webhook: {
2062
+ ...options.webhook,
2063
+ secret: options.webhook?.secret ?? options.paystackWebhookSecret
2064
+ }
2065
+ };
1957
2066
  return {
1958
2067
  id: "paystack",
1959
2068
  endpoints: {
1960
- initializeTransaction: initializeTransaction(routeOptions),
1961
- verifyTransaction: verifyTransaction(routeOptions),
1962
- listSubscriptions: listSubscriptions(routeOptions),
1963
- paystackWebhook: paystackWebhook(routeOptions),
1964
- listTransactions: listTransactions(routeOptions),
1965
- getConfig: getConfig(routeOptions),
1966
- disableSubscription: disablePaystackSubscription(routeOptions),
1967
- enableSubscription: enablePaystackSubscription(routeOptions),
1968
- getSubscriptionManageLink: getSubscriptionManageLink(routeOptions),
2069
+ initializeTransaction: initializeTransaction(routeOptions, "/paystack/initialize-transaction"),
2070
+ verifyTransaction: verifyTransaction(routeOptions, "/paystack/verify-transaction"),
2071
+ listSubscriptions: listSubscriptions(routeOptions, "/paystack/list-subscriptions"),
2072
+ paystackWebhook: paystackWebhook(routeOptions, "/paystack/webhook"),
2073
+ listTransactions: listTransactions(routeOptions, "/paystack/list-transactions"),
2074
+ getConfig: getConfig(routeOptions, "/paystack/config"),
2075
+ disableSubscription: disablePaystackSubscription(routeOptions, "/paystack/disable-subscription"),
2076
+ enableSubscription: enablePaystackSubscription(routeOptions, "/paystack/enable-subscription"),
2077
+ getSubscriptionManageLink: getSubscriptionManageLink(routeOptions, "/paystack/subscription-manage-link"),
1969
2078
  subscriptionManageLink: getSubscriptionManageLink(routeOptions, "/paystack/subscription/manage-link"),
1970
- createSubscription: createSubscription(routeOptions),
1971
- upgradeSubscription: upgradeSubscription(routeOptions),
1972
- cancelSubscription: cancelSubscription(routeOptions),
1973
- restoreSubscription: restoreSubscription(routeOptions),
1974
- chargeRecurringSubscription: chargeRecurringSubscription(routeOptions),
1975
- syncProducts: syncProducts(routeOptions),
1976
- listProducts: listProducts(routeOptions),
1977
- syncPlans: syncPlans(routeOptions),
1978
- listPlans: listPlans(routeOptions)
2079
+ createSubscription: createSubscription(routeOptions, "/paystack/create-subscription"),
2080
+ upgradeSubscription: upgradeSubscription(routeOptions, "/paystack/upgrade-subscription"),
2081
+ cancelSubscription: cancelSubscription(routeOptions, "/paystack/cancel-subscription"),
2082
+ restoreSubscription: restoreSubscription(routeOptions, "/paystack/restore-subscription"),
2083
+ listProducts: listProducts(routeOptions, "/paystack/list-products"),
2084
+ listPlans: listPlans(routeOptions, "/paystack/list-plans")
1979
2085
  },
1980
2086
  schema: getSchema(options),
1981
2087
  init: (ctx) => {
@@ -1983,25 +2089,39 @@ const paystack = (options) => {
1983
2089
  databaseHooks: {
1984
2090
  user: { create: { async after(user, hookCtx) {
1985
2091
  if (!hookCtx || options.createCustomerOnSignUp !== true || user.email === null || user.email === void 0 || user.email === "") return;
1986
- const sdkRes = unwrapSdkResult(await getPaystackOps(options.paystackClient).customerCreate({
1987
- email: user.email,
1988
- first_name: user.name ?? void 0,
1989
- metadata: { userId: user.id }
1990
- }));
1991
- const customerCode = sdkRes?.customer_code ?? (sdkRes?.data)?.customer_code;
1992
- if (customerCode === "" || customerCode === null || customerCode === void 0) return;
1993
- await ctx.adapter.update({
1994
- model: "user",
1995
- where: [{
1996
- field: "id",
1997
- value: user.id
1998
- }],
1999
- update: { paystackCustomerCode: customerCode }
2000
- });
2092
+ try {
2093
+ const paystackOps = getPaystackOps(options.paystackClient);
2094
+ if (!paystackOps) return;
2095
+ const sdkRes = unwrapSdkResult(await paystackOps.customer?.create({ body: {
2096
+ email: user.email,
2097
+ first_name: user.name ?? void 0,
2098
+ metadata: JSON.stringify({ userId: user.id })
2099
+ } }) ?? await Promise.reject(/* @__PURE__ */ new Error("Paystack client missing customer ops")));
2100
+ const customerCode = sdkRes?.customer_code;
2101
+ if (customerCode !== void 0 && customerCode !== null && customerCode !== "") {
2102
+ await ctx.adapter.update({
2103
+ model: "user",
2104
+ where: [{
2105
+ field: "id",
2106
+ value: user.id
2107
+ }],
2108
+ update: { paystackCustomerCode: customerCode }
2109
+ });
2110
+ if (typeof options.onCustomerCreate === "function") await options.onCustomerCreate({
2111
+ paystackCustomer: sdkRes,
2112
+ user: {
2113
+ ...user,
2114
+ paystackCustomerCode: customerCode
2115
+ }
2116
+ }, hookCtx);
2117
+ }
2118
+ } catch (error) {
2119
+ ctx.logger.error("Failed to create Paystack customer for user", error);
2120
+ }
2001
2121
  } } },
2002
2122
  organization: options.organization?.enabled === true ? { create: { async after(org, hookCtx) {
2003
2123
  try {
2004
- const extraCreateParams = options.organization?.getCustomerCreateParams ? await options.organization.getCustomerCreateParams(org, hookCtx) : {};
2124
+ const extraCreateParams = typeof options.organization?.getCustomerCreateParams === "function" ? await options.organization.getCustomerCreateParams(org, hookCtx) : {};
2005
2125
  let targetEmail = org.email;
2006
2126
  if (targetEmail === void 0 || targetEmail === null) {
2007
2127
  const ownerMember = await ctx.adapter.findOne({
@@ -2014,7 +2134,7 @@ const paystack = (options) => {
2014
2134
  value: "owner"
2015
2135
  }]
2016
2136
  });
2017
- if (ownerMember) targetEmail = (await ctx.adapter.findOne({
2137
+ if (ownerMember !== null && ownerMember !== void 0) targetEmail = (await ctx.adapter.findOne({
2018
2138
  model: "user",
2019
2139
  where: [{
2020
2140
  field: "id",
@@ -2026,19 +2146,29 @@ const paystack = (options) => {
2026
2146
  const params = defu({
2027
2147
  email: targetEmail,
2028
2148
  first_name: org.name,
2029
- metadata: { organizationId: org.id }
2149
+ metadata: JSON.stringify({ organizationId: org.id })
2030
2150
  }, extraCreateParams);
2031
- const sdkRes = unwrapSdkResult(await getPaystackOps(options.paystackClient).customerCreate(params));
2032
- const customerCode = sdkRes?.customer_code ?? (sdkRes?.data)?.customer_code;
2033
- if (customerCode === "" || customerCode === null || customerCode === void 0 || sdkRes === null || sdkRes === void 0) return;
2034
- await ctx.internalAdapter.updateOrganization(org.id, { paystackCustomerCode: customerCode });
2035
- await options.organization?.onCustomerCreate?.({
2036
- paystackCustomer: sdkRes,
2037
- organization: {
2038
- ...org,
2039
- paystackCustomerCode: customerCode
2040
- }
2041
- }, hookCtx);
2151
+ const paystackOps = getPaystackOps(options.paystackClient);
2152
+ if (!paystackOps) return;
2153
+ const sdkRes = unwrapSdkResult(await paystackOps.customer?.create({ body: params }) ?? await Promise.reject(/* @__PURE__ */ new Error("Paystack client missing customer ops")));
2154
+ const customerCode = sdkRes?.customer_code;
2155
+ if (customerCode !== void 0 && customerCode !== null && customerCode !== "" && sdkRes !== void 0 && sdkRes !== null) {
2156
+ await ctx.adapter.update({
2157
+ model: "organization",
2158
+ where: [{
2159
+ field: "id",
2160
+ value: org.id
2161
+ }],
2162
+ update: { paystackCustomerCode: customerCode }
2163
+ });
2164
+ if (typeof options.organization?.onCustomerCreate === "function") await options.organization.onCustomerCreate({
2165
+ paystackCustomer: sdkRes,
2166
+ organization: {
2167
+ ...org,
2168
+ paystackCustomerCode: customerCode
2169
+ }
2170
+ }, hookCtx);
2171
+ }
2042
2172
  } catch (error) {
2043
2173
  ctx.logger.error("Failed to create Paystack customer for organization", error);
2044
2174
  }
@@ -2073,7 +2203,7 @@ const paystack = (options) => {
2073
2203
  team: { create: { before: async (team, ctx) => {
2074
2204
  if (options.subscription?.enabled === true && team.organizationId && ctx) {
2075
2205
  const subscription = await getOrganizationSubscription(ctx, team.organizationId);
2076
- if (subscription) {
2206
+ if (subscription !== null && subscription !== void 0) {
2077
2207
  const maxTeams = ((await getPlanByName(routeOptions, subscription.plan))?.limits)?.teams;
2078
2208
  if (typeof maxTeams === "number") await checkTeamLimit(ctx, team.organizationId, maxTeams);
2079
2209
  }
@@ -2081,10 +2211,11 @@ const paystack = (options) => {
2081
2211
  } } }
2082
2212
  } };
2083
2213
  },
2084
- $ERROR_CODES: INTERNAL_ERROR_CODES
2214
+ $ERROR_CODES: INTERNAL_ERROR_CODES,
2215
+ options
2085
2216
  };
2086
2217
  };
2087
2218
  //#endregion
2088
- export { paystack };
2219
+ export { chargeSubscriptionRenewal, paystack, syncPaystackPlans, syncPaystackProducts };
2089
2220
 
2090
2221
  //# sourceMappingURL=index.mjs.map