This commit is contained in:
Your Name committed 2026-06-07 18:21:27 +07:00
1 parent c6bf2f70ac
commit d3e817d8ba
4 files changed
+556 -70

No files matched your search

+2 -1
View File
@@ -21,6 +21,7 @@ import { couponsRoute } from "./routes/coupons";
import { reviewsRoute } from "./routes/reviews";
import { promotionsRoute } from "./routes/promotions";
import { pagesRoute } from "./routes/pages";
import { paymentsRoute } from "./routes/payments";
const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3000;
const CORS_ORIGIN = process.env.CORS_ORIGIN || "http://localhost:5173";
const JWT_SECRET = process.env.JWT_SECRET || "homaroma_secret_2026_scent";
@@ -84,7 +85,7 @@ const app = new Elysia()
return file;
})
.group("/api", (app) =>
app.use(authRoute).use(categoriesRoute).use(productsRoute).use(uploadRoute).use(heroRoute).use(ordersRoute).use(settingsRoute).use(articlesRoute).use(tiktokRoute).use(rawMaterialsRoute).use(shopeeRoute).use(tiktokOrderRoute).use(lazadaOrderRoute).use(customersRoute).use(couponsRoute).use(reviewsRoute).use(promotionsRoute).use(pagesRoute)
app.use(authRoute).use(categoriesRoute).use(productsRoute).use(uploadRoute).use(heroRoute).use(ordersRoute).use(settingsRoute).use(articlesRoute).use(tiktokRoute).use(rawMaterialsRoute).use(shopeeRoute).use(tiktokOrderRoute).use(lazadaOrderRoute).use(customersRoute).use(couponsRoute).use(reviewsRoute).use(promotionsRoute).use(pagesRoute).use(paymentsRoute)
)
.listen(PORT);
+173
View File
@@ -0,0 +1,173 @@
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" };
}
});