diff --git a/src/models/cart.model.ts b/src/models/cart.model.ts index 569c2f0..90b62f9 100644 --- a/src/models/cart.model.ts +++ b/src/models/cart.model.ts @@ -1,25 +1,18 @@ import { type Product } from "./product.model"; -import { type User } from "./user.model"; -export interface CartItem { - id: number; +import type { + Cart as PrismaCart, + CartItem as PrismaCartItem, +} from "generated/prisma/client"; + +export type CartItem = PrismaCartItem & { product: Pick< Product, "id" | "title" | "imgSrc" | "alt" | "price" | "isOnSale" >; - quantity: number; - createdAt: string; - updatedAt: string; -} +}; -export interface Cart { - id: number; - userId: User["id"] | null; - sessionCartId: string; - items: CartItem[]; - createdAt: string; - updatedAt: string; -} +export type Cart = PrismaCart; export interface CartItemInput { productId: Product["id"]; @@ -28,3 +21,21 @@ export interface CartItemInput { price: Product["price"]; imgSrc: Product["imgSrc"]; } + +// Tipo para representar un producto simplificado en el carrito + +export type CartProductInfo = Pick< + Product, + "id" | "title" | "imgSrc" | "alt" | "price" | "isOnSale" +>; + +// Tipo para representar un item de carrito con su producto +export type CartItemWithProduct = { + product: CartProductInfo; + quantity: number; +}; + +// Tipo para el carrito con items y productos incluidos +export type CartWithItems = Cart & { + items: CartItem[]; +}; diff --git a/src/models/product.model.ts b/src/models/product.model.ts index 2f56d20..6f46237 100644 --- a/src/models/product.model.ts +++ b/src/models/product.model.ts @@ -1,22 +1,5 @@ -// import { type Category } from "./category.model"; - import type { Product as PrismaProduct } from "generated/prisma/client"; -// export interface Product { -// id: number; -// title: string; -// imgSrc: string; -// alt: string | null; -// price: number; -// description: string | null; -// categoryId: number; -// // categorySlug: Category["slug"]; -// isOnSale: boolean; -// features: string[]; -// createdAt: string; -// updatedAt: string; -// } - export type Product = Omit & { price: number; }; diff --git a/src/repositories/cart.repository.ts b/src/repositories/cart.repository.ts deleted file mode 100644 index d8c76de..0000000 --- a/src/repositories/cart.repository.ts +++ /dev/null @@ -1,224 +0,0 @@ -import * as db from "@/db"; -import { type Cart, type CartItem } from "@/models/cart.model"; - -export async function getCart( - userId: number | undefined, - sessionCartId: string | undefined, - id?: number -): Promise { - let whereClause: string; - let paramValue: number | string; - - if (userId) { - whereClause = "WHERE c.user_id = $1"; - paramValue = userId; - } else if (sessionCartId) { - whereClause = "WHERE c.session_cart_id = $1"; - paramValue = sessionCartId; - } else if (id) { - whereClause = "WHERE c.id = $1"; - paramValue = id; - } else { - // Si no se proporciona ningún identificador, devolvemos null - return null; - } - - const query = ` - SELECT - c.*, - COALESCE( - ( - SELECT json_agg( - json_build_object( - 'id', ci.id, - 'quantity', ci.quantity, - 'product', ( - SELECT json_build_object( - 'id', p.id, - 'title', p.title, - 'imgSrc', p.img_src, - 'alt', p.alt, - 'price', p.price, - 'isOnSale', p.is_on_sale - ) - FROM products p - WHERE p.id = ci.product_id - ), - 'createdAt', ci.created_at, - 'updatedAt', ci.updated_at - ) - ORDER BY ci.id ASC - ) - FROM cart_items ci - LEFT JOIN products pr on pr.id = ci.product_id - WHERE ci.cart_id = c.id - )::json, - '[]'::json - ) as items - FROM carts c - ${whereClause} - `; - return await db.queryOne(query, [paramValue]); -} - -export async function createCart(): Promise { - const query = "INSERT INTO carts DEFAULT VALUES RETURNING *"; - const cart = await db.queryOne(query); - return getCart(undefined, undefined, cart?.id); -} - -// export async function createGuestCart(sessionCartId: string): Promise { // new function -// const query = "INSERT INTO carts (session_cart_id) VALUES ($1) RETURNING *"; -// return db.queryOne(query, [sessionCartId]); -// } - -export async function addCartItem( - cartId: number, - productId: number, - quantity: number -): Promise { - const query = ` - INSERT INTO cart_items (cart_id, product_id, quantity) - VALUES ($1, $2, $3) - RETURNING * - `; - - return await db.queryOne(query, [cartId, productId, quantity]); -} - -export async function addCartItems( - cartId: number, - items: { productId: number; quantity: number }[] | [] -): Promise { - // Si no hay elementos para agregar, retornar un array vacío - if (items.length === 0) { - return []; - } - - const valuesClause = items - .map((_, i) => `($1, $${i * 2 + 2}, $${i * 2 + 3})`) - .join(","); - - const query = ` - INSERT INTO cart_items (cart_id, product_id, quantity) - VALUES ${valuesClause} - RETURNING * - `; - - const values = items.reduce( - (acc, item) => { - acc.push(item.productId, item.quantity); - return acc; - }, - [cartId] as (string | number)[] - ); - - return await db.query(query, values); -} - -export async function updateCartItem( - cartId: number, - itemId: number, - quantity: number -): Promise { - const query = - "UPDATE cart_items SET quantity = $1 WHERE id = $2 AND cart_id = $3 RETURNING *"; - - return await db.queryOne(query, [quantity, itemId, cartId]); -} - -export async function removeCartItem( - cartId: number, - itemId: number -): Promise { - const query = "DELETE FROM cart_items WHERE id = $1 AND cart_id = $2"; - await db.query(query, [itemId, cartId]); -} - -export async function clearCart(cartId: number): Promise { - const query = "DELETE FROM carts WHERE id = $1"; - await db.query(query, [cartId]); -} - -export async function updateCartWithUserId( - cartId: number, - userId: number -): Promise { - const query = ` - UPDATE carts - SET user_id = $2 - WHERE id = $1 - RETURNING * - `; - - return await db.queryOne(query, [cartId, userId]); -} - -export async function updateCartBySessionId( - sessionCartId: string, - userId: number -): Promise { - const query = ` - UPDATE carts - SET user_id = $2 - WHERE session_cart_id = $1 - RETURNING * - `; - - return await db.queryOne(query, [sessionCartId, userId]); -} - -export async function mergeGuestCartWithUserCart( - userId: number | undefined, - sessionCartId: string -): Promise { - // Primero, obtenemos el carrito del usuario y el carrito de invitado - const userCart = await getCart(userId, undefined); - const guestCart = await getCart(undefined, sessionCartId); - - if (!guestCart) { - return userCart; - } - - if (!userCart) { - // Si el usuario no tiene carrito, actualizamos el carrito de invitado con el ID del usuario - const query = ` - UPDATE carts - SET user_id = $1 - WHERE session_cart_id = $2 - RETURNING * - `; - return await db.queryOne(query, [userId, sessionCartId]); - } - - // Eliminamos productos del carrito usuario que también existan en el carrito invitado - await db.query( - ` - DELETE FROM cart_items - WHERE cart_id = $1 - AND product_id IN ( - SELECT product_id FROM cart_items WHERE cart_id = $2 - ) - `, - [userCart.id, guestCart.id] - ); - - // Insertamos los artículos del carrito invitado al carrito usuario - const query = ` - INSERT INTO cart_items (cart_id, product_id, quantity) - SELECT $1, product_id, quantity - FROM cart_items - WHERE cart_id = $2 - RETURNING * - `; - - await db.query(query, [userCart.id, guestCart.id]); - - // Eliminamos el carrito de invitado - await db.query(`DELETE FROM carts WHERE session_cart_id = $1`, [ - sessionCartId, - ]); - - // Devolvemos el carrito actualizado del usuario - return await getCart(userId, undefined); -} diff --git a/src/routes/root/index.tsx b/src/routes/root/index.tsx index 99be149..99b4610 100644 --- a/src/routes/root/index.tsx +++ b/src/routes/root/index.tsx @@ -17,7 +17,7 @@ import { Separator, } from "@/components/ui"; import { getCart } from "@/lib/cart"; -import type { Cart } from "@/models/cart.model"; +import type { CartWithItems } from "@/models/cart.model"; import { getCurrentUser } from "@/services/auth.service"; import { createRemoteItems } from "@/services/cart.service"; import { commitSession, getSession } from "@/session.server"; @@ -45,7 +45,7 @@ export async function action({ request }: Route.ActionArgs) { export async function loader({ request }: Route.LoaderArgs) { const session = await getSession(request.headers.get("Cookie")); const sessionCartId = session.get("sessionCartId"); - let cart: Cart | null = null; + let cart: CartWithItems | null = null; // Obtenemos el usuario actual (autenticado o no) const user = await getCurrentUser(request); diff --git a/src/services/cart.service.ts b/src/services/cart.service.ts index c0c8b86..f742706 100644 --- a/src/services/cart.service.ts +++ b/src/services/cart.service.ts @@ -1,62 +1,142 @@ -import { type Cart, type CartItem } from "@/models/cart.model"; +import { prisma } from "@/db/prisma"; +import type { CartItemWithProduct, CartWithItems } from "@/models/cart.model"; import type { User } from "@/models/user.model"; -import * as cartRepository from "@/repositories/cart.repository"; import { getSession } from "@/session.server"; -export async function getRemoteCart(userId: User["id"]): Promise { - const cart = await cartRepository.getCart(userId, undefined); - return cart; +// Función para obtener un carrito con sus ítems +async function getCart( + userId?: number, + sessionCartId?: string, + id?: number +): Promise { + const whereCondition = userId + ? { userId } + : sessionCartId + ? { sessionCartId } + : id + ? { id } + : undefined; + + if (!whereCondition) return null; + + const data = await prisma.cart.findFirst({ + where: whereCondition, + include: { + items: { + include: { + product: { + select: { + id: true, + title: true, + imgSrc: true, + alt: true, + price: true, + isOnSale: true, + }, + }, + }, + orderBy: { + id: "asc", + }, + }, + }, + }); + + if (!data) return null; + + return { + ...data, + items: data.items.map((item) => ({ + ...item, + product: { + ...item.product, + price: item.product.price.toNumber(), + }, + })), + }; +} + +export async function getRemoteCart( + userId: User["id"] +): Promise { + return await getCart(userId); } export async function getOrCreateCart( userId: User["id"] | undefined, sessionCartId: string | undefined -) { - let cart: Cart | null = null; +): Promise { + const cart = await getCart(userId, sessionCartId); - cart = await cartRepository.getCart(userId, sessionCartId); - - // Si no se encontró un carrito creamos uno nuevo - if (!cart) { - // Creamos un carrito sin userId ni sessionCartId, dejando que la BD genere el UUID - cart = await cartRepository.createCart(); - // Si se crea el carrito, lo vinculamos a un usuario si se proporciona un userId - if (cart && userId) { - await cartRepository.updateCartWithUserId(cart.id, userId); - } + if (cart) { + return cart; } - if (!cart) throw new Error("Failed to create cart"); + // Si no se encontró un carrito creamos uno nuevo - return cart; + // Creamos un carrito con userId si se proporciona + const newCart = await prisma.cart.create({ + data: { + userId: userId || null, + }, + include: { + items: { + include: { + product: { + select: { + id: true, + title: true, + imgSrc: true, + alt: true, + price: true, + isOnSale: true, + }, + }, + }, + }, + }, + }); + + if (!newCart) throw new Error("Failed to create cart"); + + return { + ...newCart, + items: newCart.items.map((item) => ({ + ...item, + product: { + ...item.product, + price: item.product.price.toNumber(), + }, + })), + }; } export async function createRemoteItems( userId: User["id"] | undefined, sessionCartId: string | undefined, - items: CartItem[] = [] -): Promise { - const mappedItems = items.map(({ product, quantity }) => ({ - productId: product.id, - quantity, - })); - + items: CartItemWithProduct[] = [] +): Promise { const cart = await getOrCreateCart(userId, sessionCartId); + // Eliminar todos los ítems existentes en el carrito if (cart.items.length > 0) { - await cartRepository.clearCart(cart.id); + await prisma.cartItem.deleteMany({ + where: { cartId: cart.id }, + }); } // Si hay elementos para agregar, agregarlos if (items.length > 0) { - await cartRepository.addCartItems(cart.id, mappedItems); + await prisma.cartItem.createMany({ + data: items.map((item) => ({ + cartId: cart.id, + productId: item.product.id, + quantity: item.quantity, + })), + }); } - const updatedCart = await cartRepository.getCart( - userId, - sessionCartId, - cart.id - ); + const updatedCart = await getCart(userId, sessionCartId, cart.id); if (!updatedCart) throw new Error("Cart not found after creation"); @@ -68,7 +148,7 @@ export async function alterQuantityCartItem( sessionCartId: string | undefined, productId: number, quantity: number = 1 -): Promise { +): Promise { const cart = await getOrCreateCart(userId, sessionCartId); const existingItem = cart.items.find((item) => item.product.id === productId); @@ -78,16 +158,25 @@ export async function alterQuantityCartItem( if (newQuantity <= 0) throw new Error("Cannot set item quantity to 0 or less"); - await cartRepository.updateCartItem(cart.id, existingItem.id, newQuantity); + await prisma.cartItem.update({ + where: { + id: existingItem.id, + }, + data: { + quantity: newQuantity, + }, + }); } else { - await cartRepository.addCartItem(cart.id, productId, quantity); + await prisma.cartItem.create({ + data: { + cartId: cart.id, + productId, + quantity, + }, + }); } - const updatedCart = await cartRepository.getCart( - userId, - cart.sessionCartId, - cart.id - ); + const updatedCart = await getCart(userId, cart.sessionCartId, cart.id); if (!updatedCart) throw new Error("Cart not found after update"); @@ -97,17 +186,18 @@ export async function alterQuantityCartItem( export async function deleteRemoteCartItem( userId: User["id"] | undefined, sessionCartId: string | undefined, - itemId: CartItem["id"] -): Promise { - let cart: Cart | null = null; - - if (userId || sessionCartId) { - cart = await cartRepository.getCart(userId, sessionCartId); - } + itemId: number +): Promise { + const cart = await getCart(userId, sessionCartId); if (!cart) throw new Error("Cart not found"); - await cartRepository.removeCartItem(cart.id, itemId); + await prisma.cartItem.delete({ + where: { + id: itemId, + cartId: cart.id, + }, + }); const updatedCart = await getOrCreateCart(userId, sessionCartId); return updatedCart; @@ -118,43 +208,136 @@ export async function deleteRemoteCart(request: Request): Promise { const sessionCartId = session.get("sessionCartId"); const userId = session.get("userId"); - let cart: Cart | null = null; - - if (userId || sessionCartId) { - cart = await cartRepository.getCart(userId, sessionCartId); - } + const cart = await getCart(userId, sessionCartId); if (!cart) throw new Error("Cart not found"); - await cartRepository.clearCart(cart.id); + + // Eliminar todos los items del carrito primero + // await prisma.cartItem.deleteMany({ + // where: { cartId: cart.id }, + // }); + + // Luego eliminar el carrito + await prisma.cart.delete({ + where: { id: cart.id }, + }); } export async function linkCartToUser( userId: User["id"], sessionCartId: string -): Promise { +): Promise { if (!sessionCartId) throw new Error("Session cart ID not found"); if (!userId) throw new Error("User ID not found"); - const updatedCart = await cartRepository.updateCartBySessionId( - sessionCartId, - userId - ); + const updatedCart = await prisma.cart.update({ + where: { sessionCartId }, + data: { userId }, + include: { + items: { + include: { + product: { + select: { + id: true, + title: true, + imgSrc: true, + alt: true, + price: true, + isOnSale: true, + }, + }, + }, + }, + }, + }); if (!updatedCart) throw new Error("Cart not found after linking"); - return updatedCart; + return { + ...updatedCart, + items: updatedCart.items.map((item) => ({ + ...item, + product: { + ...item.product, + price: item.product.price.toNumber(), + }, + })), + }; } export async function mergeGuestCartWithUserCart( userId: User["id"], sessionCartId: string -): Promise { - const mergedCart = await cartRepository.mergeGuestCartWithUserCart( - userId, - sessionCartId - ); +): Promise { + // Obtener el carrito de usuario y el carrito de invitado + const userCart = await getCart(userId); + const guestCart = await getCart(undefined, sessionCartId); + + if (!guestCart) { + return userCart; + } - if (!mergedCart) throw new Error("Cart not found after merging"); + if (!userCart) { + // Si el usuario no tiene carrito, actualizamos el carrito de invitado con el ID del usuario + const updatedCart = await prisma.cart.update({ + where: { sessionCartId }, + data: { userId }, + include: { + items: { + include: { + product: { + select: { + id: true, + title: true, + imgSrc: true, + alt: true, + price: true, + isOnSale: true, + }, + }, + }, + }, + }, + }); + return { + ...updatedCart, + items: updatedCart.items.map((item) => ({ + ...item, + product: { + ...item.product, + price: item.product.price.toNumber(), + }, + })), + }; + } - return mergedCart; + // Obtener productos duplicados para eliminarlos del carrito del usuario + const guestProductIds = guestCart.items.map((item) => item.productId); + + // Eliminar productos del carrito usuario que también existan en el carrito invitado + await prisma.cartItem.deleteMany({ + where: { + cartId: userCart.id, + productId: { + in: guestProductIds, + }, + }, + }); + + // Mover los items del carrito de invitado al carrito de usuario + await prisma.cartItem.createMany({ + data: guestCart.items.map((item) => ({ + cartId: userCart.id, + productId: item.productId, + quantity: item.quantity, + })), + }); + + // Eliminar el carrito de invitado + await prisma.cart.delete({ + where: { id: guestCart.id }, + }); + + // Devolver el carrito actualizado del usuario + return await getCart(userId); } diff --git a/src/services/order.service.test.ts b/src/services/order.service.test.ts index 35e9ffe..f6bcf52 100644 --- a/src/services/order.service.test.ts +++ b/src/services/order.service.test.ts @@ -29,7 +29,6 @@ vi.mock("@/db/prisma", () => ({ vi.mock("./user.service"); vi.mock("@/lib/cart"); -vi.mock("@/repositories/order.repository"); vi.mock("@/session.server"); describe("Order Service", () => { diff --git a/src/services/product.service.ts b/src/services/product.service.ts index 43ce73f..3406570 100644 --- a/src/services/product.service.ts +++ b/src/services/product.service.ts @@ -31,7 +31,6 @@ export async function getProductById(id: number): Promise { } export async function getAllProducts(): Promise { - // No la utilizamos en repository. return (await prisma.product.findMany()).map((p) => ({ ...p, price: p.price.toNumber(), diff --git a/src/services/user.service.test.ts b/src/services/user.service.test.ts index 1362c66..dc69ff3 100644 --- a/src/services/user.service.test.ts +++ b/src/services/user.service.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { prisma } from "@/db/prisma"; import { hashPassword } from "@/lib/security"; import { createMockSession, @@ -7,7 +8,7 @@ import { createTestUser, } from "@/lib/utils.tests"; import { getSession } from "@/session.server"; -import { prisma } from "@/db/prisma"; + import * as userService from "./user.service"; // Mocking dependencies for unit tests @@ -98,7 +99,6 @@ describe("user service", () => { id: 10, }); - // Mock repository function to return existing user vi.mocked(prisma.user.findUnique).mockResolvedValue(existingUser); // Call service function @@ -118,7 +118,7 @@ describe("user service", () => { id: 20, isGuest: true, }); - // Mock repository functions + vi.mocked(prisma.user.findUnique).mockResolvedValue(null); vi.mocked(prisma.user.create).mockResolvedValue(newUser); // Call service function