260 lines
9.9 KiB
TypeScript
260 lines
9.9 KiB
TypeScript
|
|
import assert from "node:assert/strict";
|
||
|
|
import { test } from "node:test";
|
||
|
|
|
||
|
|
import { UniqueID } from "@repo/rdx-ddd";
|
||
|
|
import { Maybe } from "@repo/rdx-utils";
|
||
|
|
|
||
|
|
import { mapResourceInput } from "../application/resource-input.mapper";
|
||
|
|
import { ResourceCatalogPersistenceMapper } from "../infrastructure/persistence/resource-catalog-persistence.mapper";
|
||
|
|
|
||
|
|
import { Resource } from "./resource.aggregate";
|
||
|
|
import { ResourceCatalog } from "./resource-catalog.aggregate";
|
||
|
|
import { seedResourceCatalog } from "./resource-catalog-seed";
|
||
|
|
import { ResourceDefinitionSchema } from "./resource-definition";
|
||
|
|
import { ResourceDefinition } from "./resource-definition.entity";
|
||
|
|
import type { ResourceInput } from "./resource-values";
|
||
|
|
import { ResourceDefinitionValues } from "./value-objects/resource-definition-values.vo";
|
||
|
|
import { ResourceSpecification, ResourceValues } from "./value-objects/resource-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 = ResourceDefinitionValues.create(input);
|
||
|
|
assert.ok(result.isSuccess);
|
||
|
|
return result.data;
|
||
|
|
}
|
||
|
|
|
||
|
|
test("secciones: nombres, identidades, referencias y un solo nivel", () => {
|
||
|
|
assert.ok(ResourceDefinitionSchema.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(ResourceDefinitionSchema.safeParse(candidate).success, false);
|
||
|
|
}
|
||
|
|
assert.ok(
|
||
|
|
ResourceDefinitionSchema.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 = ResourceDefinition.create(Maybe.none(), first, true);
|
||
|
|
assert.equal(definition.sections[0]?.id.toPrimitive(), section.id);
|
||
|
|
assert.equal(definition.sections[0]?.name.toPrimitive(), "Hardware");
|
||
|
|
const parsed = ResourceDefinitionSchema.parse(JSON.parse(JSON.stringify(first.data)));
|
||
|
|
const restored = ResourceDefinitionValues.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 = ResourceDefinitionValues.create({
|
||
|
|
kind: "field",
|
||
|
|
name: "Campo",
|
||
|
|
description: "",
|
||
|
|
type: "text",
|
||
|
|
unit: "",
|
||
|
|
choices: [],
|
||
|
|
});
|
||
|
|
assert.ok(field.isSuccess);
|
||
|
|
const root = ResourceCatalog.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<UniqueID>(), 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 = ResourceDefinitionValues.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 ResourceCatalogPersistenceMapper().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: ampliación idempotente sin cambiar disponibilidad ni referencias", () => {
|
||
|
|
const seeded = seedResourceCatalog(ResourceCatalog.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 = ResourceDefinitionValues.create({
|
||
|
|
...flat,
|
||
|
|
fields: flat.fields.map(({ fieldId, required }) => ({ fieldId, required })),
|
||
|
|
});
|
||
|
|
assert.ok(oldValues.isSuccess);
|
||
|
|
const old = ResourceDefinition.rehydrate(
|
||
|
|
{
|
||
|
|
values: oldValues.data,
|
||
|
|
active: false,
|
||
|
|
companyId: grouped.companyId,
|
||
|
|
stockKey: grouped.stockKey,
|
||
|
|
bindings: grouped.bindings,
|
||
|
|
},
|
||
|
|
grouped.id
|
||
|
|
);
|
||
|
|
const catalog = ResourceCatalog.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 = seedResourceCatalog(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.ok(next.sections.length > 0);
|
||
|
|
assert.equal(seedResourceCatalog(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 = seedResourceCatalog(ResourceCatalog.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: ResourceInput = {
|
||
|
|
name: "Registro",
|
||
|
|
templateId: selected.id.toPrimitive(),
|
||
|
|
active: true,
|
||
|
|
notes: "",
|
||
|
|
characteristics: {},
|
||
|
|
};
|
||
|
|
const context = { catalog, resources: [], references: [], actorNames: new Map() };
|
||
|
|
const mapped = mapResourceInput(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 = Resource.create(
|
||
|
|
company,
|
||
|
|
mapped.data.specification,
|
||
|
|
mapped.data.values,
|
||
|
|
actor,
|
||
|
|
"2026-10-09T12:00:00.000Z"
|
||
|
|
);
|
||
|
|
assert.ok(created.isSuccess);
|
||
|
|
const flatSpec = ResourceSpecification.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 = ResourceValues.create(input, flatSpec.data);
|
||
|
|
assert.ok(previousValues.isSuccess);
|
||
|
|
const previous = Resource.create(
|
||
|
|
company,
|
||
|
|
flatSpec.data,
|
||
|
|
previousValues.data,
|
||
|
|
actor,
|
||
|
|
"2026-10-09T11:00:00.000Z"
|
||
|
|
);
|
||
|
|
assert.ok(previous.isSuccess);
|
||
|
|
const edit = mapResourceInput({ ...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 = ResourceSpecification.create({
|
||
|
|
...spec,
|
||
|
|
fields: spec.fields.map((field) => ({
|
||
|
|
...field,
|
||
|
|
required: field.fieldId === firstField.fieldId,
|
||
|
|
})),
|
||
|
|
});
|
||
|
|
assert.ok(required.isSuccess);
|
||
|
|
assert.equal(ResourceValues.create(input, required.data).isSuccess, false);
|
||
|
|
assert.ok(
|
||
|
|
ResourceValues.create(
|
||
|
|
{ ...input, characteristics: { [firstField.fieldId]: "Dato" } },
|
||
|
|
required.data
|
||
|
|
).isSuccess
|
||
|
|
);
|
||
|
|
});
|