02. Modelando Relaciones: De Visitante a Cliente Fiel
Una base de datos mal diseñada es como un cimiento débil para un edificio: tarde o temprano la estructura colapsará. En el e-commerce, el diseño de la base de datos no solo determina cómo se almacenan los productos, sino también qué tan fluida y flexible puede ser la experiencia de compra del usuario.
En este artículo, analizaremos el esquema de datos de DevMart implementado con Prisma ORM y PostgreSQL. Veremos cómo reflejamos reglas de negocio complejas en modelos de datos simples, con un enfoque especial en una característica clave para la conversión de ventas: el carrito de compras para invitados.
🗺️ El Mapa de Datos: Relaciones en DevMart
El esquema relacional de DevMart se organiza en torno a cuatro áreas principales: la cuenta de usuario, el catálogo de productos, la gestión del carrito y las órdenes de compra.
erDiagram
CUSTOMER ||--o[0..N] CART : "tiene"
CUSTOMER ||--o[0..N] ORDER : "genera"
CUSTOMER ||--o[0..N] FAVORITE : "agrega"
CATEGORY ||--o[0..N] PRODUCT : "agrupa"
CART ||--o[0..N] CART_DETAIL : "contiene"
PRODUCT ||--o[0..N] CART_DETAIL : "se añade a"
ORDER ||--|{ ORDER_DETAIL : "detalla"
PRODUCT ||--o[0..N] ORDER_DETAIL : "se compra en"
PRODUCT ||--o[0..N] FAVORITE : "gusta a"
1. El Catálogo (Categorías y Productos)
El catálogo es sencillo pero robusto. Cada producto pertenece a una única categoría.
model Category {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
sku String? @unique
categoryName String @unique @map("category_name")
products Product[]
}
model Product {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
categoryId String @map("category_id") @db.Uuid
productName String @unique @map("product_name")
stock Int @default(0)
price Decimal @db.Decimal(10, 2)
sku String? @unique
category Category @relation(fields: [categoryId], references: [id])
}
💡 Nota técnica: Los campos SKU son únicos y están indexados (@@index(sku) en el modelo Product), lo que permite búsquedas instantáneas y seguras tanto desde el buscador web como desde el chatbot.
🛒 El Caso de Negocio: Reduciendo la Fricción con el Carrito de Invitado
¿Cuántas veces has abandonado una tienda online porque te obligaron a registrarte para poder añadir un artículo al carrito? Obligar a los usuarios a crear una cuenta en su primer contacto es uno de los errores de conversión más graves en el e-commerce.
En DevMart resolvimos esto a nivel de base de datos diseñando un modelo de carrito híbrido.
model Cart {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
customerId String? @map("customer_id") @db.Uuid // 🔍 OPCIONAL (Permite invitados)
status CartStatus @default(ACTIVE)
customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
cartDetails CartDetail[]
@@index([customerId, status])
}
¿Cómo funciona la lógica de negocio?
- El campo
customerIdes opcional (nullable): Cuando un visitante no autenticado entra a la tienda y agrega un producto, no forzamos una fila en la base de datos de inmediato. En su lugar, el frontend guarda estos artículos en el almacenamiento local del navegador (localStorage). - Fusión al iniciar sesión (Merge): Cuando el usuario finalmente decide registrarse o iniciar sesión, el frontend envía el carrito de invitado al backend a través del endpoint
/cart/merge. - Validación transaccional: El backend procesa cada elemento, valida el stock disponible en ese instante y asocia los productos al carrito activo en la base de datos (
customerIdahora tiene el valor real).
Esto le permite al negocio ofrecer una experiencia sin fricciones al inicio del embudo de ventas, aumentando hasta un 20% la probabilidad de que la visita resulte en una compra real.
🤖 Conectando el Canal Conversacional: Telegram y OTPs
Para que el asistente de compras en Telegram (ECode) sepa qué usuario está chateando y qué carrito le corresponde, necesitamos asociar la sesión de chat con una cuenta registrada en la plataforma web.
Para ello, el modelo de datos incluye dos piezas clave:
model Customer {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
email String @unique
telegramId String? @unique @map("telegram_id") // 🔍 Identificador de chat de Telegram
// ...
}
model OtpRequest {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
telegramId String @map("telegram_id")
email String
otp String // Código de 6 dígitos autogenerado
expiresAt DateTime @map("expires_at")
createdAt DateTime @default(now()) @map("created_at")
@@unique([telegramId, email])
}
El proceso de vinculación:
- El usuario envía su correo electrónico a través de Telegram.
- El sistema genera una fila en
OtpRequestcon un código de un solo uso (OTP) de 6 dígitos que expira en 30 minutos, y se lo envía a su bandeja de correo. - El usuario escribe el código en el chat.
- El backend valida el código y actualiza el campo
telegramIden el registroCustomercorrespondiente en la base de datos. - A partir de ese momento, cada consulta o acción de compra en Telegram se mapea directamente al cliente correcto usando su
telegramIdde forma 100% segura.
En el siguiente post, explicaremos a fondo cómo implementamos el Procesamiento del Lenguaje Natural (NLP) usando Inteligencia Artificial para que nuestro asistente de Telegram sea capaz de comprender intenciones como "quiero añadir 2 mouses a mi carrito".
📅 Publicado en la Bitácora de Desarrollo de DevMart. Ver Índice General