@alexasomba/better-auth-paystack
Version:
Production-ready Paystack billing plugin for Better Auth. Supports subscriptions, one-time payments, organization billing, secure webhooks and more
2,760 lines • 106 kB
JavaScript
import { a as getMetadataBoolean, c as hasPaystackMetadata, i as createRenewalMetadata, l as parsePaystackMetadata, n as createCheckoutMetadata, o as getMetadataNumber, r as createProrationMetadata, s as getMetadataString, t as PACKAGE_VERSION, u as stringifyPaystackMetadata } from "./version-B4mP7Bib.mjs";
import { HIDE_METADATA, defineErrorCodes } from "better-auth";
import { APIError, createAuthEndpoint, createAuthMiddleware, getSessionFromCtx, originCheck, sessionMiddleware } from "better-auth/api";
import { defu } from "defu";
import { z } from "zod";
import { PaystackError, PaystackResponse } from "@alexasomba/paystack-node";
import { mergeSchema } from "better-auth/db";
//#region src/billing-store.ts
function sortSubscriptionsForCurrent(subscriptions) {
const statusRank = /* @__PURE__ */ new Map([
["active", 0],
["trialing", 1],
["incomplete", 2],
["past_due", 3],
["canceled", 4]
]);
return [...subscriptions].sort((a, b) => {
const rankA = statusRank.get(a.status) ?? 99;
const rankB = statusRank.get(b.status) ?? 99;
if (rankA !== rankB) return rankA - rankB;
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
});
}
function createBillingStore(ctx) {
return createBillingStoreFromAdapter(ctx.context.adapter);
}
function createBillingStoreFromAdapter(adapter) {
const findOne = async (model, where) => await adapter.findOne({
model,
where
}) ?? null;
const findMany = async (model, where) => await adapter.findMany({
model,
...where ? { where } : {}
}) ?? [];
return {
findSubscriptionById: (id) => findOne("subscription", [{
field: "id",
value: id
}]),
findSubscriptionByCode: (subscriptionCode) => findOne("subscription", [{
field: "paystackSubscriptionCode",
value: subscriptionCode
}]),
findSubscriptionsByReference: (referenceId) => findMany("subscription", [{
field: "referenceId",
value: referenceId
}]),
async findCurrentSubscription(referenceId, groupId) {
const subscriptions = await this.findSubscriptionsByReference(referenceId);
return sortSubscriptionsForCurrent(groupId === void 0 ? subscriptions : subscriptions.filter((subscription) => groupId === null ? subscription.groupId === void 0 || subscription.groupId === null || subscription.groupId === "" : subscription.groupId === groupId))[0] ?? null;
},
async retireCompetingSubscriptions(referenceId, groupId, exceptId) {
const competitors = (await this.findSubscriptionsByReference(referenceId)).filter((subscription) => subscription.id !== exceptId && (subscription.status === "active" || subscription.status === "trialing") && (groupId === null ? subscription.groupId === void 0 || subscription.groupId === null || subscription.groupId === "" : subscription.groupId === groupId));
const now = /* @__PURE__ */ new Date();
for (const subscription of competitors) await this.updateSubscription(subscription.id, {
status: "canceled",
cancelAtPeriodEnd: false,
canceledAt: now,
endedAt: now,
updatedAt: now
});
},
findSubscriptionsByTransactionReference: (reference) => findMany("subscription", [{
field: "paystackTransactionReference",
value: reference
}]),
createSubscription: async (data) => await adapter.create({
model: "subscription",
data
}),
updateSubscription: (id, update) => adapter.update({
model: "subscription",
update,
where: [{
field: "id",
value: id
}]
}),
updateSubscriptionByCode: (subscriptionCode, update) => adapter.update({
model: "subscription",
update,
where: [{
field: "paystackSubscriptionCode",
value: subscriptionCode
}]
}),
createTransaction: async (data) => await adapter.create({
model: "paystackTransaction",
data
}),
findTransactionByReference: (reference) => findOne("paystackTransaction", [{
field: "reference",
value: reference
}]),
updateTransactionByReference: (reference, update) => adapter.update({
model: "paystackTransaction",
update,
where: [{
field: "reference",
value: reference
}]
}),
async listTransactions(referenceId) {
return (await findMany("paystackTransaction", [{
field: "referenceId",
value: referenceId
}])).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
},
async listProducts() {
return (await findMany("paystackProduct")).sort((a, b) => a.name.localeCompare(b.name));
},
findProductByName: (name) => findOne("paystackProduct", [{
field: "name",
value: name
}]),
findProductBySlug: (slug) => findOne("paystackProduct", [{
field: "slug",
value: slug
}]),
async updateProduct(id, update) {
await adapter.update({
model: "paystackProduct",
update,
where: [{
field: "id",
value: id
}]
});
},
async upsertProductByPaystackId(paystackId, data) {
const existing = await findOne("paystackProduct", [{
field: "paystackId",
value: paystackId
}]);
if (existing?.id !== void 0) {
const { createdAt: _createdAt, ...update } = data;
await adapter.update({
model: "paystackProduct",
update,
where: [{
field: "id",
value: String(existing.id)
}]
});
return;
}
await adapter.create({
model: "paystackProduct",
data
});
},
listPlans: () => findMany("paystackPlan"),
findPlanByName: (name) => findOne("paystackPlan", [{
field: "name",
value: name
}]),
findPlanByCode: (planCode) => findOne("paystackPlan", [{
field: "planCode",
value: planCode
}]),
async upsertPlanByPaystackId(paystackId, data) {
const existing = await findOne("paystackPlan", [{
field: "paystackId",
value: paystackId
}]);
if (existing?.id !== void 0) {
const { createdAt: _createdAt, ...update } = data;
await adapter.update({
model: "paystackPlan",
update,
where: [{
field: "id",
value: existing.id
}]
});
return;
}
await adapter.create({
model: "paystackPlan",
data
});
},
findUser: (id) => findOne("user", [{
field: "id",
value: id
}]),
findOrganization: (id) => findOne("organization", [{
field: "id",
value: id
}]),
findOrganizationOwner: (organizationId) => findOne("member", [{
field: "organizationId",
value: organizationId
}, {
field: "role",
value: "owner"
}]),
listMembers: (organizationId) => findMany("member", [{
field: "organizationId",
value: organizationId
}]),
listTeams: (organizationId) => findMany("team", [{
field: "organizationId",
value: organizationId
}]),
async saveCustomerCode(referenceId, customerCode, isOrganization) {
await adapter.update({
model: isOrganization ? "organization" : "user",
update: { paystackCustomerCode: customerCode },
where: [{
field: "id",
value: referenceId
}]
});
}
};
}
//#endregion
//#region src/paystack-sdk.ts
/**
* Interface for checking if a result is a PaystackResponse from the SDK v1.9.1+
*/
function IsPaystackResponse(value) {
return value instanceof PaystackResponse;
}
/**
* Unwraps a Paystack SDK result, extracting the data or throwing an APIError if the request failed.
* Leverages the native .unwrap() method in SDK v1.9.1+ if available.
*/
function unwrapSdkResult(result) {
if (IsPaystackResponse(result)) try {
return result.unwrap();
} catch (e) {
if (e instanceof PaystackError) throw new APIError("BAD_REQUEST", {
message: e.message,
status: e.status
});
throw new APIError("BAD_REQUEST", { message: e?.message ?? "Paystack API error" });
}
let current = result;
while (current !== null && current !== void 0 && typeof current === "object") {
const body = current;
if (body.status === false) throw new APIError("BAD_REQUEST", { message: body.message ?? "Paystack API error" });
if ("authorization_url" in body || "reference" in body || "customer_code" in body) break;
if ("data" in body && body.data !== void 0 && body.data !== null && typeof body.data === "object") {
current = body.data;
continue;
}
break;
}
return current;
}
/**
* Returns the operations object from a Paystack client.
* For v1.9.1+, the client itself uses the grouped structure.
*/
function getPaystackOps(client) {
return client;
}
function createPaystackAdapter(client) {
const requireClient = () => {
if (client === void 0 || client === null) throw new APIError("BAD_REQUEST", { message: "Paystack client is not configured" });
return client;
};
return {
async initializeTransaction(body) {
return unwrapSdkResult(await requireClient().transaction?.initialize({ body }));
},
async verifyTransaction(reference) {
return unwrapSdkResult(await requireClient().transaction?.verify(reference));
},
async chargeAuthorization(body) {
return unwrapSdkResult(await requireClient().transaction?.chargeAuthorization({ body }));
},
async createCustomer(body) {
return unwrapSdkResult(await requireClient().customer?.create({ body }));
},
async fetchCustomer(emailOrCode) {
return unwrapSdkResult(await requireClient().customer?.fetch(emailOrCode));
},
async updateCustomer(emailOrCode, body) {
return unwrapSdkResult(await requireClient().customer?.update(emailOrCode, { body }));
},
async listProducts() {
return unwrapSdkResult(await requireClient().product?.list({}));
},
async fetchProduct(productId) {
return unwrapSdkResult(await requireClient().product?.fetch(productId));
},
async listPlans() {
return unwrapSdkResult(await requireClient().plan?.list());
},
async createSubscription(body) {
return unwrapSdkResult(await requireClient().subscription?.create({ body }));
},
async fetchSubscription(subscriptionCode) {
return unwrapSdkResult(await requireClient().subscription?.fetch(subscriptionCode));
},
async disableSubscription(body) {
return unwrapSdkResult(await requireClient().subscription?.disable({ body }));
},
async enableSubscription(body) {
return unwrapSdkResult(await requireClient().subscription?.enable({ body }));
},
async manageSubscriptionLink(subscriptionCode) {
return unwrapSdkResult(await requireClient().subscription?.manageLink(subscriptionCode));
}
};
}
//#endregion
//#region src/utils.ts
function getPlanSeatAmount(plan) {
if (plan.seatAmount !== void 0) {
if (typeof plan.seatAmount === "number" && Number.isFinite(plan.seatAmount)) return plan.seatAmount;
throw new Error(`Invalid seatAmount for plan '${plan.name}'. Expected a finite number.`);
}
if (plan.seatPriceId === void 0 || plan.seatPriceId === null || plan.seatPriceId === "") return;
const parsed = typeof plan.seatPriceId === "string" ? Number(plan.seatPriceId) : plan.seatPriceId;
if (typeof parsed === "number" && Number.isFinite(parsed)) return parsed;
throw new Error(`Invalid seatPriceId for plan '${plan.name}'. Expected a numeric amount in the smallest currency unit.`);
}
function calculatePlanAmount(plan, quantity) {
return (plan.amount ?? 0) + quantity * (getPlanSeatAmount(plan) ?? 0);
}
function normalizeSubscriptionGroup(group) {
const normalized = group?.trim().toLowerCase();
return normalized === void 0 || normalized === "" ? null : normalized;
}
function isLocalSubscriptionCode(subscriptionCode) {
return typeof subscriptionCode === "string" && (subscriptionCode.startsWith("LOC_") || subscriptionCode.startsWith("sub_local_"));
}
function isLocallyManagedSubscription(subscription) {
if (isLocalSubscriptionCode(subscription.paystackSubscriptionCode)) return true;
if (typeof subscription.paystackSubscriptionCode === "string" && subscription.paystackSubscriptionCode !== "") return false;
return subscription.paystackPlanCode === void 0 || subscription.paystackPlanCode === null || subscription.paystackPlanCode === "";
}
function assertLocallyManagedSubscription(subscription, action) {
if (!isLocallyManagedSubscription(subscription)) throw new Error(`Paystack-managed subscriptions do not support ${action}. Use local billing for seat-based or prorated subscription changes.`);
}
async function getPlans(subscriptionOptions) {
if (subscriptionOptions?.enabled === true) return typeof subscriptionOptions.plans === "function" ? subscriptionOptions.plans() : subscriptionOptions.plans;
throw new Error("Subscriptions are not enabled in the Paystack options.");
}
async function getPlanByName(options, name) {
if (typeof name !== "string" || name.trim() === "") return null;
if (options.subscription?.enabled === true) {
const plans = await getPlans(options.subscription);
const normalizedName = name.toLowerCase();
return plans.find((plan) => typeof plan.name === "string" && plan.name.toLowerCase() === normalizedName) ?? null;
}
return null;
}
async function getProducts(productOptions) {
if (productOptions?.products) return typeof productOptions.products === "function" ? await productOptions.products() : productOptions.products;
return [];
}
async function getProductByName(options, name) {
return await getProducts(options.products).then((products) => products !== void 0 && products !== null ? products.find((product) => product.name.toLowerCase() === name.toLowerCase()) ?? null : null);
}
function getNextPeriodEnd(startDate, interval) {
const date = new Date(startDate);
switch (interval) {
case "daily":
date.setDate(date.getDate() + 1);
break;
case "weekly":
date.setDate(date.getDate() + 7);
break;
case "monthly":
date.setMonth(date.getMonth() + 1);
break;
case "quarterly":
date.setMonth(date.getMonth() + 3);
break;
case "biannually":
date.setMonth(date.getMonth() + 6);
break;
case "annually":
date.setFullYear(date.getFullYear() + 1);
break;
default: date.setMonth(date.getMonth() + 1);
}
return date;
}
/**
* Validates if the amount meets Paystack's minimum transaction requirements.
* Amounts should be in the smallest currency unit (e.g., kobo, cents).
*/
function validateMinAmount(amount, currency) {
const min = {
NGN: 5e3,
GHS: 10,
ZAR: 100,
KES: 300,
USD: 200,
XOF: 100
}[currency.toUpperCase()];
return min !== void 0 ? amount >= min : true;
}
async function syncProductQuantityFromPaystack(ctx, productName, paystackClient) {
const store = createBillingStore(ctx);
let localProduct = await store.findProductByName(productName);
localProduct ??= await store.findProductBySlug(productName.toLowerCase().replace(/\s+/g, "-"));
if (localProduct?.paystackId === void 0 || localProduct.paystackId === null || localProduct.paystackId === "") {
if (localProduct?.id !== void 0 && localProduct.unlimited !== true && typeof localProduct.quantity === "number" && localProduct.quantity > 0) await store.updateProduct(localProduct.id, {
quantity: localProduct.quantity - 1,
updatedAt: /* @__PURE__ */ new Date()
});
return;
}
try {
const paystackProductId = Number(localProduct.paystackId);
if (!Number.isFinite(paystackProductId)) return;
const remoteQuantity = (await createPaystackAdapter(paystackClient).fetchProduct(paystackProductId))?.quantity;
if (remoteQuantity !== void 0 && localProduct.id !== void 0) await store.updateProduct(localProduct.id, {
quantity: remoteQuantity,
updatedAt: /* @__PURE__ */ new Date()
});
} catch {
if (localProduct?.id !== void 0 && localProduct.unlimited !== true && typeof localProduct.quantity === "number" && localProduct.quantity > 0) await store.updateProduct(localProduct.id, {
quantity: localProduct.quantity - 1,
updatedAt: /* @__PURE__ */ new Date()
});
}
}
async function syncSubscriptionSeats(ctx, organizationId, options) {
if (options.subscription?.enabled !== true) return;
const store = createBillingStore(ctx);
const subscriptions = (await store.findSubscriptionsByReference(organizationId)).filter((subscription) => subscription.status === "active" || subscription.status === "trialing");
const seatSubscriptions = [];
for (const candidate of subscriptions) {
const candidatePlan = await getPlanByName(options, candidate.plan);
if (candidatePlan !== null && getPlanSeatAmount(candidatePlan) !== void 0) seatSubscriptions.push(candidate);
}
const quantity = (await store.listMembers(organizationId)).length;
for (const subscription of seatSubscriptions) {
if (subscription.paystackSubscriptionCode === void 0 || subscription.paystackSubscriptionCode === null || subscription.paystackSubscriptionCode === "") continue;
try {
assertLocallyManagedSubscription(subscription, "automatic seat sync");
await store.updateSubscription(subscription.id, {
seats: quantity,
updatedAt: /* @__PURE__ */ new Date()
});
} catch (e) {
ctx.context.logger.error("Failed to sync subscription seats", e);
}
}
}
//#endregion
//#region src/reference-access.ts
const DEFAULT_BILLING_ORG_ROLES = ["owner", "admin"];
function normalizeBillingRoles(roles) {
return new Set(roles.map((value) => value.trim()).filter((value) => value !== ""));
}
function getBillingRoles(options) {
return options.organization?.billingRoles ?? DEFAULT_BILLING_ORG_ROLES;
}
function hasBillingRole(role, billingRoles = DEFAULT_BILLING_ORG_ROLES) {
const allowedRoles = normalizeBillingRoles(billingRoles);
if (Array.isArray(role)) return role.some((value) => hasBillingRole(value, billingRoles));
if (typeof role !== "string") return false;
return role.split(",").map((value) => value.trim()).some((value) => allowedRoles.has(value));
}
function resolveBillingReferenceId(input) {
const body = input.body ?? {};
const query = input.query ?? {};
const requestQueryReferenceId = typeof input.requestUrl === "string" ? new URL(input.requestUrl).searchParams.get("referenceId") ?? void 0 : void 0;
return body.referenceId ?? query.referenceId ?? requestQueryReferenceId ?? input.fallbackUserId;
}
async function authorizeBillingReference(ctx, options, data) {
if (data.referenceId === data.user.id) return;
if (options.subscription?.enabled === true && typeof options.subscription.authorizeReference === "function") {
if (await options.subscription.authorizeReference({
user: data.user,
session: data.session,
referenceId: data.referenceId,
action: data.action
}, ctx) === true) return;
throw new APIError("UNAUTHORIZED");
}
if (options.organization?.enabled === true) {
const member = await ctx.context.adapter.findOne({
model: "member",
where: [{
field: "userId",
value: data.user.id
}, {
field: "organizationId",
value: data.referenceId
}]
});
if (member !== null && member !== void 0 && hasBillingRole(member.role, getBillingRoles(options))) return;
}
throw new APIError("UNAUTHORIZED");
}
//#endregion
//#region src/middleware.ts
const referenceMiddleware = (options, action) => createAuthMiddleware(async (ctx) => {
const session = ctx.context.session;
if (session === null || session === void 0) throw new APIError("UNAUTHORIZED");
const referenceId = resolveBillingReferenceId({
body: ctx.body,
query: ctx.query,
requestUrl: ctx.request?.url,
fallbackUserId: session.user.id
});
await authorizeBillingReference(ctx, options, {
user: session.user,
session: session.session,
referenceId,
action
});
return { context: {
...ctx.context,
referenceId
} };
});
//#endregion
//#region src/limits.ts
const getOrganizationSubscription = async (ctx, organizationId, groupId) => {
return createBillingStore(ctx).findCurrentSubscription(organizationId, groupId);
};
const checkSeatLimit = async (ctx, organizationId, seatsToAdd = 1) => {
const store = createBillingStore(ctx);
const seatLimit = (await store.findSubscriptionsByReference(organizationId)).filter((subscription) => subscription.status === "active" || subscription.status === "trialing").reduce((maximum, subscription) => typeof subscription.seats === "number" ? Math.max(maximum ?? subscription.seats, subscription.seats) : maximum, void 0);
const members = await store.listMembers(organizationId);
if (seatLimit === void 0) return true;
if (members.length + seatsToAdd > seatLimit) throw new APIError("FORBIDDEN", { message: `Organization member limit reached. Used: ${members.length}, Max: ${seatLimit}` });
return true;
};
async function getOrganizationEntitlements(ctx, organizationId, options) {
const subscriptions = (await createBillingStore(ctx).findSubscriptionsByReference(organizationId)).filter((subscription) => subscription.status === "active" || subscription.status === "trialing");
const limits = {};
const features = /* @__PURE__ */ new Set();
for (const subscription of subscriptions) {
const plan = await getPlanByName(options, subscription.plan);
for (const [name, value] of Object.entries(plan?.limits ?? {})) if (typeof value === "number" && Number.isFinite(value)) limits[name] = Math.max(limits[name] ?? value, value);
for (const feature of plan?.features ?? []) features.add(feature);
}
return {
limits,
features: [...features]
};
}
const checkTeamLimit = async (ctx, organizationId, maxTeams) => {
if ((await createBillingStore(ctx).listTeams(organizationId)).length >= maxTeams) throw new APIError("FORBIDDEN", { message: `Organization team limit reached. Max teams: ${maxTeams}` });
return true;
};
//#endregion
//#region src/subscription-lifecycle.ts
async function scheduleSubscriptionLifecycleChange(ctx, input) {
const groupId = normalizeSubscriptionGroup(input.plan?.group);
if (input.plan !== void 0 && input.scheduleAtPeriodEnd === true) {
const existingSub = input.subscriptionId === void 0 ? await getOrganizationSubscription(ctx, input.referenceId, groupId) : await createBillingStore(ctx).findSubscriptionById(input.subscriptionId);
if (existingSub?.status === "active") {
await ctx.context.adapter.update({
model: "subscription",
where: [{
field: "id",
value: existingSub.id
}],
update: {
pendingPlan: input.plan.name,
updatedAt: /* @__PURE__ */ new Date()
}
});
return {
kind: "scheduled",
status: "success",
message: "Plan change scheduled at period end.",
scheduled: true
};
}
}
if (input.cancelAtPeriodEnd === true) {
const existingSub = input.subscriptionId === void 0 ? await getOrganizationSubscription(ctx, input.referenceId, input.plan === void 0 ? void 0 : groupId) : await createBillingStore(ctx).findSubscriptionById(input.subscriptionId);
if (existingSub?.status === "active") {
await ctx.context.adapter.update({
model: "subscription",
where: [{
field: "id",
value: existingSub.id
}],
update: {
cancelAtPeriodEnd: true,
cancelAt: existingSub.periodEnd ?? null,
canceledAt: /* @__PURE__ */ new Date(),
updatedAt: /* @__PURE__ */ new Date()
}
});
return {
kind: "scheduled",
status: "success",
message: "Subscription cancellation scheduled at period end.",
scheduled: true
};
}
}
return null;
}
async function resolveTrialLifecycle(ctx, input) {
const requestedDays = input.plan?.freeTrial?.days !== void 0 && input.plan.freeTrial.days > 0 ? input.plan.freeTrial.days : 0;
const requested = requestedDays > 0;
if (!requested) return {
requestedDays,
requested: false,
granted: false
};
if ((await ctx.context.adapter.findMany({
model: "subscription",
where: [{
field: "referenceId",
value: input.referenceId
}]
}))?.some((subscription) => subscription.trialStart !== void 0 && subscription.trialStart !== null || subscription.trialEnd !== void 0 && subscription.trialEnd !== null || subscription.status === "trialing") === true) return {
requestedDays,
requested,
granted: false,
deniedReason: "already_used"
};
const trialStart = /* @__PURE__ */ new Date();
const trialEnd = /* @__PURE__ */ new Date();
trialEnd.setDate(trialEnd.getDate() + requestedDays);
return {
trialStart,
trialEnd,
requestedDays,
requested,
granted: true
};
}
async function resolveCheckoutTargetEmail(ctx, options, input) {
const targetEmail = input.email ?? input.user.email;
if (options.organization?.enabled !== true || input.referenceId === input.user.id || input.referenceId === "") return targetEmail;
const org = await ctx.context.adapter.findOne({
model: "organization",
where: [{
field: "id",
value: input.referenceId
}]
});
if (org === void 0 || org === null) return targetEmail;
const orgWithEmail = org;
if (orgWithEmail.email !== void 0 && orgWithEmail.email !== null && orgWithEmail.email !== "") return orgWithEmail.email;
const ownerMember = await ctx.context.adapter.findOne({
model: "member",
where: [{
field: "organizationId",
value: input.referenceId
}, {
field: "role",
value: "owner"
}]
});
if (ownerMember === void 0 || ownerMember === null) return targetEmail;
const ownerUser = await ctx.context.adapter.findOne({
model: "user",
where: [{
field: "id",
value: ownerMember.userId
}]
});
return ownerUser?.email !== void 0 && ownerUser.email !== "" ? ownerUser.email : targetEmail;
}
async function handleProratedUpgrade(ctx, options, input) {
const store = createBillingStore(ctx);
const existingSub = input.subscriptionId === void 0 ? await store.findCurrentSubscription(input.referenceId, normalizeSubscriptionGroup(input.plan.group)) : await store.findSubscriptionById(input.subscriptionId);
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;
const now = /* @__PURE__ */ new Date();
const periodEnd = new Date(existingSub.periodEnd);
const periodStart = new Date(existingSub.periodStart);
const totalDays = Math.max(1, Math.ceil((periodEnd.getTime() - periodStart.getTime()) / (1e3 * 60 * 60 * 24)));
const remainingDays = Math.max(0, Math.ceil((periodEnd.getTime() - now.getTime()) / (1e3 * 60 * 60 * 24)));
let oldAmount = 0;
if (existingSub.plan !== "") {
const oldPlan = await getPlanByName(options, existingSub.plan) ?? await store.findPlanByName(existingSub.plan);
if (oldPlan !== void 0 && oldPlan !== null) oldAmount = calculatePlanAmount(oldPlan, existingSub.seats);
}
let membersCount = 1;
let newSeatCount;
let newAmount;
try {
assertLocallyManagedSubscription(existingSub, "plan or seat changes");
if (getPlanSeatAmount(input.plan) !== void 0) {
const members = await store.listMembers(input.referenceId);
membersCount = members.length > 0 ? members.length : 1;
}
newSeatCount = input.quantity ?? existingSub.seats ?? membersCount;
newAmount = calculatePlanAmount(input.plan, newSeatCount);
} catch (error) {
throw new APIError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Invalid seat configuration for plan." });
}
const costDifference = newAmount - oldAmount;
const serializedProrationMetadata = stringifyPaystackMetadata(createProrationMetadata({
subscriptionId: existingSub.id,
referenceId: input.referenceId,
newPlan: input.plan.name.toLowerCase(),
oldPlan: existingSub.plan,
newSeatCount,
remainingDays
}));
let completedProrationReference;
if (costDifference > 0 && remainingDays > 0) {
const proratedAmount = Math.round(costDifference / totalDays * remainingDays);
if (proratedAmount < 5e3) throw new APIError("BAD_REQUEST", {
message: "Prorated upgrade amount is below Paystack's minimum charge. Schedule the change for period end instead.",
status: 400
});
const paystack = createPaystackAdapter(options.paystackClient);
if (existingSub.paystackAuthorizationCode !== void 0 && existingSub.paystackAuthorizationCode !== null && existingSub.paystackAuthorizationCode !== "") {
const sdkRes = await paystack.chargeAuthorization({
email: input.targetEmail,
amount: proratedAmount,
authorization_code: existingSub.paystackAuthorizationCode,
reference: `upg_${existingSub.id}_${Date.now()}_${Math.random().toString(36).substring(7)}`,
metadata: serializedProrationMetadata
});
if (sdkRes?.status !== "success") throw new APIError("BAD_REQUEST", { message: "Failed to process prorated charge via saved authorization." });
await store.createTransaction({
reference: sdkRes.reference ?? "",
paystackId: sdkRes.id !== void 0 && sdkRes.id !== null ? String(sdkRes.id) : void 0,
referenceId: input.referenceId,
userId: input.userId,
amount: sdkRes.amount ?? proratedAmount,
currency: sdkRes.currency ?? input.finalCurrency,
status: "success",
plan: input.plan.name.toLowerCase(),
metadata: serializedProrationMetadata,
createdAt: /* @__PURE__ */ new Date(),
updatedAt: /* @__PURE__ */ new Date()
});
completedProrationReference = sdkRes.reference ?? void 0;
} else {
const initRes = await paystack.initializeTransaction({
email: input.targetEmail,
amount: proratedAmount,
currency: input.finalCurrency,
callback_url: input.callbackURL ?? void 0,
metadata: serializedProrationMetadata,
...input.allowedSubscriptionChannels !== void 0 ? { channels: input.allowedSubscriptionChannels } : {}
});
await store.createTransaction({
reference: initRes?.reference ?? "",
referenceId: input.referenceId,
userId: input.userId,
amount: proratedAmount,
currency: input.finalCurrency,
status: "pending",
plan: input.plan.name.toLowerCase(),
metadata: serializedProrationMetadata,
createdAt: /* @__PURE__ */ new Date(),
updatedAt: /* @__PURE__ */ new Date()
});
return {
kind: "checkout",
url: initRes?.authorization_url,
reference: initRes?.reference,
accessCode: initRes?.access_code,
redirect: true
};
}
}
await store.updateSubscription(existingSub.id, {
plan: input.plan.name,
seats: newSeatCount,
...completedProrationReference !== void 0 ? { paystackTransactionReference: completedProrationReference } : {},
updatedAt: /* @__PURE__ */ new Date()
});
return {
kind: "prorated",
status: "success",
message: "Subscription successfully upgraded with prorated charge.",
prorated: true
};
}
//#endregion
//#region src/reconciliation.ts
function getAllowedSubscriptionChannels$1(options) {
const channels = options.subscription?.allowedPaymentChannels;
return Array.isArray(channels) && channels.length > 0 ? channels : void 0;
}
function isAllowedSubscriptionChannel(channel, allowedChannels) {
if (allowedChannels === void 0) return true;
return channel !== void 0 && channel !== null && allowedChannels.includes(channel);
}
function createSummary() {
return {
transaction: {
found: false,
updated: false
},
subscription: {
found: false,
updated: false,
prorationApplied: false
},
customer: { saved: false },
product: { synced: false }
};
}
function getErrorMessage(error, fallback) {
return error instanceof Error && error.message !== "" ? error.message : fallback;
}
function createFailureResult(input) {
return {
ok: false,
source: input.source,
status: input.status,
reference: input.reference,
data: input.data,
error: input.error,
...input.summary
};
}
function throwOrReturnFailure(input) {
if (input.throwOnError) throw new APIError(input.apiStatus, {
code: input.error.code,
message: input.error.message,
status: input.error.status
});
return createFailureResult(input);
}
function hasReferenceMismatch(input) {
return input.expectedReferenceId !== void 0 && input.expectedReferenceId !== "" && input.transactionReferenceId !== void 0 && input.transactionReferenceId !== null && input.transactionReferenceId !== "" && input.expectedReferenceId !== input.transactionReferenceId;
}
function getNonEmptyString(value) {
return typeof value === "string" && value !== "" ? value : void 0;
}
async function reconcilePaystackTransaction(ctx, options, input) {
const source = input.source ?? "server";
const throwOnError = input.throwOnError === true;
const summary = createSummary();
const paystack = getPaystackOps(options.paystackClient);
let data;
try {
data = unwrapSdkResult(await paystack?.transaction?.verify(input.reference));
} catch (error) {
ctx.context.logger.error("Failed to verify Paystack transaction", error);
return throwOrReturnFailure({
throwOnError,
apiStatus: "BAD_REQUEST",
source,
status: "error",
reference: input.reference,
data: null,
summary,
error: {
code: "FAILED_TO_VERIFY_TRANSACTION",
message: getErrorMessage(error, "Failed to verify transaction"),
status: 400
}
});
}
if (data === void 0 || data === null) return throwOrReturnFailure({
throwOnError,
apiStatus: "BAD_REQUEST",
source,
status: "error",
reference: input.reference,
data: null,
summary,
error: {
code: "FAILED_TO_VERIFY_TRANSACTION",
message: "Failed to fetch transaction data from Paystack.",
status: 400
}
});
const status = data.status ?? "failed";
const reference = data.reference ?? input.reference;
const paystackIdRaw = data.id;
const paystackId = paystackIdRaw !== void 0 && paystackIdRaw !== null ? String(paystackIdRaw) : void 0;
const authorizationCode = data.authorization?.authorization_code;
const store = createBillingStore(ctx);
const txRecord = await store.findTransactionByReference(reference);
summary.transaction.found = txRecord !== null;
summary.transaction.previousStatus = txRecord?.status;
if (hasReferenceMismatch({
expectedReferenceId: input.referenceId,
transactionReferenceId: txRecord?.referenceId
})) return throwOrReturnFailure({
throwOnError,
apiStatus: "UNAUTHORIZED",
source,
status,
reference,
data,
summary,
error: {
code: "REFERENCE_ID_MISMATCH",
message: "Transaction reference does not belong to the expected billing reference.",
status: 401
}
});
const referenceId = input.referenceId ?? getNonEmptyString(txRecord?.referenceId) ?? getNonEmptyString(input.actor?.user.id);
summary.transaction.referenceId = referenceId;
if (input.actor !== void 0 && referenceId !== void 0 && referenceId !== input.actor.user.id) try {
await authorizeBillingReference(ctx, options, {
user: input.actor.user,
session: input.actor.session,
referenceId,
action: "verify-transaction"
});
} catch (error) {
return throwOrReturnFailure({
throwOnError,
apiStatus: "UNAUTHORIZED",
source,
status,
reference,
data,
summary,
error: {
code: "UNAUTHORIZED",
message: getErrorMessage(error, "Not authorized to reconcile this transaction."),
status: 401
}
});
}
const transactionUpdate = {
status,
paystackId,
amount: data.amount,
currency: data.currency,
updatedAt: /* @__PURE__ */ new Date()
};
const updatedTransaction = await store.updateTransactionByReference(reference, transactionUpdate);
summary.transaction.updated = updatedTransaction !== null;
summary.transaction.status = status;
if (status !== "success") return {
ok: true,
source,
status,
reference,
data,
...summary
};
const allowedSubscriptionChannels = getAllowedSubscriptionChannels$1(options);
if ((txRecord?.plan !== void 0 && txRecord.plan !== null && txRecord.plan !== "" || Boolean(data.plan)) && isAllowedSubscriptionChannel(data.channel ?? void 0, allowedSubscriptionChannels) === false) {
await store.updateTransactionByReference(reference, {
...transactionUpdate,
status: "failed"
});
summary.transaction.updated = true;
summary.transaction.status = "failed";
return throwOrReturnFailure({
throwOnError,
apiStatus: "BAD_REQUEST",
source,
status: "failed",
reference,
data,
summary,
error: {
code: "SUBSCRIPTION_PAYMENT_CHANNEL_NOT_ALLOWED",
message: `This subscription requires one of: ${allowedSubscriptionChannels?.join(", ") ?? "allowed channels"}.`,
status: 400
}
});
}
const paystackCustomerCodeFromPaystack = data.customer?.customer_code;
if (paystackCustomerCodeFromPaystack !== void 0 && paystackCustomerCodeFromPaystack !== null && paystackCustomerCodeFromPaystack !== "" && referenceId !== void 0 && referenceId !== "") {
let isOrganization = options.organization?.enabled === true && typeof referenceId === "string" && referenceId.startsWith("org_");
if (isOrganization === false && options.organization?.enabled === true) isOrganization = await store.findOrganization(referenceId) !== null;
await store.saveCustomerCode(referenceId, paystackCustomerCodeFromPaystack, isOrganization);
summary.customer.saved = true;
summary.customer.referenceId = referenceId;
summary.customer.customerCode = paystackCustomerCodeFromPaystack;
summary.customer.model = isOrganization ? "organization" : "user";
}
const transaction = updatedTransaction ?? await store.findTransactionByReference(reference);
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);
summary.product.synced = true;
summary.product.name = transaction.product;
}
if (options.subscription?.enabled !== true) return {
ok: true,
source,
status,
reference,
data,
...summary
};
const metadataObj = parsePaystackMetadata(data.metadata);
const isTrial = getMetadataBoolean(metadataObj, "isTrial");
const trialEnd = getMetadataString(metadataObj, "trialEnd");
const targetPlan = getMetadataString(metadataObj, "plan");
if (metadataObj.type === "proration") {
const subscriptionId = getMetadataString(metadataObj, "subscriptionId");
const newPlan = getMetadataString(metadataObj, "newPlan");
const newSeatCount = getMetadataNumber(metadataObj, "newSeatCount");
if (subscriptionId !== void 0 && subscriptionId !== "" && newPlan !== void 0 && newPlan !== "") {
const updatedSubscription = await store.updateSubscription(subscriptionId, {
plan: newPlan,
...typeof newSeatCount === "number" ? { seats: newSeatCount } : {},
paystackTransactionReference: reference,
...authorizationCode !== void 0 && authorizationCode !== null ? { paystackAuthorizationCode: authorizationCode } : {},
updatedAt: /* @__PURE__ */ new Date()
});
summary.subscription.found = updatedSubscription !== null;
summary.subscription.updated = updatedSubscription !== null;
summary.subscription.id = updatedSubscription?.id ?? subscriptionId;
summary.subscription.status = updatedSubscription?.status;
summary.subscription.prorationApplied = updatedSubscription !== null;
}
return {
ok: true,
source,
status,
reference,
data,
...summary
};
}
let paystackSubscriptionCode;
const targetSub = (await store.findSubscriptionsByTransactionReference(reference)).find((subscription) => referenceId === void 0 || referenceId === "" || subscription.referenceId === referenceId);
summary.subscription.found = targetSub !== void 0;
summary.subscription.id = targetSub?.id;
summary.subscription.status = targetSub?.status;
if (isTrial && targetPlan !== void 0 && trialEnd !== void 0) {
const email = data.customer?.email;
const planConfig = (await getPlans(options.subscription)).find((plan) => plan.name.toLowerCase() === targetPlan.toLowerCase());
if (planConfig !== void 0 && planConfig !== null && (planConfig.planCode === void 0 || planConfig.planCode === null || planConfig.planCode === "")) paystackSubscriptionCode = `LOC_${reference}`;
else if (targetSub?.paystackSubscriptionCode !== void 0 && targetSub.paystackSubscriptionCode !== null && targetSub.paystackSubscriptionCode !== "") paystackSubscriptionCode = targetSub.paystackSubscriptionCode;
else 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: {
customer: email,
plan: planConfig.planCode,
authorization: authorizationCode,
start_date: trialEnd
} }))?.subscription_code;
} else if (isTrial === false) {
const planCodeFromPaystack = data.plan?.plan_code;
if (planCodeFromPaystack === void 0 || planCodeFromPaystack === null || planCodeFromPaystack === "") paystackSubscriptionCode = `LOC_${reference}`;
else paystackSubscriptionCode = data.subscription?.subscription_code ?? void 0;
}
let updatedSubscription = null;
if (targetSub !== void 0 && targetSub !== null) {
const resolvedPlan = (await getPlans(options.subscription)).find((candidate) => candidate.name.toLowerCase() === targetSub.plan.toLowerCase());
updatedSubscription = await store.updateSubscription(targetSub.id, {
status: isTrial ? "trialing" : "active",
billingInterval: resolvedPlan?.interval ?? targetSub.billingInterval ?? null,
periodStart: /* @__PURE__ */ new Date(),
updatedAt: /* @__PURE__ */ new Date(),
...isTrial && trialEnd !== void 0 ? {
trialStart: /* @__PURE__ */ new Date(),
trialEnd: new Date(trialEnd),
periodEnd: new Date(trialEnd)
} : {},
...paystackSubscriptionCode !== void 0 ? { paystackSubscriptionCode } : {},
...authorizationCode !== void 0 && authorizationCode !== null ? { paystackAuthorizationCode: authorizationCode } : {}
});
summary.subscription.updated = updatedSubscription !== null;
summary.subscription.id = updatedSubscription?.id ?? targetSub.id;
summary.subscription.status = updatedSubscription?.status ?? targetSub.status;
if (updatedSubscription !== null && (updatedSubscription.status === "active" || updatedSubscription.status === "trialing")) await store.retireCompetingSubscriptions(updatedSubscription.referenceId, updatedSubscription.groupId ?? null, updatedSubscription.id);
}
if (updatedSubscription !== void 0 && updatedSubscription !== null) {
const plan = (await getPlans(options.subscription)).find((candidate) => candidate.name.toLowerCase() === updatedSubscription.plan.toLowerCase());
if (plan !== void 0) {
const callbackData = {
event: data,
subscription: updatedSubscription,
plan
};
for (const callback of [options.subscription?.onSubscriptionComplete, options.subscription?.onSubscriptionUpdate]) try {
await callback?.(callbackData, ctx);
} catch (error) {
ctx.context.logger.error("Paystack subscription callback failed", error);
}
if (targetSub?.status === "trialing" && updatedSubscription.status === "active") try {
await plan.freeTrial?.onTrialEnd?.(updatedSubscription);
} catch (error) {
ctx.context.logger.error("Paystack trial end callback failed", error);
}
}
}
return {
ok: true,
source,
status,
reference,
data,
...summary
};
}
//#endregion
//#region src/route-modules/checkout.ts
const initializeTransactionBodySchema = z.object({
plan: z.string().optional(),
product: z.string().optional(),
amount: z.number().int().positive().optional(),
currency: z.string().optional(),
email: z.string().optional(),
metadata: z.record(z.string(), z.unknown()).optional(),
referenceId: z.string().optional(),
subscriptionId: z.string().optional(),
callbackURL: z.string().optional(),
quantity: z.number().int().positive().optional(),
scheduleAtPeriodEnd: z.boolean().optional(),
cancelAtPeriodEnd: z.boolean().optional(),
prorateAndCharge: z.boolean().optional()
});
//#endregion
//#region src/route-modules/catalog.ts
async function listStoredProducts(ctx) {
return createBillingStore(ctx).listProducts();
}
async function listStoredPlans(ctx) {
return createBillingStore(ctx).listPlans();
}
async function getConfiguredCatalog(options) {
return {
plans: options.subscription?.enabled === true ? await getPlans(options.subscription) : [],
products: await getProducts(options.products)
};
}
//#endregion
//#region src/route-modules/subscriptions.ts
const enableDisableBodySchema = z.object({
referenceId: z.string().optional(),
subscriptionCode: z.string(),
emailToken: z.string().optional(),
atPeriodEnd: z.boolean().optional()
});
function decodeBase64UrlToString(value) {
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
const padded = normalized + "===".slice((normalized.length + 3) % 4);
const binaryString = atob(padded);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) bytes[i] = binaryString.charCodeAt(i);
return new TextDecoder().decode(bytes);
}
function tryGetEmailTokenFromSubscriptionManageLink(link) {
try {
const subscriptionToken = new URL(link).searchParams.get("subscription_token");
if (subscriptionToken === void 0 || subscriptionToken === null || subscriptionToken === "") return void 0;
const parts = subscriptionToken.split(".");
if (parts.length < 2) return void 0;
const payloadJson = decodeBase64UrlToString(parts[1] ?? "");
const payload = JSON.parse(payloadJson);
return typeof payload.email_token === "string" ? payload.email_token : void 0;
} catch {
return;
}
}
//#endregion
//#region src/route-modules/webhook.ts
function getWebhookRequest(ctx) {
return ctx.requestClone ?? ctx.request;
}
function getWebhookHeaders(ctx) {
return ctx.headers ?? ctx.request?.headers;
}
function getWebhookClientIP(ctx, headers) {
return headers?.get("x-forwarded-for")?.split(",")[0]?.trim() ?? headers?.get("x-real-ip") ?? ctx.request.ip;
}
//#endregion
//#region src/route-modules/shared.ts
const PAYSTACK_ERROR_CODES = defineErrorCodes({
SUBSCRIPTION_NOT_FOUND: "Subscription not found",
SUBSCRIPTION_PLAN_NOT_FOUND: "Subscription plan not found",
UNABLE_TO_CREATE_CUSTOMER: "Unable to create customer",
FAILED_TO_INITIALIZE_TRANSACTION: "Failed to initialize transaction",
FAILED_TO_VERIFY_TRANSACTION: "Failed to verify transaction",
FAILED_TO_DISABLE_SUBSCRIPTION: "Failed to disable subscription",
FAILED_TO_ENABLE_SUBSCRIPTION: "Failed to enable subscription",
EMAIL_VERIFICATION_REQUIRED: "Email verification is required before you can subscribe to a plan",
SUBSCRIPTION_PAYMENT_CHANNEL_NOT_ALLOWED: "This subscription only supports specific payment channels"
});
function getAllowedSubscriptionChannels(options) {
const channels = options.subscription?.allowedPaymentChannels;
return Array.isArray(channels) && channels.length > 0 ? channels : void 0;
}
async function hmacSha512Hex(secret, message) {
const encoder = new TextEncoder();
const keyData = encoder.encode(secret);
const msgData = encoder.encode(message);
const crypto = globalThis.crypto;
if (crypto !== void 0 && crypto !== null && "subtle" in crypto) {
const subtle = crypto.subtle;
const key = await subtle.importKey("raw", keyData, {
name: "HMAC",
hash: "SHA-512"
}, false, ["sign"]);
const signature = await subtle.sign("HMAC", key, msgData);
return Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("");
}
const { createHmac } = await import("node:crypto");
return createHmac("sha512", secret).update(message).digest("hex");
}
//#endregion
//#region src/routes.ts
const paystackWebhook = (options, path = "/webhook") => {
return createAuthEndpoint(path, {
method: "POST",
metadata: {
...HIDE_METADATA,
openapi: { operationId: "handlePaystackWebhook" }
},
cloneRequest: true,
disableBody: true
}, async (ctx) => {
const request = getWebhookRequest(ctx);
if (request === void 0 || request === null) throw new APIError("BAD_REQUEST", { message: "Request object is missing from context" });
const payload = await request.text();
const headers = getWebhookHeaders(ctx);
const signature = headers?.get("x-paystack-signature");
if (options.webhook?.verifyIP === true) {
const trustedIPs = options.webhook.trustedIPs ?? [
"52.31.139.75",
"52.49.173.169",
"52.214.14.220"
];
const clientIP = getWebhookClientIP(ctx, headers);
if (clientIP !== void 0 && clientIP !== null && trustedIPs.includes(clientIP) === false) throw new APIError("UNAUTHORIZED", {
message: `Forbidden IP: ${clientIP}`,
status: 401
});
}
if (signature === void 0 || signature === null || signature === "") throw new APIError("UNAUTHORIZED", {
message: "Missing x-paystack-signature header",
status: 401
});
if (await hmacSha512Hex(options.secretKey, payload) !== signature) throw new APIError("UNAUTHORIZED", {
message: "Invalid Paystack webhook signature",
status: 401
});
const event = JSON.parse(payload);
const eventName = event.event;
const data = event.data;
if (eventName === "charge.success") {
const reference = data?.reference;
const paystackIdRaw = data?.id;
const paystackId = paystackIdRaw !== void 0 && paystackIdRaw !== null ? String(paystackIdRaw) : void 0;
if (reference !== void 0 && reference !== null && reference !== "") {
try {
await ctx.context.adapter.update({
model: "paystackTransaction",
update: {
status: "success",
paystackId,
updatedAt: /* @__PURE__ */ new Date()
},
where: [{
field: "reference",
value: reference
}]
});
} catch (e) {
ctx.context.logger.warn("Failed to update transaction status for charge.success", e);
}
try {
const transaction = await ctx.context.adapter.findOne({
model: "paystackTransaction",
where: [{
field: "reference",
value: reference
}]
});
if (transaction !== void 0 && transaction !== null && transaction.product !== void 0 && transaction.product !== null && transaction.product !== "") {
if (options.paystackClient !== void 0 && options.paystackClient !== null) await syncProductQuantityFromPaystack(ctx, transaction.product, options.paystackClient);
}
} catch (e) {
ctx.context.logger.warn("Failed to sync product quantity", e);
}
}
}
if (eventName === "charge.failure") {
const reference = data?.reference;
if (reference !== void 0 && reference !== null && reference !== "") try {
await ctx.context.adapter.update({
model: "paystackTransaction",
update: {
status: "failed",
updatedAt: /* @__PURE__ */ new Date()
},
where: [{
field: "reference",
value: reference
}]
});
} catch (e) {
ctx.context.logger.warn("Failed to update transaction status for charge.failure", e);
}
}
if (options.subscription?.enabled === true) try {
if (eventName === "subscription.create") {
const subscriptionData = data;
const subscriptionCode = subscriptionData.subscription_code ?? "";
const customerCode = subscriptionData.customer?.customer_code;
const planCode = subscriptionData.plan?.plan_code;
const metadataObj = parsePaystackMetadata(subscriptionData.metadata);
const referenceIdFromMetadata = typeof metadataObj.referenceId === "string" ? metadataObj.referenceId : void 0;
let planNameFromMetadata = typeof metadataObj.plan === "string" ? metadataObj.plan : void 0;
if (typeof planNameFromMetadata === "string") planNameFromMetadata = planNameFromMetadata.toLowerCase();
const plans = await getPlans(options.subscription);
const planFromCode = planCode !== void 0 && planCode !== null && planCode !== "" ? plans.find((p) => p.planCode === planCode) : void 0;
const groupIdFromMetadata = typeof metadataObj.groupId === "string" ? normalizeSubscriptionGroup(metadataObj.groupId) : normalizeSubscriptionGroup(planFromCode?.group);
const planPart = planFromCode?.name ?? planNameFromMetadata;
const planName = planPart !== void 0 && planPart !== null && planPart !== "" ? planPart.toLowerCase() : void 0;
if (subscriptionCode !== void 0 && subscriptionCode !== null && subscriptionCode !== "") {
const where = [];
if (referenceIdFromMetadata !== void 0 && referenceIdFromMetadata !== null && referenceIdFromMetadata !== "") where.push({
field: "referenceId",
value: referenceIdFromMetadata
});
else if (customerCode !== void 0 && customerCode !== null && customerCode !== "") where.push({
field: "paystackCustomerCode",
value: customerCode
});
if (planName !== void 0 && planName !== null && planName !== "") where.push({
field: "plan",
value: planName
});
if (where.length > 0) {
const subscription = (await ctx.context.adapter.findMany({
model: "subscription",
where
}))?.find((candidate) => groupIdFromMetadata === null ? candidate.groupId === void 0 || candidate.groupId === null || candidate.groupId === "" : candidate.groupId === groupIdFromMetadata);
if (subscription !== void 0 && subscription !== null) {
const plan = planFromCode ?? (planName !== void 0 && planName !== null && planName !== "" ? await getPlanByName(options, planName) : void 0);
const now = /* @__PURE__ */ new Date();
const persistedSubscription = {
...subscription,
paystackSubscriptionCode: subscriptionCode,
status: "active",
billingInterval: plan?.interval ?? subscription.billingInterval ?? null,
periodEnd: subscriptionData.next_payment_date !== void 0 && subscriptionData.next_payment_date !== null ? new Date(subscriptionData.next_payment_date) : subscription.periodEnd,
updatedAt: now
};
await ctx.context.adapter.update({
model: "subscription",
update: {
paystackSubscriptionCode: persistedSubscription.paystackSubscriptionCode,
status: persistedSubscription.status,
billingInterval: persistedSubscription.billingInterval,
periodEnd: persistedSubscription.periodEnd,
updatedAt: persistedSubscription.updatedAt
},
where: [{
field: "id",
value: subscription.id
}]
});
await createBillingStore(ctx).retireCompetingSubscriptions(subscription.referenceId, subscription.groupId ?? null, subscription.id);
if (plan !== void 0 && plan !== null) {
const callbackData = {
event,
subscription: persistedSubscription,
plan
};
for (const callback of [
options.subscription.onSubscriptionComplete,
options.subscription.onSubscriptionCreated,
options.subscription.onSubscriptionUpdate
]) try {
await callback?.(callbackData, ctx);
} catch (error) {
ctx.context.logger.error("Paystack subscription callback failed", error);
}
if (subscription.status === "trialing") try {
await plan.freeTrial?.onTrialEnd?.(persistedSubscription);
} catch (error) {
ctx.context.logger.error("Paystack trial end callback failed", error);
}
}
}
}
}
}
if (eventName === "subscription.disable" || eventName === "subscription.not_renew") {
const subscriptionData = data;
const subscriptionCode = subscriptionData.subscription_code ?? "";
if (subscriptionCode !== "") {
const existing = await ctx.context.adapter.findOne({
model: "subscription",
where: [{
field: "paystackSubscriptionCode",
value: subscriptionCode
}]
});
let newStatus = "canceled";
const nextPaymentDate = subscriptionData.next_payment_date;
const periodEnd = nextPaymentDate !== void 0 && nextPaymentDate !== null && nextPaymentDate !== "" ? new Date(nextPaymentDate) : existing?.periodEnd !== void 0 && existing.periodEnd !== null ? new Date(existing.periodEnd) : void 0;
if (periodEnd !== void 0 && periodEnd.getTime() > Date.now()) newStatus = "active";
const now = /* @__PURE__ */ new Date();
const persistedSubscription = existing === null || existing === void 0 ? void 0 : {
...existing,
status: newStatus,
cancelAtPeriodEnd: newStatus === "active",
cancelAt: newStatus === "active" ? periodEnd ?? null : null,
canceledAt: now,
endedAt: newStatus === "canceled" ? now : null,
...periodEnd ? { periodEnd } : {},
updatedAt: now
};
await ctx.context.adapter.update({
model: "subscription",
update: {
status: newStatus,
cancelAtPeriodEnd: newStatus === "active",
cancelAt: newStatus === "active" ? periodEnd ?? null : null,
canceledAt: now,
endedAt: newStatus === "canceled" ? now : null,
...periodEnd ? { periodEnd } : {},
updatedAt: now
},
where: [{
field: "paystackSubscriptionCode",
value: subscriptionCode
}]
});
if (persistedSubscription !== void 0) {
try {
await options.subscription.onSubscriptionCancel?.({
event,
subscription: persistedSubscription
}, ctx);
} catch (error) {
ctx.context.logger.error("Paystack subscription cancel callback failed", error);
}
const plan = await getPlanByName(options, persistedSubscription.plan);
if (plan !== void 0 && plan !== null) {
try {
await options.subscription.onSubscriptionUpdate?.({
event,
subscription: persistedSubscription,
plan
}, ctx);
} catch (error) {
ctx.context.logger.error("Paystack subscription update callback failed", error);
}
if (existing?.status === "trialing" && newStatus === "canceled") try {
await plan.freeTrial?.onTrialExpired?.(persistedSubscription);
} catch (error) {
ctx.context.logger.error("Paystack trial expiry callback failed", error);
}
}
}
}
}
if (eventName === "charge.success" || eventName === "invoice.update") {
const subscriptionCodeRaw = (data?.subscription)?.subscription_code ?? data?.subscription_code;
const subscriptionCode = subscriptionCodeRaw !== void 0 && subscriptionCodeRaw !== null && subscriptionCodeRaw !== "" ? subscriptionCodeRaw : void 0;
if (subscriptionCode !== void 0) {
const existingSub = await ctx.context.adapter.findOne({
model: "subscription",
where: [{
field: "paystackSubscriptionCode",
value: subscriptionCode
}]
});
if (existingSub !== void 0 && existingSub !== null && existingSub.pendingPlan !== void 0 && existingSub.pendingPlan !== null && existingSub.pendingPlan !== "") await ctx.context.adapter.update({
model: "subscription",
update: {
plan: existingSub.pendingPlan,
pendingPlan: null,
updatedAt: /* @__PURE__ */ new Date()
},
where: [{
field: "id",
value: existingSub.id
}]
});
}
}
} catch (_e) {
ctx.context.logger.error("Failed to sync Paystack webhook event", _e);
}
await options.onEvent?.(event);
return ctx.json({ received: true });
});
};
const initializeTransaction = (options, path = "/initialize-transaction") => {
const subscriptionOptions = options.subscription;
return createAuthEndpoint(path, {
method: "POST",
body: initializeTransactionBodySchema,
use: subscriptionOptions?.enabled === true ? [
sessionMiddleware,
originCheck,
referenceMiddleware(options, "initialize-transaction")
] : [sessionMiddleware, originCheck]
}, async (ctx) => {
const paystack = getPaystackOps(options.paystackClient);
const { plan: planName, product: productName, amount: bodyAmount, currency, email, metadata: extraMetadata, callbackURL, quantity, scheduleAtPeriodEnd, cancelAtPeriodEnd, prorateAndCharge, subscriptionId } = ctx.body;
if (callbackURL !== void 0 && callbackURL !== null && callbackURL !== "") {
const checkTrusted = () => {
try {
if (callbackURL?.startsWith("/") === true) return true;
const baseUrl = ctx.context?.baseURL ?? ctx.request?.url ?? "";
if (baseUrl === "") return false;
const baseOrigin = new URL(baseUrl).origin;
return new URL(callbackURL).origin === baseOrigin;
} catch {
return false;
}
};
if (checkTrusted() === false) throw new APIError("FORBIDDEN", {
message: "callbackURL is not a trusted origin.",
status: 403
});
}
const session = await getSessionFromCtx(ctx);
if (session === void 0 || session === null) throw new APIError("UNAUTHORIZED");
const user = session.user;
if (subscriptionOptions?.enabled === true && subscriptionOptions.requireEmailVerification === true && user.emailVerified !== true) throw new APIError("BAD_REQUEST", {
code: "EMAIL_VERIFICATION_REQUIRED",
message: PAYSTACK_ERROR_CODES.EMAIL_VERIFICATION_REQUIRED.message
});
let plan;
let product;
if (planName !== void 0 && planName !== null && planName !== "") {
if (subscriptionOptions?.enabled !== true) throw new APIError("BAD_REQUEST", { message: "Subscriptions are not enabled." });
plan = await getPlanByName(options, planName) ?? void 0;
if (plan === void 0 || plan === null) try {
const nativePlan = await ctx.context.adapter.findOne({
model: "paystackPlan",
where: [{
field: "name",
value: planName
}]
});
if (nativePlan !== void 0 && nativePlan !== null) plan = nativePlan;
else plan = await ctx.context.adapter.findOne({
model: "paystackPlan",
where: [{
field: "planCode",
value: planName
}]
}) ?? void 0;
} catch {
plan = void 0;
}
if (plan === void 0 || plan === null) throw new APIError("BAD_REQUEST", {
code: "SUBSCRIPTION_PLAN_NOT_FOUND",
message: PAYSTACK_ERROR_CODES.SUBSCRIPTION_PLAN_NOT_FOUND.message,
status: 400
});
} else if (productName !== void 0 && productName !== null && productName !== "") {
if (typeof productName === "string") {
product = await getProductByName(options, productName) ?? void 0;
product ??= await ctx.context.adapter.findOne({
model: "paystackProduct",
where: [{
field: "name",
value: productName
}]
}) ?? void 0;
}
if (product === void 0 || product === null) throw new APIError("BAD_REQUEST", {
message: `Product '${productName}' not found.`,
status: 400
});
} else if (bodyAmount === void 0 || bodyAmount === null) throw new APIError("BAD_REQUEST", {
message: "Either 'plan', 'product', or 'amount' is required to initialize a transaction.",
status: 400
});
let amount = bodyAmount ?? product?.price ?? product?.amount;
const finalCurrency = currency ?? product?.currency ?? product?.currency ?? plan?.currency ?? "NGN";
const referenceIdFromCtx = ctx.context.referenceId;
const referenceId = ctx.body.referenceId ?? referenceIdFromCtx ?? session.user.id;
const groupId = normalizeSubscriptionGroup(plan?.group);
if (subscriptionId !== void 0) {
const selectedSubscription = await createBillingStore(ctx).findSubscriptionById(subscriptionId);
if (selectedSubscription === null || selectedSubscription.referenceId !== referenceId || normalizeSubscriptionGroup(selectedSubscription.groupId) !== groupId) throw new APIError("BAD_REQUEST", { message: "Subscription does not belong to the authorized reference and plan group." });
}
const scheduledChange = await scheduleSubscriptionLifecycleChange(ctx, {
referenceId,
subscriptionId,
plan,
scheduleAtPeriodEnd,
cancelAtPeriodEnd
});
if (scheduledChange !== null) return ctx.json(scheduledChange);
if (plan !== void 0) try {
if (getPlanSeatAmount(plan) !== void 0) {
const members = await ctx.context.adapter.findMany({
model: "member",
where: [{
field: "organizationId",
value: referenceId
}]
});
const seatCount = members.length > 0 ? members.length : 1;
amount = calculatePlanAmount(plan, quantity ?? seatCount);
}
} catch (error) {
throw new APIError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Invalid seat configuration for plan." });
}
let url;
let reference;
let accessCode;
const trial = await resolveTrialLifecycle(ctx, {
referenceId,
plan
});
const { trialStart, trialEnd } = trial;
try {
const targetEmail = await resolveCheckoutTargetEmail(ctx, options, {
email,
referenceId,
user
});
const allowedSubscriptionChannels = plan ? getAllowedSubscriptionChannels(options) : void 0;
const metadata = stringifyPaystackMetadata(createCheckoutMetadata({
referenceId,
userId: user.id,
plan: plan !== void 0 ? plan.name.toLowerCase() : void 0,
groupId,
product: product !== void 0 ? product.name.toLowerCase() : void 0,
extra: extraMetadata,
trial: {
isTrial: trialStart !== void 0,
requested: trial.requested,
granted: trial.granted,
deniedReason: trial.deniedReason,
endsAt: trialEnd
}
}));
const initBody = {
email: targetEmail,
callback_url: callbackURL ?? void 0,
metadata,
currency: finalCurrency,
quantity
};
if (allowedSubscriptionChannels !== void 0) initBody.channels = allowedSubscriptionChannels;
if (plan !== void 0 && prorateAndCharge === true) {
const proration = await handleProratedUpgrade(ctx, options, {
plan,
referenceId,
subscriptionId,
quantity,
targetEmail,
userId: user.id,
finalCurrency,
callbackURL,
allowedSubscriptionChannels
});
if (proration?.kind === "checkout") return ctx.json({
kind: "checkout",
url: proration.url ?? "",
reference: proration.reference ?? "",
accessCode: proration.accessCode ?? "",
redirect: proration.redirect
});
if (proration?.kind === "prorated") return ctx.json({
kind: "prorated",
status: proration.status,
message: proration.message,
prorated: proration.prorated
});
}
if (plan !== void 0) if (trialStart !== void 0) initBody.amount = 5e3;
else {
initBody.plan = plan.planCode;
initBody.invoice_limit = plan.invoiceLimit;
let finalAmount;
if (amount !== void 0 && amount !== null) {
finalAmount = amount;
initBody.quantity = 1;
} else finalAmount = (plan.amount ?? 0) * (quantity ?? 1);
initBody.amount = Math.max(Math.round(finalAmount), 5e3);
}
else {
if (amount === void 0 || amount === null) throw new APIError("BAD_REQUEST", { message: "Amount is required for one-time payments" });
initBody.amount = Math.round(amount);
}
const sdkRes = unwrapSdkResult(await paystack?.transaction?.initialize({ body: initBody }));
url = sdkRes?.authorization_url;
reference = sdkRes?.reference;
accessCode = sdkRes?.access_code;
} catch (error) {
ctx.context.logger.error("Failed to initialize Paystack transaction", error);
throw new APIError("BAD_REQUEST", {
code: "FAILED_TO_INITIALIZE_TRANSACTION",
message: error instanceof Error ? error.message : PAYSTACK_ERROR_CODES.FAILED_TO_INITIALIZE_TRANSACTION.message
});
}
await ctx.context.adapter.create({
model: "paystackTransaction",
data: {
reference: reference ?? "",
referenceId,
userId: user.id,
amount: amount ?? 0,
currency: plan?.currency ?? currency ?? "NGN",
status: "pending",
plan: plan !== void 0 ? plan.name.toLowerCase() : void 0,
product: product !== void 0 ? product.name.toLowerCase() : void 0,
metadata: hasPaystackMetadata(extraMetadata) ? stringifyPaystackMetadata(extraMetadata) : void 0,
createdAt: /* @__PURE__ */ new Date(),
updatedAt: /* @__PURE__ */ new Date()
}
});
if (plan !== void 0) {
let storedCustomerCode = user.paystackCustomerCode;
if (options.organization?.enabled === true && referenceId !== user.id) {
const org = await ctx.context.adapter.findOne({
model: "organization",
where: [{
field: "id",
value: referenceId
}]
});
if (org !== void 0 && org !== null) {
const paystackOrg = org;
if (paystackOrg.paystackCustomerCode !== void 0 && paystackOrg.paystackCustomerCode !== null && paystackOrg.paystackCustomerCode !== "") storedCustomerCode = paystackOrg.paystackCustomerCode;
}
}
const newSubscription = await ctx.context.adapter.create({
model: "subscription",
data: {
plan: plan.name.toLowerCase(),
groupId,
referenceId,
userId: user.id,
paystackCustomerCode: storedCustomerCode ?? "",
paystackSubscriptionCode: "",
paystackPlanCode: plan.planCode,
paystackAuthorizationCode: "",
paystackTransactionReference: reference ?? "",
status: trialStart !== void 0 ? "trialing" : "incomplete",
billingInterval: plan.interval ?? null,
seats: quantity ?? 1,
periodStart: /* @__PURE__ */ new Date(),
periodEnd: new Date(Date.now() + 720 * 60 * 60 * 1e3),
cancelAtPeriodEnd: false,
trialStart,
trialEnd,
createdAt: /* @__PURE__ */ new Date(),
updatedAt: /* @__PURE__ */ new Date()
}
});
if (trialStart !== void 0 && newSubscription !== void 0 && newSubscription !== null && plan.freeTrial?.onTrialStart !== void 0) await plan.freeTrial.onTrialStart(newSubscription);
}
return ctx.json({
kind: "checkout",
url: url ?? "",
reference: reference ?? "",
accessCode: accessCode ?? "",
redirect: true
});
});
};
const createSubscription = (options, path = "/create-subscription") => initializeTransaction(options, path);
const upgradeSubscription = (options, path = "/upgrade-subscription") => initializeTransaction(options, path);
const cancelSubscription = (options, path = "/cancel-subscription") => disablePaystackSubscription(options, path);
const restoreSubscription = (options, path = "/restore-subscription") => enablePaystackSubscription(options, path);
const verifyTransaction = (options, path = "/verify-transaction") => {
return createAuthEndpoint(path, {
method: "POST",
body: z.object({ reference: z.string() }),
use: options.subscription?.enabled === true ? [
sessionMiddleware,
originCheck,
referenceMiddleware(options, "verify-transaction")
] : [sessionMiddleware, originCheck]
}, async (ctx) => {
const session = await getSessionFromCtx(ctx);
const result = await reconcilePaystackTransaction(ctx, options, {
reference: ctx.body.reference,
source: "browser",
actor: session !== void 0 && session !== null ? {
user: session.user,
session: session.session
} : void 0,
throwOnError: true
});
if (!result.ok || result.data === null) throw new APIError("BAD_REQUEST", {
code: result.error?.code ?? "FAILED_TO_VERIFY_TRANSACTION",
message: result.error?.message ?? PAYSTACK_ERROR_CODES.FAILED_TO_VERIFY_TRANSACTION.message,
status: result.error?.status
});
return ctx.json({
status: result.status,
reference: result.reference,
data: result.data
});
});
};
const listSubscriptions = (options, path = "/list-subscriptions") => {
const listQuerySchema = z.object({ referenceId: z.string().optional() });
const subscriptionOptions = options.subscription;
return createAuthEndpoint(path, {
method: "GET",
query: listQuerySchema,
use: subscriptionOptions?.enabled === true ? [
sessionMiddleware,
originCheck,
referenceMiddleware(options, "list-subscriptions")
] : [sessionMiddleware, originCheck]
}, async (ctx) => {
if (subscriptionOptions?.enabled !== true) throw new APIError("BAD_REQUEST", { message: "Subscriptions are not enabled in the Paystack options." });
const session = await getSessionFromCtx(ctx);
if (session === void 0 || session === null) throw new APIError("UNAUTHORIZED");
const store = createBillingStore(ctx);
const referenceIdPart = ctx.context.referenceId;
const queryRefId = ctx.query?.referenceId ?? (typeof ctx.request?.url === "string" ? new URL(ctx.request.url).searchParams.get("referenceId") ?? void 0 : void 0);
const userId = session.user.id;
if (queryRefId !== void 0 && queryRefId !== userId && referenceIdPart !== queryRefId) await authorizeBillingReference(ctx, options, {
user: session.user,
session: session.session,
referenceId: queryRefId,
action: "list-subscriptions"
});
const referenceId = queryRefId ?? referenceIdPart ?? userId;
const res = await store.findSubscriptionsByReference(referenceId);
return ctx.json({ subscriptions: res });
});
};
const listTransactions = (options, path = "/list-transactions") => {
return createAuthEndpoint(path, {
method: "GET",
query: z.object({ referenceId: z.string().optional() }),
use: options.subscription?.enabled === true ? [
sessionMiddleware,
originCheck,
referenceMiddleware(options, "list-transactions")
] : [sessionMiddleware, originCheck]
}, async (ctx) => {
const session = await getSessionFromCtx(ctx);
if (session === void 0 || session === null) throw new APIError("UNAUTHORIZED");
const store = createBillingStore(ctx);
const referenceIdPart = ctx.context.referenceId;
const queryRefId = ctx.query?.referenceId ?? (typeof ctx.request?.url === "string" ? new URL(ctx.request.url).searchParams.get("referenceId") ?? void 0 : void 0);
const userId = session.user.id;
if (queryRefId !== void 0 && queryRefId !== userId && referenceIdPart !== queryRefId) await authorizeBillingReference(ctx, options, {
user: session.user,
session: session.session,
referenceId: queryRefId,
action: "list-transactions"
});
const referenceId = queryRefId ?? referenceIdPart ?? userId;
const transactions = await store.listTransactions(referenceId);
return ctx.json({ transactions });
});
};
const disablePaystackSubscription = (options, path = "/disable-subscription") => {
return createAuthEndpoint(path, {
method: "POST",
body: enableDisableBodySchema,
use: options.subscription?.enabled === true ? [
sessionMiddleware,
originCheck,
referenceMiddleware(options, "disable-subscription")
] : [sessionMiddleware, originCheck]
}, async (ctx) => {
const { subscriptionCode, atPeriodEnd } = ctx.body;
const paystack = getPaystackOps(options.paystackClient);
try {
if (isLocalSubscriptionCode(subscriptionCode)) {
const sub = await ctx.context.adapter.findOne({
model: "subscription",
where: [{
field: "paystackSubscriptionCode",
value: subscriptionCode
}]
});
if (sub !== null && sub !== void 0) {
const now = /* @__PURE__ */ new Date();
const immediate = atPeriodEnd === false;
await ctx.context.adapter.update({
model: "subscription",
update: {
status: immediate ? "canceled" : "active",
cancelAtPeriodEnd: !immediate,
cancelAt: immediate ? null : sub.periodEnd ?? null,
canceledAt: now,
endedAt: immediate ? now : null,
updatedAt: now
},
where: [{
field: "id",
value: sub.id
}]
});
return ctx.json({ status: "success" });
}
throw new APIError("BAD_REQUEST", { message: "Subscription not found" });
}
let emailToken = ctx.body.emailToken;
let nextPaymentDate;
try {
const fetchRes = unwrapSdkResult(await paystack?.subscription?.fetch(subscriptionCode));
if (fetchRes !== void 0 && fetchRes !== null) {
emailToken ??= fetchRes.email_token ?? void 0;
nextPaymentDate = fetchRes.next_payment_date ?? void 0;
}
} catch {}
if (emailToken === void 0 || emailToken === null || emailToken === "") try {
const link = unwrapSdkResult(await paystack?.subscription?.manageLink(subscriptionCode))?.link;
if (link !== void 0 && link !== null && link !== "") emailToken = tryGetEmailTokenFromSubscriptionManageLink(link);
} catch {}
if (emailToken === void 0 || emailToken === null || emailToken === "") throw new Error("Could not retrieve email_token for subscription disable.");
await paystack?.subscription?.disable({ body: {
code: subscriptionCode,
token: emailToken
} });
const periodEnd = nextPaymentDate !== void 0 && nextPaymentDate !== null && nextPaymentDate !== "" ? new Date(nextPaymentDate) : void 0;
const sub = await ctx.context.adapter.findOne({
model: "subscription",
where: [{
field: "paystackSubscriptionCode",
value: subscriptionCode
}]
});
if (sub !== void 0 && sub !== null) {
const now = /* @__PURE__ */ new Date();
const immediate = atPeriodEnd === false;
await ctx.context.adapter.update({
model: "subscription",
update: {
status: immediate ? "canceled" : "active",
cancelAtPeriodEnd: !immediate,
cancelAt: immediate ? null : periodEnd ?? sub.periodEnd ?? null,
canceledAt: now,
endedAt: immediate ? now : null,
periodEnd,
updatedAt: now
},
where: [{
field: "id",
value: sub.id
}]
});
} else ctx.context.logger.warn(`Could not find subscription with code ${subscriptionCode} to disable`);
return ctx.json({ status: "success" });
} catch (error) {
ctx.context.logger.error("Failed to disable subscription", error);
throw new APIError("BAD_REQUEST", {
code: "FAILED_TO_DISABLE_SUBSCRIPTION",
message: error instanceof Error ? error.message : PAYSTACK_ERROR_CODES.FAILED_TO_DISABLE_SUBSCRIPTION.message
});
}
});
};
const enablePaystackSubscription = (options, path = "/enable-subscription") => {
return createAuthEndpoint(path, {
method: "POST",
body: enableDisableBodySchema,
use: options.subscription?.enabled === true ? [
sessionMiddleware,
originCheck,
referenceMiddleware(options, "enable-subscription")
] : [sessionMiddleware, originCheck]
}, async (ctx) => {
const { subscriptionCode } = ctx.body;
const paystack = getPaystackOps(options.paystackClient);
try {
let emailToken = ctx.body.emailToken;
if (emailToken === void 0 || emailToken === null || emailToken === "") try {
const fetchRes = unwrapSdkResult(await paystack?.subscription?.fetch(subscriptionCode));
if (fetchRes !== void 0 && fetchRes !== null) emailToken = fetchRes.email_token ?? void 0;
} catch {}
if (emailToken === void 0 || emailToken === null || emailToken === "") try {
const link = unwrapSdkResult(await paystack?.subscription?.manageLink(subscriptionCode))?.link;
if (link !== void 0 && link !== null && link !== "") emailToken = tryGetEmailTokenFromSubscriptionManageLink(link);
} catch {}
if (emailToken === void 0 || emailToken === null || emailToken === "") throw new APIError("BAD_REQUEST", { message: "Could not retrieve email_token for subscription enable." });
await paystack?.subscription?.enable({ body: {
code: subscriptionCode,
token: emailToken
} });
await ctx.context.adapter.update({
model: "subscription",
update: {
status: "active",
cancelAtPeriodEnd: false,
cancelAt: null,
canceledAt: null,
endedAt: null,
updatedAt: /* @__PURE__ */ new Date()
},
where: [{
field: "paystackSubscriptionCode",
value: subscriptionCode
}]
});
return ctx.json({ status: "success" });
} catch (error) {
ctx.context.logger.error("Failed to enable subscription", error);
throw new APIError("BAD_REQUEST", {
code: "FAILED_TO_ENABLE_SUBSCRIPTION",
message: error instanceof Error ? error.message : PAYSTACK_ERROR_CODES.FAILED_TO_ENABLE_SUBSCRIPTION.message
});
}
});
};
const getSubscriptionManageLink = (options, path = "/subscription-manage-link") => {
const manageLinkQuerySchema = z.object({ subscriptionCode: z.string() });
const useMiddlewares = options.subscription?.enabled === true ? [
sessionMiddleware,
originCheck,
referenceMiddleware(options, "get-subscription-manage-link")
] : [sessionMiddleware, originCheck];
const handler = async (ctx) => {
const { subscriptionCode } = ctx.query;
if (isLocalSubscriptionCode(subscriptionCode)) return ctx.json({
link: null,
message: "Local subscriptions cannot be managed on Paystack"
});
const paystack = getPaystackOps(options.paystackClient);
try {
const res = unwrapSdkResult(await paystack?.subscription?.manageLink(subscriptionCode));
return ctx.json({ link: res?.link || null });
} catch (error) {
ctx.context.logger.error("Failed to get subscription manage link", error);
throw new APIError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Failed to get subscription manage link" });
}
};
return createAuthEndpoint(path, {
method: "GET",
query: manageLinkQuerySchema,
use: useMiddlewares
}, handler);
};
const listProducts = (_options, path = "/list-products") => {
return createAuthEndpoint(path, {
method: "GET",
metadata: { openapi: { operationId: "listPaystackProducts" } }
}, async (ctx) => {
const products = await listStoredProducts(ctx);
return ctx.json({ products });
});
};
const listPlans = (_options, path = "/list-plans") => {
return createAuthEndpoint(path, {
method: "GET",
metadata: { ...HIDE_METADATA },
use: [sessionMiddleware]
}, async (ctx) => {
try {
const plans = await listStoredPlans(ctx);
return ctx.json({ plans });
} catch (error) {
ctx.context.logger.error("Failed to list plans", error);
throw new APIError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Failed to list plans" });
}
});
};
const getConfig = (options, path = "/get-config") => {
return createAuthEndpoint(path, {
method: "GET",
metadata: { openapi: { operationId: "getPaystackConfig" } }
}, async (ctx) => {
return ctx.json(await getConfiguredCatalog(options));
});
};
const transactions = { paystackTransaction: { fields: {
reference: {
type: "string",
required: true,
unique: true
},
paystackId: {
type: "string",
required: false
},
referenceId: {
type: "string",
required: true,
index: true
},
userId: {
type: "string",
required: true,
index: true
},
amount: {
type: "number",
required: true
},
currency: {
type: "string",
required: true
},
status: {
type: "string",
required: true
},
plan: {
type: "string",
required: false
},
product: {
type: "string",
required: false
},
metadata: {
type: "string",
required: false
},
createdAt: {
type: "date",
required: true
},
updatedAt: {
type: "date",
required: true
}
} } };
const subscriptions = { subscription: { fields: {
plan: {
type: "string",
required: true,
index: true
},
referenceId: {
type: "string",
required: true,
index: true
},
paystackCustomerCode: {
type: "string",
required: false,
index: true
},
paystackSubscriptionCode: {
type: "string",
required: false,
unique: true
},
paystackTransactionReference: {
type: "string",
required: false,
index: true
},
paystackAuthorizationCode: {
type: "string",
required: false
},
paystackEmailToken: {
type: "string",
required: false
},
status: {
type: "string",
defaultValue: "incomplete"
},
periodStart: {
type: "date",
required: false
},
periodEnd: {
type: "date",
required: false
},
trialStart: {
type: "date",
required: false
},
trialEnd: {
type: "date",
required: false
},
cancelAtPeriodEnd: {
type: "boolean",
required: false,
defaultValue: false
},
cancelAt: {
type: "date",
required: false
},
canceledAt: {
type: "date",
required: false
},
endedAt: {
type: "date",
required: false
},
billingInterval: {
type: "string",
required: false
},
groupId: {
type: "string",
required: false
},
seats: {
type: "number",
required: false
},
pendingPlan: {
type: "string",
required: false
}
} } };
const user = { user: { fields: { paystackCustomerCode: {
type: "string",
required: false,
index: true
} } } };
const organization = { organization: { fields: {
paystackCustomerCode: {
type: "string",
required: false,
index: true
},
email: {
type: "string",
required: false
}
} } };
const products = { paystackProduct: { fields: {
name: {
type: "string",
required: true
},
description: {
type: "string",
required: false
},
price: {
type: "number",
required: true
},
currency: {
type: "string",
required: true
},
quantity: {
type: "number",
required: false,
defaultValue: 0
},
unlimited: {
type: "boolean",
required: false,
defaultValue: true
},
paystackId: {
type: "string",
required: false,
unique: true
},
slug: {
type: "string",
required: true,
unique: true
},
metadata: {
type: "string",
required: false
},
createdAt: {
type: "date",
required: true
},
updatedAt: {
type: "date",
required: true
}
} } };
const plans = { paystackPlan: { fields: {
name: {
type: "string",
required: true
},
description: {
type: "string",
required: false
},
amount: {
type: "number",
required: true
},
currency: {
type: "string",
required: true
},
interval: {
type: "string",
required: true
},
group: {
type: "string",
required: false
},
planCode: {
type: "string",
required: true,
unique: true
},
paystackId: {
type: "string",
required: true,
unique: true
},
metadata: {
type: "string",
required: false
},
createdAt: {
type: "date",
required: true
},
updatedAt: {
type: "date",
required: true
}
} } };
({
...subscriptions,
...transactions,
...user,
...organization,
...products,
...plans
});
const getSchema = (options) => {
let baseSchema;
const optionSchema = options.schema;
if (options.subscription?.enabled === true) baseSchema = {
...subscriptions,
...transactions,
...user,
...products,
...plans
};
else baseSchema = {
...user,
...transactions,
...products,
...plans
};
if (options.organization?.enabled === true) baseSchema = {
...baseSchema,
...organization
};
if (options.schema !== void 0 && options.subscription?.enabled !== true && "subscription" in options.schema) {
const { subscription: _subscription, ...restSchema } = optionSchema ?? {};
return mergeSchema(baseSchema, restSchema);
}
return mergeSchema(baseSchema, optionSchema);
};
//#endregion
//#region src/customer.ts
function isNotFound(error) {
if (error === null || typeof error !== "object") return false;
const candidate = error;
return candidate.status === 404 || candidate.statusCode === 404;
}
function customerCode(customer) {
if (customer === null || typeof customer !== "object") return void 0;
const code = customer.customer_code;
return typeof code === "string" && code !== "" ? code : void 0;
}
function ownsCustomer(customer, reference) {
if (customer === null || typeof customer !== "object") return false;
const metadata = parsePaystackMetadata(customer.metadata);
const expectedIdKey = reference.type === "user" ? "userId" : "organizationId";
const otherIdKey = reference.type === "user" ? "organizationId" : "userId";
return metadata[expectedIdKey] === reference.id && metadata[otherIdKey] === void 0 && (metadata.customerType === void 0 || metadata.customerType === reference.type);
}
function hasOwner(customer) {
if (customer === null || typeof customer !== "object") return false;
const metadata = parsePaystackMetadata(customer.metadata);
return typeof metadata.userId === "string" || typeof metadata.organizationId === "string" || typeof metadata.customerType === "string";
}
function ownershipMetadata(reference, existing) {
return stringifyPaystackMetadata({
...parsePaystackMetadata(existing),
customerType: reference.type,
...reference.type === "user" ? { userId: reference.id } : { organizationId: reference.id }
});
}
async function resolvePaystackCustomer(input) {
const { adapter, client, logger, reference } = input;
const store = createBillingStoreFromAdapter(adapter);
const persisted = reference.type === "user" ? await store.findUser(reference.id) : await store.findOrganization(reference.id);
const existingCode = reference.paystackCustomerCode ?? persisted?.paystackCustomerCode;
if (typeof existingCode === "string" && existingCode !== "") return null;
const sdk = createPaystackAdapter(client);
let existing = null;
if (typeof client.customer?.fetch === "function") try {
existing = await sdk.fetchCustomer(reference.email) ?? null;
} catch (error) {
if (!isNotFound(error)) {
logger.error("Failed to look up Paystack customer; customer creation was skipped", error);
return null;
}
}
if (existing !== null) {
const code = customerCode(existing);
if (code === void 0) {
logger.error("Paystack customer lookup returned no customer code");
return null;
}
const owned = ownsCustomer(existing, reference);
const canClaimLegacy = reference.type === "user" && reference.emailVerified === true && !hasOwner(existing);
if (!owned && !canClaimLegacy) {
logger.error("Paystack customer belongs to another billing reference");
return null;
}
const existingMetadata = existing.metadata;
if (canClaimLegacy || parsePaystackMetadata(existingMetadata).customerType !== reference.type) await sdk.updateCustomer(code, { metadata: ownershipMetadata(reference, existingMetadata) });
await store.saveCustomerCode(reference.id, code, reference.type === "organization");
return {
customer: existing,
created: false
};
}
const customer = await sdk.createCustomer({
...input.createParams,
email: reference.email,
first_name: reference.name ?? void 0,
metadata: ownershipMetadata(reference)
});
const code = customerCode(customer);
if (code === void 0) return null;
await store.saveCustomerCode(reference.id, code, reference.type === "organization");
return {
customer,
created: true
};
}
//#endregion
//#region src/operations.ts
async function syncPaystackProducts(ctx, options) {
const paystack = createPaystackAdapter(options.paystackClient);
const store = createBillingStore(ctx);
try {
const productsData = await paystack.listProducts();
if (!Array.isArray(productsData)) return {
status: "success",
count: 0
};
for (const product of productsData) {
const paystackId = String(product.id);
const productFields = {
name: product.name ?? "",
description: product.description ?? "",
price: product.price ?? 0,
currency: product.currency ?? "",
quantity: product.quantity ?? 0,
unlimited: product.unlimited !== void 0 && product.unlimited !== null && product.unlimited !== false,
paystackId,
slug: product.slug ?? product.name?.toLowerCase().replace(/\s+/g, "-") ?? "",
metadata: stringifyPaystackMetadata(product.metadata),
updatedAt: /* @__PURE__ */ new Date()
};
await store.upsertProductByPaystackId(paystackId, {
...productFields,
createdAt: /* @__PURE__ */ new Date()
});
}
return {
status: "success",
count: productsData.length
};
} catch (error) {
ctx.context.logger.error("Failed to sync products", error);
throw new APIError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Failed to sync products" });
}
}
async function syncPaystackPlans(ctx, options) {
const paystack = createPaystackAdapter(options.paystackClient);
const store = createBillingStore(ctx);
try {
const plansData = await paystack.listPlans();
if (!Array.isArray(plansData)) return {
status: "success",
count: 0
};
for (const plan of plansData) {
const paystackId = String(plan.id);
const planData = {
name: plan.name ?? "",
description: typeof plan.description === "string" ? plan.description : "",
amount: plan.amount ?? 0,
currency: plan.currency ?? "",
interval: plan.interval ?? "",
planCode: plan.plan_code ?? "",
paystackId,
metadata: stringifyPaystackMetadata(plan.metadata),
updatedAt: /* @__PURE__ */ new Date()
};
await store.upsertPlanByPaystackId(paystackId, {
...planData,
createdAt: /* @__PURE__ */ new Date()
});
}
return {
status: "success",
count: plansData.length
};
} catch (error) {
ctx.context.logger.error("Failed to sync plans", error);
throw new APIError("BAD_REQUEST", { message: error instanceof Error ? error.message : "Failed to sync plans" });
}
}
async function chargeSubscriptionRenewal(ctx, options, input) {
const { subscriptionId, amount: bodyAmount } = input;
const store = createBillingStore(ctx);
const subscription = await store.findSubscriptionById(subscriptionId);
if (subscription === void 0 || subscription === null) throw new APIError("NOT_FOUND", { message: "Subscription not found" });
if (subscription.paystackAuthorizationCode === void 0 || subscription.paystackAuthorizationCode === null || subscription.paystackAuthorizationCode === "") throw new APIError("BAD_REQUEST", { message: "No authorization code found for this subscription" });
const plan = (await getPlans(options.subscription)).find((candidate) => candidate.name.toLowerCase() === subscription.plan.toLowerCase());
if (plan === void 0 || plan === null) throw new APIError("NOT_FOUND", { message: "Plan not found" });
const amount = bodyAmount ?? plan.amount;
if (amount === void 0 || amount === null) throw new APIError("BAD_REQUEST", { message: "Plan amount is not defined" });
let email;
let billingUserId = subscription.userId;
const referenceId = subscription.referenceId;
if (referenceId !== void 0 && referenceId !== null && referenceId !== "") {
const user = await store.findUser(referenceId);
if (user !== void 0 && user !== null) {
email = user.email;
billingUserId = user.id;
} else if (options.organization?.enabled === true) {
const ownerMember = await store.findOrganizationOwner(referenceId);
if (ownerMember !== void 0 && ownerMember !== null) {
const ownerUser = await store.findUser(ownerMember.userId);
email = ownerUser?.email;
billingUserId = ownerUser?.id ?? ownerMember.userId;
}
}
}
if (email === void 0 || email === null || email === "") throw new APIError("NOT_FOUND", { message: "User email not found" });
const finalCurrency = plan.currency ?? "NGN";
if (!validateMinAmount(amount, finalCurrency)) throw new APIError("BAD_REQUEST", {
message: `Amount ${amount} is less than the minimum required for ${finalCurrency}.`,
status: 400
});
const paystack = createPaystackAdapter(options.paystackClient);
const serializedRenewalMetadata = stringifyPaystackMetadata(createRenewalMetadata({
subscriptionId,
referenceId
}));
const typedChargeData = await paystack.chargeAuthorization({
email,
amount,
authorization_code: subscription.paystackAuthorizationCode,
reference: `rec_${subscription.id}_${Date.now()}`,
metadata: serializedRenewalMetadata
});
if (typedChargeData?.status === "success" && typedChargeData.reference !== void 0) {
const now = /* @__PURE__ */ new Date();
const nextPeriodEnd = getNextPeriodEnd(now, plan.interval ?? "monthly");
await store.createTransaction({
reference: typedChargeData.reference,
paystackId: typedChargeData.id !== void 0 && typedChargeData.id !== null ? String(typedChargeData.id) : void 0,
referenceId,
userId: billingUserId,
amount: typedChargeData.amount,
currency: typedChargeData.currency,
status: "success",
plan: plan.name.toLowerCase(),
metadata: serializedRenewalMetadata,
createdAt: now,
updatedAt: now
});
await store.updateSubscription(subscription.id, {
periodStart: now,
periodEnd: nextPeriodEnd,
updatedAt: now,
paystackTransactionReference: typedChargeData.reference
});
return {
status: "success",
data: typedChargeData
};
}
return {
status: "failed",
data: typedChargeData
};
}
//#endregion
//#region src/index.ts
const INTERNAL_ERROR_CODES = defineErrorCodes(Object.fromEntries(Object.entries(PAYSTACK_ERROR_CODES).map(([key, value]) => [key, typeof value === "string" ? value : value.message])));
const createPaystackPlugin = (options) => {
const routeOptions = {
...options,
webhook: options.webhook
};
return {
id: "paystack",
version: PACKAGE_VERSION,
endpoints: {
initializeTransaction: initializeTransaction(routeOptions, "/paystack/initialize-transaction"),
verifyTransaction: verifyTransaction(routeOptions, "/paystack/verify-transaction"),
listSubscriptions: listSubscriptions(routeOptions, "/paystack/list-subscriptions"),
paystackWebhook: paystackWebhook(routeOptions, "/paystack/webhook"),
listTransactions: listTransactions(routeOptions, "/paystack/list-transactions"),
getConfig: getConfig(routeOptions, "/paystack/config"),
disableSubscription: disablePaystackSubscription(routeOptions, "/paystack/disable-subscription"),
enableSubscription: enablePaystackSubscription(routeOptions, "/paystack/enable-subscription"),
getSubscriptionManageLink: getSubscriptionManageLink(routeOptions, "/paystack/subscription-manage-link"),
subscriptionManageLink: getSubscriptionManageLink(routeOptions, "/paystack/subscription/manage-link"),
createSubscription: createSubscription(routeOptions, "/paystack/create-subscription"),
upgradeSubscription: upgradeSubscription(routeOptions, "/paystack/upgrade-subscription"),
cancelSubscription: cancelSubscription(routeOptions, "/paystack/cancel-subscription"),
restoreSubscription: restoreSubscription(routeOptions, "/paystack/restore-subscription"),
listProducts: listProducts(routeOptions, "/paystack/list-products"),
listPlans: listPlans(routeOptions, "/paystack/list-plans")
},
schema: getSchema(options),
init: ((ctx) => {
const organizationPluginAvailable = ctx.hasPlugin("organization");
if (options.organization?.enabled === true && !organizationPluginAvailable) ctx.logger.error("Paystack organization billing is enabled, but the Better Auth organization plugin was not found. Organization billing hooks will be skipped.");
return { options: { databaseHooks: {
user: {
create: { async after(user, hookCtx) {
if (!hookCtx || options.createCustomerOnSignUp !== true || user.email === null || user.email === void 0 || user.email === "") return;
try {
const result = await resolvePaystackCustomer({
adapter: ctx.adapter,
client: options.paystackClient,
logger: ctx.logger,
reference: {
id: user.id,
type: "user",
email: user.email,
name: user.name,
emailVerified: user.emailVerified,
paystackCustomerCode: user.paystackCustomerCode
}
});
const customerCode = result?.customer.customer_code;
if (result?.created === true && typeof customerCode === "string" && typeof options.onCustomerCreate === "function") await options.onCustomerCreate({
paystackCustomer: result.customer,
user: {
...user,
paystackCustomerCode: customerCode
}
}, hookCtx);
} catch (error) {
ctx.logger.error("Failed to create Paystack customer for user", error);
}
} },
update: { async after(user) {
const persisted = await createBillingStoreFromAdapter(ctx.adapter).findUser(user.id);
const customerCode = user.paystackCustomerCode ?? persisted?.paystackCustomerCode;
if (typeof customerCode !== "string" || customerCode === "" || typeof user.email !== "string" || user.email === "") return;
try {
await createPaystackAdapter(options.paystackClient).updateCustomer(customerCode, { email: user.email });
} catch (error) {
ctx.logger.error("Failed to synchronize Paystack customer email", error);
}
} }
},
organization: options.organization?.enabled === true && organizationPluginAvailable ? {
create: { async after(org, hookCtx) {
try {
const extraCreateParams = typeof options.organization?.getCustomerCreateParams === "function" ? await options.organization.getCustomerCreateParams(org, hookCtx) : {};
let targetEmail = org.email;
if (targetEmail === void 0 || targetEmail === null) {
const store = createBillingStoreFromAdapter(ctx.adapter);
const ownerMember = await store.findOrganizationOwner(org.id);
if (ownerMember !== null && ownerMember !== void 0) targetEmail = (await store.findUser(ownerMember.userId))?.email;
}
if (targetEmail === void 0 || targetEmail === null) return;
const result = await resolvePaystackCustomer({
adapter: ctx.adapter,
client: options.paystackClient,
logger: ctx.logger,
reference: {
id: org.id,
type: "organization",
email: targetEmail,
name: org.name,
paystackCustomerCode: org.paystackCustomerCode
},
createParams: defu({}, extraCreateParams)
});
const customerCode = result?.customer.customer_code;
if (result?.created === true && typeof customerCode === "string" && typeof options.organization?.onCustomerCreate === "function") await options.organization.onCustomerCreate({
paystackCustomer: result.customer,
organization: {
...org,
paystackCustomerCode: customerCode
}
}, hookCtx);
} catch (error) {
ctx.logger.error("Failed to create Paystack customer for organization", error);
}
} },
update: { async after(org, hookCtx) {
const persisted = await createBillingStoreFromAdapter(ctx.adapter).findOrganization(org.id);
const customerCode = org.paystackCustomerCode ?? persisted?.paystackCustomerCode;
if (typeof customerCode !== "string" || customerCode === "") return;
try {
const configured = typeof options.organization?.getCustomerCreateParams === "function" && hookCtx ? await options.organization.getCustomerCreateParams({
id: org.id,
name: org.name ?? persisted?.name ?? "",
email: org.email ?? persisted?.email
}, hookCtx) : {};
const configuredEmail = typeof configured.email === "string" && configured.email !== "" ? configured.email : void 0;
await createPaystackAdapter(options.paystackClient).updateCustomer(customerCode, {
...configuredEmail !== void 0 || typeof org.email === "string" && org.email !== "" ? { email: configuredEmail ?? org.email ?? void 0 } : {},
...typeof org.name === "string" && org.name !== "" ? { first_name: org.name } : {}
});
} catch (error) {
ctx.logger.error("Failed to synchronize Paystack organization customer", error);
}
} },
delete: { async before(org) {
const store = createBillingStoreFromAdapter(ctx.adapter);
if ((await store.findSubscriptionsByReference(org.id)).some((subscription) => subscription.status === "active" || subscription.status === "trialing")) throw new APIError("BAD_REQUEST", { message: "Organization cannot be deleted while it has an active subscription" });
const persisted = await store.findOrganization(org.id);
const customerCode = org.paystackCustomerCode ?? persisted?.paystackCustomerCode;
if (typeof customerCode !== "string" || customerCode === "") return;
try {
if ((await createPaystackAdapter(options.paystackClient).fetchCustomer(customerCode)).subscriptions?.some((subscription) => subscription.status === "active" || subscription.status === "trialing") === true) throw new APIError("BAD_REQUEST", { message: "Organization cannot be deleted while it has an active subscription" });
} catch (error) {
if (error instanceof APIError) throw error;
ctx.logger.error("Failed to check Paystack subscriptions before organization deletion", error);
throw new APIError("BAD_REQUEST", { message: "Organization deletion could not verify its Paystack subscriptions" });
}
} }
} : void 0,
member: organizationPluginAvailable ? {
create: {
before: async (member, ctx) => {
if (options.subscription?.enabled === true && member.organizationId && ctx !== null && ctx !== void 0) await checkSeatLimit(ctx, member.organizationId);
},
after: async (member, ctx) => {
if (options.subscription?.enabled === true && typeof member?.organizationId === "string" && ctx) await syncSubscriptionSeats(ctx, member.organizationId, routeOptions);
}
},
delete: { after: async (member, ctx) => {
if (options.subscription?.enabled === true && typeof member?.organizationId === "string" && ctx) await syncSubscriptionSeats(ctx, member.organizationId, routeOptions);
} }
} : void 0,
invitation: organizationPluginAvailable ? {
create: {
before: async (invitation, ctx) => {
if (options.subscription?.enabled === true && invitation.organizationId && ctx !== null && ctx !== void 0) await checkSeatLimit(ctx, invitation.organizationId);
},
after: async (invitation, ctx) => {
if (options.subscription?.enabled === true && typeof invitation?.organizationId === "string" && ctx) await syncSubscriptionSeats(ctx, invitation.organizationId, routeOptions);
}
},
delete: { after: async (invitation, ctx) => {
if (options.subscription?.enabled === true && typeof invitation?.organizationId === "string" && ctx) await syncSubscriptionSeats(ctx, invitation.organizationId, routeOptions);
} }
} : void 0,
team: organizationPluginAvailable ? { create: { before: async (team, ctx) => {
if (options.subscription?.enabled === true && team.organizationId && ctx) {
const maxTeams = (await getOrganizationEntitlements(ctx, team.organizationId, routeOptions)).limits.teams;
if (typeof maxTeams === "number") await checkTeamLimit(ctx, team.organizationId, maxTeams);
}
} } } : void 0
} } };
}),
$ERROR_CODES: INTERNAL_ERROR_CODES,
options
};
};
const paystack = createPaystackPlugin;
//#endregion
export { chargeSubscriptionRenewal, createCheckoutMetadata, createProrationMetadata, createRenewalMetadata, getMetadataBoolean, getMetadataNumber, getMetadataString, getOrganizationEntitlements, hasPaystackMetadata, parsePaystackMetadata, paystack, reconcilePaystackTransaction, stringifyPaystackMetadata, syncPaystackPlans, syncPaystackProducts };
//# sourceMappingURL=index.mjs.map