195 lines
5.8 KiB
TypeScript
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]);
|
|
}
|
|
}
|