fix: generate missing migration for Menu and Newsletter tables

This commit is contained in:
Your Name committed 2026-06-14 00:16:05 -04:00
1 parent 363be70e11
commit f5029fc935
123 files changed
+8683

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
import "dotenv/config";
declare const _default: import("prisma/config").PrismaConfigInternal;
export default _default;
//# sourceMappingURL=prisma.config.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"prisma.config.d.ts","sourceRoot":"","sources":["prisma.config.ts"],"names":[],"mappings":"AAGA,OAAO,eAAe,CAAC;;AAGvB,wBASG"}
+16
View File
@@ -0,0 +1,16 @@
// Prisma 7 Config
// Note: prisma CLI uses jiti (Node.js) to load this file, NOT Bun
// So we must import dotenv explicitly here to load DATABASE_URL
import "dotenv/config";
import { defineConfig } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
seed: "bun prisma/seed.cjs",
},
datasource: {
url: process.env["DATABASE_URL"],
},
});
//# sourceMappingURL=prisma.config.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"prisma.config.js","sourceRoot":"","sources":["prisma.config.ts"],"names":[],"mappings":"AAAA,kBAAkB;AAClB,kEAAkE;AAClE,gEAAgE;AAChE,OAAO,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE7C,eAAe,YAAY,CAAC;IAC1B,MAAM,EAAE,sBAAsB;IAC9B,UAAU,EAAE;QACV,IAAI,EAAE,mBAAmB;QACzB,IAAI,EAAE,qBAAqB;KAC5B;IACD,UAAU,EAAE;QACV,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;KACjC;CACF,CAAC,CAAC"}
@@ -0,0 +1,54 @@
-- AlterTable
ALTER TABLE "Product" ALTER COLUMN "name" DROP NOT NULL;
-- AlterTable
ALTER TABLE "StoreSettings" ADD COLUMN "facebookUrl" TEXT,
ADD COLUMN "footerDescription" TEXT,
ADD COLUMN "instagramUrl" TEXT,
ADD COLUMN "newsletterButtonText" TEXT,
ADD COLUMN "newsletterDescription" TEXT,
ADD COLUMN "newsletterPlaceholder" TEXT,
ADD COLUMN "newsletterTitle" TEXT,
ADD COLUMN "tiktokUrl" TEXT;
-- CreateTable
CREATE TABLE "NewsletterSubscriber" (
"id" SERIAL NOT NULL,
"email" TEXT NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "NewsletterSubscriber_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Menu" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Menu_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "MenuItem" (
"id" SERIAL NOT NULL,
"menuId" INTEGER NOT NULL,
"label" TEXT NOT NULL,
"url" TEXT NOT NULL,
"order" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "MenuItem_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "NewsletterSubscriber_email_key" ON "NewsletterSubscriber"("email");
-- CreateIndex
CREATE UNIQUE INDEX "Menu_name_key" ON "Menu"("name");
-- AddForeignKey
ALTER TABLE "MenuItem" ADD CONSTRAINT "MenuItem_menuId_fkey" FOREIGN KEY ("menuId") REFERENCES "Menu"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+3
View File
@@ -44,6 +44,9 @@ model Product {
price Float? @default(0) price Float? @default(0)
categoryId Int categoryId Int
category Category @relation(fields: [categoryId], references: [id]) category Category @relation(fields: [categoryId], references: [id])
name String?
description String?
details String?
scentId Int scentId Int
scent Scent @relation(fields: [scentId], references: [id]) scent Scent @relation(fields: [scentId], references: [id])
variants ProductVariant[] variants ProductVariant[]
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=seed-pages.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"seed-pages.d.ts","sourceRoot":"","sources":["seed-pages.ts"],"names":[],"mappings":""}
+662
View File
@@ -0,0 +1,662 @@
import { PrismaClient } from '@prisma/client';
import { Pool } from 'pg';
import { PrismaPg } from '@prisma/adapter-pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const adapter = new PrismaPg(pool);
const prisma = new PrismaClient({ adapter });
async function main() {
console.log("🌱 Seeding Dynamic Page Builder...");
// 1. Create the HomePage
const page = await prisma.page.upsert({
where: { slug: "home" },
update: {},
create: {
slug: "home",
title: "Home Page",
isPublished: true,
}
});
console.log(`✅ Created page: ${page.title} (slug: ${page.slug})`);
// Clear existing sections for home page so we don't duplicate
await prisma.pageSection.deleteMany({
where: { pageId: page.id }
});
// 2. Create default sections: HERO, CATEGORIES, PRODUCTS, BANNER, TEXT
// Section 1: Hero
await prisma.pageSection.create({
data: {
pageId: page.id,
type: "HERO",
order: 1,
isActive: true,
payload: {
tagline: "VIBRANT ELEGANCE COLLECTION",
title: "Inhale [Serenity]",
description: "Crafting premium sensory experiences that transform your space into a sanctuary of peace.",
imageUrl: "/images/hero.png",
buttonText: "Explore Collection",
buttonLink: "/shop"
}
}
});
// Section 2: Categories
await prisma.pageSection.create({
data: {
pageId: page.id,
type: "CATEGORIES",
order: 2,
isActive: true,
payload: {
title: "Our Collections",
subtitle: "Discover the perfect scent for every moment"
}
}
});
// Section 3: Products (Featured Picks)
await prisma.pageSection.create({
data: {
pageId: page.id,
type: "PRODUCTS",
order: 3,
isActive: true,
payload: {
title: "Featured Picks",
subtitle: "Hand-picked fragrances recommended just for you",
type: "featured",
limit: 3
}
}
});
// Section 4: Banner
await prisma.pageSection.create({
data: {
pageId: page.id,
type: "BANNER",
order: 4,
isActive: true,
payload: {
imageUrl: "/images/oil_ozone.png",
linkUrl: "/category/1",
title: "Special Offer",
description: "Get 10% off on your first purchase of Oil Ozone collection",
buttonText: "Shop Now"
}
}
});
// Section 5: Text info
await prisma.pageSection.create({
data: {
pageId: page.id,
type: "TEXT",
order: 5,
isActive: true,
payload: {
title: "About HomAroma",
content: "HomAroma is dedicated to sourcing the finest raw materials to formulate high-quality essential oils, reed diffusers, and room sprays. Our goal is to bring nature's purity directly into your life.",
textAlign: "center"
}
}
});
console.log("✅ Seeded 5 default home page sections successfully!");
// 3. Create the Shop Page Layout
const shopPage = await prisma.page.upsert({
where: { slug: "shop-layout" },
update: {},
create: {
slug: "shop-layout",
title: "Shop Page Layout",
isPublished: true,
}
});
console.log(`✅ Created page: ${shopPage.title} (slug: ${shopPage.slug})`);
// Clear existing sections for shop page so we don't duplicate
await prisma.pageSection.deleteMany({
where: { pageId: shopPage.id }
});
// Shop Section 1: Announcement Bar
await prisma.pageSection.create({
data: {
pageId: shopPage.id,
type: "ANNOUNCEMENT_BAR",
order: 1,
isActive: true,
payload: {
slides: ["[FREE SHIPPING] on orders over ฿1,500", "New Premium Collection is out now!"],
backgroundColor: "#1F3D2D",
textColor: "#FDFDF9",
speed: 3000
}
}
});
// Shop Section 2: Hero Banner (Set to active by default to showcase the header capabilities)
await prisma.pageSection.create({
data: {
pageId: shopPage.id,
type: "HERO",
order: 2,
isActive: true,
payload: {
tagline: "CURATED BOTANICALS",
title: "The [Shop] Collection",
description: "Transform your daily space into a luxury escape with our natural aroma range.",
imageUrl: "/images/hero.png",
buttonText: "Learn More",
buttonLink: "/about"
}
}
});
// Shop Section 3: Editorial Tile
await prisma.pageSection.create({
data: {
pageId: shopPage.id,
type: "EDITORIAL_TILE",
order: 3,
isActive: true,
payload: {
title: "Crafted with [Sensory Passion]",
description: "Every formulation at HomAroma is a tribute to pure botanical essences, hand-blended to capture nature’s quiet intelligence.",
imageUrl: "/images/oil_ozone.png",
imagePosition: "right",
buttonText: "Read Our Story",
buttonLink: "/about",
backgroundColor: "#FDFDF9",
textColor: "#1F3D2D"
}
}
});
// Shop Section 4: Newsletter
await prisma.pageSection.create({
data: {
pageId: shopPage.id,
type: "NEWSLETTER",
order: 4,
isActive: true,
payload: {
title: "Begin Your Wellness Journey",
description: "Subscribe to receive 10% off your first purchase and exclusive updates.",
buttonText: "Subscribe",
placeholderText: "Enter your email address",
showPrivacyCheckbox: true,
privacyText: "I agree to the privacy policy."
}
}
});
console.log("✅ Seeded default shop-layout page sections successfully!");
// 4. Create the Scent Guide Page Layout
const scentGuidePage = await prisma.page.upsert({
where: { slug: "scent-guide-layout" },
update: {},
create: {
slug: "scent-guide-layout",
title: "Scent Guide Layout",
isPublished: true,
}
});
console.log(`✅ Created page: ${scentGuidePage.title} (slug: ${scentGuidePage.slug})`);
// Clear existing sections for scent guide page so we don't duplicate
await prisma.pageSection.deleteMany({
where: { pageId: scentGuidePage.id }
});
// Scent Guide Section 1: Announcement Bar
await prisma.pageSection.create({
data: {
pageId: scentGuidePage.id,
type: "ANNOUNCEMENT_BAR",
order: 1,
isActive: true,
payload: {
slides: ["Explore our Scent Guide to find your signature fragrance", "Read wellness articles crafted by experts"],
backgroundColor: "#1F3D2D",
textColor: "#FDFDF9",
speed: 3000
}
}
});
// Scent Guide Section 2: Hero Banner
await prisma.pageSection.create({
data: {
pageId: scentGuidePage.id,
type: "HERO",
order: 2,
isActive: true,
payload: {
tagline: "JOURNAL & INSIGHTS",
title: "The [Scent] Guide",
description: "Welcome to the sensory archive of HomAroma. Explore the art and science of botanical aromatherapy.",
imageUrl: "/images/hero.png",
buttonText: "Read Story",
buttonLink: "/about"
}
}
});
// Scent Guide Section 3: Newsletter
await prisma.pageSection.create({
data: {
pageId: scentGuidePage.id,
type: "NEWSLETTER",
order: 3,
isActive: true,
payload: {
title: "Stay Inspired",
description: "Subscribe to our journal and get regular wellness guides, blending tips, and exclusive offers.",
buttonText: "Subscribe",
placeholderText: "Enter your email address",
showPrivacyCheckbox: true,
privacyText: "I agree to the privacy policy."
}
}
});
console.log("✅ Seeded default scent-guide-layout page sections successfully!");
// 5. Create Category Page Layout
const categoryPage = await prisma.page.upsert({
where: { slug: "category-layout" },
update: {},
create: {
slug: "category-layout",
title: "Category Page Layout",
isPublished: true,
}
});
console.log(`✅ Created page: ${categoryPage.title} (slug: ${categoryPage.slug})`);
await prisma.pageSection.deleteMany({
where: { pageId: categoryPage.id }
});
// Announcement Bar for Categories
await prisma.pageSection.create({
data: {
pageId: categoryPage.id,
type: "ANNOUNCEMENT_BAR",
order: 1,
isActive: true,
payload: {
slides: ["Explore our specialized collections for your sanctuary", "Free premium shipping on orders over ฿1,500"],
backgroundColor: "#1F3D2D",
textColor: "#FDFDF9",
speed: 3000
}
}
});
// Newsletter at the bottom of Categories
await prisma.pageSection.create({
data: {
pageId: categoryPage.id,
type: "NEWSLETTER",
order: 2,
isActive: true,
payload: {
title: "Join [HomAroma] Circle",
description: "Subscribe to receive announcements of new single-origin botanical releases.",
buttonText: "Subscribe",
placeholderText: "Enter your email address",
showPrivacyCheckbox: true,
privacyText: "I agree to the privacy policy."
}
}
});
// 6. Create Product Detail Page Layout
const productDetailPage = await prisma.page.upsert({
where: { slug: "product-detail-layout" },
update: {},
create: {
slug: "product-detail-layout",
title: "Product Detail Layout",
isPublished: true,
}
});
console.log(`✅ Created page: ${productDetailPage.title} (slug: ${productDetailPage.slug})`);
await prisma.pageSection.deleteMany({
where: { pageId: productDetailPage.id }
});
// Announcement Bar for Product Details
await prisma.pageSection.create({
data: {
pageId: productDetailPage.id,
type: "ANNOUNCEMENT_BAR",
order: 1,
isActive: true,
payload: {
slides: ["Special Offer: Get 10% off your first purchase of single-origin oils", "Hand-formulated with pure botanical extracts"],
backgroundColor: "#1F3D2D",
textColor: "#FDFDF9",
speed: 3000
}
}
});
// Editorial banner at the bottom of Product Details
await prisma.pageSection.create({
data: {
pageId: productDetailPage.id,
type: "EDITORIAL_TILE",
order: 2,
isActive: true,
payload: {
title: "The [Botanical] Sourcing",
description: "All our raw materials are sustainably harvested at peak potency from verified organic micro-farms worldwide.",
imageUrl: "/images/oil_ozone.png",
imagePosition: "left",
buttonText: "Sourcing Map",
buttonLink: "/about",
backgroundColor: "#FDFDF9",
textColor: "#1F3D2D"
}
}
});
// 7. Create FAQ Page Layout
const faqPage = await prisma.page.upsert({
where: { slug: "faq-layout" },
update: {},
create: {
slug: "faq-layout",
title: "FAQ Page Layout",
isPublished: true,
}
});
console.log(`✅ Created page: ${faqPage.title} (slug: ${faqPage.slug})`);
await prisma.pageSection.deleteMany({
where: { pageId: faqPage.id }
});
// Announcement Bar for FAQ
await prisma.pageSection.create({
data: {
pageId: faqPage.id,
type: "ANNOUNCEMENT_BAR",
order: 1,
isActive: true,
payload: {
slides: ["Need instant help? Reach out on LINE @homaroma", "FAQ updated for Summer 2026 delivery guidelines"],
backgroundColor: "#1F3D2D",
textColor: "#FDFDF9",
speed: 4000
}
}
});
// Newsletter at the bottom of FAQ
await prisma.pageSection.create({
data: {
pageId: faqPage.id,
type: "NEWSLETTER",
order: 2,
isActive: true,
payload: {
title: "Stay informed with [HomAroma]",
description: "Sign up to receive shipping updates, care guides, and exclusive collection invites.",
buttonText: "Subscribe",
placeholderText: "Enter your email address",
showPrivacyCheckbox: true,
privacyText: "I agree to the privacy policy."
}
}
});
console.log("✅ Seeded default category, product-detail, and FAQ layout sections successfully!");
// 8. Create Wishlist Page Layout
const wishlistPage = await prisma.page.upsert({
where: { slug: "wishlist-layout" },
update: {},
create: {
slug: "wishlist-layout",
title: "Wishlist Page Layout",
isPublished: true,
}
});
console.log(`✅ Created page: ${wishlistPage.title} (slug: ${wishlistPage.slug})`);
await prisma.pageSection.deleteMany({ where: { pageId: wishlistPage.id } });
await prisma.pageSection.create({
data: {
pageId: wishlistPage.id,
type: "ANNOUNCEMENT_BAR",
order: 1,
isActive: true,
payload: {
slides: ["Sign up to HomAroma Circle to get 10% off your wishlist items", "Hand-crafted botanicals shipped directly to you"],
backgroundColor: "#1F3D2D",
textColor: "#FDFDF9",
speed: 3000
}
}
});
await prisma.pageSection.create({
data: {
pageId: wishlistPage.id,
type: "NEWSLETTER",
order: 2,
isActive: true,
payload: {
title: "Don't lose your favorites",
description: "Subscribe to receive price drops, back-in-stock alerts, and exclusive discounts on items in your wishlist.",
buttonText: "Subscribe",
placeholderText: "Enter your email address",
showPrivacyCheckbox: true,
privacyText: "I agree to the privacy policy."
}
}
});
// 9. Create Search Page Layout
const searchPage = await prisma.page.upsert({
where: { slug: "search-layout" },
update: {},
create: {
slug: "search-layout",
title: "Search Page Layout",
isPublished: true,
}
});
console.log(`✅ Created page: ${searchPage.title} (slug: ${searchPage.slug})`);
await prisma.pageSection.deleteMany({ where: { pageId: searchPage.id } });
await prisma.pageSection.create({
data: {
pageId: searchPage.id,
type: "ANNOUNCEMENT_BAR",
order: 1,
isActive: true,
payload: {
slides: ["Need help finding something? Live Chat with our scent experts on LINE @homaroma"],
backgroundColor: "#1F3D2D",
textColor: "#FDFDF9",
speed: 3000
}
}
});
await prisma.pageSection.create({
data: {
pageId: searchPage.id,
type: "EDITORIAL_TILE",
order: 2,
isActive: true,
payload: {
title: "Find Your [Signature Scent]",
description: "Aromatherapy is the art of tuning your environment. Discover our curation of room sprays, reed diffusers, and essential oils designed for modern sanctuaries.",
imageUrl: "/images/hero.png",
imagePosition: "right",
buttonText: "Explore Guide",
buttonLink: "/scent-guide",
backgroundColor: "#FDFDF9",
textColor: "#1F3D2D"
}
}
});
// 10. Create Coupons Page Layout
const couponsPage = await prisma.page.upsert({
where: { slug: "coupons-layout" },
update: {},
create: {
slug: "coupons-layout",
title: "Coupons Page Layout",
isPublished: true,
}
});
console.log(`✅ Created page: ${couponsPage.title} (slug: ${couponsPage.slug})`);
await prisma.pageSection.deleteMany({ where: { pageId: couponsPage.id } });
await prisma.pageSection.create({
data: {
pageId: couponsPage.id,
type: "ANNOUNCEMENT_BAR",
order: 1,
isActive: true,
payload: {
slides: ["Enjoy limited-time discount codes for your sensory sanctuary"],
backgroundColor: "#1F3D2D",
textColor: "#FDFDF9",
speed: 3000
}
}
});
await prisma.pageSection.create({
data: {
pageId: couponsPage.id,
type: "NEWSLETTER",
order: 2,
isActive: true,
payload: {
title: "Get Exclusive Secret Codes",
description: "Become a subscriber to get weekly private discount codes and early access to new collections.",
buttonText: "Subscribe",
placeholderText: "Enter your email address",
showPrivacyCheckbox: true,
privacyText: "I agree to the privacy policy."
}
}
});
// 11. Create Contact Page Layout
const contactPage = await prisma.page.upsert({
where: { slug: "contact-layout" },
update: {},
create: {
slug: "contact-layout",
title: "Contact Page Layout",
isPublished: true,
}
});
console.log(`✅ Created page: ${contactPage.title} (slug: ${contactPage.slug})`);
await prisma.pageSection.deleteMany({ where: { pageId: contactPage.id } });
await prisma.pageSection.create({
data: {
pageId: contactPage.id,
type: "ANNOUNCEMENT_BAR",
order: 1,
isActive: true,
payload: {
slides: ["Customer support is available Mon-Sat, 9:00 - 18:00 on LINE @homaroma"],
backgroundColor: "#1F3D2D",
textColor: "#FDFDF9",
speed: 4000
}
}
});
await prisma.pageSection.create({
data: {
pageId: contactPage.id,
type: "EDITORIAL_TILE",
order: 2,
isActive: true,
payload: {
title: "Our [Bangkok Studio]",
description: "Visit our flagship boutique to experience custom blending sessions and smell our full botanical library in person.",
imageUrl: "/images/hero.png",
imagePosition: "left",
buttonText: "Book Session",
buttonLink: "/about",
backgroundColor: "#FDFDF9",
textColor: "#1F3D2D"
}
}
});
// 12. Create Order Success Page Layout
const orderSuccessPage = await prisma.page.upsert({
where: { slug: "order-success-layout" },
update: {},
create: {
slug: "order-success-layout",
title: "Order Success Page Layout",
isPublished: true,
}
});
console.log(`✅ Created page: ${orderSuccessPage.title} (slug: ${orderSuccessPage.slug})`);
await prisma.pageSection.deleteMany({ where: { pageId: orderSuccessPage.id } });
await prisma.pageSection.create({
data: {
pageId: orderSuccessPage.id,
type: "ANNOUNCEMENT_BAR",
order: 1,
isActive: true,
payload: {
slides: ["Thank you for shopping with HomAroma! Your package is preparing.", "Your order tracking status will be updated here."],
backgroundColor: "#1F3D2D",
textColor: "#FDFDF9",
speed: 3000
}
}
});
await prisma.pageSection.create({
data: {
pageId: orderSuccessPage.id,
type: "EDITORIAL_TILE",
order: 2,
isActive: true,
payload: {
title: "Crafting Your [Sanctuary]",
description: "Every order is carefully packaged by hand using sustainable, biodegradable materials. We ensure your botanicals arrive fresh and ready to enrich your space.",
imageUrl: "/images/oil_ozone.png",
imagePosition: "right",
buttonText: "Our Sustainability Commitments",
buttonLink: "/about",
backgroundColor: "#FDFDF9",
textColor: "#1F3D2D"
}
}
});
// 13. Create Track Order Page Layout
const trackOrderPage = await prisma.page.upsert({
where: { slug: "track-order-layout" },
update: {},
create: {
slug: "track-order-layout",
title: "Track Order Page Layout",
isPublished: true,
}
});
console.log(`✅ Created page: ${trackOrderPage.title} (slug: ${trackOrderPage.slug})`);
await prisma.pageSection.deleteMany({ where: { pageId: trackOrderPage.id } });
await prisma.pageSection.create({
data: {
pageId: trackOrderPage.id,
type: "ANNOUNCEMENT_BAR",
order: 1,
isActive: true,
payload: {
slides: ["Check shipping status live. For any delivery queries, reach LINE @homaroma"],
backgroundColor: "#1F3D2D",
textColor: "#FDFDF9",
speed: 3000
}
}
});
await prisma.pageSection.create({
data: {
pageId: trackOrderPage.id,
type: "EDITORIAL_TILE",
order: 2,
isActive: true,
payload: {
title: "Scenting Your [Daily Rituals]",
description: "While waiting for your order, explore how different botanical notes impact focus, sleep, and emotional balance in our Scent Guide.",
imageUrl: "/images/hero.png",
imagePosition: "left",
buttonText: "Read Scent Guide",
buttonLink: "/scent-guide",
backgroundColor: "#FDFDF9",
textColor: "#1F3D2D"
}
}
});
console.log("✅ Seeded default Category, Product Detail, FAQ, Wishlist, Search, Coupons, Contact, Order Success, and Track Order layouts successfully!");
}
main()
.catch((e) => {
console.error("❌ Error seeding page:", e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
await pool.end();
});
//# sourceMappingURL=seed-pages.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"seed-pages.js","sourceRoot":"","sources":["seed-pages.ts"],"names":[],"mappings":"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 truncated
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=migrate-i18n.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"migrate-i18n.d.ts","sourceRoot":"","sources":["migrate-i18n.ts"],"names":[],"mappings":""}
+41
View File
@@ -0,0 +1,41 @@
import prisma from '../src/lib/prisma';
async function main() {
console.log('Starting i18n migration...');
const products = await prisma.product.findMany();
console.log(`Found ${products.length} products to migrate.`);
for (const product of products) {
const existingTranslation = await prisma.productTranslation.findUnique({
where: {
productId_langCode: {
productId: product.id,
langCode: 'th',
},
},
});
if (!existingTranslation) {
await prisma.productTranslation.create({
data: {
productId: product.id,
langCode: 'th',
name: product.name,
description: product.description,
details: product.details,
},
});
console.log(`Created Thai translation for product ID: ${product.id}`);
}
else {
console.log(`Thai translation already exists for product ID: ${product.id}`);
}
}
console.log('Migration completed successfully.');
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
//# sourceMappingURL=migrate-i18n.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"migrate-i18n.js","sourceRoot":"","sources":["migrate-i18n.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,mBAAmB,CAAC;AAEvC,KAAK,UAAU,IAAI;IACjB,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;IAE1C,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;IACjD,OAAO,CAAC,GAAG,CAAC,SAAS,QAAQ,CAAC,MAAM,uBAAuB,CAAC,CAAC;IAE7D,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,mBAAmB,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,UAAU,CAAC;YACrE,KAAK,EAAE;gBACL,kBAAkB,EAAE;oBAClB,SAAS,EAAE,OAAO,CAAC,EAAE;oBACrB,QAAQ,EAAE,IAAI;iBACf;aACF;SACF,CAAC,CAAC;QAEH,IAAI,CAAC,mBAAmB,EAAE,CAAC;YACzB,MAAM,MAAM,CAAC,kBAAkB,CAAC,MAAM,CAAC;gBACrC,IAAI,EAAE;oBACJ,SAAS,EAAE,OAAO,CAAC,EAAE;oBACrB,QAAQ,EAAE,IAAI;oBACd,IAAI,EAAE,OAAO,CAAC,IAAI;oBAClB,WAAW,EAAE,OAAO,CAAC,WAAW;oBAChC,OAAO,EAAE,OAAO,CAAC,OAAO;iBACzB;aACF,CAAC,CAAC;YACH,OAAO,CAAC,GAAG,CAAC,4CAA4C,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC;QACxE,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,mDAAmD,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC;QAC/E,CAAC;IACH,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAC;AACnD,CAAC;AAED,IAAI,EAAE;KACH,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE;IACX,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACjB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC;KACD,OAAO,CAAC,KAAK,IAAI,EAAE;IAClB,MAAM,MAAM,CAAC,WAAW,EAAE,CAAC;AAC7B,CAAC,CAAC,CAAC"}
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=index.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":""}
+98
View File
@@ -0,0 +1,98 @@
import { Elysia } from "elysia";
import { cors } from "@elysiajs/cors";
import { swagger } from "@elysiajs/swagger";
import { jwt } from "@elysiajs/jwt";
import { staticPlugin } from "@elysiajs/static";
import { categoriesRoute } from "./routes/categories";
import { productsRoute } from "./routes/products";
import { authRoute } from "./routes/auth";
import { uploadRoute } from "./routes/upload";
import { heroRoute } from "./routes/hero";
import { ordersRoute } from "./routes/orders";
import { settingsRoute } from "./routes/settings";
import { articlesRoute } from "./routes/articles";
import { tiktokRoute } from "./routes/tiktok";
import { rawMaterialsRoute } from "./routes/raw-materials";
import { shopeeRoute } from "./routes/shopee";
import { tiktokOrderRoute } from "./routes/tiktokOrder";
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";
import { pagesRoute } from "./routes/pages";
import { paymentsRoute } from "./routes/payments";
import { profileRoute } from "./routes/profile";
import { lineRoutes } from "./routes/line";
import { menusRoute } from "./routes/menus";
import { newsletterRoute } from "./routes/newsletter";
const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3051;
const CORS_ORIGIN = process.env.CORS_ORIGIN || "http://localhost:5173";
const JWT_SECRET = process.env.JWT_SECRET || "homaroma_secret_2026_scent";
const app = new Elysia()
.onParse(async ({ request }) => {
console.log('[onParse] Incoming request URL:', request.url);
if (request.url.includes('/api/line/webhook')) {
const text = await request.text();
console.log('[onParse] Intercepted LINE webhook raw body length:', text.length);
return text;
}
})
.use(cors({
origin: [
...CORS_ORIGIN.split(",").map((o) => o.trim()),
"https://admin.homaroma.shop",
"https://spa.homaroma.shop", // LIFF (SPA)
"https://homaroma.shop", // Main Website
"http://localhost:5174", // Admin
"http://localhost:4173",
"http://localhost:3001", // SSR Next.js
"https://react5173.kitizone.com",
"https://elysia3051.kitizone.com"
],
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
credentials: true,
}))
.use(jwt({
name: "jwt",
secret: JWT_SECRET,
exp: "24h",
}))
.use(swagger({
documentation: {
info: {
title: "HomAroma API",
version: "1.0.0",
description: "API for HomAroma & Essential Oil Products",
},
tags: [
{ name: "categories", description: "Product categories" },
{ name: "products", description: "Products and variants" },
{ name: "auth", description: "Authentication for Admin" },
],
},
}))
.use(staticPlugin({
assets: "public",
prefix: "", // use empty string for root
}))
.get("/", () => ({
name: "HomAroma API",
version: "1.0.0",
status: "running",
docs: `/swagger`,
}))
// Explicit route for uploads to guarantee it works even if staticPlugin fails
.get("/uploads/:filename", ({ params: { filename }, set }) => {
const file = Bun.file(`public/uploads/${filename}`);
if (file.size === 0) {
set.status = 404;
return "File not found";
}
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).use(profileRoute).use(lineRoutes).use(menusRoute).use(newsletterRoute))
.listen({ port: PORT, hostname: "0.0.0.0" });
console.log(`🌿 HomAroma API is running at http://${app.server?.hostname}:${app.server?.port}`);
console.log(`📖 Swagger docs: http://${app.server?.hostname}:${app.server?.port}/swagger`);
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"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"}
+7
View File
@@ -0,0 +1,7 @@
import * as line from '@line/bot-sdk';
export declare const lineConfig: {
channelAccessToken: string;
channelSecret: string;
};
export declare const lineClient: line.messagingApi.MessagingApiClient;
//# sourceMappingURL=lineClient.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"lineClient.d.ts","sourceRoot":"","sources":["lineClient.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AAKtC,eAAO,MAAM,UAAU;;;CAGtB,CAAC;AAEF,eAAO,MAAM,UAAU,sCAErB,CAAC"}
+11
View File
@@ -0,0 +1,11 @@
import * as line from '@line/bot-sdk';
const channelAccessToken = process.env.LINE_CHANNEL_ACCESS_TOKEN || '';
const channelSecret = process.env.LINE_CHANNEL_SECRET || '';
export const lineConfig = {
channelAccessToken,
channelSecret,
};
export const lineClient = new line.messagingApi.MessagingApiClient({
channelAccessToken,
});
//# sourceMappingURL=lineClient.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"lineClient.js","sourceRoot":"","sources":["lineClient.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AAEtC,MAAM,kBAAkB,GAAG,OAAO,CAAC,GAAG,CAAC,yBAAyB,IAAI,EAAE,CAAC;AACvE,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,EAAE,CAAC;AAE5D,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,kBAAkB;IAClB,aAAa;CACd,CAAC;AAEF,MAAM,CAAC,MAAM,UAAU,GAAG,IAAI,IAAI,CAAC,YAAY,CAAC,kBAAkB,CAAC;IACjE,kBAAkB;CACnB,CAAC,CAAC"}
+6
View File
@@ -0,0 +1,6 @@
export declare function sendOrderCreatedNotification(orderId: number): Promise<void>;
export declare function sendPaymentConfirmedNotification(orderId: number): Promise<void>;
export declare function sendOrderShippedNotification(orderId: number): Promise<void>;
export declare function sendOrderCancelledNotification(orderId: number): Promise<void>;
export declare function markOrderAsPaid(orderId: number): Promise<void>;
//# sourceMappingURL=notifications.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"notifications.d.ts","sourceRoot":"","sources":["notifications.ts"],"names":[],"mappings":"AAGA,wBAAsB,4BAA4B,CAAC,OAAO,EAAE,MAAM,iBAqNjE;AAED,wBAAsB,gCAAgC,CAAC,OAAO,EAAE,MAAM,iBA0HrE;AAED,wBAAsB,4BAA4B,CAAC,OAAO,EAAE,MAAM,iBA2GjE;AAED,wBAAsB,8BAA8B,CAAC,OAAO,EAAE,MAAM,iBAkFnE;AAED,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,iBAoCpD"}
+554
View File
@@ -0,0 +1,554 @@
import prisma from "./prisma";
import { lineClient } from "./lineClient";
export async function sendOrderCreatedNotification(orderId) {
try {
const order = await prisma.order.findUnique({
where: { id: orderId },
include: { items: true, customer: true }
});
if (!order || !order.customer?.lineUserId) {
console.log(`[Notification] Order #${orderId} has no associated lineUserId or does not exist.`);
return;
}
const settings = await prisma.storeSettings.findFirst();
const bankName = settings?.bankName || "KASIKORNBANK (KBANK)";
const bankAccountNumber = settings?.bankAccountNumber || "012-3-45678-9";
const bankAccountName = settings?.bankAccountName || "HomAroma Co., Ltd.";
const itemContents = order.items.map(item => ({
type: "box",
layout: "horizontal",
contents: [
{
type: "text",
text: `${item.productName}${item.variantSize ? ` (${item.variantSize})` : ""} x ${item.quantity}`,
size: "sm",
color: "#333333",
flex: 3,
wrap: true
},
{
type: "text",
text: `฿${item.priceAtPurchase * item.quantity}`,
size: "sm",
color: "#1C3F3A",
weight: "bold",
align: "end",
flex: 1
}
]
}));
const flexMessage = {
type: "flex",
altText: `HomAroma: คำสั่งซื้อ #${order.id} ได้รับแล้ว กรุณาชำระเงิน`,
contents: {
type: "bubble",
header: {
type: "box",
layout: "vertical",
backgroundColor: "#1C3F3A",
contents: [
{
type: "text",
text: "HomAroma Official",
color: "#D4B16A",
weight: "bold",
size: "xs"
},
{
type: "text",
text: "ได้รับคำสั่งซื้อของคุณแล้ว",
color: "#FFFFFF",
weight: "bold",
size: "md",
margin: "sm"
}
]
},
body: {
type: "box",
layout: "vertical",
contents: [
{
type: "text",
text: `หมายเลขออเดอร์: #HM-${order.id}`,
weight: "bold",
size: "sm",
color: "#333333"
},
{
type: "separator",
margin: "md"
},
{
type: "box",
layout: "vertical",
margin: "md",
spacing: "sm",
contents: [
{
type: "box",
layout: "horizontal",
contents: [
{
type: "text",
text: "รายการสินค้า",
size: "xs",
color: "#888888",
flex: 3
},
{
type: "text",
text: "ยอดรวม",
size: "xs",
color: "#888888",
align: "end",
flex: 1
}
]
},
...itemContents
]
},
{
type: "separator",
margin: "md"
},
{
type: "box",
layout: "horizontal",
margin: "md",
contents: [
{
type: "text",
text: "ยอดชำระสุทธิ (รวมค่าส่ง)",
size: "sm",
color: "#555555",
weight: "bold",
flex: 3
},
{
type: "text",
text: `฿${order.totalAmount}`,
size: "sm",
color: "#1C3F3A",
weight: "bold",
align: "end",
flex: 1
}
]
},
{
type: "separator",
margin: "md"
},
{
type: "box",
layout: "vertical",
margin: "md",
contents: [
{
type: "text",
text: "🏦 ช่องทางการชำระเงินโอนเงิน:",
size: "xs",
color: "#555555",
weight: "bold"
},
{
type: "text",
text: `${bankName} (${bankAccountNumber})`,
size: "xs",
color: "#1C3F3A",
margin: "xs",
weight: "bold"
},
{
type: "text",
text: `ชื่อบัญชี: ${bankAccountName}`,
size: "xs",
color: "#555555"
}
]
}
]
},
footer: {
type: "box",
layout: "vertical",
spacing: "sm",
contents: [
{
type: "button",
style: "primary",
color: "#1C3F3A",
action: {
"type": "uri",
"label": "แนบสลิปชำระเงิน",
"uri": `https://spa.homaroma.shop/order-success?id=${order.id}`
}
},
{
type: "button",
style: "link",
height: "sm",
action: {
"type": "uri",
"label": "ติดตามสถานะออเดอร์",
"uri": "https://spa.homaroma.shop/track-order"
}
}
]
}
}
};
await lineClient.pushMessage({
to: order.customer.lineUserId,
messages: [flexMessage]
});
console.log(`[Notification] OrderCreated notification sent for Order #${orderId}`);
}
catch (err) {
console.error(`[Notification] Failed to send OrderCreated notification for Order #${orderId}:`, err);
}
}
export async function sendPaymentConfirmedNotification(orderId) {
try {
const order = await prisma.order.findUnique({
where: { id: orderId },
include: { customer: true }
});
if (!order || !order.customer?.lineUserId) {
console.log(`[Notification] Order #${orderId} has no associated lineUserId or does not exist.`);
return;
}
const flexMessage = {
type: "flex",
altText: "HomAroma: ได้รับการชำระเงินเรียบร้อยแล้ว",
contents: {
type: "bubble",
body: {
type: "box",
layout: "vertical",
contents: [
{
type: "box",
layout: "vertical",
alignItems: "center",
contents: [
{
type: "text",
text: "✨ ชำระเงินสำเร็จ ✨",
weight: "bold",
size: "xl",
color: "#1C3F3A"
},
{
type: "text",
text: `ขอขอบคุณสำหรับคำสั่งซื้อออเดอร์ #HM-${order.id}`,
size: "xs",
color: "#888888",
margin: "xs"
}
]
},
{
type: "box",
layout: "vertical",
backgroundColor: "#F4F6F5",
margin: "lg",
paddingAll: "md",
cornerRadius: "md",
contents: [
{
type: "box",
layout: "horizontal",
contents: [
{
type: "text",
text: "ได้รับแต้มครั้งนี้",
size: "sm",
color: "#555555"
},
{
type: "text",
text: `+${order.pointsEarned} แต้ม`,
weight: "bold",
size: "sm",
color: "#1C3F3A",
align: "end"
}
]
},
{
type: "box",
layout: "horizontal",
margin: "xs",
contents: [
{
type: "text",
text: "แต้มสะสมรวมล่าสุด",
size: "sm",
color: "#555555"
},
{
type: "text",
text: `${order.customer.points} แต้ม`,
weight: "bold",
size: "sm",
color: "#D4B16A",
align: "end"
}
]
}
]
}
]
},
footer: {
type: "box",
layout: "vertical",
contents: [
{
type: "button",
style: "primary",
color: "#1C3F3A",
action: {
"type": "uri",
"label": "แลกของรางวัล & คูปองสะสม",
"uri": "https://spa.homaroma.shop/profile"
}
}
]
}
}
};
await lineClient.pushMessage({
to: order.customer.lineUserId,
messages: [flexMessage]
});
console.log(`[Notification] PaymentConfirmed notification sent for Order #${orderId}`);
}
catch (err) {
console.error(`[Notification] Failed to send PaymentConfirmed notification for Order #${orderId}:`, err);
}
}
export async function sendOrderShippedNotification(orderId) {
try {
const order = await prisma.order.findUnique({
where: { id: orderId },
include: { customer: true }
});
if (!order || !order.customer?.lineUserId) {
console.log(`[Notification] Order #${orderId} has no associated lineUserId or does not exist.`);
return;
}
const flexMessage = {
type: "flex",
altText: "HomAroma: สินค้าของคุณอยู่ระหว่างจัดส่ง",
contents: {
type: "bubble",
header: {
type: "box",
layout: "vertical",
backgroundColor: "#1C3F3A",
contents: [
{
type: "text",
text: "ออเดอร์ของคุณกำลังเดินทาง 🚚",
color: "#FFFFFF",
weight: "bold",
size: "md"
}
]
},
body: {
type: "box",
layout: "vertical",
spacing: "md",
contents: [
{
type: "box",
layout: "horizontal",
contents: [
{
type: "text",
text: "หมายเลขออเดอร์:",
size: "sm",
color: "#888888",
flex: 2
},
{
type: "text",
text: `#HM-${order.id}`,
size: "sm",
color: "#333333",
weight: "bold",
flex: 3
}
]
},
{
type: "box",
layout: "horizontal",
contents: [
{
type: "text",
text: "หมายเลขพัสดุ:",
size: "sm",
color: "#888888",
flex: 2
},
{
type: "text",
text: order.trackingNo || "อยู่ระหว่างเข้ารับพัสดุ",
size: "sm",
color: "#1C3F3A",
weight: "bold",
flex: 3
}
]
}
]
},
footer: {
type: "box",
layout: "vertical",
contents: [
{
type: "button",
style: "primary",
color: "#1C3F3A",
action: {
"type": "uri",
"label": "เช็คสถานะการจัดส่ง",
"uri": "https://spa.homaroma.shop/track-order"
}
}
]
}
}
};
await lineClient.pushMessage({
to: order.customer.lineUserId,
messages: [flexMessage]
});
console.log(`[Notification] OrderShipped notification sent for Order #${orderId}`);
}
catch (err) {
console.error(`[Notification] Failed to send OrderShipped notification for Order #${orderId}:`, err);
}
}
export async function sendOrderCancelledNotification(orderId) {
try {
const order = await prisma.order.findUnique({
where: { id: orderId },
include: { customer: true }
});
if (!order || !order.customer?.lineUserId) {
console.log(`[Notification] Order #${orderId} has no associated lineUserId or does not exist.`);
return;
}
const flexMessage = {
type: "flex",
altText: "HomAroma: คำสั่งซื้อของคุณถูกยกเลิกแล้ว",
contents: {
type: "bubble",
header: {
type: "box",
layout: "vertical",
backgroundColor: "#8C2A2A",
contents: [
{
type: "text",
text: "คำสั่งซื้อถูกยกเลิก 🚫",
color: "#FFFFFF",
weight: "bold",
size: "md"
}
]
},
body: {
type: "box",
layout: "vertical",
spacing: "md",
contents: [
{
type: "text",
text: `คำสั่งซื้อหมายเลข #HM-${order.id} ได้ถูกยกเลิกแล้ว เนื่องจากเกินกำหนดเวลาชำระเงิน หรือสิทธิประโยชน์บางประการ หรือตามความประสงค์ของคุณ`,
size: "sm",
color: "#333333",
wrap: true
},
...(order.pointsUsed > 0 ? [
{
type: "text",
text: `* ระบบได้ทำการคืนคะแนนสะสมจำนวน ${order.pointsUsed} แต้มเข้าบัญชีของคุณเรียบร้อยแล้ว`,
size: "xs",
color: "#8C2A2A",
weight: "bold",
margin: "md"
}
] : [])
]
},
footer: {
type: "box",
layout: "vertical",
contents: [
{
type: "button",
style: "primary",
color: "#1C3F3A",
action: {
"type": "uri",
"label": "ช้อปต่ออีกครั้ง",
"uri": "https://spa.homaroma.shop/"
}
}
]
}
}
};
await lineClient.pushMessage({
to: order.customer.lineUserId,
messages: [flexMessage]
});
console.log(`[Notification] OrderCancelled notification sent for Order #${orderId}`);
}
catch (err) {
console.error(`[Notification] Failed to send OrderCancelled notification for Order #${orderId}:`, err);
}
}
export async function markOrderAsPaid(orderId) {
try {
const order = await prisma.order.findUnique({
where: { id: orderId }
});
if (!order) {
console.log(`[Payment] Order #${orderId} not found in markOrderAsPaid.`);
return;
}
if (order.status === "PAID") {
console.log(`[Payment] Order #${orderId} is already PAID.`);
return;
}
// 1. Update status to PAID
await prisma.order.update({
where: { id: orderId },
data: { status: "PAID" }
});
// 2. Grant points
if (order.customerId && order.pointsEarned > 0) {
await prisma.customer.update({
where: { id: order.customerId },
data: { points: { increment: order.pointsEarned } }
});
console.log(`[Payment] Granted ${order.pointsEarned} points to Customer #${order.customerId} for Order #${orderId}`);
}
// 3. Send payment notification
await sendPaymentConfirmedNotification(orderId);
}
catch (err) {
console.error(`[Payment] Error in markOrderAsPaid for Order #${orderId}:`, err);
}
}
//# sourceMappingURL=notifications.js.map
@@ -0,0 +1 @@
{"version":3,"file":"notifications.js","sourceRoot":"","sources":["notifications.ts"],"names":[],"mappings":"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 truncated
+7
View File
@@ -0,0 +1,7 @@
import { PrismaClient } from "@prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
declare const prisma: PrismaClient<{
adapter: PrismaPg;
}, never, import("@prisma/client/runtime/client").DefaultArgs>;
export default prisma;
//# sourceMappingURL=prisma.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"prisma.d.ts","sourceRoot":"","sources":["prisma.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAS9C,QAAA,MAAM,MAAM;;8DAAgC,CAAC;AAE7C,eAAe,MAAM,CAAC"}
+9
View File
@@ -0,0 +1,9 @@
import { PrismaClient } from "@prisma/client";
import { PrismaPg } from "@prisma/adapter-pg";
import dotenv from "dotenv";
dotenv.config();
const connectionString = process.env.DATABASE_URL;
const adapter = new PrismaPg({ connectionString });
const prisma = new PrismaClient({ adapter });
export default prisma;
//# sourceMappingURL=prisma.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"prisma.js","sourceRoot":"","sources":["prisma.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B,MAAM,CAAC,MAAM,EAAE,CAAC;AAEhB,MAAM,gBAAgB,GAAG,OAAO,CAAC,GAAG,CAAC,YAAa,CAAC;AAEnD,MAAM,OAAO,GAAG,IAAI,QAAQ,CAAC,EAAE,gBAAgB,EAAE,CAAC,CAAC;AAEnD,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;AAE7C,eAAe,MAAM,CAAC"}
+25
View File
@@ -0,0 +1,25 @@
import { Elysia } from "elysia";
import { jwt } from "@elysiajs/jwt";
export const adminOnly = new Elysia({ name: "adminOnly" })
.use(jwt({
name: "jwt",
secret: process.env.JWT_SECRET || "fallback-secret"
}))
.onBeforeHandle(async ({ jwt, headers, set }) => {
const auth = headers.authorization;
if (!auth?.startsWith("Bearer ")) {
set.status = 401;
return { error: "Unauthorized: Missing or invalid token format" };
}
const token = auth.substring(7);
const payload = await jwt.verify(token);
if (!payload) {
set.status = 401;
return { error: "Unauthorized: Invalid token" };
}
if (payload.role !== "ADMIN") {
set.status = 401;
return { error: "Unauthorized: Admin access required" };
}
});
//# sourceMappingURL=auth.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"auth.js","sourceRoot":"","sources":["auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AAEpC,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,MAAM,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;KACvD,GAAG,CACF,GAAG,CAAC;IACF,IAAI,EAAE,KAAK;IACX,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,iBAAiB;CACpD,CAAC,CACH;KACA,cAAc,CAAC,KAAK,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE;IAC9C,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC;IACnC,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACjC,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;QACjB,OAAO,EAAE,KAAK,EAAE,+CAA+C,EAAE,CAAC;IACpE,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IAChC,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAExC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;QACjB,OAAO,EAAE,KAAK,EAAE,6BAA6B,EAAE,CAAC;IAClD,CAAC;IAED,IAAK,OAAe,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QACtC,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;QACjB,OAAO,EAAE,KAAK,EAAE,qCAAqC,EAAE,CAAC;IAC1D,CAAC;AACH,CAAC,CAAC,CAAC"}
+157
View File
@@ -0,0 +1,157 @@
import { Elysia } from "elysia";
export declare const articlesRoute: Elysia<"/articles", {
decorator: {};
store: {};
derive: {};
resolve: {};
}, {
typebox: {};
error: {};
}, {
schema: {};
standaloneSchema: {};
macro: {};
macroFn: {};
parser: {};
response: {};
}, {
articles: {
get: {
body: unknown;
params: {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
} & {
articles: {
":slug": {
get: {
body: unknown;
params: {
slug: string;
} & {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
};
} & {
articles: {
id: {
":id": {
get: {
body: unknown;
params: {
id: string;
} & {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
};
};
} & {
articles: {
post: {
body: {
coverImage?: string;
slug: string;
title: string;
isPublished: boolean;
content: string;
};
params: {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
} & {
articles: {
":id": {
put: {
body: {
coverImage?: string;
slug: string;
title: string;
isPublished: boolean;
content: string;
};
params: {
id: string;
} & {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
};
} & {
articles: {
":id": {
delete: {
body: unknown;
params: {
id: string;
} & {};
query: unknown;
headers: unknown;
response: {
200: {
success: boolean;
error?: never;
} | {
error: string;
success?: never;
};
422: {
type: "validation";
on: string;
summary?: string;
message?: string;
found?: unknown;
property?: string;
expected?: string;
};
};
};
};
};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}>;
//# sourceMappingURL=articles.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"articles.d.ts","sourceRoot":"","sources":["articles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,QAAQ,CAAC;AAGnC,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2GtB,CAAC"}
+116
View File
@@ -0,0 +1,116 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
export const articlesRoute = new Elysia({ prefix: "/articles" })
.get("/", async ({ query, set }) => {
try {
const { publishedOnly } = query;
const where = publishedOnly === 'true' ? { isPublished: true } : {};
return await prisma.article.findMany({
where,
orderBy: { createdAt: 'desc' }
});
}
catch (err) {
set.status = 500;
return { error: "Failed to load articles" };
}
})
.get("/:slug", async ({ params: { slug }, set }) => {
try {
const article = await prisma.article.findUnique({
where: { slug }
});
if (!article) {
set.status = 404;
return { error: "Article not found" };
}
return article;
}
catch (err) {
set.status = 500;
return { error: "Failed to fetch article" };
}
})
.get("/id/:id", async ({ params: { id }, set }) => {
try {
const article = await prisma.article.findUnique({
where: { id: parseInt(id, 10) }
});
if (!article) {
set.status = 404;
return { error: "Article not found" };
}
return article;
}
catch (err) {
set.status = 500;
return { error: "Failed to fetch article" };
}
})
.post("/", async ({ body, set }) => {
try {
const data = body;
return await prisma.article.create({
data: {
title: data.title,
slug: data.slug,
coverImage: data.coverImage,
content: data.content,
isPublished: data.isPublished
}
});
}
catch (err) {
console.error(err);
set.status = 500;
return { error: "Failed to create article. Ensure slug is unique." };
}
}, {
body: t.Object({
title: t.String(),
slug: t.String(),
coverImage: t.Optional(t.String()),
content: t.String(),
isPublished: t.Boolean()
})
})
.put("/:id", async ({ params: { id }, body, set }) => {
try {
const data = body;
return await prisma.article.update({
where: { id: parseInt(id, 10) },
data: {
title: data.title,
slug: data.slug,
coverImage: data.coverImage,
content: data.content,
isPublished: data.isPublished
}
});
}
catch (err) {
set.status = 500;
return { error: "Failed to update article" };
}
}, {
body: t.Object({
title: t.String(),
slug: t.String(),
coverImage: t.Optional(t.String()),
content: t.String(),
isPublished: t.Boolean()
})
})
.delete("/:id", async ({ params: { id }, set }) => {
try {
await prisma.article.delete({
where: { id: parseInt(id, 10) }
});
return { success: true };
}
catch (err) {
set.status = 500;
return { error: "Failed to delete article" };
}
});
//# sourceMappingURL=articles.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"articles.js","sourceRoot":"","sources":["articles.ts"],"names":[],"mappings":"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"}
+95
View File
@@ -0,0 +1,95 @@
import { Elysia, t } from "elysia";
import { jwt } from "@elysiajs/jwt";
import bcrypt from "bcryptjs";
import prisma from "../lib/prisma";
export const authRoute = new Elysia({ prefix: "/auth" })
.use(jwt({
name: "jwt",
secret: process.env.JWT_SECRET || "homaroma_secret_2026_scent",
exp: "24h",
}))
.post("/login", async ({ body, jwt, set }) => {
const { username, password } = body;
// 1. Find user
const user = await prisma.user.findUnique({
where: { username },
});
if (!user) {
set.status = 401;
return { success: false, message: "Invalid credentials" };
}
// 2. Check password
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
set.status = 401;
return { success: false, message: "Invalid credentials" };
}
// 3. Generate Token
const token = await jwt.sign({
id: user.id,
username: user.username,
role: user.role,
});
return {
success: true,
token,
user: {
id: user.id,
username: user.username,
role: user.role,
},
};
}, {
body: t.Object({
username: t.String(),
password: t.String(),
}),
detail: {
summary: "Admin Login",
tags: ["auth"],
},
})
.post("/line", async ({ body, jwt, set }) => {
const { lineUserId, displayName, pictureUrl, email, phone } = body;
// Upsert Customer
const customer = await prisma.customer.upsert({
where: { lineUserId },
update: {
displayName,
pictureUrl,
...(email && { email }),
...(phone && { phone }),
},
create: {
lineUserId,
displayName,
pictureUrl,
email,
phone,
},
});
// Generate Token
const token = await jwt.sign({
id: customer.id,
lineUserId: customer.lineUserId,
role: "CUSTOMER",
});
return {
success: true,
token,
customer,
};
}, {
body: t.Object({
lineUserId: t.String(),
displayName: t.Optional(t.String()),
pictureUrl: t.Optional(t.String()),
email: t.Optional(t.String()),
phone: t.Optional(t.String()),
}),
detail: {
summary: "LINE Login for Customers",
tags: ["auth"],
},
});
//# sourceMappingURL=auth.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"auth.js","sourceRoot":"","sources":["auth.ts"],"names":[],"mappings":"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"}
+201
View File
@@ -0,0 +1,201 @@
import { Elysia } from "elysia";
export declare const categoriesRoute: Elysia<"/categories", {
decorator: {};
store: {};
derive: {};
resolve: {};
}, {
typebox: {};
error: {};
}, {
schema: {};
standaloneSchema: {};
macro: {};
macroFn: {};
parser: {};
response: {};
}, {
categories: {
get: {
body: unknown;
params: {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
} & {
categories: {
":id": {
products: {
get: {
body: unknown;
params: {
id: string;
} & {};
query: unknown;
headers: unknown;
response: {
200: {
category: {
id: any;
name: any;
imageUrl: any;
_count: any;
};
data: any;
meta: {
total: any;
page: number;
limit: number;
lastPage: number;
};
};
400: {
readonly message: "Invalid category ID";
};
404: {
readonly message: "Category not found";
};
422: {
type: "validation";
on: string;
summary?: string;
message?: string;
found?: unknown;
property?: string;
expected?: string;
};
};
};
};
};
};
} & {
categories: {
":id": {
translations: {
patch: {
body: unknown;
params: {
id: string;
} & {};
query: unknown;
headers: unknown;
response: {
200: {
success: boolean;
translations: any[];
category: any;
};
400: {
readonly message: "Invalid category ID";
};
422: {
type: "validation";
on: string;
summary?: string;
message?: string;
found?: unknown;
property?: string;
expected?: string;
};
};
};
};
};
};
} & {
categories: {
post: {
body: {
imageUrl?: string | null;
name: string;
};
params: {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
} & {
categories: {
":id": {
put: {
body: {
name?: string;
imageUrl?: string | null;
};
params: {
id: string;
} & {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
};
} & {
categories: {
":id": {
delete: {
body: unknown;
params: {
id: string;
} & {};
query: unknown;
headers: unknown;
response: {
200: {
success: boolean;
};
400: {
readonly message: "Invalid category ID";
} | {
readonly message: "Cannot delete category with existing products";
};
404: {
readonly message: "Category not found";
};
422: {
type: "validation";
on: string;
summary?: string;
message?: string;
found?: unknown;
property?: string;
expected?: string;
};
500: {
readonly message: "Failed to delete category";
};
};
};
};
};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}>;
//# sourceMappingURL=categories.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"categories.d.ts","sourceRoot":"","sources":["categories.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,QAAQ,CAAC;AAkBnC,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgNxB,CAAC"}
+222
View File
@@ -0,0 +1,222 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
const LANGS = ['th', 'en', 'ja', 'zh', 'vi', 'ko'];
/** Resolve category name based on lang */
function resolveCatName(cat, lang) {
if (lang === 'all') {
const nameObj = {};
cat.translations?.forEach((tr) => { nameObj[tr.langCode] = tr.name; });
return nameObj;
}
const trans = cat.translations?.find((t) => t.langCode === lang)
|| cat.translations?.find((t) => t.langCode === 'th')
|| cat.translations?.[0];
return trans?.name ?? cat.name;
}
export const categoriesRoute = new Elysia({ prefix: "/categories" })
.get("/", async ({ query }) => {
const lang = query.lang || 'th';
const categories = await prisma.category.findMany({
orderBy: { name: "asc" },
include: {
translations: true,
_count: {
select: { products: true },
},
},
});
return categories.map((cat) => ({
...cat,
name: resolveCatName(cat, lang),
}));
})
.get("/:id/products", async ({ params, query, status }) => {
const id = parseInt(params.id);
const lang = query.lang || 'th';
if (isNaN(id))
return status(400, { message: "Invalid category ID" });
// Pagination params
const page = query.page ? parseInt(query.page) : 1;
const limit = query.limit ? parseInt(query.limit) : 12;
const skip = (page - 1) * limit;
const [category, total] = await Promise.all([
prisma.category.findUnique({
where: { id },
include: {
translations: true,
products: {
include: {
scent: true,
category: { include: { translations: true } },
translations: true,
variants: { orderBy: { price: "asc" } },
images: { orderBy: { order: "asc" } },
},
orderBy: { id: "asc" },
skip,
take: limit,
},
_count: {
select: { products: true }
}
},
}),
prisma.product.count({ where: { categoryId: id } }),
]);
if (!category)
return status(404, { message: "Category not found" });
return {
category: {
id: category.id,
name: resolveCatName(category, lang),
imageUrl: category.imageUrl,
_count: category._count,
},
data: category.products.map((p) => {
if (lang === 'all') {
const nameObj = {};
const descObj = {};
const detailsObj = {};
p.translations?.forEach((t) => {
nameObj[t.langCode] = t.name;
descObj[t.langCode] = t.description;
detailsObj[t.langCode] = t.details;
});
return {
...p,
name: nameObj,
description: descObj,
details: detailsObj,
category: { ...p.category, name: resolveCatName(p.category, lang) },
};
}
else {
const trans = p.translations?.find((t) => t.langCode === lang) || p.translations?.find((t) => t.langCode === 'th') || p.translations?.[0];
return {
...p,
name: trans?.name || p.scent?.name || '',
description: trans?.description,
details: trans?.details,
category: { ...p.category, name: resolveCatName(p.category, lang) },
};
}
}),
meta: {
total,
page,
limit,
lastPage: Math.ceil(total / limit),
},
};
})
.patch("/:id/translations", async ({ params, body, status }) => {
const id = parseInt(params.id);
if (isNaN(id))
return status(400, { message: "Invalid category ID" });
const payload = body;
const results = [];
for (const lang of LANGS) {
const name = typeof payload.name === 'string'
? (lang === 'th' ? payload.name : null)
: payload.name?.[lang];
if (!name)
continue;
const existing = await prisma.categoryTranslation.findUnique({
where: { categoryId_langCode: { categoryId: id, langCode: lang } }
});
if (existing) {
const updated = await prisma.categoryTranslation.update({
where: { id: existing.id },
data: { name },
});
results.push(updated);
}
else {
const created = await prisma.categoryTranslation.create({
data: { categoryId: id, langCode: lang, name },
});
results.push(created);
}
}
const updated = await prisma.category.findUnique({
where: { id },
include: { translations: true, _count: { select: { products: true } } },
});
return { success: true, translations: results, category: updated };
})
.post("/", async ({ body, status }) => {
try {
const category = await prisma.category.create({
data: {
name: body.name,
imageUrl: body.imageUrl,
// Auto-seed Thai translation
translations: { create: { langCode: 'th', name: body.name } },
},
});
return category;
}
catch (err) {
if (err.code === "P2002") {
return status(400, { message: "Category name already exists" });
}
return status(500, { message: "Failed to create category" });
}
}, {
body: t.Object({
name: t.String(),
imageUrl: t.Optional(t.Union([t.String(), t.Null()])),
})
})
.put("/:id", async ({ params, body, status }) => {
const id = parseInt(params.id);
if (isNaN(id))
return status(400, { message: "Invalid category ID" });
try {
const category = await prisma.category.update({
where: { id },
data: {
name: body.name,
imageUrl: body.imageUrl,
},
});
return category;
}
catch (err) {
if (err.code === "P2025") {
return status(404, { message: "Category not found" });
}
if (err.code === "P2002") {
return status(400, { message: "Category name already exists" });
}
console.error("Update category error:", err);
return status(500, { message: "Failed to update category" });
}
}, {
body: t.Object({
name: t.Optional(t.String()),
imageUrl: t.Optional(t.Union([t.String(), t.Null()])),
})
})
.delete("/:id", async ({ params, status }) => {
const id = parseInt(params.id);
if (isNaN(id))
return status(400, { message: "Invalid category ID" });
try {
const category = await prisma.category.findUnique({
where: { id },
include: { _count: { select: { products: true } } }
});
if (!category)
return status(404, { message: "Category not found" });
if (category._count.products > 0) {
return status(400, { message: "Cannot delete category with existing products" });
}
await prisma.category.delete({ where: { id } });
return { success: true };
}
catch (err) {
return status(500, { message: "Failed to delete category" });
}
});
//# sourceMappingURL=categories.js.map
@@ -0,0 +1 @@
{"version":3,"file":"categories.js","sourceRoot":"","sources":["categories.ts"],"names":[],"mappings":"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 truncated
+185
View File
@@ -0,0 +1,185 @@
import { Elysia } from "elysia";
export declare const couponsRoute: Elysia<"/coupons", {
decorator: {};
store: {};
derive: {};
resolve: {};
}, {
typebox: {};
error: {};
}, {
schema: {};
standaloneSchema: {};
macro: {};
macroFn: {};
parser: {};
response: {};
}, {
coupons: {
get: {
body: unknown;
params: {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
} & {
coupons: {
active: {
get: {
body: unknown;
params: {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
};
} & {
coupons: {
post: {
body: {
isActive?: boolean;
minPurchase?: number;
maxDiscount?: number | null;
usageLimit?: number | null;
expiresAt?: string | null;
type: string;
value: number;
code: string;
};
params: {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
} & {
coupons: {
":id": {
patch: {
body: {
type?: string;
isActive?: boolean;
value?: number;
code?: string;
minPurchase?: number;
maxDiscount?: number | null;
usageLimit?: number | null;
expiresAt?: string | null;
};
params: {
id: string;
} & {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
};
} & {
coupons: {
":id": {
delete: {
body: unknown;
params: {
id: string;
} & {};
query: unknown;
headers: unknown;
response: {
200: {
success: boolean;
error?: never;
} | {
error: string;
success?: never;
};
422: {
type: "validation";
on: string;
summary?: string;
message?: string;
found?: unknown;
property?: string;
expected?: string;
};
};
};
};
};
} & {
coupons: {
validate: {
post: {
body: {
code: string;
cartTotal: number;
};
params: {};
query: unknown;
headers: unknown;
response: {
200: {
valid: boolean;
error: string;
coupon?: never;
discountAmount?: never;
isFreeShipping?: never;
} | {
valid: boolean;
coupon: any;
discountAmount: number;
isFreeShipping: boolean;
error?: never;
} | {
error: string;
valid?: never;
coupon?: never;
discountAmount?: never;
isFreeShipping?: never;
};
422: {
type: "validation";
on: string;
summary?: string;
message?: string;
found?: unknown;
property?: string;
expected?: string;
};
};
};
};
};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}>;
//# sourceMappingURL=coupons.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"coupons.d.ts","sourceRoot":"","sources":["coupons.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,QAAQ,CAAC;AAGnC,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA6LrB,CAAC"}
+186
View File
@@ -0,0 +1,186 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
export const couponsRoute = new Elysia({ prefix: "/coupons" })
// Get all coupons (Admin)
.get("/", async () => {
return await prisma.coupon.findMany({
orderBy: { createdAt: 'desc' }
});
})
// Get active coupons (Storefront)
.get("/active", async () => {
const now = new Date();
return await prisma.coupon.findMany({
where: {
isActive: true,
OR: [
{ expiresAt: null },
{ expiresAt: { gt: now } }
]
},
orderBy: { createdAt: 'desc' }
});
})
// Create new coupon (Admin)
.post("/", async ({ body, set }) => {
try {
const data = body;
// Check if code already exists
const existing = await prisma.coupon.findUnique({
where: { code: data.code.toUpperCase() }
});
if (existing) {
set.status = 400;
return { error: "Coupon code already exists" };
}
const coupon = await prisma.coupon.create({
data: {
code: data.code.toUpperCase(),
type: data.type,
value: data.value,
minPurchase: data.minPurchase || 0,
maxDiscount: data.maxDiscount || null,
usageLimit: data.usageLimit || null,
expiresAt: data.expiresAt ? new Date(data.expiresAt) : null,
isActive: data.isActive !== undefined ? data.isActive : true
}
});
return coupon;
}
catch (err) {
console.error("[Coupons] Create Error:", err);
set.status = 500;
return { error: "Failed to create coupon" };
}
}, {
body: t.Object({
code: t.String(),
type: t.String(),
value: t.Number(),
minPurchase: t.Optional(t.Number()),
maxDiscount: t.Optional(t.Nullable(t.Number())),
usageLimit: t.Optional(t.Nullable(t.Number())),
expiresAt: t.Optional(t.Nullable(t.String())),
isActive: t.Optional(t.Boolean())
})
})
// Update coupon (Admin)
.patch("/:id", async ({ params: { id }, body, set }) => {
try {
const data = body;
const coupon = await prisma.coupon.update({
where: { id: Number(id) },
data: {
...(data.code && { code: data.code.toUpperCase() }),
...(data.type && { type: data.type }),
...(data.value !== undefined && { value: data.value }),
...(data.minPurchase !== undefined && { minPurchase: data.minPurchase }),
...(data.maxDiscount !== undefined && { maxDiscount: data.maxDiscount }),
...(data.usageLimit !== undefined && { usageLimit: data.usageLimit }),
...(data.expiresAt !== undefined && { expiresAt: data.expiresAt ? new Date(data.expiresAt) : null }),
...(data.isActive !== undefined && { isActive: data.isActive })
}
});
return coupon;
}
catch (err) {
console.error("[Coupons] Update Error:", err);
set.status = 500;
return { error: "Failed to update coupon" };
}
}, {
body: t.Object({
code: t.Optional(t.String()),
type: t.Optional(t.String()),
value: t.Optional(t.Number()),
minPurchase: t.Optional(t.Number()),
maxDiscount: t.Optional(t.Nullable(t.Number())),
usageLimit: t.Optional(t.Nullable(t.Number())),
expiresAt: t.Optional(t.Nullable(t.String())),
isActive: t.Optional(t.Boolean())
})
})
// Delete coupon (Admin)
.delete("/:id", async ({ params: { id }, set }) => {
try {
await prisma.coupon.delete({
where: { id: Number(id) }
});
return { success: true };
}
catch (err) {
set.status = 500;
return { error: "Failed to delete coupon" };
}
})
// Validate coupon (Storefront)
.post("/validate", async ({ body, set }) => {
try {
const { code, cartTotal } = body;
const upperCode = code.toUpperCase();
const coupon = await prisma.coupon.findUnique({
where: { code: upperCode }
});
if (!coupon) {
set.status = 404;
return { valid: false, error: "Coupon not found" };
}
if (!coupon.isActive) {
set.status = 400;
return { valid: false, error: "Coupon is no longer active" };
}
if (coupon.expiresAt && new Date() > coupon.expiresAt) {
set.status = 400;
return { valid: false, error: "Coupon has expired" };
}
if (coupon.usageLimit && coupon.usedCount >= coupon.usageLimit) {
set.status = 400;
return { valid: false, error: "Coupon usage limit reached" };
}
if (cartTotal < coupon.minPurchase) {
set.status = 400;
return { valid: false, error: `Minimum purchase of ฿${coupon.minPurchase} required` };
}
// Calculate discount amount
let discountAmount = 0;
if (coupon.type === "PERCENTAGE") {
discountAmount = cartTotal * (coupon.value / 100);
if (coupon.maxDiscount && discountAmount > coupon.maxDiscount) {
discountAmount = coupon.maxDiscount;
}
}
else if (coupon.type === "FIXED_AMOUNT") {
discountAmount = coupon.value;
}
else if (coupon.type === "FREE_SHIPPING") {
// We handle free shipping separately, returning discountAmount = 0 but a flag
return {
valid: true,
coupon,
discountAmount: 0,
isFreeShipping: true
};
}
// Ensure discount doesn't exceed cart total
if (discountAmount > cartTotal) {
discountAmount = cartTotal;
}
return {
valid: true,
coupon,
discountAmount,
isFreeShipping: false
};
}
catch (err) {
console.error("[Coupons] Validate Error:", err);
set.status = 500;
return { error: "Failed to validate coupon" };
}
}, {
body: t.Object({
code: t.String(),
cartTotal: t.Number()
})
});
//# sourceMappingURL=coupons.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"coupons.js","sourceRoot":"","sources":["coupons.ts"],"names":[],"mappings":"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 truncated
+85
View File
@@ -0,0 +1,85 @@
import { Elysia } from "elysia";
export declare const customersRoute: Elysia<"/customers", {
decorator: {};
store: {};
derive: {};
resolve: {};
}, {
typebox: {};
error: {};
}, {
schema: {};
standaloneSchema: {};
macro: {};
macroFn: {};
parser: {};
response: {};
}, {
customers: {
get: {
body: unknown;
params: {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
} & {
customers: {
":id": {
get: {
body: unknown;
params: {
id: string;
} & {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
};
} & {
customers: {
":id": {
points: {
patch: {
body: {
reason?: string;
pointsDelta: number;
};
params: {
id: string;
} & {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
};
};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}>;
//# sourceMappingURL=customers.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"customers.d.ts","sourceRoot":"","sources":["customers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,QAAQ,CAAC;AAGnC,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiEvB,CAAC"}
+63
View File
@@ -0,0 +1,63 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
export const customersRoute = new Elysia({ prefix: "/customers" })
.get("/", async ({ query }) => {
const search = query.search;
const whereClause = search ? {
OR: [
{ displayName: { contains: search, mode: "insensitive" } },
{ email: { contains: search, mode: "insensitive" } },
{ phone: { contains: search, mode: "insensitive" } },
{ lineUserId: { contains: search, mode: "insensitive" } }
]
} : {};
return await prisma.customer.findMany({
where: whereClause,
orderBy: { createdAt: 'desc' },
include: {
_count: {
select: { orders: true }
}
}
});
})
.get("/:id", async ({ params: { id }, set }) => {
const customer = await prisma.customer.findUnique({
where: { id: Number(id) },
include: { orders: { orderBy: { createdAt: 'desc' } } }
});
if (!customer) {
set.status = 404;
return { error: "Customer not found" };
}
return customer;
})
.patch("/:id/points", async ({ params: { id }, body, set }) => {
try {
const { pointsDelta, reason } = body;
const customer = await prisma.customer.findUnique({
where: { id: Number(id) }
});
if (!customer) {
set.status = 404;
return { error: "Customer not found" };
}
const newPoints = Math.max(0, customer.points + pointsDelta);
const updatedCustomer = await prisma.customer.update({
where: { id: Number(id) },
data: { points: newPoints }
});
return updatedCustomer;
}
catch (err) {
console.error("[Customers] Adjust Points Error:", err);
set.status = 500;
return { error: "Failed to adjust points" };
}
}, {
body: t.Object({
pointsDelta: t.Number(),
reason: t.Optional(t.String())
})
});
//# sourceMappingURL=customers.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"customers.js","sourceRoot":"","sources":["customers.ts"],"names":[],"mappings":"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"}
+147
View File
@@ -0,0 +1,147 @@
import { Elysia } from "elysia";
export declare const heroRoute: Elysia<"/hero", {
decorator: {};
store: {};
derive: {};
resolve: {};
}, {
typebox: {};
error: {};
}, {
schema: {};
standaloneSchema: {};
macro: {};
macroFn: {};
parser: {};
response: {};
}, {
hero: {
get: {
body: unknown;
params: {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
} & {
hero: {
all: {
get: {
body: unknown;
params: {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
};
} & {
hero: {
post: {
body: {
id?: number;
title: string;
isActive: boolean;
tagline: string;
description: string;
imageUrl: string;
buttonText: string;
buttonLink: string;
};
params: {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
} & {
hero: {
":id": {
activate: {
patch: {
body: unknown;
params: {
id: string;
} & {};
query: unknown;
headers: unknown;
response: {
200: {
success: boolean;
error?: never;
} | {
error: string;
success?: never;
};
422: {
type: "validation";
on: string;
summary?: string;
message?: string;
found?: unknown;
property?: string;
expected?: string;
};
};
};
};
};
};
} & {
hero: {
":id": {
delete: {
body: unknown;
params: {
id: string;
} & {};
query: unknown;
headers: unknown;
response: {
200: {
success: boolean;
error?: never;
} | {
error: string;
success?: never;
};
422: {
type: "validation";
on: string;
summary?: string;
message?: string;
found?: unknown;
property?: string;
expected?: string;
};
};
};
};
};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}>;
//# sourceMappingURL=hero.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"hero.d.ts","sourceRoot":"","sources":["hero.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,QAAQ,CAAC;AAGnC,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyElB,CAAC"}
+80
View File
@@ -0,0 +1,80 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
export const heroRoute = new Elysia({ prefix: "/hero" })
.get("/", async () => {
return await prisma.hero.findFirst({
where: { isActive: true },
orderBy: { updatedAt: 'desc' }
}) || await prisma.hero.findFirst({
orderBy: { updatedAt: 'desc' }
});
})
.get("/all", async () => {
return await prisma.hero.findMany({
orderBy: { updatedAt: 'desc' }
});
})
.post("/", async ({ body, set }) => {
try {
const data = body;
const { id, ...heroData } = data;
if (id) {
return await prisma.hero.update({
where: { id: Number(id) },
data: heroData
});
}
else {
return await prisma.hero.create({
data: heroData
});
}
}
catch (err) {
console.error("[Hero] Error:", err);
set.status = 500;
return { error: "Failed to save hero data" };
}
}, {
body: t.Object({
id: t.Optional(t.Number()),
tagline: t.String(),
title: t.String(),
description: t.String(),
imageUrl: t.String(),
buttonText: t.String(),
buttonLink: t.String(),
isActive: t.Boolean()
})
})
.patch("/:id/activate", async ({ params: { id }, set }) => {
try {
await prisma.$transaction([
prisma.hero.updateMany({
data: { isActive: false }
}),
prisma.hero.update({
where: { id: Number(id) },
data: { isActive: true }
})
]);
return { success: true };
}
catch (err) {
set.status = 500;
return { error: "Failed to activate hero" };
}
})
.delete("/:id", async ({ params: { id }, set }) => {
try {
await prisma.hero.delete({
where: { id: Number(id) }
});
return { success: true };
}
catch (err) {
set.status = 500;
return { error: "Failed to delete hero" };
}
});
//# sourceMappingURL=hero.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"hero.js","sourceRoot":"","sources":["hero.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,MAAM,MAAM,eAAe,CAAC;AAEnC,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,MAAM,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;KACrD,GAAG,CAAC,GAAG,EAAE,KAAK,IAAI,EAAE;IACnB,OAAO,MAAM,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC;QACjC,KAAK,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;QACzB,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE;KAC/B,CAAC,IAAI,MAAM,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC;QAChC,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE;KAC/B,CAAC,CAAC;AACL,CAAC,CAAC;KACD,GAAG,CAAC,MAAM,EAAE,KAAK,IAAI,EAAE;IACtB,OAAO,MAAM,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC;QAChC,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE;KAC/B,CAAC,CAAC;AACL,CAAC,CAAC;KACD,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE;IACjC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAW,CAAC;QACzB,MAAM,EAAE,EAAE,EAAE,GAAG,QAAQ,EAAE,GAAG,IAAI,CAAC;QAEjC,IAAI,EAAE,EAAE,CAAC;YACP,OAAO,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;gBAC9B,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,EAAE;gBACzB,IAAI,EAAE,QAAQ;aACf,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,OAAO,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;gBAC9B,IAAI,EAAE,QAAQ;aACf,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,eAAe,EAAE,GAAG,CAAC,CAAC;QACpC,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;QACjB,OAAO,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAC;IAC/C,CAAC;AACH,CAAC,EAAE;IACD,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;QACb,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;QAC1B,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;QACnB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;QACjB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE;QACvB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE;QACpB,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE;QACtB,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE;QACtB,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE;KACtB,CAAC;CACH,CAAC;KACD,KAAK,CAAC,eAAe,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE;IACxD,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,YAAY,CAAC;YACxB,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC;gBACrB,IAAI,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE;aAC1B,CAAC;YACF,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;gBACjB,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,EAAE;gBACzB,IAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;aACzB,CAAC;SACH,CAAC,CAAC;QACH,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC3B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;QACjB,OAAO,EAAE,KAAK,EAAE,yBAAyB,EAAE,CAAC;IAC9C,CAAC;AACH,CAAC,CAAC;KACD,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE;IAChD,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;YACvB,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,EAAE;SAC1B,CAAC,CAAC;QACH,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC3B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;QACjB,OAAO,EAAE,KAAK,EAAE,uBAAuB,EAAE,CAAC;IAC5C,CAAC;AACH,CAAC,CAAC,CAAC"}
+241
View File
@@ -0,0 +1,241 @@
import { Elysia } from "elysia";
export declare const lazadaOrderRoute: Elysia<"/lazada-order", {
decorator: {};
store: {};
derive: {};
resolve: {};
}, {
typebox: {};
error: {};
}, {
schema: {};
standaloneSchema: {};
macro: {};
macroFn: {};
parser: {};
response: {};
}, {
"lazada-order": {
import: {
post: {
body: {
shop?: string;
rows: any[];
};
params: {};
query: unknown;
headers: unknown;
response: {
200: {
success: boolean;
message: string;
ordersCount: number;
rowsCount: number;
} | {
success: boolean;
message: any;
ordersCount?: never;
rowsCount?: never;
};
422: {
type: "validation";
on: string;
summary?: string;
message?: string;
found?: unknown;
property?: string;
expected?: string;
};
};
};
};
};
} & {
"lazada-order": {
clear: {
delete: {
body: unknown;
params: {};
query: unknown;
headers: unknown;
response: {
200: {
success: boolean;
message: string;
count: any;
} | {
success: boolean;
message: any;
count?: never;
};
};
};
};
};
} & {
"lazada-order": {
orders: {
":orderNumber": {
get: {
body: unknown;
params: {
orderNumber: string;
} & {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
};
};
} & {
"lazada-order": {
orders: {
get: {
body: unknown;
params: {};
query: unknown;
headers: unknown;
response: {
200: {
items: any;
total: any;
page: number;
limit: number;
totalPages: number;
};
};
};
};
};
} & {
"lazada-order": {
summary: {
get: {
body: unknown;
params: {};
query: unknown;
headers: unknown;
response: {
200: {
orderCount: any;
totalAmount: any;
totalShipping: any;
};
};
};
};
};
} & {
"lazada-order": {
analytics: {
get: {
body: unknown;
params: {};
query: unknown;
headers: unknown;
response: {
200: {
monthly: {
month: string;
revenue: number;
orders: number;
cancelled: number;
items: number;
platformDiscount: number;
sellerDiscount: number;
}[];
statusBreakdown: {
status: string;
count: number;
}[];
paymentBreakdown: {
method: string;
count: number;
}[];
topProducts: {
name: string;
variation: string;
qty: number;
revenue: number;
}[];
summary: {
totalRevenue: any;
totalShipping: any;
totalPlatformDisc: any;
totalSellerDisc: any;
netRevenue: number;
totalOrders: any;
completedOrders: number;
cancelledOrders: any;
cancelRate: number;
aov: number;
};
};
};
};
};
};
} & {
"lazada-order": {
"analytics-overview": {
get: {
body: unknown;
params: {};
query: unknown;
headers: unknown;
response: {
200: {
shops: {
shop: string;
orders: number;
completed: number;
cancelled: number;
revenue: number;
shippingFee: number;
platformDiscount: number;
sellerDiscount: number;
items: number;
}[];
monthly: any[];
topProducts: {
name: string;
variation: string;
qty: number;
revenue: number;
}[];
summary: {
totalRevenue: number;
totalShipping: number;
totalPlatformDisc: number;
totalSellerDisc: number;
netRevenue: number;
totalOrders: number;
completedOrders: number;
cancelledOrders: number;
cancelRate: number;
aov: number;
};
};
};
};
};
};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}>;
//# sourceMappingURL=lazadaOrder.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"lazadaOrder.d.ts","sourceRoot":"","sources":["lazadaOrder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,QAAQ,CAAC;AA2BnC,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mCA4KiB,MAAM;qCAAW,MAAM;oCAAU,MAAM;uCAAa,MAAM;mCAAS,MAAM;8CAAoB,MAAM;4CAAkB,MAAM;;;;;;;;;;;kCA+B1H,MAAM;uCAAa,MAAM;iCAAO,MAAM;qCAAW,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kCAwD1F,MAAM;oCAAU,MAAM;uCAAa,MAAM;uCAAa,MAAM;qCACzD,MAAM;yCAAe,MAAM;8CAAoB,MAAM;4CAAkB,MAAM;mCAAS,MAAM;;;;kCAiD5D,MAAM;uCAAa,MAAM;iCAAO,MAAM;qCAAW,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsClG,CAAC"}
+348
View File
@@ -0,0 +1,348 @@
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
@@ -0,0 +1 @@
{"version":3,"file":"lazadaOrder.js","sourceRoot":"","sources":["lazadaOrder.ts"],"names":[],"mappings":"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 truncated
+70
View File
@@ -0,0 +1,70 @@
import { Elysia } from 'elysia';
export declare const lineRoutes: Elysia<"/line", {
decorator: {};
store: {};
derive: {};
resolve: {};
}, {
typebox: {};
error: {};
}, {
schema: {};
standaloneSchema: {};
macro: {};
macroFn: {};
parser: {};
response: {};
}, {
line: {
webhook: {
post: {
body: unknown;
params: {};
query: unknown;
headers: unknown;
response: {
200: {
error: string;
success?: never;
} | {
success: boolean;
error?: never;
};
};
};
};
};
} & {
line: {
webhook: {
post: {
body: unknown;
params: {};
query: unknown;
headers: unknown;
response: {
200: {
error: string;
success?: never;
} | {
success: boolean;
error?: never;
};
};
};
};
};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}>;
//# sourceMappingURL=line.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"line.d.ts","sourceRoot":"","sources":["line.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAkEhC,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAEa,CAAC"}
+60
View File
@@ -0,0 +1,60 @@
import { Elysia } from 'elysia';
import * as line from '@line/bot-sdk';
import { lineConfig, lineClient } from '../lib/lineClient';
import crypto from 'crypto';
const webhookHandler = async ({ request, body, set }) => {
console.log('[LINE Webhook] Handler body type:', typeof body);
console.log('[LINE Webhook] Channel Secret configured:', !!lineConfig.channelSecret, lineConfig.channelSecret ? lineConfig.channelSecret.substring(0, 4) + '...' : 'NONE');
// 1. Verify Signature
const signature = request.headers.get('x-line-signature');
if (!signature) {
set.status = 400;
return { error: 'Missing Signature' };
}
// xxIn our global onParse hook, we intercepted the body and returned it as a raw text string.
const rawBody = typeof body === 'string' ? body : JSON.stringify(body);
if (!rawBody) {
set.status = 400;
return { error: 'Empty Body' };
}
const hash = crypto
.createHmac('SHA256', lineConfig.channelSecret)
.update(rawBody)
.digest('base64');
console.log('[LINE Webhook] Signature Check - Received:', signature, 'Calculated:', hash);
if (hash !== signature) {
console.error('[LINE Webhook] Invalid Signature Blocked');
set.status = 401;
return { error: 'Invalid Signature' };
}
try {
const parsedBody = typeof body === 'string' ? JSON.parse(body) : body;
const events = parsedBody.events || [];
for (const event of events) {
if (event.type === 'message' && event.message.type === 'text') {
// Ignore verification dummy tokens
if (event.replyToken === '00000000000000000000000000000000' || event.replyToken === 'ffffffffffffffffffffffffffffffff') {
continue;
}
// Simple echo bot for now
await lineClient.replyMessage({
replyToken: event.replyToken,
messages: [{
type: 'text',
text: `คุณส่งข้อความว่า: ${event.message.text}`
}]
});
}
}
return { success: true };
}
catch (err) {
console.error('[LINE Webhook] Error:', err);
set.status = 500;
return { error: 'Webhook processing failed' };
}
};
export const lineRoutes = new Elysia({ prefix: '/line' })
.post('/webhook', webhookHandler)
.post('/webhook/', webhookHandler);
//# sourceMappingURL=line.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"line.js","sourceRoot":"","sources":["line.ts"],"names":[],"mappings":"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"}
+58
View File
@@ -0,0 +1,58 @@
import { Elysia } from "elysia";
export declare const menusRoute: Elysia<"/menus", {
decorator: any;
store: any;
derive: any;
resolve: any;
}, any, any, {
menus: {
get: {
body: unknown;
params: {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
} & {
menus: {
":name": {
get: {
body: unknown;
params: {
name: string;
} & {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
};
} & {
menus: any;
} & {
menus: {
post: any;
};
} & {
menus: {
":name": {
put: any;
};
};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}, any>;
//# sourceMappingURL=menus.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"menus.d.ts","sourceRoot":"","sources":["menus.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,QAAQ,CAAC;AAInC,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6FnB,CAAC"}
+97
View File
@@ -0,0 +1,97 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
import { adminOnly } from "../middleware/auth";
export const menusRoute = new Elysia({ prefix: "/menus" })
.get("/", async () => {
return await prisma.menu.findMany({
include: {
items: {
orderBy: { order: "asc" }
}
}
});
})
.get("/:name", async ({ params: { name }, set }) => {
const menu = await prisma.menu.findUnique({
where: { name },
include: {
items: {
orderBy: { order: "asc" }
}
}
});
if (!menu) {
set.status = 404;
return { error: "Menu not found" };
}
return menu;
})
.use(adminOnly)
.post("/", async ({ body, set }) => {
try {
const { name, items } = body;
const menu = await prisma.menu.create({
data: {
name,
items: items ? {
create: items
} : undefined
},
include: { items: true }
});
return menu;
}
catch (err) {
console.error(err);
set.status = 500;
return { error: "Failed to create menu" };
}
}, {
body: t.Object({
name: t.String(),
items: t.Optional(t.Array(t.Object({
label: t.String(),
url: t.String(),
order: t.Number()
})))
})
})
.put("/:name", async ({ params: { name }, body, set }) => {
try {
const { items } = body;
let menu = await prisma.menu.findUnique({ where: { name } });
if (!menu) {
menu = await prisma.menu.create({ data: { name } });
}
await prisma.$transaction([
prisma.menuItem.deleteMany({ where: { menuId: menu.id } }),
prisma.menuItem.createMany({
data: items.map(item => ({
menuId: menu.id,
label: item.label,
url: item.url,
order: item.order
}))
})
]);
return await prisma.menu.findUnique({
where: { name },
include: { items: { orderBy: { order: "asc" } } }
});
}
catch (err) {
console.error(err);
set.status = 500;
return { error: "Failed to update menu" };
}
}, {
body: t.Object({
items: t.Array(t.Object({
id: t.Optional(t.Number()),
label: t.String(),
url: t.String(),
order: t.Number()
}))
})
});
//# sourceMappingURL=menus.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"menus.js","sourceRoot":"","sources":["menus.ts"],"names":[],"mappings":"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"}
+77
View File
@@ -0,0 +1,77 @@
import { Elysia } from "elysia";
export declare const newsletterRoute: Elysia<"/newsletter", {
decorator: {};
store: {};
derive: {};
resolve: {};
}, {
typebox: {};
error: {};
}, {
schema: {};
standaloneSchema: {};
macro: {};
macroFn: {};
parser: {};
response: {};
}, {
newsletter: {
subscribe: {
post: {
body: {
email: string;
};
params: {};
query: unknown;
headers: unknown;
response: {
200: {
message: string;
error?: never;
} | {
error: string;
message?: never;
};
422: {
type: "validation";
on: string;
summary?: string;
message?: string;
found?: unknown;
property?: string;
expected?: string;
};
};
};
};
};
} & {
newsletter: {
subscribers: {
get: {
body: unknown;
params: {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}>;
//# sourceMappingURL=newsletter.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"newsletter.d.ts","sourceRoot":"","sources":["newsletter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,QAAQ,CAAC;AAGnC,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgCxB,CAAC"}
+33
View File
@@ -0,0 +1,33 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
export const newsletterRoute = new Elysia({ prefix: "/newsletter" })
.post("/subscribe", async ({ body, set }) => {
try {
const { email } = body;
const existing = await prisma.newsletterSubscriber.findUnique({
where: { email }
});
if (existing) {
return { message: "Email already subscribed" };
}
await prisma.newsletterSubscriber.create({
data: { email }
});
return { message: "Successfully subscribed" };
}
catch (err) {
console.error(err);
set.status = 500;
return { error: "Failed to subscribe" };
}
}, {
body: t.Object({
email: t.String({ format: "email" })
})
})
.get("/subscribers", async () => {
return await prisma.newsletterSubscriber.findMany({
orderBy: { createdAt: "desc" }
});
});
//# sourceMappingURL=newsletter.js.map
@@ -0,0 +1 @@
{"version":3,"file":"newsletter.js","sourceRoot":"","sources":["newsletter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,MAAM,MAAM,eAAe,CAAC;AAEnC,MAAM,CAAC,MAAM,eAAe,GAAG,IAAI,MAAM,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC;KACjE,IAAI,CAAC,YAAY,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE;IAC1C,IAAI,CAAC;QACH,MAAM,EAAE,KAAK,EAAE,GAAG,IAAyB,CAAC;QAE5C,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,oBAAoB,CAAC,UAAU,CAAC;YAC5D,KAAK,EAAE,EAAE,KAAK,EAAE;SACjB,CAAC,CAAC;QAEH,IAAI,QAAQ,EAAE,CAAC;YACb,OAAO,EAAE,OAAO,EAAE,0BAA0B,EAAE,CAAC;QACjD,CAAC;QAED,MAAM,MAAM,CAAC,oBAAoB,CAAC,MAAM,CAAC;YACvC,IAAI,EAAE,EAAE,KAAK,EAAE;SAChB,CAAC,CAAC;QAEH,OAAO,EAAE,OAAO,EAAE,yBAAyB,EAAE,CAAC;IAChD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACnB,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;QACjB,OAAO,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC;IAC1C,CAAC;AACH,CAAC,EAAE;IACD,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;QACb,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;KACrC,CAAC;CACH,CAAC;KACD,GAAG,CAAC,cAAc,EAAE,KAAK,IAAI,EAAE;IAC9B,OAAO,MAAM,MAAM,CAAC,oBAAoB,CAAC,QAAQ,CAAC;QAChD,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE;KAC/B,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
+222
View File
@@ -0,0 +1,222 @@
import { Elysia } from "elysia";
export declare const ordersRoute: Elysia<"/orders", {
decorator: {};
store: {};
derive: {};
resolve: {};
}, {
typebox: {};
error: {};
}, {
schema: {};
standaloneSchema: {};
macro: {};
macroFn: {};
parser: {};
response: {};
}, {
orders: {
get: {
body: unknown;
params: {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
} & {
orders: {
":id": {
get: {
body: unknown;
params: {
id: string;
} & {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
};
} & {
orders: {
post: {
body: {
discountAmount?: number;
customerId?: number | null;
subtotal?: number;
promoLevel1Name?: string | null;
promoLevel1Disc?: number;
promoLevel2Name?: string | null;
promoLevel2Disc?: number;
couponCode?: string | null;
pointsUsed?: number;
pointsDiscount?: number;
items: {
variantId?: number;
variantSize?: string;
productImage?: string;
productId: number;
quantity: number;
priceAtPurchase: number;
productName: string;
}[];
totalAmount: number;
customerName: string;
shippingFee: number;
customerEmail: string;
customerPhone: string;
shippingAddress: string;
};
params: {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
} & {
orders: {
":id": {
status: {
patch: {
body: {
status: string;
};
params: {
id: string;
} & {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
};
};
} & {
orders: {
":id": {
slip: {
patch: {
body: {
paymentSlipUrl: string;
};
params: {
id: string;
} & {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
};
};
} & {
orders: {
":id": {
tracking: {
patch: {
body: {
trackingNo: string;
};
params: {
id: string;
} & {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
};
};
} & {
orders: {
customer: {
":customerId": {
get: {
body: unknown;
params: {
customerId: string;
} & {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
};
};
} & {
orders: {
track: {
post: {
body: {
phone: string;
orderId: number;
};
params: {};
query: unknown;
headers: unknown;
response: {
200: {
error: string;
success?: never;
orderId?: never;
} | {
success: boolean;
orderId: any;
error?: never;
};
422: {
type: "validation";
on: string;
summary?: string;
message?: string;
found?: unknown;
property?: string;
expected?: string;
};
};
};
};
};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}>;
//# sourceMappingURL=orders.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"orders.d.ts","sourceRoot":"","sources":["orders.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,QAAQ,CAAC;AAUnC,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuPpB,CAAC"}
+247
View File
@@ -0,0 +1,247 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
import { sendOrderCreatedNotification, sendPaymentConfirmedNotification, sendOrderShippedNotification, sendOrderCancelledNotification, markOrderAsPaid } from "../lib/notifications";
export const ordersRoute = new Elysia({ prefix: "/orders" })
.get("/", async () => {
return await prisma.order.findMany({
orderBy: { createdAt: 'desc' },
include: { items: true, customer: true }
});
})
.get("/:id", async ({ params: { id }, set }) => {
const order = await prisma.order.findUnique({
where: { id: Number(id) },
include: { items: true, customer: true }
});
if (!order) {
set.status = 404;
return { error: "Order not found" };
}
return order;
})
.post("/", async ({ body, set }) => {
try {
const data = body;
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,
customerName: data.customerName,
customerEmail: data.customerEmail,
customerPhone: data.customerPhone,
shippingAddress: data.shippingAddress,
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) => ({
productId: item.productId,
variantId: item.variantId,
quantity: item.quantity,
priceAtPurchase: item.priceAtPurchase,
productName: item.productName,
variantSize: item.variantSize,
productImage: item.productImage
}))
}
},
include: { items: true }
});
// If a coupon was used, increment its usedCount
if (data.couponCode) {
try {
await prisma.coupon.update({
where: { code: data.couponCode.toUpperCase() },
data: { usedCount: { increment: 1 } }
});
}
catch (e) {
console.error("Failed to increment coupon usedCount:", e);
}
}
// Trigger LINE notification asynchronously
sendOrderCreatedNotification(order.id).catch(e => {
console.error("Failed to trigger sendOrderCreatedNotification:", e);
});
return order;
}
catch (err) {
console.error("[Orders] Error:", err);
set.status = 500;
return { error: "Failed to create order" };
}
}, {
body: t.Object({
customerId: t.Optional(t.Nullable(t.Number())),
customerName: t.String(),
customerEmail: t.String(),
customerPhone: t.String(),
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()),
quantity: t.Number(),
priceAtPurchase: t.Number(),
productName: t.String(),
variantSize: t.Optional(t.String()),
productImage: t.Optional(t.String())
}))
})
})
.patch("/:id/status", async ({ params: { id }, body, set }) => {
try {
const { status } = body;
const order = await prisma.order.findUnique({ where: { id: Number(id) } });
if (!order) {
set.status = 404;
return { error: "Order not found" };
}
if (status === 'PAID') {
await markOrderAsPaid(order.id);
}
else {
await prisma.order.update({
where: { id: order.id },
data: { status }
});
// If status becomes CANCELLED, refund the points used and notify
if (status === 'CANCELLED' && order.status !== 'CANCELLED') {
if (order.customerId && order.pointsUsed > 0) {
await prisma.customer.update({
where: { id: order.customerId },
data: { points: { increment: order.pointsUsed } }
});
}
sendOrderCancelledNotification(order.id).catch(e => console.error(e));
}
// If status becomes SHIPPED, notify
if (status === 'SHIPPED' && order.status !== 'SHIPPED') {
sendOrderShippedNotification(order.id).catch(e => console.error(e));
}
}
return await prisma.order.findUnique({
where: { id: order.id },
include: { items: true, customer: true }
});
}
catch (err) {
set.status = 500;
return { error: "Failed to update order status" };
}
}, {
body: t.Object({
status: t.String()
})
})
.patch("/:id/slip", async ({ params: { id }, body, set }) => {
try {
const { paymentSlipUrl } = body;
return await prisma.order.update({
where: { id: Number(id) },
data: { paymentSlipUrl },
include: { items: true }
});
}
catch (err) {
set.status = 500;
return { error: "Failed to upload slip" };
}
}, {
body: t.Object({
paymentSlipUrl: t.String()
})
})
.patch("/:id/tracking", async ({ params: { id }, body, set }) => {
try {
const { trackingNo } = body;
const order = await prisma.order.update({
where: { id: Number(id) },
data: { trackingNo, status: "SHIPPED" },
include: { items: true }
});
// Trigger notification asynchronously
sendOrderShippedNotification(order.id).catch(e => {
console.error("Failed to trigger sendOrderShippedNotification:", e);
});
return order;
}
catch (err) {
set.status = 500;
return { error: "Failed to update tracking number" };
}
}, {
body: t.Object({
trackingNo: t.String()
})
})
.get("/customer/:customerId", async ({ params: { customerId } }) => {
return await prisma.order.findMany({
where: { customerId: Number(customerId) },
orderBy: { createdAt: 'desc' },
include: { items: true }
});
})
.post("/track", async ({ body, set }) => {
try {
const { orderId, phone } = body;
const order = await prisma.order.findUnique({
where: { id: Number(orderId) }
});
if (!order) {
set.status = 404;
return { error: "Order not found" };
}
const sanitizePhone = (p) => p.replace(/[^0-9]/g, '');
if (sanitizePhone(order.customerPhone) !== sanitizePhone(phone)) {
set.status = 401;
return { error: "Authentication failed. Phone number does not match." };
}
return { success: true, orderId: order.id };
}
catch (err) {
set.status = 500;
return { error: "Invalid tracking request" };
}
}, {
body: t.Object({
orderId: t.Number(),
phone: t.String()
})
});
//# sourceMappingURL=orders.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"orders.js","sourceRoot":"","sources":["orders.ts"],"names":[],"mappings":"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 truncated
+151
View File
@@ -0,0 +1,151 @@
import { Elysia } from "elysia";
export declare const pagesRoute: Elysia<"/pages", {
decorator: any;
store: any;
derive: any;
resolve: any;
}, any, any, {
pages: {
get: {
body: unknown;
params: {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
} & {
pages: {
":slug": {
get: {
body: unknown;
params: {
slug: string;
} & {};
query: unknown;
headers: unknown;
response: {
422: {
type: "validation";
on: string;
summary?: string;
message?: string;
found?: unknown;
property?: string;
expected?: string;
};
};
};
};
};
} & {
pages: {
":slug": {
sections: {
get: {
body: unknown;
params: {
slug: string;
} & {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
};
};
} & {
pages: {
newsletter: {
post: {
body: {
email: string;
};
params: {};
query: unknown;
headers: unknown;
response: {
200: {
error: string;
success?: never;
data?: never;
} | {
success: boolean;
data: any;
error?: never;
};
422: {
type: "validation";
on: string;
summary?: string;
message?: string;
found?: unknown;
property?: string;
expected?: string;
};
};
};
};
};
} & {
pages: any;
} & {
pages: {
post: any;
};
} & {
pages: {
":slug": {
delete: any;
};
};
} & {
pages: {
":slug": {
sections: {
post: any;
};
};
};
} & {
pages: {
sections: {
":sectionId": {
put: any;
};
};
};
} & {
pages: {
":slug": {
sections: {
reorder: {
put: any;
};
};
};
};
} & {
pages: {
sections: {
":sectionId": {
delete: any;
};
};
};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}, any>;
//# sourceMappingURL=pages.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"pages.d.ts","sourceRoot":"","sources":["pages.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,QAAQ,CAAC;AAkBnC,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA0UnB,CAAC"}
+484
View File
@@ -0,0 +1,484 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
import { HeroArticlePayloadSchema, RichTextPayloadSchema, ScentNotesBreakdownPayloadSchema, QuoteCalloutPayloadSchema, ProductCarouselPayloadSchema } from "@homaroma/types";
import { adminOnly } from "../middleware/auth";
const RESERVED_SLUGS = [
"shop", "category", "products", "cart", "checkout", "account", "search",
"contact", "login", "admin", "wishlist", "track", "coupons", "scent-guide",
"faq", "about", "profile", "article", "pages", "orders", "customers",
"settings", "menus", "auth", "api"
];
export const pagesRoute = new Elysia({ prefix: "/pages" })
.get("/", async () => {
return await prisma.page.findMany({
include: {
sections: {
orderBy: { order: "asc" }
}
}
});
})
.get("/:slug", async ({ params: { slug }, query, set }) => {
const includeInactive = String(query?.includeInactive || "").toLowerCase() === "true";
const lang = query?.lang || 'th';
const page = await prisma.page.findUnique({
where: { slug },
include: {
sections: {
where: includeInactive ? undefined : { isActive: true },
orderBy: { order: "asc" }
}
}
});
if (!page) {
set.status = 404;
return { error: "Page not found" };
}
const populatedSections = await Promise.all(page.sections.map(async (section) => {
const payload = section.payload;
if (!payload)
return section;
const populatedPayload = { ...payload };
if (section.type === 'PRODUCTS') {
let products = [];
const limit = payload.limit || 3;
const productInclude = {
category: true,
scent: true,
translations: true,
variants: {
include: {
bomItems: { include: { rawMaterial: true } }
},
orderBy: { price: "asc" }
},
images: {
orderBy: { order: "asc" }
}
};
if (payload.type === 'featured') {
products = await prisma.product.findMany({
where: { categoryId: 1 },
include: productInclude,
take: limit
});
}
else if (payload.type === 'scent' && payload.scentId) {
products = await prisma.product.findMany({
where: { scentId: Number(payload.scentId) },
include: productInclude,
take: limit
});
}
else if (payload.type === 'custom' && Array.isArray(payload.productIds)) {
const ids = payload.productIds.map(Number);
products = await prisma.product.findMany({
where: { id: { in: ids } },
include: productInclude
});
products.sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id));
}
populatedPayload.products = products.map((p) => {
if (lang === 'all') {
const nameObj = {};
const descObj = {};
const detailsObj = {};
p.translations?.forEach((t) => {
nameObj[t.langCode] = t.name;
descObj[t.langCode] = t.description;
detailsObj[t.langCode] = t.details;
});
return { ...p, name: nameObj, description: descObj, details: detailsObj };
}
else {
const trans = p.translations?.find((t) => t.langCode === lang) || p.translations?.find((t) => t.langCode === 'th') || p.translations?.[0];
return { ...p, name: trans?.name || p.scent?.name || '', description: trans?.description, details: trans?.details };
}
});
}
else if (section.type === 'CATEGORIES') {
let categories = [];
if (Array.isArray(payload.categoryIds) && payload.categoryIds.length > 0) {
const ids = payload.categoryIds.map(Number);
categories = await prisma.category.findMany({
where: { id: { in: ids } },
include: {
_count: {
select: { products: true }
}
}
});
categories.sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id));
}
else {
categories = await prisma.category.findMany({
include: {
_count: {
select: { products: true }
}
}
});
}
populatedPayload.categories = categories;
}
return {
...section,
payload: populatedPayload
};
}));
return {
...page,
sections: populatedSections
};
})
.get("/:slug/sections", async ({ params: { slug }, set }) => {
try {
return await prisma.pageSection.findMany({
where: { pageId: Number(slug) },
orderBy: { order: "asc" }
});
}
catch (err) {
set.status = 500;
return { error: "Failed to fetch page sections" };
}
})
.post("/newsletter", async ({ body, set }) => {
try {
const { email } = body;
if (!email || !email.includes("@")) {
set.status = 400;
return { error: "Invalid email address" };
}
const sub = await prisma.newsletterSubscription.upsert({
where: { email },
update: {},
create: { email }
});
return { success: true, data: sub };
}
catch (err) {
console.error("[Newsletter] Error:", err);
set.status = 500;
return { error: "Failed to subscribe" };
}
}, {
body: t.Object({
email: t.String()
})
})
.use(adminOnly)
.post("/", async ({ body, set }) => {
try {
const data = body;
const { id, slug, title, isPublished, status, metadata } = data;
if (typeof slug === 'string' && RESERVED_SLUGS.includes(slug.toLowerCase())) {
set.status = 400;
return { error: "Reserved slug cannot be used" };
}
let finalIsPublished = true;
if (status !== undefined) {
finalIsPublished = status === "published";
}
else if (isPublished !== undefined) {
finalIsPublished = isPublished;
}
if (id) {
return await prisma.page.update({
where: { id: Number(id) },
data: { slug, title, isPublished: finalIsPublished }
});
}
else {
return await prisma.page.create({
data: { slug, title, isPublished: finalIsPublished }
});
}
}
catch (err) {
console.error("[Pages] Error:", err);
set.status = 500;
return { error: "Failed to save page" };
}
}, {
body: t.Object({
id: t.Optional(t.Number()),
slug: t.String(),
title: t.String(),
isPublished: t.Optional(t.Boolean()),
status: t.Optional(t.String()),
metadata: t.Optional(t.Any())
})
})
.delete("/:slug", async ({ params: { slug }, set }) => {
try {
await prisma.page.delete({
where: { id: Number(slug) }
});
return { success: true };
}
catch (err) {
set.status = 500;
return { error: "Failed to delete page" };
}
})
.post("/:slug/sections", async ({ params: { slug }, body, set }) => {
try {
const pageId = Number(slug);
const { type, order, payload, isActive } = body;
const validationError = validateSectionPayload(type, payload);
if (validationError) {
set.status = 400;
return { error: validationError };
}
let finalOrder = order;
if (finalOrder === undefined) {
const lastSection = await prisma.pageSection.findFirst({
where: { pageId },
orderBy: { order: "desc" }
});
finalOrder = lastSection ? lastSection.order + 1 : 1;
}
return await prisma.pageSection.create({
data: {
pageId,
type: type.toUpperCase(),
order: finalOrder,
payload,
isActive: isActive !== false
}
});
}
catch (err) {
console.error("[PageSections] Error:", err);
set.status = 500;
return { error: "Failed to add section" };
}
}, {
body: t.Object({
type: t.String(),
order: t.Optional(t.Number()),
payload: t.Any(),
isActive: t.Optional(t.Boolean())
})
})
.put("/sections/:sectionId", async ({ params: { sectionId }, body, set }) => {
try {
const id = Number(sectionId);
const { type, order, payload, isActive } = body;
if (payload && type) {
const validationError = validateSectionPayload(type, payload);
if (validationError) {
set.status = 400;
return { error: validationError };
}
}
return await prisma.pageSection.update({
where: { id },
data: {
type: type ? type.toUpperCase() : undefined,
order,
payload,
isActive
}
});
}
catch (err) {
console.error("[PageSections] Update Error:", err);
set.status = 500;
return { error: "Failed to update section" };
}
}, {
body: t.Object({
type: t.Optional(t.String()),
order: t.Optional(t.Number()),
payload: t.Optional(t.Any()),
isActive: t.Optional(t.Boolean())
})
})
.put("/:slug/sections/reorder", async ({ params: { slug }, body, set }) => {
try {
const pageId = Number(slug);
const { sectionOrders } = body;
await prisma.$transaction(sectionOrders.map((item) => prisma.pageSection.update({
where: { id: item.id, pageId },
data: { order: item.order }
})));
return { success: true };
}
catch (err) {
console.error("[PageSections] Reorder Error:", err);
set.status = 500;
return { error: "Failed to reorder sections" };
}
}, {
body: t.Object({
sectionOrders: t.Array(t.Object({
id: t.Number(),
order: t.Number()
}))
})
})
.delete("/sections/:sectionId", async ({ params: { sectionId }, set }) => {
try {
await prisma.pageSection.delete({
where: { id: Number(sectionId) }
});
return { success: true };
}
catch (err) {
set.status = 500;
return { error: "Failed to delete section" };
}
});
function validateSectionPayload(type, payload) {
if (!payload || typeof payload !== 'object') {
return "Payload must be a JSON object";
}
switch (type.toUpperCase()) {
case 'HERO':
if (typeof payload.title !== 'string' || !payload.title.trim())
return "Hero payload must contain a non-empty string title";
if (typeof payload.description !== 'string')
return "Hero payload must contain a string description";
if (typeof payload.imageUrl !== 'string' || !payload.imageUrl.trim())
return "Hero payload must contain a non-empty string imageUrl";
if (typeof payload.tagline !== 'string')
return "Hero payload must contain a string tagline";
if (typeof payload.buttonText !== 'string')
return "Hero payload must contain a string buttonText";
if (typeof payload.buttonLink !== 'string')
return "Hero payload must contain a string buttonLink";
break;
case 'CATEGORIES':
if (typeof payload.title !== 'string' || !payload.title.trim())
return "Categories payload must contain a non-empty string title";
if (payload.subtitle !== undefined && typeof payload.subtitle !== 'string')
return "Subtitle must be a string";
if (payload.categoryIds !== undefined && !Array.isArray(payload.categoryIds)) {
return "categoryIds must be an array of numbers";
}
break;
case 'PRODUCTS':
if (typeof payload.title !== 'string' || !payload.title.trim())
return "Products payload must contain a non-empty string title";
if (payload.subtitle !== undefined && typeof payload.subtitle !== 'string')
return "Subtitle must be a string";
if (!['featured', 'scent', 'custom'].includes(payload.type)) {
return "Products type must be 'featured', 'scent', or 'custom'";
}
if (payload.type === 'scent' && typeof payload.scentId !== 'number') {
return "Scent products section requires a numeric scentId";
}
if (payload.type === 'custom' && !Array.isArray(payload.productIds)) {
return "Custom products section requires an array of productIds";
}
if (payload.limit !== undefined && typeof payload.limit !== 'number') {
return "Limit must be a number";
}
if (payload.layout !== undefined && !['grid', 'carousel'].includes(payload.layout)) {
return "layout must be 'grid' or 'carousel'";
}
if (payload.enableQuickShop !== undefined && typeof payload.enableQuickShop !== 'boolean') {
return "enableQuickShop must be a boolean";
}
break;
case 'BANNER':
if (typeof payload.imageUrl !== 'string' || !payload.imageUrl.trim())
return "Banner payload must contain a non-empty string imageUrl";
if (payload.linkUrl !== undefined && typeof payload.linkUrl !== 'string')
return "linkUrl must be a string";
if (payload.title !== undefined && typeof payload.title !== 'string')
return "title must be a string";
if (payload.description !== undefined && typeof payload.description !== 'string')
return "description must be a string";
if (payload.buttonText !== undefined && typeof payload.buttonText !== 'string')
return "buttonText must be a string";
break;
case 'TEXT':
if (typeof payload.content !== 'string' || !payload.content.trim())
return "Text payload must contain a non-empty string content";
if (payload.title !== undefined && typeof payload.title !== 'string')
return "title must be a string";
if (payload.textAlign !== undefined && !['left', 'center', 'right'].includes(payload.textAlign)) {
return "textAlign must be 'left', 'center', or 'right'";
}
break;
case 'ANNOUNCEMENT_BAR':
if (!Array.isArray(payload.slides) || payload.slides.length === 0)
return "Announcement Bar requires an array of slides";
if (payload.slides.some((s) => typeof s !== 'string'))
return "All slides must be strings";
break;
case 'NEWSLETTER':
if (typeof payload.title !== 'string' || !payload.title.trim())
return "Newsletter payload must contain a non-empty string title";
if (typeof payload.description !== 'string')
return "Newsletter description must be a string";
if (typeof payload.buttonText !== 'string' || !payload.buttonText.trim())
return "Newsletter buttonText must be a non-empty string";
if (typeof payload.showPrivacyCheckbox !== 'boolean')
return "showPrivacyCheckbox must be a boolean";
break;
case 'EDITORIAL_TILE':
if (typeof payload.title !== 'string' || !payload.title.trim())
return "Editorial Tile payload must contain a non-empty string title";
if (typeof payload.description !== 'string')
return "Editorial Tile description must be a string";
if (typeof payload.imageUrl !== 'string' || !payload.imageUrl.trim())
return "Editorial Tile imageUrl must be a non-empty string";
if (!['left', 'right'].includes(payload.imagePosition))
return "imagePosition must be 'left' or 'right'";
break;
case 'EDITORIAL_CAROUSEL':
if (typeof payload.title !== 'string' || !payload.title.trim())
return "Editorial Carousel payload must contain a non-empty string title";
if (!Array.isArray(payload.slides) || payload.slides.length === 0)
return "Editorial Carousel requires an array of slides";
for (const [idx, slide] of payload.slides.entries()) {
if (typeof slide.title !== 'string' || !slide.title.trim())
return `Slide #${idx + 1} must contain a non-empty title`;
if (typeof slide.description !== 'string')
return `Slide #${idx + 1} must contain a description string`;
if (typeof slide.imageUrl !== 'string' || !slide.imageUrl.trim())
return `Slide #${idx + 1} must contain a non-empty imageUrl`;
if (typeof slide.category !== 'string')
return `Slide #${idx + 1} must contain a category string`;
}
break;
case 'HERO_ARTICLE': {
const res = HeroArticlePayloadSchema.safeParse(payload);
if (!res.success)
return res.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ');
break;
}
case 'RICH_TEXT': {
const res = RichTextPayloadSchema.safeParse(payload);
if (!res.success)
return res.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ');
break;
}
case 'SCENT_NOTES_BREAKDOWN': {
const res = ScentNotesBreakdownPayloadSchema.safeParse(payload);
if (!res.success)
return res.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ');
break;
}
case 'QUOTE_CALLOUT': {
const res = QuoteCalloutPayloadSchema.safeParse(payload);
if (!res.success)
return res.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ');
break;
}
case 'PRODUCT_CAROUSEL': {
const res = ProductCarouselPayloadSchema.safeParse(payload);
if (!res.success)
return res.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ');
break;
}
default:
return `Unknown section type: ${type}`;
}
return null;
}
//# sourceMappingURL=pages.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"pages.js","sourceRoot":"","sources":["pages.ts"],"names":[],"mappings":"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 truncated
+99
View File
@@ -0,0 +1,99 @@
import { Elysia } from "elysia";
export declare const paymentsRoute: Elysia<"/payments", {
decorator: {};
store: {};
derive: {};
resolve: {};
}, {
typebox: {};
error: {};
}, {
schema: {};
standaloneSchema: {};
macro: {};
macroFn: {};
parser: {};
response: {};
}, {
payments: {
charge: {
post: {
body: {
token?: string;
type: "card" | "promptpay";
orderId: number;
amount: number;
};
params: {};
query: unknown;
headers: unknown;
response: {
200: {
error: any;
success?: never;
status?: never;
chargeId?: never;
authorizeUri?: never;
qrCodeUrl?: never;
} | {
success: boolean;
status: any;
chargeId: any;
authorizeUri: any;
error?: never;
qrCodeUrl?: never;
} | {
success: boolean;
status: any;
chargeId: any;
qrCodeUrl: any;
error?: never;
authorizeUri?: never;
};
422: {
type: "validation";
on: string;
summary?: string;
message?: string;
found?: unknown;
property?: string;
expected?: string;
};
};
};
};
};
} & {
payments: {
webhook: {
post: {
body: unknown;
params: {};
query: unknown;
headers: unknown;
response: {
200: {
received: boolean;
error?: never;
} | {
error: string;
received?: never;
};
};
};
};
};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}>;
//# sourceMappingURL=payments.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"payments.d.ts","sourceRoot":"","sources":["payments.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,QAAQ,CAAC;AAMnC,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqKtB,CAAC"}
+152
View File
@@ -0,0 +1,152 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
import { markOrderAsPaid } from "../lib/notifications";
const OMISE_SECRET_KEY = process.env.OMISE_SECRET_KEY || "";
export const paymentsRoute = new Elysia({ prefix: "/payments" })
.post("/charge", async ({ body, set }) => {
console.log("[Payment Charge] Incoming request body:", JSON.stringify(body));
try {
const { orderId, amount, type, token } = body;
if (!OMISE_SECRET_KEY) {
console.error("[Payment Charge] Error: OMISE_SECRET_KEY is empty");
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();
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 markOrderAsPaid(orderId);
}
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();
console.log("[Payment Charge] Omise PromptPay Source response:", JSON.stringify(source));
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();
console.log("[Payment Charge] Omise PromptPay Charge response:", JSON.stringify(charge));
if (charge.error) {
set.status = 400;
return { error: charge.message || "Failed to create PromptPay charge" };
}
// Get QR Code Image URL from scannable_code or references
const qrCodeUrl = charge.source?.scannable_code?.image?.download_uri || charge.source?.references?.barcode || null;
return {
success: true,
status: charge.status,
chargeId: charge.id,
qrCodeUrl: qrCodeUrl
};
}
}
catch (err) {
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;
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 markOrderAsPaid(orderId);
}
}
return { received: true };
}
catch (err) {
console.error("[Omise Webhook] Error processing event:", err);
set.status = 500;
return { error: "Failed to process webhook" };
}
});
//# sourceMappingURL=payments.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"payments.js","sourceRoot":"","sources":["payments.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,MAAM,MAAM,eAAe,CAAC;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAEvD,MAAM,gBAAgB,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,EAAE,CAAC;AAE5D,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,MAAM,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;KAC7D,IAAI,CAAC,SAAS,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE;IACvC,OAAO,CAAC,GAAG,CAAC,yCAAyC,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;IAC7E,IAAI,CAAC;QACH,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,IAKxC,CAAC;QAEF,IAAI,CAAC,gBAAgB,EAAE,CAAC;YACtB,OAAO,CAAC,KAAK,CAAC,mDAAmD,CAAC,CAAC;YACnE,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;YACjB,OAAO,EAAE,KAAK,EAAE,oCAAoC,EAAE,CAAC;QACzD,CAAC;QAED,2BAA2B;QAC3B,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC;YAC1C,KAAK,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE;SACvB,CAAC,CAAC;QACH,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;YACjB,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC;QACtC,CAAC;QAED,qCAAqC;QACrC,MAAM,cAAc,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC;QAEhD,oCAAoC;QACpC,MAAM,UAAU,GAAG,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,gBAAgB,GAAG,GAAG,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAErF,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;YACpB,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;gBACjB,OAAO,EAAE,KAAK,EAAE,0CAA0C,EAAE,CAAC;YAC/D,CAAC;YAED,gCAAgC;YAChC,MAAM,SAAS,GAAG,MAAM,KAAK,CAAC,8BAA8B,EAAE;gBAC5D,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACP,eAAe,EAAE,UAAU;oBAC3B,cAAc,EAAE,kBAAkB;iBACnC;gBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;oBACnB,MAAM,EAAE,cAAc;oBACtB,QAAQ,EAAE,KAAK;oBACf,IAAI,EAAE,KAAK;oBACX,QAAQ,EAAE;wBACR,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC;qBACzB;iBACF,CAAC;aACH,CAAC,CAAC;YAEH,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,IAAI,EAAS,CAAC;YAE7C,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;gBACjB,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;gBACjB,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,OAAO,IAAI,qCAAqC,EAAE,CAAC;YAC5E,CAAC;YAED,2EAA2E;YAC3E,IAAI,MAAM,CAAC,MAAM,KAAK,YAAY,EAAE,CAAC;gBACnC,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;YACjC,CAAC;YAED,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,QAAQ,EAAE,MAAM,CAAC,EAAE;gBACnB,YAAY,EAAE,MAAM,CAAC,aAAa,IAAI,IAAI,CAAC,mCAAmC;aAC/E,CAAC;QACJ,CAAC;aAAM,CAAC;YACN,aAAa;YACb,mBAAmB;YACnB,MAAM,SAAS,GAAG,MAAM,KAAK,CAAC,8BAA8B,EAAE;gBAC5D,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACP,eAAe,EAAE,UAAU;oBAC3B,cAAc,EAAE,kBAAkB;iBACnC;gBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;oBACnB,IAAI,EAAE,WAAW;oBACjB,MAAM,EAAE,cAAc;oBACtB,QAAQ,EAAE,KAAK;iBAChB,CAAC;aACH,CAAC,CAAC;YACK,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,IAAI,EAAS,CAAC;YACrD,OAAO,CAAC,GAAG,CAAC,mDAAmD,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;YACzF,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;gBACjB,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;gBACjB,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,OAAO,IAAI,mCAAmC,EAAE,CAAC;YAC1E,CAAC;YAED,mBAAmB;YACnB,MAAM,SAAS,GAAG,MAAM,KAAK,CAAC,8BAA8B,EAAE;gBAC5D,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACP,eAAe,EAAE,UAAU;oBAC3B,cAAc,EAAE,kBAAkB;iBACnC;gBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;oBACnB,MAAM,EAAE,cAAc;oBACtB,QAAQ,EAAE,KAAK;oBACf,MAAM,EAAE,MAAM,CAAC,EAAE;oBACjB,QAAQ,EAAE;wBACR,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC;qBACzB;iBACF,CAAC;aACH,CAAC,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,IAAI,EAAS,CAAC;YAC7C,OAAO,CAAC,GAAG,CAAC,mDAAmD,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;YACzF,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;gBACjB,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;gBACjB,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,OAAO,IAAI,mCAAmC,EAAE,CAAC;YAC1E,CAAC;YAEO,0DAA0D;YAClE,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,IAAI,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,IAAI,IAAI,CAAC;YAEnH,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,QAAQ,EAAE,MAAM,CAAC,EAAE;gBACnB,SAAS,EAAE,SAAS;aACrB,CAAC;QACJ,CAAC;IACH,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,OAAO,CAAC,KAAK,CAAC,yBAAyB,EAAE,GAAG,CAAC,CAAC;QAC9C,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;QACjB,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,OAAO,IAAI,mCAAmC,EAAE,CAAC;IACvE,CAAC;AACH,CAAC,EAAE;IACD,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;QACb,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;QACnB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;QAClB,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;QAC1D,KAAK,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;KAC9B,CAAC;CACH,CAAC;KACD,IAAI,CAAC,UAAU,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE;IACxC,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,IAAW,CAAC;QAC1B,OAAO,CAAC,GAAG,CAAC,mCAAmC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC;QAE5D,IAAI,KAAK,CAAC,GAAG,KAAK,iBAAiB,EAAE,CAAC;YACpC,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC;YAC1B,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;YAC7B,MAAM,UAAU,GAAG,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC;YAE5C,IAAI,UAAU,IAAI,MAAM,KAAK,YAAY,EAAE,CAAC;gBAC1C,MAAM,OAAO,GAAG,QAAQ,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;gBACzC,OAAO,CAAC,GAAG,CAAC,iDAAiD,OAAO,EAAE,CAAC,CAAC;gBAExE,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;YACjC,CAAC;QACH,CAAC;QAED,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC5B,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,OAAO,CAAC,KAAK,CAAC,yCAAyC,EAAE,GAAG,CAAC,CAAC;QAC9D,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC;QACjB,OAAO,EAAE,KAAK,EAAE,2BAA2B,EAAE,CAAC;IAChD,CAAC;AACH,CAAC,CAAC,CAAC"}
+160
View File
@@ -0,0 +1,160 @@
import { Elysia } from "elysia";
export declare const productsRoute: Elysia<"/products", {
decorator: {};
store: {};
derive: {};
resolve: {};
}, {
typebox: {};
error: {};
}, {
schema: {};
standaloneSchema: {};
macro: {};
macroFn: {};
parser: {};
response: {};
}, {
products: {
scents: {
get: {
body: unknown;
params: {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
};
} & {
products: {
get: {
body: unknown;
params: {};
query: unknown;
headers: unknown;
response: {
200: {
data: any;
meta: {
total: any;
page: number;
limit: number;
lastPage: number;
};
};
};
};
};
} & {
products: {
":id": {
get: {
body: unknown;
params: {
id: string;
} & {};
query: unknown;
headers: unknown;
response: {
422: {
type: "validation";
on: string;
summary?: string;
message?: string;
found?: unknown;
property?: string;
expected?: string;
};
};
};
};
};
} & {
products: {
":id": {
put: {
body: any;
params: {
id: string;
} & {};
query: unknown;
headers: unknown;
response: {
200: {
success: boolean;
product: any;
};
400: {
readonly message: "Invalid product ID";
};
422: {
type: "validation";
on: string;
summary?: string;
message?: string;
found?: unknown;
property?: string;
expected?: string;
};
500: {
readonly success: false;
readonly message: "Failed to update product";
};
};
};
};
};
} & {
products: {
":id": {
patch: {
body: any;
params: {
id: string;
} & {};
query: unknown;
headers: unknown;
response: {
200: {
success: boolean;
product: any;
};
400: {
readonly message: "Invalid product ID";
};
422: {
type: "validation";
on: string;
summary?: string;
message?: string;
found?: unknown;
property?: string;
expected?: string;
};
500: {
readonly success: false;
readonly message: "Failed to patch product";
};
};
};
};
};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}>;
//# sourceMappingURL=products.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"products.d.ts","sourceRoot":"","sources":["products.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,QAAQ,CAAC;AAGnC,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA8RvB,CAAC"}
+279
View File
@@ -0,0 +1,279 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
export const productsRoute = new Elysia({ prefix: "/products" })
.get("/scents", async () => {
return await prisma.scent.findMany({
orderBy: { name: "asc" }
});
})
.get("/", async ({ query }) => {
const categoryId = query.categoryId ? parseInt(query.categoryId) : undefined;
const scentId = query.scentId ? parseInt(query.scentId) : undefined;
const search = query.search?.trim() || undefined;
const lang = query.lang || 'th';
// Pagination params
const page = query.page ? parseInt(query.page) : 1;
const limit = query.limit ? parseInt(query.limit) : 12;
const skip = (page - 1) * limit;
const where = {
...(categoryId && { categoryId }),
...(scentId && { scentId }),
...(search && {
OR: [
{ translations: { some: { name: { contains: search, mode: "insensitive" } } } },
{ scent: { name: { contains: search, mode: "insensitive" } } },
{ category: { name: { contains: search, mode: "insensitive" } } },
],
}),
};
const [products, total] = await Promise.all([
prisma.product.findMany({
where,
include: {
category: { include: { translations: true } },
scent: true,
translations: true,
variants: {
include: {
bomItems: { include: { rawMaterial: true } }
},
orderBy: { price: "asc" }
},
images: { orderBy: { order: "asc" } },
},
skip,
take: limit,
}),
prisma.product.count({ where }),
]);
// Map translations
const mappedProducts = products.map(p => {
if (lang === 'all') {
const nameObj = {};
const descObj = {};
const detailsObj = {};
p.translations.forEach(t => {
nameObj[t.langCode] = t.name;
descObj[t.langCode] = t.description;
detailsObj[t.langCode] = t.details;
});
return { ...p, name: nameObj, description: descObj, details: detailsObj, category: { ...p.category, name: p.category.name } }; // Actually for 'all', keep as is, but wait, category name needs resolving if we want it? Let's just resolve it simply below.
}
else {
const trans = p.translations.find(t => t.langCode === lang) || p.translations.find(t => t.langCode === 'th') || p.translations[0];
const catTrans = p.category.translations?.find((t) => t.langCode === lang) || p.category.translations?.find((t) => t.langCode === 'th') || p.category.translations?.[0];
return {
...p,
name: trans?.name || '',
description: trans?.description,
details: trans?.details,
category: { ...p.category, name: catTrans?.name || p.category.name }
};
}
});
mappedProducts.sort((a, b) => {
const aName = typeof a.name === 'string' ? a.name : (a.name?.th || '');
const bName = typeof b.name === 'string' ? b.name : (b.name?.th || '');
return aName.localeCompare(bName);
});
return {
data: mappedProducts,
meta: {
total,
page,
limit,
lastPage: Math.ceil(total / limit),
},
};
})
.get("/:id", async ({ params, query, status }) => {
const id = parseInt(params.id);
if (isNaN(id))
return status(400, { message: "Invalid product ID" });
const lang = query.lang || 'th';
const product = await prisma.product.findUnique({
where: { id },
include: {
category: { include: { translations: true } },
scent: true,
translations: true,
variants: {
include: {
bomItems: { include: { rawMaterial: true } }
},
orderBy: { price: "asc" }
},
images: { orderBy: { order: "asc" } },
},
});
if (!product)
return status(404, { message: "Product not found" });
if (lang === 'all') {
const nameObj = {};
const descObj = {};
const detailsObj = {};
product.translations.forEach(t => {
nameObj[t.langCode] = t.name;
descObj[t.langCode] = t.description;
detailsObj[t.langCode] = t.details;
});
return { ...product, name: nameObj, description: descObj, details: detailsObj };
}
else {
const trans = product.translations.find(t => t.langCode === lang) || product.translations.find(t => t.langCode === 'th') || product.translations[0];
const catTrans = product.category.translations?.find((t) => t.langCode === lang) || product.category.translations?.find((t) => t.langCode === 'th') || product.category.translations?.[0];
return {
...product,
name: trans?.name || '',
description: trans?.description,
details: trans?.details,
category: { ...product.category, name: catTrans?.name || product.category.name }
};
}
})
.put("/:id", async ({ params, body, status }) => {
const id = parseInt(params.id);
if (isNaN(id))
return status(400, { message: "Invalid product ID" });
const { categoryId, description, details, variants, images } = body;
try {
const updatedProduct = await prisma.$transaction(async (tx) => {
// 1. Update basic product info
const prod = await tx.product.update({
where: { id },
data: { categoryId },
});
// 2. Sync Variants
await tx.productVariant.deleteMany({
where: { productId: id },
});
if (variants && variants.length > 0) {
for (const v of variants) {
await tx.productVariant.create({
data: {
productId: id,
size: v.size,
price: v.price,
bomItems: v.bomItems && v.bomItems.length > 0 ? {
create: v.bomItems.map((b) => ({
rawMaterialId: b.rawMaterialId,
quantity: b.quantity,
}))
} : undefined
}
});
}
}
// 3. Sync Images
await tx.productImage.deleteMany({
where: { productId: id },
});
if (images && images.length > 0) {
for (const [idx, img] of images.entries()) {
await tx.productImage.create({
data: {
productId: id,
url: img.url,
order: img.order ?? (typeof img.order === 'number' ? img.order : idx),
}
});
}
}
return prod;
});
return { success: true, product: updatedProduct };
}
catch (err) {
console.error("Update product error:", err);
return status(500, { success: false, message: "Failed to update product" });
}
}, {
body: t.Any(),
detail: {
summary: "Update Product",
tags: ["products"],
},
})
.patch("/:id", async ({ params, body, status }) => {
const id = parseInt(params.id);
if (isNaN(id))
return status(400, { message: "Invalid product ID" });
try {
const payload = body;
const prodData = {};
if (payload.categoryId !== undefined)
prodData.categoryId = payload.categoryId;
if (payload.scentId !== undefined)
prodData.scentId = payload.scentId;
if (payload.price !== undefined)
prodData.price = payload.price;
const prod = await prisma.product.update({
where: { id },
data: prodData,
});
// Update translations
// If name, description, details are strings, treat as 'th'
// If they are objects, iterate over them
const languages = ['th', 'en', 'ja', 'zh', 'vi', 'ko'];
for (const lang of languages) {
const updateData = {};
let shouldUpdate = false;
if (payload.name) {
const val = typeof payload.name === 'string' ? (lang === 'th' ? payload.name : null) : payload.name[lang];
if (val !== undefined && val !== null) {
updateData.name = val;
shouldUpdate = true;
}
}
if (payload.description) {
const val = typeof payload.description === 'string' ? (lang === 'th' ? payload.description : null) : payload.description[lang];
if (val !== undefined && val !== null) {
updateData.description = val;
shouldUpdate = true;
}
}
if (payload.details) {
const val = typeof payload.details === 'string' ? (lang === 'th' ? payload.details : null) : payload.details[lang];
if (val !== undefined && val !== null) {
updateData.details = val;
shouldUpdate = true;
}
}
if (shouldUpdate) {
// First check if translation exists to create or update
const existing = await prisma.productTranslation.findUnique({
where: { productId_langCode: { productId: id, langCode: lang } }
});
if (existing) {
await prisma.productTranslation.update({
where: { id: existing.id },
data: updateData
});
}
else if (updateData.name) { // name is required for creation
await prisma.productTranslation.create({
data: {
productId: id,
langCode: lang,
name: updateData.name,
description: updateData.description,
details: updateData.details
}
});
}
}
}
return { success: true, product: prod };
}
catch (err) {
console.error("Patch product error:", err);
return status(500, { success: false, message: "Failed to patch product" });
}
}, {
body: t.Any(),
detail: {
summary: "Partial Update Product",
tags: ["products"],
},
});
//# sourceMappingURL=products.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"products.js","sourceRoot":"","sources":["products.ts"],"names":[],"mappings":"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 truncated
+211
View File
@@ -0,0 +1,211 @@
import { Elysia } from "elysia";
export declare const profileRoute: Elysia<"/profile", {
decorator: {};
store: {};
derive: {};
resolve: {};
}, {
typebox: {};
error: {};
}, {
schema: {};
standaloneSchema: {};
macro: {};
macroFn: {};
parser: {};
response: {};
}, {
profile: {
":customerId": {
get: {
body: unknown;
params: {
customerId: string;
} & {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
};
} & {
profile: {
":customerId": {
put: {
body: {
email?: string | null;
phone?: string | null;
};
params: {
customerId: string;
} & {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
};
} & {
profile: {
":customerId": {
addresses: {
get: {
body: unknown;
params: {
customerId: string;
} & {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
};
};
} & {
profile: {
":customerId": {
addresses: {
post: {
body: {
phone: string;
recipientName: string;
addressLine: string;
subdistrict: string;
district: string;
province: string;
zipcode: string;
isDefault: boolean;
};
params: {
customerId: string;
} & {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
};
};
} & {
profile: {
":customerId": {
addresses: {
":addressId": {
put: {
body: {
phone: string;
recipientName: string;
addressLine: string;
subdistrict: string;
district: string;
province: string;
zipcode: string;
isDefault: boolean;
};
params: {
customerId: string;
addressId: string;
} & {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
};
};
};
} & {
profile: {
":customerId": {
addresses: {
":addressId": {
delete: {
body: unknown;
params: {
customerId: string;
addressId: string;
} & {};
query: unknown;
headers: unknown;
response: {
200: {
success: boolean;
error?: never;
} | {
error: any;
success?: never;
};
422: {
type: "validation";
on: string;
summary?: string;
message?: string;
found?: unknown;
property?: string;
expected?: string;
};
};
};
};
};
};
};
} & {
profile: {
":customerId": {
addresses: {
":addressId": {
default: {
patch: {
body: unknown;
params: {
customerId: string;
addressId: string;
} & {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
};
};
};
};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}>;
//# sourceMappingURL=profile.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["profile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,QAAQ,CAAC;AAGnC,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA8JrB,CAAC"}
+161
View File
@@ -0,0 +1,161 @@
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;
const customer = await prisma.customer.update({
where: { id: Number(customerId) },
data: {
email: email || null,
phone: phone || null
}
});
return customer;
}
catch (err) {
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;
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) {
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;
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) {
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) {
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) {
set.status = 500;
return { error: err.message || "Failed to set default address" };
}
});
//# sourceMappingURL=profile.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"profile.js","sourceRoot":"","sources":["profile.ts"],"names":[],"mappings":"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 truncated
+141
View File
@@ -0,0 +1,141 @@
import { Elysia } from "elysia";
export declare const promotionsRoute: Elysia<"/promotions", {
decorator: {};
store: {};
derive: {};
resolve: {};
}, {
typebox: {};
error: {};
}, {
schema: {};
standaloneSchema: {};
macro: {};
macroFn: {};
parser: {};
response: {};
}, {
promotions: {
active: {
get: {
body: unknown;
params: {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
};
} & {
promotions: {
get: {
body: unknown;
params: {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
} & {
promotions: {
post: {
body: {
startDate?: string | null;
endDate?: string | null;
name: string;
type: string;
isActive: boolean;
value: number;
minPurchase: number;
level: number;
};
params: {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
} & {
promotions: {
":id": {
put: {
body: {
startDate?: string | null;
endDate?: string | null;
name: string;
type: string;
isActive: boolean;
value: number;
minPurchase: number;
level: number;
};
params: {
id: string;
} & {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
};
} & {
promotions: {
":id": {
delete: {
body: unknown;
params: {
id: string;
} & {};
query: unknown;
headers: unknown;
response: {
200: {
success: boolean;
error?: never;
} | {
error: string;
success?: never;
};
422: {
type: "validation";
on: string;
summary?: string;
message?: string;
found?: unknown;
property?: string;
expected?: string;
};
};
};
};
};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}>;
//# sourceMappingURL=promotions.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"promotions.d.ts","sourceRoot":"","sources":["promotions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,QAAQ,CAAC;AAGnC,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2ExB,CAAC"}
+82
View File
@@ -0,0 +1,82 @@
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' }
});
})
.get("/", async () => {
return await prisma.promotion.findMany({
orderBy: [{ level: 'asc' }, { createdAt: 'desc' }]
});
})
.post("/", async ({ body, set }) => {
try {
const data = body;
return await prisma.promotion.create({ data });
}
catch (err) {
set.status = 500;
return { error: "Failed to create promotion" };
}
}, {
body: t.Object({
name: t.String(),
level: t.Number(),
type: t.String(),
value: t.Number(),
minPurchase: t.Number(),
isActive: t.Boolean(),
startDate: t.Optional(t.Nullable(t.String())),
endDate: t.Optional(t.Nullable(t.String()))
})
})
.put("/:id", async ({ params: { id }, body, set }) => {
try {
const data = body;
return await prisma.promotion.update({
where: { id: Number(id) },
data
});
}
catch (err) {
set.status = 500;
return { error: "Failed to update promotion" };
}
}, {
body: t.Object({
name: t.String(),
level: t.Number(),
type: t.String(),
value: t.Number(),
minPurchase: t.Number(),
isActive: t.Boolean(),
startDate: t.Optional(t.Nullable(t.String())),
endDate: t.Optional(t.Nullable(t.String()))
})
})
.delete("/:id", async ({ params: { id }, set }) => {
try {
await prisma.promotion.delete({
where: { id: Number(id) }
});
return { success: true };
}
catch (err) {
set.status = 500;
return { error: "Failed to delete promotion" };
}
});
//# sourceMappingURL=promotions.js.map
@@ -0,0 +1 @@
{"version":3,"file":"promotions.js","sourceRoot":"","sources":["promotions.ts"],"names":[],"mappings":"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"}
+145
View File
@@ -0,0 +1,145 @@
import { Elysia } from "elysia";
export declare const rawMaterialsRoute: Elysia<"/raw-materials", {
decorator: {};
store: {};
derive: {};
resolve: {};
}, {
typebox: {};
error: {};
}, {
schema: {};
standaloneSchema: {};
macro: {};
macroFn: {};
parser: {};
response: {};
}, {
"raw-materials": {
get: {
body: unknown;
params: {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
} & {
"raw-materials": {
":id": {
get: {
body: unknown;
params: {
id: string;
} & {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
};
} & {
"raw-materials": {
post: {
body: {
name: string;
unit: string;
pricePerUnit: number;
};
params: {};
query: unknown;
headers: unknown;
response: {
201: any;
422: {
type: "validation";
on: string;
summary?: string;
message?: string;
found?: unknown;
property?: string;
expected?: string;
};
500: {
readonly message: "Failed to create raw material";
};
};
};
};
} & {
"raw-materials": {
":id": {
put: {
body: {
name: string;
unit: string;
pricePerUnit: number;
};
params: {
id: string;
} & {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
};
} & {
"raw-materials": {
":id": {
delete: {
body: unknown;
params: {
id: string;
} & {};
query: unknown;
headers: unknown;
response: {
200: {
success: boolean;
};
400: {
readonly message: "Invalid ID";
};
422: {
type: "validation";
on: string;
summary?: string;
message?: string;
found?: unknown;
property?: string;
expected?: string;
};
500: {
readonly message: "Failed to delete raw material";
};
};
};
};
};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}>;
//# sourceMappingURL=raw-materials.d.ts.map
@@ -0,0 +1 @@
{"version":3,"file":"raw-materials.d.ts","sourceRoot":"","sources":["raw-materials.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,QAAQ,CAAC;AAGnC,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyE1B,CAAC"}
+80
View File
@@ -0,0 +1,80 @@
import { Elysia, t } from "elysia";
import prisma from "../lib/prisma";
export const rawMaterialsRoute = new Elysia({ prefix: "/raw-materials" })
.get("/", async () => {
const rawMaterials = await prisma.rawMaterial.findMany({
orderBy: { name: "asc" },
});
return rawMaterials;
})
.get("/:id", async ({ params, status }) => {
const id = parseInt(params.id);
if (isNaN(id))
return status(400, { message: "Invalid ID" });
const rawMaterial = await prisma.rawMaterial.findUnique({
where: { id },
});
if (!rawMaterial)
return status(404, { message: "Raw material not found" });
return rawMaterial;
})
.post("/", async ({ body, status }) => {
try {
const rawMaterial = await prisma.rawMaterial.create({
data: body,
});
return status(201, rawMaterial);
}
catch (err) {
console.error(err);
return status(500, { message: "Failed to create raw material" });
}
}, {
body: t.Object({
name: t.String(),
unit: t.String(),
pricePerUnit: t.Number(),
}),
detail: { tags: ["raw-materials"] }
})
.put("/:id", async ({ params, body, status }) => {
const id = parseInt(params.id);
if (isNaN(id))
return status(400, { message: "Invalid ID" });
try {
const rawMaterial = await prisma.rawMaterial.update({
where: { id },
data: body,
});
return rawMaterial;
}
catch (err) {
console.error(err);
return status(500, { message: "Failed to update raw material" });
}
}, {
body: t.Object({
name: t.String(),
unit: t.String(),
pricePerUnit: t.Number(),
}),
detail: { tags: ["raw-materials"] }
})
.delete("/:id", async ({ params, status }) => {
const id = parseInt(params.id);
if (isNaN(id))
return status(400, { message: "Invalid ID" });
try {
await prisma.rawMaterial.delete({
where: { id },
});
return { success: true };
}
catch (err) {
console.error(err);
return status(500, { message: "Failed to delete raw material" });
}
}, {
detail: { tags: ["raw-materials"] }
});
//# sourceMappingURL=raw-materials.js.map
@@ -0,0 +1 @@
{"version":3,"file":"raw-materials.js","sourceRoot":"","sources":["raw-materials.ts"],"names":[],"mappings":"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"}
+70
View File
@@ -0,0 +1,70 @@
import { Elysia } from "elysia";
export declare const reviewsRoute: Elysia<"/reviews", {
decorator: {};
store: {};
derive: {};
resolve: {};
}, {
typebox: {};
error: {};
}, {
schema: {};
standaloneSchema: {};
macro: {};
macroFn: {};
parser: {};
response: {};
}, {
reviews: {
product: {
":productId": {
get: {
body: unknown;
params: {
productId: string;
} & {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
};
};
} & {
reviews: {
post: {
body: {
customerName?: string;
customerId?: number | null;
comment?: string;
productId: number;
rating: number;
};
params: {};
query: unknown;
headers: unknown;
response: {
[x: string]: any;
[x: number]: any;
[x: symbol]: any;
};
};
};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}, {
derive: {};
resolve: {};
schema: {};
standaloneSchema: {};
response: {};
}>;
//# sourceMappingURL=reviews.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"reviews.d.ts","sourceRoot":"","sources":["reviews.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAK,MAAM,QAAQ,CAAC;AAGnC,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA6CrB,CAAC"}
Loaded 100 of 123 files, more files were not shown because too many files have changed in this diff. Show more