.
This commit is contained in:
parent
7d1c441d34
commit
5a9e7261f9
1
apps/server/src/contexts/accounts/application/index.ts
Normal file
1
apps/server/src/contexts/accounts/application/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./list-accounts";
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./list-accounts.use-case";
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
import { Collection, Result } from "@common/helpers";
|
||||||
|
import { ITransactionManager } from "@common/infrastructure/database";
|
||||||
|
import { Account } from "@contexts/accounts/domain";
|
||||||
|
import { IAccountService } from "@contexts/accounts/domain/services/account-service.interface";
|
||||||
|
|
||||||
|
export class ListAccountsUseCase {
|
||||||
|
constructor(
|
||||||
|
private readonly accountService: IAccountService,
|
||||||
|
private readonly transactionManager: ITransactionManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public execute(): Promise<Result<Collection<Account>, Error>> {
|
||||||
|
return this.transactionManager.complete((transaction) => {
|
||||||
|
return this.accountService.findAccounts(transaction);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,7 +8,7 @@ import {
|
|||||||
} from "@common/domain";
|
} from "@common/domain";
|
||||||
import { Maybe, Result } from "@common/helpers";
|
import { Maybe, Result } from "@common/helpers";
|
||||||
|
|
||||||
export interface ICompanyProps {
|
export interface IAccountProps {
|
||||||
isFreelancer: boolean;
|
isFreelancer: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
tin: TINNumber;
|
tin: TINNumber;
|
||||||
@ -27,7 +27,7 @@ export interface ICompanyProps {
|
|||||||
logo: Maybe<string>;
|
logo: Maybe<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICompany {
|
export interface IAccount {
|
||||||
id: UniqueID;
|
id: UniqueID;
|
||||||
name: string;
|
name: string;
|
||||||
tin: TINNumber;
|
tin: TINNumber;
|
||||||
@ -44,24 +44,24 @@ export interface ICompany {
|
|||||||
website: Maybe<string>;
|
website: Maybe<string>;
|
||||||
logo: Maybe<string>;
|
logo: Maybe<string>;
|
||||||
|
|
||||||
isCompany: boolean;
|
isAccount: boolean;
|
||||||
isFreelancer: boolean;
|
isFreelancer: boolean;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Company extends AggregateRoot<ICompanyProps> implements ICompany {
|
export class Account extends AggregateRoot<IAccountProps> implements IAccount {
|
||||||
static create(props: ICompanyProps, id?: UniqueID): Result<Company, Error> {
|
static create(props: IAccountProps, id?: UniqueID): Result<Account, Error> {
|
||||||
const company = new Company(props, id);
|
const account = new Account(props, id);
|
||||||
|
|
||||||
// Reglas de negocio / validaciones
|
// Reglas de negocio / validaciones
|
||||||
// ...
|
// ...
|
||||||
// ...
|
// ...
|
||||||
|
|
||||||
// 🔹 Disparar evento de dominio "CompanyAuthenticatedEvent"
|
// 🔹 Disparar evento de dominio "AccountAuthenticatedEvent"
|
||||||
//const { company } = props;
|
//const { account } = props;
|
||||||
//user.addDomainEvent(new CompanyAuthenticatedEvent(id, company.toString()));
|
//user.addDomainEvent(new AccountAuthenticatedEvent(id, account.toString()));
|
||||||
|
|
||||||
return Result.ok(company);
|
return Result.ok(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
@ -116,7 +116,7 @@ export class Company extends AggregateRoot<ICompanyProps> implements ICompany {
|
|||||||
return this.props.logo;
|
return this.props.logo;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isCompany(): boolean {
|
get isAccount(): boolean {
|
||||||
return !this.props.isFreelancer;
|
return !this.props.isFreelancer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./account";
|
||||||
3
apps/server/src/contexts/accounts/domain/index.ts
Normal file
3
apps/server/src/contexts/accounts/domain/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./aggregates";
|
||||||
|
|
||||||
|
export * from "./repositories";
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { EmailAddress, UniqueID } from "@common/domain";
|
||||||
|
import { Collection, Result } from "@common/helpers";
|
||||||
|
import { Account } from "../aggregates";
|
||||||
|
|
||||||
|
export interface IAccountRepository {
|
||||||
|
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>>;
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./account-repository.interface";
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
import { UniqueID } from "@common/domain";
|
||||||
|
import { Collection, Result } from "@common/helpers";
|
||||||
|
import { Account } from "../aggregates";
|
||||||
|
|
||||||
|
export interface IAccountService {
|
||||||
|
findAccounts(transaction?: any): Promise<Result<Collection<Account>, Error>>;
|
||||||
|
findAccountById(userId: UniqueID, transaction?: any): Promise<Result<Account>>;
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
import { UniqueID } from "@common/domain";
|
||||||
|
import { Collection, Result } from "@common/helpers";
|
||||||
|
import { Account, IAccountRepository } from "..";
|
||||||
|
import { IAccountService } from "./account-service.interface";
|
||||||
|
|
||||||
|
export class AccountService implements IAccountService {
|
||||||
|
constructor(private readonly accountRepository: IAccountRepository) {}
|
||||||
|
|
||||||
|
async findAccounts(transaction?: any): Promise<Result<Collection<Account>, Error>> {
|
||||||
|
const accountsOrError = await this.accountRepository.findAll(transaction);
|
||||||
|
if (accountsOrError.isFailure) {
|
||||||
|
return Result.fail(accountsOrError.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Solo devolver usuarios activos
|
||||||
|
const activeAccounts = accountsOrError.data.filter((account) => account.isActive);
|
||||||
|
return Result.ok(new Collection(activeAccounts));
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAccountById(accountId: UniqueID, transaction?: any): Promise<Result<Account>> {
|
||||||
|
return await this.accountRepository.findById(accountId, transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,17 +5,17 @@ import {
|
|||||||
MapperParamsType,
|
MapperParamsType,
|
||||||
SequelizeMapper,
|
SequelizeMapper,
|
||||||
} from "@common/infrastructure/sequelize/sequelize-mapper";
|
} from "@common/infrastructure/sequelize/sequelize-mapper";
|
||||||
import { Company } from "@contexts/companies/domain/aggregates/company";
|
import { Account } from "@contexts/accounts/domain/";
|
||||||
import { CompanyCreationAttributes, CompanyModel } from "../sequelize/company.model";
|
import { AccountCreationAttributes, AccountModel } from "../sequelize/account.model";
|
||||||
|
|
||||||
export interface ICompanyMapper
|
export interface IAccountMapper
|
||||||
extends ISequelizeMapper<CompanyModel, CompanyCreationAttributes, Company> {}
|
extends ISequelizeMapper<AccountModel, AccountCreationAttributes, Account> {}
|
||||||
|
|
||||||
export class CompanyMapper
|
export class AccountMapper
|
||||||
extends SequelizeMapper<CompanyModel, CompanyCreationAttributes, Company>
|
extends SequelizeMapper<AccountModel, AccountCreationAttributes, Account>
|
||||||
implements ICompanyMapper
|
implements IAccountMapper
|
||||||
{
|
{
|
||||||
public mapToDomain(source: CompanyModel, params?: MapperParamsType): Result<Company, Error> {
|
public mapToDomain(source: AccountModel, params?: MapperParamsType): Result<Account, Error> {
|
||||||
const idOrError = UniqueID.create(source.id);
|
const idOrError = UniqueID.create(source.id);
|
||||||
const tinOrError = TINNumber.create(source.tin);
|
const tinOrError = TINNumber.create(source.tin);
|
||||||
const emailOrError = EmailAddress.create(source.email);
|
const emailOrError = EmailAddress.create(source.email);
|
||||||
@ -42,7 +42,7 @@ export class CompanyMapper
|
|||||||
return Result.fail(result.error);
|
return Result.fail(result.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Company.create(
|
return Account.create(
|
||||||
{
|
{
|
||||||
isFreelancer: source.is_freelancer,
|
isFreelancer: source.is_freelancer,
|
||||||
name: source.name,
|
name: source.name,
|
||||||
@ -65,9 +65,9 @@ export class CompanyMapper
|
|||||||
}
|
}
|
||||||
|
|
||||||
public mapToPersistence(
|
public mapToPersistence(
|
||||||
source: Company,
|
source: Account,
|
||||||
params?: MapperParamsType
|
params?: MapperParamsType
|
||||||
): Result<CompanyCreationAttributes, Error> {
|
): Result<AccountCreationAttributes, Error> {
|
||||||
return Result.ok({
|
return Result.ok({
|
||||||
id: source.id.toString(),
|
id: source.id.toString(),
|
||||||
is_freelancer: source.isFreelancer,
|
is_freelancer: source.isFreelancer,
|
||||||
@ -96,5 +96,5 @@ export class CompanyMapper
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const companyMapper: CompanyMapper = new CompanyMapper();
|
const accountMapper: AccountMapper = new AccountMapper();
|
||||||
export { companyMapper };
|
export { accountMapper };
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./account.mapper";
|
||||||
@ -7,11 +7,11 @@ import {
|
|||||||
Sequelize,
|
Sequelize,
|
||||||
} from "sequelize";
|
} from "sequelize";
|
||||||
|
|
||||||
export type CompanyCreationAttributes = InferCreationAttributes<CompanyModel, {}> & {};
|
export type AccountCreationAttributes = InferCreationAttributes<AccountModel, {}> & {};
|
||||||
|
|
||||||
export class CompanyModel extends Model<
|
export class AccountModel extends Model<
|
||||||
InferAttributes<CompanyModel>,
|
InferAttributes<AccountModel>,
|
||||||
InferCreationAttributes<CompanyModel>
|
InferCreationAttributes<AccountModel>
|
||||||
> {
|
> {
|
||||||
// To avoid table creation
|
// To avoid table creation
|
||||||
/*static async sync(): Promise<any> {
|
/*static async sync(): Promise<any> {
|
||||||
@ -46,7 +46,7 @@ export class CompanyModel extends Model<
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default (sequelize: Sequelize) => {
|
export default (sequelize: Sequelize) => {
|
||||||
CompanyModel.init(
|
AccountModel.init(
|
||||||
{
|
{
|
||||||
id: {
|
id: {
|
||||||
type: DataTypes.UUID,
|
type: DataTypes.UUID,
|
||||||
@ -148,7 +148,7 @@ export default (sequelize: Sequelize) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
sequelize,
|
sequelize,
|
||||||
tableName: "companies",
|
tableName: "accounts",
|
||||||
|
|
||||||
paranoid: true, // softs deletes
|
paranoid: true, // softs deletes
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
@ -166,5 +166,5 @@ export default (sequelize: Sequelize) => {
|
|||||||
scopes: {},
|
scopes: {},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return CompanyModel;
|
return AccountModel;
|
||||||
};
|
};
|
||||||
@ -0,0 +1,82 @@
|
|||||||
|
import { EmailAddress, UniqueID } from "@common/domain";
|
||||||
|
import { Collection, Result } from "@common/helpers";
|
||||||
|
import { SequelizeRepository } from "@common/infrastructure";
|
||||||
|
import { Account } from "@contexts/accounts/domain";
|
||||||
|
import { IAccountRepository } from "@contexts/accounts/domain/repositories/company-repository.interface";
|
||||||
|
import { Transaction } from "sequelize";
|
||||||
|
import { accountMapper, IAccountMapper } 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 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountRepository = new AccountRepository(accountMapper);
|
||||||
|
export { accountRepository };
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
import { IAccountRepository } from "@contexts/accounts/domain/repositories/company-repository.interface";
|
||||||
|
import { accountRepository } from "./account.repository";
|
||||||
|
|
||||||
|
export * from "./account.model";
|
||||||
|
|
||||||
|
export const createAccountRepository = (): IAccountRepository => {
|
||||||
|
return accountRepository;
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./list-accounts";
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { SequelizeTransactionManager } from "@common/infrastructure";
|
||||||
|
import { ListAccountsUseCase } from "@contexts/accounts/application/list-accounts/list-accounts.use-case";
|
||||||
|
import { AccountService } from "@contexts/accounts/domain/services/company.service";
|
||||||
|
import { accountRepository } from "@contexts/accounts/infraestructure/sequelize/account.repository";
|
||||||
|
import { ListAccountsController } from "./list-accounts.controller";
|
||||||
|
import { listAccountsPresenter } from "./list-accounts.presenter";
|
||||||
|
|
||||||
|
export const listAccountsController = () => {
|
||||||
|
const transactionManager = new SequelizeTransactionManager();
|
||||||
|
const accountService = new AccountService(accountRepository);
|
||||||
|
|
||||||
|
const useCase = new ListAccountsUseCase(accountService, transactionManager);
|
||||||
|
const presenter = listAccountsPresenter;
|
||||||
|
|
||||||
|
return new ListAccountsController(useCase, presenter);
|
||||||
|
};
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
import { ExpressController } from "@common/presentation";
|
||||||
|
import { ListAccountsUseCase } from "@contexts/accounts/application/list-accounts/list-accounts.use-case";
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
import { Collection, ensureBoolean, ensureNumber, ensureString } from "@common/helpers";
|
||||||
|
import { Account } from "@contexts/accounts/domain";
|
||||||
|
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_freelancer: 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"),
|
||||||
|
lang_code: ensureString(account.langCode),
|
||||||
|
currency_code: ensureString(account.currencyCode),
|
||||||
|
logo: ensureString(account.logo.getOrUndefined()),
|
||||||
|
})),
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export interface IListAccountsRequestDTO {}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
export interface IListCompaniesResponseDTO {
|
export interface IListAccountsResponseDTO {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
is_freelancer: boolean;
|
is_freelancer: boolean;
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ListAccountsSchema = z.object({});
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./accounts.request.dto";
|
||||||
|
export * from "./accounts.response.dto";
|
||||||
|
export * from "./accounts.validation.dto";
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./list-companies";
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./list-companies.use-case";
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import { Collection, Result } from "@common/helpers";
|
|
||||||
import { ITransactionManager } from "@common/infrastructure/database";
|
|
||||||
import { Company } from "@contexts/companies/domain";
|
|
||||||
import { ICompanyService } from "@contexts/companies/domain/services/company-service.interface";
|
|
||||||
|
|
||||||
export class ListCompaniesUseCase {
|
|
||||||
constructor(
|
|
||||||
private readonly companyService: ICompanyService,
|
|
||||||
private readonly transactionManager: ITransactionManager
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public execute(): Promise<Result<Collection<Company>, Error>> {
|
|
||||||
return this.transactionManager.complete((transaction) => {
|
|
||||||
return this.companyService.findCompanies(transaction);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./company";
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
export * from "./aggregates";
|
|
||||||
export * from "./entities";
|
|
||||||
export * from "./events";
|
|
||||||
export * from "./repositories";
|
|
||||||
export * from "./value-objects";
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
import { EmailAddress, UniqueID } from "@common/domain";
|
|
||||||
import { Collection, Result } from "@common/helpers";
|
|
||||||
import { Company } from "../aggregates";
|
|
||||||
|
|
||||||
export interface ICompanyRepository {
|
|
||||||
findAll(transaction?: any): Promise<Result<Collection<Company>, Error>>;
|
|
||||||
findById(id: UniqueID, transaction?: any): Promise<Result<Company, Error>>;
|
|
||||||
findByEmail(email: EmailAddress, transaction?: any): Promise<Result<Company, Error>>;
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./company-repository.interface";
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import { UniqueID } from "@common/domain";
|
|
||||||
import { Collection, Result } from "@common/helpers";
|
|
||||||
import { Company } from "../aggregates";
|
|
||||||
|
|
||||||
export interface ICompanyService {
|
|
||||||
findCompanies(transaction?: any): Promise<Result<Collection<Company>, Error>>;
|
|
||||||
findCompanyById(userId: UniqueID, transaction?: any): Promise<Result<Company>>;
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
import { UniqueID } from "@common/domain";
|
|
||||||
import { Collection, Result } from "@common/helpers";
|
|
||||||
import { Company, ICompanyRepository } from "..";
|
|
||||||
import { ICompanyService } from "./company-service.interface";
|
|
||||||
|
|
||||||
export class CompanyService implements ICompanyService {
|
|
||||||
constructor(private readonly companyRepository: ICompanyRepository) {}
|
|
||||||
|
|
||||||
async findCompanies(transaction?: any): Promise<Result<Collection<Company>, Error>> {
|
|
||||||
const companysOrError = await this.companyRepository.findAll(transaction);
|
|
||||||
if (companysOrError.isFailure) {
|
|
||||||
return Result.fail(companysOrError.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Solo devolver usuarios activos
|
|
||||||
const activeCompanies = companysOrError.data.filter((company) => company.isActive);
|
|
||||||
return Result.ok(new Collection(activeCompanies));
|
|
||||||
}
|
|
||||||
|
|
||||||
async findCompanyById(companyId: UniqueID, transaction?: any): Promise<Result<Company>> {
|
|
||||||
return await this.companyRepository.findById(companyId, transaction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./company.mapper";
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
import { ICompanyRepository } from "@contexts/companies/domain";
|
|
||||||
import { companyRepository } from "./company.repository";
|
|
||||||
|
|
||||||
export * from "./company.model";
|
|
||||||
export * from "./company.repository";
|
|
||||||
|
|
||||||
export const createCompanyRepository = (): ICompanyRepository => {
|
|
||||||
return companyRepository;
|
|
||||||
};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export * from "./list-companies";
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import { SequelizeTransactionManager } from "@common/infrastructure";
|
|
||||||
import { ListCompaniesUseCase } from "@contexts/companies/application/list-companies/list-companies.use-case";
|
|
||||||
import { CompanyService } from "@contexts/companies/domain/services/company.service";
|
|
||||||
import { companyRepository } from "@contexts/companies/infraestructure";
|
|
||||||
import { ListCompaniesController } from "./list-companies.controller";
|
|
||||||
import { listCompaniesPresenter } from "./list-companies.presenter";
|
|
||||||
|
|
||||||
export const listCompaniesController = () => {
|
|
||||||
const transactionManager = new SequelizeTransactionManager();
|
|
||||||
const companyService = new CompanyService(companyRepository);
|
|
||||||
|
|
||||||
const useCase = new ListCompaniesUseCase(companyService, transactionManager);
|
|
||||||
const presenter = listCompaniesPresenter;
|
|
||||||
|
|
||||||
return new ListCompaniesController(useCase, presenter);
|
|
||||||
};
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
import { ExpressController } from "@common/presentation";
|
|
||||||
import { ListCompaniesUseCase } from "@contexts/companies/application/list-companies/list-companies.use-case";
|
|
||||||
import { IListCompaniesPresenter } from "./list-companies.presenter";
|
|
||||||
|
|
||||||
export class ListCompaniesController extends ExpressController {
|
|
||||||
public constructor(
|
|
||||||
private readonly listCompanies: ListCompaniesUseCase,
|
|
||||||
private readonly presenter: IListCompaniesPresenter
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async executeImpl() {
|
|
||||||
const companiesOrError = await this.listCompanies.execute();
|
|
||||||
|
|
||||||
if (companiesOrError.isFailure) {
|
|
||||||
return this.handleError(companiesOrError.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.ok(this.presenter.toDTO(companiesOrError.data));
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleError(error: Error) {
|
|
||||||
const message = error.message;
|
|
||||||
|
|
||||||
if (
|
|
||||||
message.includes("Database connection lost") ||
|
|
||||||
message.includes("Database request timed out")
|
|
||||||
) {
|
|
||||||
return this.unavailableError(
|
|
||||||
"Database service is currently unavailable. Please try again later."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.conflictError(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
import { Collection, ensureBoolean, ensureNumber, ensureString } from "@common/helpers";
|
|
||||||
import { Company } from "@contexts/companies/domain";
|
|
||||||
import { IListCompaniesResponseDTO } from "../../dto";
|
|
||||||
|
|
||||||
export interface IListCompaniesPresenter {
|
|
||||||
toDTO: (companies: Collection<Company>) => IListCompaniesResponseDTO[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const listCompaniesPresenter: IListCompaniesPresenter = {
|
|
||||||
toDTO: (companies: Collection<Company>): IListCompaniesResponseDTO[] =>
|
|
||||||
companies.map((company) => ({
|
|
||||||
id: ensureString(company.id.toString()),
|
|
||||||
|
|
||||||
is_freelancer: ensureBoolean(company.isFreelancer),
|
|
||||||
name: ensureString(company.name),
|
|
||||||
trade_name: ensureString(company.tradeName.getOrUndefined()),
|
|
||||||
tin: ensureString(company.tin.toString()),
|
|
||||||
|
|
||||||
street: ensureString(company.address.street),
|
|
||||||
city: ensureString(company.address.city),
|
|
||||||
state: ensureString(company.address.state),
|
|
||||||
postal_code: ensureString(company.address.postalCode),
|
|
||||||
country: ensureString(company.address.country),
|
|
||||||
|
|
||||||
email: ensureString(company.email.toString()),
|
|
||||||
phone: ensureString(company.phone.toString()),
|
|
||||||
fax: ensureString(company.fax.getOrUndefined()?.toString()),
|
|
||||||
website: ensureString(company.website.getOrUndefined()),
|
|
||||||
|
|
||||||
legal_record: ensureString(company.legalRecord),
|
|
||||||
|
|
||||||
default_tax: ensureNumber(company.defaultTax),
|
|
||||||
status: ensureString(company.isActive ? "active" : "inactive"),
|
|
||||||
lang_code: ensureString(company.langCode),
|
|
||||||
currency_code: ensureString(company.currencyCode),
|
|
||||||
logo: ensureString(company.logo.getOrUndefined()),
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export interface IListCompaniesRequestDTO {}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
export const ListCompaniesSchema = z.object({});
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export * from "./companies.request.dto";
|
|
||||||
export * from "./companies.response.dto";
|
|
||||||
export * from "./companies.validation.dto";
|
|
||||||
1
apps/server/src/contexts/contacts/application/index.ts
Normal file
1
apps/server/src/contexts/contacts/application/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./list-contacts.use-case";
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { Collection, Result } from "@common/helpers";
|
||||||
|
import { ITransactionManager } from "@common/infrastructure/database";
|
||||||
|
import { Contact, IContactService } from "../domain";
|
||||||
|
|
||||||
|
export class ListContactsUseCase {
|
||||||
|
constructor(
|
||||||
|
private readonly contactService: IContactService,
|
||||||
|
private readonly transactionManager: ITransactionManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public execute(): Promise<Result<Collection<Contact>, Error>> {
|
||||||
|
return this.transactionManager.complete((transaction) => {
|
||||||
|
return this.contactService.findContact(transaction);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
130
apps/server/src/contexts/contacts/domain/aggregates/contact.ts
Normal file
130
apps/server/src/contexts/contacts/domain/aggregates/contact.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import {
|
||||||
|
AggregateRoot,
|
||||||
|
EmailAddress,
|
||||||
|
PhoneNumber,
|
||||||
|
PostalAddress,
|
||||||
|
TINNumber,
|
||||||
|
UniqueID,
|
||||||
|
} from "@common/domain";
|
||||||
|
import { Maybe, Result } from "@common/helpers";
|
||||||
|
|
||||||
|
export interface IContactProps {
|
||||||
|
reference: string;
|
||||||
|
isFreelancer: boolean;
|
||||||
|
name: string;
|
||||||
|
tin: TINNumber;
|
||||||
|
address: PostalAddress;
|
||||||
|
email: EmailAddress;
|
||||||
|
phone: PhoneNumber;
|
||||||
|
legalRecord: string;
|
||||||
|
defaultTax: number;
|
||||||
|
status: string;
|
||||||
|
langCode: string;
|
||||||
|
currencyCode: string;
|
||||||
|
|
||||||
|
tradeName: Maybe<string>;
|
||||||
|
website: Maybe<string>;
|
||||||
|
fax: Maybe<PhoneNumber>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IContact {
|
||||||
|
id: UniqueID;
|
||||||
|
reference: string;
|
||||||
|
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>;
|
||||||
|
|
||||||
|
isContact: boolean;
|
||||||
|
isFreelancer: boolean;
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Contact extends AggregateRoot<IContactProps> implements IContact {
|
||||||
|
static create(props: IContactProps, id?: UniqueID): Result<Contact, Error> {
|
||||||
|
const contact = new Contact(props, id);
|
||||||
|
|
||||||
|
// Reglas de negocio / validaciones
|
||||||
|
// ...
|
||||||
|
// ...
|
||||||
|
|
||||||
|
// 🔹 Disparar evento de dominio "ContactAuthenticatedEvent"
|
||||||
|
//const { contact } = props;
|
||||||
|
//user.addDomainEvent(new ContactAuthenticatedEvent(id, contact.toString()));
|
||||||
|
|
||||||
|
return Result.ok(contact);
|
||||||
|
}
|
||||||
|
|
||||||
|
get reference() {
|
||||||
|
return this.props.reference;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 isContact(): boolean {
|
||||||
|
return !this.props.isFreelancer;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isFreelancer(): boolean {
|
||||||
|
return this.props.isFreelancer;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isActive(): boolean {
|
||||||
|
return this.props.status === "active";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./contact";
|
||||||
3
apps/server/src/contexts/contacts/domain/index.ts
Normal file
3
apps/server/src/contexts/contacts/domain/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./aggregates";
|
||||||
|
export * from "./repositories";
|
||||||
|
export * from "./services";
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { EmailAddress, UniqueID } from "@common/domain";
|
||||||
|
import { Collection, Result } from "@common/helpers";
|
||||||
|
import { Contact } from "../aggregates";
|
||||||
|
|
||||||
|
export interface IContactRepository {
|
||||||
|
findAll(transaction?: any): Promise<Result<Collection<Contact>, Error>>;
|
||||||
|
findById(id: UniqueID, transaction?: any): Promise<Result<Contact, Error>>;
|
||||||
|
findByEmail(email: EmailAddress, transaction?: any): Promise<Result<Contact, Error>>;
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./contact-repository.interface";
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
import { UniqueID } from "@common/domain";
|
||||||
|
import { Collection, Result } from "@common/helpers";
|
||||||
|
import { Contact } from "../aggregates";
|
||||||
|
|
||||||
|
export interface IContactService {
|
||||||
|
findContact(transaction?: any): Promise<Result<Collection<Contact>, Error>>;
|
||||||
|
findContactById(contactId: UniqueID, transaction?: any): Promise<Result<Contact>>;
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
import { UniqueID } from "@common/domain";
|
||||||
|
import { Collection, Result } from "@common/helpers";
|
||||||
|
import { Contact } from "../aggregates";
|
||||||
|
import { IContactRepository } from "../repositories";
|
||||||
|
import { IContactService } from "./contact-service.interface";
|
||||||
|
|
||||||
|
export class ContactService implements IContactService {
|
||||||
|
constructor(private readonly contactRepository: IContactRepository) {}
|
||||||
|
|
||||||
|
async findContact(transaction?: any): Promise<Result<Collection<Contact>, Error>> {
|
||||||
|
const contactsOrError = await this.contactRepository.findAll(transaction);
|
||||||
|
if (contactsOrError.isFailure) {
|
||||||
|
return Result.fail(contactsOrError.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Solo devolver usuarios activos
|
||||||
|
const activeContacts = contactsOrError.data.filter((contact) => contact.isActive);
|
||||||
|
return Result.ok(new Collection(activeContacts));
|
||||||
|
}
|
||||||
|
|
||||||
|
async findContactById(contactId: UniqueID, transaction?: any): Promise<Result<Contact>> {
|
||||||
|
return await this.contactRepository.findById(contactId, transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./contact-service.interface";
|
||||||
|
export * from "./contact.service";
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./mappers";
|
||||||
|
export * from "./sequelize";
|
||||||
@ -0,0 +1,100 @@
|
|||||||
|
import { EmailAddress, PhoneNumber, PostalAddress, TINNumber, UniqueID } from "@common/domain";
|
||||||
|
import { Maybe, Result } from "@common/helpers";
|
||||||
|
import {
|
||||||
|
ISequelizeMapper,
|
||||||
|
MapperParamsType,
|
||||||
|
SequelizeMapper,
|
||||||
|
} from "@common/infrastructure/sequelize/sequelize-mapper";
|
||||||
|
import { Contact } from "../../domain";
|
||||||
|
import { ContactCreationAttributes, ContactModel } from "../sequelize/contact.model";
|
||||||
|
|
||||||
|
export interface IContactMapper
|
||||||
|
extends ISequelizeMapper<ContactModel, ContactCreationAttributes, Contact> {}
|
||||||
|
|
||||||
|
export class ContactMapper
|
||||||
|
extends SequelizeMapper<ContactModel, ContactCreationAttributes, Contact>
|
||||||
|
implements IContactMapper
|
||||||
|
{
|
||||||
|
public mapToDomain(source: ContactModel, params?: MapperParamsType): Result<Contact, Error> {
|
||||||
|
const idOrError = UniqueID.create(source.id);
|
||||||
|
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,
|
||||||
|
tinOrError,
|
||||||
|
emailOrError,
|
||||||
|
phoneOrError,
|
||||||
|
faxOrError,
|
||||||
|
postalAddressOrError,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (result.isFailure) {
|
||||||
|
return Result.fail(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Contact.create(
|
||||||
|
{
|
||||||
|
isFreelancer: source.is_freelancer,
|
||||||
|
reference: source.reference,
|
||||||
|
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,
|
||||||
|
status: source.status,
|
||||||
|
langCode: source.lang_code,
|
||||||
|
currencyCode: source.currency_code,
|
||||||
|
},
|
||||||
|
idOrError.data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public mapToPersistence(
|
||||||
|
source: Contact,
|
||||||
|
params?: MapperParamsType
|
||||||
|
): Result<ContactCreationAttributes, Error> {
|
||||||
|
return Result.ok({
|
||||||
|
id: source.id.toString(),
|
||||||
|
reference: source.reference,
|
||||||
|
is_freelancer: source.isFreelancer,
|
||||||
|
name: source.name,
|
||||||
|
trade_name: source.tradeName.getOrUndefined(),
|
||||||
|
tin: source.tin.toString(),
|
||||||
|
|
||||||
|
street: source.address.street,
|
||||||
|
city: source.address.city,
|
||||||
|
state: source.address.state,
|
||||||
|
postal_code: source.address.postalCode,
|
||||||
|
country: source.address.country,
|
||||||
|
|
||||||
|
email: source.email.toString(),
|
||||||
|
phone: source.phone.toString(),
|
||||||
|
fax: source.fax.isSome() ? source.fax.getOrUndefined()?.toString() : undefined,
|
||||||
|
website: source.website.getOrUndefined(),
|
||||||
|
|
||||||
|
legal_record: source.legalRecord,
|
||||||
|
default_tax: source.defaultTax,
|
||||||
|
status: source.isActive ? "active" : "inactive",
|
||||||
|
lang_code: source.langCode,
|
||||||
|
currency_code: source.currencyCode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const contactMapper: ContactMapper = new ContactMapper();
|
||||||
|
export { contactMapper };
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./contact.mapper";
|
||||||
@ -0,0 +1,172 @@
|
|||||||
|
import {
|
||||||
|
CreationOptional,
|
||||||
|
DataTypes,
|
||||||
|
InferAttributes,
|
||||||
|
InferCreationAttributes,
|
||||||
|
Model,
|
||||||
|
Sequelize,
|
||||||
|
} from "sequelize";
|
||||||
|
|
||||||
|
export type ContactCreationAttributes = InferCreationAttributes<ContactModel, {}> & {};
|
||||||
|
|
||||||
|
export class ContactModel extends Model<
|
||||||
|
InferAttributes<ContactModel>,
|
||||||
|
InferCreationAttributes<ContactModel>
|
||||||
|
> {
|
||||||
|
// To avoid table creation
|
||||||
|
/*static async sync(): Promise<any> {
|
||||||
|
return Promise.resolve();
|
||||||
|
}*/
|
||||||
|
|
||||||
|
declare id: string;
|
||||||
|
declare reference: CreationOptional<string>;
|
||||||
|
|
||||||
|
declare is_freelancer: 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 lang_code: string;
|
||||||
|
declare currency_code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (sequelize: Sequelize) => {
|
||||||
|
ContactModel.init(
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
primaryKey: true,
|
||||||
|
},
|
||||||
|
reference: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
is_freelancer: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
trade_name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
website: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
validate: {
|
||||||
|
isUrl: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legal_record: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
default_tax: {
|
||||||
|
type: new DataTypes.SMALLINT(),
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: 2100,
|
||||||
|
},
|
||||||
|
|
||||||
|
lang_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: "contacts",
|
||||||
|
|
||||||
|
paranoid: true, // softs deletes
|
||||||
|
timestamps: true,
|
||||||
|
|
||||||
|
createdAt: "created_at",
|
||||||
|
updatedAt: "updated_at",
|
||||||
|
deletedAt: "deleted_at",
|
||||||
|
|
||||||
|
indexes: [
|
||||||
|
{ name: "email_idx", fields: ["email"], unique: true },
|
||||||
|
{ name: "reference_idx", fields: ["reference"], unique: true },
|
||||||
|
],
|
||||||
|
|
||||||
|
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
|
||||||
|
|
||||||
|
defaultScope: {},
|
||||||
|
|
||||||
|
scopes: {},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return ContactModel;
|
||||||
|
};
|
||||||
@ -1,53 +1,53 @@
|
|||||||
import { EmailAddress, UniqueID } from "@common/domain";
|
import { EmailAddress, UniqueID } from "@common/domain";
|
||||||
import { Collection, Result } from "@common/helpers";
|
import { Collection, Result } from "@common/helpers";
|
||||||
import { SequelizeRepository } from "@common/infrastructure";
|
import { SequelizeRepository } from "@common/infrastructure";
|
||||||
import { Company, ICompanyRepository } from "@contexts/companies/domain";
|
|
||||||
import { Transaction } from "sequelize";
|
import { Transaction } from "sequelize";
|
||||||
import { companyMapper, ICompanyMapper } from "../mappers";
|
import { Contact, IContactRepository } from "../../domain";
|
||||||
import { CompanyModel } from "./company.model";
|
import { contactMapper, IContactMapper } from "../mappers";
|
||||||
|
import { ContactModel } from "./contact.model";
|
||||||
|
|
||||||
class CompanyRepository extends SequelizeRepository<Company> implements ICompanyRepository {
|
class ContactRepository extends SequelizeRepository<Contact> implements IContactRepository {
|
||||||
private readonly _mapper!: ICompanyMapper;
|
private readonly _mapper!: IContactMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 🔹 Función personalizada para mapear errores de unicidad en autenticación
|
* 🔹 Función personalizada para mapear errores de unicidad en autenticación
|
||||||
*/
|
*/
|
||||||
private _customErrorMapper(error: Error): string | null {
|
private _customErrorMapper(error: Error): string | null {
|
||||||
if (error.name === "SequelizeUniqueConstraintError") {
|
if (error.name === "SequelizeUniqueConstraintError") {
|
||||||
return "Company with this email already exists";
|
return "Contact with this email already exists";
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(mapper: ICompanyMapper) {
|
constructor(mapper: IContactMapper) {
|
||||||
super();
|
super();
|
||||||
this._mapper = mapper;
|
this._mapper = mapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAll(transaction?: Transaction): Promise<Result<Collection<Company>, Error>> {
|
async findAll(transaction?: Transaction): Promise<Result<Collection<Contact>, Error>> {
|
||||||
try {
|
try {
|
||||||
const rawCompanys: any = await this._findAll(CompanyModel, {}, transaction);
|
const rawContacts: any = await this._findAll(ContactModel, {}, transaction);
|
||||||
|
|
||||||
if (!rawCompanys === true) {
|
if (!rawContacts === true) {
|
||||||
return Result.fail(new Error("Company with email not exists"));
|
return Result.fail(new Error("Contact with email not exists"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._mapper.mapArrayToDomain(rawCompanys);
|
return this._mapper.mapArrayToDomain(rawContacts);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return this._handleDatabaseError(error, this._customErrorMapper);
|
return this._handleDatabaseError(error, this._customErrorMapper);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async findById(id: UniqueID, transaction?: Transaction): Promise<Result<Company, Error>> {
|
async findById(id: UniqueID, transaction?: Transaction): Promise<Result<Contact, Error>> {
|
||||||
try {
|
try {
|
||||||
const rawCompany: any = await this._getById(CompanyModel, id, {}, transaction);
|
const rawContact: any = await this._getById(ContactModel, id, {}, transaction);
|
||||||
|
|
||||||
if (!rawCompany === true) {
|
if (!rawContact === true) {
|
||||||
return Result.fail(new Error(`Company with id ${id.toString()} not exists`));
|
return Result.fail(new Error(`Contact with id ${id.toString()} not exists`));
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._mapper.mapToDomain(rawCompany);
|
return this._mapper.mapToDomain(rawContact);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return this._handleDatabaseError(error, this._customErrorMapper);
|
return this._handleDatabaseError(error, this._customErrorMapper);
|
||||||
}
|
}
|
||||||
@ -56,26 +56,26 @@ class CompanyRepository extends SequelizeRepository<Company> implements ICompany
|
|||||||
async findByEmail(
|
async findByEmail(
|
||||||
email: EmailAddress,
|
email: EmailAddress,
|
||||||
transaction?: Transaction
|
transaction?: Transaction
|
||||||
): Promise<Result<Company, Error>> {
|
): Promise<Result<Contact, Error>> {
|
||||||
try {
|
try {
|
||||||
const rawCompany: any = await this._getBy(
|
const rawContact: any = await this._getBy(
|
||||||
CompanyModel,
|
ContactModel,
|
||||||
"email",
|
"email",
|
||||||
email.toString(),
|
email.toString(),
|
||||||
{},
|
{},
|
||||||
transaction
|
transaction
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!rawCompany === true) {
|
if (!rawContact === true) {
|
||||||
return Result.fail(new Error(`Company with email ${email.toString()} not exists`));
|
return Result.fail(new Error(`Contact with email ${email.toString()} not exists`));
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._mapper.mapToDomain(rawCompany);
|
return this._mapper.mapToDomain(rawContact);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
return this._handleDatabaseError(error, this._customErrorMapper);
|
return this._handleDatabaseError(error, this._customErrorMapper);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const companyRepository = new CompanyRepository(companyMapper);
|
const contactRepository = new ContactRepository(contactMapper);
|
||||||
export { companyRepository };
|
export { contactRepository };
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { IContactRepository } from "../../domain";
|
||||||
|
import { contactRepository } from "./contact.repository";
|
||||||
|
|
||||||
|
export * from "./contact.model";
|
||||||
|
export * from "./contact.repository";
|
||||||
|
|
||||||
|
export const createContactRepository = (): IContactRepository => {
|
||||||
|
return contactRepository;
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./list";
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { SequelizeTransactionManager } from "@common/infrastructure";
|
||||||
|
import { ListContactsUseCase } from "../../../application";
|
||||||
|
import { ContactService } from "../../../domain";
|
||||||
|
import { contactRepository } from "../../../infraestructure";
|
||||||
|
import { ListContactsController } from "./list-contacts.controller";
|
||||||
|
import { listContactsPresenter } from "./list-contacts.presenter";
|
||||||
|
|
||||||
|
export const listContactsController = () => {
|
||||||
|
const transactionManager = new SequelizeTransactionManager();
|
||||||
|
const contactService = new ContactService(contactRepository);
|
||||||
|
|
||||||
|
const useCase = new ListContactsUseCase(contactService, transactionManager);
|
||||||
|
const presenter = listContactsPresenter;
|
||||||
|
|
||||||
|
return new ListContactsController(useCase, presenter);
|
||||||
|
};
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
import { ExpressController } from "@common/presentation";
|
||||||
|
import { ListContactsUseCase } from "../../../application";
|
||||||
|
import { IListContactsPresenter } from "./list-contacts.presenter";
|
||||||
|
|
||||||
|
export class ListContactsController extends ExpressController {
|
||||||
|
public constructor(
|
||||||
|
private readonly listContacts: ListContactsUseCase,
|
||||||
|
private readonly presenter: IListContactsPresenter
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async executeImpl() {
|
||||||
|
const contactsOrError = await this.listContacts.execute();
|
||||||
|
|
||||||
|
if (contactsOrError.isFailure) {
|
||||||
|
return this.handleError(contactsOrError.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.ok(this.presenter.toDTO(contactsOrError.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
import { Collection, ensureBoolean, ensureNumber, ensureString } from "@common/helpers";
|
||||||
|
import { Contact } from "../../../domain";
|
||||||
|
import { IListContactsResponseDTO } from "../../dto";
|
||||||
|
|
||||||
|
export interface IListContactsPresenter {
|
||||||
|
toDTO: (contacts: Collection<Contact>) => IListContactsResponseDTO[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listContactsPresenter: IListContactsPresenter = {
|
||||||
|
toDTO: (contacts: Collection<Contact>): IListContactsResponseDTO[] =>
|
||||||
|
contacts.map((contact) => ({
|
||||||
|
id: ensureString(contact.id.toString()),
|
||||||
|
reference: ensureString(contact.reference),
|
||||||
|
|
||||||
|
is_freelancer: ensureBoolean(contact.isFreelancer),
|
||||||
|
name: ensureString(contact.name),
|
||||||
|
trade_name: ensureString(contact.tradeName.getOrUndefined()),
|
||||||
|
tin: ensureString(contact.tin.toString()),
|
||||||
|
|
||||||
|
street: ensureString(contact.address.street),
|
||||||
|
city: ensureString(contact.address.city),
|
||||||
|
state: ensureString(contact.address.state),
|
||||||
|
postal_code: ensureString(contact.address.postalCode),
|
||||||
|
country: ensureString(contact.address.country),
|
||||||
|
|
||||||
|
email: ensureString(contact.email.toString()),
|
||||||
|
phone: ensureString(contact.phone.toString()),
|
||||||
|
fax: ensureString(contact.fax.getOrUndefined()?.toString()),
|
||||||
|
website: ensureString(contact.website.getOrUndefined()),
|
||||||
|
|
||||||
|
legal_record: ensureString(contact.legalRecord),
|
||||||
|
|
||||||
|
default_tax: ensureNumber(contact.defaultTax),
|
||||||
|
status: ensureString(contact.isActive ? "active" : "inactive"),
|
||||||
|
lang_code: ensureString(contact.langCode),
|
||||||
|
currency_code: ensureString(contact.currencyCode),
|
||||||
|
})),
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export interface IListContactsRequestDTO {}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
export interface IListContactsResponseDTO {
|
||||||
|
id: string;
|
||||||
|
reference: string;
|
||||||
|
|
||||||
|
is_freelancer: 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;
|
||||||
|
lang_code: string;
|
||||||
|
currency_code: string;
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const ListContactsSchema = z.object({});
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./contacts.request.dto";
|
||||||
|
export * from "./contacts.response.dto";
|
||||||
|
export * from "./contacts.validation.dto";
|
||||||
2
apps/server/src/contexts/contacts/presentation/index.ts
Normal file
2
apps/server/src/contexts/contacts/presentation/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./controllers";
|
||||||
|
export * from "./dto";
|
||||||
@ -1,20 +1,21 @@
|
|||||||
import { validateRequestDTO } from "@common/presentation";
|
import { validateRequestDTO } from "@common/presentation";
|
||||||
|
import { ListAccountsSchema } from "@contexts/accounts/presentation";
|
||||||
|
import { listAccountsController } from "@contexts/accounts/presentation/controllers/list-accounts";
|
||||||
import { checkTabContext } from "@contexts/auth/infraestructure";
|
import { checkTabContext } from "@contexts/auth/infraestructure";
|
||||||
import { listCompaniesController, ListCompaniesSchema } from "@contexts/companies/presentation";
|
|
||||||
import { NextFunction, Request, Response, Router } from "express";
|
import { NextFunction, Request, Response, Router } from "express";
|
||||||
|
|
||||||
export const companiesRouter = (appRouter: Router) => {
|
export const accountsRouter = (appRouter: Router) => {
|
||||||
const routes: Router = Router({ mergeParams: true });
|
const routes: Router = Router({ mergeParams: true });
|
||||||
|
|
||||||
routes.get(
|
routes.get(
|
||||||
"/",
|
"/",
|
||||||
validateRequestDTO(ListCompaniesSchema),
|
validateRequestDTO(ListAccountsSchema),
|
||||||
checkTabContext,
|
checkTabContext,
|
||||||
//checkUser,
|
//checkUser,
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
listCompaniesController().execute(req, res, next);
|
listAccountsController().execute(req, res, next);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
appRouter.use("/companies", routes);
|
appRouter.use("/accounts", routes);
|
||||||
};
|
};
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
|
import { accountsRouter } from "./accounts.routes";
|
||||||
import { authRouter } from "./auth.routes";
|
import { authRouter } from "./auth.routes";
|
||||||
import { companiesRouter } from "./companies.routes";
|
|
||||||
import { customersRouter } from "./customers.routes";
|
import { customersRouter } from "./customers.routes";
|
||||||
import { usersRouter } from "./users.routes";
|
import { usersRouter } from "./users.routes";
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ export const v1Routes = () => {
|
|||||||
|
|
||||||
authRouter(routes);
|
authRouter(routes);
|
||||||
usersRouter(routes);
|
usersRouter(routes);
|
||||||
companiesRouter(routes);
|
accountsRouter(routes);
|
||||||
|
|
||||||
// Sales
|
// Sales
|
||||||
customersRouter(routes);
|
customersRouter(routes);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user