97 lines
3.1 KiB
TypeScript
97 lines
3.1 KiB
TypeScript
import type { InitializePaymentInput, PaymentService } from "./core.ts";
|
|
import type { PaymentIntent } from "./types.ts";
|
|
|
|
export type PaymentApiAction =
|
|
| "list"
|
|
| "read"
|
|
| "initialize"
|
|
| "sync"
|
|
| "cancel"
|
|
| "capture"
|
|
| "refund"
|
|
| "methods"
|
|
| "reconcile";
|
|
|
|
export interface PaymentApiContext {
|
|
req: Request;
|
|
params?: Record<string, string>;
|
|
user?: { id?: string };
|
|
session?: { userId?: string; subject?: string };
|
|
}
|
|
|
|
export interface PaymentApiConfig {
|
|
enabled?: boolean;
|
|
/** Resolve the authenticated subject on the server. Request-body subject values are ignored. */
|
|
subject?(context: PaymentApiContext): string | undefined | Promise<string | undefined>;
|
|
/** Additional authorization. Administrative actions deny by default. */
|
|
authorize?(
|
|
context: PaymentApiContext,
|
|
action: PaymentApiAction,
|
|
payment?: PaymentIntent,
|
|
): boolean | Promise<boolean>;
|
|
/** Replace client input with authoritative catalog/order data before creating an intent. */
|
|
initialize?(
|
|
context: PaymentApiContext,
|
|
body: Record<string, unknown>,
|
|
subject: string,
|
|
): Omit<InitializePaymentInput, "subject"> | Promise<Omit<InitializePaymentInput, "subject">>;
|
|
}
|
|
|
|
const HTTP = Symbol.for("@wrnexus/payment:http:v1");
|
|
type PaymentHttpGlobal = typeof globalThis & { [HTTP]?: PaymentApiConfig };
|
|
|
|
export function configurePaymentApi(config: PaymentApiConfig = {}) {
|
|
(globalThis as PaymentHttpGlobal)[HTTP] = config;
|
|
}
|
|
|
|
export function getPaymentApiConfig(): PaymentApiConfig {
|
|
return (globalThis as PaymentHttpGlobal)[HTTP] ?? {};
|
|
}
|
|
|
|
export async function paymentApiSubject(context: PaymentApiContext) {
|
|
const config = getPaymentApiConfig();
|
|
const resolved = await config.subject?.(context);
|
|
return resolved?.trim() || context.user?.id?.trim() || context.session?.userId?.trim() || "";
|
|
}
|
|
|
|
export async function paymentApiAllowed(
|
|
context: PaymentApiContext,
|
|
action: PaymentApiAction,
|
|
payment?: PaymentIntent,
|
|
) {
|
|
const config = getPaymentApiConfig();
|
|
if (config.enabled === false) return false;
|
|
if (config.authorize) return Boolean(await config.authorize(context, action, payment));
|
|
const subject = await paymentApiSubject(context);
|
|
if (!subject) return false;
|
|
if (["capture", "refund", "reconcile"].includes(action)) return false;
|
|
return !payment || payment.subject === subject;
|
|
}
|
|
|
|
export function publicPayment(payment: PaymentIntent, status: string) {
|
|
return {
|
|
id: payment.id,
|
|
amount: payment.amount,
|
|
currency: payment.currency,
|
|
gateway: payment.gateway,
|
|
status,
|
|
createdAt: payment.createdAt,
|
|
checkoutUrl: payment.checkoutUrl,
|
|
client: payment.client,
|
|
metadata: payment.metadata,
|
|
};
|
|
}
|
|
|
|
export async function ensureOwnedPayment(
|
|
service: PaymentService,
|
|
context: PaymentApiContext,
|
|
id: string,
|
|
action: PaymentApiAction,
|
|
) {
|
|
const payment = await service.getPayment(id);
|
|
if (!payment) return { response: Response.json({ error: "payment not found" }, { status: 404 }) };
|
|
if (!(await paymentApiAllowed(context, action, payment)))
|
|
return { response: Response.json({ error: "payment access denied" }, { status: 403 }) };
|
|
return { payment };
|
|
}
|