UNPKG

better-auth-paystack

Version:

Production-ready Paystack billing plugin for Better Auth. Supports subscriptions, one-time payments, organization billing, secure webhooks and more

3,885 lines • 144 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-ClvErcin.mjs";
import { HIDE_METADATA, defineErrorCodes } from "better-auth";
import { APIError, createAuthEndpoint, createAuthMiddleware, getSessionFromCtx, originCheckMiddleware, sessionMiddleware } from "better-auth/api";
import { defu } from "defu";
import { PaystackError, PaystackResponse } from "@alexasomba/paystack-node";
import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID } from "node:crypto";
import { z } from "zod";
import { Buffer } from "node:buffer";
import * as z$1 from "zod/v4";
import { mergeSchema } from "better-auth/db";
//#region src/models.ts
/** Provider-owned Better Auth model names. Keep these centralized so a provider
* model cannot accidentally fall back to Better Auth's generic billing tables. */
const PAYSTACK_MODELS = {
	customer: "paystackCustomer",
	paymentCredential: "paystackPaymentCredential",
	subscription: "paystackSubscription",
	transaction: "paystackTransaction",
	product: "paystackProduct",
	plan: "paystackPlan",
	webhookEvent: "paystackWebhookEvent"
};
const LEGACY_PAYSTACK_MODELS = { subscription: "subscription" };
//#endregion
//#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) => {
		try {
			return await adapter.findOne({
				model,
				where
			}) ?? null;
		} catch {
			return null;
		}
	};
	const findMany = async (model, where) => {
		try {
			return await adapter.findMany({
				model,
				...where ? { where } : {}
			}) ?? [];
		} catch {
			return [];
		}
	};
	return {
		findSubscriptionById: (id) => findOne(PAYSTACK_MODELS.subscription, [{
			field: "id",
			value: id
		}]),
		findSubscriptionByCode: (subscriptionCode) => findOne(PAYSTACK_MODELS.subscription, [{
			field: "subscriptionCode",
			value: subscriptionCode
		}]),
		findSubscriptionsByReference: (referenceId) => findMany(PAYSTACK_MODELS.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 subscriptions = await this.findSubscriptionsByReference(referenceId);
			const selected = subscriptions.find((subscription) => subscription.id === exceptId);
			const competitors = subscriptions.filter((subscription) => subscription.id !== exceptId && (selected === void 0 || new Date(subscription.createdAt).getTime() <= new Date(selected.createdAt).getTime()) && (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(PAYSTACK_MODELS.subscription, [{
			field: "transactionReference",
			value: reference
		}]),
		createSubscription: async (data) => await adapter.create({
			model: PAYSTACK_MODELS.subscription,
			data
		}),
		updateSubscription: (id, update) => adapter.update({
			model: PAYSTACK_MODELS.subscription,
			update,
			where: [{
				field: "id",
				value: id
			}]
		}),
		updateSubscriptionByCode: (subscriptionCode, update) => adapter.update({
			model: PAYSTACK_MODELS.subscription,
			update,
			where: [{
				field: "subscriptionCode",
				value: subscriptionCode
			}]
		}),
		createTransaction: async (data) => await adapter.create({
			model: PAYSTACK_MODELS.transaction,
			data
		}),
		findTransactionByReference: (reference) => findOne(PAYSTACK_MODELS.transaction, [{
			field: "reference",
			value: reference
		}]),
		updateTransactionByReference: (reference, update) => adapter.update({
			model: PAYSTACK_MODELS.transaction,
			update,
			where: [{
				field: "reference",
				value: reference
			}]
		}),
		createWebhookEvent: (data) => adapter.create({
			model: PAYSTACK_MODELS.webhookEvent,
			data
		}),
		findWebhookEvent: (eventId) => adapter.findOne({
			model: PAYSTACK_MODELS.webhookEvent,
			where: [{
				field: "eventId",
				value: eventId
			}]
		}),
		updateWebhookEvent: (eventId, update) => adapter.update({
			model: PAYSTACK_MODELS.webhookEvent,
			update,
			where: [{
				field: "eventId",
				value: eventId
			}]
		}),
		async listTransactions(referenceId) {
			return (await findMany(PAYSTACK_MODELS.transaction, [{
				field: "referenceId",
				value: referenceId
			}])).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
		},
		async listProducts() {
			return (await findMany(PAYSTACK_MODELS.product)).sort((a, b) => a.name.localeCompare(b.name));
		},
		findProductByName: (name) => findOne(PAYSTACK_MODELS.product, [{
			field: "name",
			value: name
		}]),
		findProductBySlug: (slug) => findOne(PAYSTACK_MODELS.product, [{
			field: "slug",
			value: slug
		}]),
		async updateProduct(id, update) {
			await adapter.update({
				model: PAYSTACK_MODELS.product,
				update,
				where: [{
					field: "id",
					value: id
				}]
			});
		},
		async upsertProductByPaystackId(paystackId, data) {
			const existing = await findOne(PAYSTACK_MODELS.product, [{
				field: "paystackId",
				value: paystackId
			}]);
			if (existing?.id !== void 0) {
				const { createdAt: _createdAt, ...update } = data;
				await adapter.update({
					model: PAYSTACK_MODELS.product,
					update,
					where: [{
						field: "id",
						value: String(existing.id)
					}]
				});
				return;
			}
			await adapter.create({
				model: PAYSTACK_MODELS.product,
				data
			});
		},
		listPlans: () => findMany(PAYSTACK_MODELS.plan),
		findPlanByName: (name) => findOne(PAYSTACK_MODELS.plan, [{
			field: "name",
			value: name
		}]),
		findPlanByCode: (planCode) => findOne(PAYSTACK_MODELS.plan, [{
			field: "planCode",
			value: planCode
		}]),
		async upsertPlanByPaystackId(paystackId, data) {
			const existing = await findOne(PAYSTACK_MODELS.plan, [{
				field: "paystackId",
				value: paystackId
			}]);
			if (existing?.id !== void 0) {
				const { createdAt: _createdAt, ...update } = data;
				await adapter.update({
					model: PAYSTACK_MODELS.plan,
					update,
					where: [{
						field: "id",
						value: existing.id
					}]
				});
				return;
			}
			await adapter.create({
				model: PAYSTACK_MODELS.plan,
				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
		}]),
		findCustomerByReference: (referenceType, referenceId) => findOne(PAYSTACK_MODELS.customer, [{
			field: "referenceKey",
			value: `${referenceType}:${referenceId}`
		}]),
		findCustomerByCode: (customerCode) => findOne(PAYSTACK_MODELS.customer, [{
			field: "customerCode",
			value: customerCode
		}]),
		async saveCustomer(referenceType, referenceId, customerCode, email) {
			const referenceKey = `${referenceType}:${referenceId}`;
			const existing = await findOne(PAYSTACK_MODELS.customer, [{
				field: "referenceKey",
				value: referenceKey
			}]);
			const now = /* @__PURE__ */ new Date();
			const hasEmail = email !== void 0 && email !== null && email !== "";
			if (existing) return await adapter.update({
				model: PAYSTACK_MODELS.customer,
				update: {
					customerCode,
					...hasEmail ? { email } : {},
					updatedAt: now
				},
				where: [{
					field: "id",
					value: existing.id
				}]
			}) ?? {
				...existing,
				customerCode,
				email: email ?? existing.email,
				updatedAt: now
			};
			return adapter.create({
				model: PAYSTACK_MODELS.customer,
				data: {
					referenceType,
					referenceId,
					referenceKey,
					customerCode,
					...hasEmail ? { email } : {},
					createdAt: now,
					updatedAt: now
				}
			});
		},
		async saveCustomerCode(referenceId, customerCode, isOrganization, email) {
			await this.saveCustomer(isOrganization ? "organization" : "user", referenceId, customerCode, email);
		}
	};
}
//#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(e.status === 404 ? "NOT_FOUND" : "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/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 = await store.findCustomerByReference(reference.type, reference.id);
	let legacyCode;
	try {
		legacyCode = (await adapter.findOne({
			model: reference.type,
			where: [{
				field: "id",
				value: reference.id
			}],
			select: ["paystackCustomerCode"]
		}))?.paystackCustomerCode ?? void 0;
	} catch {}
	const existingCode = reference.paystackCustomerCode ?? persisted?.customerCode ?? legacyCode;
	if (typeof existingCode === "string" && existingCode !== "") {
		if (persisted === null) await store.saveCustomer(reference.type, reference.id, existingCode, reference.email);
		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.saveCustomer(reference.type, reference.id, code, reference.email);
		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.saveCustomer(reference.type, reference.id, code, reference.email);
	return {
		customer,
		created: true
	};
}
//#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.subscriptionCode)) return true;
	if (typeof subscription.subscriptionCode === "string" && subscription.subscriptionCode !== "") return false;
	return subscription.planCode === void 0 || subscription.planCode === null || subscription.planCode === "";
}
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.subscriptionCode === void 0 || subscription.subscriptionCode === null || subscription.subscriptionCode === "") 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/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/credential-crypto.ts
const ALGORITHM = "aes-256-gcm";
const VERSION = "v1";
const IV_LENGTH = 12;
function deriveKey(secret) {
	if (typeof secret !== "string" || secret.trim() === "") throw new Error("A non-empty credential encryption key is required");
	return createHash("sha256").update(secret, "utf8").digest();
}
function encryptPaystackCredential(value, secret) {
	if (value === "") throw new Error("Cannot encrypt an empty Paystack credential");
	const iv = randomBytes(IV_LENGTH);
	const cipher = createCipheriv(ALGORITHM, deriveKey(secret), iv);
	const ciphertext = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
	const authTag = cipher.getAuthTag();
	return [
		VERSION,
		iv.toString("hex"),
		authTag.toString("hex"),
		ciphertext.toString("hex")
	].join(":");
}
function decryptPaystackCredential(encrypted, secret) {
	const parts = encrypted.split(":");
	if (parts.length !== 4 || parts[0] !== VERSION) throw new Error("Invalid Paystack credential ciphertext");
	const [, ivHex, authTagHex, ciphertextHex] = parts;
	const iv = Buffer.from(ivHex, "hex");
	const authTag = Buffer.from(authTagHex, "hex");
	const ciphertext = Buffer.from(ciphertextHex, "hex");
	if (iv.length !== IV_LENGTH || authTag.length !== 16 || ciphertext.length === 0) throw new Error("Invalid Paystack credential ciphertext");
	const decipher = createDecipheriv(ALGORITHM, deriveKey(secret), iv);
	decipher.setAuthTag(authTag);
	return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
}
function resolveCredentialEncryptionKey(options) {
	const key = options.credentialEncryptionKey ?? options.secretKey;
	if (typeof key !== "string" || key.trim() === "") throw new Error("credentialEncryptionKey or secretKey must be a non-empty string");
	return key;
}
//#endregion
//#region src/payment-credentials.ts
async function readPaystackPaymentCredentials(adapter, options, subscriptionId) {
	let row;
	try {
		row = await adapter.findOne({
			model: PAYSTACK_MODELS.paymentCredential,
			where: [{
				field: "subscriptionId",
				value: subscriptionId
			}]
		});
	} catch {
		return null;
	}
	if (!row) return null;
	const key = resolveCredentialEncryptionKey(options);
	const authorizationCodeEncrypted = row.authorizationCodeEncrypted;
	const emailTokenEncrypted = row.emailTokenEncrypted;
	return {
		authorizationCode: typeof authorizationCodeEncrypted === "string" && authorizationCodeEncrypted !== "" ? decryptPaystackCredential(authorizationCodeEncrypted, key) : void 0,
		emailToken: typeof emailTokenEncrypted === "string" && emailTokenEncrypted !== "" ? decryptPaystackCredential(emailTokenEncrypted, key) : void 0
	};
}
async function savePaystackPaymentCredentials(adapter, options, subscriptionId, values) {
	const authorizationCode = values.authorizationCode ?? void 0;
	const emailToken = values.emailToken ?? void 0;
	if (authorizationCode === void 0 && emailToken === void 0) return;
	const key = resolveCredentialEncryptionKey(options);
	const now = /* @__PURE__ */ new Date();
	let existing = null;
	try {
		existing = await adapter.findOne({
			model: PAYSTACK_MODELS.paymentCredential,
			where: [{
				field: "subscriptionId",
				value: subscriptionId
			}]
		});
	} catch {}
	const update = {
		...authorizationCode !== void 0 ? { authorizationCodeEncrypted: encryptPaystackCredential(authorizationCode, key) } : {},
		...emailToken !== void 0 ? { emailTokenEncrypted: encryptPaystackCredential(emailToken, key) } : {},
		updatedAt: now
	};
	if (existing !== null) {
		await adapter.update({
			model: PAYSTACK_MODELS.paymentCredential,
			update,
			where: [{
				field: "id",
				value: existing.id
			}]
		});
		return;
	}
	await adapter.create({
		model: PAYSTACK_MODELS.paymentCredential,
		data: {
			subscriptionId,
			...update,
			createdAt: now
		}
	});
}
//#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/subscription-fulfillment.ts
const progressSchema = z$1.object({
	version: z$1.literal(1),
	completed: z$1.array(z$1.enum([
		"complete",
		"update",
		"trialStart"
	])),
	remoteCreationSubmitted: z$1.boolean().optional(),
	remoteSubscriptionCode: z$1.string().optional()
});
const unavailable = () => new APIError("SERVICE_UNAVAILABLE", { message: "Subscription fulfillment is pending. Retry verification." });
/** Persist delivery intent before activation, sharing one lease across verification transports. */
async function claimSubscriptionFulfillment(ctx, reference, subscription, phase = "subscription-fulfillment") {
	const store = createBillingStore(ctx);
	const eventId = `${phase}:${createHash("sha256").update(JSON.stringify([reference, subscription.id])).digest("hex")}`;
	const findRecord = () => ctx.context.adapter.findOne({
		model: PAYSTACK_MODELS.webhookEvent,
		where: [{
			field: "eventId",
			value: eventId
		}]
	});
	let record = await findRecord();
	if (record === null) {
		if (subscription.status !== "incomplete") return null;
		try {
			record = await store.createWebhookEvent({
				eventId,
				eventType: "subscription.fulfillment",
				reference,
				status: "pending",
				payload: JSON.stringify({
					version: 1,
					completed: []
				}),
				createdAt: /* @__PURE__ */ new Date(),
				updatedAt: /* @__PURE__ */ new Date()
			});
		} catch (error) {
			record = await findRecord();
			if (record === null) throw error;
		}
	}
	if (record.status === "processed") return null;
	if (record.status.startsWith("processing:") && Date.now() - new Date(record.updatedAt).getTime() < 3e5) throw unavailable();
	const progress = progressSchema.parse(JSON.parse(record.payload));
	const status = `processing:${randomUUID()}`;
	if (await ctx.context.adapter.update({
		model: PAYSTACK_MODELS.webhookEvent,
		where: [
			{
				field: "eventId",
				value: eventId
			},
			{
				field: "status",
				value: record.status
			},
			{
				field: "updatedAt",
				value: record.updatedAt
			}
		],
		update: {
			status,
			updatedAt: /* @__PURE__ */ new Date()
		}
	}) === null) throw unavailable();
	return {
		eventId,
		status,
		progress
	};
}
async function deliverTrialStart(ctx, claim, subscription, plan) {
	if (!claim.progress.completed.includes("trialStart")) {
		await checkpoint(ctx, claim);
		await plan?.freeTrial?.onTrialStart?.(subscription);
		claim.progress.completed.push("trialStart");
		await checkpoint(ctx, claim);
	}
	await checkpoint(ctx, claim, true);
}
async function reserveRemoteTrialSubscription(ctx, claim, create) {
	if (claim.progress.remoteSubscriptionCode !== void 0) return claim.progress.remoteSubscriptionCode;
	if (claim.progress.remoteCreationSubmitted === true) throw new APIError("SERVICE_UNAVAILABLE", { message: "Remote trial subscription creation is unresolved. Reconcile the existing provider subscription before retrying." });
	claim.progress.remoteCreationSubmitted = true;
	await checkpoint(ctx, claim);
	const code = await create();
	if (code === void 0 || code === "") throw unavailable();
	claim.progress.remoteSubscriptionCode = code;
	await checkpoint(ctx, claim);
	return code;
}
async function checkpoint(ctx, claim, processed = false) {
	if (await ctx.context.adapter.update({
		model: PAYSTACK_MODELS.webhookEvent,
		where: [{
			field: "eventId",
			value: claim.eventId
		}, {
			field: "status",
			value: claim.status
		}],
		update: {
			payload: JSON.stringify(claim.progress),
			updatedAt: /* @__PURE__ */ new Date(),
			...processed ? {
				status: "processed",
				processedAt: /* @__PURE__ */ new Date()
			} : {}
		}
	}) === null) throw unavailable();
}
async function deliverSubscriptionFulfillment(ctx, options, claim, subscription, plan, event) {
	if (plan !== void 0) {
		const callbacks = {
			complete: options.subscription?.onSubscriptionComplete,
			update: options.subscription?.onSubscriptionUpdate
		};
		for (const name of ["complete", "update"]) {
			if (claim.progress.completed.includes(name)) continue;
			await checkpoint(ctx, claim);
			await callbacks[name]?.({
				event,
				subscription,
				plan
			}, ctx);
			claim.progress.completed.push(name);
			await checkpoint(ctx, claim);
		}
	}
	await checkpoint(ctx, claim, true);
}
async function releaseSubscriptionFulfillment(ctx, claim) {
	await ctx.context.adapter.update({
		model: PAYSTACK_MODELS.webhookEvent,
		where: [{
			field: "eventId",
			value: claim.eventId
		}, {
			field: "status",
			value: claim.status
		}],
		update: {
			status: "pending",
			updatedAt: /* @__PURE__ */ new Date()
		}
	});
}
//#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 = 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
			}
		});
	}
	if (data.reference !== reference || txRecord !== null && typeof txRecord.amount === "number" && txRecord.amount !== data.amount || txRecord !== null && typeof txRecord.currency === "string" && txRecord.currency !== data.currency) return throwOrReturnFailure({
		throwOnError,
		apiStatus: "BAD_REQUEST",
		source,
		status,
		reference,
		data,
		summary,
		error: {
			code: "PAYMENT_INTENT_MISMATCH",
			message: "Verified payment does not match the stored reference, amount, and currency.",
			status: 400
		}
	});
	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, data.customer?.email);
		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 originalSubscription = await store.findSubscriptionById(subscriptionId);
			if (!originalSubscription || originalSubscription.status === "canceled" || originalSubscription.transactionReference === reference) return {
				ok: true,
				source,
				status,
				reference,
				data,
				...summary
			};
			const updatedSubscription = await store.updateSubscription(subscriptionId, {
				plan: newPlan,
				...typeof newSeatCount === "number" ? { seats: newSeatCount } : {},
				transactionReference: reference,
				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;
			if (updatedSubscription !== null && authorizationCode !== void 0 && authorizationCode !== null && authorizationCode !== "") await savePaystackPaymentCredentials(ctx.context.adapter, options, updatedSubscription.id, { authorizationCode });
		}
		return {
			ok: true,
			source,
			status,
			reference,
			data,
			...summary
		};
	}
	let subscriptionCode;
	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 (!targetSub || ![
		"incomplete",
		"active",
		"trialing"
	].includes(targetSub.status)) return {
		ok: true,
		source,
		status,
		reference,
		data,
		...summary
	};
	const claim = await claimSubscriptionFulfillment(ctx, reference, targetSub);
	if (claim === null) return {
		ok: true,
		source,
		status,
		reference,
		data,
		...summary
	};
	try {
		if (targetSub.status !== "incomplete") {
			if (authorizationCode !== void 0 && authorizationCode !== null && authorizationCode !== "") await savePaystackPaymentCredentials(ctx.context.adapter, options, targetSub.id, { authorizationCode });
			await store.retireCompetingSubscriptions(targetSub.referenceId, targetSub.groupId ?? null, targetSub.id);
			await deliverSubscriptionFulfillment(ctx, options, claim, targetSub, (await getPlans(options.subscription)).find((candidate) => candidate.name.toLowerCase() === targetSub.plan.toLowerCase()), data);
			return {
				ok: true,
				source,
				status,
				reference,
				data,
				...summary
			};
		}
		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 === "")) subscriptionCode = `LOC_${reference}`;
			else if (targetSub?.subscriptionCode !== void 0 && targetSub.subscriptionCode !== null && targetSub.subscriptionCode !== "") subscriptionCode = targetSub.subscriptionCode;
			else if (authorizationCode !== void 0 && authorizationCode !== null && email !== void 0 && email !== null && email !== "" && planConfig?.planCode !== void 0 && planConfig.planCode !== null && planConfig.planCode !== "") {
				const planCode = planConfig.planCode;
				subscriptionCode = await reserveRemoteTrialSubscription(ctx, claim, async () => {
					return unwrapSdkResult(await paystack?.subscription?.create({ body: {
						customer: email,
						plan: 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 === "") subscriptionCode = `LOC_${reference}`;
			else subscriptionCode = 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 ctx.context.adapter.update({
				model: PAYSTACK_MODELS.subscription,
				where: [
					{
						field: "id",
						value: targetSub.id
					},
					{
						field: "status",
						value: "incomplete"
					},
					{
						field: "transactionReference",
						value: reference
					}
				],
				update: {
					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)
					} : {},
					...subscriptionCode !== void 0 ? { subscriptionCode } : {}
				}
			});
			summary.subscription.updated = updatedSubscription !== null;
			summary.subscription.id = updatedSubscription?.id ?? targetSub.id;
			summary.subscription.status = updatedSubscription?.status ?? targetSub.status;
			if (authorizationCode !== void 0 && authorizationCode !== null && authorizationCode !== "" && updatedSubscription !== null) await savePaystackPaymentCredentials(ctx.context.adapter, options, updatedSubscription.id, { authorizationCode });
			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());
			await deliverSubscriptionFulfillment(ctx, options, claim, updatedSubscription, plan, data);
		} else throw new APIError("SERVICE_UNAVAILABLE", { message: "Subscription activation is pending. Retry verification." });
		return {
			ok: true,
			source,
			status,
			reference,
			data,
			...summary
		};
	} catch (error) {
		await releaseSubscriptionFulfillment(ctx, claim);
		throw error;
	}
}
//#endregion
//#region src/reference-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/route-modules/callback-url.ts
/** Accept only absolute HTTP(S) URLs or root-relative paths on the Better Auth origin. */
function hasControlCharacters(value) {
	for (const character of value) {
		const codePoint = character.codePointAt(0);
		if (codePoint !== void 0 && (codePoint <= 31 || codePoint >= 127 && codePoint <= 159)) return true;
	}
	return false;
}
function isTrustedCallbackURL(callbackURL, baseURL) {
	if (callbackURL.startsWith("//") || callbackURL.includes("\\") || hasControlCharacters(callbackURL)) return false;
	try {
		const base = new URL(baseURL);
		if (!/^https?:\/\//i.test(callbackURL) && !callbackURL.startsWith("/")) return false;
		const callback = new URL(callbackURL, base);
		return (callback.protocol === "http:" || callback.protocol === "https:") && callback.origin === base.origin && callback.username === "" && callback.password === "";
	} catch {
		return false;
	}
}
//#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/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(),
	confirmTrialTokenizationCharge: z.boolean().optional()
});
//#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/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/subscription-lifecycle.ts
/** Keep reservation context invocation-local, including concurrent requests through one plugin. */
async function runSubscriptionOperation(ctx, options, operation, run) {
	const hooks = options?.subscription?.operationHooks;
	if (hooks === void 0) return run(void 0, () => void 0);
	const decision = await hooks.before(operation, ctx);
	if (decision.kind === "replay") {
		if (!(operation.kind === "schedule" ? decision.result.kind === "scheduled" : operation.kind === "initialize" ? decision.result.kind === "checkout" : operation.kind === "local-change" ? decision.result.kind === "prorated" : decision.result.kind === "checkout" || decision.result.kind === "prorated")) throw new APIError("BAD_REQUEST", { message: "Operation replay does not match the validated operation kind." });
		return decision.result;
	}
	if (decision.kind === "block") throw new APIError("CONFLICT", { message: decision.message ?? "Subscription operation is blocked." });
	if (decision.kind !== "proceed" || typeof decision.providerReference !== "string" || decision.providerReference.trim() === "") throw new APIError("BAD_REQUEST", { message: "Operation hook must reserve a provider reference." });
	const invocation = {
		...operation,
		providerReference: decision.providerReference,
		context: decision.context
	};
	let phase = operation.kind === "schedule" || operation.kind === "local-change" ? "persistence" : "provider";
	try {
		const result = await run(decision.providerReference, (next) => {
			phase = next;
		});
		phase = "completion";
		await hooks.after?.({
			...invocation,
			result
		}, ctx);
		return result;
	} catch (error) {
		try {
			await hooks.onError?.({
				...invocation,
				phase,
				error
			}, ctx);
		} catch {
			ctx.context.logger.error("Paystack operation error hook failed", { phase });
		}
		throw error;
	}
}
async function scheduleSubscriptionLifecycleChange(ctx, input, options) {
	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") return runSubscriptionOperation(ctx, options, {
			actor: { id: input.userId ?? existingSub.userId },
			referenceId: input.referenceId,
			kind: "schedule",
			subscription: existingSub,
			intent: {
				plan: input.plan?.name ?? existingSub.plan,
				interval: input.plan?.interval ?? existingSub.billingInterval ?? void 0,
				groupId,
				scheduleAtPeriodEnd: input.scheduleAtPeriodEnd,
				cancelAtPeriodEnd: input.cancelAtPeriodEnd
			}
		}, async () => {
			await ctx.context.adapter.update({
				model: PAYSTACK_MODELS.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") return runSubscriptionOperation(ctx, options, {
			actor: { id: input.userId ?? existingSub.userId },
			referenceId: input.referenceId,
			kind: "schedule",
			subscription: existingSub,
			intent: {
				plan: input.plan?.name ?? existingSub.plan,
				interval: input.plan?.interval ?? existingSub.billingInterval ?? void 0,
				groupId,
				scheduleAtPeriodEnd: input.scheduleAtPeriodEnd,
				cancelAtPeriodEnd: input.cancelAtPeriodEnd
			}
		}, async () => {
			await ctx.context.adapter.update({
				model: PAYSTACK_MODELS.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: PAYSTACK_MODELS.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.subscriptionCode === void 0 || existingSub.subscriptionCode === null || existingSub.subscriptionCode === "" || 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()) / 864e5));
	const remainingDays = Math.max(0, Math.ceil((periodEnd.getTime() - now.getTime()) / 864e5));
	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 ?? 1);
	}
	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 prorationMetadata = createProrationMetadata({
		subscriptionId: existingSub.id,
		referenceId: input.referenceId,
		newPlan: input.plan.name.toLowerCase(),
		oldPlan: existingSub.plan,
		newSeatCount,
		remainingDays
	});
	const serializedProrationMetadata = stringifyPaystackMetadata(prorationMetadata);
	const proratedAmount = Math.round(costDifference / totalDays * remainingDays);
	const hasCharge = costDifference > 0 && remainingDays > 0;
	if (hasCharge && 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 credentials = hasCharge ? await readPaystackPaymentCredentials(ctx.context.adapter, options, existingSub.id) : null;
	const hasAuthorization = credentials?.authorizationCode !== void 0 && credentials.authorizationCode !== null && credentials.authorizationCode !== "";
	return runSubscriptionOperation(ctx, options, {
		actor: { id: input.userId ?? existingSub.userId },
		referenceId: input.referenceId,
		kind: hasCharge ? hasAuthorization ? "proration-charge" : "proration-initialize" : "local-change",
		subscription: existingSub,
		intent: {
			plan: input.plan.name.toLowerCase(),
			amount: hasCharge ? proratedAmount : 0,
			currency: input.finalCurrency,
			interval: input.plan.interval,
			quantity: newSeatCount,
			groupId: normalizeSubscriptionGroup(input.plan.group),
			callbackURL: input.callbackURL
		}
	}, async (providerReference, setPhase) => {
		let completedProrationReference;
		if (costDifference > 0 && remainingDays > 0) {
			const paystack = createPaystackAdapter(options.paystackClient);
			if (credentials?.authorizationCode !== void 0 && credentials.authorizationCode !== null && credentials.authorizationCode !== "") {
				const sdkRes = await paystack.chargeAuthorization({
					email: input.targetEmail,
					amount: proratedAmount,
					authorization_code: credentials.authorizationCode,
					reference: providerReference ?? `upg_${existingSub.id}_${Date.now()}_${Math.random().toString(36).substring(7)}`,
					metadata: serializedProrationMetadata
				});
				if (providerReference !== void 0 && sdkRes?.reference !== providerReference) throw new APIError("BAD_REQUEST", { message: "Provider reference does not match reserved operation." });
				if (sdkRes?.status !== "success") throw new APIError("BAD_REQUEST", { message: "Failed to process prorated charge via saved authorization." });
				setPhase("persistence");
				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({
					...providerReference !== void 0 ? { reference: providerReference } : {},
					email: input.targetEmail,
					amount: proratedAmount,
					currency: input.finalCurrency,
					callback_url: input.callbackURL ?? void 0,
					metadata: serializedProrationMetadata,
					...input.allowedSubscriptionChannels !== void 0 ? { channels: input.allowedSubscriptionChannels } : {}
				});
				if (providerReference !== void 0 && initRes?.reference !== providerReference) throw new APIError("BAD_REQUEST", { message: "Provider reference does not match reserved operation." });
				setPhase("persistence");
				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
				};
			}
		}
		setPhase("persistence");
		await store.updateSubscription(existingSub.id, {
			plan: input.plan.name,
			seats: newSeatCount,
			...completedProrationReference !== void 0 ? { transactionReference: completedProrationReference } : {},
			updatedAt: /* @__PURE__ */ new Date()
		});
		return {
			kind: "prorated",
			status: "success",
			message: "Subscription successfully upgraded with prorated charge.",
			prorated: true
		};
	});
}
//#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;
		const reference = data?.reference;
		const eventId = createHash("sha256").update(payload).digest("hex");
		const store = createBillingStore(ctx);
		let claimStatus;
		try {
			let existingEvent = await store.findWebhookEvent(eventId);
			if (existingEvent?.status === "processed") return ctx.json({ received: true });
			if (existingEvent === null) try {
				existingEvent = await store.createWebhookEvent({
					eventId,
					eventType: eventName,
					reference: reference ?? null,
					payload,
					status: "pending",
					createdAt: /* @__PURE__ */ new Date(),
					updatedAt: /* @__PURE__ */ new Date()
				});
			} catch (error) {
				existingEvent = await store.findWebhookEvent(eventId);
				if (!existingEvent) throw error;
			}
			if (existingEvent.status === "processed") return ctx.json({ received: true });
			if (existingEvent.status.startsWith("processing:") && new Date(existingEvent.updatedAt).getTime() > Date.now() - 3e5) throw new Error("Webhook is already processing; retry later.");
			claimStatus = `processing:${randomUUID()}`;
			if (await ctx.context.adapter.update({
				model: PAYSTACK_MODELS.webhookEvent,
				where: [
					{
						field: "eventId",
						value: eventId
					},
					{
						field: "status",
						value: existingEvent.status
					},
					{
						field: "updatedAt",
						value: existingEvent.updatedAt
					}
				],
				update: {
					status: claimStatus,
					updatedAt: /* @__PURE__ */ new Date()
				}
			}) === null) throw new Error("Webhook claim was lost; retry later.");
			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);
						throw e;
					}
					if (options.subscription?.enabled === true) {
						const pendingTrial = await ctx.context.adapter.findOne({
							model: PAYSTACK_MODELS.subscription,
							where: [{
								field: "transactionReference",
								value: reference
							}]
						});
						if (pendingTrial !== null && ["incomplete", "trialing"].includes(pendingTrial.status) && pendingTrial.trialStart !== void 0 && pendingTrial.trialStart !== null) {
							const trialClaim = await claimSubscriptionFulfillment(ctx, reference, pendingTrial, "trial-start");
							if (trialClaim !== null) try {
								const startedTrial = pendingTrial.status === "trialing" ? pendingTrial : await ctx.context.adapter.update({
									model: PAYSTACK_MODELS.subscription,
									where: [
										{
											field: "id",
											value: pendingTrial.id
										},
										{
											field: "status",
											value: "incomplete"
										},
										{
											field: "transactionReference",
											value: reference
										}
									],
									update: {
										status: "trialing",
										updatedAt: /* @__PURE__ */ new Date()
									}
								});
								if (startedTrial === null) throw new Error("Trial activation changed concurrently; retry later.");
								await deliverTrialStart(ctx, trialClaim, startedTrial, await getPlanByName(options, pendingTrial.plan) ?? void 0);
							} catch (error) {
								await releaseSubscriptionFulfillment(ctx, trialClaim);
								throw error;
							}
						}
					}
					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);
						throw 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);
						throw e;
					}
					if (options.subscription?.enabled === true) {
						const pendingTrial = await ctx.context.adapter.findOne({
							model: PAYSTACK_MODELS.subscription,
							where: [{
								field: "transactionReference",
								value: reference
							}]
						});
						if (pendingTrial?.status === "incomplete" && pendingTrial.trialStart !== void 0 && pendingTrial.trialStart !== null) {
							const endedAt = /* @__PURE__ */ new Date();
							await ctx.context.adapter.update({
								model: PAYSTACK_MODELS.subscription,
								where: [{
									field: "id",
									value: pendingTrial.id
								}],
								update: {
									status: "canceled",
									endedAt,
									updatedAt: endedAt
								}
							});
						}
					}
				}
			}
			if (eventName === "invoice.payment_failed") {
				const invoice = data;
				const subscriptionCode = invoice.subscription?.subscription_code ?? invoice.subscription_code;
				if (subscriptionCode !== null && subscriptionCode !== void 0 && subscriptionCode !== "") {
					const existing = await ctx.context.adapter.findOne({
						model: PAYSTACK_MODELS.subscription,
						where: [{
							field: "subscriptionCode",
							value: subscriptionCode
						}]
					});
					if (existing && existing.status !== "canceled" && existing.status !== "past_due") {
						const pastDue = {
							...existing,
							status: "past_due",
							updatedAt: /* @__PURE__ */ new Date()
						};
						await ctx.context.adapter.update({
							model: PAYSTACK_MODELS.subscription,
							where: [{
								field: "id",
								value: existing.id
							}],
							update: {
								status: pastDue.status,
								updatedAt: pastDue.updatedAt
							}
						});
						const plan = await getPlanByName(options, existing.plan);
						if (plan) await options.subscription?.onSubscriptionUpdate?.({
							event,
							subscription: pastDue,
							plan
						}, ctx);
					}
				}
			}
			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: "customerCode",
							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: PAYSTACK_MODELS.subscription,
								where
							}))?.find((candidate) => candidate.status !== "canceled" && (candidate.subscriptionCode === subscriptionCode || candidate.status === "incomplete" || candidate.status === "trialing" && (candidate.subscriptionCode === void 0 || candidate.subscriptionCode === null || candidate.subscriptionCode === "")) && 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,
									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: PAYSTACK_MODELS.subscription,
									update: {
										subscriptionCode: persistedSubscription.subscriptionCode,
										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);
										throw error;
									}
									if (subscription.status === "trialing") try {
										await plan.freeTrial?.onTrialEnd?.(persistedSubscription);
									} catch (error) {
										ctx.context.logger.error("Paystack trial end callback failed", error);
										throw 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: PAYSTACK_MODELS.subscription,
							where: [{
								field: "subscriptionCode",
								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: PAYSTACK_MODELS.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: "subscriptionCode",
								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: PAYSTACK_MODELS.subscription,
							where: [{
								field: "subscriptionCode",
								value: subscriptionCode
							}]
						});
						if (existingSub?.status === "past_due") {
							const recovered = {
								...existingSub,
								status: "active",
								updatedAt: /* @__PURE__ */ new Date()
							};
							await ctx.context.adapter.update({
								model: PAYSTACK_MODELS.subscription,
								update: {
									status: recovered.status,
									updatedAt: recovered.updatedAt
								},
								where: [{
									field: "id",
									value: existingSub.id
								}]
							});
							const plan = await getPlanByName(options, existingSub.plan);
							if (plan) await options.subscription.onSubscriptionUpdate?.({
								event,
								subscription: recovered,
								plan
							}, ctx);
						}
						if (existingSub !== void 0 && existingSub !== null && existingSub.pendingPlan !== void 0 && existingSub.pendingPlan !== null && existingSub.pendingPlan !== "") await ctx.context.adapter.update({
							model: PAYSTACK_MODELS.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);
				throw _e;
			}
			await options.onEvent?.(event);
			if (await ctx.context.adapter.update({
				model: PAYSTACK_MODELS.webhookEvent,
				where: [{
					field: "eventId",
					value: eventId
				}, {
					field: "status",
					value: claimStatus
				}],
				update: {
					status: "processed",
					processedAt: /* @__PURE__ */ new Date(),
					updatedAt: /* @__PURE__ */ new Date()
				}
			}) === null) throw new Error("Webhook processing lease was lost.");
			return ctx.json({ received: true });
		} catch (error) {
			if (claimStatus !== void 0) await ctx.context.adapter.update({
				model: PAYSTACK_MODELS.webhookEvent,
				where: [{
					field: "eventId",
					value: eventId
				}, {
					field: "status",
					value: claimStatus
				}],
				update: {
					status: "pending",
					updatedAt: /* @__PURE__ */ new Date()
				}
			});
			ctx.context.logger.error("Paystack webhook processing failed; delivery must be retried", error);
			throw new APIError("SERVICE_UNAVAILABLE", { message: "Webhook processing failed; retry delivery." });
		}
	});
};
const initializeTransaction = (options, path = "/initialize-transaction") => {
	const subscriptionOptions = options.subscription;
	const useMiddlewares = subscriptionOptions?.enabled === true ? [
		sessionMiddleware,
		originCheckMiddleware,
		referenceMiddleware(options, "initialize-transaction")
	] : [sessionMiddleware, originCheckMiddleware];
	return createAuthEndpoint(path, {
		method: "POST",
		body: initializeTransactionBodySchema,
		use: useMiddlewares
	}, async (ctx) => {
		const paystack = getPaystackOps(options.paystackClient);
		const { plan: planName, product: productName, amount: bodyAmount, currency, email, metadata: extraMetadata, callbackURL, quantity, scheduleAtPeriodEnd, cancelAtPeriodEnd, prorateAndCharge, subscriptionId, confirmTrialTokenizationCharge } = ctx.body;
		if (callbackURL !== void 0 && callbackURL !== null && callbackURL !== "") {
			if (!isTrustedCallbackURL(callbackURL, ctx.context?.baseURL ?? ctx.request?.url ?? "")) 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 = plan !== void 0 ? plan.amount : product ? product.price ?? product.amount : bodyAmount;
		const finalCurrency = plan !== void 0 || product ? product?.currency ?? product?.currency ?? plan?.currency ?? "NGN" : 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, {
			userId: user.id,
			referenceId,
			subscriptionId,
			plan,
			scheduleAtPeriodEnd,
			cancelAtPeriodEnd
		}, options);
		if (scheduledChange !== null) return ctx.json(scheduledChange);
		let validatedQuantity = quantity ?? 1;
		if (product && amount !== void 0) amount *= quantity ?? 1;
		if (amount !== void 0 && (!Number.isSafeInteger(amount) || amount <= 0)) throw new APIError("BAD_REQUEST", { message: "Catalog amount must be a positive safe integer." });
		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;
				const quantityToUse = quantity ?? seatCount;
				validatedQuantity = quantityToUse;
				amount = calculatePlanAmount(plan, quantityToUse);
			}
		} 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;
		const trialTokenizationCharge = trial.granted ? getTrialTokenizationCharge(plan, finalCurrency) : void 0;
		if (trial.granted && confirmTrialTokenizationCharge !== true) throw new APIError("BAD_REQUEST", {
			code: "TRIAL_TOKENIZATION_CHARGE_CONFIRMATION_REQUIRED",
			message: `This Paystack trial requires a one-time tokenization charge of ${trialTokenizationCharge} minor units in ${finalCurrency}. Tell the customer the exact charge and retry with confirmTrialTokenizationCharge: true after they accept it.`
		});
		const normalizeInitializationError = (error) => {
			ctx.context.logger.error("Failed to initialize Paystack transaction", error);
			return new APIError("BAD_REQUEST", {
				code: "FAILED_TO_INITIALIZE_TRANSACTION",
				message: error instanceof Error ? error.message : PAYSTACK_ERROR_CODES.FAILED_TO_INITIALIZE_TRANSACTION.message
			});
		};
		const prepared = await (async () => {
			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 { earlyResult: {
					kind: "checkout",
					url: proration.url ?? "",
					reference: proration.reference ?? "",
					accessCode: proration.accessCode ?? "",
					redirect: proration.redirect
				} };
				if (proration?.kind === "prorated") return { earlyResult: {
					kind: "prorated",
					status: proration.status,
					message: proration.message,
					prorated: proration.prorated
				} };
			}
			if (plan !== void 0) {
				if (trialStart !== void 0) initBody.amount = trialTokenizationCharge;
				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);
			}
			return { initBody };
		})().catch((error) => {
			if (options.subscription?.operationHooks === void 0) throw normalizeInitializationError(error);
			throw error;
		});
		if (prepared.earlyResult !== void 0) return ctx.json(prepared.earlyResult);
		const initBody = prepared.initBody;
		const operation = plan !== void 0 && options.subscription?.operationHooks !== void 0 ? {
			actor: { id: user.id },
			referenceId,
			kind: "initialize",
			subscription: subscriptionId !== void 0 ? await createBillingStore(ctx).findSubscriptionById(subscriptionId) : await createBillingStore(ctx).findCurrentSubscription(referenceId, groupId),
			intent: {
				plan: plan.name.toLowerCase(),
				amount: initBody.amount,
				currency: finalCurrency,
				interval: plan.interval,
				quantity: validatedQuantity,
				groupId,
				callbackURL,
				trialStart,
				trialEnd
			}
		} : void 0;
		const execute = async (providerReference, setPhase) => {
			if (providerReference !== void 0) initBody.reference = providerReference;
			try {
				const sdkRes = unwrapSdkResult(await paystack?.transaction?.initialize({ body: initBody }));
				if (providerReference !== void 0 && sdkRes?.reference !== providerReference) throw new APIError("BAD_REQUEST", { message: "Provider reference does not match reserved operation." });
				url = sdkRes?.authorization_url;
				reference = sdkRes?.reference;
				accessCode = sdkRes?.access_code;
			} catch (error) {
				throw normalizeInitializationError(error);
			}
			setPhase("persistence");
			await ctx.context.adapter.create({
				model: PAYSTACK_MODELS.transaction,
				data: {
					reference: reference ?? "",
					referenceId,
					userId: user.id,
					amount: initBody.amount ?? 0,
					currency: finalCurrency,
					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) {
				const store = createBillingStore(ctx);
				let customerCode = (await store.findCustomerByReference(referenceId === user.id ? "user" : "organization", referenceId))?.customerCode;
				if (customerCode === void 0) try {
					customerCode = (await ctx.context.adapter.findOne({
						model: referenceId === user.id ? "user" : "organization",
						where: [{
							field: "id",
							value: referenceId
						}],
						select: ["paystackCustomerCode"]
					}))?.paystackCustomerCode ?? void 0;
					if (customerCode !== void 0 && customerCode !== null && customerCode !== "") await store.saveCustomer(referenceId === user.id ? "user" : "organization", referenceId, customerCode);
				} catch {}
				await ctx.context.adapter.create({
					model: PAYSTACK_MODELS.subscription,
					data: {
						plan: plan.name.toLowerCase(),
						groupId,
						referenceId,
						userId: user.id,
						customerCode,
						subscriptionCode: void 0,
						planCode: plan.planCode,
						transactionReference: reference ?? "",
						status: "incomplete",
						billingInterval: plan.interval ?? null,
						seats: quantity ?? 1,
						periodStart: /* @__PURE__ */ new Date(),
						periodEnd: new Date(Date.now() + 2592e6),
						cancelAtPeriodEnd: false,
						trialStart,
						trialEnd,
						createdAt: /* @__PURE__ */ new Date(),
						updatedAt: /* @__PURE__ */ new Date()
					}
				});
			}
			return ctx.json({
				kind: "checkout",
				url: url ?? "",
				reference: reference ?? "",
				accessCode: accessCode ?? "",
				redirect: true,
				...trial.granted && trialTokenizationCharge !== void 0 ? { trial: {
					days: trial.requestedDays,
					tokenizationCharge: {
						amount: trialTokenizationCharge,
						currency: finalCurrency
					}
				} } : {}
			});
		};
		return operation !== void 0 ? runSubscriptionOperation(ctx, options, operation, execute) : execute(void 0, () => void 0);
	});
};
function getTrialTokenizationCharge(plan, currency) {
	const configured = plan?.freeTrial?.tokenizationCharge;
	if (configured !== void 0) {
		if (!Number.isSafeInteger(configured) || configured <= 0) throw new APIError("BAD_REQUEST", {
			code: "INVALID_TRIAL_TOKENIZATION_CHARGE",
			message: "freeTrial.tokenizationCharge must be a positive safe integer in minor units."
		});
		return configured;
	}
	const amount = {
		NGN: 5e3,
		GHS: 10,
		ZAR: 100,
		KES: 300,
		USD: 200
	}[currency.toUpperCase()];
	if (amount === void 0) throw new APIError("BAD_REQUEST", {
		code: "TRIAL_TOKENIZATION_CHARGE_REQUIRED",
		message: `Configure freeTrial.tokenizationCharge in minor units for ${currency}.`
	});
	return amount;
}
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") => {
	const verifyBodySchema = z.object({ reference: z.string() });
	const useMiddlewares = options.subscription?.enabled === true ? [
		sessionMiddleware,
		originCheckMiddleware,
		referenceMiddleware(options, "verify-transaction")
	] : [sessionMiddleware, originCheckMiddleware];
	return createAuthEndpoint(path, {
		method: "POST",
		body: verifyBodySchema,
		use: useMiddlewares
	}, 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;
	const useMiddlewares = subscriptionOptions?.enabled === true ? [
		sessionMiddleware,
		originCheckMiddleware,
		referenceMiddleware(options, "list-subscriptions")
	] : [sessionMiddleware, originCheckMiddleware];
	return createAuthEndpoint(path, {
		method: "GET",
		query: listQuerySchema,
		use: useMiddlewares
	}, 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") => {
	const listQuerySchema = z.object({ referenceId: z.string().optional() });
	const useMiddlewares = options.subscription?.enabled === true ? [
		sessionMiddleware,
		originCheckMiddleware,
		referenceMiddleware(options, "list-transactions")
	] : [sessionMiddleware, originCheckMiddleware];
	return createAuthEndpoint(path, {
		method: "GET",
		query: listQuerySchema,
		use: useMiddlewares
	}, 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") => {
	const useMiddlewares = options.subscription?.enabled === true ? [
		sessionMiddleware,
		originCheckMiddleware,
		referenceMiddleware(options, "disable-subscription")
	] : [sessionMiddleware, originCheckMiddleware];
	return createAuthEndpoint(path, {
		method: "POST",
		body: enableDisableBodySchema,
		use: useMiddlewares
	}, async (ctx) => {
		const { subscriptionCode, atPeriodEnd } = ctx.body;
		const paystack = getPaystackOps(options.paystackClient);
		try {
			if (isLocalSubscriptionCode(subscriptionCode)) {
				const sub = await ctx.context.adapter.findOne({
					model: PAYSTACK_MODELS.subscription,
					where: [{
						field: "subscriptionCode",
						value: subscriptionCode
					}]
				});
				if (sub !== null && sub !== void 0) {
					const now = /* @__PURE__ */ new Date();
					const immediate = atPeriodEnd === false;
					await ctx.context.adapter.update({
						model: PAYSTACK_MODELS.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" });
			}
			const storedSubscription = await createBillingStore(ctx).findSubscriptionByCode(subscriptionCode);
			const storedCredentials = storedSubscription ? await readPaystackPaymentCredentials(ctx.context.adapter, options, storedSubscription.id) : null;
			let emailToken = ctx.body.emailToken ?? storedCredentials?.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.");
			if (storedSubscription) await savePaystackPaymentCredentials(ctx.context.adapter, options, storedSubscription.id, { emailToken });
			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: PAYSTACK_MODELS.subscription,
				where: [{
					field: "subscriptionCode",
					value: subscriptionCode
				}]
			});
			if (sub !== void 0 && sub !== null) {
				const now = /* @__PURE__ */ new Date();
				const immediate = atPeriodEnd === false;
				await ctx.context.adapter.update({
					model: PAYSTACK_MODELS.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);
			const errorMessage = error instanceof Error ? error.message : PAYSTACK_ERROR_CODES.FAILED_TO_DISABLE_SUBSCRIPTION.message;
			throw new APIError("BAD_REQUEST", {
				code: "FAILED_TO_DISABLE_SUBSCRIPTION",
				message: errorMessage
			});
		}
	});
};
const enablePaystackSubscription = (options, path = "/enable-subscription") => {
	const useMiddlewares = options.subscription?.enabled === true ? [
		sessionMiddleware,
		originCheckMiddleware,
		referenceMiddleware(options, "enable-subscription")
	] : [sessionMiddleware, originCheckMiddleware];
	return createAuthEndpoint(path, {
		method: "POST",
		body: enableDisableBodySchema,
		use: useMiddlewares
	}, async (ctx) => {
		const { subscriptionCode } = ctx.body;
		const paystack = getPaystackOps(options.paystackClient);
		try {
			const storedSubscription = await createBillingStore(ctx).findSubscriptionByCode(subscriptionCode);
			const storedCredentials = storedSubscription ? await readPaystackPaymentCredentials(ctx.context.adapter, options, storedSubscription.id) : null;
			let emailToken = ctx.body.emailToken ?? storedCredentials?.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." });
			if (storedSubscription) await savePaystackPaymentCredentials(ctx.context.adapter, options, storedSubscription.id, { emailToken });
			await paystack?.subscription?.enable({ body: {
				code: subscriptionCode,
				token: emailToken
			} });
			await ctx.context.adapter.update({
				model: PAYSTACK_MODELS.subscription,
				update: {
					status: "active",
					cancelAtPeriodEnd: false,
					cancelAt: null,
					canceledAt: null,
					endedAt: null,
					updatedAt: /* @__PURE__ */ new Date()
				},
				where: [{
					field: "subscriptionCode",
					value: subscriptionCode
				}]
			});
			return ctx.json({ status: "success" });
		} catch (error) {
			ctx.context.logger.error("Failed to enable subscription", error);
			const errorMessage = error instanceof Error ? error.message : PAYSTACK_ERROR_CODES.FAILED_TO_ENABLE_SUBSCRIPTION.message;
			throw new APIError("BAD_REQUEST", {
				code: "FAILED_TO_ENABLE_SUBSCRIPTION",
				message: errorMessage
			});
		}
	});
};
const getSubscriptionManageLink = (options, path = "/subscription-manage-link") => {
	const manageLinkQuerySchema = z.object({ subscriptionCode: z.string() });
	const useMiddlewares = options.subscription?.enabled === true ? [
		sessionMiddleware,
		originCheckMiddleware,
		referenceMiddleware(options, "get-subscription-manage-link")
	] : [sessionMiddleware, originCheckMiddleware];
	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);
			const errorMessage = error instanceof Error ? error.message : "Failed to get subscription manage link";
			throw new APIError("BAD_REQUEST", { message: errorMessage });
		}
	};
	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);
			const errorMessage = error instanceof Error ? error.message : "Failed to list plans";
			throw new APIError("BAD_REQUEST", { message: errorMessage });
		}
	});
};
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 = { paystackSubscription: { fields: {
	plan: {
		type: "string",
		required: true,
		index: true
	},
	referenceId: {
		type: "string",
		required: true,
		index: true
	},
	userId: {
		type: "string",
		required: true,
		index: true
	},
	customerCode: {
		type: "string",
		required: false,
		index: true
	},
	subscriptionCode: {
		type: "string",
		required: false,
		unique: true
	},
	transactionReference: {
		type: "string",
		required: false,
		index: true
	},
	planCode: {
		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
	},
	createdAt: {
		type: "date",
		required: true
	},
	updatedAt: {
		type: "date",
		required: true
	}
} } };
const customers = { paystackCustomer: { fields: {
	referenceType: {
		type: "string",
		required: true
	},
	referenceId: {
		type: "string",
		required: true,
		index: true
	},
	referenceKey: {
		type: "string",
		required: true,
		unique: true
	},
	customerCode: {
		type: "string",
		required: true,
		unique: true
	},
	email: {
		type: "string",
		required: false
	},
	createdAt: {
		type: "date",
		required: true
	},
	updatedAt: {
		type: "date",
		required: true
	}
} } };
const paymentCredentials = { paystackPaymentCredential: { fields: {
	subscriptionId: {
		type: "string",
		required: true,
		unique: true,
		index: true
	},
	authorizationCodeEncrypted: {
		type: "string",
		required: false
	},
	emailTokenEncrypted: {
		type: "string",
		required: false
	},
	createdAt: {
		type: "date",
		required: true
	},
	updatedAt: {
		type: "date",
		required: true
	}
} } };
const user = { user: { fields: {} } };
const organization = { organization: { fields: { 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
	}
} } };
const webhookEvents = { paystackWebhookEvent: { fields: {
	eventId: {
		type: "string",
		required: true,
		unique: true
	},
	eventType: {
		type: "string",
		required: true,
		index: true
	},
	reference: {
		type: "string",
		required: false,
		index: true
	},
	payload: {
		type: "string",
		required: true
	},
	status: {
		type: "string",
		required: true,
		defaultValue: "pending",
		index: true
	},
	processedAt: {
		type: "date",
		required: false
	},
	createdAt: {
		type: "date",
		required: true
	},
	updatedAt: {
		type: "date",
		required: true
	}
} } };
({
	...subscriptions,
	...transactions,
	...customers,
	...paymentCredentials,
	...user,
	...organization,
	...products,
	...plans,
	...webhookEvents
});
const getSchema = (options) => {
	let baseSchema;
	const optionSchema = options.schema;
	if (options.subscription?.enabled === true) baseSchema = {
		...subscriptions,
		...transactions,
		...customers,
		...paymentCredentials,
		...user,
		...products,
		...plans,
		...webhookEvents
	};
	else baseSchema = {
		...user,
		...transactions,
		...customers,
		...paymentCredentials,
		...products,
		...plans,
		...webhookEvents
	};
	if (options.organization?.enabled === true) baseSchema = {
		...baseSchema,
		...organization
	};
	if (options.schema !== void 0 && "subscription" in options.schema) {
		const { subscription: _subscription, ...restSchema } = optionSchema ?? {};
		return mergeSchema(baseSchema, restSchema);
	}
	return mergeSchema(baseSchema, optionSchema);
};
//#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);
		const errorMessage = error instanceof Error ? error.message : "Failed to sync products";
		throw new APIError("BAD_REQUEST", { message: errorMessage });
	}
}
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);
		const errorMessage = error instanceof Error ? error.message : "Failed to sync plans";
		throw new APIError("BAD_REQUEST", { message: errorMessage });
	}
}
async function chargeSubscriptionRenewal(ctx, options, input) {
	const { subscriptionId } = 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" });
	const credentials = await readPaystackPaymentCredentials(ctx.context.adapter, options, subscription.id);
	if (credentials?.authorizationCode === void 0 || credentials.authorizationCode === null || credentials.authorizationCode === "") 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 = calculatePlanAmount(plan, subscription.seats ?? 1);
	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 renewalMetadata = createRenewalMetadata({
		subscriptionId,
		referenceId
	});
	const serializedRenewalMetadata = stringifyPaystackMetadata(renewalMetadata);
	const typedChargeData = await paystack.chargeAuthorization({
		email,
		amount,
		authorization_code: credentials.authorizationCode,
		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,
			transactionReference: typedChargeData.reference
		});
		return {
			status: "success",
			data: typedChargeData
		};
	}
	return {
		status: "failed",
		data: typedChargeData
	};
}
//#endregion
//#region src/migrations.ts
function asString(value) {
	return typeof value === "string" && value !== "" ? value : void 0;
}
function asIdentifier(value) {
	if (typeof value === "number" && Number.isFinite(value)) return String(value);
	return asString(value);
}
function asDate(value, fallback) {
	if (value instanceof Date) return value;
	if (typeof value === "string" || typeof value === "number") {
		const date = new Date(value);
		if (!Number.isNaN(date.getTime())) return date;
	}
	return fallback;
}
function asOptionalDate(value) {
	if (value === null) return null;
	if (value === void 0) return void 0;
	if (value instanceof Date) return value;
	if (typeof value === "string" || typeof value === "number") {
		const date = new Date(value);
		if (!Number.isNaN(date.getTime())) return date;
	}
}
async function findManySafely(adapter, model) {
	try {
		return await adapter.findMany({ model }) ?? [];
	} catch {
		return [];
	}
}
function migrationData(legacy) {
	const now = /* @__PURE__ */ new Date();
	const referenceId = asIdentifier(legacy.referenceId) ?? asIdentifier(legacy.userId) ?? "";
	const id = asIdentifier(legacy.id);
	return {
		...id !== void 0 ? { id } : {},
		plan: asString(legacy.plan) ?? "",
		referenceId,
		userId: asIdentifier(legacy.userId) ?? referenceId,
		customerCode: asString(legacy.customerCode) ?? asString(legacy.paystackCustomerCode) ?? void 0,
		subscriptionCode: asString(legacy.subscriptionCode) ?? asString(legacy.paystackSubscriptionCode) ?? void 0,
		transactionReference: asString(legacy.transactionReference) ?? asString(legacy.paystackTransactionReference) ?? void 0,
		planCode: asString(legacy.planCode) ?? asString(legacy.paystackPlanCode) ?? void 0,
		status: asString(legacy.status) ?? "incomplete",
		periodStart: asDate(legacy.periodStart, now),
		periodEnd: asOptionalDate(legacy.periodEnd),
		trialStart: asOptionalDate(legacy.trialStart),
		trialEnd: asOptionalDate(legacy.trialEnd),
		cancelAtPeriodEnd: legacy.cancelAtPeriodEnd === true,
		cancelAt: asOptionalDate(legacy.cancelAt),
		canceledAt: asOptionalDate(legacy.canceledAt),
		endedAt: asOptionalDate(legacy.endedAt),
		billingInterval: asString(legacy.billingInterval) ?? void 0,
		groupId: asString(legacy.groupId) ?? void 0,
		seats: typeof legacy.seats === "number" ? legacy.seats : void 0,
		pendingPlan: asString(legacy.pendingPlan) ?? void 0,
		createdAt: asDate(legacy.createdAt, now),
		updatedAt: asDate(legacy.updatedAt, now)
	};
}
async function migrateCustomer(adapter, referenceType, legacy, report) {
	const customerCode = asString(legacy.paystackCustomerCode) ?? asString(legacy.customerCode);
	const referenceId = asIdentifier(legacy.id);
	if (customerCode === void 0 || referenceId === void 0) return;
	const referenceKey = `${referenceType}:${referenceId}`;
	try {
		if (await adapter.findOne({
			model: PAYSTACK_MODELS.customer,
			where: [{
				field: "referenceKey",
				value: referenceKey
			}]
		})) {
			report.customers.skipped += 1;
			report.skipped += 1;
			return;
		}
		const now = /* @__PURE__ */ new Date();
		const email = asString(legacy.email);
		await adapter.create({
			model: PAYSTACK_MODELS.customer,
			data: {
				referenceType,
				referenceId,
				referenceKey,
				customerCode,
				...email !== void 0 ? { email } : {},
				createdAt: asDate(legacy.createdAt, now),
				updatedAt: asDate(legacy.updatedAt, now)
			}
		});
		report.customers.migrated += 1;
	} catch (error) {
		report.customers.failed += 1;
		report.failures.push({
			kind: "customer",
			id: referenceKey,
			message: error instanceof Error ? error.message : "Failed to migrate customer"
		});
	}
}
async function migratePaystackSubscriptionSchema(ctx, options) {
	const adapter = ctx.context.adapter;
	const report = {
		status: "complete",
		subscriptions: {
			migrated: 0,
			skipped: 0,
			failed: 0
		},
		customers: {
			migrated: 0,
			skipped: 0,
			failed: 0
		},
		credentials: {
			migrated: 0,
			skipped: 0,
			failed: 0
		},
		skipped: 0,
		failures: []
	};
	for (const referenceType of ["user", "organization"]) {
		const records = await findManySafely(adapter, referenceType);
		for (const record of records) await migrateCustomer(adapter, referenceType, record, report);
	}
	const legacySubscriptions = await findManySafely(adapter, LEGACY_PAYSTACK_MODELS.subscription);
	for (const legacy of legacySubscriptions) {
		const id = asIdentifier(legacy.id);
		if (id === void 0) continue;
		try {
			if (await adapter.findOne({
				model: PAYSTACK_MODELS.subscription,
				where: [{
					field: "id",
					value: id
				}]
			})) {
				report.subscriptions.skipped += 1;
				report.skipped += 1;
			} else {
				await adapter.create({
					model: PAYSTACK_MODELS.subscription,
					data: migrationData(legacy),
					forceAllowId: true
				});
				report.subscriptions.migrated += 1;
			}
		} catch (error) {
			report.subscriptions.failed += 1;
			report.failures.push({
				kind: "subscription",
				id,
				message: error instanceof Error ? error.message : "Failed to migrate subscription"
			});
		}
		const authorizationCode = asString(legacy.authorizationCode) ?? asString(legacy.paystackAuthorizationCode);
		const emailToken = asString(legacy.emailToken) ?? asString(legacy.paystackEmailToken);
		if (authorizationCode !== void 0 || emailToken !== void 0) try {
			if (await adapter.findOne({
				model: PAYSTACK_MODELS.paymentCredential,
				where: [{
					field: "subscriptionId",
					value: id
				}]
			})) {
				report.credentials.skipped += 1;
				report.skipped += 1;
			} else {
				await savePaystackPaymentCredentials(adapter, options, id, {
					authorizationCode,
					emailToken
				});
				report.credentials.migrated += 1;
			}
		} catch (error) {
			report.credentials.failed += 1;
			report.failures.push({
				kind: "credential",
				id,
				message: error instanceof Error ? error.message : "Failed to migrate credential"
			});
		}
	}
	report.status = report.failures.length > 0 ? "partial" : "complete";
	return report;
}
//#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 customerCode = (await createBillingStoreFromAdapter(ctx.adapter).findCustomerByReference("user", user.id))?.customerCode;
						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 = (await createBillingStoreFromAdapter(ctx.adapter).findCustomerByReference("organization", org.id))?.customerCode;
						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 customerCode = (await store.findCustomerByReference("organization", org.id))?.customerCode;
						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, migratePaystackSubscriptionSchema, parsePaystackMetadata, paystack, reconcilePaystackTransaction, stringifyPaystackMetadata, syncPaystackPlans, syncPaystackProducts };

//# sourceMappingURL=index.mjs.map