//#region src/errors.ts
var RefundExceedsPaidError = class extends Error {
	context;
	name = "RefundExceedsPaidError";
	code = "REFUND_EXCEEDS_PAID";
	httpStatus = 422;
	constructor(message, context) {
		super(message);
		this.context = context;
	}
};
/**
* `settleCharge` found a durable `pending` charge intent already owning this
* chargeKey — a concurrent in-flight attempt or a prior crash (floor #6). This
* call does NOT charge again (a re-charge risks a double-charge: provider dedup
* windows may be short). Recovery is `reconcileCharge`, driven by the
* host's `listUnsettledIntents` sweep. 409 = the in-flight conflict.
*/
var ChargeIntentPendingError = class extends Error {
	chargeKey;
	name = "ChargeIntentPendingError";
	code = "CHARGE_INTENT_PENDING";
	httpStatus = 409;
	constructor(chargeKey) {
		super(`a charge intent for "${chargeKey}" is already in-flight or stuck pending; reconcile it (do not re-charge)`);
		this.chargeKey = chargeKey;
	}
};
var WebhookVerificationError = class extends Error {
	name = "WebhookVerificationError";
	code = "WEBHOOK_VERIFICATION_FAILED";
	httpStatus = 400;
	constructor(message) {
		super(message);
	}
};
var InvalidAmountError = class extends Error {
	context;
	name = "InvalidAmountError";
	code = "INVALID_AMOUNT";
	httpStatus = 422;
	constructor(message, context) {
		super(message);
		this.context = context;
	}
};
var RefundFailedError = class extends Error {
	context;
	name = "RefundFailedError";
	code = "REFUND_FAILED";
	httpStatus = 502;
	constructor(context) {
		super(`refund ${context.providerRef} for "${context.chargeKey}" failed: status=${context.status}`);
		this.context = context;
	}
};
var InvalidKeyComponentError = class extends Error {
	context;
	name = "InvalidKeyComponentError";
	code = "INVALID_KEY_COMPONENT";
	httpStatus = 422;
	constructor(message, context) {
		super(message);
		this.context = context;
	}
};
//#endregion
//#region src/index.ts
/**
* Deterministic idempotency key — stable parts only; never Date.now/random/UUID.
* Each part is percent-encoded before joining so a host-supplied part containing
* the `:` separator cannot collide two distinct part-vectors onto one key
* (e.g. `['refund','ord','1:r']` vs `['refund','ord:1','r']`) — that collision
* would silently no-op the second `appendEntry` (`inserted:false`) and overstate
* the ledger. Encoding keeps `join(':')` injective over arbitrary parts.
*/
function idempotencyKey(parts) {
	return parts.map(encodeURIComponent).join(":");
}
/** Integer minor-units → bigint (M7). Unit normalization is the adapter's job. */
function toMinorUnits(amount) {
	if (!Number.isInteger(amount)) throw new Error(`toMinorUnits: amount must be an integer minor-units value, got ${amount}`);
	return BigInt(amount);
}
async function confirmSettlement(chargeKey, settlement, { ledger, db }) {
	if (settlement.amount < 0) throw new InvalidAmountError(`confirmSettlement: settlement amount must be non-negative, got ${settlement.amount}`, {
		value: settlement.amount,
		field: "settlement.amount"
	});
	const key = idempotencyKey(["charge", chargeKey]);
	await ledger.appendEntry(db, {
		key,
		delta: toMinorUnits(settlement.amount),
		currency: settlement.currency,
		reason: `settlement:${chargeKey}`
	});
}
async function confirmRefund(refundKey, refund, { ledger, db }) {
	if (!Number.isInteger(refund.amount) || refund.amount <= 0) throw new RefundExceedsPaidError("refund amount must be a positive integer minor-units value", {
		chargeKey: refund.chargeKey,
		amount: refund.amount
	});
	await ledger.appendEntry(db, {
		key: refundKey,
		delta: -toMinorUnits(refund.amount),
		currency: refund.currency,
		reason: `refund:${refund.chargeKey}:${refundKey}`
	});
}
/**
* M1/M2/M4 three-phase charge: a durable intent is claimed BEFORE the provider
* call, so a crash between charge and ledger-post leaves a RECOVERABLE `pending`
* intent rather than a silent charge-without-record (spec floor #6).
*
* Order: claimIntent → provider.charge → recordProviderRef → confirmSettlement
* → markIntentSettled.
*
* A `'settled'` claim is the idempotent replay short-circuit — the charge was
* already settled, so this returns without re-calling the provider (never a
* double-charge). A `'pending'` claim means a concurrent in-flight attempt OR a
* prior crash already owns this chargeKey; this call does NOT charge again —
* recovery is `reconcileCharge`, driven by the host's `listUnsettledIntents`
* sweep. The provider is called ONLY on a fresh `'won'` claim.
*/
async function settleCharge(req, { provider, ledger, db, intentStore }) {
	const claim = await intentStore.claimIntent(req.chargeKey, {
		amount: req.amount,
		currency: req.currency
	});
	if (claim === "settled") return {
		kind: "settled",
		chargeKey: req.chargeKey,
		providerRef: "",
		amount: req.amount,
		currency: req.currency
	};
	if (claim === "pending") throw new ChargeIntentPendingError(req.chargeKey);
	const result = await provider.charge(req);
	if (result.kind === "settled") {
		try {
			await intentStore.recordProviderRef(result.chargeKey, result.providerRef);
		} catch {}
		await confirmSettlement(result.chargeKey, {
			amount: result.amount,
			currency: result.currency,
			providerRef: result.providerRef,
			documentUrls: result.documentUrls
		}, {
			ledger,
			db
		});
		await intentStore.markIntentSettled(result.chargeKey);
	}
	return result;
}
/**
* Recovery for ONE stale `pending` charge intent (the host crons
* `listUnsettledIntents` and calls this per entry). Reads the prior outcome via
* the host resolver; if the charge settled, posts the idempotent
* `confirmSettlement` and flips the intent — never re-charging. An unsettled /
* unknown (`null`) outcome leaves the intent `pending` for the next sweep.
* Returns the action taken (for host observability).
*/
async function reconcileCharge(chargeKey, { intentStore, resolveChargeOutcome, ledger, db }) {
	const outcome = await resolveChargeOutcome(chargeKey);
	if (!outcome || !outcome.settled) return "unresolved";
	await confirmSettlement(chargeKey, {
		amount: outcome.amount,
		currency: outcome.currency,
		providerRef: outcome.providerRef
	}, {
		ledger,
		db
	});
	await intentStore.markIntentSettled(chargeKey);
	return "settled";
}
async function refundCharge(req, { provider, ledger, db }) {
	const result = await provider.refund(req);
	if (result.kind === "refunded") await confirmRefund(result.refundKey, {
		chargeKey: result.chargeKey,
		amount: result.amount,
		currency: result.currency,
		providerRef: result.providerRef
	}, {
		ledger,
		db
	});
	return result;
}
async function ingestWebhook(req, { provider, dedupStore, dispatch, ledger, db, settlementPreflight }) {
	if (req.bodyUsed) return new Response("webhook body already consumed — pass raw unparsed bytes", { status: 500 });
	let raw;
	try {
		raw = await req.text();
	} catch {
		return new Response("failed to read webhook body", { status: 500 });
	}
	let event;
	try {
		event = await provider.parseWebhook(raw, req.headers);
	} catch (err) {
		const status = err instanceof WebhookVerificationError ? 400 : 500;
		return new Response(err instanceof WebhookVerificationError ? "webhook verification failed" : "webhook processing failed", { status });
	}
	let claim;
	try {
		claim = await dedupStore.claim(event.eventId);
	} catch {
		return new Response("webhook claim failed", { status: 500 });
	}
	if (claim === "lost") return new Response(null, { status: 200 });
	try {
		if (event.kind === "settlement") {
			if (await settlementPreflight?.(event) === "reject") {
				try {
					await dedupStore.markProcessed(event.eventId);
				} catch {
					return new Response("webhook rejection could not be recorded", { status: 500 });
				}
				return new Response(null, { status: 200 });
			}
			await confirmSettlement(event.chargeKey, {
				amount: event.amount,
				currency: event.currency,
				providerRef: event.providerRef,
				documentUrls: event.documentUrls
			}, {
				ledger,
				db
			});
		}
	} catch {
		return new Response("webhook post failed", { status: 500 });
	}
	try {
		await dispatch(event);
	} catch {
		return new Response("webhook handler failed after dispatch", { status: 500 });
	}
	try {
		await dedupStore.markProcessed(event.eventId);
	} catch {
		return new Response(null, { status: 200 });
	}
	return new Response(null, { status: 200 });
}
let providerFactories = {};
function setProviderFactories(factories) {
	providerFactories = {
		...providerFactories,
		...factories
	};
}
function getProvider(name, creds) {
	const factory = providerFactories[name];
	if (!factory) throw new Error(`No provider factory registered for "${name}". Import @platform-modules/billing/${name} and register via setProviderFactories.`);
	return factory(creds);
}
//#endregion
export { ingestWebhook as a, setProviderFactories as c, ChargeIntentPendingError as d, InvalidAmountError as f, WebhookVerificationError as g, RefundFailedError as h, idempotencyKey as i, settleCharge as l, RefundExceedsPaidError as m, confirmSettlement as n, reconcileCharge as o, InvalidKeyComponentError as p, getProvider as r, refundCharge as s, confirmRefund as t, toMinorUnits as u };
