import type { Name, UniqueID } from "@repo/rdx-ddd"; import { AggregateRoot } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { CompanyError } from "./company-error"; import type { Department, DepartmentProps } from "./department.entity"; import type { AssignmentValues } from "./department-assignment.entity"; import { DepartmentAssignment } from "./department-assignment.entity"; import type { Person, PersonProps } from "./person.entity"; import type { Site, SiteProps } from "./site.entity"; export type CompanyProps = Readonly<{ name: Name; active: boolean; people: readonly Person[]; departments: readonly Department[]; assignments: readonly DepartmentAssignment[]; sites: readonly Site[]; }>; export class Company extends AggregateRoot { protected constructor(props: CompanyProps, id?: UniqueID) { super( { ...props, people: Object.freeze([...props.people]), departments: Object.freeze([...props.departments]), assignments: Object.freeze([...props.assignments]), sites: Object.freeze([...props.sites]), }, id ); } static create(props: Pick): Result { if (props.name.toPrimitive().length > 100) { return Result.fail(new CompanyError("INVALID_REQUEST")); } return Result.ok( new Company({ ...props, people: [], departments: [], assignments: [], sites: [] }) ); } static rehydrate(props: CompanyProps, id: UniqueID): Company { return new Company(props, id); } get name() { return this.props.name; } get active() { return this.props.active; } get people() { return this.props.people; } get contacts() { return this.people.filter((person) => person.isContact); } get departments() { return this.props.departments; } get assignments() { return this.props.assignments; } get sites() { return this.props.sites; } update(values: Pick): Result { if (values.name.toPrimitive().length > 100) { return Result.fail(new CompanyError("INVALID_REQUEST")); } return this.name.equals(values.name) && this.active === values.active ? Result.ok(this) : Result.ok(new Company({ ...this.props, ...values }, this.id)); } addPerson(contact: Person): Result { if (!contact.companyId.equals(this.id)) { return Result.fail(new CompanyError("INVALID_REQUEST")); } if (this.people.some((existing) => existing.id.equals(contact.id))) { return Result.ok(this); } return Result.ok(new Company({ ...this.props, people: [...this.people, contact] }, this.id)); } updatePerson( id: UniqueID, values: Omit ): Result { const current = this.people.find((contact) => contact.id.equals(id)); if (!current) { return Result.fail(new CompanyError("PERSON_NOT_FOUND")); } const updated = current.update(values); if (!updated.isSuccess) { return updated; } return updated.data === current ? Result.ok(this) : Result.ok( new Company( { ...this.props, people: this.people.map((contact) => contact.id.equals(id) ? updated.data : contact ), }, this.id ) ); } private departmentNameExists(name: string, except?: UniqueID): boolean { const key = name.normalize("NFC").toLowerCase(); return this.departments.some( (department) => !department.id.equals(except) && department.name.toPrimitive().normalize("NFC").toLowerCase() === key ); } addDepartment(department: Department): Result { if (!department.companyId.equals(this.id)) { return Result.fail(new CompanyError("INVALID_REQUEST")); } if (this.departments.some((current) => current.id.equals(department.id))) { return Result.ok(this); } if (this.departmentNameExists(department.name.toPrimitive())) { return Result.fail(new CompanyError("DEPARTMENT_NAME_EXISTS")); } return Result.ok( new Company({ ...this.props, departments: [...this.departments, department] }, this.id) ); } updateDepartment( id: UniqueID, values: Omit ): Result { const current = this.departments.find((department) => department.id.equals(id)); if (!current) { return Result.fail(new CompanyError("DEPARTMENT_NOT_FOUND")); } if (this.departmentNameExists(values.name.toPrimitive(), id)) { return Result.fail(new CompanyError("DEPARTMENT_NAME_EXISTS")); } const changed = current.update(values); if (!changed.isSuccess) { return changed; } return changed.data === current ? Result.ok(this) : Result.ok( new Company( { ...this.props, departments: this.departments.map((department) => department.id.equals(id) ? changed.data : department ), }, this.id ) ); } private validateAssignment( candidate: DepartmentAssignment, previous?: DepartmentAssignment ): Result { const person = this.people.find((item) => item.id.equals(candidate.personId)); const department = this.departments.find((item) => item.id.equals(candidate.departmentId)); if (!person) { return Result.fail(new CompanyError("PERSON_NOT_FOUND")); } if (!department) { return Result.fail(new CompanyError("DEPARTMENT_NOT_FOUND")); } const end = candidate.values.endDate.getOrUndefined(); // Una baja o corrección histórica sigue permitida tras desactivar la persona o el departamento. const createsOrReopens = !previous || (!end && previous.values.endDate.getOrUndefined() !== undefined); if (createsOrReopens && !(person.active && department.active)) { return Result.fail(new CompanyError("INACTIVE_ASSIGNMENT_TARGET")); } const conflict = this.assignments.some((other) => { if (other.id.equals(candidate.id) || !other.personId.equals(candidate.personId)) { return false; } const otherEnd = other.values.endDate.getOrUndefined(); if (!(end || otherEnd) && candidate.values.isPrimary && other.values.isPrimary) { return true; } if (!other.departmentId.equals(candidate.departmentId)) { return false; } if (!(end || otherEnd)) { return true; } // Periodos semiabiertos: se puede cerrar un puesto e iniciar otro el mismo día. return ( (!otherEnd || candidate.values.startDate.toPrimitive() < otherEnd.toPrimitive()) && (!end || other.values.startDate.toPrimitive() < end.toPrimitive()) ); }); return conflict ? Result.fail(new CompanyError("ASSIGNMENT_CONFLICT")) : Result.ok(candidate); } addAssignment( personId: UniqueID, departmentId: UniqueID, values: AssignmentValues, recordedAt: string ): Result { const created = DepartmentAssignment.create( { ...values, companyId: this.id, personId, departmentId }, recordedAt ); if (!created.isSuccess) { return created; } const validated = this.validateAssignment(created.data); if (!validated.isSuccess) { return validated; } return Result.ok( new Company({ ...this.props, assignments: [...this.assignments, created.data] }, this.id) ); } updateAssignment( personId: UniqueID, id: UniqueID, values: AssignmentValues, recordedAt: string ): Result { const current = this.assignments.find( (item) => item.id.equals(id) && item.personId.equals(personId) ); if (!current) { return Result.fail(new CompanyError("ASSIGNMENT_NOT_FOUND")); } const changed = current.update(values, recordedAt); if (!changed.isSuccess) { return changed; } const validated = this.validateAssignment(changed.data, current); if (!validated.isSuccess) { return validated; } return changed.data === current ? Result.ok(this) : Result.ok( new Company( { ...this.props, assignments: this.assignments.map((item) => item.id.equals(id) ? changed.data : item ), }, this.id ) ); } addSite(site: Site): Result { if (!site.companyId.equals(this.id)) { return Result.fail(new CompanyError("INVALID_REQUEST")); } if (this.sites.some((existing) => existing.id.equals(site.id))) { return Result.ok(this); } return Result.ok(new Company({ ...this.props, sites: [...this.sites, site] }, this.id)); } updateSite(id: UniqueID, values: Omit): Result { const current = this.sites.find((site) => site.id.equals(id)); if (!current) { return Result.fail(new CompanyError("SITE_NOT_FOUND")); } const updated = current.update(values); if (!updated.isSuccess) { return updated; } return updated.data === current ? Result.ok(this) : Result.ok( new Company( { ...this.props, sites: this.sites.map((site) => (site.id.equals(id) ? updated.data : site)), }, this.id ) ); } }