Saltar a contenido

Especificación del catálogo premium multi-tenant

Documento de diseño, no de estado. Nada de esto está construido. Define qué debe tener el catálogo de muebles para estar al nivel del mejor producto del monorepo, y qué se hereda de Gastro sin reescribir. Creado 2026-08-29. Autoridad sobre M1–M2 de project.md.


1. La tesis

La carta de Gastro ya resolvió el problema difícil: un escaparate público multi-tenant que se ve caro sin que el dueño pueda arruinarlo. El catálogo de muebles es el mismo problema con tres diferencias que sí obligan a diseñar de nuevo: la variante es un SKU real, el producto tiene dimensiones físicas que deciden la compra, y la conversión no es un pedido sino una solicitud de crédito.

Todo lo demás se porta.


2. Lo que se hereda de Gastro sin rediseñar

Estas piezas están probadas en producción. Se copian con adaptación de nombres, no se reinventan.

Pieza Dónde vive hoy Qué resuelve
Catálogo de marca curado domains/tenant/theme_catalog.py El dueño elige de una lista validada, nunca valores libres (patrón Shopify). 19 fuentes con pareja de cuerpo recomendada + paletas curadas
ThemeConfig validado domains/tenant/schema.py brand_color, accent_color, font_heading, font_body, hero_style, card_radius, layout_style. Valor fuera de catálogo se coacciona, no revienta
Derivación de paleta con pisos WCAG public-menu/src/shared/theme/derivePalette.ts El guardarraíl que hace imposible un escaparate ilegible: camina el color hacia claro u oscuro hasta alcanzar 12:1 / 7:1 / 4.5:1
Endpoint público Redis-first domains/public/router.py + service.py slug → tenant_id (TTL 300s) y payload (TTL 60s). Hit < 5 ms, miss < 50 ms. Rate limit 60 req/min por IP
Caché en idioma base + traducción superpuesta domains/public/service.py Una edición invalida una clave, no una por idioma
Resolución de idioma resolve_locale() ?lang=Accept-Language → base. Solo entre los idiomas que el tenant encendió
Layouts intercambiables public-menu/src/widgets/menu-layouts/ 5 disposiciones que el dueño elige. El mismo dato se ve como rejilla, acordeón o galería
Slug con palabras reservadas core/slugs.py (events) + gastro Un tenant no puede quedarse con app, api ni un segmento del enrutado
ISR + generateMetadata public-menu/src/app/[slug]/page.tsx revalidate = 60, OG image por tenant
Historial de precios GastroPriceHistory Auditoría de cambios de precio. En muebles es obligatorio: hay crédito de por medio

Regla: cualquier desviación de estas piezas exige ADR. No se "mejora" al portar.


3. La divergencia central: variante ≠ modificador

Gastro modela las opciones como deltas sobre un precio base: un grupo Salsas con price_delta por opción, sin existencias propias. Es correcto para comida y es incorrecto para muebles.

En una mueblería, la misma sala en tela beige y en cuero: - no cuesta lo mismo — y no por un delta, sino por precio propio; - no se acaba a la vez — el stock es por combinación; - no se ve igual — necesita su propia foto; - puede no existir — no todas las combinaciones se fabrican.

Modelo correcto: matriz de variantes

Producto  "Sala Milano"
  ├─ OptionType "Tela"    → [Lino beige, Chenille gris, Cuero café]
  ├─ OptionType "Puestos" → [3 puestos, 5 puestos, Esquinera]
  └─ Variantes (combinaciones que EXISTEN, no el producto cartesiano)
       ├─ Lino beige · 3 puestos   → SKU, precio, stock 4, fotos, dimensiones
       ├─ Cuero café · 3 puestos   → SKU, precio, stock 0 (sobre pedido 20 días)
       └─ Lino beige · Esquinera   → SKU, precio, stock 1 (última de exhibición)

Límites duros, recalibrados con la investigación (ver research/como-lo-hacen-los-mejores.md): máximo 3 tipos de opción y 250 variantes por producto.

El 3 es el número del líder del mercado: Shopify subió su tope de variantes de 100 a 2.048 en octubre de 2025 y dejó las opciones en 3. Es lo que impide que el selector se vuelva un formulario. El 250 es nuestro, y es de carga de datos, no técnico: por encima de ~100 variantes la industria coincide en que hace falta edición masiva — de ahí el requisito de importación CSV en M2.

Precio y stock viven en la variante. El producto no tiene precio, solo un rango derivado (desde $2.890.000).


4. Modelo de datos

Nombres de tabla con prefijo mueb_. Toda tabla lleva tenant_id, política RLS registrada en core/rls.py, created_at, updated_at y deleted_at.

Tabla Papel Campos que no son obvios
mueb_categories Sala, Comedor, Alcoba, Colchones sort_order, is_active, imagen de portada
mueb_products La ficha comercial name, description, story (texto largo editorial), tags[], is_featured, is_hero, warranty_months, care_instructions
mueb_option_types Tela, Color, Medida display_style: swatch | chip | dropdown. Una tela se elige viéndola, no en una lista
mueb_option_values Lino beige, Cuero café swatch_hex o swatch_image_key — la muestra visual del material
mueb_variants La entidad que se vende sku, price, compare_at_price (para tachado), stock_qty, availability_mode, lead_time_days, is_display_unit
mueb_variant_options Une variante ↔ valores tabla puente
mueb_media Galería variant_id nullable, sort_order, alt_text, lqip, width, height. La variante sin foto propia hereda la del producto
mueb_dimensions Medidas físicas width_cm, height_cm, depth_cm, weight_kg, boxed_width_cm (lo que pasa por la puerta), seats
mueb_materials Ficha técnica material_type, value, detail — densidad de espuma, tipo de madera, gramaje de tela
mueb_collections "Sala Milano completa" Conjunto con precio de paquete. El mueble se vende en juegos
mueb_price_history Auditoría portado de Gastro, obligatorio: hay crédito de por medio

Herencia de imagen — por qué la foto por variante no puede ser obligatoria

IKEA genera con CGI el 75% de sus imágenes de producto, y llegó ahí desde el 12% en 2012. La razón es aritmética: una mesa de comedor en 4 acabados × 3 tamaños son 12 sesiones de fotos. Ninguna mueblería PYME las paga, y por eso el mundo renderiza.

No vamos a montar una tubería de CGI — está fuera del alcance del cliente. Lo que sí se deriva es un requisito de modelo: una variante sin foto propia hereda la del producto, y la ficha lo indica sin fingir que la foto corresponde a esa tela. El model_3d_key queda como hueco previsto para el día en que existan renders, sin que eso cueste una migración.

El error del 28% — la ficha se renderiza desde UN objeto

Baymard mide que el 28% de los sitios tienen fallos graves con las variantes: los datos no se sincronizan entre variaciones y el comprador no logra evaluar el producto. Es el error más caro que este modelo permite cometer.

Al cambiar de variante deben actualizarse a la vez: precio, precio tachado, existencias, plazo de entrega, galería, medidas y SKU.

Regla: la ficha se arma desde un único objeto de variante seleccionada. Está prohibido leer las medidas del producto y el precio de la variante en el mismo render — es la forma exacta en que nace ese bug, y en muebles termina en devolución porque el comprador decidió con las medidas de otra combinación.

availability_mode — tres estados, no un booleano

Gastro usa is_available: bool ("hoy se acabó el ceviche"). En muebles hay tres realidades comercialmente distintas, y confundirlas cuesta ventas:

Modo Qué dice la ficha Qué implica
in_stock "Entrega en 3 días" stock_qty > 0, sale de bodega
made_to_order "Sobre pedido · 20 días" stock_qty = 0 pero se vende igual, con lead_time_days
display_only "Última unidad de exhibición" Una sola, posiblemente con detalle. No se repone

Un stock_qty = 0 con availability_mode = in_stock es el único caso que oculta la variante. Los otros dos venden.


5. Contrato de la API pública

Sin autenticación, rate limit 60 req/min por IP, todo Redis-first.

GET /api/v1/public/catalog/{slug}
    ?lang=es&category=salas&sort=price_asc&page=1
    → tenant (marca + tema), categorías, productos con rango de precio,
      primera foto con LQIP, badge de disponibilidad

GET /api/v1/public/catalog/{slug}/product/{product_slug}
    → ficha completa: variantes con precio/stock/fotos, tipos de opción con
      muestras, dimensiones, materiales, garantía, simulación de financiación

GET /api/v1/public/catalog/{slug}/collection/{collection_slug}
    → el juego completo con precio de paquete

GET /api/v1/public/theme/catalog
    → fuentes y paletas curadas (idéntico a Gastro)

POST /api/v1/public/catalog/{slug}/lead
    → solicitud del comprador. Idempotency-Key obligatoria

El listado nunca devuelve todas las variantes. Devuelve rango de precio y la foto principal. Las variantes completas solo en la ficha: son el 80% del payload.


6. El sistema de marca multi-tenant

Se porta el de Gastro con una recuración: sus paletas están afinadas a psicología del color gastronómica —acentos cálidos que estimulan el apetito, azul prohibido— y eso no aplica aquí. El mueble se vende con neutros calmados, maderas y un acento sobrio; el azul es perfectamente válido.

Lo que no se toca: 1. El dueño elige de un catálogo curado, jamás un color arbitrario. Es la diferencia entre un producto premium y un constructor de páginas feo. 2. derivePalette con pisos de contraste 12:1 / 7:1 / 4.5:1. Si el dueño elige un acento ilegible sobre su fondo, el sistema lo camina hasta que cumpla. No hay forma de publicar un catálogo que no se pueda leer. 3. Fuente fuera de catálogo se coacciona a la base, no revienta el render.

Añadir para muebles: layout_style con disposiciones propias —editorial (foto grande, poco texto), grid_dense (mueblería de volumen), showroom (ambientes completos)— y photo_ratio, porque una sala se fotografía apaisada y un armario vertical.


7. Rendimiento — el presupuesto es parte del contrato

Se abre desde WhatsApp, en 4G, en un gama media. No es negociable.

Métrica Presupuesto Cómo se logra
LCP < 2.0 s en 4G Foto principal con priority, srcset responsive, AVIF con fallback WEBP
CLS 0 width/height reales en mueb_media; el hueco existe antes que la imagen
Cambio de variante < 100 ms, sin recarga Precarga de las fotos de variantes adyacentes al montar
Respuesta de API < 5 ms cacheada Redis-first idéntico a Gastro
Peso de la ficha < 250 KB inicial LQIP en base64 para el blur, galería diferida

LQIP obligatorio. Un catálogo de muebles es fotografía; un salto de imagen gris a foto se ve barato. Se genera al subir, junto al WEBP, y viaja en el JSON.


8. SEO y datos estructurados — aquí sí importa

Una carta de restaurante casi no se busca en Google. Un sofá sí. Esta es la diferencia de prioridad más grande frente a Gastro.

  • JSON-LD Product + Offer por variante, con priceCurrency: "COP", availability e itemCondition. Google pide name, image y un Offer bien formado para el fragmento de producto; para merchant listing completo hacen falta además brand, un identificador de producto, aggregateRating y shippingDetails — y son shippingRate + deliveryTime los que disparan la anotación de fecha de entrega.
  • La ficha técnica es lo que hace citable el producto por una IA. Los asistentes de compra leen las propiedades detalladas —materiales, medidas— para responder preguntas concretas. mueb_materials y mueb_dimensions no son solo para el humano.
  • sitemap.xml por tenant, generado desde el catálogo activo.
  • URL canónica por producto: /{slug}/{categoria}/{producto}. La variante va en query (?tela=lino-beige) para no fragmentar la autoridad de la página.
  • OG image por producto, no solo por tenant: lo que se ve al pegar el enlace en WhatsApp es la miniatura del producto.

9. El gesto central: el enlace de WhatsApp

Es la razón de existir de la vertical y Gastro no tiene equivalente.

Cada producto y cada variante genera un enlace con mensaje prellenado:

https://wa.me/57XXXXXXXXXX?text=Hola,%20me%20interesa%20la%20Sala%20Milano
%20en%20lino%20beige%20(3%20puestos)%20—%20muebles.timeliber.com.co/lacasa/sala-milano?tela=lino-beige

Requisitos: - Botón "Compartir" en la ficha, con la variante seleccionada ya incluida. - El panel muestra el enlace listo para copiar de cada producto. - Atribución: el enlace lleva parámetro de origen para poder medir la métrica de éxito de mission.md — cuántas fichas abiertas desde WhatsApp terminan en solicitud.


10. Lo específico de muebles que casi nadie hace

Estas cinco cosas son la diferencia entre un e-commerce genérico y una herramienta que un mueblero colombiano reconoce como hecha para él.

  1. "¿Cabe?" — no es un diferencial, es EL pilar. La investigación del sector lo pone como la pregunta más común del comercio de muebles en línea, y la ansiedad de escala explica una parte importante de las devoluciones. Tres piezas: boxed_width_cm con advertencia al superar los 80 cm de puerta habitual; medidas que cambian con la variante seleccionada; y comparación con objetos familiares, porque traducir «180 cm» a un número no basta — hay que anclarlo a algo que el comprador reconozca.
  2. La cuota antes de preguntar. El bloque de financiación en la ficha, con cifras de la pasarela, nunca calculadas en el frontend. Es la mitad de la propuesta de valor y depende de [MUE-005].
  3. Muestra de material visible y GRANDE. Elegir "Chenille gris" en un desplegable es inútil: se elige viendo la tela. Sustituir desplegables por muestras visuales está entre las pruebas A/B más consistentes de todo el comercio electrónico. Y la lección del mueble a medida es que la muestra se vea grande, no como un círculo de 20 px.
  4. Ficha técnica real — densidad de espuma, tipo de madera, gramaje. El comprador informado de muebles pregunta esto, y quien responde, vende.
  5. Comparador de 2–3 productos. Nadie compra el primer sofá que ve. Se reaprovecha la mecánica de features/shortlist que Gastro ya construyó para el Modo Mesero.

11. Accesibilidad

WCAG 2.2 AA es el mínimo del estándar de frontend del monorepo y hay gate en CI (a11y-gate.yml, reutilizable, ya lo invocan cinco frontends).

Lo que rompe específicamente en un catálogo de muebles: - Las muestras de color necesitan etiqueta de texto: "Lino beige" no puede comunicarse solo por el color del círculo. - El estado seleccionado de una variante necesita aria-pressed, no solo un borde. - El zoom de foto debe funcionar con teclado, no solo con pinch. - Los precios con font-variant-numeric: tabular-nums para que la comparación alinee.


12. Líneas rojas

  • Nunca una cuota de financiación calculada en el cliente.
  • Nunca precio en el producto: vive en la variante.
  • Nunca el producto cartesiano de opciones como variantes.
  • Nunca un color de marca sin pasar por derivePalette.
  • Nunca una foto sin width/height reales — es CLS garantizado.
  • Nunca devolver todas las variantes en el listado.
  • Nunca ocultar una variante made_to_order por tener stock 0: se vende.

13. Referencia de conversión

La conversión media de muebles y hogar en 2025 está entre 1.2% y 1.6%. Es el listón contra el cual medir, y ordena la expectativa: la métrica de éxito de la misión no son visitas al catálogo, es qué fracción de las fichas abiertas desde WhatsApp termina en solicitud.

Contexto útil: Baymard mide que el 52% de las fichas de producto en escritorio y el 62% en móvil tienen un desempeño de UX «mediocre o peor». El listón real del mercado está bajo, y eso es precisamente la oportunidad.

Investigación completa con fuentes en research/como-lo-hacen-los-mejores.md.