import assert from "node:assert/strict"; import { test } from "node:test"; import { UniqueID } from "@repo/rdx-ddd"; import { Maybe } from "@repo/rdx-utils"; import { mapAssetInput } from "../application/asset-input.mapper"; import { AssetCatalogPersistenceMapper } from "../infrastructure/persistence/asset-catalog-persistence.mapper"; import { Asset } from "./asset.aggregate"; import { AssetCatalog } from "./asset-catalog.aggregate"; import { seedAssetCatalog } from "./asset-catalog-seed"; import { AssetDefinitionSchema } from "./asset-definition"; import { AssetDefinition } from "./asset-definition.entity"; import type { AssetInput } from "./asset-values"; import { AssetDefinitionValues } from "./value-objects/asset-definition-values.vo"; import { AssetSpecification, AssetValues } from "./value-objects/asset-values.vo"; const company = UniqueID.generateNewID(); const actor = UniqueID.generateNewID(); const fieldId = UniqueID.generateNewID().toPrimitive(); const section = { id: UniqueID.generateNewID().toPrimitive(), name: "Hardware", description: "Datos técnicos", }; const otherSection = { id: UniqueID.generateNewID().toPrimitive(), name: "Compra", description: "", }; const template = { kind: "template" as const, name: "Plantilla", description: "", sections: [section, otherSection], fields: [{ fieldId, required: false, sectionId: section.id }], }; function valid(input = template) { const result = AssetDefinitionValues.create(input); assert.ok(result.isSuccess); return result.data; } test("secciones: nombres, identidades, referencias y un solo nivel", () => { assert.ok(AssetDefinitionSchema.safeParse(template).success); for (const candidate of [ { ...template, sections: [section, { ...otherSection, name: " hardware " }] }, { ...template, sections: [section, { ...otherSection, id: section.id.toUpperCase() }] }, { ...template, sections: [{ ...section, name: " " }] }, { ...template, sections: [], fields: template.fields }, { ...template, sections: [{ ...section, sections: [] }] }, { ...template, sections: Array.from({ length: 31 }, (_, index) => ({ ...section, id: UniqueID.generateNewID().toPrimitive(), name: `Sección ${index}`, })), }, ]) { assert.equal(AssetDefinitionSchema.safeParse(candidate).success, false); } assert.ok( AssetDefinitionSchema.safeParse({ ...template, sections: [], fields: [{ fieldId, required: false }], }).success ); }); test("secciones: igualdad, orden, identidad y persistencia JSON", () => { const first = valid(); assert.ok(first.equals(valid())); assert.equal(first.equals(valid({ ...template, sections: [otherSection, section] })), false); assert.equal( first.equals( valid({ ...template, fields: [{ ...template.fields[0], fieldId, required: false, sectionId: otherSection.id }], }) ), false ); const definition = AssetDefinition.create(Maybe.none(), first, true); assert.equal(definition.sections[0]?.id.toPrimitive(), section.id); assert.equal(definition.sections[0]?.name.toPrimitive(), "Hardware"); const parsed = AssetDefinitionSchema.parse(JSON.parse(JSON.stringify(first.data))); const restored = AssetDefinitionValues.create(parsed); assert.ok(restored.isSuccess && first.equals(restored.data)); assert.ok(Object.isFrozen(first.data.kind === "template" ? first.data.sections : [])); }); test("secciones: plantillas globales y locales conservan separación y UUID al editar", () => { const field = AssetDefinitionValues.create({ kind: "field", name: "Campo", description: "", type: "text", unit: "", choices: [], }); assert.ok(field.isSuccess); const root = AssetCatalog.create().save(Maybe.none(), Maybe.none(), field.data); assert.ok(root.isSuccess); const id = root.data.definitions[0]?.id.toPrimitive(); assert.ok(id); const grouped = valid({ ...template, fields: [{ fieldId: id, required: false, sectionId: section.id }], }); for (const scope of [Maybe.none(), Maybe.some(company)]) { const saved = root.data.save(scope, Maybe.none(), grouped); assert.ok(saved.isSuccess); const definition = saved.data.definitions.at(-1); assert.ok(definition); assert.equal(definition.sections[0]?.id.toPrimitive(), section.id); const edited = AssetDefinitionValues.create({ ...grouped.data, name: "Editada" }); assert.ok(edited.isSuccess); const updated = saved.data.save(scope, Maybe.some(definition.id), edited.data); assert.ok(updated.isSuccess); const next = updated.data.definitions.at(-1); assert.ok(next); assert.equal(next.id.toPrimitive(), definition.id.toPrimitive()); assert.equal(next.sections[0]?.id.toPrimitive(), section.id); assert.equal(next.bindings[0]?.id.toPrimitive(), definition.bindings[0]?.id.toPrimitive()); const rows = new AssetCatalogPersistenceMapper().toPersistence(updated.data); const serialized: (typeof rows.definitions)[number] | undefined = rows.definitions.find( (item) => item.id === definition.id.toPrimitive() ); assert.ok(serialized?.values.kind === "template"); assert.deepEqual(serialized.values.sections, template.sections); } const local = root.data.save(Maybe.some(company), Maybe.none(), field.data); assert.ok(!local.isSuccess); // nombre visible duplicado const unrelated = root.data.save(Maybe.some(company), Maybe.none(), valid()); assert.ok(!unrelated.isSuccess); // no admite campos de otro catálogo o compañía }); test("secciones de serie: el arranque conserva una plantilla sin grupos y su disponibilidad", () => { const seeded = seedAssetCatalog(AssetCatalog.create()); const grouped = seeded.definitions.find( (item) => item.values.data.kind === "template" && (item.values.data.sections?.length ?? 0) > 0 ); assert.ok(grouped && grouped.values.data.kind === "template"); const { sections: omitted, ...flat } = grouped.values.data; assert.ok(omitted); const oldValues = AssetDefinitionValues.create({ ...flat, fields: flat.fields.map(({ fieldId, required }) => ({ fieldId, required })), }); assert.ok(oldValues.isSuccess); const old = AssetDefinition.rehydrate( { values: oldValues.data, active: false, companyId: grouped.companyId, stockKey: grouped.stockKey, bindings: grouped.bindings, }, grouped.id ); const catalog = AssetCatalog.rehydrate( { definitions: seeded.definitions.map((item) => (item.id.equals(old.id) ? old : item)), availability: seeded.availability, }, seeded.id ); const disabled = catalog.setActive(Maybe.some(company), old.id, false); assert.ok(disabled.isSuccess); const upgraded = seedAssetCatalog(disabled.data); const next = upgraded.definitions.find((item) => item.id.equals(old.id)); assert.ok(next); assert.equal(next.active, false); assert.equal(upgraded.active(next, Maybe.some(company)), false); assert.deepEqual( next.bindings.map((item) => item.id.toPrimitive()), old.bindings.map((item) => item.id.toPrimitive()) ); assert.equal(next.sections.length, 0); assert.equal(next, old); assert.equal(seedAssetCatalog(upgraded), upgraded); assert.ok(next.sections.every((item) => item.id.toPrimitive()[14] === "7")); }); test("especificación: secciones capturadas, edición posterior y obligatoriedad independiente", () => { const catalog = seedAssetCatalog(AssetCatalog.create()); const selected = catalog.definitions.find( (item) => item.values.data.kind === "template" && (item.values.data.sections?.length ?? 0) > 0 ); assert.ok(selected && selected.values.data.kind === "template"); const input: AssetInput = { name: "Registro", templateId: selected.id.toPrimitive(), siteId: null, status: "available", notes: "", characteristics: {}, }; const context = { catalog, brands: [], workstations: [], assets: [], relationships: [], sites: [], people: [], actorNames: new Map(), }; const mapped = mapAssetInput(input, company, context); assert.ok(mapped.isSuccess); assert.deepEqual(mapped.data.specification.toPrimitive().sections, selected.values.data.sections); assert.deepEqual( mapped.data.specification.toPrimitive().fields.map((item) => item.sectionId), selected.values.data.fields.map((item) => item.sectionId) ); const created = Asset.create( company, mapped.data.specification, mapped.data.values, actor, "2026-10-09T12:00:00.000Z" ); assert.ok(created.isSuccess); const flatSpec = AssetSpecification.create({ templateId: selected.id.toPrimitive(), templateName: "Anterior", fields: mapped.data.specification .toPrimitive() .fields.map(({ sectionId: omitted, ...field }) => { assert.ok(omitted); return field; }), }); assert.ok(flatSpec.isSuccess); const previousValues = AssetValues.create(input, flatSpec.data); assert.ok(previousValues.isSuccess); const previous = Asset.create( company, flatSpec.data, previousValues.data, actor, "2026-10-09T11:00:00.000Z" ); assert.ok(previous.isSuccess); const edit = mapAssetInput({ ...input, name: "Cambio" }, company, context, previous.data); assert.ok(edit.isSuccess); assert.equal(edit.data.specification, previous.data.specification); assert.equal(edit.data.specification.toPrimitive().sections, undefined); const spec = mapped.data.specification.toPrimitive(); const firstField = spec.fields[0]; assert.ok(firstField); const required = AssetSpecification.create({ ...spec, fields: spec.fields.map((field) => ({ ...field, required: field.fieldId === firstField.fieldId, })), }); assert.ok(required.isSuccess); assert.equal(AssetValues.create(input, required.data).isSuccess, false); assert.ok( AssetValues.create( { ...input, characteristics: { [firstField.fieldId]: "Dato" } }, required.data ).isSuccess ); });