diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index 3b6f09c..1547529 100644 --- a/apps/backend/prisma/schema.prisma +++ b/apps/backend/prisma/schema.prisma @@ -82,6 +82,7 @@ model Customer { pictureUrl String? email String? phone String? + points Int @default(0) orders Order[] reviews Review[] createdAt DateTime @default(now()) @@ -116,12 +117,27 @@ model Coupon { updatedAt DateTime @updatedAt } +model Promotion { + id Int @id @default(autoincrement()) + name String + level Int + type String + value Float + minPurchase Float @default(0) + isActive Boolean @default(true) + startDate DateTime? + endDate DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + model StoreSettings { id Int @id @default(1) shippingFee Float @default(35) bankName String @default("KASIKORNBANK (KBANK)") bankAccountName String @default("HomAroma Co., Ltd.") bankAccountNumber String @default("012-3-45678-9") + pointsEarnRate Float @default(1.0) updatedAt DateTime @updatedAt } @@ -131,10 +147,22 @@ model Order { customerEmail String customerPhone String shippingAddress String - totalAmount Float - shippingFee Float @default(35) + + subtotal Float @default(0) + promoLevel1Name String? + promoLevel1Disc Float @default(0) + promoLevel2Name String? + promoLevel2Disc Float @default(0) + couponCode String? discountAmount Float @default(0) + + pointsUsed Int @default(0) + pointsDiscount Float @default(0) + pointsEarned Int @default(0) + + shippingFee Float @default(35) + totalAmount Float status String @default("PENDING_PAYMENT") paymentSlipUrl String? diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index bfb9a14..e254f01 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -19,6 +19,7 @@ import { lazadaOrderRoute } from "./routes/lazadaOrder"; import { customersRoute } from "./routes/customers"; import { couponsRoute } from "./routes/coupons"; import { reviewsRoute } from "./routes/reviews"; +import { promotionsRoute } from "./routes/promotions"; 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"; @@ -82,7 +83,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) + 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) ) .listen(PORT); diff --git a/apps/backend/src/routes/orders.ts b/apps/backend/src/routes/orders.ts index b72981e..ca89de4 100644 --- a/apps/backend/src/routes/orders.ts +++ b/apps/backend/src/routes/orders.ts @@ -22,7 +22,25 @@ export const ordersRoute = new Elysia({ prefix: "/orders" }) .post("/", async ({ body, set }) => { try { const data = body as any; - + const settings = await prisma.storeSettings.findFirst() + const earnRate = settings?.pointsEarnRate || 1.0 + const netAmount = data.totalAmount - data.shippingFee + const pointsEarned = Math.floor(Math.max(0, netAmount) * earnRate) + + if (data.customerId && data.pointsUsed && data.pointsUsed > 0) { + // Verify customer has enough points + const customer = await prisma.customer.findUnique({ where: { id: data.customerId } }) + if (!customer || customer.points < data.pointsUsed) { + set.status = 400 + return { error: "Insufficient points" } + } + // Deduct points immediately + await prisma.customer.update({ + where: { id: data.customerId }, + data: { points: { decrement: data.pointsUsed } } + }) + } + const order = await prisma.order.create({ data: { customerId: data.customerId, @@ -30,10 +48,18 @@ export const ordersRoute = new Elysia({ prefix: "/orders" }) customerEmail: data.customerEmail, customerPhone: data.customerPhone, shippingAddress: data.shippingAddress, - totalAmount: data.totalAmount, - shippingFee: data.shippingFee, + subtotal: data.subtotal || 0, + promoLevel1Name: data.promoLevel1Name, + promoLevel1Disc: data.promoLevel1Disc || 0, + promoLevel2Name: data.promoLevel2Name, + promoLevel2Disc: data.promoLevel2Disc || 0, couponCode: data.couponCode || null, discountAmount: data.discountAmount || 0, + pointsUsed: data.pointsUsed || 0, + pointsDiscount: data.pointsDiscount || 0, + pointsEarned: pointsEarned, + shippingFee: data.shippingFee, + totalAmount: data.totalAmount, items: { create: data.items.map((item: any) => ({ productId: item.productId, @@ -76,8 +102,15 @@ export const ordersRoute = new Elysia({ prefix: "/orders" }) shippingAddress: t.String(), totalAmount: t.Number(), shippingFee: t.Number(), + subtotal: t.Optional(t.Number()), + promoLevel1Name: t.Optional(t.Nullable(t.String())), + promoLevel1Disc: t.Optional(t.Number()), + promoLevel2Name: t.Optional(t.Nullable(t.String())), + promoLevel2Disc: t.Optional(t.Number()), couponCode: t.Optional(t.Nullable(t.String())), discountAmount: t.Optional(t.Number()), + pointsUsed: t.Optional(t.Number()), + pointsDiscount: t.Optional(t.Number()), items: t.Array(t.Object({ productId: t.Number(), variantId: t.Optional(t.Number()), @@ -92,11 +125,31 @@ export const ordersRoute = new Elysia({ prefix: "/orders" }) .patch("/:id/status", async ({ params: { id }, body, set }) => { try { const { status } = body as any; - return await prisma.order.update({ + const order = await prisma.order.findUnique({ where: { id: Number(id) } }); + + const updatedOrder = await prisma.order.update({ where: { id: Number(id) }, data: { status }, include: { items: true } }); + + // Grant points if status becomes PAID and it wasn't PAID before + if (status === 'PAID' && order?.status !== 'PAID' && order?.customerId && order?.pointsEarned > 0) { + await prisma.customer.update({ + where: { id: order.customerId }, + data: { points: { increment: order.pointsEarned } } + }); + } + + // If status becomes CANCELLED, refund the points used + if (status === 'CANCELLED' && order?.status !== 'CANCELLED' && order?.customerId && order?.pointsUsed > 0) { + await prisma.customer.update({ + where: { id: order.customerId }, + data: { points: { increment: order.pointsUsed } } + }); + } + + return updatedOrder; } catch (err) { set.status = 500; return { error: "Failed to update order status" }; diff --git a/apps/backend/src/routes/promotions.ts b/apps/backend/src/routes/promotions.ts new file mode 100644 index 0000000..bd27540 --- /dev/null +++ b/apps/backend/src/routes/promotions.ts @@ -0,0 +1,20 @@ +import { Elysia, t } from "elysia"; +import prisma from "../lib/prisma"; + +export const promotionsRoute = new Elysia({ prefix: "/promotions" }) + .get("/active", async () => { + const now = new Date() + return await prisma.promotion.findMany({ + where: { + isActive: true, + OR: [ + { startDate: null }, + { startDate: { lte: now } } + ], + AND: [ + { OR: [{ endDate: null }, { endDate: { gte: now } }] } + ] + }, + orderBy: { level: 'asc' } + }); + });