FactuGES_ERP/modules/identity/src/api/domain/company-memberships/company-membership.aggregate.ts
2026-07-01 09:51:22 +02:00

195 lines
5.8 KiB
TypeScript

import { AggregateRoot, type UniqueID, type UtcDate } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { CompanyMembershipStatus } from "./company-membership-status.vo";
import {
CompanyMembershipCannotAccessCompanyError,
CompanyMembershipDuplicatedRoleError,
CompanyMembershipWithoutRolesError,
} from "./errors";
export interface ICompanyMembershipCreateProps {
accountId: UniqueID;
companyId: UniqueID;
roleIds: UniqueID[];
status: CompanyMembershipStatus;
createdAt: UtcDate;
updatedAt: UtcDate;
}
export type CompanyMembershipInternalProps = ICompanyMembershipCreateProps;
export class CompanyMembership extends AggregateRoot<CompanyMembershipInternalProps> {
protected constructor(props: CompanyMembershipInternalProps, id?: UniqueID) {
super(props, id);
}
static create(
props: ICompanyMembershipCreateProps,
id?: UniqueID
): Result<CompanyMembership, Error> {
const validationResult = CompanyMembership.validateState(props.status, props.roleIds);
if (validationResult.isFailure) {
return Result.fail(validationResult.error);
}
if (!(props.accountId && props.companyId)) {
return Result.fail(new Error("Company membership accountId and companyId are required."));
}
if (!(props.createdAt && props.updatedAt)) {
return Result.fail(new Error("Company membership timestamps are required."));
}
return Result.ok(new CompanyMembership(props, id));
}
static rehydrate(props: CompanyMembershipInternalProps, id: UniqueID): CompanyMembership {
return new CompanyMembership(props, id);
}
public get accountId(): UniqueID {
return this.props.accountId;
}
public get companyId(): UniqueID {
return this.props.companyId;
}
public get roleIds(): UniqueID[] {
return [...this.props.roleIds];
}
public get status(): CompanyMembershipStatus {
return this.props.status;
}
public get createdAt(): UtcDate {
return this.props.createdAt;
}
public get updatedAt(): UtcDate {
return this.props.updatedAt;
}
public replaceRoles(params: { roleIds: UniqueID[]; updatedAt: UtcDate }): Result<boolean, Error> {
const validationResult = CompanyMembership.validateRoleIds(params.roleIds);
if (validationResult.isFailure) {
return Result.fail(validationResult.error);
}
if (CompanyMembership.sameRoleIds(this.props.roleIds, params.roleIds)) {
return Result.ok(false);
}
this.props.roleIds = [...params.roleIds];
this.props.updatedAt = params.updatedAt;
return Result.ok(true);
}
public disable(updatedAt: UtcDate): Result<boolean, Error> {
if (this.status.isDisabled()) {
return Result.ok(false);
}
this.props.status = CompanyMembershipStatus.createDisabled();
this.props.updatedAt = updatedAt;
return Result.ok(true);
}
public enable(updatedAt: UtcDate): Result<boolean, Error> {
const validationResult = CompanyMembership.validateRoleIds(this.props.roleIds);
if (validationResult.isFailure) {
return Result.fail(validationResult.error);
}
if (this.status.isActive()) {
return Result.ok(false);
}
this.props.status = CompanyMembershipStatus.createActive();
this.props.updatedAt = updatedAt;
return Result.ok(true);
}
public acceptInvitation(params: {
updatedAt: UtcDate;
roleIds?: UniqueID[];
}): Result<boolean, Error> {
const nextRoleIds = params.roleIds ? [...params.roleIds] : [...this.props.roleIds];
const validationResult = CompanyMembership.validateRoleIds(nextRoleIds);
if (validationResult.isFailure) {
return Result.fail(validationResult.error);
}
const sameStatus = this.status.isActive();
const sameRoles = CompanyMembership.sameRoleIds(this.props.roleIds, nextRoleIds);
if (sameStatus && sameRoles) {
return Result.ok(false);
}
this.props.roleIds = nextRoleIds;
this.props.status = CompanyMembershipStatus.createActive();
this.props.updatedAt = params.updatedAt;
return Result.ok(true);
}
public canAccessCompany(): Result<boolean, Error> {
const validationResult = CompanyMembership.validateState(this.status, this.props.roleIds);
if (validationResult.isFailure) {
return Result.fail(validationResult.error);
}
if (!this.status.isActive()) {
return Result.fail(
new CompanyMembershipCannotAccessCompanyError(
`Membership cannot access company while status is "${this.status.toPrimitive()}".`
)
);
}
return Result.ok(true);
}
private static validateState(
status: CompanyMembershipStatus,
roleIds: UniqueID[]
): Result<void, Error> {
const rolesValidation = CompanyMembership.validateRoleIds(roleIds, !status.isActive());
if (rolesValidation.isFailure) {
return Result.fail(rolesValidation.error);
}
return Result.ok();
}
private static validateRoleIds(roleIds: UniqueID[], allowEmpty = false): Result<void, Error> {
if (!allowEmpty && (!roleIds || roleIds.length === 0)) {
return Result.fail(
new CompanyMembershipWithoutRolesError("Active memberships must have at least one role.")
);
}
const uniqueCount = new Set((roleIds ?? []).map((roleId) => roleId.toPrimitive())).size;
if ((roleIds ?? []).length !== uniqueCount) {
return Result.fail(
new CompanyMembershipDuplicatedRoleError("Membership role IDs cannot contain duplicates.")
);
}
return Result.ok();
}
private static sameRoleIds(current: UniqueID[], next: UniqueID[]): boolean {
if (current.length !== next.length) {
return false;
}
const currentValues = current.map((roleId) => roleId.toPrimitive()).sort();
const nextValues = next.map((roleId) => roleId.toPrimitive()).sort();
return currentValues.every((value, index) => value === nextValues[index]);
}
}