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 { private constructor(props: Props, id?: UniqueID) { super(Object.freeze({ ...props, history: Object.freeze([...props.history]) }), id); } static create( companyId: UniqueID, values: ProcessValues, actor: Maybe, recordedAt: string ): Result { 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 { 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 { 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 ) ); } }