19 KiB
Modelo de datos - Documentación completa
📋 Tabla de contenidos
- Visión general
- Catálogo
- Objetos configurables
- Secciones
- Atributos
- Opciones
- Reglas de validación
- 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.
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: {}
};