feat(payment): expose secure APIs and themed pages
This commit is contained in:
1 parent
dd8d6ecc3b
commit
2d77f5457c
15 files changed
+654
-13
No files matched your search
@@ -0,0 +1,117 @@
|
||||
import { getPaymentService } from "./core.ts";
|
||||
import {
|
||||
ensureOwnedPayment,
|
||||
getPaymentApiConfig,
|
||||
paymentApiAllowed,
|
||||
paymentApiSubject,
|
||||
publicPayment,
|
||||
type PaymentApiContext,
|
||||
} from "./http.ts";
|
||||
|
||||
const json = (value: unknown, status = 200) => Response.json(value, { status });
|
||||
const bodyOf = async (req: Request) =>
|
||||
(await req.json().catch(() => ({}))) as Record<string, unknown>;
|
||||
const idOf = (ctx: PaymentApiContext) => ctx.params?.id?.trim() ?? "";
|
||||
const resultResponse = (result: any, success = 200) =>
|
||||
result.ok
|
||||
? json(result.value, success)
|
||||
: json({ error: result.reason, fault: result.fault }, 422);
|
||||
|
||||
export async function GET(ctx: PaymentApiContext) {
|
||||
const service = getPaymentService();
|
||||
const path = new URL(ctx.req.url).pathname;
|
||||
if (path.endsWith("/providers")) {
|
||||
if (getPaymentApiConfig().enabled === false)
|
||||
return json({ error: "payment API disabled" }, 404);
|
||||
return json({ providers: service.providers() });
|
||||
}
|
||||
if (path.endsWith("/methods")) {
|
||||
if (!(await paymentApiAllowed(ctx, "methods")))
|
||||
return json({ error: "payment access denied" }, 403);
|
||||
return json({ methods: await service.paymentMethods(await paymentApiSubject(ctx)) });
|
||||
}
|
||||
if (idOf(ctx)) {
|
||||
const owned = await ensureOwnedPayment(service, ctx, idOf(ctx), "read");
|
||||
if (owned.response) return owned.response;
|
||||
return json(publicPayment(owned.payment!, await service.checkPayment(owned.payment!.id)));
|
||||
}
|
||||
if (!(await paymentApiAllowed(ctx, "list"))) return json({ error: "payment access denied" }, 403);
|
||||
const subject = await paymentApiSubject(ctx);
|
||||
const rows = await service.listPayments({ subject });
|
||||
return json({
|
||||
payments: await Promise.all(
|
||||
rows.map(async (payment) => publicPayment(payment, await service.checkPayment(payment.id))),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
export async function POST(ctx: PaymentApiContext) {
|
||||
const service = getPaymentService();
|
||||
const path = new URL(ctx.req.url).pathname;
|
||||
const body = await bodyOf(ctx.req);
|
||||
if (path.endsWith("/intents")) {
|
||||
if (!(await paymentApiAllowed(ctx, "initialize")))
|
||||
return json({ error: "payment access denied" }, 403);
|
||||
const subject = await paymentApiSubject(ctx);
|
||||
const resolver = getPaymentApiConfig().initialize;
|
||||
const input = resolver
|
||||
? await resolver(ctx, body, subject)
|
||||
: {
|
||||
amount: Number(body.amount),
|
||||
currency: String(body.currency ?? "").toUpperCase(),
|
||||
gateway: typeof body.gateway === "string" ? body.gateway : undefined,
|
||||
country: typeof body.country === "string" ? body.country : undefined,
|
||||
capture: body.capture === "manual" ? ("manual" as const) : ("automatic" as const),
|
||||
idempotencyKey: String(
|
||||
body.idempotencyKey ?? ctx.req.headers.get("idempotency-key") ?? "",
|
||||
),
|
||||
returnUrl: typeof body.returnUrl === "string" ? body.returnUrl : undefined,
|
||||
metadata:
|
||||
body.metadata && typeof body.metadata === "object"
|
||||
? (body.metadata as Record<string, string>)
|
||||
: undefined,
|
||||
};
|
||||
const result = await service.initializePayment({ ...input, subject });
|
||||
if (!result.ok) return resultResponse(result, 201);
|
||||
return json(publicPayment(result.value, await service.checkPayment(result.value.id)), 201);
|
||||
}
|
||||
if (path.endsWith("/reconcile")) {
|
||||
if (!(await paymentApiAllowed(ctx, "reconcile")))
|
||||
return json({ error: "payment access denied" }, 403);
|
||||
return json({ discrepancies: await service.reconcilePayments() });
|
||||
}
|
||||
const id = idOf(ctx);
|
||||
if (!id) return json({ error: "payment id is required" }, 400);
|
||||
const action = path.endsWith("/sync")
|
||||
? "sync"
|
||||
: path.endsWith("/capture")
|
||||
? "capture"
|
||||
: path.endsWith("/cancel")
|
||||
? "cancel"
|
||||
: path.endsWith("/refund")
|
||||
? "refund"
|
||||
: undefined;
|
||||
if (!action) return json({ error: "payment operation not found" }, 404);
|
||||
const owned = await ensureOwnedPayment(service, ctx, id, action);
|
||||
if (owned.response) return owned.response;
|
||||
if (action === "sync") return resultResponse(await service.syncPayment(id));
|
||||
if (action === "capture")
|
||||
return resultResponse(
|
||||
await service.capturePayment(
|
||||
id,
|
||||
body.amount === undefined
|
||||
? undefined
|
||||
: { amount: Number(body.amount), currency: owned.payment!.currency },
|
||||
),
|
||||
);
|
||||
if (action === "cancel")
|
||||
return resultResponse(await service.cancelPayment(id, String(body.reason ?? "")));
|
||||
return resultResponse(
|
||||
await service.refundPayment({
|
||||
id,
|
||||
amount: body.amount === undefined ? undefined : Number(body.amount),
|
||||
reason: String(body.reason ?? ""),
|
||||
idempotencyKey: String(body.idempotencyKey ?? ctx.req.headers.get("idempotency-key") ?? ""),
|
||||
}),
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user