payload:
address	"Address"
city	"Baghdad"
closest_point	"Shap"
coupon_id	null
full_name	"Mustafa Kareem"
governorate	"baghdad"
items	[…]
0	{…}
color_id	null
product_id	875
quantity	1
size_id	1976
1	{…}
color_id	null
product_id	903
quantity	1
size_id	2067
notes	""
phone_number	"07752546762"
type	"home"
response:
{"errors":[{"type":"field","value":"home","msg":"Invalid value","path":"type","location":"body"},{"type":"field","value":null,"msg":"Invalid value","path":"coupon_id","location":"body"},{"type":"field","msg":"Product ID must be an integer","path":"items..product_id","location":"body"},{"type":"field","msg":"Quantity must be greater than zero","path":"items..quantity","location":"body"}]}

// routes/cartRoutes.ts
import express from 'express';
import { body } from 'express-validator';
import { validateRequest } from '../middleware/validateRequest';
import { isAuthenticated } from '../middleware/isAuthenticated';
import * as cartController from '../controllers/cartController';

const router = express.Router();

// Create a new cart with items
router.post(
    '/',
    [
        // Validate cart fields
        body('notes').optional().isString(),
        body('full_name').optional().isString(),
        body('governorate').optional().isString(),
        body('city').optional().isString(),
        body('address').optional().isString(),
        body('closest_point').optional().isString(),
        body('phone_number').optional().isString(),
        body('type').optional().isIn(['Home', 'Work', 'Other']),
        body('coupon_id').optional().isInt(),

        // Validate items
        body('items').isArray({ min: 1 }).withMessage('Items array is required'),
        body('items..product_id').isInt().withMessage('Product ID must be an integer'),
        body('items..size_id').optional().isInt(),
        body('items..color_id').optional().isInt(),
        body('items..quantity').isInt({ gt: 0 }).withMessage('Quantity must be greater than zero'),
    ],
    validateRequest,
    cartController.createCart
);

router.get(
    '/',
    isAuthenticated,
    cartController.getCart
);

export default router;
import { query } from '../config/database';

// Interfaces for TypeScript typing
export interface Cart {
    id: number;
    user_id?: number | null;
    status: string;
    notes?: string;
    full_name?: string;
    governorate?: string;
    city?: string;
    address?: string;
    closest_point?: string;
    phone_number?: string;
    type?: string;
    created_at: Date;
    coupon_id?: number | null;
}

export interface CartItem {
    id: number;
    cart_id: number;
    product_id: number;
    size_id?: number | null;
    color_id?: number | null;
    quantity: number;
}

// Function to create a cart with items in a single transaction
export async function createCartWithItems(
    cartData: Partial<Cart>,
    items: Partial<CartItem>[]
): Promise<{ cart: Cart; items: CartItem[] }> {
    try {
        await query('BEGIN');

        // Insert into carts table
        const cartResult = await query<Cart>(
            `INSERT INTO carts (
        user_id, status, notes, full_name, governorate, city, address, closest_point, phone_number, type, coupon_id
      ) VALUES (
        $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
      ) RETURNING *`,
            [
                cartData.user_id,
                cartData.status || 'Pending',
                cartData.notes,
                cartData.full_name,
                cartData.governorate,
                cartData.city,
                cartData.address,
                cartData.closest_point,
                cartData.phone_number,
                cartData.type,
                cartData.coupon_id,
            ]
        );

        const cart = cartResult.rows[0];
        if (!cart) {
            throw new Error('Failed to create cart');
        }

        // Insert cart items
        const cartItems: CartItem[] = [];
        const insertItemText = `INSERT INTO cart_items (
      cart_id, product_id, size_id, color_id, quantity
    ) VALUES ($1, $2, $3, $4, $5) RETURNING *`;

        for (const item of items) {
            const itemResult = await query<CartItem>(insertItemText, [
                cart.id,
                item.product_id,
                item.size_id,
                item.color_id,
                item.quantity,
            ]);
            cartItems.push(itemResult.rows[0]);
        }

        await query('COMMIT');
        return { cart, items: cartItems };
    } catch (error) {
        await query('ROLLBACK');
        console.error('Error creating cart with items:', error);
        throw error;
    }
}

export async function getCartWithItemsByUserId(
    userId: number
): Promise<{ cart: Cart; items: CartItem[] } | null> {
    // Get the most recent cart for the user
    const cartResult = await query<Cart>(
        `SELECT * FROM carts WHERE user_id = $1 ORDER BY created_at DESC LIMIT 1`,
        [userId]
    );

    const cart = cartResult.rows[0];
    if (!cart) {
        return null;
    }

    // Get the cart items
    const itemsResult = await query<CartItem>(
        `SELECT * FROM cart_items WHERE cart_id = $1`,
        [cart.id]
    );

    return { cart, items: itemsResult.rows };
}
// controllers/cartController.ts
import { Request, Response, NextFunction } from 'express';
import * as cartModel from '../models/cartModel';

export async function createCart(req: Request, res: Response, next: NextFunction) {
    try {
        const userId: any = req.user?.id || null;

        const {
            status,
            notes,
            full_name,
            governorate,
            city,
            address,
            closest_point,
            phone_number,
            type,
            coupon_id,
            items,
        } = req.body;

        if (!items || !Array.isArray(items) || items.length === 0) {
            return res.status(400).json({ message: 'Items are required to create a cart' });
        }

        // Prepare cart data
        const cartData = {
            user_id: userId,
            status: status || 'Pending',
            notes,
            full_name,
            governorate,
            city,
            address,
            closest_point,
            phone_number,
            type,
            coupon_id,
        };

        // Create cart with items
        const { cart, items: cartItems } = await cartModel.createCartWithItems(cartData, items);

        res.status(201).json({ cart, items: cartItems });
    } catch (error) {
        next(error);
    }
}

export async function getCart(req: Request, res: Response, next: NextFunction) {
    try {
        const userId = req.user?.id;
        if (!userId) {
            return res.status(401).json({ message: 'Unauthorized' });
        }

        const cartWithItems = await cartModel.getCartWithItemsByUserId(userId);
        if (!cartWithItems) {
            return res.status(404).json({ message: 'No cart found for the user' });
        }

        res.json(cartWithItems);
    } catch (error) {
        next(error);
    }
}
import express from 'express';
import morgan from 'morgan';
import dotenv from 'dotenv';
import cookieParser from 'cookie-parser';
import cors from 'cors';
import compression from 'compression';
import { errorHandler } from './middleware/errorHandler';
import categoryRoutes from './routes/categoryRoutes';
import subcategoryRoutes from './routes/subcategoryRoutes';
import productRoutes from "./routes/productRoutes";
import userRoutes from "./routes/userRoutes";
import counterRoutes from "./routes/counterRoutes";
import addressRoutes from "./routes/addressRoutes";
import couponRoutes from "./routes/couponRoutes";
import cartRoutes from "./routes/cartRoutes";

dotenv.config();

const app = express();

app.use(cors())
app.use(compression());

app.use(express.json());
app.use(cookieParser());
app.use(morgan('dev'));

app.set('trust proxy', 1);

// Middleware to add caching headers
const addCacheHeaders = (maxAge: number) => (req: express.Request, res: express.Response, next: express.NextFunction) => {
    res.set('Cache-Control', `public, s-maxage=${maxAge}, stale-while-revalidate`);
    res.set('CDN-Cache-Control', `public, s-maxage=${maxAge}, stale-while-revalidate`);
    res.set('Vercel-CDN-Cache-Control', `public, s-maxage=${maxAge}, stale-while-revalidate`);
    next();
};

// Routes with caching
app.use('/user', addCacheHeaders(84600), userRoutes); // Cache for 1 minute
app.use('/counter', counterRoutes);
app.use('/categories', addCacheHeaders(84600), categoryRoutes); // Cache for 1 hour
app.use('/subcategories', addCacheHeaders(84600), subcategoryRoutes); // Cache for 1 hour
app.use('/products', productRoutes); // Cache for 10 minutes
app.use('/addresses', addCacheHeaders(84600), addressRoutes); // Cache for 1 hour
app.use('/coupons', addCacheHeaders(84600), couponRoutes); // Cache for 1 hour
app.use('/cart', cartRoutes);

app.use(errorHandler);

const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});

export default app;