|
|
|
@ -1,797 +1,177 @@
|
|
|
|
/**
|
|
|
|
/**
|
|
|
|
* ============================================================================
|
|
|
|
* ============================================================================
|
|
|
|
* CATÁLOGO DE VIVIENDAS - FIXTURE ACTUALIZADO
|
|
|
|
* PRICING ENGINE — dynamicExpression y QuantifiableAttribute
|
|
|
|
* ============================================================================
|
|
|
|
* ============================================================================
|
|
|
|
*
|
|
|
|
|
|
|
|
* Modelo A: Apartamento (salón, baño, dormitorio, cocina)
|
|
|
|
|
|
|
|
* Modelo B: Dúplex (salón, baño, aseo, 2 dormitorios)
|
|
|
|
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import type {
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
ConfigurationCatalog,
|
|
|
|
import { PricingEngine } from '../engines/pricing';
|
|
|
|
ConfigurableObject,
|
|
|
|
import type { Logr } from '@/libs/logr';
|
|
|
|
OptionDefinition,
|
|
|
|
import type { SelectionMap } from '../engines/rule';
|
|
|
|
ValidationRule,
|
|
|
|
import {
|
|
|
|
VisualSection,
|
|
|
|
CATALOGO_VIVIENDAS,
|
|
|
|
DynamicAttribute,
|
|
|
|
IDS_TEST,
|
|
|
|
FixedAttribute,
|
|
|
|
} from './housing-catalog.fixture';
|
|
|
|
QuantifiableAttribute,
|
|
|
|
|
|
|
|
SectionView,
|
|
|
|
const {
|
|
|
|
} from '../types';
|
|
|
|
OBJ_APARTAMENTO,
|
|
|
|
|
|
|
|
ATTR_CALIDAD,
|
|
|
|
import type { AttrID, SectionID, OptionID, ObjectID, RuleID, ViewID } from '../types';
|
|
|
|
ATTR_SUELO,
|
|
|
|
|
|
|
|
ATTR_PARED,
|
|
|
|
// ============================================================================
|
|
|
|
ATTR_SANITARIO,
|
|
|
|
// IDs — OPCIONES
|
|
|
|
ATTR_REVEST_BANO,
|
|
|
|
// ============================================================================
|
|
|
|
ATTR_ENCIMERA,
|
|
|
|
|
|
|
|
ATTR_MUEBLE_COC,
|
|
|
|
const OPT_SUELO_PARQUET : OptionID = 'op:suelo_parquet';
|
|
|
|
ATTR_PUERTA,
|
|
|
|
const OPT_SUELO_PORCELANICO : OptionID = 'op:suelo_porcelanico';
|
|
|
|
ATTR_ARMARIO_PUERTAS,
|
|
|
|
const OPT_SUELO_MICROCEMENTO : OptionID = 'op:suelo_microcemento';
|
|
|
|
OPT_CALIDAD_ESTANDAR,
|
|
|
|
const OPT_SUELO_CERAMICA : OptionID = 'op:suelo_ceramica';
|
|
|
|
OPT_SUELO_CERAMICA,
|
|
|
|
|
|
|
|
OPT_PARED_BLANCO,
|
|
|
|
const OPT_PARED_BLANCO : OptionID = 'op:pared_blanco';
|
|
|
|
OPT_SANIT_BLANCO,
|
|
|
|
const OPT_PARED_GRIS : OptionID = 'op:pared_gris';
|
|
|
|
OPT_REVEST_CERAMICA,
|
|
|
|
const OPT_PARED_BEIGE : OptionID = 'op:pared_beige';
|
|
|
|
OPT_ENCIMERA_GRANITO,
|
|
|
|
const OPT_PARED_PIEDRA : OptionID = 'op:pared_piedra';
|
|
|
|
OPT_MUEBLE_BLANCO,
|
|
|
|
|
|
|
|
OPT_PUERTA_LACADA,
|
|
|
|
const OPT_PUERTA_ROBLE : OptionID = 'op:puerta_roble';
|
|
|
|
OPT_ARMARIO_LACADO,
|
|
|
|
const OPT_PUERTA_LACADA : OptionID = 'op:puerta_lacada';
|
|
|
|
OPT_ARMARIO_MADERA,
|
|
|
|
const OPT_PUERTA_CRISTAL : OptionID = 'op:puerta_cristal';
|
|
|
|
} = IDS_TEST;
|
|
|
|
|
|
|
|
|
|
|
|
const OPT_ENCIMERA_GRANITO : OptionID = 'op:encimera_granito';
|
|
|
|
// ============================================================================
|
|
|
|
const OPT_ENCIMERA_SILESTONE : OptionID = 'op:encimera_silestone';
|
|
|
|
// SETUP
|
|
|
|
const OPT_ENCIMERA_MADERA : OptionID = 'op:encimera_madera';
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
const OPT_MUEBLE_BLANCO : OptionID = 'op:mueble_blanco';
|
|
|
|
function makeLogr(): Logr {
|
|
|
|
const OPT_MUEBLE_GRIS : OptionID = 'op:mueble_gris';
|
|
|
|
return {
|
|
|
|
const OPT_MUEBLE_MADERA : OptionID = 'op:mueble_madera';
|
|
|
|
debug : vi.fn(),
|
|
|
|
|
|
|
|
info : vi.fn(),
|
|
|
|
const OPT_SANIT_BLANCO : OptionID = 'op:sanit_blanco';
|
|
|
|
warn : vi.fn(),
|
|
|
|
const OPT_SANIT_NEGRO : OptionID = 'op:sanit_negro';
|
|
|
|
error : vi.fn(),
|
|
|
|
|
|
|
|
getLogs : vi.fn(),
|
|
|
|
const OPT_REVEST_MARMOL : OptionID = 'op:revest_marmol';
|
|
|
|
clear : vi.fn(),
|
|
|
|
const OPT_REVEST_MICROCEMENTO: OptionID = 'op:revest_microcemento';
|
|
|
|
serialize: vi.fn(),
|
|
|
|
const OPT_REVEST_CERAMICA : OptionID = 'op:revest_ceramica';
|
|
|
|
setLevel : vi.fn(),
|
|
|
|
|
|
|
|
setMaxLogs: vi.fn(),
|
|
|
|
const OPT_CALIDAD_ESTANDAR : OptionID = 'op:calidad_estandar';
|
|
|
|
} as unknown as Logr;
|
|
|
|
|
|
|
|
}
|
|
|
|
// Armario — material puertas
|
|
|
|
|
|
|
|
const OPT_ARMARIO_LACADO : OptionID = 'op:armario_lacado';
|
|
|
|
/** Estado base con armario lacado 2 puertas */
|
|
|
|
const OPT_ARMARIO_MADERA : OptionID = 'op:armario_madera';
|
|
|
|
function baseState(): SelectionMap {
|
|
|
|
const OPT_CALIDAD_PREMIUM : OptionID = 'op:calidad_premium';
|
|
|
|
return {
|
|
|
|
const OPT_CALIDAD_LUJO : OptionID = 'op:calidad_lujo';
|
|
|
|
[ATTR_CALIDAD] : OPT_CALIDAD_ESTANDAR,
|
|
|
|
|
|
|
|
[ATTR_SUELO] : OPT_SUELO_CERAMICA,
|
|
|
|
// ============================================================================
|
|
|
|
[ATTR_PARED] : OPT_PARED_BLANCO,
|
|
|
|
// IDs — ATRIBUTOS
|
|
|
|
[ATTR_SANITARIO] : OPT_SANIT_BLANCO,
|
|
|
|
// ============================================================================
|
|
|
|
[ATTR_REVEST_BANO] : OPT_REVEST_CERAMICA,
|
|
|
|
|
|
|
|
[ATTR_ENCIMERA] : OPT_ENCIMERA_GRANITO,
|
|
|
|
const ATTR_CALIDAD : AttrID = 'at:calidad';
|
|
|
|
[ATTR_MUEBLE_COC] : OPT_MUEBLE_BLANCO,
|
|
|
|
const ATTR_PUERTA : AttrID = 'at:puerta';
|
|
|
|
[ATTR_PUERTA] : OPT_PUERTA_LACADA,
|
|
|
|
const ATTR_SUELO : AttrID = 'at:suelo';
|
|
|
|
[ATTR_ARMARIO_PUERTAS] : OPT_ARMARIO_LACADO,
|
|
|
|
const ATTR_PARED : AttrID = 'at:pared';
|
|
|
|
};
|
|
|
|
const ATTR_ENCIMERA : AttrID = 'at:encimera';
|
|
|
|
}
|
|
|
|
const ATTR_MUEBLE_COC : AttrID = 'at:mueble_cocina';
|
|
|
|
|
|
|
|
const ATTR_M2_COCINA : AttrID = 'at:m2_cocina';
|
|
|
|
let engine: PricingEngine;
|
|
|
|
const ATTR_SANITARIO : AttrID = 'at:sanitario';
|
|
|
|
|
|
|
|
const ATTR_REVEST_BANO: AttrID = 'at:revest_bano';
|
|
|
|
beforeEach(() => {
|
|
|
|
const ATTR_M2_BANO : AttrID = 'at:m2_bano';
|
|
|
|
engine = new PricingEngine(makeLogr());
|
|
|
|
const ATTR_M2_DORM : AttrID = 'at:m2_dormitorio';
|
|
|
|
});
|
|
|
|
const ATTR_M2_SALON : AttrID = 'at:m2_salon';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Armario
|
|
|
|
|
|
|
|
const ATTR_ARMARIO_PUERTAS : AttrID = 'at:armario_puertas';
|
|
|
|
|
|
|
|
const ATTR_ARMARIO_MATERIAL : AttrID = 'at:armario_material';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
// IDs — SECCIONES
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const SEC_SALON : SectionID = 'sc:salon';
|
|
|
|
|
|
|
|
const SEC_BANO : SectionID = 'sc:bano';
|
|
|
|
|
|
|
|
const SEC_ASEO : SectionID = 'sc:aseo';
|
|
|
|
|
|
|
|
const SEC_DORMITORIO1: SectionID = 'sc:dormitorio1';
|
|
|
|
|
|
|
|
const SEC_DORMITORIO2: SectionID = 'sc:dormitorio2';
|
|
|
|
|
|
|
|
const SEC_COCINA : SectionID = 'sc:cocina';
|
|
|
|
|
|
|
|
const SEC_ARMARIO : SectionID = 'sc:armario';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
// IDs — VISTAS
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const VIEW_FRONT: ViewID = 'vw:front';
|
|
|
|
|
|
|
|
const VIEW_360 : ViewID = 'vw:360';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
// IDs — OBJETOS
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const OBJ_APARTAMENTO: ObjectID = 'ob:apartamento';
|
|
|
|
|
|
|
|
const OBJ_DUPLEX : ObjectID = 'ob:duplex';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
// IDs — REGLAS
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const RULE_LUJO_MARMOL : RuleID = 'rl:lujo_requiere_marmol';
|
|
|
|
|
|
|
|
const RULE_ESTANDAR_NO_LUJO : RuleID = 'rl:estandar_no_lujo';
|
|
|
|
|
|
|
|
const RULE_MICRO_CONSISTENTE: RuleID = 'rl:microcemento_consistente';
|
|
|
|
|
|
|
|
const RULE_MADERA_NO_BANO : RuleID = 'rl:madera_no_bano';
|
|
|
|
|
|
|
|
const RULE_NEGRO_PREMIUM : RuleID = 'rl:negro_requiere_premium';
|
|
|
|
|
|
|
|
const RULE_ESTANDAR_NO_NEGRO: RuleID = 'rl:estandar_no_negro';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
// ============================================================================
|
|
|
|
// OPCIONES
|
|
|
|
// dynamicExpression — armario lacado con descuento por volumen
|
|
|
|
// ============================================================================
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
export const OPTIONS: Record<OptionID, OptionDefinition> = {
|
|
|
|
describe('dynamicExpression — armario lacado', () => {
|
|
|
|
[OPT_SUELO_PARQUET]: {
|
|
|
|
|
|
|
|
id: OPT_SUELO_PARQUET, code: 'parquet',
|
|
|
|
|
|
|
|
name: { es: 'Parquet', en: 'Parquet' },
|
|
|
|
|
|
|
|
description: { es: 'Suelo de madera natural', en: 'Natural wood floor' },
|
|
|
|
|
|
|
|
pricing: { baseAmount: 45 }, tags: ['madera', 'calido']
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[OPT_SUELO_PORCELANICO]: {
|
|
|
|
|
|
|
|
id: OPT_SUELO_PORCELANICO, code: 'porcelanico',
|
|
|
|
|
|
|
|
name: { es: 'Porcelánico', en: 'Porcelain tile' },
|
|
|
|
|
|
|
|
description: { es: 'Suelo porcelánico de gran formato', en: 'Large format porcelain tile' },
|
|
|
|
|
|
|
|
pricing: { baseAmount: 55 }, tags: ['ceramica', 'moderno']
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[OPT_SUELO_MICROCEMENTO]: {
|
|
|
|
|
|
|
|
id: OPT_SUELO_MICROCEMENTO, code: 'microcemento',
|
|
|
|
|
|
|
|
name: { es: 'Microcemento', en: 'Microcement' },
|
|
|
|
|
|
|
|
description: { es: 'Microcemento continuo', en: 'Continuous microcement' },
|
|
|
|
|
|
|
|
pricing: { baseAmount: 75 }, tags: ['moderno', 'premium']
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[OPT_SUELO_CERAMICA]: {
|
|
|
|
|
|
|
|
id: OPT_SUELO_CERAMICA, code: 'ceramica',
|
|
|
|
|
|
|
|
name: { es: 'Cerámica', en: 'Ceramic tile' },
|
|
|
|
|
|
|
|
description: { es: 'Cerámica estándar', en: 'Standard ceramic tile' },
|
|
|
|
|
|
|
|
pricing: { baseAmount: 30 }, tags: ['ceramica', 'estandar']
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[OPT_PARED_BLANCO]: {
|
|
|
|
|
|
|
|
id: OPT_PARED_BLANCO, code: 'blanco',
|
|
|
|
|
|
|
|
name: { es: 'Blanco roto', en: 'Off white' },
|
|
|
|
|
|
|
|
description: { es: 'Pintura blanca rota', en: 'Off white paint' },
|
|
|
|
|
|
|
|
pricing: { baseAmount: 0 }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[OPT_PARED_GRIS]: {
|
|
|
|
|
|
|
|
id: OPT_PARED_GRIS, code: 'gris',
|
|
|
|
|
|
|
|
name: { es: 'Gris perla', en: 'Pearl grey' },
|
|
|
|
|
|
|
|
description: { es: 'Pintura gris perla', en: 'Pearl grey paint' },
|
|
|
|
|
|
|
|
pricing: { baseAmount: 0 }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[OPT_PARED_BEIGE]: {
|
|
|
|
|
|
|
|
id: OPT_PARED_BEIGE, code: 'beige',
|
|
|
|
|
|
|
|
name: { es: 'Beige', en: 'Beige' },
|
|
|
|
|
|
|
|
description: { es: 'Pintura beige', en: 'Beige paint' },
|
|
|
|
|
|
|
|
pricing: { baseAmount: 0 }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[OPT_PARED_PIEDRA]: {
|
|
|
|
|
|
|
|
id: OPT_PARED_PIEDRA, code: 'piedra',
|
|
|
|
|
|
|
|
name: { es: 'Efecto piedra', en: 'Stone effect' },
|
|
|
|
|
|
|
|
description: { es: 'Pintura efecto piedra', en: 'Stone effect paint' },
|
|
|
|
|
|
|
|
pricing: { baseAmount: 800 }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[OPT_PUERTA_ROBLE]: {
|
|
|
|
|
|
|
|
id: OPT_PUERTA_ROBLE, code: 'roble',
|
|
|
|
|
|
|
|
name: { es: 'Roble natural', en: 'Natural oak' },
|
|
|
|
|
|
|
|
description: { es: 'Puerta en madera de roble', en: 'Oak wood door' },
|
|
|
|
|
|
|
|
pricing: { baseAmount: 350 }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[OPT_PUERTA_LACADA]: {
|
|
|
|
|
|
|
|
id: OPT_PUERTA_LACADA, code: 'lacada',
|
|
|
|
|
|
|
|
name: { es: 'Lacada blanca', en: 'White lacquered' },
|
|
|
|
|
|
|
|
description: { es: 'Puerta lacada en blanco', en: 'White lacquered door' },
|
|
|
|
|
|
|
|
pricing: { baseAmount: 250 }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[OPT_PUERTA_CRISTAL]: {
|
|
|
|
|
|
|
|
id: OPT_PUERTA_CRISTAL, code: 'cristal',
|
|
|
|
|
|
|
|
name: { es: 'Cristal', en: 'Glass' },
|
|
|
|
|
|
|
|
description: { es: 'Puerta de cristal templado', en: 'Tempered glass door' },
|
|
|
|
|
|
|
|
pricing: { baseAmount: 500 }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[OPT_ENCIMERA_GRANITO]: {
|
|
|
|
|
|
|
|
id: OPT_ENCIMERA_GRANITO, code: 'granito',
|
|
|
|
|
|
|
|
name: { es: 'Granito', en: 'Granite' },
|
|
|
|
|
|
|
|
description: { es: 'Encimera de granito natural', en: 'Natural granite countertop' },
|
|
|
|
|
|
|
|
pricing: { baseAmount: 400 }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[OPT_ENCIMERA_SILESTONE]: {
|
|
|
|
|
|
|
|
id: OPT_ENCIMERA_SILESTONE, code: 'silestone',
|
|
|
|
|
|
|
|
name: { es: 'Silestone', en: 'Silestone' },
|
|
|
|
|
|
|
|
description: { es: 'Encimera de Silestone', en: 'Silestone countertop' },
|
|
|
|
|
|
|
|
pricing: { baseAmount: 600 }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[OPT_ENCIMERA_MADERA]: {
|
|
|
|
|
|
|
|
id: OPT_ENCIMERA_MADERA, code: 'madera',
|
|
|
|
|
|
|
|
name: { es: 'Madera', en: 'Wood' },
|
|
|
|
|
|
|
|
description: { es: 'Encimera de madera maciza', en: 'Solid wood countertop' },
|
|
|
|
|
|
|
|
pricing: { baseAmount: 500 }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[OPT_MUEBLE_BLANCO]: {
|
|
|
|
|
|
|
|
id: OPT_MUEBLE_BLANCO, code: 'blanco',
|
|
|
|
|
|
|
|
name: { es: 'Blanco mate', en: 'Matte white' },
|
|
|
|
|
|
|
|
description: { es: 'Muebles lacados en blanco mate', en: 'Matte white lacquered cabinets' },
|
|
|
|
|
|
|
|
pricing: { baseAmount: 3500 }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[OPT_MUEBLE_GRIS]: {
|
|
|
|
|
|
|
|
id: OPT_MUEBLE_GRIS, code: 'gris',
|
|
|
|
|
|
|
|
name: { es: 'Gris antracita', en: 'Anthracite grey' },
|
|
|
|
|
|
|
|
description: { es: 'Muebles en gris antracita', en: 'Anthracite grey cabinets' },
|
|
|
|
|
|
|
|
pricing: { baseAmount: 4000 }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[OPT_MUEBLE_MADERA]: {
|
|
|
|
|
|
|
|
id: OPT_MUEBLE_MADERA, code: 'madera',
|
|
|
|
|
|
|
|
name: { es: 'Madera natural', en: 'Natural wood' },
|
|
|
|
|
|
|
|
description: { es: 'Muebles en madera natural', en: 'Natural wood cabinets' },
|
|
|
|
|
|
|
|
pricing: { baseAmount: 5000 }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[OPT_SANIT_BLANCO]: {
|
|
|
|
|
|
|
|
id: OPT_SANIT_BLANCO, code: 'blanco',
|
|
|
|
|
|
|
|
name: { es: 'Blanco', en: 'White' },
|
|
|
|
|
|
|
|
description: { es: 'Sanitarios blancos', en: 'White sanitary ware' },
|
|
|
|
|
|
|
|
pricing: { baseAmount: 0 }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[OPT_SANIT_NEGRO]: {
|
|
|
|
|
|
|
|
id: OPT_SANIT_NEGRO, code: 'negro',
|
|
|
|
|
|
|
|
name: { es: 'Negro mate', en: 'Matte black' },
|
|
|
|
|
|
|
|
description: { es: 'Sanitarios en negro mate', en: 'Matte black sanitary ware' },
|
|
|
|
|
|
|
|
pricing: { baseAmount: 1200 }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[OPT_REVEST_MARMOL]: {
|
|
|
|
|
|
|
|
id: OPT_REVEST_MARMOL, code: 'marmol',
|
|
|
|
|
|
|
|
name: { es: 'Mármol', en: 'Marble' },
|
|
|
|
|
|
|
|
description: { es: 'Revestimiento de mármol natural', en: 'Natural marble cladding' },
|
|
|
|
|
|
|
|
pricing: { baseAmount: 120 }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[OPT_REVEST_MICROCEMENTO]: {
|
|
|
|
|
|
|
|
id: OPT_REVEST_MICROCEMENTO, code: 'microcemento',
|
|
|
|
|
|
|
|
name: { es: 'Microcemento', en: 'Microcement' },
|
|
|
|
|
|
|
|
description: { es: 'Revestimiento de microcemento', en: 'Microcement cladding' },
|
|
|
|
|
|
|
|
pricing: { baseAmount: 90 }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[OPT_REVEST_CERAMICA]: {
|
|
|
|
|
|
|
|
id: OPT_REVEST_CERAMICA, code: 'ceramica',
|
|
|
|
|
|
|
|
name: { es: 'Cerámica', en: 'Ceramic tile' },
|
|
|
|
|
|
|
|
description: { es: 'Revestimiento cerámico', en: 'Ceramic tile cladding' },
|
|
|
|
|
|
|
|
pricing: { baseAmount: 40 }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
// Armario — material puertas
|
|
|
|
|
|
|
|
[OPT_ARMARIO_LACADO]: {
|
|
|
|
|
|
|
|
id: OPT_ARMARIO_LACADO, code: 'lacado',
|
|
|
|
|
|
|
|
name: { es: 'Lacado blanco', en: 'White lacquered' },
|
|
|
|
|
|
|
|
description: { es: 'Puertas lacadas en blanco mate', en: 'Matte white lacquered doors' },
|
|
|
|
|
|
|
|
pricing: {
|
|
|
|
|
|
|
|
baseAmount : 0,
|
|
|
|
|
|
|
|
// Precio por puerta con descuento por volumen:
|
|
|
|
|
|
|
|
// 2 puertas → 350€/u, 3 puertas → 300€/u
|
|
|
|
|
|
|
|
dynamicExpression: {
|
|
|
|
|
|
|
|
'if': [
|
|
|
|
|
|
|
|
{ '>=': [{ var: 'quantity' }, 3] },
|
|
|
|
|
|
|
|
{ '*': [{ var: 'quantity' }, 300] },
|
|
|
|
|
|
|
|
{ '*': [{ var: 'quantity' }, 350] }
|
|
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[OPT_ARMARIO_MADERA]: {
|
|
|
|
|
|
|
|
id: OPT_ARMARIO_MADERA, code: 'madera',
|
|
|
|
|
|
|
|
name: { es: 'Madera natural', en: 'Natural wood' },
|
|
|
|
|
|
|
|
description: { es: 'Puertas en madera natural', en: 'Natural wood doors' },
|
|
|
|
|
|
|
|
pricing: {
|
|
|
|
|
|
|
|
baseAmount : 0,
|
|
|
|
|
|
|
|
// Precio fijo por puerta sin descuento
|
|
|
|
|
|
|
|
dynamicExpression: { '*': [{ var: 'quantity' }, 500] }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[OPT_CALIDAD_ESTANDAR]: {
|
|
|
|
|
|
|
|
id: OPT_CALIDAD_ESTANDAR, code: 'estandar',
|
|
|
|
|
|
|
|
name: { es: 'Estándar', en: 'Standard' },
|
|
|
|
|
|
|
|
description: { es: 'Acabados estándar', en: 'Standard quality finishes' },
|
|
|
|
|
|
|
|
pricing: { baseAmount: 0 }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[OPT_CALIDAD_PREMIUM]: {
|
|
|
|
|
|
|
|
id: OPT_CALIDAD_PREMIUM, code: 'premium',
|
|
|
|
|
|
|
|
name: { es: 'Premium', en: 'Premium' },
|
|
|
|
|
|
|
|
description: { es: 'Acabados de alta calidad', en: 'High quality finishes' },
|
|
|
|
|
|
|
|
pricing: { baseAmount: 15000 }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[OPT_CALIDAD_LUJO]: {
|
|
|
|
|
|
|
|
id: OPT_CALIDAD_LUJO, code: 'lujo',
|
|
|
|
|
|
|
|
name: { es: 'Lujo', en: 'Luxury' },
|
|
|
|
|
|
|
|
description: { es: 'Acabados de lujo exclusivos', en: 'Exclusive luxury finishes' },
|
|
|
|
|
|
|
|
pricing: { baseAmount: 35000 }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
it('2 puertas lacadas → 350 × 2 = 700', () => {
|
|
|
|
// ATRIBUTOS COMUNES
|
|
|
|
// El fixture tiene quantity: 2 por defecto en ATTR_ARMARIO_PUERTAS
|
|
|
|
// ============================================================================
|
|
|
|
const result = engine.calculate(OBJ_APARTAMENTO, baseState(), CATALOGO_VIVIENDAS);
|
|
|
|
|
|
|
|
const entry = result.breakdown.find(e => e.attrId === ATTR_ARMARIO_PUERTAS);
|
|
|
|
const attrSuelo = (): DynamicAttribute => ({
|
|
|
|
expect(entry?.amount).toBe(700);
|
|
|
|
id: ATTR_SUELO, code: 'suelo',
|
|
|
|
expect(entry?.isDynamic).toBe(true);
|
|
|
|
name: { es: 'Suelo', en: 'Flooring' },
|
|
|
|
});
|
|
|
|
description: { es: 'Material del suelo', en: 'Floor material' },
|
|
|
|
|
|
|
|
type: 'dynamic', dataType: 'reference',
|
|
|
|
|
|
|
|
defaultValue: OPT_SUELO_CERAMICA,
|
|
|
|
|
|
|
|
options: [
|
|
|
|
|
|
|
|
{ optionId: OPT_SUELO_CERAMICA, priority: 1 },
|
|
|
|
|
|
|
|
{ optionId: OPT_SUELO_PARQUET, priority: 2 },
|
|
|
|
|
|
|
|
{ optionId: OPT_SUELO_PORCELANICO, priority: 3 },
|
|
|
|
|
|
|
|
{ optionId: OPT_SUELO_MICROCEMENTO, priority: 4 },
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
display: { uiVisible: true, affectsVisual: true, affectsPrice: true }
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const attrPared = (): DynamicAttribute => ({
|
|
|
|
it('el subtotal incluye el precio del armario', () => {
|
|
|
|
id: ATTR_PARED, code: 'pared',
|
|
|
|
const result = engine.calculate(OBJ_APARTAMENTO, baseState(), CATALOGO_VIVIENDAS);
|
|
|
|
name: { es: 'Color paredes', en: 'Wall color' },
|
|
|
|
// subtotal base sin armario = 4220, con armario lacado 2 puertas = 4220 + 700
|
|
|
|
description: { es: 'Color de la pintura de paredes', en: 'Wall paint color' },
|
|
|
|
expect(result.subtotal).toBe(4220 + 700);
|
|
|
|
type: 'dynamic', dataType: 'reference',
|
|
|
|
});
|
|
|
|
defaultValue: OPT_PARED_BLANCO,
|
|
|
|
|
|
|
|
options: [
|
|
|
|
|
|
|
|
{ optionId: OPT_PARED_BLANCO, priority: 1 },
|
|
|
|
|
|
|
|
{ optionId: OPT_PARED_GRIS, priority: 2 },
|
|
|
|
|
|
|
|
{ optionId: OPT_PARED_BEIGE, priority: 3 },
|
|
|
|
|
|
|
|
{ optionId: OPT_PARED_PIEDRA, priority: 4 },
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
display: { uiVisible: true, affectsVisual: true, affectsPrice: true }
|
|
|
|
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
// ============================================================================
|
|
|
|
// VISTAS HELPERS
|
|
|
|
// dynamicExpression — armario madera precio fijo por puerta
|
|
|
|
// ============================================================================
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
const viewFront = (template: string, globalAttrDependencies?: AttrID[]): SectionView => ({
|
|
|
|
describe('dynamicExpression — armario madera', () => {
|
|
|
|
id : VIEW_FRONT,
|
|
|
|
|
|
|
|
code : 'front',
|
|
|
|
|
|
|
|
name : { es: 'Vista frontal', en: 'Front view' },
|
|
|
|
|
|
|
|
type : 'front',
|
|
|
|
|
|
|
|
visualConfig: {
|
|
|
|
|
|
|
|
strategy: 'static_image',
|
|
|
|
|
|
|
|
template,
|
|
|
|
|
|
|
|
globalAttrDependencies,
|
|
|
|
|
|
|
|
basePath : '/renders/vivienda/',
|
|
|
|
|
|
|
|
fallbackImage: '/renders/fallback.jpg',
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const view360 = (template: string, globalAttrDependencies?: AttrID[]): SectionView => ({
|
|
|
|
it('2 puertas madera → 500 × 2 = 1000', () => {
|
|
|
|
id : VIEW_360,
|
|
|
|
const state = { ...baseState(), [ATTR_ARMARIO_PUERTAS]: OPT_ARMARIO_MADERA };
|
|
|
|
code : '360',
|
|
|
|
const result = engine.calculate(OBJ_APARTAMENTO, state, CATALOGO_VIVIENDAS);
|
|
|
|
name : { es: 'Vista 360°', en: '360° view' },
|
|
|
|
const entry = result.breakdown.find(e => e.attrId === ATTR_ARMARIO_PUERTAS);
|
|
|
|
type : 'panoramic',
|
|
|
|
expect(entry?.amount).toBe(1000);
|
|
|
|
visualConfig: {
|
|
|
|
expect(entry?.isDynamic).toBe(true);
|
|
|
|
strategy: 'static_image',
|
|
|
|
});
|
|
|
|
template,
|
|
|
|
|
|
|
|
globalAttrDependencies,
|
|
|
|
|
|
|
|
basePath : '/renders/vivienda/',
|
|
|
|
|
|
|
|
fallbackImage: '/renders/fallback.jpg',
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
// ============================================================================
|
|
|
|
// SECCIONES
|
|
|
|
// QuantifiableAttribute — cantidad afecta al precio
|
|
|
|
// ============================================================================
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
const seccionSalon = (): VisualSection => ({
|
|
|
|
describe('QuantifiableAttribute — cantidad', () => {
|
|
|
|
kind : 'visual',
|
|
|
|
|
|
|
|
id : SEC_SALON,
|
|
|
|
it('3 puertas lacadas → 300 × 3 = 900 (descuento volumen)', () => {
|
|
|
|
code : 'salon',
|
|
|
|
// Modificamos el fixture en memoria cambiando la cantidad del atributo
|
|
|
|
name : { es: 'Salón', en: 'Living room' },
|
|
|
|
const catalogWith3Doors = structuredClone(CATALOGO_VIVIENDAS);
|
|
|
|
description: { es: 'Salón - comedor principal', en: 'Main living - dining room' },
|
|
|
|
const dormSection = catalogWith3Doors.objects[OBJ_APARTAMENTO]
|
|
|
|
attributes : [
|
|
|
|
.sections['sc:armario' as any] as any;
|
|
|
|
attrSuelo(),
|
|
|
|
const attr = dormSection.attributes.find((a: any) => a.id === ATTR_ARMARIO_PUERTAS);
|
|
|
|
attrPared(),
|
|
|
|
attr.quantity = 3;
|
|
|
|
{
|
|
|
|
|
|
|
|
id: ATTR_M2_SALON, code: 'm2_salon',
|
|
|
|
const result = engine.calculate(OBJ_APARTAMENTO, baseState(), catalogWith3Doors);
|
|
|
|
name: { es: 'Superficie', en: 'Surface' },
|
|
|
|
const entry = result.breakdown.find(e => e.attrId === ATTR_ARMARIO_PUERTAS);
|
|
|
|
description: { es: 'Superficie del salón en m²', en: 'Living room surface in m²' },
|
|
|
|
expect(entry?.amount).toBe(900);
|
|
|
|
type: 'fixed', dataType: 'number', value: 25,
|
|
|
|
});
|
|
|
|
unit: { es: 'm²', en: 'sqm' },
|
|
|
|
|
|
|
|
display: { uiVisible: false, readonly: true }
|
|
|
|
it('3 puertas madera → 500 × 3 = 1500', () => {
|
|
|
|
} as FixedAttribute,
|
|
|
|
const catalogWith3Doors = structuredClone(CATALOGO_VIVIENDAS);
|
|
|
|
{
|
|
|
|
const dormSection = catalogWith3Doors.objects[OBJ_APARTAMENTO]
|
|
|
|
id: ATTR_PUERTA, code: 'puerta',
|
|
|
|
.sections['sc:armario' as any] as any;
|
|
|
|
name: { es: 'Puerta', en: 'Door' },
|
|
|
|
const attr = dormSection.attributes.find((a: any) => a.id === ATTR_ARMARIO_PUERTAS);
|
|
|
|
description: { es: 'Tipo de puerta', en: 'Door type' },
|
|
|
|
attr.quantity = 3;
|
|
|
|
type: 'dynamic', dataType: 'reference',
|
|
|
|
|
|
|
|
defaultValue: OPT_PUERTA_LACADA,
|
|
|
|
const state = { ...baseState(), [ATTR_ARMARIO_PUERTAS]: OPT_ARMARIO_MADERA };
|
|
|
|
options: [
|
|
|
|
const result = engine.calculate(OBJ_APARTAMENTO, state, catalogWith3Doors);
|
|
|
|
{ optionId: OPT_PUERTA_LACADA, priority: 1 },
|
|
|
|
const entry = result.breakdown.find(e => e.attrId === ATTR_ARMARIO_PUERTAS);
|
|
|
|
{ optionId: OPT_PUERTA_ROBLE, priority: 2 },
|
|
|
|
expect(entry?.amount).toBe(1500);
|
|
|
|
{ optionId: OPT_PUERTA_CRISTAL, priority: 3 },
|
|
|
|
});
|
|
|
|
],
|
|
|
|
|
|
|
|
display: { uiVisible: true, affectsVisual: true, affectsPrice: true }
|
|
|
|
it('isDynamic true para atributo con dynamicExpression', () => {
|
|
|
|
} as DynamicAttribute,
|
|
|
|
const result = engine.calculate(OBJ_APARTAMENTO, baseState(), CATALOGO_VIVIENDAS);
|
|
|
|
],
|
|
|
|
const entry = result.breakdown.find(e => e.attrId === ATTR_ARMARIO_PUERTAS);
|
|
|
|
views : {
|
|
|
|
expect(entry?.isDynamic).toBe(true);
|
|
|
|
[VIEW_FRONT]: viewFront('{object}/{section}/{view}/{at:suelo}_{at:pared}.jpg', [ATTR_CALIDAD]),
|
|
|
|
});
|
|
|
|
[VIEW_360] : view360 ('{object}/{section}/{view}/{at:suelo}_{at:pared}.jpg', [ATTR_CALIDAD]),
|
|
|
|
|
|
|
|
},
|
|
|
|
it('isDynamic false para atributo con precio fijo', () => {
|
|
|
|
defaultView : VIEW_FRONT,
|
|
|
|
const result = engine.calculate(OBJ_APARTAMENTO, baseState(), CATALOGO_VIVIENDAS);
|
|
|
|
availability: { mode: 'required' },
|
|
|
|
const entry = result.breakdown.find(e => e.attrId === ATTR_ENCIMERA);
|
|
|
|
});
|
|
|
|
expect(entry?.isDynamic).toBe(false);
|
|
|
|
|
|
|
|
});
|
|
|
|
const seccionBano = (m2: number = 6): VisualSection => ({
|
|
|
|
|
|
|
|
kind : 'visual',
|
|
|
|
|
|
|
|
id : SEC_BANO,
|
|
|
|
|
|
|
|
code : 'bano',
|
|
|
|
|
|
|
|
name : { es: 'Baño', en: 'Bathroom' },
|
|
|
|
|
|
|
|
description: { es: 'Baño principal', en: 'Main bathroom' },
|
|
|
|
|
|
|
|
attributes : [
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
id: ATTR_SANITARIO, code: 'sanitario',
|
|
|
|
|
|
|
|
name: { es: 'Sanitarios', en: 'Sanitary ware' },
|
|
|
|
|
|
|
|
description: { es: 'Color de los sanitarios', en: 'Sanitary ware color' },
|
|
|
|
|
|
|
|
type: 'dynamic', dataType: 'reference',
|
|
|
|
|
|
|
|
defaultValue: OPT_SANIT_BLANCO,
|
|
|
|
|
|
|
|
options: [
|
|
|
|
|
|
|
|
{ optionId: OPT_SANIT_BLANCO, priority: 1 },
|
|
|
|
|
|
|
|
{ optionId: OPT_SANIT_NEGRO, priority: 2 },
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
display: { uiVisible: true, affectsVisual: true, affectsPrice: true }
|
|
|
|
|
|
|
|
} as DynamicAttribute,
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
id: ATTR_REVEST_BANO, code: 'revest_bano',
|
|
|
|
|
|
|
|
name: { es: 'Revestimiento', en: 'Cladding' },
|
|
|
|
|
|
|
|
description: { es: 'Revestimiento de paredes del baño', en: 'Bathroom wall cladding' },
|
|
|
|
|
|
|
|
type: 'dynamic', dataType: 'reference',
|
|
|
|
|
|
|
|
defaultValue: OPT_REVEST_CERAMICA,
|
|
|
|
|
|
|
|
options: [
|
|
|
|
|
|
|
|
{ optionId: OPT_REVEST_CERAMICA, priority: 1 },
|
|
|
|
|
|
|
|
{ optionId: OPT_REVEST_MICROCEMENTO, priority: 2 },
|
|
|
|
|
|
|
|
{ optionId: OPT_REVEST_MARMOL, priority: 3 },
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
display: { uiVisible: true, affectsVisual: true, affectsPrice: true }
|
|
|
|
|
|
|
|
} as DynamicAttribute,
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
id: ATTR_M2_BANO, code: 'm2_bano',
|
|
|
|
|
|
|
|
name: { es: 'Superficie', en: 'Surface' },
|
|
|
|
|
|
|
|
description: { es: 'Superficie del baño en m²', en: 'Bathroom surface in m²' },
|
|
|
|
|
|
|
|
type: 'fixed', dataType: 'number', value: m2,
|
|
|
|
|
|
|
|
unit: { es: 'm²', en: 'sqm' },
|
|
|
|
|
|
|
|
display: { uiVisible: false, readonly: true }
|
|
|
|
|
|
|
|
} as FixedAttribute,
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
views : {
|
|
|
|
|
|
|
|
[VIEW_FRONT]: viewFront('{object}/{section}/{view}/{at:sanitario}_{at:revest_bano}.jpg', [ATTR_CALIDAD]),
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
defaultView : VIEW_FRONT,
|
|
|
|
|
|
|
|
availability: { mode: 'required' },
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const seccionAseo = (): VisualSection => ({
|
|
|
|
|
|
|
|
kind : 'visual',
|
|
|
|
|
|
|
|
id : SEC_ASEO,
|
|
|
|
|
|
|
|
code : 'aseo',
|
|
|
|
|
|
|
|
name : { es: 'Aseo', en: 'Toilet' },
|
|
|
|
|
|
|
|
description: { es: 'Aseo de cortesía', en: 'Guest toilet' },
|
|
|
|
|
|
|
|
attributes : [
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
id: ATTR_SANITARIO, code: 'sanitario',
|
|
|
|
|
|
|
|
name: { es: 'Sanitarios', en: 'Sanitary ware' },
|
|
|
|
|
|
|
|
description: { es: 'Color de los sanitarios', en: 'Sanitary ware color' },
|
|
|
|
|
|
|
|
type: 'dynamic', dataType: 'reference',
|
|
|
|
|
|
|
|
defaultValue: OPT_SANIT_BLANCO,
|
|
|
|
|
|
|
|
options: [
|
|
|
|
|
|
|
|
{ optionId: OPT_SANIT_BLANCO, priority: 1 },
|
|
|
|
|
|
|
|
{ optionId: OPT_SANIT_NEGRO, priority: 2 },
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
display: { uiVisible: true, affectsVisual: true, affectsPrice: true }
|
|
|
|
|
|
|
|
} as DynamicAttribute,
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
id: ATTR_REVEST_BANO, code: 'revest_bano',
|
|
|
|
|
|
|
|
name: { es: 'Revestimiento', en: 'Cladding' },
|
|
|
|
|
|
|
|
description: { es: 'Revestimiento', en: 'Cladding' },
|
|
|
|
|
|
|
|
type: 'dynamic', dataType: 'reference',
|
|
|
|
|
|
|
|
defaultValue: OPT_REVEST_CERAMICA,
|
|
|
|
|
|
|
|
options: [
|
|
|
|
|
|
|
|
{ optionId: OPT_REVEST_CERAMICA, priority: 1 },
|
|
|
|
|
|
|
|
{ optionId: OPT_REVEST_MICROCEMENTO, priority: 2 },
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
display: { uiVisible: true, affectsVisual: true, affectsPrice: true }
|
|
|
|
|
|
|
|
} as DynamicAttribute,
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
views : {
|
|
|
|
|
|
|
|
[VIEW_FRONT]: viewFront('{object}/{section}/{view}/{at:sanitario}_{at:revest_bano}.jpg', [ATTR_CALIDAD]),
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
defaultView : VIEW_FRONT,
|
|
|
|
|
|
|
|
availability: { mode: 'required' },
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const seccionDormitorio = (id: SectionID, code: string, nombre: string, m2: number): VisualSection => ({
|
|
|
|
|
|
|
|
kind : 'visual',
|
|
|
|
|
|
|
|
id,
|
|
|
|
|
|
|
|
code,
|
|
|
|
|
|
|
|
name : { es: nombre, en: nombre.replace('Dormitorio', 'Bedroom') },
|
|
|
|
|
|
|
|
description: { es: `${nombre}`, en: `${nombre.replace('Dormitorio', 'Bedroom')}` },
|
|
|
|
|
|
|
|
attributes : [
|
|
|
|
|
|
|
|
attrSuelo(),
|
|
|
|
|
|
|
|
attrPared(),
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
id: ATTR_M2_DORM, code: 'm2_dormitorio',
|
|
|
|
|
|
|
|
name: { es: 'Superficie', en: 'Surface' },
|
|
|
|
|
|
|
|
description: { es: 'Superficie del dormitorio en m²', en: 'Bedroom surface in m²' },
|
|
|
|
|
|
|
|
type: 'fixed', dataType: 'number', value: m2,
|
|
|
|
|
|
|
|
unit: { es: 'm²', en: 'sqm' },
|
|
|
|
|
|
|
|
display: { uiVisible: false, readonly: true }
|
|
|
|
|
|
|
|
} as FixedAttribute,
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
views : {
|
|
|
|
|
|
|
|
[VIEW_FRONT]: viewFront(`{object}/{section}/{view}/{at:suelo}_{at:pared}.jpg`, [ATTR_CALIDAD]),
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
defaultView : VIEW_FRONT,
|
|
|
|
|
|
|
|
availability: { mode: 'required' },
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const seccionCocina = (): VisualSection => ({
|
|
|
|
|
|
|
|
kind : 'visual',
|
|
|
|
|
|
|
|
id : SEC_COCINA,
|
|
|
|
|
|
|
|
code : 'cocina',
|
|
|
|
|
|
|
|
name : { es: 'Cocina', en: 'Kitchen' },
|
|
|
|
|
|
|
|
description: { es: 'Cocina equipada', en: 'Equipped kitchen' },
|
|
|
|
|
|
|
|
attributes : [
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
id: ATTR_ENCIMERA, code: 'encimera',
|
|
|
|
|
|
|
|
name: { es: 'Encimera', en: 'Countertop' },
|
|
|
|
|
|
|
|
description: { es: 'Material de la encimera', en: 'Countertop material' },
|
|
|
|
|
|
|
|
type: 'dynamic', dataType: 'reference',
|
|
|
|
|
|
|
|
defaultValue: OPT_ENCIMERA_GRANITO,
|
|
|
|
|
|
|
|
options: [
|
|
|
|
|
|
|
|
{ optionId: OPT_ENCIMERA_GRANITO, priority: 1 },
|
|
|
|
|
|
|
|
{ optionId: OPT_ENCIMERA_SILESTONE, priority: 2 },
|
|
|
|
|
|
|
|
{ optionId: OPT_ENCIMERA_MADERA, priority: 3 },
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
display: { uiVisible: true, affectsVisual: true, affectsPrice: true }
|
|
|
|
|
|
|
|
} as DynamicAttribute,
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
id: ATTR_MUEBLE_COC, code: 'mueble_cocina',
|
|
|
|
|
|
|
|
name: { es: 'Muebles', en: 'Cabinets' },
|
|
|
|
|
|
|
|
description: { es: 'Muebles de cocina', en: 'Kitchen cabinets' },
|
|
|
|
|
|
|
|
type: 'dynamic', dataType: 'reference',
|
|
|
|
|
|
|
|
defaultValue: OPT_MUEBLE_BLANCO,
|
|
|
|
|
|
|
|
options: [
|
|
|
|
|
|
|
|
{ optionId: OPT_MUEBLE_BLANCO, priority: 1 },
|
|
|
|
|
|
|
|
{ optionId: OPT_MUEBLE_GRIS, priority: 2 },
|
|
|
|
|
|
|
|
{ optionId: OPT_MUEBLE_MADERA, priority: 3 },
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
display: { uiVisible: true, affectsVisual: true, affectsPrice: true }
|
|
|
|
|
|
|
|
} as DynamicAttribute,
|
|
|
|
|
|
|
|
attrSuelo(),
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
id: ATTR_M2_COCINA, code: 'm2_cocina',
|
|
|
|
|
|
|
|
name: { es: 'Superficie', en: 'Surface' },
|
|
|
|
|
|
|
|
description: { es: 'Superficie de la cocina en m²', en: 'Kitchen surface in m²' },
|
|
|
|
|
|
|
|
type: 'fixed', dataType: 'number', value: 10,
|
|
|
|
|
|
|
|
unit: { es: 'm²', en: 'sqm' },
|
|
|
|
|
|
|
|
display: { uiVisible: false, readonly: true }
|
|
|
|
|
|
|
|
} as FixedAttribute,
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
views : {
|
|
|
|
|
|
|
|
[VIEW_FRONT]: viewFront('{object}/{section}/{view}/{at:encimera}_{at:mueble_cocina}.jpg', [ATTR_CALIDAD]),
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
defaultView : VIEW_FRONT,
|
|
|
|
|
|
|
|
availability: { mode: 'required' },
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const seccionArmario = (): VisualSection => ({
|
|
|
|
|
|
|
|
kind : 'visual',
|
|
|
|
|
|
|
|
id : SEC_ARMARIO,
|
|
|
|
|
|
|
|
code : 'armario',
|
|
|
|
|
|
|
|
name : { es: 'Armario', en: 'Wardrobe' },
|
|
|
|
|
|
|
|
description: { es: 'Armario empotrado con puertas correderas', en: 'Built-in wardrobe with sliding doors' },
|
|
|
|
|
|
|
|
attributes : [
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
id : ATTR_ARMARIO_PUERTAS,
|
|
|
|
|
|
|
|
code : 'armario_puertas',
|
|
|
|
|
|
|
|
name : { es: 'Puertas de armario', en: 'Wardrobe doors' },
|
|
|
|
|
|
|
|
description : { es: 'Número y material de las puertas del armario', en: 'Number and material of wardrobe doors' },
|
|
|
|
|
|
|
|
type : 'quantifiable',
|
|
|
|
|
|
|
|
dataType : 'reference',
|
|
|
|
|
|
|
|
defaultValue: OPT_ARMARIO_LACADO,
|
|
|
|
|
|
|
|
quantity : 2,
|
|
|
|
|
|
|
|
minQuantity : 2,
|
|
|
|
|
|
|
|
maxQuantity : 3,
|
|
|
|
|
|
|
|
unit : { es: 'puertas', en: 'doors' },
|
|
|
|
|
|
|
|
options : [
|
|
|
|
|
|
|
|
{ optionId: OPT_ARMARIO_LACADO, priority: 1 },
|
|
|
|
|
|
|
|
{ optionId: OPT_ARMARIO_MADERA, priority: 2 },
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
display : { uiVisible: true, affectsVisual: true, affectsPrice: true }
|
|
|
|
|
|
|
|
} as QuantifiableAttribute,
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
id : ATTR_ARMARIO_MATERIAL,
|
|
|
|
|
|
|
|
code : 'armario_material',
|
|
|
|
|
|
|
|
name : { es: 'Material interior', en: 'Interior material' },
|
|
|
|
|
|
|
|
description : { es: 'Material del interior del armario', en: 'Wardrobe interior material' },
|
|
|
|
|
|
|
|
type : 'dynamic',
|
|
|
|
|
|
|
|
dataType : 'reference',
|
|
|
|
|
|
|
|
defaultValue: OPT_ARMARIO_LACADO,
|
|
|
|
|
|
|
|
options : [
|
|
|
|
|
|
|
|
{ optionId: OPT_ARMARIO_LACADO, priority: 1 },
|
|
|
|
|
|
|
|
{ optionId: OPT_ARMARIO_MADERA, priority: 2 },
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
display : { uiVisible: true, affectsVisual: false, affectsPrice: false }
|
|
|
|
|
|
|
|
} as DynamicAttribute,
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
views : {
|
|
|
|
|
|
|
|
[VIEW_FRONT]: viewFront('{object}/{section}/{view}/{at:armario_puertas}.jpg'),
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
defaultView : VIEW_FRONT,
|
|
|
|
|
|
|
|
availability: { mode: 'optional' },
|
|
|
|
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
// ============================================================================
|
|
|
|
// ATRIBUTO GLOBAL — CALIDAD
|
|
|
|
// dynamicExpression — fallback a baseAmount si la expresión falla
|
|
|
|
// ============================================================================
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
const attrCalidadGlobal: DynamicAttribute = {
|
|
|
|
describe('dynamicExpression — fallback a baseAmount', () => {
|
|
|
|
id: ATTR_CALIDAD, code: 'calidad',
|
|
|
|
|
|
|
|
name: { es: 'Calidad de acabados', en: 'Finish quality' },
|
|
|
|
|
|
|
|
description: { es: 'Nivel de calidad general', en: 'General finish quality level' },
|
|
|
|
|
|
|
|
type: 'dynamic', dataType: 'reference',
|
|
|
|
|
|
|
|
defaultValue: OPT_CALIDAD_ESTANDAR,
|
|
|
|
|
|
|
|
options: [
|
|
|
|
|
|
|
|
{ optionId: OPT_CALIDAD_ESTANDAR, priority: 1 },
|
|
|
|
|
|
|
|
{ optionId: OPT_CALIDAD_PREMIUM, priority: 2 },
|
|
|
|
|
|
|
|
{ optionId: OPT_CALIDAD_LUJO, priority: 3 },
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
controls: [],
|
|
|
|
|
|
|
|
display : { uiVisible: true, affectsVisual: true, affectsPrice: true }
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
it('expresión malformada → usa baseAmount y logea error', () => {
|
|
|
|
// OBJETOS
|
|
|
|
const logr = makeLogr();
|
|
|
|
// ============================================================================
|
|
|
|
const eng = new PricingEngine(logr);
|
|
|
|
|
|
|
|
const broken = structuredClone(CATALOGO_VIVIENDAS);
|
|
|
|
export const APARTAMENTO: ConfigurableObject = {
|
|
|
|
const opt = broken.options[OPT_ARMARIO_LACADO] as any;
|
|
|
|
id : OBJ_APARTAMENTO,
|
|
|
|
opt.pricing.dynamicExpression = { 'operadorFalso': null };
|
|
|
|
code : 'apartamento',
|
|
|
|
// operadorFalso devuelve null → no es number → cae a baseAmount (0)
|
|
|
|
name : { es: 'Apartamento', en: 'Apartment' },
|
|
|
|
const result = eng.calculate(OBJ_APARTAMENTO, baseState(), broken);
|
|
|
|
description: { es: 'Apartamento de 1 dormitorio (55 m²)', en: '1 bedroom apartment (55 sqm)' },
|
|
|
|
const entry = result.breakdown.find(e => e.attrId === ATTR_ARMARIO_PUERTAS);
|
|
|
|
attributes : [attrCalidadGlobal],
|
|
|
|
// baseAmount es 0 → no aparece en breakdown
|
|
|
|
sections : {
|
|
|
|
expect(entry).toBeUndefined();
|
|
|
|
[SEC_SALON] : seccionSalon(),
|
|
|
|
});
|
|
|
|
[SEC_BANO] : seccionBano(6),
|
|
|
|
});
|
|
|
|
[SEC_DORMITORIO1]: seccionDormitorio(SEC_DORMITORIO1, 'dormitorio', 'Dormitorio', 14),
|
|
|
|
|
|
|
|
[SEC_COCINA] : seccionCocina(),
|
|
|
|
|
|
|
|
[SEC_ARMARIO] : seccionArmario(),
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
sectionOrder: [SEC_SALON, SEC_COCINA, SEC_DORMITORIO1, SEC_BANO, SEC_ARMARIO],
|
|
|
|
|
|
|
|
basePath : '/renders/vivienda/',
|
|
|
|
|
|
|
|
category : 'residencial',
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const DUPLEX: ConfigurableObject = {
|
|
|
|
|
|
|
|
id : OBJ_DUPLEX,
|
|
|
|
|
|
|
|
code : 'duplex',
|
|
|
|
|
|
|
|
name : { es: 'Dúplex', en: 'Duplex' },
|
|
|
|
|
|
|
|
description: { es: 'Dúplex de 2 dormitorios (90 m²)', en: '2 bedroom duplex (90 sqm)' },
|
|
|
|
|
|
|
|
attributes : [attrCalidadGlobal],
|
|
|
|
|
|
|
|
sections : {
|
|
|
|
|
|
|
|
[SEC_SALON] : seccionSalon(),
|
|
|
|
|
|
|
|
[SEC_BANO] : seccionBano(8),
|
|
|
|
|
|
|
|
[SEC_ASEO] : seccionAseo(),
|
|
|
|
|
|
|
|
[SEC_DORMITORIO1]: seccionDormitorio(SEC_DORMITORIO1, 'dormitorio1', 'Dormitorio principal', 18),
|
|
|
|
|
|
|
|
[SEC_DORMITORIO2]: seccionDormitorio(SEC_DORMITORIO2, 'dormitorio2', 'Dormitorio secundario', 12),
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
sectionOrder: [SEC_SALON, SEC_DORMITORIO1, SEC_DORMITORIO2, SEC_BANO, SEC_ASEO],
|
|
|
|
|
|
|
|
basePath : '/renders/vivienda/',
|
|
|
|
|
|
|
|
category : 'residencial',
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
// REGLAS
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const RULES: Record<RuleID, ValidationRule> = {
|
|
|
|
|
|
|
|
[RULE_LUJO_MARMOL]: {
|
|
|
|
|
|
|
|
id : RULE_LUJO_MARMOL,
|
|
|
|
|
|
|
|
name : { es: 'Lujo requiere mármol', en: 'Luxury requires marble' },
|
|
|
|
|
|
|
|
condition: { '==': [{ var: 'attributes.at_calidad' }, OPT_CALIDAD_LUJO] },
|
|
|
|
|
|
|
|
action : { type: 'require', targetAttr: ATTR_REVEST_BANO, values: [OPT_REVEST_MARMOL] },
|
|
|
|
|
|
|
|
priority : 10,
|
|
|
|
|
|
|
|
affects : [ATTR_CALIDAD, ATTR_REVEST_BANO],
|
|
|
|
|
|
|
|
severity : 'warning',
|
|
|
|
|
|
|
|
message : { es: 'El acabado de lujo recomienda mármol en el baño', en: 'Luxury finish recommends marble in bathroom' }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[RULE_ESTANDAR_NO_LUJO]: {
|
|
|
|
|
|
|
|
id : RULE_ESTANDAR_NO_LUJO,
|
|
|
|
|
|
|
|
name : { es: 'Estándar sin lujo', en: 'Standard without luxury' },
|
|
|
|
|
|
|
|
condition: { '==': [{ var: 'attributes.at_calidad' }, OPT_CALIDAD_ESTANDAR] },
|
|
|
|
|
|
|
|
action : { type: 'forbid', targetAttr: ATTR_REVEST_BANO, values: [OPT_REVEST_MARMOL] },
|
|
|
|
|
|
|
|
priority : 9,
|
|
|
|
|
|
|
|
affects : [ATTR_CALIDAD, ATTR_REVEST_BANO],
|
|
|
|
|
|
|
|
severity : 'error',
|
|
|
|
|
|
|
|
message : { es: 'El mármol no está disponible en calidad estándar', en: 'Marble is not available in standard quality' }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[RULE_MICRO_CONSISTENTE]: {
|
|
|
|
|
|
|
|
id : RULE_MICRO_CONSISTENTE,
|
|
|
|
|
|
|
|
name : { es: 'Microcemento consistente', en: 'Consistent microcement' },
|
|
|
|
|
|
|
|
condition: { '==': [{ var: 'attributes.at_suelo' }, OPT_SUELO_MICROCEMENTO] },
|
|
|
|
|
|
|
|
action : { type: 'allow', targetAttr: ATTR_REVEST_BANO, values: [OPT_REVEST_MICROCEMENTO, OPT_REVEST_CERAMICA] },
|
|
|
|
|
|
|
|
priority : 5,
|
|
|
|
|
|
|
|
affects : [ATTR_SUELO, ATTR_REVEST_BANO],
|
|
|
|
|
|
|
|
severity : 'warning',
|
|
|
|
|
|
|
|
message : { es: 'Con microcemento en suelo se recomienda microcemento o cerámica en baño', en: 'With microcement floor, microcement or ceramic is recommended in bathroom' }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[RULE_MADERA_NO_BANO]: {
|
|
|
|
|
|
|
|
id : RULE_MADERA_NO_BANO,
|
|
|
|
|
|
|
|
name : { es: 'Madera sin negro', en: 'Wood without black' },
|
|
|
|
|
|
|
|
condition: { '==': [{ var: 'attributes.at_encimera' }, OPT_ENCIMERA_MADERA] },
|
|
|
|
|
|
|
|
action : { type: 'forbid', targetAttr: ATTR_SANITARIO, values: [OPT_SANIT_NEGRO] },
|
|
|
|
|
|
|
|
priority : 7,
|
|
|
|
|
|
|
|
affects : [ATTR_ENCIMERA, ATTR_SANITARIO],
|
|
|
|
|
|
|
|
severity : 'warning',
|
|
|
|
|
|
|
|
message : { es: 'La encimera de madera no combina con sanitarios negros', en: 'Wood countertop does not pair well with black sanitary ware' }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[RULE_NEGRO_PREMIUM]: {
|
|
|
|
|
|
|
|
id : RULE_NEGRO_PREMIUM,
|
|
|
|
|
|
|
|
name : { es: 'Negro requiere premium', en: 'Black requires premium' },
|
|
|
|
|
|
|
|
condition: { '==': [{ var: 'attributes.at_sanitario' }, OPT_SANIT_NEGRO] },
|
|
|
|
|
|
|
|
action : { type: 'forbid', targetAttr: ATTR_CALIDAD, values: [OPT_CALIDAD_ESTANDAR] },
|
|
|
|
|
|
|
|
priority : 8,
|
|
|
|
|
|
|
|
affects : [ATTR_SANITARIO, ATTR_CALIDAD],
|
|
|
|
|
|
|
|
severity : 'error',
|
|
|
|
|
|
|
|
message : { es: 'Los sanitarios negros requieren calidad premium o superior', en: 'Black sanitary ware requires premium quality or higher' }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[RULE_ESTANDAR_NO_NEGRO]: {
|
|
|
|
|
|
|
|
id : RULE_ESTANDAR_NO_NEGRO,
|
|
|
|
|
|
|
|
name : { es: 'Estándar prohíbe negro', en: 'Standard forbids black' },
|
|
|
|
|
|
|
|
condition: { '==': [{ var: 'attributes.at_calidad' }, OPT_CALIDAD_ESTANDAR] },
|
|
|
|
|
|
|
|
action : { type: 'forbid', targetAttr: ATTR_SANITARIO, values: [OPT_SANIT_NEGRO] },
|
|
|
|
|
|
|
|
priority : 8,
|
|
|
|
|
|
|
|
affects : [ATTR_CALIDAD, ATTR_SANITARIO],
|
|
|
|
|
|
|
|
severity : 'error',
|
|
|
|
|
|
|
|
message : { es: 'La calidad estándar no permite sanitarios en negro', en: 'Standard quality does not allow black sanitary ware' }
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
// CATÁLOGO COMPLETO
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const CATALOGO_VIVIENDAS: ConfigurationCatalog = {
|
|
|
|
|
|
|
|
id : 'ct:viviendas',
|
|
|
|
|
|
|
|
code : 'viviendas',
|
|
|
|
|
|
|
|
name : { es: 'Catálogo de Viviendas', en: 'Housing Catalog' },
|
|
|
|
|
|
|
|
description: { es: 'Configurador de acabados para viviendas de obra nueva', en: 'Finish configurator for new build homes' },
|
|
|
|
|
|
|
|
basePath : '/renders/',
|
|
|
|
|
|
|
|
options : OPTIONS,
|
|
|
|
|
|
|
|
objects : {
|
|
|
|
|
|
|
|
[OBJ_APARTAMENTO]: APARTAMENTO,
|
|
|
|
|
|
|
|
[OBJ_DUPLEX] : DUPLEX,
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
rules : RULES,
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
// EXPORTS DE IDs — para uso en tests
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const IDS_TEST = {
|
|
|
|
|
|
|
|
OBJ_APARTAMENTO, OBJ_DUPLEX,
|
|
|
|
|
|
|
|
SEC_SALON, SEC_BANO, SEC_ASEO, SEC_DORMITORIO1, SEC_DORMITORIO2, SEC_COCINA,
|
|
|
|
|
|
|
|
VIEW_FRONT, VIEW_360,
|
|
|
|
|
|
|
|
ATTR_CALIDAD, ATTR_SUELO, ATTR_PARED, ATTR_PUERTA,
|
|
|
|
|
|
|
|
ATTR_ENCIMERA, ATTR_MUEBLE_COC, ATTR_SANITARIO, ATTR_REVEST_BANO,
|
|
|
|
|
|
|
|
ATTR_M2_SALON, ATTR_M2_BANO, ATTR_M2_DORM, ATTR_M2_COCINA,
|
|
|
|
|
|
|
|
OPT_CALIDAD_ESTANDAR, OPT_CALIDAD_PREMIUM, OPT_CALIDAD_LUJO,
|
|
|
|
|
|
|
|
OPT_SUELO_PARQUET, OPT_SUELO_PORCELANICO, OPT_SUELO_MICROCEMENTO, OPT_SUELO_CERAMICA,
|
|
|
|
|
|
|
|
OPT_PARED_BLANCO, OPT_PARED_GRIS, OPT_PARED_BEIGE, OPT_PARED_PIEDRA,
|
|
|
|
|
|
|
|
OPT_PUERTA_ROBLE, OPT_PUERTA_LACADA, OPT_PUERTA_CRISTAL,
|
|
|
|
|
|
|
|
OPT_ENCIMERA_GRANITO, OPT_ENCIMERA_SILESTONE, OPT_ENCIMERA_MADERA,
|
|
|
|
|
|
|
|
OPT_MUEBLE_BLANCO, OPT_MUEBLE_GRIS, OPT_MUEBLE_MADERA,
|
|
|
|
|
|
|
|
OPT_SANIT_BLANCO, OPT_SANIT_NEGRO,
|
|
|
|
|
|
|
|
OPT_REVEST_MARMOL, OPT_REVEST_MICROCEMENTO, OPT_REVEST_CERAMICA,
|
|
|
|
|
|
|
|
RULE_LUJO_MARMOL, RULE_ESTANDAR_NO_LUJO, RULE_MICRO_CONSISTENTE,
|
|
|
|
|
|
|
|
RULE_MADERA_NO_BANO, RULE_NEGRO_PREMIUM, RULE_ESTANDAR_NO_NEGRO,
|
|
|
|
|
|
|
|
// Armario
|
|
|
|
|
|
|
|
SEC_ARMARIO,
|
|
|
|
|
|
|
|
ATTR_ARMARIO_PUERTAS,
|
|
|
|
|
|
|
|
ATTR_ARMARIO_MATERIAL,
|
|
|
|
|
|
|
|
OPT_ARMARIO_LACADO,
|
|
|
|
|
|
|
|
OPT_ARMARIO_MADERA,
|
|
|
|
|
|
|
|
};
|
|
|
|
|