import assert from "node:assert/strict"; import { test } from "node:test"; import { UniqueID } from "@repo/rdx-ddd"; import { Maybe } from "@repo/rdx-utils"; import { AssetCatalog } from "./asset-catalog.aggregate"; import { seedAssetCatalog } from "./asset-catalog-seed"; import type { AssetDefinitionSpec } from "./asset-definition"; import { AssetDefinitionSchema } from "./asset-definition"; import { AssetDefinition, TemplateField } from "./asset-definition.entity"; import { AssetDefinitionValues } from "./value-objects/asset-definition-values.vo"; const global = Maybe.none(); function values(spec: AssetDefinitionSpec) { const result = AssetDefinitionValues.create(spec); assert.ok(result.isSuccess); return result.data; } function field(name: string) { return values({ kind: "field", name, type: "text", unit: "", choices: [], description: "" }); } function changed(result: ReturnType) { assert.ok(result.isSuccess); return result.data; } test("el catálogo inicial es idempotente, con UUID propios y características opcionales", () => { const catalog = seedAssetCatalog(AssetCatalog.create()); assert.equal(catalog.definitions.length, 75); assert.equal(seedAssetCatalog(catalog), catalog); const ids = [ catalog.id, ...catalog.definitions.map((item) => item.id), ...catalog.definitions.flatMap((item) => item.bindings.map((binding) => binding.id)), ].map((id) => id.toPrimitive()); assert.equal(new Set(ids).size, ids.length); assert.ok(ids.every((id) => id.charAt(14) === "7")); const templates = catalog.definitions.filter((item) => item.values.data.kind === "template"); assert.equal(templates.length, 12); assert.ok(templates.every((item) => item.bindings.every((field) => !field.required))); const rehydrated = AssetCatalog.rehydrate( { definitions: catalog.definitions, availability: catalog.availability }, catalog.id ); assert.ok(rehydrated.id.equals(catalog.id)); assert.ok(rehydrated.definitions[0]?.id.equals(catalog.definitions[0]?.id)); const stock = catalog.definitions[0]; assert.ok(stock); const protectedResult = catalog.save(global, Maybe.some(stock.id), field("Cambio")); assert.ok(protectedResult.isSuccess); assert.equal( protectedResult.data.definitions.find((item) => item.id.equals(stock.id))?.values.data.name, "Cambio" ); }); test("la disponibilidad por compañía conserva el global y los campos desactivados", () => { let catalog = seedAssetCatalog(AssetCatalog.create()); const first = Maybe.some(UniqueID.generateNewID()); const second = Maybe.some(UniqueID.generateNewID()); const stock = catalog.definitions[0]; assert.ok(stock); catalog = changed(catalog.setActive(first, stock.id, false)); assert.equal(catalog.active(stock, first), false); assert.equal(catalog.active(stock, second), true); assert.equal(stock.active, true); assert.equal(catalog.definitions.length, 75); const overrideId = catalog.availability[0]?.id; assert.ok(overrideId); assert.equal(changed(catalog.setActive(first, stock.id, false)), catalog); catalog = changed(catalog.setActive(first, stock.id, null)); assert.equal(catalog.active(stock, first), true); assert.ok(catalog.availability[0]?.id.equals(overrideId)); catalog = changed(catalog.setActive(global, stock.id, false)); assert.equal(seedAssetCatalog(catalog), catalog); assert.equal(catalog.active(catalog.definitions[0] ?? stock, first), false); }); test("las plantillas validan ámbito, campos activos e identidad estable al editar", () => { let catalog = seedAssetCatalog(AssetCatalog.create()); const company = Maybe.some(UniqueID.generateNewID()); const other = Maybe.some(UniqueID.generateNewID()); catalog = changed(catalog.save(company, Maybe.none(), field("Campo local"))); const local = catalog.definitions.at(-1); assert.ok(local); const template = values({ kind: "template", name: "Plantilla local", description: "", fields: [{ fieldId: local.id.toPrimitive(), required: false }], }); assert.ok(!catalog.save(global, Maybe.none(), template).isSuccess); assert.ok(!catalog.save(other, Maybe.none(), template).isSuccess); catalog = changed(catalog.save(company, Maybe.none(), template)); const saved = catalog.definitions.at(-1); assert.ok(saved); assert.equal(changed(catalog.save(company, Maybe.some(saved.id), template)), catalog); const updated = values({ ...template.data, name: "Otra denominación" }); catalog = changed(catalog.save(company, Maybe.some(saved.id), updated)); const edited = catalog.definitions.at(-1); assert.ok(edited); assert.ok(edited.id.equals(saved.id)); assert.ok(edited.bindings[0]?.id.equals(saved.bindings[0]?.id)); catalog = changed(catalog.setActive(company, local.id, false)); assert.ok( !catalog.save(company, Maybe.none(), values({ ...template.data, name: "Nueva" })).isSuccess ); assert.ok(catalog.save(company, Maybe.some(saved.id), updated).isSuccess); assert.ok(!catalog.visible(other).some((item) => item.id.equals(local.id))); }); test("los campos validan listas, igualdad semántica, duplicados y tipo fijo", () => { const list: AssetDefinitionSpec = { kind: "field", name: "Batería", description: "", type: "choice", unit: "", choices: ["Buena", "Sustituir"], }; assert.ok(!AssetDefinitionValues.create({ ...list, choices: [] }).isSuccess); assert.ok(!AssetDefinitionValues.create({ ...list, choices: ["Buena", " buena "] }).isSuccess); assert.ok(!AssetDefinitionValues.create({ ...list, type: "text" }).isSuccess); const frozen = values(list); assert.ok(Object.isFrozen(frozen.data)); if (frozen.data.kind === "field") { assert.ok(Object.isFrozen(frozen.data.choices)); } let catalog = AssetCatalog.create(); catalog = changed(catalog.save(global, Maybe.none(), frozen)); const saved = catalog.definitions[0]; assert.ok(saved); assert.ok(!catalog.save(global, Maybe.none(), values({ ...list, name: " batería " })).isSuccess); assert.ok(!catalog.save(global, Maybe.some(saved.id), field("Batería")).isSuccess); assert.ok( !catalog.save(global, Maybe.some(saved.id), values({ ...list, choices: ["Buena"] })).isSuccess ); assert.ok( catalog.save( global, Maybe.some(saved.id), values({ ...list, choices: [...list.choices, "Desconocida"] }) ).isSuccess ); }); test("iconos opcionales: cambio y retirada conservan identidad y asociaciones", () => { let catalog = seedAssetCatalog(AssetCatalog.create()); const fieldId = catalog.definitions[0]?.id.toPrimitive(); assert.ok(fieldId); const base = { kind: "template", name: "Equipo con icono", description: "", fields: [{ fieldId, required: false }], } satisfies AssetDefinitionSpec; const company = Maybe.some(UniqueID.generateNewID()); for (const scope of [global, company]) { const input = { ...base, name: scope === global ? "Plantilla global con icono" : "Plantilla local con icono", }; catalog = changed(catalog.save(scope, Maybe.none(), values(input))); const original = catalog.definitions.at(-1); assert.ok(original); catalog = changed( catalog.save(scope, Maybe.some(original.id), values({ ...input, icon: "laptop" })) ); const updated = catalog.definitions.find((item) => item.id.equals(original.id)); assert.ok(updated?.values.data.kind === "template"); assert.equal(updated.values.data.icon, "laptop"); assert.ok(updated.bindings[0]?.id.equals(original.bindings[0]?.id)); assert.equal(changed(catalog.save(scope, Maybe.some(original.id), updated.values)), catalog); catalog = changed(catalog.save(scope, Maybe.some(original.id), values(input))); const cleared = catalog.definitions.find((item) => item.id.equals(original.id)); assert.ok(cleared?.values.data.kind === "template"); assert.equal(cleared.values.data.icon, undefined); } }); test("el arranque conserva un icono de serie retirado sin cambiar UUID, campos o disponibilidad", () => { const initial = seedAssetCatalog(AssetCatalog.create()); const template = initial.definitions.find( (item) => item.stockKey.getOrUndefined() === "template:laptop" ); assert.ok(template?.values.data.kind === "template"); assert.equal(template.values.data.icon, "laptop"); const { icon, ...withoutIcon } = template.values.data; assert.ok(icon); const previous = AssetDefinition.rehydrate( { companyId: template.companyId, stockKey: template.stockKey, values: values(withoutIcon), active: false, bindings: template.bindings, }, template.id ); const upgraded = seedAssetCatalog( AssetCatalog.rehydrate( { definitions: initial.definitions.map((item) => item.id.equals(template.id) ? previous : item ), availability: initial.availability, }, initial.id ) ); const saved = upgraded.definitions.find((item) => item.id.equals(template.id)); assert.ok(saved?.values.data.kind === "template"); assert.equal(saved.values.data.icon, undefined); assert.equal(saved.active, false); assert.deepEqual(saved.values.data.fields, template.values.data.fields); assert.deepEqual(saved.bindings, template.bindings); assert.equal(seedAssetCatalog(upgraded), upgraded); }); test("los iconos desconocidos o asociados a campos se rechazan", () => { const template = { kind: "template", name: "Equipo", description: "", fields: [{ fieldId: UniqueID.generateNewID().toPrimitive(), required: false }], }; assert.equal( AssetDefinitionSchema.safeParse({ ...template, icon: "unknown-icon" }).success, false ); assert.equal( AssetDefinitionSchema.safeParse({ kind: "field", name: "Campo", description: "", type: "text", unit: "", choices: [], icon: "laptop", }).success, false ); }); test("revisión explícita de Portátil conserva identidades, copias y otras plantillas", () => { const initial = seedAssetCatalog(AssetCatalog.create()); const laptop = initial.definitions.find( (item) => item.stockKey.getOrUndefined() === "template:laptop" ); assert.ok(laptop?.values.data.kind === "template"); const oldLayout: [string, string[]][] = [ ["Identificación", ["brand", "model", "serial"]], ["Hardware", ["processor", "memory", "storage"]], ["Sistema", ["os", "hostname"]], ["Pantalla y conexiones", ["screen_size", "resolution", "connections"]], ["Red", ["ip", "mac"]], ["Compra y garantía", ["purchased_on", "warranty_until"]], ]; const sections = oldLayout.map(([name]) => ({ id: UniqueID.generateNewID().toPrimitive(), name, description: "", })); const fields = oldLayout.flatMap(([, keys], index) => keys.map((key) => { const field = initial.definitions.find( (item) => item.stockKey.getOrUndefined() === `field:${key}` ); assert.ok(field); return { fieldId: field.id.toPrimitive(), required: false, sectionId: sections[index]?.id }; }) ); const oldValues = values({ ...laptop.values.data, fields, sections }); const bindings = fields.map((field) => { const definition = initial.definitions.find((item) => item.id.toPrimitive() === field.fieldId); assert.ok(definition); return TemplateField.create(definition.id, false); }); const oldLaptop = AssetDefinition.rehydrate( { companyId: laptop.companyId, stockKey: laptop.stockKey, values: oldValues, active: false, bindings, }, laptop.id ); const copy = AssetDefinition.create( global, values({ ...oldLaptop.values.data, name: "Copia personalizada" }), true ); const before = AssetCatalog.rehydrate( { definitions: [ ...initial.definitions.filter((item) => !item.id.equals(laptop.id)), oldLaptop, copy, ], availability: initial.availability, }, initial.id ); const revision = before.seed("template:laptop", laptop.values, { refreshTemplate: true }); assert.ok(revision.isSuccess); const after = revision.data; const updated = after.definitions.find((item) => item.id.equals(laptop.id)); assert.ok(updated?.values.data.kind === "template"); assert.equal(updated.active, false); assert.deepEqual( updated.values.data.sections?.map((section) => section.name), [ "Identificación", "Hardware", "Memoria RAM", "Almacenamiento", "Pantalla", "Teclado", "Batería", "Alimentación", "Compra y garantía", "Soporte y reparación", ] ); const names = updated.values.data.fields.map( (field) => after.definitions.find((item) => item.id.toPrimitive() === field.fieldId)?.values.data.name ); for (const name of [ "Tarjeta gráfica", "Webcam", "Estándar Wi-Fi", "Versión de Bluetooth", "Teclado numérico", ]) { assert.ok(names.includes(name)); } for (const name of [ "Sistema operativo", "Nombre de equipo", "Dirección IP", "Dirección MAC", "Conexiones", ]) { assert.equal(names.includes(name), false); } for (const binding of updated.bindings) { const previous = oldLaptop.bindings.find((item) => item.fieldId.equals(binding.fieldId)); if (previous) { assert.ok(previous.id.equals(binding.id)); } } assert.equal( updated.values.data.sections?.find((section) => section.name === "Hardware")?.id, sections.find((section) => section.name === "Hardware")?.id ); assert.equal( updated.values.data.sections?.find((section) => section.name === "Pantalla")?.id, sections.find((section) => section.name === "Pantalla y conexiones")?.id ); assert.equal( after.definitions.find((item) => item.id.equals(copy.id)), copy ); const desktop = before.definitions.find( (item) => item.stockKey.getOrUndefined() === "template:desktop" ); assert.equal( after.definitions.find((item) => item.stockKey.getOrUndefined() === "template:desktop"), desktop ); assert.equal(oldLaptop.values, oldValues); assert.equal(updated.values.data.fields.length, 39); const batteryState = after.definitions.find( (item) => item.stockKey.getOrUndefined() === "field:battery_status" ); assert.ok(batteryState?.values.data.kind === "field"); assert.equal(batteryState.values.data.type, "choice"); assert.deepEqual(batteryState.values.data.choices, [ "Normal", "Desgastada", "Averiada", "Hinchada", ]); for (const key of ["memory", "storage", "numeric_keypad"]) { const field = after.definitions.find( (item) => item.stockKey.getOrUndefined() === `field:${key}` ); assert.ok(field); assert.equal( after.definitions.filter((item) => item.stockKey.getOrUndefined() === `field:${key}`).length, 1 ); } assert.ok(updated.values.data.fields.every((field) => !field.required)); assert.equal(seedAssetCatalog(after), after); }); test("plantillas globales de serie editables: conserva identidad, asociaciones, disponibilidad y cambios al reiniciar", () => { const initial = seedAssetCatalog(AssetCatalog.create()); const template = initial.definitions.find((item) => item.values.data.kind === "template"); assert.ok(template?.values.data.kind === "template"); const original = template.values.data; const input = values({ ...original, name: "Plantilla revisada", description: "Personalizada", sections: [], fields: original.fields .slice(0, 1) .map((field) => ({ fieldId: field.fieldId, required: true })), }); const result = initial.save(Maybe.none(), Maybe.some(template.id), input); assert.ok(result.isSuccess); const edited = result.data.definitions.find((item) => item.id.equals(template.id)); assert.ok(edited?.values.data.kind === "template"); assert.equal(edited.values.data.name, "Plantilla revisada"); assert.equal(edited.values.data.fields[0]?.required, true); assert.ok(edited.bindings[0]?.id.equals(template.bindings[0]?.id)); assert.equal(edited.stockKey.getOrUndefined(), template.stockKey.getOrUndefined()); assert.equal(edited.active, template.active); assert.equal(seedAssetCatalog(result.data), result.data); assert.deepEqual(template.values.data, original); const repeated = result.data.save(Maybe.none(), Maybe.some(template.id), input); assert.ok(repeated.isSuccess); assert.equal(repeated.data, result.data); const crossScope = result.data.save( Maybe.some(UniqueID.generateNewID()), Maybe.some(template.id), input ); assert.ok(!crossScope.isSuccess); const protectedField = initial.definitions.find((item) => item.values.data.kind === "field"); assert.ok(protectedField); const fieldChange = initial.save( Maybe.none(), Maybe.some(protectedField.id), values({ ...protectedField.values.data, name: "Campo revisado" }) ); assert.ok(fieldChange.isSuccess); const fieldEdited = fieldChange.data.definitions.find((item) => item.id.equals(protectedField.id) ); assert.equal(fieldEdited?.values.data.name, "Campo revisado"); assert.equal(seedAssetCatalog(fieldChange.data), fieldChange.data); }); test("campos globales de serie editables: listas ampliables, tipo fijo y opciones existentes conservadas", () => { const catalog = seedAssetCatalog(AssetCatalog.create()); const field = catalog.definitions.find( (item) => item.values.data.kind === "field" && item.values.data.type === "choice" ); assert.ok(field?.values.data.kind === "field"); const previous = field.values.data; const added = values({ ...previous, name: "Lista revisada", description: "Personalizada", unit: "unidad", choices: [...previous.choices, "Nueva opción"], }); const result = catalog.save(Maybe.none(), Maybe.some(field.id), added); assert.ok(result.isSuccess); const edited = result.data.definitions.find((item) => item.id.equals(field.id)); assert.ok(edited); assert.deepEqual(edited.values.data, added.data); assert.equal(seedAssetCatalog(result.data), result.data); assert.deepEqual(field.values.data, previous); const differentType = result.data.save( Maybe.none(), Maybe.some(field.id), values({ ...previous, type: "text", choices: [] }) ); assert.ok(!differentType.isSuccess); assert.equal(differentType.error.code, "FIELD_TYPE_FIXED"); const removedOption = result.data.save(Maybe.none(), Maybe.some(field.id), values(previous)); assert.ok(!removedOption.isSuccess); assert.equal(removedOption.error.code, "FIELD_TYPE_FIXED"); });