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.
814 lines
19 KiB
814 lines
19 KiB
|
8 months ago
|
# 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<OptionID, OptionDefinition>;
|
||
|
|
objects : Record<ObjectID, ConfigurableObject>;
|
||
|
|
rules? : Record<RuleID, ValidationRule>;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**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<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:**
|
||
|
|
|
||
|
|
```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)
|