Web5 min de lectura

02. Modelando Relaciones: De Visitante a Cliente Fiel

Analizamos el esquema de datos relacional de DevMart implementado con Prisma ORM y PostgreSQL, con foco en el carrito de compras para invitados.

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?

  1. El campo customerId es 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).
  2. 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.
  3. 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 (customerId ahora 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:

  1. El usuario envía su correo electrónico a través de Telegram.
  2. El sistema genera una fila en OtpRequest con 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.
  3. El usuario escribe el código en el chat.
  4. El backend valida el código y actualiza el campo telegramId en el registro Customer correspondiente en la base de datos.
  5. A partir de ese momento, cada consulta o acción de compra en Telegram se mapea directamente al cliente correcto usando su telegramId de 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

Tags

#Base de datos#Prisma#PostgreSQL#Modelado#Backend