# Modelo de datos - Documentación completa ## 📋 Tabla de contenidos - [Visión general](#visión-general) - [Catálogo](#catálogo) - [Objetos configurables](#objetos-configurables) - [Secciones](#secciones) - [Atributos](#atributos) - [Opciones](#opciones) - [Reglas de validación](#reglas-de-validación) - [Ejemplo completo](#ejemplo-completo) --- ## Visión general El modelo CPQ está organizado jerárquicamente: ``` ConfigurationCatalog └── Objects (ConfigurableObject) ├── Attributes (global) └── Sections └── Attributes (per-section) └── Options Rules (aplican a cualquier combinación) ``` --- ## Catálogo ### `ConfigurationCatalog` Contenedor principal de todo el modelo. ```typescript interface ConfigurationCatalog { name : I18nString; description? : I18nString; supportedLocales: string[]; defaultLocale : string; options : Record; objects : Record; rules? : Record; } ``` **Ejemplo:** ```typescript const CATALOGO_VIVIENDAS: ConfigurationCatalog = { name: { es: 'Catálogo de Viviendas', en: 'Housing Catalog' }, description: { es: 'Configurador de acabados', en: 'Finish configurator' }, supportedLocales: ['es', 'en'], defaultLocale: 'es', options: { /* ... */ }, objects: { /* ... */ }, rules: { /* ... */ } }; ``` --- ## Objetos configurables ### `ConfigurableObject` Representa un producto/objeto que puede configurarse (ej: casa, coche, mueble). ```typescript interface ConfigurableObject { id : ObjectID; // 'ob:apartamento' name : I18nString; description : I18nString; attributes : Attribute[]; // Atributos GLOBALES sections : Record; sectionOrder?: SectionID[]; category? : string; metadata? : Metadata; } ``` ### Atributos globales vs de sección - **Globales**: Afectan a TODO el objeto (ej: calidad general, color principal) - **De sección**: Solo afectan a una sección específica (ej: suelo del salón) **Ejemplo:** ```typescript const APARTAMENTO: ConfigurableObject = { id: 'ob:apartamento', name: { es: 'Apartamento', en: 'Apartment' }, description: { es: 'Apartamento de 1 dormitorio', en: '1 bedroom apartment' }, // Atributos GLOBALES (afectan a todo) attributes: [ { id: 'at:calidad', name: { es: 'Calidad de acabados', en: 'Finish quality' }, type: 'dynamic', dataType: 'reference', defaultValue: 'op:calidad_estandar', options: [ { optionId: 'op:calidad_estandar' }, { optionId: 'op:calidad_premium' }, { optionId: 'op:calidad_lujo' } ], isController: true, // ✅ Controla visibilidad/precios globalmente display: { uiVisible: true, affectsPrice: true } } ], // Secciones del objeto sections: { 'sc:salon': { /* ... */ }, 'sc:bano': { /* ... */ } }, sectionOrder: ['sc:salon', 'sc:bano'] }; ``` --- ## Secciones ### `Section` Representa una parte del objeto configurable (ej: salón, baño, motor). ```typescript interface Section { id : SectionID; name : I18nString; description : I18nString; attrs : Attribute[]; // Atributos DE ESTA SECCIÓN visualConfig: SectionVisualConfig; availability: SectionAvailability; order? : number; icon? : string; metadata? : Metadata; } ``` ### Visual Config Controla cómo se renderiza visualmente la sección: ```typescript interface SectionVisualConfig { strategy: RenderingStrategy; // Para 'static_image' o 'dynamic_image' imageUrlTemplate? : string; globalAttrDependencies? : AttrID[]; sectionAttrDependencies?: AttrID[]; basePath? : string; // Para 'api_generated' apiConfig?: APIConfig; // Fallback fallbackImage?: string; } ``` **Estrategias de renderizado:** | Estrategia | Uso | |------------|-----| | `static_image` | URL con placeholders: `{basePath}/salon/{at_suelo}/{at_pared}.jpg` | | `dynamic_image` | URL dinámica calculada | | `api_generated` | Llamada a API externa para generar imagen | | `three_d` | Modelo 3D con texturas dinámicas | | `composite_layers` | Capas PNG superpuestas | | `none` | Sin renderizado visual | ### Availability Controla cuándo la sección está disponible: ```typescript interface SectionAvailability { mode: 'required' | 'optional' | 'conditional'; // Si mode es 'conditional' condition?: JsonLogic; dependsOn?: AttrID[]; // Qué hacer al desactivarse onDeactivate?: { action: 'clear' | 'preserve' | 'reset'; confirmWithUser?: boolean; confirmMessage?: I18nString; }; } ``` **Ejemplo de sección:** ```typescript const seccionSalon: Section = { id: 'sc:salon', name: { es: 'Salón', en: 'Living room' }, description: { es: 'Salón - comedor principal', en: 'Main living - dining room' }, // Atributos DE ESTA SECCIÓN attrs: [ { id: 'at:suelo', name: { es: 'Suelo', en: 'Flooring' }, type: 'dynamic', dataType: 'reference', defaultValue: 'op:suelo_ceramica', options: [ { optionId: 'op:suelo_ceramica' }, { optionId: 'op:suelo_parquet' } ], display: { uiVisible: true, affectsVisual: true, // ✅ Afecta al render affectsPrice: true } }, { id: 'at:pared', name: { es: 'Color paredes', en: 'Wall color' }, type: 'dynamic', dataType: 'reference', defaultValue: 'op:pared_blanco', options: [ /* ... */ ], display: { uiVisible: true, affectsVisual: true } } ], // Configuración visual visualConfig: { strategy: 'static_image', imageUrlTemplate: '{basePath}/salon/{at_suelo}/{at_pared}/view.jpg', basePath: '/renders/vivienda', // Atributos GLOBALES que afectan esta imagen globalAttrDependencies: ['at:calidad'], // Atributos DE SECCIÓN que afectan sectionAttrDependencies: ['at:suelo', 'at:pared'] }, // Disponibilidad availability: { mode: 'required' // Siempre visible } }; ``` --- ## Atributos ### Tipos de atributos ```typescript type Attribute = | FixedAttribute | DynamicAttribute | QuantifiableAttribute | ComputedAttribute; ``` ### 1. Fixed Attribute Valor fijo, no modificable por el usuario. ```typescript interface FixedAttribute { type : 'fixed'; dataType: DataType; value : Value; unit? : I18nString; display : AttributeDisplay; } ``` **Ejemplo:** ```typescript { id: 'at:m2_salon', name: { es: 'Superficie', en: 'Surface' }, type: 'fixed', dataType: 'number', value: 25, unit: { es: 'm²', en: 'sqm' }, display: { uiVisible: false, // ❌ No se muestra al usuario readonly: true } } ``` ### 2. Dynamic Attribute Seleccionable por el usuario entre opciones. ```typescript interface DynamicAttribute { type : 'dynamic'; dataType : 'reference'; defaultValue : OptionID; options : AttributeOption[]; required? : boolean; isController? : boolean; controls? : SectionID[]; filterExpression?: JsonLogic; display : AttributeDisplay; } ``` **Opciones:** ```typescript interface AttributeOption { optionId : OptionID; priority? : number; // Orden de presentación pricingOverride?: number; // Precio custom para esta combinación metadata? : Metadata; } ``` **Ejemplo:** ```typescript { id: 'at:calidad', name: { es: 'Calidad de acabados', en: 'Finish quality' }, type: 'dynamic', dataType: 'reference', defaultValue: 'op:calidad_estandar', options: [ { optionId: 'op:calidad_estandar', priority: 1 }, { optionId: 'op:calidad_premium', priority: 2 }, { optionId: 'op:calidad_lujo', priority: 3 } ], required: true, isController: true, // ✅ Controla otras secciones/atributos display: { uiVisible: true, affectsPrice: true, affectsVisual: true } } ``` ### 3. Quantifiable Attribute Valor numérico configurable (cantidad, tamaño). ```typescript interface QuantifiableAttribute { type : 'quantifiable'; dataType : 'number'; quantity : QuantityDefinition; unit? : I18nString; userConfigurable?: boolean; pricePerUnit? : number; min? : number; max? : number; display : AttributeDisplay; } type QuantityDefinition = | number // Fijo | { min: number; max: number; step?: number; default: number } // Rango | { values: number[]; default: number }; // Lista discreta ``` **Ejemplo:** ```typescript { id: 'at:cantidad_lamparas', name: { es: 'Número de lámparas', en: 'Number of lamps' }, type: 'quantifiable', dataType: 'number', quantity: { min: 1, max: 10, step: 1, default: 3 }, unit: { es: 'uds', en: 'units' }, userConfigurable: true, pricePerUnit: 150, // 150€ por lámpara display: { uiVisible: true, affectsPrice: true } } ``` ### 4. Computed Attribute Valor calculado dinámicamente basado en otros atributos. ```typescript interface ComputedAttribute { type : 'computed'; dataType : DataType; unit? : I18nString; expression : JsonLogic; dependencies: AttrID[]; display : AttributeDisplay; } ``` **Ejemplo:** ```typescript { id: 'at:precio_total', name: { es: 'Precio total', en: 'Total price' }, type: 'computed', dataType: 'number', unit: { es: '€', en: '€' }, expression: { '+': [ { var: 'attributes.at_precio_base' }, { var: 'attributes.at_extras' } ] }, dependencies: ['at:precio_base', 'at:extras'], display: { uiVisible: true, readonly: true, // ✅ Siempre readonly affectsPrice: false // Ya ES el precio } } ``` ### Display Config Controla cómo y cuándo se muestra un atributo: ```typescript interface AttributeDisplay { uiVisible? : boolean | JsonLogic; // ¿Se muestra? affectsVisual? : boolean | JsonLogic; // ¿Afecta al render? affectsPrice? : boolean | JsonLogic; // ¿Afecta al precio? readonly? : boolean; // ¿Solo lectura? order? : number; // Orden en UI icon? : string; helpText? : I18nString; } ``` **Visibilidad condicional:** ```typescript { display: { uiVisible: { '==': [{ var: 'attributes.at_tipo' }, 'avanzado'] }, affectsVisual: true } } ``` --- ## Opciones ### `OptionDefinition` Define una opción seleccionable (valor concreto de un atributo). ```typescript interface OptionDefinition { id : OptionID; name : I18nString; description : I18nString; media? : Media; pricing? : Pricing; tags? : string[]; style? : string; metadata? : Metadata; } ``` ### Pricing ```typescript interface Pricing { baseAmount: number | 'consultation'; currency?: string; dynamicExpression?: JsonLogic; } ``` **Ejemplo:** ```typescript { id: 'op:suelo_parquet', name: { es: 'Parquet', en: 'Parquet' }, description: { es: 'Suelo de madera natural', en: 'Natural wood floor' }, pricing: { baseAmount: 45, // €/m² currency: 'EUR', dynamicExpression: { // Descuento si calidad es lujo 'if': [ { '==': [{ var: 'attributes.at_calidad' }, 'op:calidad_lujo'] }, { '*': [45, 0.9] }, // 10% descuento 45 ] } }, tags: ['madera', 'calido', 'natural'], media: { thumbnail: '/images/parquet-thumb.jpg', images: ['/images/parquet-1.jpg', '/images/parquet-2.jpg'] } } ``` --- ## Reglas de validación ### `ValidationRule` Define restricciones y dependencias entre atributos. ```typescript interface ValidationRule { id : RuleID; name : I18nString; condition : JsonLogic; // Cuándo se activa action : ValidationRuleAction; // Qué hace priority : number; // Mayor = más importante bidirectional: boolean; affects : AttrID[]; // Atributos involucrados severity : Severity; // 'error' | 'warning' | 'info' message : I18nString; } ``` ### Tipos de acción ```typescript type ValidationRuleActionType = | 'allow' // Lista blanca de valores permitidos | 'forbid' // Prohíbe valores específicos | 'require' // Hace obligatorio un atributo | 'suggest'; // Sugiere valores (no fuerza) interface ValidationRuleAction { type : ValidationRuleActionType; targetAttr : AttrID; values : Value[]; } ``` ### Ejemplos de reglas #### 1. Regla FORBID ```typescript { id: 'rl:estandar_no_lujo', name: { es: 'Estándar sin lujo', en: 'Standard without luxury' }, // CUANDO: calidad es estándar condition: { '==': [{ var: 'attributes.at_calidad' }, 'op:calidad_estandar'] }, // ENTONCES: prohíbe mármol en baño action: { type: 'forbid', targetAttr: 'at:revest_bano', values: ['op:revest_marmol'] }, priority: 9, bidirectional: false, affects: ['at:calidad', 'at:revest_bano'], severity: 'error', message: { es: 'El mármol no está disponible en calidad estándar', en: 'Marble is not available in standard quality' } } ``` #### 2. Regla ALLOW ```typescript { id: 'rl:roble_colores', name: { es: 'Roble colores permitidos', en: 'Oak allowed colors' }, // CUANDO: material es roble condition: { '==': [{ var: 'attributes.at_material' }, 'op:material_roble'] }, // ENTONCES: solo permite blanco y natural action: { type: 'allow', targetAttr: 'at:color', values: ['op:color_blanco', 'op:color_natural'] }, priority: 5, bidirectional: false, affects: ['at:material', 'at:color'], severity: 'error', message: { es: 'Roble solo admite blanco o natural', en: 'Oak only allows white or natural' } } ``` #### 3. Regla REQUIRE ```typescript { id: 'rl:premium_require_material', name: { es: 'Premium requiere material', en: 'Premium requires material' }, // CUANDO: acabado es premium condition: { '==': [{ var: 'attributes.at_acabado' }, 'op:acabado_premium'] }, // ENTONCES: material es obligatorio action: { type: 'require', targetAttr: 'at:material', values: [] // No importa qué valor, pero debe tener uno }, priority: 8, bidirectional: false, affects: ['at:acabado', 'at:material'], severity: 'error', message: { es: 'El acabado premium requiere seleccionar material', en: 'Premium finish requires material selection' } } ``` #### 4. Regla BIDIRECCIONAL ```typescript { id: 'rl:negro_premium', name: { es: 'Negro requiere premium', en: 'Black requires premium' }, // DIRECCIÓN 1: sanitario negro → prohíbe calidad estándar condition: { '==': [{ var: 'attributes.at_sanitario' }, 'op:sanit_negro'] }, action: { type: 'forbid', targetAttr: 'at:calidad', values: ['op:calidad_estandar'] }, // ⚠️ IMPORTANTE: Para bidireccional, crear 2 reglas separadas es más claro: // - Regla 1: negro → prohíbe estándar // - Regla 2: estándar → prohíbe negro priority: 8, bidirectional: false, // ✅ Usar false y crear regla inversa manualmente affects: ['at:sanitario', 'at:calidad'], severity: 'error', message: { es: 'Los sanitarios negros requieren calidad premium o superior', en: 'Black sanitary ware requires premium quality or higher' } } ``` ### Prioridades Las reglas se evalúan en orden de prioridad **descendente**: - **10-15**: Reglas críticas (incompatibilidades técnicas) - **5-9**: Reglas importantes (restricciones de negocio) - **1-4**: Reglas sugerencias (recomendaciones estéticas) --- ## Ejemplo completo Ver archivo completo: [`housing-catalog.fixture.ts`](../tests/fixtures/housing-catalog.fixture.ts) ### Estructura mínima ```typescript const CATALOGO_MINIMO: ConfigurationCatalog = { name: { es: 'Mi Catálogo', en: 'My Catalog' }, supportedLocales: ['es', 'en'], defaultLocale: 'es', // 1. Definir opciones options: { 'op:color_rojo': { id: 'op:color_rojo', name: { es: 'Rojo', en: 'Red' }, description: { es: 'Color rojo', en: 'Red color' }, pricing: { baseAmount: 0 } }, 'op:color_azul': { id: 'op:color_azul', name: { es: 'Azul', en: 'Blue' }, description: { es: 'Color azul', en: 'Blue color' }, pricing: { baseAmount: 50 } } }, // 2. Definir objetos objects: { 'ob:producto': { id: 'ob:producto', name: { es: 'Producto', en: 'Product' }, description: { es: 'Mi producto', en: 'My product' }, // Atributos globales attributes: [ { id: 'at:color', name: { es: 'Color', en: 'Color' }, type: 'dynamic', dataType: 'reference', defaultValue: 'op:color_rojo', options: [ { optionId: 'op:color_rojo' }, { optionId: 'op:color_azul' } ], display: { uiVisible: true, affectsPrice: true } } ], // Secciones sections: { 'sc:principal': { id: 'sc:principal', name: { es: 'Principal', en: 'Main' }, description: { es: 'Sección principal', en: 'Main section' }, attrs: [], visualConfig: { strategy: 'static_image', imageUrlTemplate: '/images/{at_color}.jpg' }, availability: { mode: 'required' } } }, sectionOrder: ['sc:principal'] } }, // 3. Definir reglas (opcional) rules: {} }; ``` --- ## 📚 Recursos adicionales - [Sistema de tipos](./TYPES.md) - [Reglas de validación](./RULES.md) - [i18n](./I18N.md) - [Guía de creación](./CATALOG_GUIDE.md)