metis-desk/modules/companies/src/api/domain/process.test.ts

309 lines
9.3 KiB
TypeScript
Raw Normal View History

import assert from "node:assert/strict";
import { test } from "node:test";
import { UniqueID } from "@repo/rdx-ddd";
import { Maybe } from "@repo/rdx-utils";
2026-10-08 14:43:34 +00:00
import type { ProcessReviewSources } from "../application/process-review.builder";
import { buildProcessReview } from "../application/process-review.builder";
import { PersonProcessPersistenceMapper } from "../infrastructure/persistence/process-persistence.mapper";
import { PersonProcess } from "./process.aggregate";
import { ProcessTask } from "./process-task.entity";
import { ProcessTemplate } from "./process-template.aggregate";
2026-10-08 14:43:34 +00:00
import type { ProcessValuesInput } from "./process-values";
import {
ProcessTemplateInputSchema,
ProcessValuesSchema,
StartProcessInputSchema,
} from "./process-values";
2026-10-09 08:38:19 +00:00
import { ProcessTemplateValues } from "./value-objects/process-template-values.vo";
import { ProcessValues } from "./value-objects/process-values.vo";
const company = UniqueID.generateNewID();
const actor = UniqueID.generateNewID();
const person = UniqueID.generateNewID();
const templateId = UniqueID.generateNewID();
const now = "2026-10-06T12:00:00.000Z";
2026-10-10 10:01:58 +00:00
function values(input?: ProcessValuesInput) {
const result = ProcessValues.create(
input ?? {
personId: person.toPrimitive(),
templateId: templateId.toPrimitive(),
templateName: "Salida",
kind: "offboarding",
name: "Salida Ana",
state: "open",
notes: null,
tasks: [ProcessTask.create("Revisar correo").toPrimitive()],
}
);
2026-10-10 10:01:58 +00:00
assert.ok(result.isSuccess);
2026-10-10 10:01:58 +00:00
return result.data;
}
2026-10-10 10:01:58 +00:00
function process() {
const result = PersonProcess.create(company, values(), Maybe.some(actor), now);
2026-10-10 10:01:58 +00:00
assert.ok(result.isSuccess);
2026-10-10 10:01:58 +00:00
return result.data;
}
2026-10-10 10:01:58 +00:00
test("procesos: contratos estrictos, normalización y límites", () => {
const input = values().toPrimitive();
2026-10-10 10:01:58 +00:00
assert.ok(ProcessValuesSchema.safeParse(input).success);
assert.equal(ProcessValuesSchema.safeParse({ ...input, extra: true }).success, false);
assert.equal(
ProcessValuesSchema.safeParse({ ...input, tasks: [input.tasks[0], input.tasks[0]] }).success,
false
);
assert.equal(
ProcessTemplateInputSchema.safeParse({
name: "Plantilla",
kind: "change",
active: true,
notes: null,
instructions: ["Duplicada", "duplicada"],
}).success,
false
);
assert.equal(
StartProcessInputSchema.safeParse({
personId: person.toPrimitive(),
templateId: templateId.toPrimitive(),
name: "A",
notes: null,
actorId: actor.toPrimitive(),
}).success,
false
);
});
test("procesos: UUID v7, tareas con identidad, actualización idempotente e historial", () => {
const initial = process();
2026-10-10 10:01:58 +00:00
assert.equal(initial.id.toPrimitive()[14], "7");
assert.equal(initial.values.tasks[0]?.id.toPrimitive()[14], "7");
assert.equal(initial.history[0]?.id.toPrimitive()[14], "7");
const identical = initial.update(values(initial.values.toPrimitive()), actor, now);
2026-10-10 10:01:58 +00:00
assert.ok(identical.isSuccess);
assert.equal(identical.data, initial);
const changed = initial.update(
values({ ...initial.values.toPrimitive(), notes: "Revisión" }),
actor,
now
);
2026-10-10 10:01:58 +00:00
assert.ok(changed.isSuccess);
assert.ok(changed.data.id.equals(initial.id));
assert.equal(changed.data.history.length, 2);
assert.equal(initial.history.length, 1);
assert.equal(
changed.data.history[1]?.actorId.getOrUndefined()?.toPrimitive(),
actor.toPrimitive()
);
});
test("procesos: completar exige todas resueltas; cierre terminal y sujeto/tareas fijos", () => {
const initial = process();
const primitive = initial.values.toPrimitive();
2026-10-10 10:01:58 +00:00
assert.equal(
initial.update(values({ ...primitive, state: "completed" }), actor, now).isSuccess,
false
);
assert.equal(
initial.update(
values({ ...primitive, personId: UniqueID.generateNewID().toPrimitive() }),
actor,
now
).isSuccess,
false
);
assert.equal(
initial.update(
values({ ...primitive, tasks: primitive.tasks.map((t) => ({ ...t, title: "Otra tarea" })) }),
actor,
now
).isSuccess,
false
);
const finished = initial.update(
values({
...primitive,
state: "completed",
tasks: primitive.tasks.map((t) => ({ ...t, status: "not_applicable" })),
}),
actor,
now
);
2026-10-10 10:01:58 +00:00
assert.ok(finished.isSuccess);
assert.equal(
finished.data.update(
values({ ...finished.data.values.toPrimitive(), notes: "Alterar cerrado" }),
actor,
now
).isSuccess,
false
);
const same = finished.data.update(finished.data.values, actor, now);
2026-10-10 10:01:58 +00:00
assert.ok(same.isSuccess);
assert.equal(same.data, finished.data);
});
test("procesos: añadir revisiones conserva las resueltas e identidades y no duplica", () => {
const initial = process();
const reference = {
kind: "remote" as const,
id: UniqueID.generateNewID().toPrimitive(),
label: "VPN",
sourceKind: "personal_computer" as const,
};
const appended = initial.appendReviews([reference], actor, now);
2026-10-10 10:01:58 +00:00
assert.ok(appended.isSuccess);
assert.equal(appended.data.values.tasks.length, 2);
assert.ok(appended.data.values.tasks[0]?.id.equals(initial.values.tasks[0]?.id));
const primitive = appended.data.values.toPrimitive();
const resolved = appended.data.update(
values({ ...primitive, tasks: primitive.tasks.map((t) => ({ ...t, status: "done" })) }),
actor,
now
);
2026-10-10 10:01:58 +00:00
assert.ok(resolved.isSuccess);
const repeat = resolved.data.appendReviews([reference], actor, now);
2026-10-10 10:01:58 +00:00
assert.ok(repeat.isSuccess);
assert.equal(repeat.data, resolved.data);
assert.equal(repeat.data.history.length, 3);
});
test("plantillas: copia de instrucciones, igualdad semántica y cambios independientes", () => {
const result = ProcessTemplateValues.create({
name: "Salida",
kind: "offboarding",
active: true,
notes: null,
instructions: ["Comprobar"],
});
2026-10-10 10:01:58 +00:00
assert.ok(result.isSuccess);
const template = ProcessTemplate.create(result.data, Maybe.some(actor), now);
2026-10-10 10:01:58 +00:00
assert.ok(template.isSuccess);
const initial = process();
const input = { ...result.data.toPrimitive(), instructions: ["Nueva lista"], active: false };
const changed = ProcessTemplateValues.create(input);
2026-10-10 10:01:58 +00:00
assert.ok(changed.isSuccess);
const updated = template.data.update(changed.data, actor, now);
2026-10-10 10:01:58 +00:00
assert.ok(updated.isSuccess);
assert.equal(updated.data.history.length, 2);
assert.equal(initial.values.tasks[0]?.toPrimitive().title, "Revisar correo");
});
test("procesos: rehidratación bidireccional y rechazo de raíces/historial alterados", () => {
const mapper = new PersonProcessPersistenceMapper();
const initial = process();
const record = mapper.toPersistence(initial);
2026-10-10 10:01:58 +00:00
assert.deepEqual(
mapper.toDomain(record.root, record.revisions).values.toPrimitive(),
initial.values.toPrimitive()
);
assert.throws(() => mapper.toDomain({ ...record.root, currentKey: "bad" }, record.revisions));
assert.throws(() => mapper.toDomain(record.root, []));
assert.throws(() =>
mapper.toDomain(
record.root,
record.revisions.map((r) => ({ ...r, actorId: null }))
)
);
assert.throws(() =>
mapper.toDomain(
record.root,
record.revisions.map((r) => ({ ...r, position: 3 }))
)
);
});
const sources: ProcessReviewSources = {
assets: [
{
id: UniqueID.generateNewID().toPrimitive(),
personId: person.toPrimitive(),
name: "Portátil",
available: true,
},
],
profiles: [],
authorizations: [],
configurations: [
{
id: UniqueID.generateNewID().toPrimitive(),
personId: person.toPrimitive(),
kind: "remote",
label: "VPN móvil",
state: "configured",
sourceKind: "personal_mobile",
},
{
id: UniqueID.generateNewID().toPrimitive(),
personId: person.toPrimitive(),
kind: "mail",
label: "Cerrado",
state: "removed",
},
{
id: UniqueID.generateNewID().toPrimitive(),
personId: actor.toPrimitive(),
kind: "mail",
label: "Otra persona",
state: "configured",
},
],
relationships: [
{
id: UniqueID.generateNewID().toPrimitive(),
personId: person.toPrimitive(),
kind: "account",
label: "Cuenta antigua aún configurada",
endDate: "2000-01-01",
configuration: "configured",
},
],
};
2026-10-10 10:01:58 +00:00
test("revisión: personales, retirada pendiente y relaciones finalizadas aún configuradas", () => {
const review = buildProcessReview(person.toPrimitive(), sources, "2026-10-06");
2026-10-10 10:01:58 +00:00
assert.equal(review.length, 3);
assert.ok(review.some((r) => r.kind === "remote" && r.sourceKind === "personal_mobile"));
assert.ok(review.some((r) => r.kind === "asset"));
assert.ok(review.some((r) => r.kind === "account"));
});
test("revisión: incluye configuraciones de perfiles archivados, excluye cerradas y evita duplicar", () => {
const input = {
...sources,
relationships: [...sources.relationships, ...sources.relationships],
configurations: [
...sources.configurations,
{
id: UniqueID.generateNewID().toPrimitive(),
personId: person.toPrimitive(),
kind: "printer" as const,
label: "Archivado pero por retirar",
state: "removal_pending",
},
],
};
const review = buildProcessReview(person.toPrimitive(), input, "2026-10-06");
2026-10-10 10:01:58 +00:00
assert.equal(review.length, 4);
assert.ok(review.some((r) => r.label === "Archivado pero por retirar"));
});