173 lines
5.4 KiB
TypeScript
173 lines
5.4 KiB
TypeScript
import { Elysia, t } from "elysia";
|
|
import prisma from "../lib/prisma";
|
|
import { markOrderAsPaid } from "../lib/notifications";
|
|
|
|
const OMISE_SECRET_KEY = process.env.OMISE_SECRET_KEY || "";
|
|
|
|
export const paymentsRoute = new Elysia({ prefix: "/payments" })
|
|
.post("/charge", async ({ body, set }) => {
|
|
console.log("[Payment Charge] Incoming request body:", JSON.stringify(body));
|
|
try {
|
|
const { orderId, amount, type, token } = body as {
|
|
orderId: number;
|
|
amount: number;
|
|
type: "card" | "promptpay";
|
|
token?: string;
|
|
};
|
|
|
|
if (!OMISE_SECRET_KEY) {
|
|
console.error("[Payment Charge] Error: OMISE_SECRET_KEY is empty");
|
|
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 markOrderAsPaid(orderId);
|
|
}
|
|
|
|
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;
|
|
console.log("[Payment Charge] Omise PromptPay Source response:", JSON.stringify(source));
|
|
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;
|
|
console.log("[Payment Charge] Omise PromptPay Charge response:", JSON.stringify(charge));
|
|
if (charge.error) {
|
|
set.status = 400;
|
|
return { error: charge.message || "Failed to create PromptPay charge" };
|
|
}
|
|
|
|
// Get QR Code Image URL from scannable_code or references
|
|
const qrCodeUrl = charge.source?.scannable_code?.image?.download_uri || 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 markOrderAsPaid(orderId);
|
|
}
|
|
}
|
|
|
|
return { received: true };
|
|
} catch (err: any) {
|
|
console.error("[Omise Webhook] Error processing event:", err);
|
|
set.status = 500;
|
|
return { error: "Failed to process webhook" };
|
|
}
|
|
});
|