This commit is contained in:
David Arranz 2026-05-04 20:15:30 +02:00
parent c91455ffc2
commit 4beb7aa207
165 changed files with 0 additions and 6962 deletions

View File

@ -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);
}
}

View File

@ -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);
}
});
}
}

View File

@ -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";

View File

@ -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);
}
});
}
}

View File

@ -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");
});
});

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -1 +0,0 @@
export * from "./account";

View File

@ -1,4 +0,0 @@
export * from "./aggregates";
export * from "./repositories";
export * from "./services";
export * from "./value-objects";

View File

@ -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>;
}

View File

@ -1 +0,0 @@
export * from "./account-repository.interface";

View File

@ -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");
});
});

View File

@ -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>>;
}

View File

@ -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");
});
});

View File

@ -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"));
}
}

View File

@ -1,2 +0,0 @@
export * from "./account-service.interface";
export * from "./account.service";

View File

@ -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);
});
});
});

View File

@ -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();
}
}

View File

@ -1 +0,0 @@
export * from "./account-status";

View File

@ -1,2 +0,0 @@
export * from "./mappers";
export * from "./sequelize";

View File

@ -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 };

View File

@ -1 +0,0 @@
export * from "./account.mapper";

View File

@ -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;
};

View File

@ -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 };

View File

@ -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;
};

View File

@ -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);
}
}

View File

@ -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()),
}),
};

View File

@ -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);
};

View File

@ -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);
}
}

View File

@ -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()),
}),
};

View File

@ -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);
};

View File

@ -1,4 +0,0 @@
export * from "./create-account";
export * from "./get-account";
export * from "./list-accounts";
export * from "./update-account";

View File

@ -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);
};

View File

@ -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);
}
}

View File

@ -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()),
})),
};

View File

@ -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);
};

View File

@ -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);
}
}

View File

@ -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()),
}),
};

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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({});

View File

@ -1,3 +0,0 @@
export * from "./accounts.request.dto";
export * from "./accounts.response.dto";
export * from "./accounts.schemas";

View File

@ -1,2 +0,0 @@
export * from "./controllers";
export * from "./dto";

View File

@ -1,5 +0,0 @@
export * from "./list-users";
export * from "./login";
export * from "./logout";
export * from "./refresh-token";
export * from "./register";

View File

@ -1 +0,0 @@
export * from "./list-users.use-case";

View File

@ -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);
});
}
}

View File

@ -1 +0,0 @@
export * from "./login.use-case";

View File

@ -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);
});
}
}

View File

@ -1 +0,0 @@
export * from "./logout.use-case";

View File

@ -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);
});
}
}

View File

@ -1 +0,0 @@
export * from "./refresh-token.use-case";

View File

@ -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,
});
});
}
}

View File

@ -1 +0,0 @@
export * from "./register.use-case";

View File

@ -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);
}
});
}
}

View File

@ -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;
}
}

View File

@ -1,4 +0,0 @@
export * from "./authenticated-user";
export * from "./role";
export * from "./user";

View File

@ -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 {}
}

View File

@ -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;
}
}

View File

@ -1,5 +0,0 @@
export * from "./jwt-payload";
export * from "./login-data";
export * from "./logout-data";
export * from "./register-data";
export * from "./tab-context";

View File

@ -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(),
};
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -1 +0,0 @@
export * from "./user-authenticated.event";

View File

@ -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();
}
}

View File

@ -1,5 +0,0 @@
export * from "./aggregates";
export * from "./entities";
export * from "./events";
export * from "./repositories";
export * from "./value-objects";

View File

@ -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>>;
}

View File

@ -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";

View File

@ -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>>;
}

View File

@ -1 +0,0 @@
export type IUserPermissionRepository = {}

View File

@ -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>>;
}

View File

@ -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>>;
}

View File

@ -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);
}
}

View File

@ -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";

View File

@ -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>>;
}

View File

@ -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);
}
}

View File

@ -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>>;
}

View File

@ -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);
}*/
}

View File

@ -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;
}
}

View File

@ -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);
});
});

View File

@ -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;
}
}

View File

@ -1,5 +0,0 @@
export * from "./auth-user-roles";
export * from "./hash-password";
export * from "./plain-password";
export * from "./token";
export * from "./username";

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -1,4 +0,0 @@
export * from "../../../../../../modules/auth/src/api/lib/passport";
export * from "./mappers";
export * from "./middleware";
export * from "./sequelize";

View File

@ -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 };

View File

@ -1,3 +0,0 @@
export * from "./authenticated-user.mapper";
export * from "./tab-context.mapper";
export * from "./user.mapper";

View File

@ -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 };

View File

@ -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 };

View File

@ -1,2 +0,0 @@
export * from "./passport-auth.middleware";
export * from "./tab-context.middleware";

View File

@ -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
);
},
];

View File

@ -1,3 +0,0 @@
import { authProvider } from "../../../../../../../modules/auth/src/api/lib/passport";
export const checkTabContext = [authProvider.authenticateTabId()];

View File

@ -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;
};

View File

@ -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);
}
}
}

View File

@ -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";

View File

@ -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