2026-10-07 07:34:18 +00:00
|
|
|
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";
|
2026-10-07 07:34:18 +00:00
|
|
|
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";
|
2026-10-07 07:34:18 +00:00
|
|
|
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";
|
2026-10-07 07:34:18 +00:00
|
|
|
|
|
|
|
|
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
|
|
|
|
2026-10-07 07:34:18 +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
|
|
|
|
2026-10-07 07:34:18 +00:00
|
|
|
assert.ok(result.isSuccess);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-07 07:34:18 +00:00
|
|
|
return result.data;
|
|
|
|
|
}
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-07 07:34:18 +00:00
|
|
|
function process() {
|
|
|
|
|
const result = PersonProcess.create(company, values(), Maybe.some(actor), now);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-07 07:34:18 +00:00
|
|
|
assert.ok(result.isSuccess);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-07 07:34:18 +00:00
|
|
|
return result.data;
|
|
|
|
|
}
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-07 07:34:18 +00:00
|
|
|
test("procesos: contratos estrictos, normalización y límites", () => {
|
|
|
|
|
const input = values().toPrimitive();
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-07 07:34:18 +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
|
|
|
|
2026-10-07 07:34:18 +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
|
|
|
|
2026-10-07 07:34:18 +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
|
|
|
|
2026-10-07 07:34:18 +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
|
|
|
|
2026-10-07 07:34:18 +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
|
|
|
|
2026-10-07 07:34:18 +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
|
|
|
|
2026-10-07 07:34:18 +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
|
|
|
|
2026-10-07 07:34:18 +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
|
|
|
|
2026-10-07 07:34:18 +00:00
|
|
|
assert.ok(resolved.isSuccess);
|
|
|
|
|
const repeat = resolved.data.appendReviews([reference], actor, now);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-07 07:34:18 +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
|
|
|
|
2026-10-07 07:34:18 +00:00
|
|
|
assert.ok(result.isSuccess);
|
|
|
|
|
const template = ProcessTemplate.create(result.data, Maybe.some(actor), now);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-07 07:34:18 +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
|
|
|
|
2026-10-07 07:34:18 +00:00
|
|
|
assert.ok(changed.isSuccess);
|
|
|
|
|
const updated = template.data.update(changed.data, actor, now);
|
2026-10-10 10:01:58 +00:00
|
|
|
|
2026-10-07 07:34:18 +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
|
|
|
|
2026-10-07 07:34:18 +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
|
|
|
|
2026-10-07 07:34:18 +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
|
|
|
|
2026-10-07 07:34:18 +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
|
|
|
|
2026-10-07 07:34:18 +00:00
|
|
|
assert.equal(review.length, 4);
|
|
|
|
|
assert.ok(review.some((r) => r.label === "Archivado pero por retirar"));
|
|
|
|
|
});
|