metis-desk/modules/companies/src/api/domain/process.aggregate.ts
2026-10-10 12:01:58 +02:00

163 lines
4.4 KiB
TypeScript

import type { UniqueID } from "@repo/rdx-ddd";
import { AggregateRoot } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
import { z } from "zod/v4";
import { PersonProcessRevision } from "./process-revision.entity";
import { ProcessTask } from "./process-task.entity";
import type { ProcessReferenceInput } from "./process-values";
import { ProcessError } from "./process-values";
import { ProcessValues } from "./value-objects/process-values.vo";
type Props = Readonly<{
companyId: UniqueID;
values: ProcessValues;
history: readonly PersonProcessRevision[];
}>;
export class PersonProcess extends AggregateRoot<Props> {
private constructor(props: Props, id?: UniqueID) {
super(Object.freeze({ ...props, history: Object.freeze([...props.history]) }), id);
}
static create(
companyId: UniqueID,
values: ProcessValues,
actor: Maybe<UniqueID>,
recordedAt: string
): Result<PersonProcess, ProcessError> {
if (
!(z.iso.datetime().safeParse(recordedAt).success && actor.getOrUndefined()) ||
values.state !== "open" ||
values.tasks.some((t) => t.toPrimitive().status !== "pending")
) {
return Result.fail(new ProcessError("INVALID_REQUEST"));
}
return Result.ok(
new PersonProcess({
companyId,
values,
history: [PersonProcessRevision.create({ values, actorId: actor, recordedAt })],
})
);
}
static rehydrate(props: Props, id: UniqueID) {
return new PersonProcess(props, id);
}
get companyId() {
return this.props.companyId;
}
get values() {
return this.props.values;
}
get history() {
return this.props.history;
}
update(
values: ProcessValues,
actor: UniqueID,
recordedAt: string
): Result<PersonProcess, ProcessError> {
if (this.values.equals(values)) {
return Result.ok(this);
}
if (
!z.iso.datetime().safeParse(recordedAt).success ||
recordedAt < (this.history.at(-1)?.recordedAt ?? "")
) {
return Result.fail(new ProcessError("INVALID_REQUEST"));
}
if (this.values.closed) {
return Result.fail(new ProcessError("PROCESS_CLOSED"));
}
if (!this.values.sameSubject(values)) {
return Result.fail(new ProcessError("PROCESS_SUBJECT_IMMUTABLE"));
}
if (!this.values.sameTaskDefinitions(values)) {
return Result.fail(new ProcessError("PROCESS_TASKS_IMMUTABLE"));
}
if (values.state === "completed" && !values.complete) {
return Result.fail(new ProcessError("PROCESS_PENDING_TASKS"));
}
return Result.ok(
new PersonProcess(
{
companyId: this.companyId,
values,
history: [
...this.history,
PersonProcessRevision.create({ values, actorId: Maybe.some(actor), recordedAt }),
],
},
this.id
)
);
}
appendReviews(
references: readonly ProcessReferenceInput[],
actor: UniqueID,
recordedAt: string
): Result<PersonProcess, ProcessError> {
if (this.values.closed) {
return Result.fail(new ProcessError("PROCESS_CLOSED"));
}
if (this.values.kind === "onboarding") {
return Result.ok(this);
}
const known = new Set(
this.values.tasks.flatMap((t) => {
const ref = t.toPrimitive().reference;
return ref ? [`${ref.kind}:${ref.id}`] : [];
})
);
const additions = references
.filter((ref) => !known.has(`${ref.kind}:${ref.id}`))
.map((ref) => ProcessTask.create(ref.label.slice(0, 160), ref));
if (!additions.length) {
return Result.ok(this);
}
if (
!z.iso.datetime().safeParse(recordedAt).success ||
recordedAt < (this.history.at(-1)?.recordedAt ?? "")
) {
return Result.fail(new ProcessError("INVALID_REQUEST"));
}
const values = ProcessValues.create({
...this.values.toPrimitive(),
tasks: [...this.values.tasks, ...additions].map((t) => t.toPrimitive()),
});
if (!values.isSuccess) {
return values;
}
return Result.ok(
new PersonProcess(
{
companyId: this.companyId,
values: values.data,
history: [
...this.history,
PersonProcessRevision.create({
values: values.data,
actorId: Maybe.some(actor),
recordedAt,
}),
],
},
this.id
)
);
}
}