feat: implement user profile, order history, and address book with checkout integration

This commit is contained in:
Your Name committed 2026-06-07 18:48:56 +07:00
1 parent bae945b9d4
commit 0c3c450e1f
9 files changed
+1109 -32

No files matched your search

@@ -0,0 +1,20 @@
-- CreateTable
CREATE TABLE "CustomerAddress" (
"id" SERIAL NOT NULL,
"customerId" INTEGER NOT NULL,
"recipientName" TEXT NOT NULL,
"phone" TEXT NOT NULL,
"addressLine" TEXT NOT NULL,
"subdistrict" TEXT NOT NULL,
"district" TEXT NOT NULL,
"province" TEXT NOT NULL,
"zipcode" TEXT NOT NULL,
"isDefault" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "CustomerAddress_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "CustomerAddress" ADD CONSTRAINT "CustomerAddress_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+22 -5
View File
@@ -76,17 +76,34 @@ model User {
}
model Customer {
id Int @id @default(autoincrement())
lineUserId String @unique
id Int @id @default(autoincrement())
lineUserId String @unique
displayName String?
pictureUrl String?
email String?
phone String?
points Int @default(0)
points Int @default(0)
orders Order[]
reviews Review[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
addresses CustomerAddress[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model CustomerAddress {
id Int @id @default(autoincrement())
customerId Int
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
recipientName String
phone String
addressLine String // บ้านเลขที่ หมู่บ้าน ถนน ซอย
subdistrict String // แขวง/ตำบล
district String // เขต/อำเภอ
province String // จังหวัด
zipcode String // รหัสไปรษณีย์
isDefault Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Hero {
+2 -1
View File
@@ -22,6 +22,7 @@ import { reviewsRoute } from "./routes/reviews";
import { promotionsRoute } from "./routes/promotions";
import { pagesRoute } from "./routes/pages";
import { paymentsRoute } from "./routes/payments";
import { profileRoute } from "./routes/profile";
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";
@@ -85,7 +86,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).use(paymentsRoute)
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).use(profileRoute)
)
.listen(PORT);
+162
View File
@@ -0,0 +1,162 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
export const profileRoute = new Elysia({ prefix: "/profile" })
.get("/:customerId", async ({ params: { customerId }, set }) => {
const customer = await prisma.customer.findUnique({
where: { id: Number(customerId) },
include: { addresses: true }
});
if (!customer) {
set.status = 404;
return { error: "Customer not found" };
}
return customer;
})
.put("/:customerId", async ({ params: { customerId }, body, set }) => {
try {
const { email, phone } = body as any;
const customer = await prisma.customer.update({
where: { id: Number(customerId) },
data: {
email: email || null,
phone: phone || null
}
});
return customer;
} catch (err: any) {
set.status = 500;
return { error: err.message || "Failed to update customer profile" };
}
}, {
body: t.Object({
email: t.Optional(t.Nullable(t.String())),
phone: t.Optional(t.Nullable(t.String()))
})
})
.get("/:customerId/addresses", async ({ params: { customerId } }) => {
return await prisma.customerAddress.findMany({
where: { customerId: Number(customerId) },
orderBy: { isDefault: 'desc' }
});
})
.post("/:customerId/addresses", async ({ params: { customerId }, body, set }) => {
try {
const { recipientName, phone, addressLine, subdistrict, district, province, zipcode, isDefault } = body as any;
const cId = Number(customerId);
// If isDefault is true, unset other defaults first
if (isDefault) {
await prisma.customerAddress.updateMany({
where: { customerId: cId },
data: { isDefault: false }
});
}
const address = await prisma.customerAddress.create({
data: {
customerId: cId,
recipientName,
phone,
addressLine,
subdistrict,
district,
province,
zipcode,
isDefault: !!isDefault
}
});
return address;
} catch (err: any) {
set.status = 500;
return { error: err.message || "Failed to create address" };
}
}, {
body: t.Object({
recipientName: t.String(),
phone: t.String(),
addressLine: t.String(),
subdistrict: t.String(),
district: t.String(),
province: t.String(),
zipcode: t.String(),
isDefault: t.Boolean()
})
})
.put("/:customerId/addresses/:addressId", async ({ params: { customerId, addressId }, body, set }) => {
try {
const { recipientName, phone, addressLine, subdistrict, district, province, zipcode, isDefault } = body as any;
const cId = Number(customerId);
const aId = Number(addressId);
// If setting as default, unset other defaults first
if (isDefault) {
await prisma.customerAddress.updateMany({
where: { customerId: cId },
data: { isDefault: false }
});
}
const address = await prisma.customerAddress.update({
where: { id: aId },
data: {
recipientName,
phone,
addressLine,
subdistrict,
district,
province,
zipcode,
isDefault: !!isDefault
}
});
return address;
} catch (err: any) {
set.status = 500;
return { error: err.message || "Failed to update address" };
}
}, {
body: t.Object({
recipientName: t.String(),
phone: t.String(),
addressLine: t.String(),
subdistrict: t.String(),
district: t.String(),
province: t.String(),
zipcode: t.String(),
isDefault: t.Boolean()
})
})
.delete("/:customerId/addresses/:addressId", async ({ params: { addressId }, set }) => {
try {
await prisma.customerAddress.delete({
where: { id: Number(addressId) }
});
return { success: true };
} catch (err: any) {
set.status = 500;
return { error: err.message || "Failed to delete address" };
}
})
.patch("/:customerId/addresses/:addressId/default", async ({ params: { customerId, addressId }, set }) => {
try {
const cId = Number(customerId);
const aId = Number(addressId);
// Unset other defaults
await prisma.customerAddress.updateMany({
where: { customerId: cId },
data: { isDefault: false }
});
// Set this one as default
const address = await prisma.customerAddress.update({
where: { id: aId },
data: { isDefault: true }
});
return address;
} catch (err: any) {
set.status = 500;
return { error: err.message || "Failed to set default address" };
}
});