UNPKG

@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
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