import { Elysia, t } from "elysia"; import prisma from "../lib/prisma"; const toNum = (val) => { if (val === null || val === undefined || val === "") return 0; const s = String(val).replace(/[^0-9.\-]/g, ""); const n = parseFloat(s); return isNaN(n) ? 0 : n; }; const str = (val) => String(val ?? "").trim(); const MONTHS = { Jan: '01', Feb: '02', Mar: '03', Apr: '04', May: '05', Jun: '06', Jul: '07', Aug: '08', Sep: '09', Oct: '10', Nov: '11', Dec: '12' }; const parseLazadaDate = (val) => { const d = str(val); if (!d) return ""; // e.g. "25 Apr 2026 23:24" const parts = d.split(" "); if (parts.length >= 4) { const [day, monthStr, year, time] = parts; const month = MONTHS[monthStr] || "01"; return `${year}-${month}-${day.padStart(2, "0")} ${time}:00`; } return d; }; export const lazadaOrderRoute = new Elysia({ prefix: "/lazada-order" }) // ── Import (Excel rows as JSON) ─────────────────────────────────────────── .post("/import", async ({ body, set }) => { try { const { rows, shop } = body; const shopName = shop || "hom-aroma"; console.log(`[LazadaOrder] Received ${rows.length} raw rows for shop: ${shopName}`); // Group rows by orderNumber const orderMap = new Map(); for (const row of rows) { const orderNumber = str(row["orderNumber"]); if (!orderNumber) continue; if (!orderMap.has(orderNumber)) orderMap.set(orderNumber, { meta: row, items: [] }); orderMap.get(orderNumber).items.push(row); } console.log(`[LazadaOrder] Grouped into ${orderMap.size} unique orders`); const CHUNK = 30; const entries = Array.from(orderMap.entries()); let upserted = 0; for (let i = 0; i < entries.length; i += CHUNK) { const chunk = entries.slice(i, i + CHUNK); await prisma.$transaction(async (tx) => { for (const [orderNumber, { meta, items }] of chunk) { // Sync strategy: delete old items await tx.lazadaOrderItem.deleteMany({ where: { orderNumber } }); let totalPaidPrice = 0; let totalShippingFee = 0; const itemsData = items.map((item) => { const paidPrice = toNum(item["paidPrice"]); const shippingFee = toNum(item["shippingFee"]); totalPaidPrice += paidPrice; totalShippingFee += shippingFee; return { orderItemId: str(item["orderItemId"]), sellerSku: str(item["sellerSku"]), lazadaSku: str(item["lazadaSku"]), itemName: str(item["itemName"]), variation: str(item["variation"]), unitPrice: toNum(item["unitPrice"]), paidPrice, sellerDiscountTotal: toNum(item["sellerDiscountTotal"]), platformDiscountTotal: toNum(item["platformDiscountTotal"]), shippingFee, status: str(item["status"]), }; }); // Decide order status based on items (or first item) // We'll just use the first item's status if they share it const orderStatus = str(meta["status"]); const orderData = { shop: shopName, createTime: parseLazadaDate(meta["createTime"]), updateTime: parseLazadaDate(meta["updateTime"]), deliveredDate: parseLazadaDate(meta["deliveredDate"]), customerName: str(meta["customerName"]), shippingCity: str(meta["shippingCity"]), shippingPostCode: str(meta["shippingPostCode"]), shippingRegion: str(meta["shippingRegion"]), payMethod: str(meta["payMethod"]), status: orderStatus, trackingCode: str(meta["trackingCode"]), shippingProvider: str(meta["shippingProvider"]), orderAmount: totalPaidPrice, shippingFee: totalShippingFee, }; await tx.lazadaOrder.upsert({ where: { orderNumber }, update: { ...orderData, items: { create: itemsData } }, create: { orderNumber, ...orderData, items: { create: itemsData } }, }); upserted++; } }, { timeout: 30000 }); console.log(`[LazadaOrder] Progress: ${Math.min(i + CHUNK, entries.length)}/${entries.length}`); } console.log(`[LazadaOrder] ✅ Done — ${upserted} orders upserted`); return { success: true, message: `นำเข้าสำเร็จ ${upserted} คำสั่งซื้อ (${rows.length} แถว)`, ordersCount: upserted, rowsCount: rows.length }; } catch (error) { console.error(`[LazadaOrder] ❌ Error:`, error.message); set.status = 500; return { success: false, message: error.message }; } }, { body: t.Object({ rows: t.Array(t.Any()), shop: t.Optional(t.String()) }) }) // ── Clear Shop Data ─────────────────────────────────────────────────────── .delete("/clear", async ({ query, set }) => { try { const shop = query.shop || "hom-aroma"; const result = await prisma.lazadaOrder.deleteMany({ where: { shop } }); return { success: true, message: `ลบข้อมูลร้าน ${shop} สำเร็จ (${result.count} รายการ)`, count: result.count }; } catch (error) { set.status = 500; return { success: false, message: error.message }; } }) // ── GET Single Order ────────────────────────────────────────────────────── .get("/orders/:orderNumber", async ({ params, set }) => { const order = await prisma.lazadaOrder.findUnique({ where: { orderNumber: params.orderNumber }, include: { items: true } }); if (!order) { set.status = 404; return { error: "Order not found" }; } return order; }) // ── GET Orders (paginated) ──────────────────────────────────────────────── .get("/orders", async ({ query }) => { const page = Number(query.page ?? 1); const limit = Number(query.limit ?? 30); const skip = (page - 1) * limit; const shop = query.shop || "hom-aroma"; const where = { shop }; const [items, total] = await Promise.all([ prisma.lazadaOrder.findMany({ where, include: { items: true }, orderBy: { createTime: "desc" }, skip, take: limit }), prisma.lazadaOrder.count({ where }), ]); return { items, total, page, limit, totalPages: Math.ceil(total / limit) }; }) // ── GET Summary ─────────────────────────────────────────────────────────── .get("/summary", async ({ query }) => { const shop = query.shop || "hom-aroma"; const where = { shop }; const [orderCount, agg] = await Promise.all([ prisma.lazadaOrder.count({ where }), prisma.lazadaOrder.aggregate({ where, _sum: { orderAmount: true, shippingFee: true } }), ]); return { orderCount, totalAmount: agg._sum.orderAmount ?? 0, totalShipping: agg._sum.shippingFee ?? 0, }; }) // ── GET Analytics (Single Shop) ─────────────────────────────────────────── .get("/analytics", async ({ query }) => { const shop = query.shop || "hom-aroma"; const dateFrom = query.dateFrom; const dateTo = query.dateTo; const where = { shop }; if (dateFrom || dateTo) { where.createTime = {}; if (dateFrom) where.createTime.gte = dateFrom; if (dateTo) where.createTime.lte = dateTo + " 23:59"; } const allOrders = await prisma.lazadaOrder.findMany({ where, select: { orderNumber: true, status: true, createTime: true, orderAmount: true, shippingFee: true, payMethod: true, items: { select: { itemName: true, variation: true, sellerDiscountTotal: true, platformDiscountTotal: true, paidPrice: true, sellerSku: true } }, }, }); // Monthly aggregation const monthMap = new Map(); for (const o of allOrders) { const month = (o.createTime ?? "").slice(0, 7) || "Unknown"; if (!monthMap.has(month)) monthMap.set(month, { month, revenue: 0, orders: 0, cancelled: 0, items: 0, platformDiscount: 0, sellerDiscount: 0 }); const m = monthMap.get(month); m.orders += 1; m.revenue += o.orderAmount; m.platformDiscount += o.items.reduce((s, i) => s + Math.abs(i.platformDiscountTotal), 0); m.sellerDiscount += o.items.reduce((s, i) => s + Math.abs(i.sellerDiscountTotal), 0); if (o.status?.toLowerCase().includes("canceled") || o.status?.toLowerCase().includes("failed")) m.cancelled += 1; m.items += o.items.length; } const monthly = Array.from(monthMap.values()).sort((a, b) => a.month.localeCompare(b.month)); // Status breakdown const statusMap = new Map(); for (const o of allOrders) { const key = o.status || "Unknown"; statusMap.set(key, (statusMap.get(key) ?? 0) + 1); } const statusBreakdown = Array.from(statusMap.entries()).map(([status, count]) => ({ status, count })).sort((a, b) => b.count - a.count); // Payment method breakdown const payMap = new Map(); for (const o of allOrders) { const key = o.payMethod || "Unknown"; payMap.set(key, (payMap.get(key) ?? 0) + 1); } const paymentBreakdown = Array.from(payMap.entries()).map(([method, count]) => ({ method, count })).sort((a, b) => b.count - a.count); // Top products const productMap = new Map(); for (const o of allOrders) { for (const item of o.items) { const key = `${item.itemName}__${item.variation ?? ""}`; if (!productMap.has(key)) productMap.set(key, { name: item.itemName, variation: item.variation ?? "", qty: 0, revenue: 0 }); const p = productMap.get(key); p.qty += 1; // Lazada export is 1 row per item quantity usually, so we add 1 p.revenue += item.paidPrice; } } const topProducts = Array.from(productMap.values()).sort((a, b) => b.revenue - a.revenue).slice(0, 10); // Summary const totalRevenue = allOrders.reduce((s, o) => s + o.orderAmount, 0); const totalShipping = allOrders.reduce((s, o) => s + o.shippingFee, 0); const totalPlatformDisc = allOrders.flatMap(o => o.items).reduce((s, i) => s + Math.abs(i.platformDiscountTotal), 0); const totalSellerDisc = allOrders.flatMap(o => o.items).reduce((s, i) => s + Math.abs(i.sellerDiscountTotal), 0); const totalOrders = allOrders.length; const cancelledOrders = allOrders.filter(o => o.status?.toLowerCase().includes("canceled")).length; const cancelRate = totalOrders > 0 ? (cancelledOrders / totalOrders) * 100 : 0; const completedOrders = totalOrders - cancelledOrders; const aov = completedOrders > 0 ? totalRevenue / completedOrders : 0; return { monthly, statusBreakdown, paymentBreakdown, topProducts, summary: { totalRevenue, totalShipping, totalPlatformDisc, totalSellerDisc, netRevenue: totalRevenue - totalShipping, // Simple net (excluding lazada fees which aren't in this report usually) totalOrders, completedOrders, cancelledOrders, cancelRate, aov, }, }; }) // ── GET Analytics Overview (ALL shops combined) ────────────────────────── .get("/analytics-overview", async ({ query }) => { const dateFrom = query.dateFrom; const dateTo = query.dateTo; const where = {}; if (dateFrom || dateTo) { where.createTime = {}; if (dateFrom) where.createTime.gte = dateFrom; if (dateTo) where.createTime.lte = dateTo + " 23:59"; } const allOrders = await prisma.lazadaOrder.findMany({ where, select: { orderNumber: true, shop: true, status: true, createTime: true, orderAmount: true, shippingFee: true, items: { select: { platformDiscountTotal: true, sellerDiscountTotal: true, paidPrice: true, itemName: true, variation: true } }, }, }); // Per-shop summary const shopMap = new Map(); for (const o of allOrders) { if (!shopMap.has(o.shop)) { shopMap.set(o.shop, { shop: o.shop, orders: 0, completed: 0, cancelled: 0, revenue: 0, shippingFee: 0, platformDiscount: 0, sellerDiscount: 0, items: 0 }); } const s = shopMap.get(o.shop); s.orders += 1; s.revenue += o.orderAmount; s.shippingFee += o.shippingFee; s.platformDiscount += o.items.reduce((acc, i) => acc + Math.abs(i.platformDiscountTotal), 0); s.sellerDiscount += o.items.reduce((acc, i) => acc + Math.abs(i.sellerDiscountTotal), 0); s.items += o.items.length; if (o.status?.toLowerCase().includes("canceled")) s.cancelled += 1; else s.completed += 1; } const shops = Array.from(shopMap.values()); // Monthly stacked by shop const monthShopMap = new Map(); for (const o of allOrders) { const month = (o.createTime ?? "").slice(0, 7) || "Unknown"; if (!monthShopMap.has(month)) monthShopMap.set(month, new Map()); const sm = monthShopMap.get(month); if (!sm.has(o.shop)) sm.set(o.shop, { revenue: 0, orders: 0 }); const ms = sm.get(o.shop); ms.revenue += o.orderAmount; ms.orders += 1; } const shopKeys = Array.from(shopMap.keys()); const monthly = Array.from(monthShopMap.entries()) .map(([month, sm]) => { const row = { month }; for (const shop of shopKeys) { const d = sm.get(shop) ?? { revenue: 0, orders: 0 }; row[`${shop}_revenue`] = d.revenue; row[`${shop}_orders`] = d.orders; } row.totalRevenue = Array.from(sm.values()).reduce((s, v) => s + v.revenue, 0); row.totalOrders = Array.from(sm.values()).reduce((s, v) => s + v.orders, 0); row.revenue = row.totalRevenue; // Provide fallback for global view return row; }) .sort((a, b) => a.month.localeCompare(b.month)); // Top Products Global const productMap = new Map(); for (const o of allOrders) { for (const item of o.items) { const key = `${item.itemName}__${item.variation ?? ""}`; if (!productMap.has(key)) productMap.set(key, { name: item.itemName, variation: item.variation ?? "", qty: 0, revenue: 0 }); const p = productMap.get(key); p.qty += 1; p.revenue += item.paidPrice; } } const topProducts = Array.from(productMap.values()).sort((a, b) => b.revenue - a.revenue).slice(0, 10); // Global summary const totalRevenue = shops.reduce((s, sh) => s + sh.revenue, 0); const totalShipping = shops.reduce((s, sh) => s + sh.shippingFee, 0); const totalPlatformDisc = shops.reduce((s, sh) => s + sh.platformDiscount, 0); const totalSellerDisc = shops.reduce((s, sh) => s + sh.sellerDiscount, 0); const totalOrders = shops.reduce((s, sh) => s + sh.orders, 0); const completedOrders = shops.reduce((s, sh) => s + sh.completed, 0); const cancelledOrders = shops.reduce((s, sh) => s + sh.cancelled, 0); return { shops, monthly, topProducts, summary: { totalRevenue, totalShipping, totalPlatformDisc, totalSellerDisc, netRevenue: totalRevenue - totalShipping, totalOrders, completedOrders, cancelledOrders, cancelRate: totalOrders > 0 ? (cancelledOrders / totalOrders) * 100 : 0, aov: completedOrders > 0 ? totalRevenue / completedOrders : 0, } }; }); //# sourceMappingURL=lazadaOrder.js.map