Limpieza
This commit is contained in:
parent
c91455ffc2
commit
4beb7aa207
@ -1,99 +0,0 @@
|
|||||||
import {
|
|
||||||
EmailAddress,
|
|
||||||
PhoneNumber,
|
|
||||||
PostalAddress,
|
|
||||||
TINNumber,
|
|
||||||
UniqueID,
|
|
||||||
} from "@/core/common/domain";
|
|
||||||
|
|
||||||
import {
|
|
||||||
type Account,
|
|
||||||
AccountStatus,
|
|
||||||
type IAccountProps,
|
|
||||||
type IAccountService,
|
|
||||||
} from "@/contexts/accounts/domain";
|
|
||||||
import { ITransactionManager } from "@/core/common/infrastructure/database";
|
|
||||||
import { logger } from "@/core/logger";
|
|
||||||
import { Maybe, Result } from "@repo/rdx-utils";
|
|
||||||
import { ICreateAccountRequestDTO } from "../presentation";
|
|
||||||
|
|
||||||
export class CreateAccountUseCase {
|
|
||||||
constructor(
|
|
||||||
private readonly accountService: IAccountService,
|
|
||||||
private readonly transactionManager: ITransactionManager
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public execute(
|
|
||||||
accountID: UniqueID,
|
|
||||||
dto: ICreateAccountRequestDTO
|
|
||||||
): Promise<Result<Account, Error>> {
|
|
||||||
return this.transactionManager.complete(async (transaction) => {
|
|
||||||
try {
|
|
||||||
const validOrErrors = this.validateAccountData(dto);
|
|
||||||
if (validOrErrors.isFailure) {
|
|
||||||
return Result.fail(validOrErrors.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = validOrErrors.data;
|
|
||||||
|
|
||||||
// Update account with dto
|
|
||||||
return await this.accountService.createAccount(accountID, data, transaction);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
logger.error(error as Error);
|
|
||||||
return Result.fail(error as Error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private validateAccountData(dto: ICreateAccountRequestDTO): Result<IAccountProps, Error> {
|
|
||||||
const errors: Error[] = [];
|
|
||||||
|
|
||||||
const tinOrError = TINNumber.create(dto.tin);
|
|
||||||
const emailOrError = EmailAddress.create(dto.email);
|
|
||||||
const phoneOrError = PhoneNumber.create(dto.phone);
|
|
||||||
const faxOrError = PhoneNumber.createNullable(dto.fax);
|
|
||||||
const postalAddressOrError = PostalAddress.create({
|
|
||||||
street: dto.street,
|
|
||||||
city: dto.city,
|
|
||||||
state: dto.state,
|
|
||||||
postalCode: dto.postal_code,
|
|
||||||
country: dto.country,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = Result.combine([
|
|
||||||
tinOrError,
|
|
||||||
emailOrError,
|
|
||||||
phoneOrError,
|
|
||||||
faxOrError,
|
|
||||||
postalAddressOrError,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (result.isFailure) {
|
|
||||||
return Result.fail(result.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const validatedData: IAccountProps = {
|
|
||||||
status: AccountStatus.createInactive(),
|
|
||||||
isFreelancer: dto.is_companyr,
|
|
||||||
name: dto.name,
|
|
||||||
tradeName: dto.trade_name ? Maybe.some(dto.trade_name) : Maybe.none(),
|
|
||||||
tin: tinOrError.data,
|
|
||||||
address: postalAddressOrError.data,
|
|
||||||
email: emailOrError.data,
|
|
||||||
phone: phoneOrError.data,
|
|
||||||
fax: faxOrError.data,
|
|
||||||
website: dto.website ? Maybe.some(dto.website) : Maybe.none(),
|
|
||||||
legalRecord: dto.legal_record,
|
|
||||||
defaultTax: dto.default_tax,
|
|
||||||
langCode: dto.language_code,
|
|
||||||
currencyCode: dto.currency_code,
|
|
||||||
logo: dto.logo ? Maybe.some(dto.logo) : Maybe.none(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (errors.length > 0) {
|
|
||||||
const message = errors.map((err) => err.message).toString();
|
|
||||||
return Result.fail(new Error(message));
|
|
||||||
}
|
|
||||||
return Result.ok(validatedData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
import { Account, IAccountService } from "@/contexts/accounts/domain";
|
|
||||||
import { UniqueID } from "@/core/common/domain";
|
|
||||||
import { ITransactionManager } from "@/core/common/infrastructure/database";
|
|
||||||
import { logger } from "@/core/logger";
|
|
||||||
import { Result } from "@repo/rdx-utils";
|
|
||||||
|
|
||||||
export class GetAccountUseCase {
|
|
||||||
constructor(
|
|
||||||
private readonly accountService: IAccountService,
|
|
||||||
private readonly transactionManager: ITransactionManager
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public execute(accountID: UniqueID): Promise<Result<Account, Error>> {
|
|
||||||
return this.transactionManager.complete(async (transaction) => {
|
|
||||||
try {
|
|
||||||
return await this.accountService.findAccountById(accountID, transaction);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
logger.error(error as Error);
|
|
||||||
return Result.fail(error as Error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export * from "./create-account.use-case";
|
|
||||||
export * from "./get-account.use-case";
|
|
||||||
export * from "./list-accounts.use-case";
|
|
||||||
export * from "./update-account.use-case";
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
import { Account, IAccountService } from "@/contexts/accounts/domain";
|
|
||||||
import { ITransactionManager } from "@/core/common/infrastructure/database";
|
|
||||||
import { Collection, Result } from "@repo/rdx-utils";
|
|
||||||
|
|
||||||
export class ListAccountsUseCase {
|
|
||||||
constructor(
|
|
||||||
private readonly accountService: IAccountService,
|
|
||||||
private readonly transactionManager: ITransactionManager
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public execute(): Promise<Result<Collection<Account>, Error>> {
|
|
||||||
return this.transactionManager.complete(async (transaction) => {
|
|
||||||
try {
|
|
||||||
return await this.accountService.findAccounts(transaction);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
logger.error(error as Error);
|
|
||||||
return Result.fail(error as Error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
import { UniqueID } from "@/core/common/domain";
|
|
||||||
import { TransactionManager } from "@/core/common/infrastructure/database";
|
|
||||||
import { Result } from "@repo/rdx-utils";
|
|
||||||
import { AccountService } from "../domain";
|
|
||||||
import { UpdateAccountUseCase } from "./update-account.use-case";
|
|
||||||
|
|
||||||
const mockAccountService: AccountService = {
|
|
||||||
updateAccountById: jest.fn(),
|
|
||||||
} as unknown as AccountService;
|
|
||||||
|
|
||||||
const mockTransactionManager: TransactionManager = {
|
|
||||||
complete(work: (transaction: any) => Promise<any>): void {
|
|
||||||
jest.fn();
|
|
||||||
},
|
|
||||||
} as unknown as TransactionManager;
|
|
||||||
|
|
||||||
const id = UniqueID.create("", true).data;
|
|
||||||
|
|
||||||
describe("UpdateAccountUseCase", () => {
|
|
||||||
let updateAccountUseCase: UpdateAccountUseCase;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
updateAccountUseCase = new UpdateAccountUseCase(mockAccountService, mockTransactionManager);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("debería actualizar una cuenta y retornar un DTO", async () => {
|
|
||||||
const mockUpdatedAccount = { id: "123", name: "Nuevo Nombre" };
|
|
||||||
(mockAccountService.updateAccountById as jest.Mock).mockResolvedValue(
|
|
||||||
Result.ok(mockUpdatedAccount)
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await updateAccountUseCase.execute(id, { name: "Nuevo Nombre" });
|
|
||||||
expect(result.isSuccess).toBe(true);
|
|
||||||
expect(result.data.name).toBe("Nuevo Nombre");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("debería retornar error si la actualización falla", async () => {
|
|
||||||
(mockAccountService.updateAccountById as jest.Mock).mockResolvedValue(
|
|
||||||
Result.fail(new Error("Account not found"))
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await updateAccountUseCase.execute(id, { name: "Nuevo Nombre" });
|
|
||||||
expect(result.isFailure).toBe(true);
|
|
||||||
expect(result.error.message).toBe("Account not found");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,127 +0,0 @@
|
|||||||
import {
|
|
||||||
EmailAddress,
|
|
||||||
PhoneNumber,
|
|
||||||
PostalAddress,
|
|
||||||
TINNumber,
|
|
||||||
UniqueID,
|
|
||||||
} from "@/core/common/domain";
|
|
||||||
|
|
||||||
import { Account, IAccountProps, IAccountService } from "@/contexts/accounts/domain";
|
|
||||||
import { ITransactionManager } from "@/core/common/infrastructure/database";
|
|
||||||
import { logger } from "@/core/logger";
|
|
||||||
import { Maybe, Result } from "@repo/rdx-utils";
|
|
||||||
import { IUpdateAccountRequestDTO } from "../presentation";
|
|
||||||
|
|
||||||
export class UpdateAccountUseCase {
|
|
||||||
constructor(
|
|
||||||
private readonly accountService: IAccountService,
|
|
||||||
private readonly transactionManager: ITransactionManager
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public execute(
|
|
||||||
accountID: UniqueID,
|
|
||||||
dto: Partial<IUpdateAccountRequestDTO>
|
|
||||||
): Promise<Result<Account, Error>> {
|
|
||||||
return this.transactionManager.complete(async (transaction) => {
|
|
||||||
try {
|
|
||||||
const validOrErrors = this.validateAccountData(dto);
|
|
||||||
if (validOrErrors.isFailure) {
|
|
||||||
return Result.fail(validOrErrors.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = validOrErrors.data;
|
|
||||||
|
|
||||||
// Update account with dto
|
|
||||||
return await this.accountService.updateAccountById(accountID, data, transaction);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
logger.error(error as Error);
|
|
||||||
return Result.fail(error as Error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private validateAccountData(
|
|
||||||
dto: Partial<IUpdateAccountRequestDTO>
|
|
||||||
): Result<Partial<IAccountProps>, Error> {
|
|
||||||
const errors: Error[] = [];
|
|
||||||
const validatedData: Partial<IAccountProps> = {};
|
|
||||||
|
|
||||||
if (dto.is_companyr) {
|
|
||||||
validatedData.isFreelancer = dto.is_companyr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dto.name) {
|
|
||||||
validatedData.name = dto.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dto.trade_name) {
|
|
||||||
validatedData.tradeName = Maybe.some(dto.trade_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dto.tin) {
|
|
||||||
const tinOrError = TINNumber.create(dto.tin);
|
|
||||||
if (tinOrError.isFailure) errors.push(tinOrError.error);
|
|
||||||
else validatedData.tin = tinOrError.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dto.email) {
|
|
||||||
const emailOrError = EmailAddress.create(dto.email);
|
|
||||||
if (emailOrError.isFailure) errors.push(emailOrError.error);
|
|
||||||
else validatedData.email = emailOrError.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dto.phone) {
|
|
||||||
const phoneOrError = PhoneNumber.create(dto.phone);
|
|
||||||
if (phoneOrError.isFailure) errors.push(phoneOrError.error);
|
|
||||||
else validatedData.phone = phoneOrError.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dto.fax) {
|
|
||||||
const faxOrError = PhoneNumber.create(dto.fax);
|
|
||||||
if (faxOrError.isFailure) errors.push(faxOrError.error);
|
|
||||||
else validatedData.fax = Maybe.some(faxOrError.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dto.street || dto.city || dto.state || dto.postal_code || dto.country) {
|
|
||||||
const postalAddressOrError = PostalAddress.create({
|
|
||||||
street: dto.street ?? "",
|
|
||||||
city: dto.city ?? "",
|
|
||||||
state: dto.state ?? "",
|
|
||||||
postalCode: dto.postal_code ?? "",
|
|
||||||
country: dto.country ?? "",
|
|
||||||
});
|
|
||||||
if (postalAddressOrError.isFailure) errors.push(postalAddressOrError.error);
|
|
||||||
else validatedData.address = postalAddressOrError.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dto.website) {
|
|
||||||
validatedData.website = Maybe.some(dto.website);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dto.legal_record) {
|
|
||||||
validatedData.legalRecord = dto.legal_record;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dto.default_tax) {
|
|
||||||
validatedData.defaultTax = dto.default_tax;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dto.language_code) {
|
|
||||||
validatedData.langCode = dto.language_code;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dto.currency_code) {
|
|
||||||
validatedData.currencyCode = dto.currency_code;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dto.logo) {
|
|
||||||
validatedData.logo = Maybe.some(dto.logo);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errors.length > 0) {
|
|
||||||
const message = errors.map((err) => err.message).toString();
|
|
||||||
return Result.fail(new Error(message));
|
|
||||||
}
|
|
||||||
return Result.ok(validatedData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,188 +0,0 @@
|
|||||||
import {
|
|
||||||
AggregateRoot,
|
|
||||||
type EmailAddress,
|
|
||||||
type PhoneNumber,
|
|
||||||
PostalAddress,
|
|
||||||
type TINNumber,
|
|
||||||
UniqueID,
|
|
||||||
} from "@/core";
|
|
||||||
import { Maybe, Result } from "@repo/rdx-utils";
|
|
||||||
import { AccountStatus } from "../value-objects";
|
|
||||||
|
|
||||||
export interface IAccountProps {
|
|
||||||
status: AccountStatus;
|
|
||||||
|
|
||||||
isFreelancer: boolean;
|
|
||||||
name: string;
|
|
||||||
tin: TINNumber;
|
|
||||||
address: PostalAddress;
|
|
||||||
email: EmailAddress;
|
|
||||||
phone: PhoneNumber;
|
|
||||||
legalRecord: string;
|
|
||||||
defaultTax: number;
|
|
||||||
|
|
||||||
langCode: string;
|
|
||||||
currencyCode: string;
|
|
||||||
|
|
||||||
tradeName: Maybe<string>;
|
|
||||||
website: Maybe<string>;
|
|
||||||
fax: Maybe<PhoneNumber>;
|
|
||||||
logo: Maybe<string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IAccount {
|
|
||||||
id: UniqueID;
|
|
||||||
status: AccountStatus;
|
|
||||||
name: string;
|
|
||||||
tin: TINNumber;
|
|
||||||
address: PostalAddress;
|
|
||||||
email: EmailAddress;
|
|
||||||
phone: PhoneNumber;
|
|
||||||
legalRecord: string;
|
|
||||||
defaultTax: number;
|
|
||||||
|
|
||||||
langCode: string;
|
|
||||||
currencyCode: string;
|
|
||||||
|
|
||||||
tradeName: Maybe<string>;
|
|
||||||
fax: Maybe<PhoneNumber>;
|
|
||||||
website: Maybe<string>;
|
|
||||||
logo: Maybe<string>;
|
|
||||||
|
|
||||||
isAccount: boolean;
|
|
||||||
isFreelancer: boolean;
|
|
||||||
isActive: boolean;
|
|
||||||
|
|
||||||
activate(): boolean;
|
|
||||||
deactivate(): boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Account extends AggregateRoot<IAccountProps> implements IAccount {
|
|
||||||
id: UniqueID;
|
|
||||||
static create(props: IAccountProps, id?: UniqueID): Result<Account, Error> {
|
|
||||||
const account = new Account(props, id);
|
|
||||||
|
|
||||||
// Reglas de negocio / validaciones
|
|
||||||
// ...
|
|
||||||
// ...
|
|
||||||
|
|
||||||
// 🔹 Disparar evento de dominio "AccountAuthenticatedEvent"
|
|
||||||
//const { account } = props;
|
|
||||||
//user.addDomainEvent(new AccountAuthenticatedEvent(id, account.toString()));
|
|
||||||
|
|
||||||
return Result.ok(account);
|
|
||||||
}
|
|
||||||
|
|
||||||
static update(oldAccount: Account, data: Partial<IAccountProps>): Result<Account, Error> {
|
|
||||||
const updatedPostalAddress = PostalAddress.update(oldAccount.address, data.address ?? {}).data;
|
|
||||||
|
|
||||||
return Account.create(
|
|
||||||
{
|
|
||||||
isFreelancer: data.isFreelancer ?? oldAccount.isFreelancer,
|
|
||||||
|
|
||||||
name: data.name ?? oldAccount.name,
|
|
||||||
tin: data.tin ?? oldAccount.tin,
|
|
||||||
|
|
||||||
address: updatedPostalAddress,
|
|
||||||
email: data.email ?? oldAccount.email,
|
|
||||||
phone: data.phone ?? oldAccount.phone,
|
|
||||||
|
|
||||||
legalRecord: data.legalRecord ?? oldAccount.legalRecord,
|
|
||||||
defaultTax: data.defaultTax ?? oldAccount.defaultTax,
|
|
||||||
status: oldAccount.props.status,
|
|
||||||
langCode: data.langCode ?? oldAccount.langCode,
|
|
||||||
currencyCode: data.currencyCode ?? oldAccount.currencyCode,
|
|
||||||
|
|
||||||
tradeName: data.tradeName ?? oldAccount.tradeName,
|
|
||||||
website: data.website ?? oldAccount.website,
|
|
||||||
fax: data.fax ?? oldAccount.fax,
|
|
||||||
logo: data.logo ?? oldAccount.logo,
|
|
||||||
},
|
|
||||||
oldAccount.id
|
|
||||||
).getOrElse(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
activate() {
|
|
||||||
if (!this.props.status.canTransitionTo("active")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.props.status = AccountStatus.createActive();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
deactivate() {
|
|
||||||
if (!this.props.status.canTransitionTo("inactive")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
this.props.status = AccountStatus.createInactive();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
get status() {
|
|
||||||
return this.props.status;
|
|
||||||
}
|
|
||||||
|
|
||||||
get name() {
|
|
||||||
return this.props.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
get tradeName() {
|
|
||||||
return this.props.tradeName;
|
|
||||||
}
|
|
||||||
|
|
||||||
get tin(): TINNumber {
|
|
||||||
return this.props.tin;
|
|
||||||
}
|
|
||||||
|
|
||||||
get address(): PostalAddress {
|
|
||||||
return this.props.address;
|
|
||||||
}
|
|
||||||
|
|
||||||
get email(): EmailAddress {
|
|
||||||
return this.props.email;
|
|
||||||
}
|
|
||||||
|
|
||||||
get phone(): PhoneNumber {
|
|
||||||
return this.props.phone;
|
|
||||||
}
|
|
||||||
|
|
||||||
get fax(): Maybe<PhoneNumber> {
|
|
||||||
return this.props.fax;
|
|
||||||
}
|
|
||||||
|
|
||||||
get website() {
|
|
||||||
return this.props.website;
|
|
||||||
}
|
|
||||||
|
|
||||||
get legalRecord() {
|
|
||||||
return this.props.legalRecord;
|
|
||||||
}
|
|
||||||
|
|
||||||
get defaultTax() {
|
|
||||||
return this.props.defaultTax;
|
|
||||||
}
|
|
||||||
|
|
||||||
get langCode() {
|
|
||||||
return this.props.langCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
get currencyCode() {
|
|
||||||
return this.props.currencyCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
get logo() {
|
|
||||||
return this.props.logo;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isAccount(): boolean {
|
|
||||||
return !this.props.isFreelancer;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isFreelancer(): boolean {
|
|
||||||
return this.props.isFreelancer;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isActive(): boolean {
|
|
||||||
return this.props.status.equals(AccountStatus.createActive());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./account";
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export * from "./aggregates";
|
|
||||||
export * from "./repositories";
|
|
||||||
export * from "./services";
|
|
||||||
export * from "./value-objects";
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import { EmailAddress, UniqueID } from "@/core/common/domain";
|
|
||||||
import { Collection, Result } from "@repo/rdx-utils";
|
|
||||||
import { Account } from "../aggregates";
|
|
||||||
|
|
||||||
export interface IAccountRepository {
|
|
||||||
accountExists(id: UniqueID, transaction?: any): Promise<Result<boolean, Error>>;
|
|
||||||
findAll(transaction?: any): Promise<Result<Collection<Account>, Error>>;
|
|
||||||
findById(id: UniqueID, transaction?: any): Promise<Result<Account, Error>>;
|
|
||||||
findByEmail(email: EmailAddress, transaction?: any): Promise<Result<Account, Error>>;
|
|
||||||
|
|
||||||
create(account: Account, transaction?: any): Promise<void>;
|
|
||||||
update(account: Account, transaction?: any): Promise<void>;
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./account-repository.interface";
|
|
||||||
@ -1,153 +0,0 @@
|
|||||||
import { Maybe, Result } from "@repo/rdx-utils";
|
|
||||||
|
|
||||||
import {
|
|
||||||
EmailAddress,
|
|
||||||
PhoneNumber,
|
|
||||||
PostalAddress,
|
|
||||||
TINNumber,
|
|
||||||
UniqueID,
|
|
||||||
} from "@/core/common/domain";
|
|
||||||
|
|
||||||
import { Account, type IAccountProps } from "../aggregates";
|
|
||||||
import type { IAccountRepository } from "../repositories";
|
|
||||||
import { AccountStatus } from "../value-objects";
|
|
||||||
|
|
||||||
import { AccountService } from "./account.service";
|
|
||||||
|
|
||||||
const mockAccountRepository: IAccountRepository = {
|
|
||||||
accountExists: jest.fn(),
|
|
||||||
findAll: jest.fn(),
|
|
||||||
findByEmail: jest.fn(),
|
|
||||||
findById: jest.fn(),
|
|
||||||
create: jest.fn(),
|
|
||||||
update: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const sampleAccountPrimitives = {
|
|
||||||
id: "c5743279-e1cf-4dd5-baae-6698c8c6183c",
|
|
||||||
is_companyr: false,
|
|
||||||
name: "Empresa XYZ",
|
|
||||||
trade_name: "XYZ Trading",
|
|
||||||
tin: "123456789",
|
|
||||||
street: "Calle Principal 123",
|
|
||||||
city: "Madrid",
|
|
||||||
state: "Madrid",
|
|
||||||
postal_code: "28001",
|
|
||||||
country: "España",
|
|
||||||
email: "contacto@xyz.com",
|
|
||||||
phone: "+34 600 123 456",
|
|
||||||
fax: "+34 600 654 321",
|
|
||||||
website: "https://xyz.com",
|
|
||||||
legal_record: "Registro Mercantil XYZ",
|
|
||||||
default_tax: 21,
|
|
||||||
status: "active",
|
|
||||||
language_code: "es",
|
|
||||||
currency_code: "EUR",
|
|
||||||
logo: "https://xyz.com/logo.png",
|
|
||||||
};
|
|
||||||
|
|
||||||
const accountBuilder = (accountData: any) => {
|
|
||||||
const idOrError = UniqueID.create(sampleAccountPrimitives.id);
|
|
||||||
const tinOrError = TINNumber.create(sampleAccountPrimitives.tin);
|
|
||||||
const emailOrError = EmailAddress.create(sampleAccountPrimitives.email);
|
|
||||||
const phoneOrError = PhoneNumber.create(sampleAccountPrimitives.phone);
|
|
||||||
const faxOrError = PhoneNumber.createNullable(sampleAccountPrimitives.fax);
|
|
||||||
const postalAddressOrError = PostalAddress.create({
|
|
||||||
street: sampleAccountPrimitives.street,
|
|
||||||
city: sampleAccountPrimitives.city,
|
|
||||||
state: sampleAccountPrimitives.state,
|
|
||||||
postalCode: sampleAccountPrimitives.postal_code,
|
|
||||||
country: sampleAccountPrimitives.country,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = Result.combine([
|
|
||||||
idOrError,
|
|
||||||
tinOrError,
|
|
||||||
emailOrError,
|
|
||||||
phoneOrError,
|
|
||||||
faxOrError,
|
|
||||||
postalAddressOrError,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (result.isFailure) {
|
|
||||||
return Result.fail(result.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const validatedData: IAccountProps = {
|
|
||||||
status: AccountStatus.createInactive(),
|
|
||||||
isFreelancer: sampleAccountPrimitives.is_companyr,
|
|
||||||
name: sampleAccountPrimitives.name,
|
|
||||||
tradeName: sampleAccountPrimitives.trade_name
|
|
||||||
? Maybe.some(sampleAccountPrimitives.trade_name)
|
|
||||||
: Maybe.none(),
|
|
||||||
tin: tinOrError.data,
|
|
||||||
address: postalAddressOrError.data,
|
|
||||||
email: emailOrError.data,
|
|
||||||
phone: phoneOrError.data,
|
|
||||||
fax: faxOrError.data,
|
|
||||||
website: sampleAccountPrimitives.website
|
|
||||||
? Maybe.some(sampleAccountPrimitives.website)
|
|
||||||
: Maybe.none(),
|
|
||||||
legalRecord: sampleAccountPrimitives.legal_record,
|
|
||||||
defaultTax: sampleAccountPrimitives.default_tax,
|
|
||||||
langCode: sampleAccountPrimitives.language_code,
|
|
||||||
currencyCode: sampleAccountPrimitives.currency_code,
|
|
||||||
logo: sampleAccountPrimitives.logo ? Maybe.some(sampleAccountPrimitives.logo) : Maybe.none(),
|
|
||||||
};
|
|
||||||
|
|
||||||
return Account.create(validatedData, idOrError.data);
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("AccountService - Integración", () => {
|
|
||||||
let accountService: AccountService;
|
|
||||||
let activeSampleAccount: Account;
|
|
||||||
let inactiveSampleAccount: Account;
|
|
||||||
let accountId: UniqueID;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
accountService = new AccountService(mockAccountRepository);
|
|
||||||
inactiveSampleAccount = accountBuilder(sampleAccountPrimitives).data;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("debería activar una cuenta existente", async () => {
|
|
||||||
const existingAccount = inactiveSampleAccount;
|
|
||||||
(mockAccountRepository.findById as jest.Mock).mockResolvedValue(Result.ok(existingAccount));
|
|
||||||
(mockAccountRepository.update as jest.Mock).mockResolvedValue(undefined);
|
|
||||||
|
|
||||||
const result = await accountService.activateAccount(existingAccount.id);
|
|
||||||
|
|
||||||
expect(result.isSuccess).toBe(true);
|
|
||||||
expect(result.data.isActive).toBeTruthy();
|
|
||||||
expect(mockAccountRepository.update).toHaveBeenCalledWith(result.data);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("debería desactivar una cuenta existente", async () => {
|
|
||||||
const existingAccount = Account.update(inactiveSampleAccount, {
|
|
||||||
status: AccountStatus.createActive(),
|
|
||||||
}).data;
|
|
||||||
|
|
||||||
(mockAccountRepository.findById as jest.Mock).mockResolvedValue(Result.ok(existingAccount));
|
|
||||||
(mockAccountRepository.update as jest.Mock).mockResolvedValue(undefined);
|
|
||||||
|
|
||||||
const result = await accountService.deactivateAccount(existingAccount.id);
|
|
||||||
expect(result.isSuccess).toBe(true);
|
|
||||||
expect(result.data.isActive).toBeFalsy();
|
|
||||||
expect(mockAccountRepository.update).toHaveBeenCalledWith(result.data);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("debería retornar error si la cuenta no existe al activar", async () => {
|
|
||||||
(mockAccountRepository.findById as jest.Mock).mockResolvedValue(null);
|
|
||||||
|
|
||||||
const result = await accountService.activateAccount(UniqueID.create("", true).data);
|
|
||||||
expect(result.isFailure).toBe(true);
|
|
||||||
expect(result.error.message).toBe("Account not found");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("debería retornar error si la cuenta no existe al desactivar", async () => {
|
|
||||||
(mockAccountRepository.findById as jest.Mock).mockResolvedValue(null);
|
|
||||||
|
|
||||||
const result = await accountService.deactivateAccount(UniqueID.create("", true).data);
|
|
||||||
expect(result.isFailure).toBe(true);
|
|
||||||
expect(result.error.message).toBe("Account not found");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
import { UniqueID } from "@/core/common/domain";
|
|
||||||
import { Collection, Result } from "@repo/rdx-utils";
|
|
||||||
import { Account, IAccountProps } from "../aggregates";
|
|
||||||
|
|
||||||
export interface IAccountService {
|
|
||||||
findAccounts(transaction?: any): Promise<Result<Collection<Account>, Error>>;
|
|
||||||
findAccountById(accountId: UniqueID, transaction?: any): Promise<Result<Account>>;
|
|
||||||
|
|
||||||
updateAccountById(
|
|
||||||
accountId: UniqueID,
|
|
||||||
data: Partial<IAccountProps>,
|
|
||||||
transaction?: any
|
|
||||||
): Promise<Result<Account, Error>>;
|
|
||||||
|
|
||||||
createAccount(
|
|
||||||
accountId: UniqueID,
|
|
||||||
data: IAccountProps,
|
|
||||||
transaction?: any
|
|
||||||
): Promise<Result<Account, Error>>;
|
|
||||||
}
|
|
||||||
@ -1,70 +0,0 @@
|
|||||||
import { UniqueID } from "@/core/common/domain";
|
|
||||||
import { Account } from "../aggregates";
|
|
||||||
import { IAccountRepository } from "../repositories";
|
|
||||||
import { AccountService } from "./account.service";
|
|
||||||
|
|
||||||
const mockAccountRepository: IAccountRepository = {
|
|
||||||
accountExists: jest.fn(),
|
|
||||||
findAll: jest.fn(),
|
|
||||||
findByEmail: jest.fn(),
|
|
||||||
findById: jest.fn(),
|
|
||||||
create: jest.fn(),
|
|
||||||
update: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const sampleAccount = {
|
|
||||||
id: "c5743279-e1cf-4dd5-baae-6698c8c6183c",
|
|
||||||
is_companyr: false,
|
|
||||||
name: "Empresa XYZ",
|
|
||||||
trade_name: "XYZ Trading",
|
|
||||||
tin: "123456789",
|
|
||||||
street: "Calle Principal 123",
|
|
||||||
city: "Madrid",
|
|
||||||
state: "Madrid",
|
|
||||||
postal_code: "28001",
|
|
||||||
country: "España",
|
|
||||||
email: "contacto@xyz.com",
|
|
||||||
phone: "+34 600 123 456",
|
|
||||||
fax: "+34 600 654 321",
|
|
||||||
website: "https://xyz.com",
|
|
||||||
legal_record: "Registro Mercantil XYZ",
|
|
||||||
default_tax: 21,
|
|
||||||
status: "active",
|
|
||||||
language_code: "es",
|
|
||||||
currency_code: "EUR",
|
|
||||||
logo: "https://xyz.com/logo.png",
|
|
||||||
};
|
|
||||||
|
|
||||||
const accountId = UniqueID.create(sampleAccount.id).data;
|
|
||||||
|
|
||||||
describe("AccountService", () => {
|
|
||||||
let accountService: AccountService;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
accountService = new AccountService(mockAccountRepository);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("debería actualizar una cuenta existente", async () => {
|
|
||||||
const existingAccount = new Account(
|
|
||||||
{
|
|
||||||
/* datos simulados */
|
|
||||||
},
|
|
||||||
"123"
|
|
||||||
);
|
|
||||||
(mockAccountRepository.findById as jest.Mock).mockResolvedValue(existingAccount);
|
|
||||||
(mockAccountRepository.create as jest.Mock).mockResolvedValue(undefined);
|
|
||||||
|
|
||||||
const result = await accountService.updateAccountById(accountId, { name: "Nuevo Nombre" });
|
|
||||||
expect(result.isSuccess).toBe(true);
|
|
||||||
expect(result.data.name).toBe("Nuevo Nombre");
|
|
||||||
expect(mockAccountRepository.save).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("debería retornar error si la cuenta no existe", async () => {
|
|
||||||
(mockAccountRepository.findById as jest.Mock).mockResolvedValue(null);
|
|
||||||
|
|
||||||
const result = await accountService.updateAccountById(accountId, { name: "Nuevo Nombre" });
|
|
||||||
expect(result.isFailure).toBe(true);
|
|
||||||
expect(result.error.message).toBe("Account not found");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,105 +0,0 @@
|
|||||||
import { UniqueID } from "@/core/common/domain";
|
|
||||||
import { Collection, Result } from "@repo/rdx-utils";
|
|
||||||
import { Transaction } from "sequelize";
|
|
||||||
import { Account, IAccountProps } from "../aggregates";
|
|
||||||
import { IAccountRepository } from "../repositories";
|
|
||||||
import { IAccountService } from "./account-service.interface";
|
|
||||||
|
|
||||||
export class AccountService implements IAccountService {
|
|
||||||
constructor(private readonly repo: IAccountRepository) {}
|
|
||||||
|
|
||||||
async findAccounts(transaction?: Transaction): Promise<Result<Collection<Account>, Error>> {
|
|
||||||
const accountsOrError = await this.repo.findAll(transaction);
|
|
||||||
if (accountsOrError.isFailure) {
|
|
||||||
return Result.fail(accountsOrError.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Solo devolver usuarios activos
|
|
||||||
//const allAccounts = accountsOrError.data.filter((account) => account.isActive);
|
|
||||||
//return Result.ok(new Collection(allAccounts));
|
|
||||||
|
|
||||||
return accountsOrError;
|
|
||||||
}
|
|
||||||
|
|
||||||
async findAccountById(accountId: UniqueID, transaction?: Transaction): Promise<Result<Account>> {
|
|
||||||
return await this.repo.findById(accountId, transaction);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateAccountById(
|
|
||||||
accountId: UniqueID,
|
|
||||||
data: Partial<IAccountProps>,
|
|
||||||
transaction?: Transaction
|
|
||||||
): Promise<Result<Account, Error>> {
|
|
||||||
// Verificar si la cuenta existe
|
|
||||||
const accountOrError = await this.repo.findById(accountId, transaction);
|
|
||||||
if (accountOrError.isFailure) {
|
|
||||||
return Result.fail(new Error("Account not found"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedAccountOrError = Account.update(accountOrError.data, data);
|
|
||||||
if (updatedAccountOrError.isFailure) {
|
|
||||||
return Result.fail(
|
|
||||||
new Error(`Error updating account: ${updatedAccountOrError.error.message}`)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateAccount = updatedAccountOrError.data;
|
|
||||||
|
|
||||||
await this.repo.update(updateAccount, transaction);
|
|
||||||
return Result.ok(updateAccount);
|
|
||||||
}
|
|
||||||
|
|
||||||
async createAccount(
|
|
||||||
accountId: UniqueID,
|
|
||||||
data: IAccountProps,
|
|
||||||
transaction?: Transaction
|
|
||||||
): Promise<Result<Account, Error>> {
|
|
||||||
// Verificar si la cuenta existe
|
|
||||||
const accountOrError = await this.repo.findById(accountId, transaction);
|
|
||||||
if (accountOrError.isSuccess) {
|
|
||||||
return Result.fail(new Error("Account exists"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const newAccountOrError = Account.create(data, accountId);
|
|
||||||
if (newAccountOrError.isFailure) {
|
|
||||||
return Result.fail(new Error(`Error creating account: ${newAccountOrError.error.message}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
const newAccount = newAccountOrError.data;
|
|
||||||
|
|
||||||
await this.repo.create(newAccount, transaction);
|
|
||||||
return Result.ok(newAccount);
|
|
||||||
}
|
|
||||||
|
|
||||||
async activateAccount(id: UniqueID, transaction?: Transaction): Promise<Result<Account, Error>> {
|
|
||||||
const accountOrError = await this.repo.findById(id, transaction);
|
|
||||||
|
|
||||||
if (accountOrError.isFailure) {
|
|
||||||
return Result.fail(new Error("Account not found"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const account = accountOrError.data;
|
|
||||||
if (account.activate()) {
|
|
||||||
await this.repo.update(account, transaction);
|
|
||||||
return Result.ok();
|
|
||||||
}
|
|
||||||
return Result.fail(new Error("Error activating account"));
|
|
||||||
}
|
|
||||||
|
|
||||||
async deactivateAccount(
|
|
||||||
id: UniqueID,
|
|
||||||
transaction?: Transaction
|
|
||||||
): Promise<Result<Account, Error>> {
|
|
||||||
const accountOrError = await this.repo.findById(id, transaction);
|
|
||||||
if (accountOrError.isFailure) {
|
|
||||||
return Result.fail(new Error("Account not found"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const account = accountOrError.data;
|
|
||||||
if (account.deactivate()) {
|
|
||||||
await this.repo.update(account, transaction);
|
|
||||||
return Result.ok();
|
|
||||||
}
|
|
||||||
return Result.fail(new Error("Error deactivating account"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from "./account-service.interface";
|
|
||||||
export * from "./account.service";
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
import { ACCOUNT_STATUS, AccountStatus } from "./account-status";
|
|
||||||
|
|
||||||
describe("AccountStatus Value Object", () => {
|
|
||||||
describe("Creación de estados válidos", () => {
|
|
||||||
it("debería crear un estado inactivo correctamente", () => {
|
|
||||||
const status = AccountStatus.createInactive();
|
|
||||||
expect(status.getValue()).toBe(ACCOUNT_STATUS.INACTIVE);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("debería crear un estado activo correctamente", () => {
|
|
||||||
const status = AccountStatus.createActive();
|
|
||||||
expect(status.getValue()).toBe(ACCOUNT_STATUS.ACTIVE);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("debería crear un estado válido usando el método create", () => {
|
|
||||||
const result = AccountStatus.create(ACCOUNT_STATUS.ACTIVE);
|
|
||||||
expect(result.isSuccess).toBe(true);
|
|
||||||
expect(result.data.getValue()).toBe(ACCOUNT_STATUS.ACTIVE);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("debería fallar al crear un estado inválido", () => {
|
|
||||||
const result = AccountStatus.create("invalid-status");
|
|
||||||
expect(result.isFailure).toBe(true);
|
|
||||||
expect(result.error.message).toBe("Estado de la cuenta no válido: invalid-status");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Verificación de transiciones", () => {
|
|
||||||
it("debería permitir la transición de 'inactive' a 'active'", () => {
|
|
||||||
const status = AccountStatus.createInactive();
|
|
||||||
expect(status.canTransitionTo(ACCOUNT_STATUS.ACTIVE)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("debería permitir la transición de 'active' a 'inactive'", () => {
|
|
||||||
const status = AccountStatus.createActive();
|
|
||||||
expect(status.canTransitionTo(ACCOUNT_STATUS.INACTIVE)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("debería impedir transiciones no permitidas", () => {
|
|
||||||
const status = AccountStatus.createInactive();
|
|
||||||
expect(status.canTransitionTo("invalid")).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("debería realizar una transición válida correctamente", () => {
|
|
||||||
const inactiveStatus = AccountStatus.createInactive();
|
|
||||||
const transitionResult = inactiveStatus.transitionTo(ACCOUNT_STATUS.ACTIVE);
|
|
||||||
|
|
||||||
expect(transitionResult.isSuccess).toBe(true);
|
|
||||||
expect(transitionResult.data.getValue()).toBe(ACCOUNT_STATUS.ACTIVE);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("debería fallar en una transición inválida", () => {
|
|
||||||
const status = AccountStatus.createInactive();
|
|
||||||
const transitionResult = status.transitionTo("invalid");
|
|
||||||
|
|
||||||
expect(transitionResult.isFailure).toBe(true);
|
|
||||||
expect(transitionResult.error.message).toBe("Transición no permitida de inactive a invalid");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Métodos auxiliares", () => {
|
|
||||||
it("debería devolver correctamente el valor con getValue()", () => {
|
|
||||||
const status = AccountStatus.createInactive();
|
|
||||||
expect(status.getValue()).toBe(ACCOUNT_STATUS.INACTIVE);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("debería devolver correctamente el valor con toString()", () => {
|
|
||||||
const status = AccountStatus.createActive();
|
|
||||||
expect(status.toString()).toBe(ACCOUNT_STATUS.ACTIVE);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
import { ValueObject } from "@repo/rdx-ddd";
|
|
||||||
import { Result } from "@repo/rdx-utils";
|
|
||||||
|
|
||||||
interface IAccountStatusProps {
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum ACCOUNT_STATUS {
|
|
||||||
INACTIVE = "inactive",
|
|
||||||
ACTIVE = "active",
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AccountStatus extends ValueObject<IAccountStatusProps> {
|
|
||||||
private static readonly ALLOWED_STATUSES = ["inactive", "active"];
|
|
||||||
|
|
||||||
private static readonly TRANSITIONS: Record<string, string[]> = {
|
|
||||||
inactive: [ACCOUNT_STATUS.ACTIVE],
|
|
||||||
active: [ACCOUNT_STATUS.INACTIVE],
|
|
||||||
};
|
|
||||||
|
|
||||||
static create(value: string): Result<AccountStatus, Error> {
|
|
||||||
if (!this.ALLOWED_STATUSES.includes(value)) {
|
|
||||||
return Result.fail(new Error(`Estado de la cuenta no válido: ${value}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.ok(
|
|
||||||
value === "active" ? AccountStatus.createActive() : AccountStatus.createInactive()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createInactive(): AccountStatus {
|
|
||||||
return new AccountStatus({ value: ACCOUNT_STATUS.INACTIVE });
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createActive(): AccountStatus {
|
|
||||||
return new AccountStatus({ value: ACCOUNT_STATUS.ACTIVE });
|
|
||||||
}
|
|
||||||
|
|
||||||
getValue(): string {
|
|
||||||
return this.props.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
canTransitionTo(nextStatus: string): boolean {
|
|
||||||
return AccountStatus.TRANSITIONS[this.props.value].includes(nextStatus);
|
|
||||||
}
|
|
||||||
|
|
||||||
transitionTo(nextStatus: string): Result<AccountStatus, Error> {
|
|
||||||
if (!this.canTransitionTo(nextStatus)) {
|
|
||||||
return Result.fail(
|
|
||||||
new Error(`Transición no permitida de ${this.props.value} a ${nextStatus}`)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return AccountStatus.create(nextStatus);
|
|
||||||
}
|
|
||||||
|
|
||||||
toPrimitive(): string {
|
|
||||||
return this.getValue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./account-status";
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from "./mappers";
|
|
||||||
export * from "./sequelize";
|
|
||||||
@ -1,107 +0,0 @@
|
|||||||
import { Maybe, Result } from "@repo/rdx-utils";
|
|
||||||
|
|
||||||
import { Account, AccountStatus } from "@/contexts/accounts/domain/";
|
|
||||||
import {
|
|
||||||
EmailAddress,
|
|
||||||
PhoneNumber,
|
|
||||||
PostalAddress,
|
|
||||||
TINNumber,
|
|
||||||
UniqueID,
|
|
||||||
} from "@/core/common/domain";
|
|
||||||
import {
|
|
||||||
type ISequelizeMapper,
|
|
||||||
type MapperParamsType,
|
|
||||||
SequelizeMapper,
|
|
||||||
} from "@/core/common/infrastructure/sequelize/sequelize-mapper";
|
|
||||||
|
|
||||||
import type { AccountCreationAttributes, AccountModel } from "../sequelize/account.model";
|
|
||||||
|
|
||||||
export interface IAccountMapper
|
|
||||||
extends ISequelizeMapper<AccountModel, AccountCreationAttributes, Account> {}
|
|
||||||
|
|
||||||
export class AccountMapper
|
|
||||||
extends SequelizeMapper<AccountModel, AccountCreationAttributes, Account>
|
|
||||||
implements IAccountMapper
|
|
||||||
{
|
|
||||||
public mapToDomain(source: AccountModel, params?: MapperParamsType): Result<Account, Error> {
|
|
||||||
const idOrError = UniqueID.create(source.id);
|
|
||||||
const statusOrError = AccountStatus.create(source.status);
|
|
||||||
const tinOrError = TINNumber.create(source.tin);
|
|
||||||
const emailOrError = EmailAddress.create(source.email);
|
|
||||||
const phoneOrError = PhoneNumber.create(source.phone);
|
|
||||||
const faxOrError = PhoneNumber.createNullable(source.fax);
|
|
||||||
const postalAddressOrError = PostalAddress.create({
|
|
||||||
street: source.street,
|
|
||||||
city: source.city,
|
|
||||||
state: source.state,
|
|
||||||
postalCode: source.postal_code,
|
|
||||||
country: source.country,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = Result.combine([
|
|
||||||
idOrError,
|
|
||||||
statusOrError,
|
|
||||||
tinOrError,
|
|
||||||
emailOrError,
|
|
||||||
phoneOrError,
|
|
||||||
faxOrError,
|
|
||||||
postalAddressOrError,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (result.isFailure) {
|
|
||||||
return Result.fail(result.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Account.create(
|
|
||||||
{
|
|
||||||
status: statusOrError.data,
|
|
||||||
isFreelancer: source.is_companyr,
|
|
||||||
name: source.name,
|
|
||||||
tradeName: source.trade_name ? Maybe.some(source.trade_name) : Maybe.none(),
|
|
||||||
tin: tinOrError.data,
|
|
||||||
address: postalAddressOrError.data,
|
|
||||||
email: emailOrError.data,
|
|
||||||
phone: phoneOrError.data,
|
|
||||||
fax: faxOrError.data,
|
|
||||||
website: source.website ? Maybe.some(source.website) : Maybe.none(),
|
|
||||||
legalRecord: source.legal_record,
|
|
||||||
defaultTax: source.default_tax,
|
|
||||||
langCode: source.language_code,
|
|
||||||
currencyCode: source.currency_code,
|
|
||||||
logo: source.logo ? Maybe.some(source.logo) : Maybe.none(),
|
|
||||||
},
|
|
||||||
idOrError.data
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public mapToPersistence(source: Account, params?: MapperParamsType): AccountCreationAttributes {
|
|
||||||
return {
|
|
||||||
id: source.id.toPrimitive(),
|
|
||||||
is_companyr: source.isFreelancer,
|
|
||||||
name: source.name,
|
|
||||||
trade_name: source.tradeName.getOrUndefined(),
|
|
||||||
tin: source.tin.toPrimitive(),
|
|
||||||
|
|
||||||
street: source.address.street,
|
|
||||||
city: source.address.city,
|
|
||||||
state: source.address.state,
|
|
||||||
postal_code: source.address.postalCode,
|
|
||||||
country: source.address.country,
|
|
||||||
|
|
||||||
email: source.email.toPrimitive(),
|
|
||||||
phone: source.phone.toPrimitive(),
|
|
||||||
fax: source.fax.isSome() ? source.fax.getOrUndefined()?.toPrimitive() : undefined,
|
|
||||||
website: source.website.getOrUndefined(),
|
|
||||||
|
|
||||||
legal_record: source.legalRecord,
|
|
||||||
default_tax: source.defaultTax,
|
|
||||||
status: source.isActive ? "active" : "inactive",
|
|
||||||
language_code: source.langCode,
|
|
||||||
currency_code: source.currencyCode,
|
|
||||||
logo: source.logo.getOrUndefined(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const accountMapper: AccountMapper = new AccountMapper();
|
|
||||||
export { accountMapper };
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./account.mapper";
|
|
||||||
@ -1,171 +0,0 @@
|
|||||||
import {
|
|
||||||
CreationOptional,
|
|
||||||
DataTypes,
|
|
||||||
InferAttributes,
|
|
||||||
InferCreationAttributes,
|
|
||||||
Model,
|
|
||||||
Sequelize,
|
|
||||||
} from "sequelize";
|
|
||||||
|
|
||||||
export type AccountCreationAttributes = InferCreationAttributes<AccountModel, {}> & {};
|
|
||||||
|
|
||||||
export class AccountModel extends Model<InferAttributes<AccountModel>, AccountCreationAttributes> {
|
|
||||||
// To avoid table creation
|
|
||||||
/*static async sync(): Promise<any> {
|
|
||||||
return Promise.resolve();
|
|
||||||
}*/
|
|
||||||
|
|
||||||
declare id: string;
|
|
||||||
|
|
||||||
declare is_companyr: boolean;
|
|
||||||
declare name: string;
|
|
||||||
declare trade_name: CreationOptional<string>;
|
|
||||||
declare tin: string;
|
|
||||||
|
|
||||||
declare street: string;
|
|
||||||
declare city: string;
|
|
||||||
declare state: string;
|
|
||||||
declare postal_code: string;
|
|
||||||
declare country: string;
|
|
||||||
|
|
||||||
declare email: string;
|
|
||||||
declare phone: string;
|
|
||||||
declare fax: CreationOptional<string>;
|
|
||||||
declare website: CreationOptional<string>;
|
|
||||||
|
|
||||||
declare legal_record: string;
|
|
||||||
|
|
||||||
declare default_tax: number;
|
|
||||||
declare status: string;
|
|
||||||
declare language_code: string;
|
|
||||||
declare currency_code: string;
|
|
||||||
declare logo: CreationOptional<string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default (sequelize: Sequelize) => {
|
|
||||||
AccountModel.init(
|
|
||||||
{
|
|
||||||
id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
primaryKey: true,
|
|
||||||
},
|
|
||||||
is_companyr: {
|
|
||||||
type: DataTypes.BOOLEAN,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
trade_name: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: null,
|
|
||||||
},
|
|
||||||
tin: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
street: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
city: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
state: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
postal_code: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
country: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
email: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
validate: {
|
|
||||||
isEmail: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
phone: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
fax: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: null,
|
|
||||||
},
|
|
||||||
website: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: null,
|
|
||||||
validate: {
|
|
||||||
isUrl: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
legal_record: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
default_tax: {
|
|
||||||
type: new DataTypes.SMALLINT(),
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: 2100,
|
|
||||||
},
|
|
||||||
|
|
||||||
logo: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: true,
|
|
||||||
defaultValue: null,
|
|
||||||
},
|
|
||||||
|
|
||||||
language_code: {
|
|
||||||
type: DataTypes.STRING(2),
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: "es",
|
|
||||||
},
|
|
||||||
|
|
||||||
currency_code: {
|
|
||||||
type: new DataTypes.STRING(3),
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: "EUR",
|
|
||||||
},
|
|
||||||
|
|
||||||
status: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: "active",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
tableName: "accounts",
|
|
||||||
|
|
||||||
paranoid: true, // softs deletes
|
|
||||||
timestamps: true,
|
|
||||||
|
|
||||||
createdAt: "created_at",
|
|
||||||
updatedAt: "updated_at",
|
|
||||||
deletedAt: "deleted_at",
|
|
||||||
|
|
||||||
indexes: [{ name: "email_idx", fields: ["email"], unique: true }],
|
|
||||||
|
|
||||||
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
|
|
||||||
|
|
||||||
defaultScope: {},
|
|
||||||
|
|
||||||
scopes: {},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return AccountModel;
|
|
||||||
};
|
|
||||||
@ -1,102 +0,0 @@
|
|||||||
import { Account } from "@/contexts/accounts/domain";
|
|
||||||
import { IAccountRepository } from "@/contexts/accounts/domain/repositories/account-repository.interface";
|
|
||||||
import { EmailAddress, UniqueID } from "@/core/common/domain";
|
|
||||||
import { SequelizeRepository } from "@/core/common/infrastructure";
|
|
||||||
import { Collection, Result } from "@repo/rdx-utils";
|
|
||||||
import { Transaction } from "sequelize";
|
|
||||||
import { IAccountMapper, accountMapper } from "../mappers/account.mapper";
|
|
||||||
import { AccountModel } from "./account.model";
|
|
||||||
|
|
||||||
class AccountRepository extends SequelizeRepository<Account> implements IAccountRepository {
|
|
||||||
private readonly _mapper!: IAccountMapper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 🔹 Función personalizada para mapear errores de unicidad en autenticación
|
|
||||||
*/
|
|
||||||
private _customErrorMapper(error: Error): string | null {
|
|
||||||
if (error.name === "SequelizeUniqueConstraintError") {
|
|
||||||
return "Account with this email already exists";
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(mapper: IAccountMapper) {
|
|
||||||
super();
|
|
||||||
this._mapper = mapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
async accountExists(id: UniqueID, transaction?: Transaction): Promise<Result<boolean, Error>> {
|
|
||||||
try {
|
|
||||||
const _account = await this._getById(AccountModel, id, {}, transaction);
|
|
||||||
|
|
||||||
return Result.ok(Boolean(id.equals(_account.id)));
|
|
||||||
} catch (error: any) {
|
|
||||||
return this._handleDatabaseError(error, this._customErrorMapper);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async findAll(transaction?: Transaction): Promise<Result<Collection<Account>, Error>> {
|
|
||||||
try {
|
|
||||||
const rawAccounts: any = await this._findAll(AccountModel, {}, transaction);
|
|
||||||
|
|
||||||
if (!rawAccounts === true) {
|
|
||||||
return Result.fail(new Error("Account with email not exists"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._mapper.mapArrayToDomain(rawAccounts);
|
|
||||||
} catch (error: any) {
|
|
||||||
return this._handleDatabaseError(error, this._customErrorMapper);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async findById(id: UniqueID, transaction?: Transaction): Promise<Result<Account, Error>> {
|
|
||||||
try {
|
|
||||||
const rawAccount: any = await this._getById(AccountModel, id, {}, transaction);
|
|
||||||
|
|
||||||
if (!rawAccount === true) {
|
|
||||||
return Result.fail(new Error(`Account with id ${id.toString()} not exists`));
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._mapper.mapToDomain(rawAccount);
|
|
||||||
} catch (error: any) {
|
|
||||||
return this._handleDatabaseError(error, this._customErrorMapper);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async findByEmail(
|
|
||||||
email: EmailAddress,
|
|
||||||
transaction?: Transaction
|
|
||||||
): Promise<Result<Account, Error>> {
|
|
||||||
try {
|
|
||||||
const rawAccount: any = await this._getBy(
|
|
||||||
AccountModel,
|
|
||||||
"email",
|
|
||||||
email.toString(),
|
|
||||||
{},
|
|
||||||
transaction
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!rawAccount === true) {
|
|
||||||
return Result.fail(new Error(`Account with email ${email.toString()} not exists`));
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._mapper.mapToDomain(rawAccount);
|
|
||||||
} catch (error: any) {
|
|
||||||
return this._handleDatabaseError(error, this._customErrorMapper);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async create(account: Account, transaction?: Transaction): Promise<void> {
|
|
||||||
const accountData = this._mapper.mapToPersistence(account);
|
|
||||||
await this._save(AccountModel, account.id, accountData, {}, transaction);
|
|
||||||
}
|
|
||||||
|
|
||||||
async update(account: Account, transaction?: Transaction): Promise<void> {
|
|
||||||
const accountData = this._mapper.mapToPersistence(account);
|
|
||||||
await this._save(AccountModel, account.id, accountData, {}, transaction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const accountRepository = new AccountRepository(accountMapper);
|
|
||||||
export { accountRepository };
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import { IAccountRepository } from "@/contexts/accounts/domain/repositories/account-repository.interface";
|
|
||||||
import { accountRepository } from "./account.repository";
|
|
||||||
|
|
||||||
export * from "./account.model";
|
|
||||||
|
|
||||||
export * from "./account.repository";
|
|
||||||
|
|
||||||
export const createAccountRepository = (): IAccountRepository => {
|
|
||||||
return accountRepository;
|
|
||||||
};
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
import { UniqueID } from "@/core/common/domain";
|
|
||||||
import { ExpressController } from "@/core/common/presentation";
|
|
||||||
import { CreateAccountUseCase } from "../../../application";
|
|
||||||
import { ICreateAccountRequestDTO } from "../../dto";
|
|
||||||
import { ICreateAccountPresenter } from "./create-account.presenter";
|
|
||||||
|
|
||||||
export class CreateAccountController extends ExpressController {
|
|
||||||
public constructor(
|
|
||||||
private readonly createAccount: CreateAccountUseCase,
|
|
||||||
private readonly presenter: ICreateAccountPresenter
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async executeImpl() {
|
|
||||||
const createDTO: ICreateAccountRequestDTO = this.req.body;
|
|
||||||
|
|
||||||
// Validar ID
|
|
||||||
const accountIdOrError = UniqueID.create(createDTO.id);
|
|
||||||
if (accountIdOrError.isFailure) return this.invalidInputError("Account ID not valid");
|
|
||||||
|
|
||||||
const accountOrError = await this.createAccount.execute(accountIdOrError.data, createDTO);
|
|
||||||
|
|
||||||
if (accountOrError.isFailure) {
|
|
||||||
return this.handleError(accountOrError.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.ok(this.presenter.toDTO(accountOrError.data));
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleError(error: Error) {
|
|
||||||
const message = error.message;
|
|
||||||
|
|
||||||
if (
|
|
||||||
message.includes("Database connection lost") ||
|
|
||||||
message.includes("Database request timed out")
|
|
||||||
) {
|
|
||||||
return this.unavailableError(
|
|
||||||
"Database service is currently unavailable. Please try again later."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.conflictError(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
import { Account } from "@/contexts/accounts/domain";
|
|
||||||
import { ensureBoolean, ensureNumber, ensureString } from "@repo/rdx-utils";
|
|
||||||
import { ICreateAccountResponseDTO } from "../../dto";
|
|
||||||
|
|
||||||
export interface ICreateAccountPresenter {
|
|
||||||
toDTO: (account: Account) => ICreateAccountResponseDTO;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createAccountPresenter: ICreateAccountPresenter = {
|
|
||||||
toDTO: (account: Account): ICreateAccountResponseDTO => ({
|
|
||||||
id: ensureString(account.id.toString()),
|
|
||||||
|
|
||||||
is_companyr: ensureBoolean(account.isFreelancer),
|
|
||||||
name: ensureString(account.name),
|
|
||||||
trade_name: ensureString(account.tradeName.getOrUndefined()),
|
|
||||||
tin: ensureString(account.tin.toString()),
|
|
||||||
|
|
||||||
street: ensureString(account.address.street),
|
|
||||||
city: ensureString(account.address.city),
|
|
||||||
state: ensureString(account.address.state),
|
|
||||||
postal_code: ensureString(account.address.postalCode),
|
|
||||||
country: ensureString(account.address.country),
|
|
||||||
|
|
||||||
email: ensureString(account.email.toString()),
|
|
||||||
phone: ensureString(account.phone.toString()),
|
|
||||||
fax: ensureString(account.fax.getOrUndefined()?.toString()),
|
|
||||||
website: ensureString(account.website.getOrUndefined()),
|
|
||||||
|
|
||||||
legal_record: ensureString(account.legalRecord),
|
|
||||||
|
|
||||||
default_tax: ensureNumber(account.defaultTax),
|
|
||||||
status: ensureString(account.isActive ? "active" : "inactive"),
|
|
||||||
language_code: ensureString(account.langCode),
|
|
||||||
currency_code: ensureString(account.currencyCode),
|
|
||||||
logo: ensureString(account.logo.getOrUndefined()),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import { CreateAccountUseCase } from "@/contexts/accounts/application/create-account.use-case";
|
|
||||||
import { AccountService } from "@/contexts/accounts/domain";
|
|
||||||
import { accountRepository } from "@/contexts/accounts/infraestructure";
|
|
||||||
import { SequelizeTransactionManager } from "@/core/common/infrastructure";
|
|
||||||
import { CreateAccountController } from "./create-account.controller";
|
|
||||||
import { createAccountPresenter } from "./create-account.presenter";
|
|
||||||
|
|
||||||
export const buildCreateAccountController = () => {
|
|
||||||
const transactionManager = new SequelizeTransactionManager();
|
|
||||||
const accountService = new AccountService(accountRepository);
|
|
||||||
|
|
||||||
const useCase = new CreateAccountUseCase(accountService, transactionManager);
|
|
||||||
const presenter = createAccountPresenter;
|
|
||||||
|
|
||||||
return new CreateAccountController(useCase, presenter);
|
|
||||||
};
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
import { GetAccountUseCase } from "@/contexts/accounts/application";
|
|
||||||
import { UniqueID } from "@/core/common/domain";
|
|
||||||
import { ExpressController } from "@/core/common/presentation";
|
|
||||||
import { IGetAccountPresenter } from "./get-account.presenter";
|
|
||||||
|
|
||||||
export class GetAccountController extends ExpressController {
|
|
||||||
public constructor(
|
|
||||||
private readonly getAccount: GetAccountUseCase,
|
|
||||||
private readonly presenter: IGetAccountPresenter
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async executeImpl() {
|
|
||||||
const { accountId } = this.req.params;
|
|
||||||
|
|
||||||
// Validar ID
|
|
||||||
const accountIdOrError = UniqueID.create(accountId);
|
|
||||||
if (accountIdOrError.isFailure) return this.invalidInputError("Account ID not valid");
|
|
||||||
|
|
||||||
const accountOrError = await this.getAccount.execute(accountIdOrError.data);
|
|
||||||
|
|
||||||
if (accountOrError.isFailure) {
|
|
||||||
return this.handleError(accountOrError.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.ok(this.presenter.toDTO(accountOrError.data));
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleError(error: Error) {
|
|
||||||
const message = error.message;
|
|
||||||
|
|
||||||
if (
|
|
||||||
message.includes("Database connection lost") ||
|
|
||||||
message.includes("Database request timed out")
|
|
||||||
) {
|
|
||||||
return this.unavailableError(
|
|
||||||
"Database service is currently unavailable. Please try again later."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.conflictError(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
import { Account } from "@/contexts/accounts/domain";
|
|
||||||
import { ensureBoolean, ensureNumber, ensureString } from "@repo/rdx-utils";
|
|
||||||
import { IGetAccountResponseDTO } from "../../dto";
|
|
||||||
|
|
||||||
export interface IGetAccountPresenter {
|
|
||||||
toDTO: (account: Account) => IGetAccountResponseDTO;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getAccountPresenter: IGetAccountPresenter = {
|
|
||||||
toDTO: (account: Account): IGetAccountResponseDTO => ({
|
|
||||||
id: ensureString(account.id.toPrimitive()),
|
|
||||||
|
|
||||||
is_companyr: ensureBoolean(account.isFreelancer),
|
|
||||||
name: ensureString(account.name),
|
|
||||||
trade_name: ensureString(account.tradeName.getOrUndefined()),
|
|
||||||
tin: ensureString(account.tin.toPrimitive()),
|
|
||||||
|
|
||||||
street: ensureString(account.address.street),
|
|
||||||
city: ensureString(account.address.city),
|
|
||||||
state: ensureString(account.address.state),
|
|
||||||
postal_code: ensureString(account.address.postalCode),
|
|
||||||
country: ensureString(account.address.country),
|
|
||||||
|
|
||||||
email: ensureString(account.email.toPrimitive()),
|
|
||||||
phone: ensureString(account.phone.toPrimitive()),
|
|
||||||
fax: ensureString(account.fax.getOrUndefined()?.toPrimitive()),
|
|
||||||
website: ensureString(account.website.getOrUndefined()),
|
|
||||||
|
|
||||||
legal_record: ensureString(account.legalRecord),
|
|
||||||
|
|
||||||
default_tax: ensureNumber(account.defaultTax),
|
|
||||||
status: ensureString(account.isActive ? "active" : "inactive"),
|
|
||||||
language_code: ensureString(account.langCode),
|
|
||||||
currency_code: ensureString(account.currencyCode),
|
|
||||||
logo: ensureString(account.logo.getOrUndefined()),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import { GetAccountUseCase } from "@/contexts/accounts/application";
|
|
||||||
import { AccountService } from "@/contexts/accounts/domain";
|
|
||||||
import { accountRepository } from "@/contexts/accounts/infraestructure";
|
|
||||||
import { SequelizeTransactionManager } from "@/core/common/infrastructure";
|
|
||||||
import { GetAccountController } from "./get-account.controller";
|
|
||||||
import { getAccountPresenter } from "./get-account.presenter";
|
|
||||||
|
|
||||||
export const buildGetAccountController = () => {
|
|
||||||
const transactionManager = new SequelizeTransactionManager();
|
|
||||||
const accountService = new AccountService(accountRepository);
|
|
||||||
|
|
||||||
const useCase = new GetAccountUseCase(accountService, transactionManager);
|
|
||||||
const presenter = getAccountPresenter;
|
|
||||||
|
|
||||||
return new GetAccountController(useCase, presenter);
|
|
||||||
};
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export * from "./create-account";
|
|
||||||
export * from "./get-account";
|
|
||||||
export * from "./list-accounts";
|
|
||||||
export * from "./update-account";
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import { ListAccountsUseCase } from "@/contexts/accounts/application";
|
|
||||||
import { AccountService } from "@/contexts/accounts/domain";
|
|
||||||
import { accountRepository } from "@/contexts/accounts/infraestructure";
|
|
||||||
import { SequelizeTransactionManager } from "@/core/common/infrastructure";
|
|
||||||
import { ListAccountsController } from "./list-accounts.controller";
|
|
||||||
import { listAccountsPresenter } from "./list-accounts.presenter";
|
|
||||||
|
|
||||||
export const buildListAccountsController = () => {
|
|
||||||
const transactionManager = new SequelizeTransactionManager();
|
|
||||||
const accountService = new AccountService(accountRepository);
|
|
||||||
|
|
||||||
const useCase = new ListAccountsUseCase(accountService, transactionManager);
|
|
||||||
const presenter = listAccountsPresenter;
|
|
||||||
|
|
||||||
return new ListAccountsController(useCase, presenter);
|
|
||||||
};
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
import { ListAccountsUseCase } from "@/contexts/accounts/application";
|
|
||||||
import { ExpressController } from "@/core/common/presentation";
|
|
||||||
import { IListAccountsPresenter } from "./list-accounts.presenter";
|
|
||||||
|
|
||||||
export class ListAccountsController extends ExpressController {
|
|
||||||
public constructor(
|
|
||||||
private readonly listAccounts: ListAccountsUseCase,
|
|
||||||
private readonly presenter: IListAccountsPresenter
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async executeImpl() {
|
|
||||||
const accountsOrError = await this.listAccounts.execute();
|
|
||||||
|
|
||||||
if (accountsOrError.isFailure) {
|
|
||||||
return this.handleError(accountsOrError.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.ok(this.presenter.toDTO(accountsOrError.data));
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleError(error: Error) {
|
|
||||||
const message = error.message;
|
|
||||||
|
|
||||||
if (
|
|
||||||
message.includes("Database connection lost") ||
|
|
||||||
message.includes("Database request timed out")
|
|
||||||
) {
|
|
||||||
return this.unavailableError(
|
|
||||||
"Database service is currently unavailable. Please try again later."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.conflictError(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
import { Account } from "@/contexts/accounts/domain";
|
|
||||||
import { Collection, ensureBoolean, ensureNumber, ensureString } from "@repo/rdx-utils";
|
|
||||||
import { IListAccountsResponseDTO } from "../../dto";
|
|
||||||
|
|
||||||
export interface IListAccountsPresenter {
|
|
||||||
toDTO: (accounts: Collection<Account>) => IListAccountsResponseDTO[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const listAccountsPresenter: IListAccountsPresenter = {
|
|
||||||
toDTO: (accounts: Collection<Account>): IListAccountsResponseDTO[] =>
|
|
||||||
accounts.map((account) => ({
|
|
||||||
id: ensureString(account.id.toString()),
|
|
||||||
|
|
||||||
is_companyr: ensureBoolean(account.isFreelancer),
|
|
||||||
name: ensureString(account.name),
|
|
||||||
trade_name: ensureString(account.tradeName.getOrUndefined()),
|
|
||||||
tin: ensureString(account.tin.toString()),
|
|
||||||
|
|
||||||
street: ensureString(account.address.street),
|
|
||||||
city: ensureString(account.address.city),
|
|
||||||
state: ensureString(account.address.state),
|
|
||||||
postal_code: ensureString(account.address.postalCode),
|
|
||||||
country: ensureString(account.address.country),
|
|
||||||
|
|
||||||
email: ensureString(account.email.toString()),
|
|
||||||
phone: ensureString(account.phone.toString()),
|
|
||||||
fax: ensureString(account.fax.getOrUndefined()?.toString()),
|
|
||||||
website: ensureString(account.website.getOrUndefined()),
|
|
||||||
|
|
||||||
legal_record: ensureString(account.legalRecord),
|
|
||||||
|
|
||||||
default_tax: ensureNumber(account.defaultTax),
|
|
||||||
status: ensureString(account.isActive ? "active" : "inactive"),
|
|
||||||
language_code: ensureString(account.langCode),
|
|
||||||
currency_code: ensureString(account.currencyCode),
|
|
||||||
logo: ensureString(account.logo.getOrUndefined()),
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import { UpdateAccountUseCase } from "@/contexts/accounts/application";
|
|
||||||
import { AccountService } from "@/contexts/accounts/domain";
|
|
||||||
import { accountRepository } from "@/contexts/accounts/infraestructure";
|
|
||||||
import { SequelizeTransactionManager } from "@/core/common/infrastructure";
|
|
||||||
import { UpdateAccountController } from "./update-account.controller";
|
|
||||||
import { updateAccountPresenter } from "./update-account.presenter";
|
|
||||||
|
|
||||||
export const buildUpdateAccountController = () => {
|
|
||||||
const transactionManager = new SequelizeTransactionManager();
|
|
||||||
const accountService = new AccountService(accountRepository);
|
|
||||||
|
|
||||||
const useCase = new UpdateAccountUseCase(accountService, transactionManager);
|
|
||||||
const presenter = updateAccountPresenter;
|
|
||||||
|
|
||||||
return new UpdateAccountController(useCase, presenter);
|
|
||||||
};
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
import { UpdateAccountUseCase } from "@/contexts/accounts/application/update-account.use-case";
|
|
||||||
import { UniqueID } from "@/core/common/domain";
|
|
||||||
import { ExpressController } from "@/core/common/presentation";
|
|
||||||
import { IUpdateAccountRequestDTO } from "../../dto";
|
|
||||||
import { IUpdateAccountPresenter } from "./update-account.presenter";
|
|
||||||
|
|
||||||
export class UpdateAccountController extends ExpressController {
|
|
||||||
public constructor(
|
|
||||||
private readonly updateAccount: UpdateAccountUseCase,
|
|
||||||
private readonly presenter: IUpdateAccountPresenter
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async executeImpl() {
|
|
||||||
const { accountId } = this.req.params;
|
|
||||||
const updateDTO: IUpdateAccountRequestDTO = this.req.body;
|
|
||||||
|
|
||||||
// Validar ID
|
|
||||||
const accountIdOrError = UniqueID.create(accountId);
|
|
||||||
if (accountIdOrError.isFailure) return this.invalidInputError("Account ID not valid");
|
|
||||||
|
|
||||||
const accountOrError = await this.updateAccount.execute(accountIdOrError.data, updateDTO);
|
|
||||||
|
|
||||||
if (accountOrError.isFailure) {
|
|
||||||
return this.handleError(accountOrError.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.ok(this.presenter.toDTO(accountOrError.data));
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleError(error: Error) {
|
|
||||||
const message = error.message;
|
|
||||||
|
|
||||||
if (
|
|
||||||
message.includes("Database connection lost") ||
|
|
||||||
message.includes("Database request timed out")
|
|
||||||
) {
|
|
||||||
return this.unavailableError(
|
|
||||||
"Database service is currently unavailable. Please try again later."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.conflictError(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
import { Account } from "@/contexts/accounts/domain";
|
|
||||||
import { ensureBoolean, ensureNumber, ensureString } from "@repo/rdx-utils";
|
|
||||||
import { IUpdateAccountResponseDTO } from "../../dto";
|
|
||||||
|
|
||||||
export interface IUpdateAccountPresenter {
|
|
||||||
toDTO: (account: Account) => IUpdateAccountResponseDTO;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const updateAccountPresenter: IUpdateAccountPresenter = {
|
|
||||||
toDTO: (account: Account): IUpdateAccountResponseDTO => ({
|
|
||||||
id: ensureString(account.id.toString()),
|
|
||||||
|
|
||||||
is_companyr: ensureBoolean(account.isFreelancer),
|
|
||||||
name: ensureString(account.name),
|
|
||||||
trade_name: ensureString(account.tradeName.getOrUndefined()),
|
|
||||||
tin: ensureString(account.tin.toString()),
|
|
||||||
|
|
||||||
street: ensureString(account.address.street),
|
|
||||||
city: ensureString(account.address.city),
|
|
||||||
state: ensureString(account.address.state),
|
|
||||||
postal_code: ensureString(account.address.postalCode),
|
|
||||||
country: ensureString(account.address.country),
|
|
||||||
|
|
||||||
email: ensureString(account.email.toString()),
|
|
||||||
phone: ensureString(account.phone.toString()),
|
|
||||||
fax: ensureString(account.fax.getOrUndefined()?.toString()),
|
|
||||||
website: ensureString(account.website.getOrUndefined()),
|
|
||||||
|
|
||||||
legal_record: ensureString(account.legalRecord),
|
|
||||||
|
|
||||||
default_tax: ensureNumber(account.defaultTax),
|
|
||||||
status: ensureString(account.isActive ? "active" : "inactive"),
|
|
||||||
language_code: ensureString(account.langCode),
|
|
||||||
currency_code: ensureString(account.currencyCode),
|
|
||||||
logo: ensureString(account.logo.getOrUndefined()),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
export type IListAccountsRequestDTO = {};
|
|
||||||
|
|
||||||
export interface ICreateAccountRequestDTO {
|
|
||||||
id: string;
|
|
||||||
is_companyr: boolean;
|
|
||||||
name: string;
|
|
||||||
trade_name: string;
|
|
||||||
tin: string;
|
|
||||||
|
|
||||||
street: string;
|
|
||||||
city: string;
|
|
||||||
state: string;
|
|
||||||
postal_code: string;
|
|
||||||
country: string;
|
|
||||||
|
|
||||||
email: string;
|
|
||||||
phone: string;
|
|
||||||
fax: string;
|
|
||||||
website: string;
|
|
||||||
|
|
||||||
legal_record: string;
|
|
||||||
|
|
||||||
default_tax: number;
|
|
||||||
language_code: string;
|
|
||||||
currency_code: string;
|
|
||||||
logo: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IUpdateAccountRequestDTO {
|
|
||||||
is_companyr: boolean;
|
|
||||||
name: string;
|
|
||||||
trade_name: string;
|
|
||||||
tin: string;
|
|
||||||
|
|
||||||
street: string;
|
|
||||||
city: string;
|
|
||||||
state: string;
|
|
||||||
postal_code: string;
|
|
||||||
country: string;
|
|
||||||
|
|
||||||
email: string;
|
|
||||||
phone: string;
|
|
||||||
fax: string;
|
|
||||||
website: string;
|
|
||||||
|
|
||||||
legal_record: string;
|
|
||||||
|
|
||||||
default_tax: number;
|
|
||||||
language_code: string;
|
|
||||||
currency_code: string;
|
|
||||||
logo: string;
|
|
||||||
}
|
|
||||||
@ -1,114 +0,0 @@
|
|||||||
export interface IListAccountsResponseDTO {
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
is_companyr: boolean;
|
|
||||||
name: string;
|
|
||||||
trade_name: string;
|
|
||||||
tin: string;
|
|
||||||
|
|
||||||
street: string;
|
|
||||||
city: string;
|
|
||||||
state: string;
|
|
||||||
postal_code: string;
|
|
||||||
country: string;
|
|
||||||
|
|
||||||
email: string;
|
|
||||||
phone: string;
|
|
||||||
fax: string;
|
|
||||||
website: string;
|
|
||||||
|
|
||||||
legal_record: string;
|
|
||||||
|
|
||||||
default_tax: number;
|
|
||||||
status: string;
|
|
||||||
language_code: string;
|
|
||||||
currency_code: string;
|
|
||||||
logo: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IGetAccountResponseDTO {
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
is_companyr: boolean;
|
|
||||||
name: string;
|
|
||||||
trade_name: string;
|
|
||||||
tin: string;
|
|
||||||
|
|
||||||
street: string;
|
|
||||||
city: string;
|
|
||||||
state: string;
|
|
||||||
postal_code: string;
|
|
||||||
country: string;
|
|
||||||
|
|
||||||
email: string;
|
|
||||||
phone: string;
|
|
||||||
fax: string;
|
|
||||||
website: string;
|
|
||||||
|
|
||||||
legal_record: string;
|
|
||||||
|
|
||||||
default_tax: number;
|
|
||||||
status: string;
|
|
||||||
language_code: string;
|
|
||||||
currency_code: string;
|
|
||||||
logo: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ICreateAccountResponseDTO {
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
is_companyr: boolean;
|
|
||||||
name: string;
|
|
||||||
trade_name: string;
|
|
||||||
tin: string;
|
|
||||||
|
|
||||||
street: string;
|
|
||||||
city: string;
|
|
||||||
state: string;
|
|
||||||
postal_code: string;
|
|
||||||
country: string;
|
|
||||||
|
|
||||||
email: string;
|
|
||||||
phone: string;
|
|
||||||
fax: string;
|
|
||||||
website: string;
|
|
||||||
|
|
||||||
legal_record: string;
|
|
||||||
|
|
||||||
default_tax: number;
|
|
||||||
status: string;
|
|
||||||
language_code: string;
|
|
||||||
currency_code: string;
|
|
||||||
logo: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inferir el tipo en TypeScript desde el esquema Zod
|
|
||||||
//export type IUpdateAcccountResponseDTO = z.infer<typeof IUpdateAcccountResponseDTOSchema>;
|
|
||||||
|
|
||||||
export interface IUpdateAccountResponseDTO {
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
is_companyr: boolean;
|
|
||||||
name: string;
|
|
||||||
trade_name: string;
|
|
||||||
tin: string;
|
|
||||||
|
|
||||||
street: string;
|
|
||||||
city: string;
|
|
||||||
state: string;
|
|
||||||
postal_code: string;
|
|
||||||
country: string;
|
|
||||||
|
|
||||||
email: string;
|
|
||||||
phone: string;
|
|
||||||
fax: string;
|
|
||||||
website: string;
|
|
||||||
|
|
||||||
legal_record: string;
|
|
||||||
|
|
||||||
default_tax: number;
|
|
||||||
status: string;
|
|
||||||
language_code: string;
|
|
||||||
currency_code: string;
|
|
||||||
logo: string;
|
|
||||||
}
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
import * as z from "zod/v4";
|
|
||||||
|
|
||||||
export const ListAccountsRequestSchema = z.object({});
|
|
||||||
|
|
||||||
export const IGetAccountRequestSchema = z.object({});
|
|
||||||
|
|
||||||
export const ICreateAccountRequestSchema = z.object({
|
|
||||||
id: z.string(),
|
|
||||||
|
|
||||||
is_companyr: z.boolean(),
|
|
||||||
name: z.string(),
|
|
||||||
trade_name: z.string(),
|
|
||||||
tin: z.string(),
|
|
||||||
|
|
||||||
street: z.string(),
|
|
||||||
city: z.string(),
|
|
||||||
state: z.string(),
|
|
||||||
postal_code: z.string(),
|
|
||||||
country: z.string(),
|
|
||||||
|
|
||||||
email: z.string().email(), // Validación específica para email
|
|
||||||
phone: z.string(),
|
|
||||||
fax: z.string(),
|
|
||||||
website: z.string().url(), // Validación específica para URL
|
|
||||||
|
|
||||||
legal_record: z.string(),
|
|
||||||
|
|
||||||
default_tax: z.number(),
|
|
||||||
status: z.string(),
|
|
||||||
language_code: LanguageCodeSchema,
|
|
||||||
currency_code: CurrencyCodeSchema,
|
|
||||||
logo: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const IUpdateAccountRequestSchema = z.object({
|
|
||||||
id: z.string(),
|
|
||||||
|
|
||||||
is_companyr: z.boolean(),
|
|
||||||
name: z.string(),
|
|
||||||
trade_name: z.string(),
|
|
||||||
tin: z.string(),
|
|
||||||
|
|
||||||
street: z.string(),
|
|
||||||
city: z.string(),
|
|
||||||
state: z.string(),
|
|
||||||
postal_code: z.string(),
|
|
||||||
country: z.string(),
|
|
||||||
|
|
||||||
email: z.string().email(), // Validación específica para email
|
|
||||||
phone: z.string(),
|
|
||||||
fax: z.string(),
|
|
||||||
website: z.string().url(), // Validación específica para URL
|
|
||||||
|
|
||||||
legal_record: z.string(),
|
|
||||||
|
|
||||||
default_tax: z.number(),
|
|
||||||
status: z.string(),
|
|
||||||
language_code: LanguageCodeSchema,
|
|
||||||
currency_code: CurrencyCodeSchema,
|
|
||||||
logo: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const IDeleteAccountRequestSchema = z.object({});
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export * from "./accounts.request.dto";
|
|
||||||
export * from "./accounts.response.dto";
|
|
||||||
export * from "./accounts.schemas";
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from "./controllers";
|
|
||||||
export * from "./dto";
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
export * from "./list-users";
|
|
||||||
export * from "./login";
|
|
||||||
export * from "./logout";
|
|
||||||
export * from "./refresh-token";
|
|
||||||
export * from "./register";
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./list-users.use-case";
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import { ITransactionManager } from "@/core/common/infrastructure/database";
|
|
||||||
import { Collection, Result } from "@repo/rdx-utils";
|
|
||||||
import { User } from "../../domain";
|
|
||||||
import { IUserService } from "../../domain/services";
|
|
||||||
|
|
||||||
export class ListUsersUseCase {
|
|
||||||
constructor(
|
|
||||||
private readonly userService: IUserService,
|
|
||||||
private readonly transactionManager: ITransactionManager
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public execute(): Promise<Result<Collection<User>, Error>> {
|
|
||||||
return this.transactionManager.complete((transaction) => {
|
|
||||||
return this.userService.findUsers(transaction);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./login.use-case";
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import { ITransactionManager } from "@/core/common/infrastructure/database";
|
|
||||||
import { LoginData } from "../../domain";
|
|
||||||
import { IAuthService } from "../../domain/services";
|
|
||||||
|
|
||||||
export class LoginUseCase {
|
|
||||||
constructor(
|
|
||||||
private readonly authService: IAuthService,
|
|
||||||
private readonly transactionManager: ITransactionManager
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public async execute(loginData: LoginData) {
|
|
||||||
return await this.transactionManager.complete(async (transaction) => {
|
|
||||||
return await this.authService.loginUser(loginData, transaction);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./logout.use-case";
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import { ITransactionManager } from "@/core/common/infrastructure/database";
|
|
||||||
import { LogoutData } from "../../domain";
|
|
||||||
import { IAuthService } from "../../domain/services";
|
|
||||||
|
|
||||||
export class LogoutUseCase {
|
|
||||||
constructor(
|
|
||||||
private readonly authService: IAuthService,
|
|
||||||
private readonly transactionManager: ITransactionManager
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public async execute(logoutData: LogoutData) {
|
|
||||||
return await this.transactionManager.complete(async (transaction) => {
|
|
||||||
return await this.authService.logoutUser(logoutData, transaction);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./refresh-token.use-case";
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
import { ITransactionManager } from "@/core/common/infrastructure/database";
|
|
||||||
import { Token } from "../../domain";
|
|
||||||
import { IAuthService } from "../../domain/services";
|
|
||||||
|
|
||||||
export class RefreshTokenUseCase {
|
|
||||||
constructor(
|
|
||||||
private readonly authService: IAuthService,
|
|
||||||
private readonly transactionManager: ITransactionManager
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public async execute(token: Token) {
|
|
||||||
return await this.transactionManager.complete(async (transaction) => {
|
|
||||||
const payloadData = this.authService.verifyRefreshToken(token);
|
|
||||||
|
|
||||||
/*if (!payload || !payload.email || !payload.user_id || !payload.tab_id || !payload.roles) {
|
|
||||||
return Result.fail(new Error("Invalid input data"));
|
|
||||||
}*/
|
|
||||||
|
|
||||||
return this.authService.generateRefreshToken({
|
|
||||||
...payloadData,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./register.use-case";
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
import { ITransactionManager } from "@/core/common/infrastructure/database";
|
|
||||||
import { logger } from "@/core/logger";
|
|
||||||
import { Result } from "@repo/rdx-utils";
|
|
||||||
import { RegisterData } from "../../domain";
|
|
||||||
import { IAuthService } from "../../domain/services";
|
|
||||||
|
|
||||||
export class RegisterUseCase {
|
|
||||||
constructor(
|
|
||||||
private readonly authService: IAuthService,
|
|
||||||
private readonly transactionManager: ITransactionManager
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public async execute(registerData: RegisterData) {
|
|
||||||
return await this.transactionManager.complete(async (transaction) => {
|
|
||||||
try {
|
|
||||||
return await this.authService.registerUser(registerData, transaction);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
logger.error(error as Error);
|
|
||||||
return Result.fail(error as Error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,91 +0,0 @@
|
|||||||
import { Result } from "@repo/rdx-utils";
|
|
||||||
|
|
||||||
import { AggregateRoot, EmailAddress, UniqueID } from "@/core/common/domain";
|
|
||||||
import { UserAuthenticatedEvent } from "../events";
|
|
||||||
import { HashPassword, PlainPassword, Username } from "../value-objects";
|
|
||||||
|
|
||||||
export interface IAuthenticatedUserProps {
|
|
||||||
username: Username;
|
|
||||||
email: EmailAddress;
|
|
||||||
hashPassword: HashPassword;
|
|
||||||
roles: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IAuthenticatedUser {
|
|
||||||
username: Username;
|
|
||||||
email: EmailAddress;
|
|
||||||
hashPassword: HashPassword;
|
|
||||||
|
|
||||||
accessToken: string;
|
|
||||||
refreshToken: string;
|
|
||||||
|
|
||||||
isUser: boolean;
|
|
||||||
isAdmin: boolean;
|
|
||||||
|
|
||||||
verifyPassword(candidatePassword: PlainPassword): Promise<boolean>;
|
|
||||||
hasRole(role: string): boolean;
|
|
||||||
hasRoles(roles: string[]): boolean;
|
|
||||||
getRoles(): string[];
|
|
||||||
toPersistenceData(): any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AuthenticatedUser
|
|
||||||
extends AggregateRoot<IAuthenticatedUserProps>
|
|
||||||
implements IAuthenticatedUser
|
|
||||||
{
|
|
||||||
public accessToken = "";
|
|
||||||
public refreshToken = "";
|
|
||||||
|
|
||||||
static create(props: IAuthenticatedUserProps, id: UniqueID): Result<AuthenticatedUser, Error> {
|
|
||||||
const user = new AuthenticatedUser(props, id);
|
|
||||||
|
|
||||||
// 🔹 Disparar evento de dominio "UserAuthenticatedEvent"
|
|
||||||
const { email } = props;
|
|
||||||
user.addDomainEvent(new UserAuthenticatedEvent(id, email.toString()));
|
|
||||||
|
|
||||||
return Result.ok(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
verifyPassword(candidatePassword: PlainPassword): Promise<boolean> {
|
|
||||||
return this.props.hashPassword.verifyPassword(candidatePassword.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
getRoles(): string[] {
|
|
||||||
return this.props.roles;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasRole(role: string): boolean {
|
|
||||||
return (this.props.roles || []).some((r) => r === role);
|
|
||||||
}
|
|
||||||
|
|
||||||
hasRoles(roles: string[]): boolean {
|
|
||||||
return roles && roles.map((rol) => this.hasRole(rol)).some((value) => value != false);
|
|
||||||
}
|
|
||||||
|
|
||||||
get username(): Username {
|
|
||||||
return this.props.username;
|
|
||||||
}
|
|
||||||
|
|
||||||
get email(): EmailAddress {
|
|
||||||
return this.props.email;
|
|
||||||
}
|
|
||||||
|
|
||||||
get hashPassword(): HashPassword {
|
|
||||||
return this.props.hashPassword;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isUser(): boolean {
|
|
||||||
return this.hasRole("user");
|
|
||||||
}
|
|
||||||
|
|
||||||
get isAdmin(): boolean {
|
|
||||||
return this.hasRole("admin");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 🔹 Devuelve una representación lista para persistencia
|
|
||||||
*/
|
|
||||||
toPersistenceData(): any {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export * from "./authenticated-user";
|
|
||||||
|
|
||||||
export * from "./role";
|
|
||||||
export * from "./user";
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import { AggregateRoot, UniqueID } from "@/core/common/domain";
|
|
||||||
import { Result } from "@repo/rdx-utils";
|
|
||||||
|
|
||||||
export type IRoleProps = {};
|
|
||||||
|
|
||||||
export type IRole = {};
|
|
||||||
|
|
||||||
export class Role extends AggregateRoot<IRoleProps> implements IRole {
|
|
||||||
static create(props: IRoleProps, id: UniqueID): Result<Role, Error> {
|
|
||||||
const role = new Role(props, id);
|
|
||||||
return Result.ok(role);
|
|
||||||
}
|
|
||||||
|
|
||||||
toPersistenceData(): any {}
|
|
||||||
}
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
import { AggregateRoot, EmailAddress, UniqueID } from "@/core/common/domain";
|
|
||||||
import { Result } from "@repo/rdx-utils";
|
|
||||||
import { UserAuthenticatedEvent } from "../events";
|
|
||||||
import { Username } from "../value-objects";
|
|
||||||
|
|
||||||
export interface IUserProps {
|
|
||||||
username: Username;
|
|
||||||
email: EmailAddress;
|
|
||||||
roles: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IUser {
|
|
||||||
username: Username;
|
|
||||||
email: EmailAddress;
|
|
||||||
|
|
||||||
isUser: boolean;
|
|
||||||
isAdmin: boolean;
|
|
||||||
isActive: boolean;
|
|
||||||
|
|
||||||
hasRole(role: string): boolean;
|
|
||||||
hasRoles(roles: string[]): boolean;
|
|
||||||
getRoles(): string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class User extends AggregateRoot<IUserProps> implements IUser {
|
|
||||||
static create(props: IUserProps, id: UniqueID): Result<User, Error> {
|
|
||||||
const user = new User(props, id);
|
|
||||||
|
|
||||||
// 🔹 Disparar evento de dominio "UserAuthenticatedEvent"
|
|
||||||
const { email } = props;
|
|
||||||
user.addDomainEvent(new UserAuthenticatedEvent(id, email.toString()));
|
|
||||||
|
|
||||||
return Result.ok(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
getRoles(): string[] {
|
|
||||||
return this.props.roles;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasRole(role: string): boolean {
|
|
||||||
return (this.props.roles || []).some((r) => r === role);
|
|
||||||
}
|
|
||||||
|
|
||||||
hasRoles(roles: string[]): boolean {
|
|
||||||
return roles.map((rol) => this.hasRole(rol)).some((value) => value != false);
|
|
||||||
}
|
|
||||||
|
|
||||||
get username(): Username {
|
|
||||||
return this.props.username;
|
|
||||||
}
|
|
||||||
|
|
||||||
get email(): EmailAddress {
|
|
||||||
return this.props.email;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isUser(): boolean {
|
|
||||||
return this.hasRole("user");
|
|
||||||
}
|
|
||||||
|
|
||||||
get isAdmin(): boolean {
|
|
||||||
return this.hasRole("admin");
|
|
||||||
}
|
|
||||||
|
|
||||||
get isActive(): boolean {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
export * from "./jwt-payload";
|
|
||||||
export * from "./login-data";
|
|
||||||
export * from "./logout-data";
|
|
||||||
export * from "./register-data";
|
|
||||||
export * from "./tab-context";
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
import { DomainEntity, EmailAddress, UniqueID } from "@/core/common/domain";
|
|
||||||
import { Result } from "@repo/rdx-utils";
|
|
||||||
|
|
||||||
export interface IJWTPayloadProps {
|
|
||||||
tabId: UniqueID;
|
|
||||||
userId: UniqueID;
|
|
||||||
email: EmailAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IJWTPayloadPrimitives {
|
|
||||||
tab_id: string;
|
|
||||||
user_id: string;
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IJWTPayload {
|
|
||||||
tabId: UniqueID;
|
|
||||||
userId: UniqueID;
|
|
||||||
email: EmailAddress;
|
|
||||||
|
|
||||||
toPersistenceData(): any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class JWTPayload extends DomainEntity<IJWTPayloadProps> implements IJWTPayload {
|
|
||||||
static create(props: IJWTPayloadProps): Result<JWTPayload, Error> {
|
|
||||||
return Result.ok(new JWTPayload(props));
|
|
||||||
}
|
|
||||||
|
|
||||||
static createFromPrimitives(values: IJWTPayloadPrimitives): Result<JWTPayload, Error> {
|
|
||||||
const { email, user_id, tab_id } = values;
|
|
||||||
const emailOrError = EmailAddress.create(email);
|
|
||||||
const userIdOrError = UniqueID.create(user_id, false);
|
|
||||||
const tabIdOrError = UniqueID.create(tab_id, false);
|
|
||||||
|
|
||||||
const result = Result.combine([emailOrError, userIdOrError, tabIdOrError]);
|
|
||||||
|
|
||||||
if (result.isFailure) {
|
|
||||||
return Result.fail(result.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return JWTPayload.create({
|
|
||||||
email: emailOrError.data,
|
|
||||||
userId: userIdOrError.data,
|
|
||||||
tabId: tabIdOrError.data,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get tabId(): UniqueID {
|
|
||||||
return this.props.tabId;
|
|
||||||
}
|
|
||||||
|
|
||||||
get userId(): UniqueID {
|
|
||||||
return this.props.userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
get email(): EmailAddress {
|
|
||||||
return this.props.email;
|
|
||||||
}
|
|
||||||
|
|
||||||
toPersistenceData(): any {
|
|
||||||
return {
|
|
||||||
tab_id: this.tabId.toString(),
|
|
||||||
user_id: this.userId.toString(),
|
|
||||||
email: this.email.toString(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
import { DomainEntity, EmailAddress, UniqueID } from "@/core/common/domain";
|
|
||||||
import { Result } from "@repo/rdx-utils";
|
|
||||||
import { PlainPassword } from "../value-objects";
|
|
||||||
|
|
||||||
export interface ILoginDataProps {
|
|
||||||
email: EmailAddress;
|
|
||||||
plainPassword: PlainPassword;
|
|
||||||
tabId: UniqueID;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ILoginDataPrimitives {
|
|
||||||
email: string;
|
|
||||||
plainPassword: string;
|
|
||||||
tabId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ILoginData {
|
|
||||||
email: EmailAddress;
|
|
||||||
plainPassword: PlainPassword;
|
|
||||||
tabId: UniqueID;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class LoginData extends DomainEntity<ILoginDataProps> implements ILoginData {
|
|
||||||
static create(props: ILoginDataProps): Result<LoginData, Error> {
|
|
||||||
return Result.ok(new this(props));
|
|
||||||
}
|
|
||||||
|
|
||||||
static createFromPrimitives(values: ILoginDataPrimitives): Result<LoginData, Error> {
|
|
||||||
const { email, plainPassword, tabId } = values;
|
|
||||||
const emailOrError = EmailAddress.create(email);
|
|
||||||
const plainPasswordOrError = PlainPassword.create(plainPassword);
|
|
||||||
const tabIdOrError = UniqueID.create(tabId, false);
|
|
||||||
|
|
||||||
const result = Result.combine([emailOrError, plainPasswordOrError, tabIdOrError]);
|
|
||||||
|
|
||||||
if (result.isFailure) {
|
|
||||||
return Result.fail(result.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return LoginData.create({
|
|
||||||
email: emailOrError.data,
|
|
||||||
plainPassword: plainPasswordOrError.data,
|
|
||||||
tabId: tabIdOrError.data,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get email(): EmailAddress {
|
|
||||||
return this.props.email;
|
|
||||||
}
|
|
||||||
|
|
||||||
get plainPassword(): PlainPassword {
|
|
||||||
return this.props.plainPassword;
|
|
||||||
}
|
|
||||||
|
|
||||||
get tabId(): UniqueID {
|
|
||||||
return this.props.tabId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
import { DomainEntity, EmailAddress, UniqueID } from "@/core/common/domain";
|
|
||||||
import { Result } from "@repo/rdx-utils";
|
|
||||||
|
|
||||||
export interface ILogoutDataProps {
|
|
||||||
email: EmailAddress;
|
|
||||||
tabId: UniqueID;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ILogoutDataPrimitives {
|
|
||||||
email: string;
|
|
||||||
tabId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ILogoutData {
|
|
||||||
email: EmailAddress;
|
|
||||||
tabId: UniqueID;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class LogoutData extends DomainEntity<ILogoutDataProps> implements ILogoutData {
|
|
||||||
static create(props: ILogoutDataProps): Result<LogoutData, Error> {
|
|
||||||
return Result.ok(new this(props));
|
|
||||||
}
|
|
||||||
|
|
||||||
static createFromPrimitives(values: ILogoutDataPrimitives): Result<LogoutData, Error> {
|
|
||||||
const { email, tabId } = values;
|
|
||||||
const emailOrError = EmailAddress.create(email);
|
|
||||||
const tabIdOrError = UniqueID.create(tabId, false);
|
|
||||||
|
|
||||||
const result = Result.combine([emailOrError, tabIdOrError]);
|
|
||||||
|
|
||||||
if (result.isFailure) {
|
|
||||||
return Result.fail(result.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return LogoutData.create({
|
|
||||||
email: emailOrError.data,
|
|
||||||
tabId: tabIdOrError.data,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get email(): EmailAddress {
|
|
||||||
return this.props.email;
|
|
||||||
}
|
|
||||||
|
|
||||||
get tabId(): UniqueID {
|
|
||||||
return this.props.tabId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
import { DomainEntity, EmailAddress } from "@/core/common/domain";
|
|
||||||
import { Result } from "@repo/rdx-utils";
|
|
||||||
import { HashPassword, Username } from "../value-objects";
|
|
||||||
|
|
||||||
export interface IRegisterDataProps {
|
|
||||||
username: Username;
|
|
||||||
email: EmailAddress;
|
|
||||||
hashPassword: HashPassword;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IRegisterDataPrimitives {
|
|
||||||
username: string;
|
|
||||||
email: string;
|
|
||||||
plainPassword: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IRegisterData {
|
|
||||||
username: Username;
|
|
||||||
email: EmailAddress;
|
|
||||||
hashPassword: HashPassword;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RegisterData extends DomainEntity<IRegisterDataProps> implements IRegisterData {
|
|
||||||
static create(props: IRegisterDataProps): Result<RegisterData, Error> {
|
|
||||||
return Result.ok(new this(props));
|
|
||||||
}
|
|
||||||
|
|
||||||
static createFromPrimitives(props: IRegisterDataPrimitives): Result<RegisterData, Error> {
|
|
||||||
const { username, email, plainPassword } = props;
|
|
||||||
|
|
||||||
const userNameOrError = Username.create(username);
|
|
||||||
const emailOrError = EmailAddress.create(email);
|
|
||||||
const hashPasswordOrError = HashPassword.createFromPlainText(plainPassword);
|
|
||||||
|
|
||||||
const result = Result.combine([userNameOrError, emailOrError, hashPasswordOrError]);
|
|
||||||
|
|
||||||
if (result.isFailure) {
|
|
||||||
return Result.fail(result.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return RegisterData.create({
|
|
||||||
username: userNameOrError.data,
|
|
||||||
email: emailOrError.data,
|
|
||||||
hashPassword: hashPasswordOrError.data,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get username(): Username {
|
|
||||||
return this.props.username;
|
|
||||||
}
|
|
||||||
|
|
||||||
get email(): EmailAddress {
|
|
||||||
return this.props.email;
|
|
||||||
}
|
|
||||||
|
|
||||||
get hashPassword(): HashPassword {
|
|
||||||
return this.props.hashPassword;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
import { DomainEntity, UniqueID } from "@/core/common/domain";
|
|
||||||
import { Result } from "@repo/rdx-utils";
|
|
||||||
|
|
||||||
export interface ITabContextProps {
|
|
||||||
tabId: UniqueID;
|
|
||||||
userId: UniqueID;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ITabContextPrimitives {
|
|
||||||
id: string;
|
|
||||||
tab_id: string;
|
|
||||||
user_id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ITabContext {
|
|
||||||
tabId: UniqueID;
|
|
||||||
userId: UniqueID;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TabContext extends DomainEntity<ITabContextProps> implements ITabContext {
|
|
||||||
static create(props: ITabContextProps, id?: UniqueID): Result<TabContext, Error> {
|
|
||||||
return Result.ok(new this(props, id));
|
|
||||||
}
|
|
||||||
|
|
||||||
static createFromPrimitives(values: ITabContextPrimitives): Result<TabContext, Error> {
|
|
||||||
const { user_id, tab_id } = values;
|
|
||||||
const userIdOrError = UniqueID.create(user_id, false);
|
|
||||||
const tabIdOrError = UniqueID.create(tab_id, false);
|
|
||||||
|
|
||||||
const result = Result.combine([userIdOrError, tabIdOrError]);
|
|
||||||
|
|
||||||
if (result.isFailure) {
|
|
||||||
return Result.fail(result.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return TabContext.create({
|
|
||||||
userId: userIdOrError.data,
|
|
||||||
tabId: tabIdOrError.data,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
get tabId(): UniqueID {
|
|
||||||
return this.props.tabId;
|
|
||||||
}
|
|
||||||
|
|
||||||
get userId(): UniqueID {
|
|
||||||
return this.props.userId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./user-authenticated.event";
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
import { IDomainEvent, UniqueID } from "@/core/common/domain";
|
|
||||||
|
|
||||||
export class UserAuthenticatedEvent implements IDomainEvent {
|
|
||||||
public readonly eventName = "UserAuthenticated";
|
|
||||||
public readonly occurredAt: Date;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public readonly aggregateId: UniqueID,
|
|
||||||
public readonly email: string // Email en formato string
|
|
||||||
) {
|
|
||||||
this.occurredAt = new Date();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
export * from "./aggregates";
|
|
||||||
export * from "./entities";
|
|
||||||
export * from "./events";
|
|
||||||
export * from "./repositories";
|
|
||||||
export * from "./value-objects";
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import { Result } from "@repo/rdx-utils";
|
|
||||||
|
|
||||||
import { EmailAddress } from "@/core/common/domain";
|
|
||||||
import { AuthenticatedUser } from "../aggregates";
|
|
||||||
import { Username } from "../value-objects";
|
|
||||||
|
|
||||||
export interface IAuthenticatedUserRepository {
|
|
||||||
getUserByEmail(email: EmailAddress, transaction?: any): Promise<Result<AuthenticatedUser, Error>>;
|
|
||||||
userExists(
|
|
||||||
username: Username,
|
|
||||||
email: EmailAddress,
|
|
||||||
transaction?: any
|
|
||||||
): Promise<Result<boolean, Error>>;
|
|
||||||
createUser(user: AuthenticatedUser, transaction?: any): Promise<Result<void, Error>>;
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export * from "./authenticated-user-repository.interface";
|
|
||||||
export * from "./tab-context-repository.interface";
|
|
||||||
export * from "./user-permission-repository.interface";
|
|
||||||
export * from "./user-repository.interface";
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
import { UniqueID } from "@/core/common/domain";
|
|
||||||
import { Result } from "@repo/rdx-utils";
|
|
||||||
import { Transaction } from "sequelize";
|
|
||||||
import { TabContext } from "../entities";
|
|
||||||
|
|
||||||
export interface ITabContextRepository {
|
|
||||||
getContextByTabId(tabId: UniqueID, transaction?: any): Promise<Result<TabContext, Error>>;
|
|
||||||
|
|
||||||
contextExistsByTabId(tabId: UniqueID, transaction?: any): Promise<Result<boolean, Error>>;
|
|
||||||
|
|
||||||
registerContextByTabId(
|
|
||||||
context: TabContext,
|
|
||||||
transaction?: Transaction
|
|
||||||
): Promise<Result<void, Error>>;
|
|
||||||
|
|
||||||
unregisterContextByTabId(
|
|
||||||
context: TabContext,
|
|
||||||
transaction?: Transaction
|
|
||||||
): Promise<Result<void, Error>>;
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export type IUserPermissionRepository = {}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
import { EmailAddress, UniqueID } from "@/core/common/domain";
|
|
||||||
import { Collection, Result } from "@repo/rdx-utils";
|
|
||||||
import { User } from "../aggregates";
|
|
||||||
|
|
||||||
export interface IUserRepository {
|
|
||||||
findAll(transaction?: any): Promise<Result<Collection<User>, Error>>;
|
|
||||||
findById(id: UniqueID, transaction?: any): Promise<Result<User, Error>>;
|
|
||||||
findByEmail(email: EmailAddress, transaction?: any): Promise<Result<User, Error>>;
|
|
||||||
}
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
import { EmailAddress } from "@/core/common/domain";
|
|
||||||
import { Result } from "@repo/rdx-utils";
|
|
||||||
import {
|
|
||||||
AuthenticatedUser,
|
|
||||||
IJWTPayload,
|
|
||||||
LoginData,
|
|
||||||
LogoutData,
|
|
||||||
RegisterData,
|
|
||||||
TabContext,
|
|
||||||
Token,
|
|
||||||
} from "..";
|
|
||||||
|
|
||||||
export interface IAuthService {
|
|
||||||
generateAccessToken(payload: IJWTPayload): Result<Token, Error>;
|
|
||||||
generateRefreshToken(payload: IJWTPayload): Result<Token, Error>;
|
|
||||||
verifyRefreshToken(token: Token): IJWTPayload;
|
|
||||||
|
|
||||||
registerUser(
|
|
||||||
registerData: RegisterData,
|
|
||||||
transaction?: any
|
|
||||||
): Promise<Result<AuthenticatedUser, Error>>;
|
|
||||||
|
|
||||||
loginUser(
|
|
||||||
loginData: LoginData,
|
|
||||||
transaction?: any
|
|
||||||
): Promise<
|
|
||||||
Result<
|
|
||||||
{
|
|
||||||
user: AuthenticatedUser;
|
|
||||||
tabContext: TabContext;
|
|
||||||
tokens: {
|
|
||||||
accessToken: Token;
|
|
||||||
refreshToken: Token;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
Error
|
|
||||||
>
|
|
||||||
>;
|
|
||||||
|
|
||||||
logoutUser(logoutData: LogoutData, transaction?: any): Promise<Result<void, Error>>;
|
|
||||||
getUserByEmail(email: EmailAddress, transaction?: any): Promise<Result<AuthenticatedUser, Error>>;
|
|
||||||
}
|
|
||||||
@ -1,205 +0,0 @@
|
|||||||
import { EmailAddress } from "@/core/common/domain";
|
|
||||||
import { Result } from "@repo/rdx-utils";
|
|
||||||
import {
|
|
||||||
AuthenticatedUser,
|
|
||||||
type IJWTPayload,
|
|
||||||
type LoginData,
|
|
||||||
type RegisterData,
|
|
||||||
TabContext,
|
|
||||||
Token,
|
|
||||||
} from "..";
|
|
||||||
|
|
||||||
import { UniqueID } from "@/core/common/domain";
|
|
||||||
import { IAuthenticatedUserRepository, JWTPayload } from "..";
|
|
||||||
import { JwtHelper } from "../../../../../../../modules/auth/src/api/lib/passport/jwt.helper";
|
|
||||||
import { ITabContextRepository } from "../repositories/tab-context-repository.interface";
|
|
||||||
import { IAuthService } from "./auth-service.interface";
|
|
||||||
|
|
||||||
const ACCESS_EXPIRATION = process.env.JWT_ACCESS_EXPIRATION || "1h";
|
|
||||||
const REFRESH_EXPIRATION = process.env.JWT_REFRESH_EXPIRATION || "7d";
|
|
||||||
|
|
||||||
export class AuthService implements IAuthService {
|
|
||||||
constructor(
|
|
||||||
private readonly authUserRepo: IAuthenticatedUserRepository,
|
|
||||||
private readonly tabContextRepo: ITabContextRepository
|
|
||||||
) {}
|
|
||||||
|
|
||||||
generateAccessToken(payload: IJWTPayload): Result<Token, Error> {
|
|
||||||
const data = payload.toPersistenceData();
|
|
||||||
return Token.create(JwtHelper.generateToken(data, ACCESS_EXPIRATION));
|
|
||||||
}
|
|
||||||
|
|
||||||
generateRefreshToken(payload: IJWTPayload): Result<Token, Error> {
|
|
||||||
const data = payload.toPersistenceData();
|
|
||||||
return Token.create(JwtHelper.generateToken(data, REFRESH_EXPIRATION));
|
|
||||||
}
|
|
||||||
|
|
||||||
verifyRefreshToken(token: Token): IJWTPayload {
|
|
||||||
return JwtHelper.verifyToken(token.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Registra un nuevo usuario en la base de datos bajo transacción.
|
|
||||||
*/
|
|
||||||
async registerUser(
|
|
||||||
registerData: RegisterData,
|
|
||||||
transaction?: any
|
|
||||||
): Promise<Result<AuthenticatedUser, Error>> {
|
|
||||||
const { username, email, hashPassword } = registerData;
|
|
||||||
|
|
||||||
// Verificar si el usuario ya existe
|
|
||||||
const userExists = await this.authUserRepo.userExists(username, email, transaction);
|
|
||||||
if (userExists.isSuccess && userExists.data) {
|
|
||||||
return Result.fail(new Error("Email is already registered"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const newUserId = UniqueID.generateNewID().data;
|
|
||||||
|
|
||||||
const userOrError = AuthenticatedUser.create(
|
|
||||||
{
|
|
||||||
username,
|
|
||||||
email,
|
|
||||||
hashPassword,
|
|
||||||
roles: ["USER"],
|
|
||||||
},
|
|
||||||
newUserId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (userOrError.isFailure) {
|
|
||||||
return Result.fail(userOrError.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const createdResult = await this.authUserRepo.createUser(userOrError.data, transaction);
|
|
||||||
|
|
||||||
if (createdResult.isFailure) {
|
|
||||||
return Result.fail(createdResult.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.ok(userOrError.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Autentica a un usuario validando su email y contraseña.
|
|
||||||
*/
|
|
||||||
async loginUser(
|
|
||||||
loginData: LoginData,
|
|
||||||
transaction?: any
|
|
||||||
): Promise<
|
|
||||||
Result<
|
|
||||||
{
|
|
||||||
user: AuthenticatedUser;
|
|
||||||
tabContext: TabContext;
|
|
||||||
tokens: {
|
|
||||||
accessToken: Token;
|
|
||||||
refreshToken: Token;
|
|
||||||
};
|
|
||||||
},
|
|
||||||
Error
|
|
||||||
>
|
|
||||||
> {
|
|
||||||
let result: any;
|
|
||||||
const { email, plainPassword, tabId } = loginData;
|
|
||||||
|
|
||||||
// 🔹 Verificar si el usuario existe en la base de datos
|
|
||||||
result = await this.authUserRepo.getUserByEmail(email, transaction);
|
|
||||||
if (result.isFailure) {
|
|
||||||
return Result.fail(new Error("Invalid email or password"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = result.data;
|
|
||||||
|
|
||||||
// 🔹 Verificar que la contraseña sea correcta
|
|
||||||
const isValidPassword = await user.verifyPassword(plainPassword);
|
|
||||||
if (!isValidPassword) {
|
|
||||||
return Result.fail(new Error("Invalid email or password"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registrar o actualizar el contexto de ese tab ID
|
|
||||||
const contextOrError = TabContext.create({
|
|
||||||
userId: user.id,
|
|
||||||
tabId: tabId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 🔹 Generar Access Token y Refresh Token
|
|
||||||
const payloadOrError = JWTPayload.create({
|
|
||||||
userId: user.id,
|
|
||||||
email: email,
|
|
||||||
tabId: tabId,
|
|
||||||
//roles: ["USER"],
|
|
||||||
});
|
|
||||||
|
|
||||||
result = Result.combine([contextOrError, payloadOrError]);
|
|
||||||
if (result.isFailure) {
|
|
||||||
return Result.fail(new Error("Error on login"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabContext = contextOrError.data;
|
|
||||||
await this.tabContextRepo.registerContextByTabId(tabContext, transaction);
|
|
||||||
|
|
||||||
const accessTokenOrError = this.generateAccessToken(payloadOrError.data);
|
|
||||||
const refreshTokenOrError = this.generateRefreshToken(payloadOrError.data);
|
|
||||||
|
|
||||||
result = Result.combine([accessTokenOrError, refreshTokenOrError]);
|
|
||||||
|
|
||||||
if (result.isFailure) {
|
|
||||||
return Result.fail(result.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.ok({
|
|
||||||
user,
|
|
||||||
tabContext,
|
|
||||||
tokens: {
|
|
||||||
accessToken: accessTokenOrError.data,
|
|
||||||
refreshToken: refreshTokenOrError.data,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Autentica a un usuario validando su email y contraseña.
|
|
||||||
*/
|
|
||||||
async logoutUser(
|
|
||||||
params: { email: EmailAddress; tabId: UniqueID },
|
|
||||||
transaction?: any
|
|
||||||
): Promise<Result<void, Error>> {
|
|
||||||
const { email, tabId } = params;
|
|
||||||
|
|
||||||
// 🔹 Verificar si el usuario existe en la base de datos
|
|
||||||
const userResult = await this.authUserRepo.getUserByEmail(email, transaction);
|
|
||||||
if (userResult.isFailure) {
|
|
||||||
return Result.fail(new Error("Invalid email or password"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = userResult.data;
|
|
||||||
|
|
||||||
const contextOrError = TabContext.create({
|
|
||||||
userId: user.id,
|
|
||||||
tabId: tabId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (contextOrError.isFailure) {
|
|
||||||
return Result.fail(new Error("Error creating user context"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Desregistrar el contexto de ese tab ID
|
|
||||||
await this.tabContextRepo.unregisterContextByTabId(contextOrError.data, transaction);
|
|
||||||
|
|
||||||
return Result.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUserByEmail(
|
|
||||||
email: EmailAddress,
|
|
||||||
transaction?: any
|
|
||||||
): Promise<Result<AuthenticatedUser, Error>> {
|
|
||||||
const userResult = await this.authUserRepo.getUserByEmail(email, transaction);
|
|
||||||
|
|
||||||
if (userResult.isFailure || !userResult.data) {
|
|
||||||
return Result.fail(new Error("Invalid email or password"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.ok(userResult.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
export * from "./auth-service.interface";
|
|
||||||
export * from "./auth.service";
|
|
||||||
|
|
||||||
export * from "./tab-context-service.interface";
|
|
||||||
export * from "./tab-context.service";
|
|
||||||
|
|
||||||
export * from "./user-service.interface";
|
|
||||||
export * from "./user.service";
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import { UniqueID } from "@/core/common/domain";
|
|
||||||
import { Result } from "@repo/rdx-utils";
|
|
||||||
import { TabContext } from "../entities";
|
|
||||||
|
|
||||||
export interface ITabContextService {
|
|
||||||
getContextByTabId(tabId: UniqueID, transaction?: any): Promise<Result<TabContext, Error>>;
|
|
||||||
createContext(
|
|
||||||
params: { tabId: UniqueID; userId: UniqueID },
|
|
||||||
transaction?: any
|
|
||||||
): Promise<Result<TabContext, Error>>;
|
|
||||||
removeContext(
|
|
||||||
params: { tabId: UniqueID; userId: UniqueID },
|
|
||||||
transaction?: any
|
|
||||||
): Promise<Result<void, Error>>;
|
|
||||||
}
|
|
||||||
@ -1,87 +0,0 @@
|
|||||||
import { UniqueID } from "@/core/common/domain";
|
|
||||||
import { Result } from "@repo/rdx-utils";
|
|
||||||
import { TabContext } from "../entities";
|
|
||||||
import { ITabContextRepository } from "../repositories";
|
|
||||||
import { ITabContextService } from "./tab-context-service.interface";
|
|
||||||
|
|
||||||
export class TabContextService implements ITabContextService {
|
|
||||||
constructor(private readonly tabContextRepo: ITabContextRepository) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Obtiene el contexto de una pestaña por su ID
|
|
||||||
*/
|
|
||||||
async getContextByTabId(tabId: UniqueID, transaction?: any): Promise<Result<TabContext, Error>> {
|
|
||||||
// Verificar si la pestaña existe
|
|
||||||
const tabContextOrError = await this.tabContextRepo.getContextByTabId(tabId, transaction);
|
|
||||||
if (tabContextOrError.isSuccess && !tabContextOrError.data) {
|
|
||||||
return Result.fail(new Error("Invalid or expired Tab ID"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tabContextOrError.isFailure) {
|
|
||||||
return Result.fail(tabContextOrError.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.ok(tabContextOrError.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registra un nuevo contexto de pestaña para un usuario
|
|
||||||
*/
|
|
||||||
async createContext(
|
|
||||||
params: {
|
|
||||||
tabId: UniqueID;
|
|
||||||
userId: UniqueID;
|
|
||||||
},
|
|
||||||
transaction?: any
|
|
||||||
): Promise<Result<TabContext, Error>> {
|
|
||||||
const { tabId, userId } = params;
|
|
||||||
|
|
||||||
if (!userId || !tabId) {
|
|
||||||
return Result.fail(new Error("User ID and Tab ID are required"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const contextOrError = TabContext.create(
|
|
||||||
{
|
|
||||||
userId,
|
|
||||||
tabId,
|
|
||||||
},
|
|
||||||
UniqueID.generateNewID().data
|
|
||||||
);
|
|
||||||
|
|
||||||
if (contextOrError.isFailure) {
|
|
||||||
return Result.fail(contextOrError.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.tabContextRepo.registerContextByTabId(contextOrError.data, transaction);
|
|
||||||
|
|
||||||
return Result.ok(contextOrError.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Elimina un contexto de pestaña por su ID
|
|
||||||
*/
|
|
||||||
async removeContext(
|
|
||||||
params: { tabId: UniqueID; userId: UniqueID },
|
|
||||||
transaction?: any
|
|
||||||
): Promise<Result<void, Error>> {
|
|
||||||
const { tabId, userId } = params;
|
|
||||||
|
|
||||||
if (!userId || !tabId) {
|
|
||||||
return Result.fail(new Error("User ID and Tab ID are required"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const contextOrError = TabContext.create(
|
|
||||||
{
|
|
||||||
userId,
|
|
||||||
tabId,
|
|
||||||
},
|
|
||||||
UniqueID.generateNewID().data
|
|
||||||
);
|
|
||||||
|
|
||||||
if (contextOrError.isFailure) {
|
|
||||||
return Result.fail(contextOrError.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.tabContextRepo.unregisterContextByTabId(contextOrError.data, transaction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { UniqueID } from "@/core/common/domain";
|
|
||||||
import { Collection, Result } from "@repo/rdx-utils";
|
|
||||||
import { User } from "../aggregates";
|
|
||||||
|
|
||||||
export interface IUserService {
|
|
||||||
findUsers(transaction?: any): Promise<Result<Collection<User>, Error>>;
|
|
||||||
findUserById(userId: UniqueID, transaction?: any): Promise<Result<User>>;
|
|
||||||
}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
import { UniqueID } from "@/core/common/domain";
|
|
||||||
import { Collection, Result } from "@repo/rdx-utils";
|
|
||||||
import { IUserRepository, User } from "..";
|
|
||||||
import { IUserService } from "./user-service.interface";
|
|
||||||
|
|
||||||
export class UserService implements IUserService {
|
|
||||||
constructor(private readonly userRepository: IUserRepository) {}
|
|
||||||
|
|
||||||
async findUsers(transaction?: any): Promise<Result<Collection<User>, Error>> {
|
|
||||||
const usersOrError = await this.userRepository.findAll(transaction);
|
|
||||||
if (usersOrError.isFailure) {
|
|
||||||
return Result.fail(usersOrError.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Solo devolver usuarios activos
|
|
||||||
const activeUsers = usersOrError.data.filter((user) => user.isActive);
|
|
||||||
return Result.ok(new Collection(activeUsers));
|
|
||||||
}
|
|
||||||
|
|
||||||
async findUserById(userId: UniqueID, transaction?: any): Promise<Result<User>> {
|
|
||||||
return await this.userRepository.findById(userId, transaction);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*public async createUser(
|
|
||||||
data: { name: string; email: EmailAddress },
|
|
||||||
transaction?: Transaction
|
|
||||||
): Promise<Result<User>> {
|
|
||||||
// Evitar duplicados por email
|
|
||||||
const existingUser = await this.userRepository.findByEmail(data.email);
|
|
||||||
if (existingUser.isSuccess) {
|
|
||||||
return Result.fail(new Error("El correo ya está registrado."));
|
|
||||||
}
|
|
||||||
|
|
||||||
const newUser = User.create({
|
|
||||||
email,
|
|
||||||
username
|
|
||||||
})
|
|
||||||
return await this.userRepository.save(newUser, transaction);
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
import { ValueObject } from "@repo/rdx-ddd";
|
|
||||||
import { Result } from "@repo/rdx-utils";
|
|
||||||
import * as z from "zod/v4";
|
|
||||||
|
|
||||||
const RoleSchema = z.enum(["Admin", "User", "Manager", "Editor"]);
|
|
||||||
|
|
||||||
interface UserRolesProps {
|
|
||||||
value: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UserRoles extends ValueObject<UserRolesProps> {
|
|
||||||
static create(roles: string[]): Result<UserRoles, Error> {
|
|
||||||
const result = UserRoles.validate(roles);
|
|
||||||
|
|
||||||
return result.success
|
|
||||||
? Result.ok(new UserRoles({ value: result.data }))
|
|
||||||
: Result.fail(new Error("Invalid user roles"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static validate(roles: string[]) {
|
|
||||||
return z.array(RoleSchema).safeParse(roles);
|
|
||||||
}
|
|
||||||
|
|
||||||
hasRole(role: string): boolean {
|
|
||||||
return this.props.value.includes(role);
|
|
||||||
}
|
|
||||||
|
|
||||||
getValue() {
|
|
||||||
return this.props.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
toPrimitive() {
|
|
||||||
return this.props.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
import bcrypt from "bcrypt";
|
|
||||||
import { HashPassword } from "./hash-password";
|
|
||||||
|
|
||||||
describe("HashPassword", () => {
|
|
||||||
test("debe crear una instancia de HashPassword desde un texto plano válido", () => {
|
|
||||||
const result = HashPassword.createFromPlainText("securepassword");
|
|
||||||
expect(result.isSuccess).toBe(true);
|
|
||||||
expect(result.data).toBeInstanceOf(HashPassword);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("debe fallar al crear una instancia con una contraseña demasiado corta", () => {
|
|
||||||
const result = HashPassword.createFromPlainText("123");
|
|
||||||
expect(result.isFailure).toBe(true);
|
|
||||||
expect(result.error.message).toBe("Password must be at least 6 characters long");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("debe crear una instancia de HashPassword desde un hash válido", () => {
|
|
||||||
const hashedPassword = bcrypt.hashSync("securepassword", 10);
|
|
||||||
const result = HashPassword.createFromHash(hashedPassword);
|
|
||||||
expect(result.isSuccess).toBe(true);
|
|
||||||
expect(result.data).toBeInstanceOf(HashPassword);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("debe verificar correctamente una contraseña válida", async () => {
|
|
||||||
const password = "securepassword";
|
|
||||||
const result = HashPassword.createFromPlainText(password);
|
|
||||||
expect(result.isSuccess).toBe(true);
|
|
||||||
const hashPasswordInstance = result.data;
|
|
||||||
|
|
||||||
const isValid = await hashPasswordInstance.verifyPassword(password);
|
|
||||||
expect(isValid).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("debe fallar la verificación con una contraseña incorrecta", async () => {
|
|
||||||
const password = "securepassword";
|
|
||||||
const wrongPassword = "wrongpassword";
|
|
||||||
const result = HashPassword.createFromPlainText(password);
|
|
||||||
expect(result.isSuccess).toBe(true);
|
|
||||||
const hashPasswordInstance = result.data;
|
|
||||||
|
|
||||||
const isValid = await hashPasswordInstance.verifyPassword(wrongPassword);
|
|
||||||
expect(isValid).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
import { ValueObject } from "@repo/rdx-ddd";
|
|
||||||
import { Result } from "@repo/rdx-utils";
|
|
||||||
import bcrypt from "bcrypt";
|
|
||||||
import * as z from "zod/v4";
|
|
||||||
|
|
||||||
interface HashPasswordProps {
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class HashPassword extends ValueObject<HashPasswordProps> {
|
|
||||||
private static readonly SALT_ROUNDS = 10;
|
|
||||||
|
|
||||||
static createFromPlainText(plainTextPassword: string): Result<HashPassword, Error> {
|
|
||||||
const result = HashPassword.validate(plainTextPassword);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
return Result.fail(new Error(result.error.errors[0].message));
|
|
||||||
}
|
|
||||||
|
|
||||||
const hashed = bcrypt.hashSync(result.data, this.SALT_ROUNDS);
|
|
||||||
return Result.ok(new HashPassword({ value: hashed }));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static validate(password: string) {
|
|
||||||
const schema = z.string().min(6, { message: "Password must be at least 6 characters long" });
|
|
||||||
return schema.safeParse(password);
|
|
||||||
}
|
|
||||||
|
|
||||||
static createFromHash(hashedPassword: string): Result<HashPassword, Error> {
|
|
||||||
return Result.ok(new HashPassword({ value: hashedPassword }));
|
|
||||||
}
|
|
||||||
|
|
||||||
async verifyPassword(plainTextPassword: string): Promise<boolean> {
|
|
||||||
return await bcrypt.compare(plainTextPassword, this.props.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
getValue() {
|
|
||||||
return this.props.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
return this.props.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
toPrimitive() {
|
|
||||||
return this.props.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
export * from "./auth-user-roles";
|
|
||||||
export * from "./hash-password";
|
|
||||||
export * from "./plain-password";
|
|
||||||
export * from "./token";
|
|
||||||
export * from "./username";
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
import { ValueObject } from "@repo/rdx-ddd";
|
|
||||||
import { Result } from "@repo/rdx-utils";
|
|
||||||
import * as z from "zod/v4";
|
|
||||||
|
|
||||||
interface PlainPasswordProps {
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PlainPassword extends ValueObject<PlainPasswordProps> {
|
|
||||||
static create(plainTextPassword: string): Result<PlainPassword, Error> {
|
|
||||||
const result = PlainPassword.validate(plainTextPassword);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
return Result.fail(new Error(result.error.errors[0].message));
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.ok(new PlainPassword({ value: result.data }));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static validate(password: string) {
|
|
||||||
const schema = z.string().min(6, { message: "Password must be at least 6 characters long" });
|
|
||||||
return schema.safeParse(password);
|
|
||||||
}
|
|
||||||
|
|
||||||
getValue() {
|
|
||||||
return this.props.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
return this.props.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
toPrimitive() {
|
|
||||||
return this.props.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
import { ValueObject } from "@repo/rdx-ddd";
|
|
||||||
import { Result } from "@repo/rdx-utils";
|
|
||||||
import * as z from "zod/v4";
|
|
||||||
|
|
||||||
interface TokenProps {
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Token extends ValueObject<TokenProps> {
|
|
||||||
static create(token: string): Result<Token, Error> {
|
|
||||||
const result = Token.validate(token);
|
|
||||||
|
|
||||||
if (!result.success) {
|
|
||||||
return Result.fail(new Error(result.error.errors[0].message));
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.ok(new Token({ value: result.data }));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static validate(token: string) {
|
|
||||||
const schema = z.string().min(1, { message: "Invalid token string" });
|
|
||||||
return schema.safeParse(token);
|
|
||||||
}
|
|
||||||
|
|
||||||
getValue() {
|
|
||||||
return this.props.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
return this.props.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
toPrimitive() {
|
|
||||||
return this.props.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
import { ValueObject } from "@repo/rdx-ddd";
|
|
||||||
import { Result } from "@repo/rdx-utils";
|
|
||||||
import * as z from "zod/v4";
|
|
||||||
|
|
||||||
interface UsernameProps {
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Username extends ValueObject<UsernameProps> {
|
|
||||||
static create(username: string): Result<Username, Error> {
|
|
||||||
const result = Username.validate(username);
|
|
||||||
|
|
||||||
return result.success
|
|
||||||
? Result.ok(new Username({ value: result.data }))
|
|
||||||
: Result.fail(new Error(result.error.errors[0].message));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static validate(username: string) {
|
|
||||||
const schema = z
|
|
||||||
.string()
|
|
||||||
.min(3, { message: "Username must be at least 3 characters long" })
|
|
||||||
.max(30, { message: "Username cannot exceed 30 characters" })
|
|
||||||
.regex(/^[a-zA-Z0-9_]+$/, {
|
|
||||||
message: "Username can only contain letters, numbers, and underscores",
|
|
||||||
});
|
|
||||||
|
|
||||||
return schema.safeParse(username);
|
|
||||||
}
|
|
||||||
|
|
||||||
getValue() {
|
|
||||||
return this.props.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
return this.props.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
toPrimitive() {
|
|
||||||
return this.props.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import type { Request } from "express";
|
|
||||||
|
|
||||||
import type { AuthenticatedUser, TabContext } from "../../domain";
|
|
||||||
|
|
||||||
export interface TabContextRequest extends Request {
|
|
||||||
tabContext?: TabContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AuthenticatedRequest extends Request {
|
|
||||||
user?: AuthenticatedUser;
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export * from "../../../../../../modules/auth/src/api/lib/passport";
|
|
||||||
export * from "./mappers";
|
|
||||||
export * from "./middleware";
|
|
||||||
export * from "./sequelize";
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
import {
|
|
||||||
EmailAddress,
|
|
||||||
ISequelizeMapper,
|
|
||||||
MapperParamsType,
|
|
||||||
Result,
|
|
||||||
SequelizeMapper,
|
|
||||||
UniqueID,
|
|
||||||
} from "@/core";
|
|
||||||
import { AuthUserCreationAttributes, AuthUserModel } from '../sequelize';
|
|
||||||
import { AuthenticatedUser, HashPassword, Username } from '../../domain';
|
|
||||||
|
|
||||||
export interface IAuthenticatedUserMapper
|
|
||||||
extends ISequelizeMapper<AuthUserModel, AuthUserCreationAttributes, AuthenticatedUser> {}
|
|
||||||
|
|
||||||
export class AuthenticatedUserMapper
|
|
||||||
extends SequelizeMapper<AuthUserModel, AuthUserCreationAttributes, AuthenticatedUser>
|
|
||||||
implements IAuthenticatedUserMapper
|
|
||||||
{
|
|
||||||
public mapToDomain(
|
|
||||||
source: AuthUserModel,
|
|
||||||
params?: MapperParamsType
|
|
||||||
): Result<AuthenticatedUser, Error> {
|
|
||||||
// Crear Value Objects asegurando que sean válidos
|
|
||||||
const uniqueIdResult = UniqueID.create(source.id);
|
|
||||||
const usernameResult = Username.create(source.username);
|
|
||||||
const passwordHashResult = HashPassword.createFromHash(source.hash_password);
|
|
||||||
const emailResult = EmailAddress.create(source.email);
|
|
||||||
|
|
||||||
// Validar que no haya errores en la creación de los Value Objects
|
|
||||||
const okOrError = Result.combine([
|
|
||||||
uniqueIdResult,
|
|
||||||
usernameResult,
|
|
||||||
passwordHashResult,
|
|
||||||
emailResult,
|
|
||||||
]);
|
|
||||||
if (okOrError.isFailure) {
|
|
||||||
return Result.fail(okOrError.error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Crear el agregado de dominio
|
|
||||||
return AuthenticatedUser.create(
|
|
||||||
{
|
|
||||||
username: usernameResult.data!,
|
|
||||||
email: emailResult.data!,
|
|
||||||
hashPassword: passwordHashResult.data!,
|
|
||||||
roles: source.roles || [],
|
|
||||||
},
|
|
||||||
uniqueIdResult.data!
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public mapToPersistence(
|
|
||||||
source: AuthenticatedUser,
|
|
||||||
params?: MapperParamsType
|
|
||||||
): AuthUserCreationAttributes {
|
|
||||||
return {
|
|
||||||
id: source.id.toString(),
|
|
||||||
username: source.username.toString(),
|
|
||||||
email: source.email.toString(),
|
|
||||||
hash_password: source.hashPassword.toString(),
|
|
||||||
roles: source.getRoles().map((role) => role.toString()),
|
|
||||||
//access_token: source.accessToken,
|
|
||||||
//refresh_token: source.refreshToken,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const authenticatedUserMapper: IAuthenticatedUserMapper = new AuthenticatedUserMapper();
|
|
||||||
export { authenticatedUserMapper };
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export * from "./authenticated-user.mapper";
|
|
||||||
export * from "./tab-context.mapper";
|
|
||||||
export * from "./user.mapper";
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
import { UniqueID } from "@/core/common/domain";
|
|
||||||
import {
|
|
||||||
type ISequelizeMapper,
|
|
||||||
type MapperParamsType,
|
|
||||||
SequelizeMapper,
|
|
||||||
} from "@/core/common/infrastructure";
|
|
||||||
import { Result } from "@repo/rdx-utils";
|
|
||||||
import { TabContext } from "../../domain";
|
|
||||||
import { TabContextCreationAttributes, TabContextModel } from "../sequelize";
|
|
||||||
|
|
||||||
export interface ITabContextMapper
|
|
||||||
extends ISequelizeMapper<TabContextModel, TabContextCreationAttributes, TabContext> {}
|
|
||||||
|
|
||||||
export class TabContextMapper
|
|
||||||
extends SequelizeMapper<TabContextModel, TabContextCreationAttributes, TabContext>
|
|
||||||
implements ITabContextMapper
|
|
||||||
{
|
|
||||||
public mapToDomain(
|
|
||||||
source: TabContextModel,
|
|
||||||
params?: MapperParamsType
|
|
||||||
): Result<TabContext, Error> {
|
|
||||||
// Crear Value Objects asegurando que sean válidos
|
|
||||||
const uniqueIdResult = UniqueID.create(source.id);
|
|
||||||
const tabIdResult = UniqueID.create(source.tab_id);
|
|
||||||
const userIdResult = UniqueID.create(source.user_id);
|
|
||||||
//const companyIdResult = UniqueID.create(entity.company_id, false);
|
|
||||||
//const brachIdResult = UniqueID.create(entity.branch_id, false);
|
|
||||||
|
|
||||||
// Validar que no haya errores en la creación de los Value Objects
|
|
||||||
const okOrError = Result.combine([
|
|
||||||
uniqueIdResult,
|
|
||||||
tabIdResult,
|
|
||||||
userIdResult,
|
|
||||||
//companyIdResult,
|
|
||||||
//brachIdResult,
|
|
||||||
]);
|
|
||||||
if (okOrError.isFailure) {
|
|
||||||
return Result.fail(okOrError.error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Crear el agregado de dominio
|
|
||||||
return TabContext.create(
|
|
||||||
{
|
|
||||||
tabId: tabIdResult.data!,
|
|
||||||
userId: userIdResult.data!,
|
|
||||||
//companyId: companyIdResult.data,
|
|
||||||
//branchId: brachIdResult.data,
|
|
||||||
},
|
|
||||||
uniqueIdResult.data!
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public mapToPersistence(
|
|
||||||
source: TabContext,
|
|
||||||
params?: MapperParamsType
|
|
||||||
): TabContextCreationAttributes {
|
|
||||||
return {
|
|
||||||
id: source.id.toString(),
|
|
||||||
tab_id: source.tabId.toString(),
|
|
||||||
user_id: source.userId.toString(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabContextMapper: ITabContextMapper = new TabContextMapper();
|
|
||||||
export { tabContextMapper };
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
import { EmailAddress, UniqueID } from "@/core/common/domain";
|
|
||||||
import {
|
|
||||||
type ISequelizeMapper,
|
|
||||||
type MapperParamsType,
|
|
||||||
SequelizeMapper,
|
|
||||||
} from "@/core/common/infrastructure/sequelize/sequelize-mapper";
|
|
||||||
import { Result } from "@repo/rdx-utils";
|
|
||||||
import { User, Username } from "../../domain";
|
|
||||||
import { UserCreationAttributes, UserModel } from "../sequelize";
|
|
||||||
|
|
||||||
export interface IUserMapper extends ISequelizeMapper<UserModel, UserCreationAttributes, User> {}
|
|
||||||
|
|
||||||
class UserMapper
|
|
||||||
extends SequelizeMapper<UserModel, UserCreationAttributes, User>
|
|
||||||
implements IUserMapper
|
|
||||||
{
|
|
||||||
public mapToDomain(source: UserModel, params?: MapperParamsType): Result<User, Error> {
|
|
||||||
// Crear Value Objects asegurando que sean válidos
|
|
||||||
const uniqueIdResult = UniqueID.create(source.id);
|
|
||||||
const usernameResult = Username.create(source.username);
|
|
||||||
const emailResult = EmailAddress.create(source.email);
|
|
||||||
|
|
||||||
// Validar que no haya errores en la creación de los Value Objects
|
|
||||||
const okOrError = Result.combine([uniqueIdResult, usernameResult, emailResult]);
|
|
||||||
if (okOrError.isFailure) {
|
|
||||||
return Result.fail(okOrError.error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Crear el agregado de dominio
|
|
||||||
return User.create(
|
|
||||||
{
|
|
||||||
username: usernameResult.data!,
|
|
||||||
email: emailResult.data!,
|
|
||||||
roles: [],
|
|
||||||
//roles: entity.roles || [],
|
|
||||||
},
|
|
||||||
uniqueIdResult.data!
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public mapToPersistence(source: User, params?: MapperParamsType): UserCreationAttributes {
|
|
||||||
return {
|
|
||||||
id: source.id.toString(),
|
|
||||||
username: source.username.toString(),
|
|
||||||
email: source.email.toString(),
|
|
||||||
//roles: source.getRoles().map((role) => role.toString()),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const userMapper: IUserMapper = new UserMapper();
|
|
||||||
export { userMapper };
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export * from "./passport-auth.middleware";
|
|
||||||
export * from "./tab-context.middleware";
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
//import { authProvider } from "@/contexts/auth/infraestructure";
|
|
||||||
import type { NextFunction, Response } from "express";
|
|
||||||
|
|
||||||
import { UniqueID } from "@/core/common/domain";
|
|
||||||
import { ApiError, ExpressController } from "@/core/common/presentation";
|
|
||||||
|
|
||||||
import { authProvider } from "../../../../../../../modules/auth/src/api/lib/passport";
|
|
||||||
import type { AuthenticatedUser } from "../../domain";
|
|
||||||
import type { AuthenticatedRequest } from "../express/types";
|
|
||||||
|
|
||||||
// Comprueba el rol del usuario
|
|
||||||
const _authorizeUser = (condition: (user: AuthenticatedUser) => boolean) => {
|
|
||||||
return (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
|
||||||
const user = req.user as AuthenticatedUser;
|
|
||||||
|
|
||||||
if (!(user && condition(user))) {
|
|
||||||
return ExpressController.errorResponse(
|
|
||||||
new ApiError({
|
|
||||||
status: 401,
|
|
||||||
title: "Unauthorized",
|
|
||||||
detail: "You are not authorized to access this resource.",
|
|
||||||
}),
|
|
||||||
res
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return next();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Verifica que el usuario esté autenticado
|
|
||||||
export const checkUser = [
|
|
||||||
authProvider.authenticateJWT(),
|
|
||||||
_authorizeUser((user) => true /*user.isUser*/),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Verifica que el usuario sea administrador
|
|
||||||
export const checkUserIsAdmin = [
|
|
||||||
authProvider.authenticateJWT(),
|
|
||||||
_authorizeUser((user) => true /*user.isAdmin*/),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Middleware para verificar que el usuario sea administrador o el dueño de los datos (self)
|
|
||||||
export const checkUserIsAdminOrOwner = [
|
|
||||||
(req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
|
||||||
const user = req.user as AuthenticatedUser;
|
|
||||||
const { userId } = req.params;
|
|
||||||
|
|
||||||
// Si el usuario es admin, está autorizado
|
|
||||||
if (user.isAdmin) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si el usuario es sí mismo
|
|
||||||
if (user.isUser && userId) {
|
|
||||||
const paramIdOrError = UniqueID.create(userId);
|
|
||||||
if (paramIdOrError.isSuccess && user.id.equals(paramIdOrError.data)) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ExpressController.errorResponse(
|
|
||||||
new ApiError({
|
|
||||||
status: 401,
|
|
||||||
title: "Unauthorized",
|
|
||||||
detail: "You are not authorized to access this resource.",
|
|
||||||
}),
|
|
||||||
req,
|
|
||||||
res
|
|
||||||
);
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import { authProvider } from "../../../../../../../modules/auth/src/api/lib/passport";
|
|
||||||
|
|
||||||
export const checkTabContext = [authProvider.authenticateTabId()];
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
import { DataTypes, InferAttributes, InferCreationAttributes, Model, Sequelize } from "sequelize";
|
|
||||||
|
|
||||||
export type AuthUserCreationAttributes = InferCreationAttributes<AuthUserModel>;
|
|
||||||
|
|
||||||
export class AuthUserModel extends Model<
|
|
||||||
InferAttributes<AuthUserModel>,
|
|
||||||
InferCreationAttributes<AuthUserModel>
|
|
||||||
> {
|
|
||||||
// To avoid table creation
|
|
||||||
/*static async sync(): Promise<any> {
|
|
||||||
return Promise.resolve();
|
|
||||||
}*/
|
|
||||||
|
|
||||||
declare id: string;
|
|
||||||
declare username: string;
|
|
||||||
declare email: string;
|
|
||||||
declare hash_password: string;
|
|
||||||
declare roles: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default (sequelize: Sequelize) => {
|
|
||||||
AuthUserModel.init(
|
|
||||||
{
|
|
||||||
id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
primaryKey: true,
|
|
||||||
},
|
|
||||||
username: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
hash_password: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
roles: {
|
|
||||||
type: DataTypes.STRING,
|
|
||||||
allowNull: false,
|
|
||||||
defaultValue: "USER",
|
|
||||||
get(this: AuthUserModel): string[] {
|
|
||||||
const rawValue = this.getDataValue("roles") as any;
|
|
||||||
return String(rawValue).split(";");
|
|
||||||
},
|
|
||||||
set(this: AuthUserModel, value: string[]) {
|
|
||||||
const rawValue = value.join(";") as any;
|
|
||||||
this.setDataValue("roles", rawValue);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
tableName: "users",
|
|
||||||
paranoid: true, // softs deletes
|
|
||||||
timestamps: true,
|
|
||||||
|
|
||||||
createdAt: "created_at",
|
|
||||||
updatedAt: "updated_at",
|
|
||||||
deletedAt: "deleted_at",
|
|
||||||
|
|
||||||
indexes: [{ name: "email_idx", fields: ["email"], unique: true }],
|
|
||||||
|
|
||||||
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
|
|
||||||
|
|
||||||
defaultScope: {},
|
|
||||||
|
|
||||||
scopes: {},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return AuthUserModel;
|
|
||||||
};
|
|
||||||
@ -1,86 +0,0 @@
|
|||||||
import { EmailAddress } from "@/core/common/domain";
|
|
||||||
import { SequelizeRepository } from "@/core/common/infrastructure";
|
|
||||||
import { Result } from "@repo/rdx-utils";
|
|
||||||
import { Sequelize, Transaction } from "sequelize";
|
|
||||||
import { AuthenticatedUser, IAuthenticatedUserRepository, Username } from "../../domain";
|
|
||||||
import { IAuthenticatedUserMapper } from "../mappers";
|
|
||||||
import { AuthUserModel } from "./auth-user.model";
|
|
||||||
|
|
||||||
export class AuthenticatedUserRepository
|
|
||||||
extends SequelizeRepository<AuthenticatedUser>
|
|
||||||
implements IAuthenticatedUserRepository
|
|
||||||
{
|
|
||||||
private readonly _mapper!: IAuthenticatedUserMapper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 🔹 Función personalizada para mapear errores de unicidad en autenticación
|
|
||||||
*/
|
|
||||||
private _customErrorMapper(error: Error): string | null {
|
|
||||||
if (error.name === "SequelizeUniqueConstraintError") {
|
|
||||||
return "User with this email or username already exists";
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(database: Sequelize, mapper: IAuthenticatedUserMapper) {
|
|
||||||
super(database);
|
|
||||||
this._mapper = mapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
async userExists(
|
|
||||||
username: Username,
|
|
||||||
email: EmailAddress,
|
|
||||||
transaction?: Transaction
|
|
||||||
): Promise<Result<boolean, Error>> {
|
|
||||||
try {
|
|
||||||
const userWithEmail = await this._findById(
|
|
||||||
AuthUserModel,
|
|
||||||
"email",
|
|
||||||
email.toString(),
|
|
||||||
transaction
|
|
||||||
);
|
|
||||||
|
|
||||||
const userWithUsername = await this._findById(
|
|
||||||
AuthUserModel,
|
|
||||||
"username",
|
|
||||||
username.toString(),
|
|
||||||
transaction
|
|
||||||
);
|
|
||||||
|
|
||||||
return Result.ok(Boolean(userWithEmail || userWithUsername));
|
|
||||||
} catch (error: unknown) {
|
|
||||||
return this._handleDatabaseError(error, this._customErrorMapper);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUserByEmail(
|
|
||||||
email: EmailAddress,
|
|
||||||
transaction?: Transaction
|
|
||||||
): Promise<Result<AuthenticatedUser, Error>> {
|
|
||||||
try {
|
|
||||||
const rawUser = await this._getBy(AuthUserModel, "email", email.toString(), {}, transaction);
|
|
||||||
|
|
||||||
if (!rawUser === true) {
|
|
||||||
return Result.fail(new Error("User with email not exists"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._mapper.mapToDomain(rawUser);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
return this._handleDatabaseError(error, this._customErrorMapper);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async createUser(
|
|
||||||
user: AuthenticatedUser,
|
|
||||||
transaction?: Transaction
|
|
||||||
): Promise<Result<void, Error>> {
|
|
||||||
try {
|
|
||||||
const persistenceData = this._mapper.mapToPersistence(user);
|
|
||||||
await AuthUserModel.create(persistenceData, { transaction });
|
|
||||||
return Result.ok();
|
|
||||||
} catch (error: unknown) {
|
|
||||||
return this._handleDatabaseError(error, this._customErrorMapper);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
export * from "./auth-user.model";
|
|
||||||
export * from "./authenticated-user.repository";
|
|
||||||
|
|
||||||
export * from "./tab-context.model";
|
|
||||||
export * from "./tab-context.repository";
|
|
||||||
|
|
||||||
export * from "./user.model";
|
|
||||||
export * from "./user.repository";
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
import { DataTypes, InferAttributes, InferCreationAttributes, Model, Sequelize } from "sequelize";
|
|
||||||
|
|
||||||
export type TabContextCreationAttributes = InferCreationAttributes<TabContextModel>;
|
|
||||||
|
|
||||||
export class TabContextModel extends Model<
|
|
||||||
InferAttributes<TabContextModel>,
|
|
||||||
InferCreationAttributes<TabContextModel>
|
|
||||||
> {
|
|
||||||
// To avoid table creation
|
|
||||||
/*static async sync(): Promise<any> {
|
|
||||||
return Promise.resolve();
|
|
||||||
}*/
|
|
||||||
static associate(connection: Sequelize) {
|
|
||||||
const { AuthUserModel } = connection.models;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare id: string;
|
|
||||||
declare tab_id: string;
|
|
||||||
declare user_id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default (sequelize: Sequelize) => {
|
|
||||||
TabContextModel.init(
|
|
||||||
{
|
|
||||||
id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
primaryKey: true,
|
|
||||||
},
|
|
||||||
user_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
tab_id: {
|
|
||||||
type: DataTypes.UUID,
|
|
||||||
allowNull: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
sequelize,
|
|
||||||
tableName: "user_tab_contexts",
|
|
||||||
paranoid: true, // softs deletes
|
|
||||||
timestamps: true,
|
|
||||||
|
|
||||||
createdAt: "created_at",
|
|
||||||
updatedAt: "updated_at",
|
|
||||||
deletedAt: "deleted_at",
|
|
||||||
|
|
||||||
indexes: [{ name: "tab_id_idx", fields: ["tab_id"], unique: true }],
|
|
||||||
|
|
||||||
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
|
|
||||||
|
|
||||||
defaultScope: {},
|
|
||||||
|
|
||||||
scopes: {},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return TabContextModel;
|
|
||||||
};
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user