Skip to main content

Store — Plataforma de e-commerce (catálogo, órdenes, cupones, blog y constructor de páginas con Puck). Sigue siendo un submodule (platform/store). ⬆ índice

Store

[!NOTE] Para el flujo de despliegue remoto y ARM (sd2) ver docs/internal/operations/deployment.md y HANDOFF-sd2-aitha.md.

Qué es​

Store es la tienda online de la plataforma: multi-tenant (una tienda por tenant), con catálogo, carrito/checkout, órdenes, cupones, blog, banners y un constructor de páginas visual (Puck) para armar la portada y el contenido del sitio. Vive en un submodule propio (platform/store, rama main); se empaqueta como remote de Module Federation consumido por el portal.

Arquitectura​

  • Next.js 16 (App Router, output: 'standalone') + Drizzle ORM sobre Postgres centralizado (postgres-central, DB store_db).
  • Todo el app (páginas Y API) se sirve bajo el basePath /store (STORE_BASE_PATH, horneado en build).
  • El admin es un remote de Module Federation (webpack.mf.config.js) que el portal embebe; la sesión del navegador vive en el portal (modo "embebido").
  • El storefront público se sirve en {portal}/store/{tenant}/... o en un dominio propio del tenant (reescritura por Host vía /api/tenant-by-host).
  • Contenedor: base playwright (necesario para tests de logout), imagen standalone. Puerto interno 127.0.0.1:3050.

Modelo de datos (resumen)​

Fuente de verdad: platform/store/src/db/schema.ts. Principales:

ÁreaTablas
Tenantstores (tenantId, zitadelOrgId, customDomain, publicEnabled, onboarding)
Catálogoproducts, product_images, product_variants, product_currency_prices, external_listings, categories, product_categories, tags, product_tags
Carrito/checkoutcarts, cart_items, checkout_sessions, payment_gateways, payment_configs, payment_events
Órdenesorders, order_items, order_payments
Cuponescoupons, coupon_codes, coupon_products, coupon_categories, coupon_assignments, coupon_redemptions
Clientescustomers, customer_addresses, customer_notifications
CMS (Puck)site_pages, site_page_versions, site_regions, site_menus, posts, post_categories, post_category_links
Config/analyticssettings, banners, integrations, sync_jobs, sync_outbox, visit_daily, visit_sessions

Modelo de tenants y auth​

  • Tenant por header X-Active-Tenant: el middleware lo inyecta desde cookie active_tenant, query tenant_id o Referer; en dominio propio se deduce del Host (/api/tenant-by-host, solo si publicEnabled).
  • Admin (modo embebido): el navegador entra por el portal (gate requiredRole: 'store'). La API admin (/api/admin/**) valida además, si llega un Authorization: Bearer, el token contra Zitadel (src/lib/zitadel-introspect.ts):
    • token de usuario con rol store/store-owner/internal-assistant → tenant derivado del org del token (stores.zitadel_org_id); el X-Active-Tenant del cliente se ignora.
    • token de servicio (chat-server) → confía en X-Active-Tenant.
    • sin Bearer → comportamiento embebido histórico (solo header), control delegado al portal.
  • Comprador (storefront): NextAuth por tenant (Zitadel), requireCustomer() lee la cookie store-session.{tenant} y resuelve customers por (tenantId, zitadelSub).

Constructor de páginas (Puck)​

  • Configs en src/puck/config.tsx (puckConfig, regionConfig, cartPageConfig, checkoutPageConfig, productPageConfig, contactPageConfig); bloques en src/puck/blocks/*.tsx; campos custom en src/puck/{style-fields,data-fields,code-editor}.tsx.
  • Persistencia: site_pages.draft_data/published_data (JSON Puck {root, content, zones}), site_page_versions (historial), site_regions (header/footer), site_menus.
  • Render público: app/[tenant]/[...slug]/page.tsx + PuckRender; preview con token HMAC.
  • Catálogo de componentes vía API: GET /api/admin/site/components devuelve la estructura (labels/fields/options/defaults) de todos los bloques de cada config, más la documentación extraída de los JSDoc del código. Es la fuente de verdad para que un LLM arme páginas.

Documentación de la API (generada del código)​

La doc se genera del JSDoc del código, no se edita a mano:

  • scripts/generate-api-docs.mjs (TypeScript compiler API) lee los comentarios de los bloques Puck y de los route handlers, inventaría src/app/api/**/route.ts y produce:
    • src/generated/openapi.json — spec OpenAPI 3 (servida en GET /api/openapi.json).
    • src/generated/puck-component-docs.json — JSDoc de bloques (fusionado en /api/admin/site/components).
    • docs/generated/api-reference.md y docs/generated/puck-components.md.
  • Regeneración: platform/store/run.sh docs (o ./infra run store docs) y automáticamente en predev/prebuild.
  • Regla: tocar la doc = editar el JSDoc en el código y regenerar. Los schemas de request/response que un comentario no expresa viven en src/config/api-docs.json.

Despliegue​

  • Proyecto estándar (project.conf PRIORITY 40, DEPENDS_ON user-service). ./infra up|restart|logs|clean store.
  • OJO ARM (sd2): imágenes con --remote-build. Ver HANDOFF-sd2-aitha.md.
  • OJO migraciones: entrypoint.sh hace drizzle-kit migrate || echo "..." — traga fallos de migración (el contenedor arranca con schema viejo y luego 500s). Tras tocar schema, validar el conteo de drizzle.__drizzle_migrations contra _journal.json.
  • Las credenciales de pago se cifran AES-GCM con STORE_SECRETS_KEY.

Tools de la tienda (MCP + internal tools = una sola implementación)​

Las tools de la tienda viven en store, no en el MCP ni en chat-server:

  • Catálogo: platform/store/src/lib/tools/catalog.ts — una única lista (STORE_TOOLS) con id, descripción, inputSchema y requiredRoles por tool, más cómo construir la llamada a la API admin.
  • Endpoints: GET /api/internal-tools (listar catálogo) y POST /api/internal-tools/{id}/execute (ejecutar). El execute resuelve la identidad Zitadel del caller (requireToolSession), verifica el rol requerido por la tool y llama a las propias rutas /api/admin/*** de la tienda (self-fetch con el tenant resuelto). Una única implementación.
  • MCP de store (infrastructure/mcp_servers/store/server.py): es un puente fino. En el arranque descarga el catálogo y registra una tool MCP por entrada; cada ejecución reenvía el Bearer del usuario a POST /api/internal-tools/{id}/execute. Sin lógica duplicada.
  • Internal tools de chat-server (app/features/tools/builtin/store.py): también un puente fino. Al arrancar descarga el catálogo y registra las mismas tools con id store_<id>; cada una llama al execute de store con el access token del usuario. chat-server solo orquesta.
  • Verificación de rol: en store (requiredRoles de cada tool, contra la introspección del token). Los tokens de servicio solo pueden ejecutar tools globales (catálogos).

Regla: si falta una tool, se agrega una sola vez en catalog.ts; MCP e internal tools la toman automáticamente. GET /store/api/internal-tools es la referencia viva del set.

Gotchas​

  • src/collections/, src/blocks/, src/app/(payload) son vestigios vacíos (un intento de Payload CMS que nunca existió). Ignorar.
  • src/puck/fields/ está vacío: los fields custom viven en style-fields.tsx, data-fields.tsx, code-editor.tsx, embed-block.tsx, nav-blocks.tsx.
  • Submodule: commitear en el submodule y luego bump del puntero en el padre (ver reglas de submodule en el AGENTS raíz).