metis-desk/modules/companies/src/api/domain/company.aggregate.ts
David 18324f92bd feat: add person and department management features
- Implemented Person entity with validation and update methods.
- Created CalendarDate value object for date validation.
- Developed migration script to transition company_contacts to person and department structures.
- Established Sequelize models for departments and assignments.
- Introduced request DTOs for person and department management.
- Added shared DTO schemas for validation.
- Built assignment and department forms with validation and submission handling.
- Created people section to manage and display company personnel.
- Implemented person form for adding and editing personnel details.
2026-10-03 13:13:15 +02:00

286 lines
9.5 KiB
TypeScript

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<CompanyProps> {
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<CompanyProps, "name" | "active">): Result<Company, CompanyError> {
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<CompanyProps, "name" | "active">): Result<Company, CompanyError> {
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<Company, CompanyError> {
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<PersonProps, "companyId">
): Result<Company, CompanyError> {
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<Company, CompanyError> {
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<DepartmentProps, "companyId">
): Result<Company, CompanyError> {
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<DepartmentAssignment, CompanyError> {
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<Company, CompanyError> {
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<Company, CompanyError> {
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<Company, CompanyError> {
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<SiteProps, "companyId">): Result<Company, CompanyError> {
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
)
);
}
}