notshop-bundle/server/routes.ts

640 lines
29 KiB
TypeScript

import type { Express, Request, Response } from "express";
import { db } from "./db";
import {
products, productVariants, categories, brands,
customers, addresses, paymentMethods,
carts, cartItems,
orders, orderItems, orderEvents,
discountCodes, adminUsers,
} from "../shared/schema";
import {
eq, and, desc, asc, ilike, inArray, sql, or,
} from "drizzle-orm";
import bcrypt from "bcrypt";
import crypto from "crypto";
import { chargeOpaqueData, refundTransaction, createCustomerProfile, chargeCustomerProfile } from "./payments/authorizenet";
import { createPaymentIntent, verifyPaymentIntent, isStripeConfigured, refundPaymentIntent } from "./payments/stripe";
import { sendOrderConfirmation, sendShippingNotification } from "./email";
import { requestMagicLink, verifyMagicLink, requireCustomer, requireAdmin, logout, me } from "./auth";
function generateOrderNumber(): string {
return `NS-${Date.now().toString(36).toUpperCase()}`;
}
function toCents(value: string | number): number {
return Math.round(parseFloat(String(value)) * 100);
}
export function registerRoutes(app: Express) {
// ── Config (public) ──────────────────────────────────────────────────────
app.get("/api/config", (_req, res) => {
res.json({
storeName: process.env.STORE_NAME || "My Store",
authorizeNetClientKey: process.env.AUTHORIZE_NET_PUBLIC_CLIENT_KEY || null,
authorizeNetApiLoginId: process.env.AUTHORIZE_NET_API_LOGIN_ID || null,
authorizeNetSandbox: process.env.AUTHORIZE_NET_SANDBOX !== "false",
stripePublishableKey: process.env.STRIPE_PUBLISHABLE_KEY || null,
stripeEnabled: isStripeConfigured(),
});
});
// ── Auth ──────────────────────────────────────────────────────────────────
app.post("/api/auth/magic-link", requestMagicLink);
app.get("/api/auth/verify", verifyMagicLink);
app.post("/api/auth/logout", logout);
app.get("/api/auth/me", me);
// Admin login
app.post("/api/admin/login", async (req: Request, res: Response) => {
const { email, password } = req.body as { email?: string; password?: string };
if (!email || !password) return res.status(400).json({ error: "Email and password required" });
const [admin] = await db.select().from(adminUsers).where(eq(adminUsers.email, email.toLowerCase()));
if (!admin || !admin.passwordHash) return res.status(401).json({ error: "Invalid credentials" });
const ok = await bcrypt.compare(password, admin.passwordHash);
if (!ok) return res.status(401).json({ error: "Invalid credentials" });
(req.session as any).adminUserId = admin.id;
res.json({ ok: true, admin: { id: admin.id, email: admin.email, name: admin.name, role: admin.role } });
});
app.post("/api/admin/logout", (req: Request, res: Response) => {
req.session.destroy(() => res.json({ ok: true }));
});
app.get("/api/admin/me", requireAdmin, async (req: Request, res: Response) => {
const [admin] = await db.select({ id: adminUsers.id, email: adminUsers.email, name: adminUsers.name, role: adminUsers.role })
.from(adminUsers).where(eq(adminUsers.id, (req.session as any).adminUserId));
res.json({ admin: admin || null });
});
// ── Products (public) ─────────────────────────────────────────────────────
app.get("/api/products", async (req: Request, res: Response) => {
const { q, category, brand, tag, limit = "48", page = "1" } = req.query as Record<string, string>;
const pageSize = Math.min(parseInt(limit) || 48, 100);
const offset = (parseInt(page) - 1) * pageSize;
const conditions = [eq(products.status, "active")];
if (q) conditions.push(ilike(products.title, `%${q}%`));
if (category) {
const [cat] = await db.select({ id: categories.id }).from(categories).where(eq(categories.slug, category));
if (cat) conditions.push(eq(products.categoryId, cat.id));
}
if (brand) {
const [b] = await db.select({ id: brands.id }).from(brands).where(eq(brands.slug, brand));
if (b) conditions.push(eq(products.brandId, b.id));
}
const rows = await db
.select()
.from(products)
.where(and(...conditions))
.orderBy(desc(products.createdAt))
.limit(pageSize)
.offset(offset);
const [{ count }] = await db
.select({ count: sql<number>`count(*)` })
.from(products)
.where(and(...conditions));
res.json({ products: rows, total: Number(count), page: parseInt(page), pageSize });
});
app.get("/api/products/:handle", async (req: Request, res: Response) => {
const [product] = await db.select().from(products).where(eq(products.handle, req.params.handle));
if (!product) return res.status(404).json({ error: "Not found" });
const variants = await db.select().from(productVariants)
.where(eq(productVariants.productId, product.id))
.orderBy(asc(productVariants.position));
res.json({ product, variants });
});
app.get("/api/categories", async (_req, res) => {
const rows = await db.select().from(categories).orderBy(asc(categories.position));
res.json(rows);
});
app.get("/api/brands", async (_req, res) => {
const rows = await db.select().from(brands).orderBy(asc(brands.name));
res.json(rows);
});
// ── Cart ──────────────────────────────────────────────────────────────────
async function getOrCreateCart(req: Request) {
const sessionId = req.sessionID;
const customerId = (req.session as any).customerId || null;
let [cart] = await db.select().from(carts)
.where(and(eq(carts.sessionId, sessionId), eq(carts.status, "active")));
if (!cart) {
[cart] = await db.insert(carts).values({ sessionId, customerId, status: "active" }).returning();
}
return cart;
}
app.get("/api/cart", async (req: Request, res: Response) => {
const cart = await getOrCreateCart(req);
const items = await db.select().from(cartItems).where(eq(cartItems.cartId, cart.id));
res.json({ cart, items });
});
app.post("/api/cart/items", async (req: Request, res: Response) => {
const { productId, variantId, quantity = 1 } = req.body as {
productId?: number; variantId?: number; quantity?: number;
};
if (!productId) return res.status(400).json({ error: "productId required" });
const [product] = await db.select().from(products).where(eq(products.id, productId));
if (!product) return res.status(404).json({ error: "Product not found" });
let price = product.price;
let variantTitle: string | undefined;
let imageUrl: string | undefined;
if (variantId) {
const [variant] = await db.select().from(productVariants).where(eq(productVariants.id, variantId));
if (variant) { price = variant.price; variantTitle = variant.title; imageUrl = variant.imageUrl || undefined; }
}
if (!imageUrl && Array.isArray(product.images) && product.images.length > 0) {
imageUrl = product.images[0] as string;
}
const cart = await getOrCreateCart(req);
const [existing] = await db.select().from(cartItems)
.where(and(eq(cartItems.cartId, cart.id), eq(cartItems.productId, productId),
variantId ? eq(cartItems.variantId, variantId) : sql`${cartItems.variantId} IS NULL`));
if (existing) {
const [updated] = await db.update(cartItems)
.set({ quantity: existing.quantity + (quantity || 1) })
.where(eq(cartItems.id, existing.id))
.returning();
return res.json(updated);
}
const [item] = await db.insert(cartItems).values({
cartId: cart.id,
productId,
variantId,
title: product.title,
variantTitle,
price,
quantity: quantity || 1,
imageUrl,
productHandle: product.handle,
}).returning();
res.json(item);
});
app.patch("/api/cart/items/:id", async (req: Request, res: Response) => {
const { quantity } = req.body as { quantity?: number };
if (quantity === undefined) return res.status(400).json({ error: "quantity required" });
if (quantity <= 0) {
await db.delete(cartItems).where(eq(cartItems.id, parseInt(req.params.id)));
return res.json({ deleted: true });
}
const [item] = await db.update(cartItems).set({ quantity }).where(eq(cartItems.id, parseInt(req.params.id))).returning();
res.json(item);
});
app.delete("/api/cart/items/:id", async (req: Request, res: Response) => {
await db.delete(cartItems).where(eq(cartItems.id, parseInt(req.params.id)));
res.json({ ok: true });
});
app.post("/api/cart/discount", async (req: Request, res: Response) => {
const { code } = req.body as { code?: string };
if (!code) return res.status(400).json({ error: "Code required" });
const [discount] = await db.select().from(discountCodes)
.where(and(eq(discountCodes.code, code.toUpperCase()), eq(discountCodes.active, true)));
if (!discount) return res.status(404).json({ error: "Invalid or expired discount code" });
if (discount.expiresAt && new Date() > new Date(discount.expiresAt)) {
return res.status(400).json({ error: "Discount code has expired" });
}
if (discount.maxUses !== null && discount.usedCount >= discount.maxUses!) {
return res.status(400).json({ error: "Discount code has reached its usage limit" });
}
const cart = await getOrCreateCart(req);
await db.update(carts).set({ discountCode: code.toUpperCase() }).where(eq(carts.id, cart.id));
res.json({ ok: true, discount: { code: discount.code, type: discount.type, value: discount.value } });
});
app.delete("/api/cart/discount", async (req: Request, res: Response) => {
const cart = await getOrCreateCart(req);
await db.update(carts).set({ discountCode: null, discountAmount: null }).where(eq(carts.id, cart.id));
res.json({ ok: true });
});
// ── Checkout / Orders ─────────────────────────────────────────────────────
// Authorize.net: create PaymentIntent-equivalent (nonce is submitted client-side via Accept.js)
// The frontend gets the opaque data and POSTs it here to complete the charge.
app.post("/api/checkout/authorizenet", async (req: Request, res: Response) => {
const {
dataDescriptor, dataValue,
email, firstName, lastName,
shippingAddress, billingAddress,
saveCard = false,
} = req.body as {
dataDescriptor?: string;
dataValue?: string;
email?: string;
firstName?: string;
lastName?: string;
shippingAddress?: any;
billingAddress?: any;
saveCard?: boolean;
};
if (!dataDescriptor || !dataValue || !email) {
return res.status(400).json({ error: "Payment data and email are required" });
}
const cart = await getOrCreateCart(req);
const items = await db.select().from(cartItems).where(eq(cartItems.cartId, cart.id));
if (!items.length) return res.status(400).json({ error: "Cart is empty" });
let subtotal = items.reduce((sum, i) => sum + parseFloat(i.price) * i.quantity, 0);
let discountAmount = 0;
if (cart.discountCode) {
const [discount] = await db.select().from(discountCodes)
.where(and(eq(discountCodes.code, cart.discountCode), eq(discountCodes.active, true)));
if (discount) {
if (discount.type === "percentage") {
discountAmount = subtotal * (parseFloat(discount.value) / 100);
} else {
discountAmount = parseFloat(discount.value);
}
discountAmount = Math.min(discountAmount, subtotal);
}
}
const tax = 0;
const shipping = 0;
const total = Math.max(0, subtotal - discountAmount + tax + shipping);
const amountCents = toCents(total);
const normalEmail = email.toLowerCase().trim();
let [customer] = await db.select().from(customers).where(eq(customers.email, normalEmail));
if (!customer) {
[customer] = await db.insert(customers)
.values({ email: normalEmail, firstName, lastName })
.returning();
}
const chargeResult = await chargeOpaqueData({
dataDescriptor,
dataValue,
amountCents,
email: normalEmail,
description: `Order from ${process.env.STORE_NAME || "store"}`,
billingAddress: billingAddress || shippingAddress,
});
if (!chargeResult.success) {
return res.status(402).json({ error: chargeResult.error || "Payment declined" });
}
if (saveCard && customer) {
try {
const profileResult = await createCustomerProfile({
email: normalEmail,
dataDescriptor,
dataValue,
});
if (profileResult.success && profileResult.profileId) {
await db.update(customers).set({ authNetProfileId: profileResult.profileId }).where(eq(customers.id, customer.id));
if (profileResult.paymentProfileId) {
await db.insert(paymentMethods).values({
customerId: customer.id,
source: "authorizenet",
authNetPaymentProfileId: profileResult.paymentProfileId,
isDefault: true,
});
}
}
} catch (_) {}
}
const orderNumber = generateOrderNumber();
const [order] = await db.insert(orders).values({
orderNumber,
customerId: customer.id,
email: normalEmail,
status: "confirmed",
paymentStatus: "paid",
paymentProcessor: "authorizenet",
subtotal: subtotal.toFixed(2),
tax: tax.toFixed(2),
shipping: shipping.toFixed(2),
discountCode: cart.discountCode || undefined,
discountAmount: discountAmount > 0 ? discountAmount.toFixed(2) : "0.00",
total: total.toFixed(2),
authNetTransactionId: chargeResult.transactionId,
shippingAddress,
billingAddress: billingAddress || shippingAddress,
paymentVerified: true,
}).returning();
await db.insert(orderItems).values(items.map((i) => ({
orderId: order.id,
productId: i.productId,
productHandle: i.productHandle || undefined,
variantId: i.variantId || undefined,
title: i.title,
variantTitle: i.variantTitle || undefined,
quantity: i.quantity,
price: i.price,
total: (parseFloat(i.price) * i.quantity).toFixed(2),
imageUrl: i.imageUrl || undefined,
})));
await db.insert(orderEvents).values({
orderId: order.id,
type: "order_placed",
body: `Payment captured via Authorize.net. Transaction ID: ${chargeResult.transactionId}`,
actor: "system",
});
if (cart.discountCode) {
await db.update(discountCodes)
.set({ usedCount: sql`${discountCodes.usedCount} + 1` })
.where(eq(discountCodes.code, cart.discountCode));
}
await db.update(carts).set({ status: "converted" }).where(eq(carts.id, cart.id));
await sendOrderConfirmation({
email: normalEmail,
orderNumber,
total: total.toFixed(2),
items: items.map((i) => ({ title: i.title, variantTitle: i.variantTitle || undefined, quantity: i.quantity, price: i.price })),
shippingAddress,
});
res.json({ ok: true, orderNumber, orderId: order.id });
});
// Stripe: create a PaymentIntent and return clientSecret to the frontend
app.post("/api/checkout/stripe/intent", async (req: Request, res: Response) => {
if (!isStripeConfigured()) return res.status(501).json({ error: "Stripe is not configured" });
const { email } = req.body as { email?: string };
if (!email) return res.status(400).json({ error: "Email required" });
const cart = await getOrCreateCart(req);
const items = await db.select().from(cartItems).where(eq(cartItems.cartId, cart.id));
if (!items.length) return res.status(400).json({ error: "Cart is empty" });
const total = items.reduce((sum, i) => sum + parseFloat(i.price) * i.quantity, 0);
const result = await createPaymentIntent({ amountCents: toCents(total), email, metadata: { cartId: String(cart.id) } });
if (!result.success) return res.status(500).json({ error: result.error });
res.json({ clientSecret: result.clientSecret, paymentIntentId: result.paymentIntentId });
});
// Stripe: confirm order after payment succeeded on client
app.post("/api/checkout/stripe/confirm", async (req: Request, res: Response) => {
const { paymentIntentId, email, shippingAddress, billingAddress, firstName, lastName } = req.body as {
paymentIntentId?: string; email?: string; shippingAddress?: any; billingAddress?: any;
firstName?: string; lastName?: string;
};
if (!paymentIntentId || !email) return res.status(400).json({ error: "paymentIntentId and email required" });
const verify = await verifyPaymentIntent(paymentIntentId);
if (!verify.success) return res.status(402).json({ error: "Payment not confirmed" });
const cart = await getOrCreateCart(req);
const items = await db.select().from(cartItems).where(eq(cartItems.cartId, cart.id));
if (!items.length) return res.status(400).json({ error: "Cart is empty" });
const subtotal = items.reduce((sum, i) => sum + parseFloat(i.price) * i.quantity, 0);
const total = subtotal;
const normalEmail = email.toLowerCase().trim();
let [customer] = await db.select().from(customers).where(eq(customers.email, normalEmail));
if (!customer) {
[customer] = await db.insert(customers).values({ email: normalEmail, firstName, lastName }).returning();
}
const orderNumber = generateOrderNumber();
const [order] = await db.insert(orders).values({
orderNumber,
customerId: customer.id,
email: normalEmail,
status: "confirmed",
paymentStatus: "paid",
paymentProcessor: "stripe",
subtotal: subtotal.toFixed(2),
tax: "0.00",
shipping: "0.00",
total: total.toFixed(2),
stripePaymentIntentId: paymentIntentId,
shippingAddress,
billingAddress: billingAddress || shippingAddress,
paymentVerified: true,
}).returning();
await db.insert(orderItems).values(items.map((i) => ({
orderId: order.id, productHandle: i.productHandle || undefined, variantId: i.variantId || undefined,
title: i.title, variantTitle: i.variantTitle || undefined, quantity: i.quantity,
price: i.price, total: (parseFloat(i.price) * i.quantity).toFixed(2), imageUrl: i.imageUrl || undefined,
})));
await db.update(carts).set({ status: "converted" }).where(eq(carts.id, cart.id));
await sendOrderConfirmation({ email: normalEmail, orderNumber, total: total.toFixed(2), items, shippingAddress });
res.json({ ok: true, orderNumber, orderId: order.id });
});
// ── Customer Account ──────────────────────────────────────────────────────
app.get("/api/account/orders", requireCustomer, async (req: Request, res: Response) => {
const customerId = (req.session as any).customerId;
const rows = await db.select().from(orders)
.where(eq(orders.customerId, customerId))
.orderBy(desc(orders.createdAt));
res.json(rows);
});
app.get("/api/account/orders/:id", requireCustomer, async (req: Request, res: Response) => {
const customerId = (req.session as any).customerId;
const [order] = await db.select().from(orders)
.where(and(eq(orders.id, parseInt(req.params.id)), eq(orders.customerId, customerId)));
if (!order) return res.status(404).json({ error: "Not found" });
const items = await db.select().from(orderItems).where(eq(orderItems.orderId, order.id));
res.json({ order, items });
});
app.patch("/api/account/profile", requireCustomer, async (req: Request, res: Response) => {
const customerId = (req.session as any).customerId;
const { firstName, lastName, phone } = req.body as { firstName?: string; lastName?: string; phone?: string };
const [updated] = await db.update(customers)
.set({ ...(firstName !== undefined && { firstName }), ...(lastName !== undefined && { lastName }), ...(phone !== undefined && { phone }) })
.where(eq(customers.id, customerId)).returning();
res.json(updated);
});
// ── Admin: Products ───────────────────────────────────────────────────────
app.get("/api/admin/products", requireAdmin, async (req: Request, res: Response) => {
const { q, limit = "50", page = "1" } = req.query as Record<string, string>;
const pageSize = parseInt(limit);
const offset = (parseInt(page) - 1) * pageSize;
const conditions = q ? [ilike(products.title, `%${q}%`)] : [];
const rows = await db.select().from(products).where(conditions.length ? and(...conditions) : undefined)
.orderBy(desc(products.createdAt)).limit(pageSize).offset(offset);
const [{ count }] = await db.select({ count: sql<number>`count(*)` }).from(products)
.where(conditions.length ? and(...conditions) : undefined);
res.json({ products: rows, total: Number(count) });
});
app.post("/api/admin/products", requireAdmin, async (req: Request, res: Response) => {
const [product] = await db.insert(products).values(req.body).returning();
res.json(product);
});
app.patch("/api/admin/products/:id", requireAdmin, async (req: Request, res: Response) => {
const [product] = await db.update(products).set({ ...req.body, updatedAt: new Date() })
.where(eq(products.id, parseInt(req.params.id))).returning();
res.json(product);
});
app.delete("/api/admin/products/:id", requireAdmin, async (req: Request, res: Response) => {
await db.delete(products).where(eq(products.id, parseInt(req.params.id)));
res.json({ ok: true });
});
// ── Admin: Orders ─────────────────────────────────────────────────────────
app.get("/api/admin/orders", requireAdmin, async (req: Request, res: Response) => {
const { status, q, limit = "50", page = "1" } = req.query as Record<string, string>;
const pageSize = parseInt(limit);
const offset = (parseInt(page) - 1) * pageSize;
const conditions: any[] = [];
if (status) conditions.push(eq(orders.status, status));
if (q) conditions.push(or(ilike(orders.orderNumber, `%${q}%`), ilike(orders.email, `%${q}%`)));
const rows = await db.select().from(orders).where(conditions.length ? and(...conditions) : undefined)
.orderBy(desc(orders.createdAt)).limit(pageSize).offset(offset);
const [{ count }] = await db.select({ count: sql<number>`count(*)` }).from(orders)
.where(conditions.length ? and(...conditions) : undefined);
res.json({ orders: rows, total: Number(count) });
});
app.get("/api/admin/orders/:id", requireAdmin, async (req: Request, res: Response) => {
const [order] = await db.select().from(orders).where(eq(orders.id, parseInt(req.params.id)));
if (!order) return res.status(404).json({ error: "Not found" });
const items = await db.select().from(orderItems).where(eq(orderItems.orderId, order.id));
const events = await db.select().from(orderEvents).where(eq(orderEvents.orderId, order.id)).orderBy(asc(orderEvents.createdAt));
res.json({ order, items, events });
});
app.patch("/api/admin/orders/:id", requireAdmin, async (req: Request, res: Response) => {
const { fulfillmentStatus, trackingNumber, trackingCarrier, status, notes } = req.body as {
fulfillmentStatus?: string; trackingNumber?: string; trackingCarrier?: string; status?: string; notes?: string;
};
const updates: Record<string, any> = { updatedAt: new Date() };
if (fulfillmentStatus) updates.fulfillmentStatus = fulfillmentStatus;
if (trackingNumber) updates.trackingNumber = trackingNumber;
if (trackingCarrier) updates.trackingCarrier = trackingCarrier;
if (status) updates.status = status;
if (notes !== undefined) updates.notes = notes;
const [order] = await db.update(orders).set(updates).where(eq(orders.id, parseInt(req.params.id))).returning();
if (trackingNumber && order.email) {
await sendShippingNotification({ email: order.email, orderNumber: order.orderNumber, trackingNumber, trackingCarrier });
}
if (Object.keys(req.body).length > 0) {
await db.insert(orderEvents).values({
orderId: order.id, type: "admin_update",
body: Object.entries(req.body).map(([k, v]) => `${k}: ${v}`).join(", "), actor: "admin",
});
}
res.json(order);
});
app.post("/api/admin/orders/:id/refund", requireAdmin, async (req: Request, res: Response) => {
const [order] = await db.select().from(orders).where(eq(orders.id, parseInt(req.params.id)));
if (!order) return res.status(404).json({ error: "Not found" });
const amountCents = req.body.amountCents ? parseInt(req.body.amountCents) : toCents(order.total);
let result: { success: boolean; error?: string };
if (order.authNetTransactionId) {
result = await refundTransaction({
transactionId: order.authNetTransactionId,
amountCents,
cardLast4: req.body.cardLast4 || "0000",
});
} else if (order.stripePaymentIntentId) {
result = await refundPaymentIntent({ paymentIntentId: order.stripePaymentIntentId, amountCents });
} else {
return res.status(400).json({ error: "No transaction ID on record for this order" });
}
if (!result.success) return res.status(502).json({ error: result.error });
await db.update(orders).set({ paymentStatus: "refunded", updatedAt: new Date() }).where(eq(orders.id, order.id));
await db.insert(orderEvents).values({
orderId: order.id, type: "refunded",
body: `Refunded $${(amountCents / 100).toFixed(2)}`, actor: "admin",
});
res.json({ ok: true });
});
// ── Admin: Customers ──────────────────────────────────────────────────────
app.get("/api/admin/customers", requireAdmin, async (req: Request, res: Response) => {
const { q, limit = "50", page = "1" } = req.query as Record<string, string>;
const pageSize = parseInt(limit);
const offset = (parseInt(page) - 1) * pageSize;
const conditions = q
? [or(ilike(customers.email, `%${q}%`), ilike(customers.firstName, `%${q}%`), ilike(customers.lastName, `%${q}%`))]
: [];
const rows = await db.select().from(customers).where(conditions.length ? and(...conditions) : undefined)
.orderBy(desc(customers.createdAt)).limit(pageSize).offset(offset);
res.json(rows);
});
// ── Admin: Discounts ──────────────────────────────────────────────────────
app.get("/api/admin/discounts", requireAdmin, async (_req, res) => {
const rows = await db.select().from(discountCodes).orderBy(desc(discountCodes.createdAt));
res.json(rows);
});
app.post("/api/admin/discounts", requireAdmin, async (req: Request, res: Response) => {
const [code] = await db.insert(discountCodes).values({
...req.body,
code: req.body.code?.toUpperCase(),
}).returning();
res.json(code);
});
app.patch("/api/admin/discounts/:id", requireAdmin, async (req: Request, res: Response) => {
const [code] = await db.update(discountCodes).set(req.body).where(eq(discountCodes.id, parseInt(req.params.id))).returning();
res.json(code);
});
app.delete("/api/admin/discounts/:id", requireAdmin, async (req: Request, res: Response) => {
await db.delete(discountCodes).where(eq(discountCodes.id, parseInt(req.params.id)));
res.json({ ok: true });
});
// ── Admin: Dashboard Stats ────────────────────────────────────────────────
app.get("/api/admin/stats", requireAdmin, async (_req, res) => {
const [orderStats] = await db.select({
totalOrders: sql<number>`count(*)`,
totalRevenue: sql<number>`coalesce(sum(cast(total as numeric)), 0)`,
}).from(orders).where(eq(orders.paymentStatus, "paid"));
const [customerCount] = await db.select({ count: sql<number>`count(*)` }).from(customers);
const [productCount] = await db.select({ count: sql<number>`count(*)` }).from(products).where(eq(products.status, "active"));
const recentOrders = await db.select().from(orders).orderBy(desc(orders.createdAt)).limit(5);
res.json({
totalOrders: Number(orderStats.totalOrders),
totalRevenue: Number(orderStats.totalRevenue),
totalCustomers: Number(customerCount.count),
activeProducts: Number(productCount.count),
recentOrders,
});
});
}