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; }; export const shopeeRoute = new Elysia({ prefix: "/shopee" }) // ── Import Orders ───────────────────────────────────────────────────────── .post("/import/orders", async ({ body, set }) => { try { const { rows, shop } = body; const shopName = shop || "hom-aroma"; console.log(`[Shopee] Received ${rows.length} raw rows for shop: ${shopName}`); const orderMap = new Map(); for (const row of rows) { const orderId = String(row["หมายเลขคำสั่งซื้อ"] || "").trim(); if (!orderId) continue; if (!orderMap.has(orderId)) orderMap.set(orderId, { meta: row, items: [] }); orderMap.get(orderId).items.push(row); } console.log(`[Shopee] Grouped into ${orderMap.size} unique orders`); const CHUNK_SIZE = 30; const entries = Array.from(orderMap.entries()); let upserted = 0; for (let i = 0; i < entries.length; i += CHUNK_SIZE) { const chunk = entries.slice(i, i + CHUNK_SIZE); await prisma.$transaction(async (tx) => { for (const [orderId, { meta, items }] of chunk) { await tx.shopeeOrderItem.deleteMany({ where: { orderId } }); const itemsData = items.map((item) => ({ parentSku: String(item["เลขอ้างอิง Parent SKU"] || ""), productName: String(item["ชื่อสินค้า"] || ""), skuRef: String(item["เลขอ้างอิง SKU (SKU Reference No.)"] || ""), variantName: String(item["ชื่อตัวเลือก"] || ""), originalPrice: toNum(item["ราคาตั้งต้น"]), dealPrice: toNum(item["ราคาขาย"]), quantity: Math.max(1, Math.round(toNum(item["จำนวน"]))), totalPayment: toNum(item["ยอดชำระเงิน"]), rawData: item, })); const orderData = { shop: shopName, status: String(meta["สถานะการสั่งซื้อ"] || ""), buyerUsername: String(meta["ชื่อผู้ใช้ (ผู้ซื้อ)"] || ""), orderCreatedAt: String(meta["วันที่ทำการสั่งซื้อ"] || ""), paymentTime: String(meta["เวลาการชำระสินค้า"] || ""), paymentMethod: String(meta["ช่องทางการชำระเงิน"] || ""), trackingNo: String(meta["*หมายเลขติดตามพัสดุ"] || ""), totalAmount: toNum(meta["จำนวนเงินทั้งหมด"]), buyerPaidAmount: toNum(meta["ราคาสินค้าที่ชำระโดยผู้ซื้อ (THB)"]), shippingFee: toNum(meta["ค่าจัดส่งที่ชำระโดยผู้ซื้อ"]), commission: toNum(meta["ค่าคอมมิชชั่น"]), completedAt: String(meta["เวลาที่ทำการสั่งซื้อสำเร็จ"] || ""), }; await tx.shopeeOrder.upsert({ where: { orderId }, update: { ...orderData, items: { create: itemsData } }, create: { orderId, ...orderData, items: { create: itemsData } }, }); upserted++; } }, { timeout: 30000 }); console.log(`[Shopee] Progress: ${Math.min(i + CHUNK_SIZE, entries.length)}/${entries.length}`); } console.log(`[Shopee] ✅ Done — ${upserted} orders upserted`); return { success: true, message: `นำเข้าสำเร็จ ${upserted} คำสั่งซื้อ (จาก ${rows.length} แถว)`, ordersCount: upserted, rowsCount: rows.length }; } catch (error) { console.error(`[Shopee] ❌ 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()) }) }) // ── GET Single Order ────────────────────────────────────────────────────── .get("/orders/:orderId", async ({ params, set }) => { const { orderId } = params; const order = await prisma.shopeeOrder.findUnique({ where: { orderId }, 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.shopeeOrder.findMany({ where, include: { items: true }, orderBy: { orderCreatedAt: "desc" }, skip, take: limit }), prisma.shopeeOrder.count({ where }), ]); return { items, total, page, limit, totalPages: Math.ceil(total / limit) }; }) // ── GET Analytics (with date range filter) ─────────────────────────────── .get("/analytics", async ({ query }) => { const shop = query.shop || "hom-aroma"; const dateFrom = query.dateFrom; // "YYYY-MM-DD" const dateTo = query.dateTo; // Build where — orderCreatedAt stored as "YYYY-MM-DD HH:mm" string const where = { shop }; if (dateFrom || dateTo) { where.orderCreatedAt = {}; if (dateFrom) where.orderCreatedAt.gte = dateFrom; if (dateTo) where.orderCreatedAt.lte = dateTo + " 23:59"; } const allOrders = await prisma.shopeeOrder.findMany({ where, select: { orderId: true, status: true, orderCreatedAt: true, totalAmount: true, buyerPaidAmount: true, commission: true, shippingFee: true, paymentMethod: true, items: { select: { quantity: true, totalPayment: true } }, }, }); // Monthly aggregation const monthMap = new Map(); for (const o of allOrders) { const month = (o.orderCreatedAt ?? "").slice(0, 7) || "Unknown"; if (!monthMap.has(month)) monthMap.set(month, { month, revenue: 0, orders: 0, commission: 0, cancelled: 0, items: 0 }); const m = monthMap.get(month); m.orders += 1; m.revenue += o.totalAmount; m.commission += o.commission; if (o.status.includes("ยกเลิก")) m.cancelled += 1; m.items += o.items.reduce((s, i) => s + i.quantity, 0); } 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) { let status = o.status; if (status.includes("สำเร็จแล้ว")) status = "สำเร็จแล้ว"; else if (status.includes("ได้รับสินค้าแล้ว")) status = "ได้รับสินค้าแล้ว"; else if (status.includes("ยกเลิก")) status = "ยกเลิก"; statusMap.set(status, (statusMap.get(status) ?? 0) + 1); } const statusBreakdown = Array.from(statusMap.entries()) .map(([status, count]) => ({ status, count })) .sort((a, b) => b.count - a.count); // Payment method breakdown const paymentMap = new Map(); for (const o of allOrders) { const key = o.paymentMethod || "Unknown"; if (!paymentMap.has(key)) paymentMap.set(key, { count: 0, revenue: 0 }); const p = paymentMap.get(key); p.count += 1; p.revenue += o.buyerPaidAmount; } const paymentBreakdown = Array.from(paymentMap.entries()) .map(([method, v]) => ({ method, ...v })) .sort((a, b) => b.count - a.count); // Top products (from filtered order IDs) const filteredOrderIds = allOrders.map(o => o.orderId); const allItems = filteredOrderIds.length > 0 ? await prisma.shopeeOrderItem.findMany({ where: { orderId: { in: filteredOrderIds } }, select: { productName: true, variantName: true, quantity: true, totalPayment: true, dealPrice: true }, }) : []; const productMap = new Map(); for (const item of allItems) { const key = `${item.productName}__${item.variantName ?? ""}`; if (!productMap.has(key)) productMap.set(key, { name: item.productName, variant: item.variantName ?? "", qty: 0, revenue: 0, orders: 0 }); const p = productMap.get(key); p.qty += item.quantity; p.revenue += item.totalPayment; p.orders += 1; } 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.totalAmount, 0); const totalCommission = allOrders.reduce((s, o) => s + o.commission, 0); const totalShipping = allOrders.reduce((s, o) => s + o.shippingFee, 0); const totalOrders = allOrders.length; const completedOrders = allOrders.filter(o => !o.status.includes("ยกเลิก")).length; const cancelledOrders = allOrders.filter(o => o.status.includes("ยกเลิก")).length; const cancelRate = totalOrders > 0 ? (cancelledOrders / totalOrders) * 100 : 0; const netRevenue = totalRevenue - totalCommission - totalShipping; const aov = completedOrders > 0 ? totalRevenue / completedOrders : 0; return { monthly, statusBreakdown, paymentBreakdown, topProducts, summary: { totalRevenue, totalCommission, totalShipping, netRevenue, totalOrders, completedOrders, cancelledOrders, cancelRate, aov, commissionRate: totalRevenue > 0 ? (totalCommission / totalRevenue) * 100 : 0, }, }; }) // ── 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.orderCreatedAt = {}; if (dateFrom) where.orderCreatedAt.gte = dateFrom; if (dateTo) where.orderCreatedAt.lte = dateTo + " 23:59"; } const allOrders = await prisma.shopeeOrder.findMany({ where, select: { orderId: true, shop: true, status: true, orderCreatedAt: true, totalAmount: true, buyerPaidAmount: true, commission: true, shippingFee: true, items: { select: { quantity: true, totalPayment: 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, commission: 0, shippingFee: 0, items: 0 }); } const s = shopMap.get(o.shop); s.orders += 1; s.revenue += o.totalAmount; s.commission += o.commission; s.shippingFee += o.shippingFee; s.items += o.items.reduce((acc, i) => acc + i.quantity, 0); if (o.status.includes("ยกเลิก")) s.cancelled += 1; else s.completed += 1; } const byShop = Array.from(shopMap.values()).map(s => ({ ...s, net: s.revenue - s.commission - s.shippingFee, cancelRate: s.orders > 0 ? (s.cancelled / s.orders) * 100 : 0, aov: s.completed > 0 ? s.revenue / s.completed : 0, })).sort((a, b) => b.revenue - a.revenue); // Monthly stacked by shop const monthShopMap = new Map(); for (const o of allOrders) { const month = (o.orderCreatedAt ?? "").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, commission: 0 }); const ms = sm.get(o.shop); ms.revenue += o.totalAmount; ms.orders += 1; ms.commission += o.commission; } const shops = Array.from(shopMap.keys()); const monthly = Array.from(monthShopMap.entries()) .map(([month, sm]) => { const row = { month }; for (const shop of shops) { const d = sm.get(shop) ?? { revenue: 0, orders: 0, commission: 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); return row; }) .sort((a, b) => a.month.localeCompare(b.month)); // Top products across all shops (filtered by date range via orderId) const filteredOrderIds = allOrders.map(o => o.orderId); const allItems = filteredOrderIds.length > 0 ? await prisma.shopeeOrderItem.findMany({ where: { orderId: { in: filteredOrderIds } }, select: { productName: true, variantName: true, quantity: true, totalPayment: true, order: { select: { shop: true } } }, }) : []; const productMap = new Map(); for (const item of allItems) { const key = `${item.productName}__${item.variantName ?? ""}`; if (!productMap.has(key)) productMap.set(key, { name: item.productName, variant: item.variantName ?? "", qty: 0, revenue: 0 }); const p = productMap.get(key); p.qty += item.quantity; p.revenue += item.totalPayment; } const topProducts = Array.from(productMap.values()).sort((a, b) => b.revenue - a.revenue).slice(0, 10); // Grand totals const totalRevenue = allOrders.reduce((s, o) => s + o.totalAmount, 0); const totalCommission = allOrders.reduce((s, o) => s + o.commission, 0); const totalShipping = allOrders.reduce((s, o) => s + o.shippingFee, 0); const totalOrders = allOrders.length; const totalCancelled = allOrders.filter(o => o.status.includes("ยกเลิก")).length; return { byShop, monthly, topProducts, shops, totals: { totalRevenue, totalCommission, totalShipping, netRevenue: totalRevenue - totalCommission - totalShipping, totalOrders, totalCancelled, cancelRate: totalOrders > 0 ? (totalCancelled / totalOrders) * 100 : 0, aov: (totalOrders - totalCancelled) > 0 ? totalRevenue / (totalOrders - totalCancelled) : 0, commissionRate: totalRevenue > 0 ? (totalCommission / totalRevenue) * 100 : 0, }, }; }) // ── GET Summary ─────────────────────────────────────────────────────────── .get("/summary", async ({ query }) => { const shop = query.shop || "hom-aroma"; const where = { shop }; const [orderCount, agg, itemAgg] = await Promise.all([ prisma.shopeeOrder.count({ where }), prisma.shopeeOrder.aggregate({ where, _sum: { totalAmount: true, buyerPaidAmount: true, commission: true, shippingFee: true } }), prisma.shopeeOrderItem.aggregate({ where: { order: where }, _sum: { quantity: true, totalPayment: true } }), ]); return { orderCount, totalAmount: agg._sum.totalAmount ?? 0, buyerPaidAmount: agg._sum.buyerPaidAmount ?? 0, totalCommission: agg._sum.commission ?? 0, totalShipping: agg._sum.shippingFee ?? 0, totalItems: itemAgg._sum.quantity ?? 0, totalItemRevenue: itemAgg._sum.totalPayment ?? 0, }; }); //# sourceMappingURL=shopee.js.map