You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

19 KiB

Modelo de datos - Documentación completa

📋 Tabla de contenidos


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)

ConfigurationCatalog

Contenedor principal de todo el modelo.

interface ConfigurationCatalog {
    name            : I18nString;
    description?    : I18nString;
    supportedLocales: string[];
    defaultLocale   : string;
    options         : Record<OptionID, OptionDefinition>;
    objects         : Record<ObjectID, ConfigurableObject>;
    rules?          : Record<RuleID, ValidationRule>;
}

Ejemplo:

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).

interface ConfigurableObject {
    id           : ObjectID;              // 'ob:apartamento'
    name         : I18nString;
    description  : I18nString;
    attributes   : Attribute[];           // Atributos GLOBALES
    sections     : Record<SectionID, Section>;
    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:

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).

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:

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:

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:

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

type Attribute = 
    | FixedAttribute
    | DynamicAttribute
    | QuantifiableAttribute
    | ComputedAttribute;

1. Fixed Attribute

Valor fijo, no modificable por el usuario.

interface FixedAttribute {
    type    : 'fixed';
    dataType: DataType;
    value   : Value;
    unit?   : I18nString;
    display : AttributeDisplay;
}

Ejemplo:

{
    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.

interface DynamicAttribute {
    type            : 'dynamic';
    dataType        : 'reference';
    defaultValue    : OptionID;
    options         : AttributeOption[];
    required?       : boolean;
    isController?   : boolean;
    controls?       : SectionID[];
    filterExpression?: JsonLogic;
    display         : AttributeDisplay;
}

Opciones:

interface AttributeOption {
    optionId        : OptionID;
    priority?       : number;        // Orden de presentación
    pricingOverride?: number;        // Precio custom para esta combinación
    metadata?       : Metadata;
}

Ejemplo:

{
    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).

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:

{
    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.

interface ComputedAttribute {
    type        : 'computed';
    dataType    : DataType;
    unit?       : I18nString;
    expression  : JsonLogic;
    dependencies: AttrID[];
    display     : AttributeDisplay;
}

Ejemplo:

{
    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:

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:

{
    display: {
        uiVisible: {
            '==': [{ var: 'attributes.at_tipo' }, 'avanzado']
        },
        affectsVisual: true
    }
}

Opciones

OptionDefinition

Define una opción seleccionable (valor concreto de un atributo).

interface OptionDefinition {
    id          : OptionID;
    name        : I18nString;
    description : I18nString;
    media?      : Media;
    pricing?    : Pricing;
    tags?       : string[];
    style?      : string;
    metadata?   : Metadata;
}

Pricing

interface Pricing {
    baseAmount: number | 'consultation';
    currency?: string;
    dynamicExpression?: JsonLogic;
}

Ejemplo:

{
    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.

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

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

{
    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

{
    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

{
    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

{
    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

Estructura mínima

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

Powered by TurnKey Linux.