@alexasomba/better-auth-paystack 2.4.0 → 2.4.3

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
@@ -1,10 +1,210 @@
1
- import { t as PACKAGE_VERSION } from "./version-C_50YiuM.mjs";
2
- import { HIDE_METADATA, defineErrorCodes, logger } from "better-auth";
1
+ import { t as PACKAGE_VERSION } from "./version-GQ15aLQo.mjs";
2
+ import { HIDE_METADATA, defineErrorCodes } from "better-auth";
3
3
  import { defu } from "defu";
4
4
  import { APIError, createAuthEndpoint, createAuthMiddleware, getSessionFromCtx, originCheck, sessionMiddleware } from "better-auth/api";
5
5
  import { z } from "zod";
6
- import { PaystackResponse } from "@alexasomba/paystack-node";
6
+ import { PaystackError, PaystackResponse } from "@alexasomba/paystack-node";
7
7
  import { mergeSchema } from "better-auth/db";
8
+ //#region src/billing-store.ts
9
+ function sortSubscriptionsForCurrent(subscriptions) {
10
+ const statusRank = new Map([
11
+ ["active", 0],
12
+ ["trialing", 1],
13
+ ["incomplete", 2],
14
+ ["past_due", 3],
15
+ ["canceled", 4]
16
+ ]);
17
+ return [...subscriptions].sort((a, b) => {
18
+ const rankA = statusRank.get(a.status) ?? 99;
19
+ const rankB = statusRank.get(b.status) ?? 99;
20
+ if (rankA !== rankB) return rankA - rankB;
21
+ return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
22
+ });
23
+ }
24
+ function createBillingStore(ctx) {
25
+ return createBillingStoreFromAdapter(ctx.context.adapter);
26
+ }
27
+ function createBillingStoreFromAdapter(adapter) {
28
+ const findOne = async (model, where) => await adapter.findOne({
29
+ model,
30
+ where
31
+ }) ?? null;
32
+ const findMany = async (model, where) => await adapter.findMany({
33
+ model,
34
+ ...where ? { where } : {}
35
+ }) ?? [];
36
+ return {
37
+ findSubscriptionById: (id) => findOne("subscription", [{
38
+ field: "id",
39
+ value: id
40
+ }]),
41
+ findSubscriptionByCode: (subscriptionCode) => findOne("subscription", [{
42
+ field: "paystackSubscriptionCode",
43
+ value: subscriptionCode
44
+ }]),
45
+ findSubscriptionsByReference: (referenceId) => findMany("subscription", [{
46
+ field: "referenceId",
47
+ value: referenceId
48
+ }]),
49
+ async findCurrentSubscription(referenceId) {
50
+ return sortSubscriptionsForCurrent(await this.findSubscriptionsByReference(referenceId))[0] ?? null;
51
+ },
52
+ findSubscriptionsByTransactionReference: (reference) => findMany("subscription", [{
53
+ field: "paystackTransactionReference",
54
+ value: reference
55
+ }]),
56
+ createSubscription: async (data) => await adapter.create({
57
+ model: "subscription",
58
+ data
59
+ }),
60
+ updateSubscription: (id, update) => adapter.update({
61
+ model: "subscription",
62
+ update,
63
+ where: [{
64
+ field: "id",
65
+ value: id
66
+ }]
67
+ }),
68
+ updateSubscriptionByCode: (subscriptionCode, update) => adapter.update({
69
+ model: "subscription",
70
+ update,
71
+ where: [{
72
+ field: "paystackSubscriptionCode",
73
+ value: subscriptionCode
74
+ }]
75
+ }),
76
+ createTransaction: async (data) => await adapter.create({
77
+ model: "paystackTransaction",
78
+ data
79
+ }),
80
+ findTransactionByReference: (reference) => findOne("paystackTransaction", [{
81
+ field: "reference",
82
+ value: reference
83
+ }]),
84
+ updateTransactionByReference: (reference, update) => adapter.update({
85
+ model: "paystackTransaction",
86
+ update,
87
+ where: [{
88
+ field: "reference",
89
+ value: reference
90
+ }]
91
+ }),
92
+ async listTransactions(referenceId) {
93
+ return (await findMany("paystackTransaction", [{
94
+ field: "referenceId",
95
+ value: referenceId
96
+ }])).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
97
+ },
98
+ async listProducts() {
99
+ return (await findMany("paystackProduct")).sort((a, b) => a.name.localeCompare(b.name));
100
+ },
101
+ findProductByName: (name) => findOne("paystackProduct", [{
102
+ field: "name",
103
+ value: name
104
+ }]),
105
+ findProductBySlug: (slug) => findOne("paystackProduct", [{
106
+ field: "slug",
107
+ value: slug
108
+ }]),
109
+ async updateProduct(id, update) {
110
+ await adapter.update({
111
+ model: "paystackProduct",
112
+ update,
113
+ where: [{
114
+ field: "id",
115
+ value: id
116
+ }]
117
+ });
118
+ },
119
+ async upsertProductByPaystackId(paystackId, data) {
120
+ const existing = await findOne("paystackProduct", [{
121
+ field: "paystackId",
122
+ value: paystackId
123
+ }]);
124
+ if (existing?.id !== void 0) {
125
+ const { createdAt: _createdAt, ...update } = data;
126
+ await adapter.update({
127
+ model: "paystackProduct",
128
+ update,
129
+ where: [{
130
+ field: "id",
131
+ value: String(existing.id)
132
+ }]
133
+ });
134
+ return;
135
+ }
136
+ await adapter.create({
137
+ model: "paystackProduct",
138
+ data
139
+ });
140
+ },
141
+ listPlans: () => findMany("paystackPlan"),
142
+ findPlanByName: (name) => findOne("paystackPlan", [{
143
+ field: "name",
144
+ value: name
145
+ }]),
146
+ findPlanByCode: (planCode) => findOne("paystackPlan", [{
147
+ field: "planCode",
148
+ value: planCode
149
+ }]),
150
+ async upsertPlanByPaystackId(paystackId, data) {
151
+ const existing = await findOne("paystackPlan", [{
152
+ field: "paystackId",
153
+ value: paystackId
154
+ }]);
155
+ if (existing?.id !== void 0) {
156
+ const { createdAt: _createdAt, ...update } = data;
157
+ await adapter.update({
158
+ model: "paystackPlan",
159
+ update,
160
+ where: [{
161
+ field: "id",
162
+ value: existing.id
163
+ }]
164
+ });
165
+ return;
166
+ }
167
+ await adapter.create({
168
+ model: "paystackPlan",
169
+ data
170
+ });
171
+ },
172
+ findUser: (id) => findOne("user", [{
173
+ field: "id",
174
+ value: id
175
+ }]),
176
+ findOrganization: (id) => findOne("organization", [{
177
+ field: "id",
178
+ value: id
179
+ }]),
180
+ findOrganizationOwner: (organizationId) => findOne("member", [{
181
+ field: "organizationId",
182
+ value: organizationId
183
+ }, {
184
+ field: "role",
185
+ value: "owner"
186
+ }]),
187
+ listMembers: (organizationId) => findMany("member", [{
188
+ field: "organizationId",
189
+ value: organizationId
190
+ }]),
191
+ listTeams: (organizationId) => findMany("team", [{
192
+ field: "organizationId",
193
+ value: organizationId
194
+ }]),
195
+ async saveCustomerCode(referenceId, customerCode, isOrganization) {
196
+ await adapter.update({
197
+ model: isOrganization ? "organization" : "user",
198
+ update: { paystackCustomerCode: customerCode },
199
+ where: [{
200
+ field: "id",
201
+ value: referenceId
202
+ }]
203
+ });
204
+ }
205
+ };
206
+ }
207
+ //#endregion
8
208
  //#region src/paystack-sdk.ts
9
209
  /**
10
210
  * Interface for checking if a result is a PaystackResponse from the SDK v1.9.1+
@@ -20,6 +220,10 @@ function unwrapSdkResult(result) {
20
220
  if (IsPaystackResponse(result)) try {
21
221
  return result.unwrap();
22
222
  } catch (e) {
223
+ if (e instanceof PaystackError) throw new APIError("BAD_REQUEST", {
224
+ message: e.message,
225
+ status: e.status
226
+ });
23
227
  throw new APIError("BAD_REQUEST", { message: e?.message ?? "Paystack API error" });
24
228
  }
25
229
  let current = result;
@@ -42,6 +246,50 @@ function unwrapSdkResult(result) {
42
246
  function getPaystackOps(client) {
43
247
  return client;
44
248
  }
249
+ function createPaystackAdapter(client) {
250
+ const requireClient = () => {
251
+ if (client === void 0 || client === null) throw new APIError("BAD_REQUEST", { message: "Paystack client is not configured" });
252
+ return client;
253
+ };
254
+ return {
255
+ async initializeTransaction(body) {
256
+ return unwrapSdkResult(await requireClient().transaction?.initialize({ body }));
257
+ },
258
+ async verifyTransaction(reference) {
259
+ return unwrapSdkResult(await requireClient().transaction?.verify(reference));
260
+ },
261
+ async chargeAuthorization(body) {
262
+ return unwrapSdkResult(await requireClient().transaction?.chargeAuthorization({ body }));
263
+ },
264
+ async createCustomer(body) {
265
+ return unwrapSdkResult(await requireClient().customer?.create({ body }));
266
+ },
267
+ async listProducts() {
268
+ return unwrapSdkResult(await requireClient().product?.list({}));
269
+ },
270
+ async fetchProduct(productId) {
271
+ return unwrapSdkResult(await requireClient().product?.fetch(productId));
272
+ },
273
+ async listPlans() {
274
+ return unwrapSdkResult(await requireClient().plan?.list());
275
+ },
276
+ async createSubscription(body) {
277
+ return unwrapSdkResult(await requireClient().subscription?.create({ body }));
278
+ },
279
+ async fetchSubscription(subscriptionCode) {
280
+ return unwrapSdkResult(await requireClient().subscription?.fetch(subscriptionCode));
281
+ },
282
+ async disableSubscription(body) {
283
+ return unwrapSdkResult(await requireClient().subscription?.disable({ body }));
284
+ },
285
+ async enableSubscription(body) {
286
+ return unwrapSdkResult(await requireClient().subscription?.enable({ body }));
287
+ },
288
+ async manageSubscriptionLink(subscriptionCode) {
289
+ return unwrapSdkResult(await requireClient().subscription?.manageLink(subscriptionCode));
290
+ }
291
+ };
292
+ }
45
293
  //#endregion
46
294
  //#region src/utils.ts
47
295
  function getPlanSeatAmount(plan) {
@@ -129,97 +377,46 @@ function validateMinAmount(amount, currency) {
129
377
  return min !== void 0 ? amount >= min : true;
130
378
  }
131
379
  async function syncProductQuantityFromPaystack(ctx, productName, paystackClient) {
132
- let localProduct = await ctx.context.adapter.findOne({
133
- model: "paystackProduct",
134
- where: [{
135
- field: "name",
136
- value: productName
137
- }]
138
- });
139
- localProduct ??= await ctx.context.adapter.findOne({
140
- model: "paystackProduct",
141
- where: [{
142
- field: "slug",
143
- value: productName.toLowerCase().replace(/\s+/g, "-")
144
- }]
145
- });
380
+ const store = createBillingStore(ctx);
381
+ let localProduct = await store.findProductByName(productName);
382
+ localProduct ??= await store.findProductBySlug(productName.toLowerCase().replace(/\s+/g, "-"));
146
383
  if (localProduct?.paystackId === void 0 || localProduct.paystackId === null || localProduct.paystackId === "") {
147
- if (localProduct?.id !== void 0 && localProduct.unlimited !== true && typeof localProduct.quantity === "number" && localProduct.quantity > 0) await ctx.context.adapter.update({
148
- model: "paystackProduct",
149
- update: {
150
- quantity: localProduct.quantity - 1,
151
- updatedAt: /* @__PURE__ */ new Date()
152
- },
153
- where: [{
154
- field: "id",
155
- value: localProduct.id
156
- }]
384
+ if (localProduct?.id !== void 0 && localProduct.unlimited !== true && typeof localProduct.quantity === "number" && localProduct.quantity > 0) await store.updateProduct(localProduct.id, {
385
+ quantity: localProduct.quantity - 1,
386
+ updatedAt: /* @__PURE__ */ new Date()
157
387
  });
158
388
  return;
159
389
  }
160
390
  try {
161
391
  const paystackProductId = Number(localProduct.paystackId);
162
392
  if (!Number.isFinite(paystackProductId)) return;
163
- const remoteQuantity = unwrapSdkResult(await paystackClient.product?.fetch(paystackProductId))?.quantity;
164
- if (remoteQuantity !== void 0 && localProduct.id !== void 0) await ctx.context.adapter.update({
165
- model: "paystackProduct",
166
- update: {
167
- quantity: remoteQuantity,
168
- updatedAt: /* @__PURE__ */ new Date()
169
- },
170
- where: [{
171
- field: "id",
172
- value: localProduct.id
173
- }]
393
+ const remoteQuantity = (await createPaystackAdapter(paystackClient).fetchProduct(paystackProductId))?.quantity;
394
+ if (remoteQuantity !== void 0 && localProduct.id !== void 0) await store.updateProduct(localProduct.id, {
395
+ quantity: remoteQuantity,
396
+ updatedAt: /* @__PURE__ */ new Date()
174
397
  });
175
398
  } catch {
176
- if (localProduct?.id !== void 0 && localProduct.unlimited !== true && typeof localProduct.quantity === "number" && localProduct.quantity > 0) await ctx.context.adapter.update({
177
- model: "paystackProduct",
178
- update: {
179
- quantity: localProduct.quantity - 1,
180
- updatedAt: /* @__PURE__ */ new Date()
181
- },
182
- where: [{
183
- field: "id",
184
- value: localProduct.id
185
- }]
399
+ if (localProduct?.id !== void 0 && localProduct.unlimited !== true && typeof localProduct.quantity === "number" && localProduct.quantity > 0) await store.updateProduct(localProduct.id, {
400
+ quantity: localProduct.quantity - 1,
401
+ updatedAt: /* @__PURE__ */ new Date()
186
402
  });
187
403
  }
188
404
  }
189
405
  async function syncSubscriptionSeats(ctx, organizationId, options) {
190
406
  if (options.subscription?.enabled !== true) return;
191
- const adapter = ctx.context.adapter;
192
- const subscription = await adapter.findOne({
193
- model: "subscription",
194
- where: [{
195
- field: "referenceId",
196
- value: organizationId
197
- }]
198
- });
407
+ const store = createBillingStore(ctx);
408
+ const subscription = await store.findCurrentSubscription(organizationId);
199
409
  if (subscription?.paystackSubscriptionCode === void 0 || subscription.paystackSubscriptionCode === null || subscription.paystackSubscriptionCode === "") return;
200
410
  if (subscription === null || subscription === void 0) return;
201
411
  const plan = await getPlanByName(options, subscription.plan);
202
412
  if (plan === null || plan === void 0) return;
203
413
  if (getPlanSeatAmount(plan) === void 0) return;
204
- const quantity = (await adapter.findMany({
205
- model: "member",
206
- where: [{
207
- field: "organizationId",
208
- value: organizationId
209
- }]
210
- })).length;
414
+ const quantity = (await store.listMembers(organizationId)).length;
211
415
  try {
212
416
  assertLocallyManagedSubscription(subscription, "automatic seat sync");
213
- await adapter.update({
214
- model: "subscription",
215
- where: [{
216
- field: "id",
217
- value: subscription.id
218
- }],
219
- update: {
220
- seats: quantity,
221
- updatedAt: /* @__PURE__ */ new Date()
222
- }
417
+ await store.updateSubscription(subscription.id, {
418
+ seats: quantity,
419
+ updatedAt: /* @__PURE__ */ new Date()
223
420
  });
224
421
  } catch (e) {
225
422
  const log = ctx.context.logger;
@@ -227,35 +424,28 @@ async function syncSubscriptionSeats(ctx, organizationId, options) {
227
424
  }
228
425
  }
229
426
  //#endregion
230
- //#region src/middleware.ts
427
+ //#region src/reference-access.ts
231
428
  const BILLING_ORG_ROLES = new Set(["owner", "admin"]);
232
429
  function hasBillingRole(role) {
233
430
  if (Array.isArray(role)) return role.some((value) => hasBillingRole(value));
234
431
  if (typeof role !== "string") return false;
235
432
  return role.split(",").map((value) => value.trim()).some((value) => BILLING_ORG_ROLES.has(value));
236
433
  }
237
- const referenceMiddleware = (options, action) => createAuthMiddleware(async (ctx) => {
238
- const session = ctx.context.session;
239
- if (session === null || session === void 0) throw new APIError("UNAUTHORIZED");
240
- const body = ctx.body ?? {};
241
- const query = ctx.query ?? {};
242
- const requestQueryReferenceId = typeof ctx.request?.url === "string" ? new URL(ctx.request.url).searchParams.get("referenceId") ?? void 0 : void 0;
243
- const referenceId = body.referenceId ?? query.referenceId ?? requestQueryReferenceId ?? session.user.id;
244
- const subscriptionOptions = options.subscription;
245
- if (referenceId === session.user.id) return { context: {
246
- ...ctx.context,
247
- referenceId
248
- } };
249
- if (subscriptionOptions?.enabled === true && "authorizeReference" in subscriptionOptions && typeof subscriptionOptions.authorizeReference === "function") {
250
- if (await subscriptionOptions.authorizeReference({
251
- user: session.user,
252
- session: session.session,
253
- referenceId,
254
- action
255
- }, ctx) === true) return { context: {
256
- ...ctx.context,
257
- referenceId
258
- } };
434
+ function resolveBillingReferenceId(input) {
435
+ const body = input.body ?? {};
436
+ const query = input.query ?? {};
437
+ const requestQueryReferenceId = typeof input.requestUrl === "string" ? new URL(input.requestUrl).searchParams.get("referenceId") ?? void 0 : void 0;
438
+ return body.referenceId ?? query.referenceId ?? requestQueryReferenceId ?? input.fallbackUserId;
439
+ }
440
+ async function authorizeBillingReference(ctx, options, data) {
441
+ if (data.referenceId === data.user.id) return;
442
+ if (options.subscription?.enabled === true && typeof options.subscription.authorizeReference === "function") {
443
+ if (await options.subscription.authorizeReference({
444
+ user: data.user,
445
+ session: data.session,
446
+ referenceId: data.referenceId,
447
+ action: data.action
448
+ }, ctx) === true) return;
259
449
  throw new APIError("UNAUTHORIZED");
260
450
  }
261
451
  if (options.organization?.enabled === true) {
@@ -263,56 +453,170 @@ const referenceMiddleware = (options, action) => createAuthMiddleware(async (ctx
263
453
  model: "member",
264
454
  where: [{
265
455
  field: "userId",
266
- value: session.user.id
456
+ value: data.user.id
267
457
  }, {
268
458
  field: "organizationId",
269
- value: referenceId
459
+ value: data.referenceId
270
460
  }]
271
461
  });
272
- if (member !== null && member !== void 0 && hasBillingRole(member.role)) return { context: {
273
- ...ctx.context,
274
- referenceId
275
- } };
462
+ if (member !== null && member !== void 0 && hasBillingRole(member.role)) return;
276
463
  }
277
- logger.error(`Passing referenceId into a subscription action isn't allowed unless subscription.authorizeReference allows it or the session user is an organization owner/admin.`);
278
- throw new APIError("BAD_REQUEST", { message: "Passing referenceId isn't allowed without subscription.authorizeReference or organization owner/admin membership." });
464
+ throw new APIError("UNAUTHORIZED");
465
+ }
466
+ //#endregion
467
+ //#region src/middleware.ts
468
+ const referenceMiddleware = (options, action) => createAuthMiddleware(async (ctx) => {
469
+ const session = ctx.context.session;
470
+ if (session === null || session === void 0) throw new APIError("UNAUTHORIZED");
471
+ const referenceId = resolveBillingReferenceId({
472
+ body: ctx.body,
473
+ query: ctx.query,
474
+ requestUrl: ctx.request?.url,
475
+ fallbackUserId: session.user.id
476
+ });
477
+ await authorizeBillingReference(ctx, options, {
478
+ user: session.user,
479
+ session: session.session,
480
+ referenceId,
481
+ action
482
+ });
483
+ return { context: {
484
+ ...ctx.context,
485
+ referenceId
486
+ } };
279
487
  });
280
488
  //#endregion
281
489
  //#region src/limits.ts
282
490
  const getOrganizationSubscription = async (ctx, organizationId) => {
283
- return await ctx.context.adapter.findOne({
284
- model: "subscription",
285
- where: [{
286
- field: "referenceId",
287
- value: organizationId
288
- }]
289
- });
491
+ return createBillingStore(ctx).findCurrentSubscription(organizationId);
290
492
  };
291
493
  const checkSeatLimit = async (ctx, organizationId, seatsToAdd = 1) => {
292
494
  const subscription = await getOrganizationSubscription(ctx, organizationId);
293
495
  if (subscription?.seats === null) return true;
294
- const members = await ctx.context.adapter.findMany({
295
- model: "member",
296
- where: [{
297
- field: "organizationId",
298
- value: organizationId
299
- }]
300
- });
496
+ const members = await createBillingStore(ctx).listMembers(organizationId);
301
497
  if (!subscription) return true;
302
498
  if (members.length + seatsToAdd > subscription.seats) throw new APIError("FORBIDDEN", { message: `Organization member limit reached. Used: ${members.length}, Max: ${subscription.seats}` });
303
499
  return true;
304
500
  };
305
501
  const checkTeamLimit = async (ctx, organizationId, maxTeams) => {
306
- if ((await ctx.context.adapter.findMany({
307
- model: "team",
308
- where: [{
309
- field: "organizationId",
310
- value: organizationId
311
- }]
312
- })).length >= maxTeams) throw new APIError("FORBIDDEN", { message: `Organization team limit reached. Max teams: ${maxTeams}` });
502
+ if ((await createBillingStore(ctx).listTeams(organizationId)).length >= maxTeams) throw new APIError("FORBIDDEN", { message: `Organization team limit reached. Max teams: ${maxTeams}` });
313
503
  return true;
314
504
  };
315
505
  //#endregion
506
+ //#region src/subscription-lifecycle.ts
507
+ async function handleProratedUpgrade(ctx, options, input) {
508
+ const store = createBillingStore(ctx);
509
+ const existingSub = await store.findCurrentSubscription(input.referenceId);
510
+ if (existingSub?.status !== "active" || existingSub.paystackSubscriptionCode === void 0 || existingSub.paystackSubscriptionCode === null || existingSub.paystackSubscriptionCode === "" || existingSub.periodEnd === void 0 || existingSub.periodEnd === null || existingSub.periodStart === void 0 || existingSub.periodStart === null) return null;
511
+ const now = /* @__PURE__ */ new Date();
512
+ const periodEnd = new Date(existingSub.periodEnd);
513
+ const periodStart = new Date(existingSub.periodStart);
514
+ const totalDays = Math.max(1, Math.ceil((periodEnd.getTime() - periodStart.getTime()) / (1e3 * 60 * 60 * 24)));
515
+ const remainingDays = Math.max(0, Math.ceil((periodEnd.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24)));
516
+ let oldAmount = 0;
517
+ if (existingSub.plan !== "") {
518
+ const oldPlan = await getPlanByName(options, existingSub.plan) ?? await store.findPlanByName(existingSub.plan);
519
+ if (oldPlan !== void 0 && oldPlan !== null) oldAmount = calculatePlanAmount(oldPlan, existingSub.seats);
520
+ }
521
+ let membersCount = 1;
522
+ let newSeatCount;
523
+ let newAmount;
524
+ try {
525
+ assertLocallyManagedSubscription(existingSub, "plan or seat changes");
526
+ if (getPlanSeatAmount(input.plan) !== void 0) {
527
+ const members = await store.listMembers(input.referenceId);
528
+ membersCount = members.length > 0 ? members.length : 1;
529
+ }
530
+ newSeatCount = input.quantity ?? existingSub.seats ?? membersCount;
531
+ newAmount = calculatePlanAmount(input.plan, newSeatCount);
532
+ } catch (error) {
533
+ throw new APIError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Invalid seat configuration for plan." });
534
+ }
535
+ const costDifference = newAmount - oldAmount;
536
+ const prorationMetadata = {
537
+ type: "proration",
538
+ subscriptionId: existingSub.id,
539
+ referenceId: input.referenceId,
540
+ newPlan: input.plan.name.toLowerCase(),
541
+ oldPlan: existingSub.plan,
542
+ newSeatCount,
543
+ remainingDays
544
+ };
545
+ let completedProrationReference;
546
+ if (costDifference > 0 && remainingDays > 0) {
547
+ const proratedAmount = Math.round(costDifference / totalDays * remainingDays);
548
+ if (proratedAmount < 5e3) throw new APIError("BAD_REQUEST", {
549
+ message: "Prorated upgrade amount is below Paystack's minimum charge. Schedule the change for period end instead.",
550
+ status: 400
551
+ });
552
+ const paystack = createPaystackAdapter(options.paystackClient);
553
+ if (existingSub.paystackAuthorizationCode !== void 0 && existingSub.paystackAuthorizationCode !== null && existingSub.paystackAuthorizationCode !== "") {
554
+ const sdkRes = await paystack.chargeAuthorization({
555
+ email: input.targetEmail,
556
+ amount: proratedAmount,
557
+ authorization_code: existingSub.paystackAuthorizationCode,
558
+ reference: `upg_${existingSub.id}_${Date.now()}_${Math.random().toString(36).substring(7)}`,
559
+ metadata: JSON.stringify(prorationMetadata)
560
+ });
561
+ if (sdkRes?.status !== "success") throw new APIError("BAD_REQUEST", { message: "Failed to process prorated charge via saved authorization." });
562
+ await store.createTransaction({
563
+ reference: sdkRes.reference ?? "",
564
+ paystackId: sdkRes.id !== void 0 && sdkRes.id !== null ? String(sdkRes.id) : void 0,
565
+ referenceId: input.referenceId,
566
+ userId: input.userId,
567
+ amount: sdkRes.amount ?? proratedAmount,
568
+ currency: sdkRes.currency ?? input.finalCurrency,
569
+ status: "success",
570
+ plan: input.plan.name.toLowerCase(),
571
+ metadata: JSON.stringify(prorationMetadata),
572
+ createdAt: /* @__PURE__ */ new Date(),
573
+ updatedAt: /* @__PURE__ */ new Date()
574
+ });
575
+ completedProrationReference = sdkRes.reference ?? void 0;
576
+ } else {
577
+ const initRes = await paystack.initializeTransaction({
578
+ email: input.targetEmail,
579
+ amount: proratedAmount,
580
+ currency: input.finalCurrency,
581
+ callback_url: input.callbackURL ?? void 0,
582
+ metadata: JSON.stringify(prorationMetadata),
583
+ ...input.allowedSubscriptionChannels !== void 0 ? { channels: input.allowedSubscriptionChannels } : {}
584
+ });
585
+ await store.createTransaction({
586
+ reference: initRes?.reference ?? "",
587
+ referenceId: input.referenceId,
588
+ userId: input.userId,
589
+ amount: proratedAmount,
590
+ currency: input.finalCurrency,
591
+ status: "pending",
592
+ plan: input.plan.name.toLowerCase(),
593
+ metadata: JSON.stringify(prorationMetadata),
594
+ createdAt: /* @__PURE__ */ new Date(),
595
+ updatedAt: /* @__PURE__ */ new Date()
596
+ });
597
+ return {
598
+ kind: "checkout",
599
+ url: initRes?.authorization_url,
600
+ reference: initRes?.reference,
601
+ accessCode: initRes?.access_code,
602
+ redirect: true
603
+ };
604
+ }
605
+ }
606
+ await store.updateSubscription(existingSub.id, {
607
+ plan: input.plan.name,
608
+ seats: newSeatCount,
609
+ ...completedProrationReference !== void 0 ? { paystackTransactionReference: completedProrationReference } : {},
610
+ updatedAt: /* @__PURE__ */ new Date()
611
+ });
612
+ return {
613
+ kind: "completed",
614
+ status: "success",
615
+ message: "Subscription successfully upgraded with prorated charge.",
616
+ prorated: true
617
+ };
618
+ }
619
+ //#endregion
316
620
  //#region src/routes.ts
317
621
  const PAYSTACK_ERROR_CODES = defineErrorCodes({
318
622
  SUBSCRIPTION_NOT_FOUND: "Subscription not found",
@@ -333,32 +637,6 @@ function isAllowedSubscriptionChannel(channel, allowedChannels) {
333
637
  if (allowedChannels === void 0) return true;
334
638
  return channel !== void 0 && channel !== null && allowedChannels.includes(channel);
335
639
  }
336
- async function assertReferenceAccess(ctx, options, data) {
337
- if (data.referenceId === data.user.id) return;
338
- if (options.subscription?.enabled === true && typeof options.subscription.authorizeReference === "function") {
339
- if (await options.subscription.authorizeReference({
340
- user: data.user,
341
- session: data.session,
342
- referenceId: data.referenceId,
343
- action: data.action
344
- }, ctx) === true) return;
345
- throw new APIError("UNAUTHORIZED");
346
- }
347
- if (options.organization?.enabled === true) {
348
- const member = await ctx.context.adapter.findOne({
349
- model: "member",
350
- where: [{
351
- field: "userId",
352
- value: data.user.id
353
- }, {
354
- field: "organizationId",
355
- value: data.referenceId
356
- }]
357
- });
358
- if (member !== null && member !== void 0 && hasBillingRole(member.role)) return;
359
- }
360
- throw new APIError("UNAUTHORIZED");
361
- }
362
640
  async function hmacSha512Hex(secret, message) {
363
641
  const encoder = new TextEncoder();
364
642
  const keyData = encoder.encode(secret);
@@ -854,148 +1132,27 @@ const initializeTransaction = (options, path = "/initialize-transaction") => {
854
1132
  };
855
1133
  if (allowedSubscriptionChannels !== void 0) initBody.channels = allowedSubscriptionChannels;
856
1134
  if (plan !== void 0 && prorateAndCharge === true) {
857
- const existingSub = await getOrganizationSubscription(ctx, referenceId);
858
- if (existingSub?.status === "active" && existingSub.paystackSubscriptionCode !== void 0 && existingSub.paystackSubscriptionCode !== null && existingSub.paystackSubscriptionCode !== "") {
859
- if (existingSub.periodEnd !== void 0 && existingSub.periodEnd !== null && existingSub.periodStart !== void 0 && existingSub.periodStart !== null) {
860
- const now = /* @__PURE__ */ new Date();
861
- const periodEndLocal = new Date(existingSub.periodEnd);
862
- const periodStartLocal = new Date(existingSub.periodStart);
863
- const totalDays = Math.max(1, Math.ceil((periodEndLocal.getTime() - periodStartLocal.getTime()) / (1e3 * 60 * 60 * 24)));
864
- const remainingDays = Math.max(0, Math.ceil((periodEndLocal.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24)));
865
- let oldAmount = 0;
866
- if (existingSub.plan !== "") {
867
- const oldPlan = await getPlanByName(options, existingSub.plan) ?? await ctx.context.adapter.findOne({
868
- model: "paystackPlan",
869
- where: [{
870
- field: "name",
871
- value: existingSub.plan
872
- }]
873
- }) ?? void 0;
874
- if (oldPlan !== void 0 && oldPlan !== null) {
875
- const oldSeatCount = existingSub.seats;
876
- oldAmount = calculatePlanAmount(oldPlan, oldSeatCount);
877
- }
878
- }
879
- let membersCount = 1;
880
- let newSeatCount = quantity ?? existingSub.seats ?? membersCount;
881
- let newAmount;
882
- try {
883
- assertLocallyManagedSubscription(existingSub, "plan or seat changes");
884
- if (getPlanSeatAmount(plan) !== void 0) {
885
- const members = await ctx.context.adapter.findMany({
886
- model: "member",
887
- where: [{
888
- field: "organizationId",
889
- value: referenceId
890
- }]
891
- });
892
- membersCount = members.length > 0 ? members.length : 1;
893
- }
894
- newSeatCount = quantity ?? existingSub.seats ?? membersCount;
895
- newAmount = calculatePlanAmount(plan, newSeatCount);
896
- } catch (error) {
897
- throw new APIError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Invalid seat configuration for plan." });
898
- }
899
- const costDifference = newAmount - oldAmount;
900
- const prorationMetadata = {
901
- type: "proration",
902
- subscriptionId: existingSub.id,
903
- referenceId,
904
- newPlan: plan.name.toLowerCase(),
905
- oldPlan: existingSub.plan,
906
- newSeatCount,
907
- remainingDays
908
- };
909
- let completedProrationReference;
910
- if (costDifference > 0 && remainingDays > 0) {
911
- const proratedAmount = Math.round(costDifference / totalDays * remainingDays);
912
- if (proratedAmount < 5e3) throw new APIError("BAD_REQUEST", {
913
- message: "Prorated upgrade amount is below Paystack's minimum charge. Schedule the change for period end instead.",
914
- status: 400
915
- });
916
- const ops = getPaystackOps(options.paystackClient);
917
- if (ops === void 0 || ops === null) {
918
- ctx.context.logger.error("Paystack client not configured for proration charge");
919
- return;
920
- }
921
- if (existingSub.paystackAuthorizationCode !== void 0 && existingSub.paystackAuthorizationCode !== null && existingSub.paystackAuthorizationCode !== "") {
922
- const sdkRes = unwrapSdkResult(await ops.transaction?.chargeAuthorization({ body: {
923
- email: targetEmail,
924
- amount: proratedAmount,
925
- authorization_code: existingSub.paystackAuthorizationCode,
926
- reference: `upg_${existingSub.id}_${Date.now()}_${Math.random().toString(36).substring(7)}`,
927
- metadata: JSON.stringify(prorationMetadata)
928
- } }));
929
- if (sdkRes?.status !== "success") throw new APIError("BAD_REQUEST", { message: "Failed to process prorated charge via saved authorization." });
930
- await ctx.context.adapter.create({
931
- model: "paystackTransaction",
932
- data: {
933
- reference: sdkRes.reference ?? "",
934
- paystackId: sdkRes.id !== void 0 && sdkRes.id !== null ? String(sdkRes.id) : void 0,
935
- referenceId,
936
- userId: user.id,
937
- amount: sdkRes.amount ?? proratedAmount,
938
- currency: sdkRes.currency ?? finalCurrency,
939
- status: "success",
940
- plan: plan.name.toLowerCase(),
941
- metadata: JSON.stringify(prorationMetadata),
942
- createdAt: /* @__PURE__ */ new Date(),
943
- updatedAt: /* @__PURE__ */ new Date()
944
- }
945
- });
946
- completedProrationReference = sdkRes.reference ?? void 0;
947
- } else {
948
- const initRes = unwrapSdkResult(await ops.transaction?.initialize({ body: {
949
- email: targetEmail,
950
- amount: proratedAmount,
951
- currency: finalCurrency,
952
- callback_url: callbackURL ?? void 0,
953
- metadata: JSON.stringify(prorationMetadata),
954
- ...allowedSubscriptionChannels !== void 0 ? { channels: allowedSubscriptionChannels } : {}
955
- } }));
956
- await ctx.context.adapter.create({
957
- model: "paystackTransaction",
958
- data: {
959
- reference: initRes?.reference ?? "",
960
- referenceId,
961
- userId: user.id,
962
- amount: proratedAmount,
963
- currency: finalCurrency,
964
- status: "pending",
965
- plan: plan.name.toLowerCase(),
966
- metadata: JSON.stringify(prorationMetadata),
967
- createdAt: /* @__PURE__ */ new Date(),
968
- updatedAt: /* @__PURE__ */ new Date()
969
- }
970
- });
971
- return ctx.json({
972
- url: initRes?.authorization_url,
973
- reference: initRes?.reference,
974
- accessCode: initRes?.access_code,
975
- redirect: true
976
- });
977
- }
978
- }
979
- await ctx.context.adapter.update({
980
- model: "subscription",
981
- where: [{
982
- field: "id",
983
- value: existingSub.id
984
- }],
985
- update: {
986
- plan: plan.name,
987
- seats: newSeatCount,
988
- ...completedProrationReference !== void 0 ? { paystackTransactionReference: completedProrationReference } : {},
989
- updatedAt: /* @__PURE__ */ new Date()
990
- }
991
- });
992
- return ctx.json({
993
- status: "success",
994
- message: "Subscription successfully upgraded with prorated charge.",
995
- prorated: true
996
- });
997
- }
998
- }
1135
+ const proration = await handleProratedUpgrade(ctx, options, {
1136
+ plan,
1137
+ referenceId,
1138
+ quantity,
1139
+ targetEmail,
1140
+ userId: user.id,
1141
+ finalCurrency,
1142
+ callbackURL,
1143
+ allowedSubscriptionChannels
1144
+ });
1145
+ if (proration?.kind === "checkout") return ctx.json({
1146
+ url: proration.url ?? "",
1147
+ reference: proration.reference ?? "",
1148
+ accessCode: proration.accessCode ?? "",
1149
+ redirect: proration.redirect
1150
+ });
1151
+ if (proration?.kind === "completed") return ctx.json({
1152
+ status: proration.status,
1153
+ message: proration.message,
1154
+ prorated: proration.prorated
1155
+ });
999
1156
  }
1000
1157
  if (plan !== void 0) if (trialStart !== void 0) initBody.amount = 5e3;
1001
1158
  else {
@@ -1150,30 +1307,12 @@ const verifyTransaction = (options, path = "/verify-transaction") => {
1150
1307
  message: `This subscription requires one of: ${allowedSubscriptionChannels?.join(", ") ?? "allowed channels"}.`
1151
1308
  });
1152
1309
  }
1153
- if (session !== void 0 && session !== null && referenceId !== void 0 && referenceId !== null && referenceId !== "" && referenceId !== session.user.id) {
1154
- const authRef = subscriptionOptions?.authorizeReference;
1155
- let authorized = false;
1156
- if (authRef !== void 0 && authRef !== null) authorized = await authRef({
1157
- user: session.user,
1158
- session: session.session,
1159
- referenceId,
1160
- action: "verify-transaction"
1161
- }, ctx);
1162
- if (authorized === false && options.organization?.enabled === true) {
1163
- const member = await ctx.context.adapter.findOne({
1164
- model: "member",
1165
- where: [{
1166
- field: "userId",
1167
- value: session.user.id
1168
- }, {
1169
- field: "organizationId",
1170
- value: referenceId
1171
- }]
1172
- });
1173
- if (member !== void 0 && member !== null) authorized = true;
1174
- }
1175
- if (authorized === false) throw new APIError("UNAUTHORIZED");
1176
- }
1310
+ if (session !== void 0 && session !== null && referenceId !== void 0 && referenceId !== null && referenceId !== "" && referenceId !== session.user.id) await authorizeBillingReference(ctx, options, {
1311
+ user: session.user,
1312
+ session: session.session,
1313
+ referenceId,
1314
+ action: "verify-transaction"
1315
+ });
1177
1316
  try {
1178
1317
  await ctx.context.adapter.update({
1179
1318
  model: "paystackTransaction",
@@ -1338,23 +1477,18 @@ const listSubscriptions = (options, path = "/list-subscriptions") => {
1338
1477
  if (subscriptionOptions?.enabled !== true) throw new APIError("BAD_REQUEST", { message: "Subscriptions are not enabled in the Paystack options." });
1339
1478
  const session = await getSessionFromCtx(ctx);
1340
1479
  if (session === void 0 || session === null) throw new APIError("UNAUTHORIZED");
1480
+ const store = createBillingStore(ctx);
1341
1481
  const referenceIdPart = ctx.context.referenceId;
1342
1482
  const queryRefId = ctx.query?.referenceId ?? (typeof ctx.request?.url === "string" ? new URL(ctx.request.url).searchParams.get("referenceId") ?? void 0 : void 0);
1343
1483
  const userId = session.user.id;
1344
- if (queryRefId !== void 0 && queryRefId !== userId && referenceIdPart !== queryRefId) await assertReferenceAccess(ctx, options, {
1484
+ if (queryRefId !== void 0 && queryRefId !== userId && referenceIdPart !== queryRefId) await authorizeBillingReference(ctx, options, {
1345
1485
  user: session.user,
1346
1486
  session: session.session,
1347
1487
  referenceId: queryRefId,
1348
1488
  action: "list-subscriptions"
1349
1489
  });
1350
1490
  const referenceId = queryRefId ?? referenceIdPart ?? userId;
1351
- const res = await ctx.context.adapter.findMany({
1352
- model: "subscription",
1353
- where: [{
1354
- field: "referenceId",
1355
- value: referenceId
1356
- }]
1357
- });
1491
+ const res = await store.findSubscriptionsByReference(referenceId);
1358
1492
  return ctx.json({ subscriptions: res });
1359
1493
  });
1360
1494
  };
@@ -1370,24 +1504,19 @@ const listTransactions = (options, path = "/list-transactions") => {
1370
1504
  }, async (ctx) => {
1371
1505
  const session = await getSessionFromCtx(ctx);
1372
1506
  if (session === void 0 || session === null) throw new APIError("UNAUTHORIZED");
1507
+ const store = createBillingStore(ctx);
1373
1508
  const referenceIdPart = ctx.context.referenceId;
1374
1509
  const queryRefId = ctx.query?.referenceId ?? (typeof ctx.request?.url === "string" ? new URL(ctx.request.url).searchParams.get("referenceId") ?? void 0 : void 0);
1375
1510
  const userId = session.user.id;
1376
- if (queryRefId !== void 0 && queryRefId !== userId && referenceIdPart !== queryRefId) await assertReferenceAccess(ctx, options, {
1511
+ if (queryRefId !== void 0 && queryRefId !== userId && referenceIdPart !== queryRefId) await authorizeBillingReference(ctx, options, {
1377
1512
  user: session.user,
1378
1513
  session: session.session,
1379
1514
  referenceId: queryRefId,
1380
1515
  action: "list-transactions"
1381
1516
  });
1382
1517
  const referenceId = queryRefId ?? referenceIdPart ?? userId;
1383
- const sorted = (await ctx.context.adapter.findMany({
1384
- model: "paystackTransaction",
1385
- where: [{
1386
- field: "referenceId",
1387
- value: referenceId
1388
- }]
1389
- })).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
1390
- return ctx.json({ transactions: sorted });
1518
+ const transactions = await store.listTransactions(referenceId);
1519
+ return ctx.json({ transactions });
1391
1520
  });
1392
1521
  };
1393
1522
  const enableDisableBodySchema = z.object({
@@ -1586,8 +1715,8 @@ const listProducts = (_options, path = "/list-products") => {
1586
1715
  method: "GET",
1587
1716
  metadata: { openapi: { operationId: "listPaystackProducts" } }
1588
1717
  }, async (ctx) => {
1589
- const sorted = (await ctx.context.adapter.findMany({ model: "paystackProduct" })).sort((a, b) => a.name.localeCompare(b.name));
1590
- return ctx.json({ products: sorted });
1718
+ const products = await createBillingStore(ctx).listProducts();
1719
+ return ctx.json({ products });
1591
1720
  });
1592
1721
  };
1593
1722
  const listPlans = (_options, path = "/list-plans") => {
@@ -1597,7 +1726,7 @@ const listPlans = (_options, path = "/list-plans") => {
1597
1726
  use: [sessionMiddleware]
1598
1727
  }, async (ctx) => {
1599
1728
  try {
1600
- const plans = await ctx.context.adapter.findMany({ model: "paystackPlan" });
1729
+ const plans = await createBillingStore(ctx).listPlans();
1601
1730
  return ctx.json({ plans });
1602
1731
  } catch (error) {
1603
1732
  ctx.context.logger.error("Failed to list plans", error);
@@ -1890,22 +2019,16 @@ const getSchema = (options) => {
1890
2019
  //#endregion
1891
2020
  //#region src/operations.ts
1892
2021
  async function syncPaystackProducts(ctx, options) {
1893
- const paystack = getPaystackOps(options.paystackClient);
2022
+ const paystack = createPaystackAdapter(options.paystackClient);
2023
+ const store = createBillingStore(ctx);
1894
2024
  try {
1895
- const productsData = unwrapSdkResult(await paystack?.product?.list({}));
2025
+ const productsData = await paystack.listProducts();
1896
2026
  if (!Array.isArray(productsData)) return {
1897
2027
  status: "success",
1898
2028
  count: 0
1899
2029
  };
1900
2030
  for (const product of productsData) {
1901
2031
  const paystackId = String(product.id);
1902
- const existing = await ctx.context.adapter.findOne({
1903
- model: "paystackProduct",
1904
- where: [{
1905
- field: "paystackId",
1906
- value: paystackId
1907
- }]
1908
- });
1909
2032
  const productFields = {
1910
2033
  name: product.name ?? "",
1911
2034
  description: product.description ?? "",
@@ -1918,20 +2041,9 @@ async function syncPaystackProducts(ctx, options) {
1918
2041
  metadata: product.metadata !== void 0 && product.metadata !== null ? JSON.stringify(product.metadata) : void 0,
1919
2042
  updatedAt: /* @__PURE__ */ new Date()
1920
2043
  };
1921
- if (existing !== void 0 && existing !== null) await ctx.context.adapter.update({
1922
- model: "paystackProduct",
1923
- update: productFields,
1924
- where: [{
1925
- field: "id",
1926
- value: String(existing.id)
1927
- }]
1928
- });
1929
- else await ctx.context.adapter.create({
1930
- model: "paystackProduct",
1931
- data: {
1932
- ...productFields,
1933
- createdAt: /* @__PURE__ */ new Date()
1934
- }
2044
+ await store.upsertProductByPaystackId(paystackId, {
2045
+ ...productFields,
2046
+ createdAt: /* @__PURE__ */ new Date()
1935
2047
  });
1936
2048
  }
1937
2049
  return {
@@ -1944,25 +2056,19 @@ async function syncPaystackProducts(ctx, options) {
1944
2056
  }
1945
2057
  }
1946
2058
  async function syncPaystackPlans(ctx, options) {
1947
- const paystack = getPaystackOps(options.paystackClient);
2059
+ const paystack = createPaystackAdapter(options.paystackClient);
2060
+ const store = createBillingStore(ctx);
1948
2061
  try {
1949
- const plansData = unwrapSdkResult(await paystack?.plan?.list());
2062
+ const plansData = await paystack.listPlans();
1950
2063
  if (!Array.isArray(plansData)) return {
1951
2064
  status: "success",
1952
2065
  count: 0
1953
2066
  };
1954
2067
  for (const plan of plansData) {
1955
2068
  const paystackId = String(plan.id);
1956
- const existing = await ctx.context.adapter.findOne({
1957
- model: "paystackPlan",
1958
- where: [{
1959
- field: "paystackId",
1960
- value: paystackId
1961
- }]
1962
- });
1963
2069
  const planData = {
1964
2070
  name: plan.name ?? "",
1965
- description: plan.description ?? "",
2071
+ description: typeof plan.description === "string" ? plan.description : "",
1966
2072
  amount: plan.amount ?? 0,
1967
2073
  currency: plan.currency ?? "",
1968
2074
  interval: plan.interval ?? "",
@@ -1971,20 +2077,9 @@ async function syncPaystackPlans(ctx, options) {
1971
2077
  metadata: plan.metadata !== void 0 && plan.metadata !== null ? JSON.stringify(plan.metadata) : void 0,
1972
2078
  updatedAt: /* @__PURE__ */ new Date()
1973
2079
  };
1974
- if (existing !== void 0 && existing !== null) await ctx.context.adapter.update({
1975
- model: "paystackPlan",
1976
- update: planData,
1977
- where: [{
1978
- field: "id",
1979
- value: existing.id
1980
- }]
1981
- });
1982
- else await ctx.context.adapter.create({
1983
- model: "paystackPlan",
1984
- data: {
1985
- ...planData,
1986
- createdAt: /* @__PURE__ */ new Date()
1987
- }
2080
+ await store.upsertPlanByPaystackId(paystackId, {
2081
+ ...planData,
2082
+ createdAt: /* @__PURE__ */ new Date()
1988
2083
  });
1989
2084
  }
1990
2085
  return {
@@ -1998,13 +2093,8 @@ async function syncPaystackPlans(ctx, options) {
1998
2093
  }
1999
2094
  async function chargeSubscriptionRenewal(ctx, options, input) {
2000
2095
  const { subscriptionId, amount: bodyAmount } = input;
2001
- const subscription = await ctx.context.adapter.findOne({
2002
- model: "subscription",
2003
- where: [{
2004
- field: "id",
2005
- value: subscriptionId
2006
- }]
2007
- });
2096
+ const store = createBillingStore(ctx);
2097
+ const subscription = await store.findSubscriptionById(subscriptionId);
2008
2098
  if (subscription === void 0 || subscription === null) throw new APIError("NOT_FOUND", { message: "Subscription not found" });
2009
2099
  if (subscription.paystackAuthorizationCode === void 0 || subscription.paystackAuthorizationCode === null || subscription.paystackAuthorizationCode === "") throw new APIError("BAD_REQUEST", { message: "No authorization code found for this subscription" });
2010
2100
  const plan = (await getPlans(options.subscription)).find((candidate) => candidate.name.toLowerCase() === subscription.plan.toLowerCase());
@@ -2015,35 +2105,14 @@ async function chargeSubscriptionRenewal(ctx, options, input) {
2015
2105
  let billingUserId = subscription.userId;
2016
2106
  const referenceId = subscription.referenceId;
2017
2107
  if (referenceId !== void 0 && referenceId !== null && referenceId !== "") {
2018
- const user = await ctx.context.adapter.findOne({
2019
- model: "user",
2020
- where: [{
2021
- field: "id",
2022
- value: referenceId
2023
- }]
2024
- });
2108
+ const user = await store.findUser(referenceId);
2025
2109
  if (user !== void 0 && user !== null) {
2026
2110
  email = user.email;
2027
2111
  billingUserId = user.id;
2028
2112
  } else if (options.organization?.enabled === true) {
2029
- const ownerMember = await ctx.context.adapter.findOne({
2030
- model: "member",
2031
- where: [{
2032
- field: "organizationId",
2033
- value: referenceId
2034
- }, {
2035
- field: "role",
2036
- value: "owner"
2037
- }]
2038
- });
2113
+ const ownerMember = await store.findOrganizationOwner(referenceId);
2039
2114
  if (ownerMember !== void 0 && ownerMember !== null) {
2040
- const ownerUser = await ctx.context.adapter.findOne({
2041
- model: "user",
2042
- where: [{
2043
- field: "id",
2044
- value: ownerMember.userId
2045
- }]
2046
- });
2115
+ const ownerUser = await store.findUser(ownerMember.userId);
2047
2116
  email = ownerUser?.email;
2048
2117
  billingUserId = ownerUser?.id ?? ownerMember.userId;
2049
2118
  }
@@ -2055,7 +2124,7 @@ async function chargeSubscriptionRenewal(ctx, options, input) {
2055
2124
  message: `Amount ${amount} is less than the minimum required for ${finalCurrency}.`,
2056
2125
  status: 400
2057
2126
  });
2058
- const chargeData = unwrapSdkResult(await getPaystackOps(options.paystackClient)?.transaction?.chargeAuthorization({ body: {
2127
+ const typedChargeData = await createPaystackAdapter(options.paystackClient).chargeAuthorization({
2059
2128
  email,
2060
2129
  amount,
2061
2130
  authorization_code: subscription.paystackAuthorizationCode,
@@ -2064,51 +2133,41 @@ async function chargeSubscriptionRenewal(ctx, options, input) {
2064
2133
  subscriptionId,
2065
2134
  referenceId
2066
2135
  })
2067
- } }));
2068
- if (chargeData?.status === "success" && chargeData.reference !== void 0) {
2136
+ });
2137
+ if (typedChargeData?.status === "success" && typedChargeData.reference !== void 0) {
2069
2138
  const now = /* @__PURE__ */ new Date();
2070
2139
  const nextPeriodEnd = getNextPeriodEnd(now, plan.interval ?? "monthly");
2071
- await ctx.context.adapter.create({
2072
- model: "paystackTransaction",
2073
- data: {
2074
- reference: chargeData.reference,
2075
- paystackId: chargeData.id !== void 0 && chargeData.id !== null ? String(chargeData.id) : void 0,
2076
- referenceId,
2077
- userId: billingUserId,
2078
- amount: chargeData.amount,
2079
- currency: chargeData.currency,
2080
- status: "success",
2081
- plan: plan.name.toLowerCase(),
2082
- metadata: JSON.stringify({
2083
- type: "renewal",
2084
- subscriptionId,
2085
- referenceId
2086
- }),
2087
- createdAt: now,
2088
- updatedAt: now
2089
- }
2140
+ await store.createTransaction({
2141
+ reference: typedChargeData.reference,
2142
+ paystackId: typedChargeData.id !== void 0 && typedChargeData.id !== null ? String(typedChargeData.id) : void 0,
2143
+ referenceId,
2144
+ userId: billingUserId,
2145
+ amount: typedChargeData.amount,
2146
+ currency: typedChargeData.currency,
2147
+ status: "success",
2148
+ plan: plan.name.toLowerCase(),
2149
+ metadata: JSON.stringify({
2150
+ type: "renewal",
2151
+ subscriptionId,
2152
+ referenceId
2153
+ }),
2154
+ createdAt: now,
2155
+ updatedAt: now
2090
2156
  });
2091
- await ctx.context.adapter.update({
2092
- model: "subscription",
2093
- update: {
2094
- periodStart: now,
2095
- periodEnd: nextPeriodEnd,
2096
- updatedAt: now,
2097
- paystackTransactionReference: chargeData.reference
2098
- },
2099
- where: [{
2100
- field: "id",
2101
- value: subscription.id
2102
- }]
2157
+ await store.updateSubscription(subscription.id, {
2158
+ periodStart: now,
2159
+ periodEnd: nextPeriodEnd,
2160
+ updatedAt: now,
2161
+ paystackTransactionReference: typedChargeData.reference
2103
2162
  });
2104
2163
  return {
2105
2164
  status: "success",
2106
- data: chargeData
2165
+ data: typedChargeData
2107
2166
  };
2108
2167
  }
2109
2168
  return {
2110
2169
  status: "failed",
2111
- data: chargeData
2170
+ data: typedChargeData
2112
2171
  };
2113
2172
  }
2114
2173
  //#endregion
@@ -2151,23 +2210,14 @@ const createPaystackPlugin = (options) => {
2151
2210
  user: { create: { async after(user, hookCtx) {
2152
2211
  if (!hookCtx || options.createCustomerOnSignUp !== true || user.email === null || user.email === void 0 || user.email === "") return;
2153
2212
  try {
2154
- const paystackOps = getPaystackOps(options.paystackClient);
2155
- if (!paystackOps) return;
2156
- const sdkRes = unwrapSdkResult(await paystackOps.customer?.create({ body: {
2213
+ const sdkRes = await createPaystackAdapter(options.paystackClient).createCustomer({
2157
2214
  email: user.email,
2158
2215
  first_name: user.name ?? void 0,
2159
2216
  metadata: JSON.stringify({ userId: user.id })
2160
- } }) ?? await Promise.reject(/* @__PURE__ */ new Error("Paystack client missing customer ops")));
2217
+ });
2161
2218
  const customerCode = sdkRes?.customer_code;
2162
2219
  if (customerCode !== void 0 && customerCode !== null && customerCode !== "") {
2163
- await ctx.adapter.update({
2164
- model: "user",
2165
- where: [{
2166
- field: "id",
2167
- value: user.id
2168
- }],
2169
- update: { paystackCustomerCode: customerCode }
2170
- });
2220
+ await createBillingStoreFromAdapter(ctx.adapter).saveCustomerCode(user.id, customerCode, false);
2171
2221
  if (typeof options.onCustomerCreate === "function") await options.onCustomerCreate({
2172
2222
  paystackCustomer: sdkRes,
2173
2223
  user: {
@@ -2185,23 +2235,9 @@ const createPaystackPlugin = (options) => {
2185
2235
  const extraCreateParams = typeof options.organization?.getCustomerCreateParams === "function" ? await options.organization.getCustomerCreateParams(org, hookCtx) : {};
2186
2236
  let targetEmail = org.email;
2187
2237
  if (targetEmail === void 0 || targetEmail === null) {
2188
- const ownerMember = await ctx.adapter.findOne({
2189
- model: "member",
2190
- where: [{
2191
- field: "organizationId",
2192
- value: org.id
2193
- }, {
2194
- field: "role",
2195
- value: "owner"
2196
- }]
2197
- });
2198
- if (ownerMember !== null && ownerMember !== void 0) targetEmail = (await ctx.adapter.findOne({
2199
- model: "user",
2200
- where: [{
2201
- field: "id",
2202
- value: ownerMember.userId
2203
- }]
2204
- }))?.email;
2238
+ const store = createBillingStoreFromAdapter(ctx.adapter);
2239
+ const ownerMember = await store.findOrganizationOwner(org.id);
2240
+ if (ownerMember !== null && ownerMember !== void 0) targetEmail = (await store.findUser(ownerMember.userId))?.email;
2205
2241
  }
2206
2242
  if (targetEmail === void 0 || targetEmail === null) return;
2207
2243
  const params = defu({
@@ -2209,19 +2245,10 @@ const createPaystackPlugin = (options) => {
2209
2245
  first_name: org.name,
2210
2246
  metadata: JSON.stringify({ organizationId: org.id })
2211
2247
  }, extraCreateParams);
2212
- const paystackOps = getPaystackOps(options.paystackClient);
2213
- if (!paystackOps) return;
2214
- const sdkRes = unwrapSdkResult(await paystackOps.customer?.create({ body: params }) ?? await Promise.reject(/* @__PURE__ */ new Error("Paystack client missing customer ops")));
2248
+ const sdkRes = await createPaystackAdapter(options.paystackClient).createCustomer(params);
2215
2249
  const customerCode = sdkRes?.customer_code;
2216
2250
  if (customerCode !== void 0 && customerCode !== null && customerCode !== "" && sdkRes !== void 0 && sdkRes !== null) {
2217
- await ctx.adapter.update({
2218
- model: "organization",
2219
- where: [{
2220
- field: "id",
2221
- value: org.id
2222
- }],
2223
- update: { paystackCustomerCode: customerCode }
2224
- });
2251
+ await createBillingStoreFromAdapter(ctx.adapter).saveCustomerCode(org.id, customerCode, true);
2225
2252
  if (typeof options.organization?.onCustomerCreate === "function") await options.organization.onCustomerCreate({
2226
2253
  paystackCustomer: sdkRes,
2227
2254
  organization: {