import { Elysia, t } from "elysia"; import prisma from "../lib/prisma"; const OMISE_SECRET_KEY = process.env.OMISE_SECRET_KEY || ""; export const paymentsRoute = new Elysia({ prefix: "/payments" }) .post("/charge", async ({ body, set }) => { try { const { orderId, amount, type, token } = body as { orderId: number; amount: number; type: "card" | "promptpay"; token?: string; }; if (!OMISE_SECRET_KEY) { set.status = 500; return { error: "Omise secret key is not configured" }; } // 1. Check if order exists const order = await prisma.order.findUnique({ where: { id: orderId } }); if (!order) { set.status = 404; return { error: "Order not found" }; } // Convert amount to satang (integer) const amountInSatang = Math.round(amount * 100); // Setup Basic Auth header for Omise const authHeader = "Basic " + Buffer.from(OMISE_SECRET_KEY + ":").toString("base64"); if (type === "card") { if (!token) { set.status = 400; return { error: "Card token is required for card payments" }; } // Create a card charge directly const chargeRes = await fetch("https://api.omise.co/charges", { method: "POST", headers: { "Authorization": authHeader, "Content-Type": "application/json" }, body: JSON.stringify({ amount: amountInSatang, currency: "THB", card: token, metadata: { orderId: String(orderId) } }) }); const charge = await chargeRes.json() as any; if (charge.error) { set.status = 400; return { error: charge.message || "Failed to create credit card charge" }; } // If card payment is immediately successful (some cards don't require 3DS) if (charge.status === "successful") { await prisma.order.update({ where: { id: orderId }, data: { status: "PAID" } }); } return { success: true, status: charge.status, chargeId: charge.id, authorizeUri: charge.authorize_uri || null // For 3DS card validation redirect }; } else { // PromptPay: // 1. Create source const sourceRes = await fetch("https://api.omise.co/sources", { method: "POST", headers: { "Authorization": authHeader, "Content-Type": "application/json" }, body: JSON.stringify({ type: "promptpay", amount: amountInSatang, currency: "THB" }) }); const source = await sourceRes.json() as any; if (source.error) { set.status = 400; return { error: source.message || "Failed to create PromptPay source" }; } // 2. Create charge const chargeRes = await fetch("https://api.omise.co/charges", { method: "POST", headers: { "Authorization": authHeader, "Content-Type": "application/json" }, body: JSON.stringify({ amount: amountInSatang, currency: "THB", source: source.id, metadata: { orderId: String(orderId) } }) }); const charge = await chargeRes.json() as any; if (charge.error) { set.status = 400; return { error: charge.message || "Failed to create PromptPay charge" }; } // Get QR Code Image URL from the charge's source references const qrCodeUrl = charge.source?.references?.barcode || null; return { success: true, status: charge.status, chargeId: charge.id, qrCodeUrl: qrCodeUrl }; } } catch (err: any) { console.error("[Payment Charge] Error:", err); set.status = 500; return { error: err.message || "Failed to process payment request" }; } }, { body: t.Object({ orderId: t.Number(), amount: t.Number(), type: t.Union([t.Literal("card"), t.Literal("promptpay")]), token: t.Optional(t.String()) }) }) .post("/webhook", async ({ body, set }) => { try { const event = body as any; console.log(`[Omise Webhook] Received event: ${event.key}`); if (event.key === "charge.complete") { const charge = event.data; const status = charge.status; const orderIdStr = charge.metadata?.orderId; if (orderIdStr && status === "successful") { const orderId = parseInt(orderIdStr, 10); console.log(`[Omise Webhook] Payment successful for Order #${orderId}`); await prisma.order.update({ where: { id: orderId }, data: { status: "PAID" } }); } } return { received: true }; } catch (err: any) { console.error("[Omise Webhook] Error processing event:", err); set.status = 500; return { error: "Failed to process webhook" }; } });