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 { protected constructor(props: CompanyMembershipInternalProps, id?: UniqueID) { super(props, id); } static create( props: ICompanyMembershipCreateProps, id?: UniqueID ): Result { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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]); } }