This commit is contained in:
David Arranz 2025-03-04 18:08:33 +01:00
parent 42987d688f
commit 2e397900e8
72 changed files with 1298 additions and 348 deletions

7
.vscode/launch.json vendored
View File

@ -14,7 +14,6 @@
"name": "Launch Chrome localhost", "name": "Launch Chrome localhost",
"type": "chrome", "type": "chrome",
"request": "launch", "request": "launch",
"reAttach": true,
"url": "http://localhost:5173", "url": "http://localhost:5173",
"webRoot": "${workspaceFolder}/client" "webRoot": "${workspaceFolder}/client"
}, },
@ -30,10 +29,12 @@
{ {
"type": "node", "type": "node",
"request": "attach", "request": "attach",
"name": "SERVER: Attach to dev:debug", "name": "Attach to ts-node-dev",
"port": 4321, "port": 4321,
"restart": true, "restart": true,
"cwd": "${workspaceRoot}" "timeout": 10000,
"sourceMaps": true,
"resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"]
}, },
{ {

View File

@ -2,7 +2,6 @@ DB_HOST=localhost
DB_USER=rodax DB_USER=rodax
DB_PASSWORD=rodax DB_PASSWORD=rodax
DB_NAME=uecko_erp DB_NAME=uecko_erp
DB_DIALECT=mariadb
DB_PORT=3306 DB_PORT=3306
PORT=3002 PORT=3002

View File

@ -4,8 +4,8 @@
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"dev": "ts-node-dev -r tsconfig-paths/register ./src/index.ts", "dev:nodebug": "ts-node-dev -r tsconfig-paths/register ./src/index.ts",
"dev:debug": "ts-node-dev --transpile-only --respawn --inspect=4321 -r tsconfig-paths/register ./src/index.ts", "dev": "ts-node-dev --transpile-only --respawn --inspect=4321 -r tsconfig-paths/register ./src/index.ts",
"clean": "rm -rf dist", "clean": "rm -rf dist",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"build": "npm run clean && npm run typecheck && esbuild src/index.ts --platform=node --format=cjs --bundle --sourcemap --minify --outdir=dist", "build": "npm run clean && npm run typecheck && esbuild src/index.ts --platform=node --format=cjs --bundle --sourcemap --minify --outdir=dist",
@ -45,6 +45,7 @@
"nodemon": "^3.1.9", "nodemon": "^3.1.9",
"ts-jest": "^29.2.5", "ts-jest": "^29.2.5",
"ts-node-dev": "^2.0.0", "ts-node-dev": "^2.0.0",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3" "typescript": "^5.7.3"
}, },
"dependencies": { "dependencies": {
@ -63,7 +64,6 @@
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"libphonenumber-js": "^1.11.20", "libphonenumber-js": "^1.11.20",
"luxon": "^3.5.0", "luxon": "^3.5.0",
"mariadb": "^3.4.0",
"module-alias": "^2.2.3", "module-alias": "^2.2.3",
"mysql2": "^3.12.0", "mysql2": "^3.12.0",
"passport": "^0.7.0", "passport": "^0.7.0",
@ -75,7 +75,6 @@
"sequelize": "^6.37.5", "sequelize": "^6.37.5",
"shallow-equal-object": "^1.1.1", "shallow-equal-object": "^1.1.1",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"uuid": "^11.0.5", "uuid": "^11.0.5",
"winston": "^3.17.0", "winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0", "winston-daily-rotate-file": "^5.0.0",

View File

@ -11,14 +11,11 @@ const postalCodeSchema = z
message: "Invalid postal code format", message: "Invalid postal code format",
}); });
const countrySchema = z.string().min(2).max(56);
const provinceSchema = z.string().min(2).max(50);
const citySchema = z.string().min(2).max(50);
const streetSchema = z.string().min(2).max(255); const streetSchema = z.string().min(2).max(255);
const street2Schema = z.string().optional(); const street2Schema = z.string().optional();
const citySchema = z.string().min(2).max(50);
const stateSchema = z.string().min(2).max(50);
const countrySchema = z.string().min(2).max(56);
interface IPostalAddressProps { interface IPostalAddressProps {
street: string; street: string;
@ -37,7 +34,7 @@ export class PostalAddress extends ValueObject<IPostalAddressProps> {
street2: street2Schema, street2: street2Schema,
city: citySchema, city: citySchema,
postalCode: postalCodeSchema, postalCode: postalCodeSchema,
state: provinceSchema, state: stateSchema,
country: countrySchema, country: countrySchema,
}) })
.safeParse(values); .safeParse(values);
@ -60,6 +57,20 @@ export class PostalAddress extends ValueObject<IPostalAddressProps> {
return PostalAddress.create(values!).map((value) => Maybe.some(value)); return PostalAddress.create(values!).map((value) => Maybe.some(value));
} }
static update(
oldAddress: PostalAddress,
data: Partial<PostalAddress>
): Result<PostalAddress, Error> {
return PostalAddress.create({
street: data.street ?? oldAddress.street,
street2: data.street2?.getOrUndefined() ?? oldAddress.street2.getOrUndefined(),
city: data.city ?? oldAddress.city,
postalCode: data.postalCode ?? oldAddress.postalCode,
state: data.state ?? oldAddress.state,
country: data.country ?? oldAddress.country,
}).getOrElse(this);
}
get street(): string { get street(): string {
return this.props.street; return this.props.street;
} }

View File

@ -15,11 +15,11 @@ interface IDomainMapper<TModel extends Model, TEntity extends DomainEntity<any>>
} }
interface IPersistenceMapper<TModelAttributes, TEntity extends DomainEntity<any>> { interface IPersistenceMapper<TModelAttributes, TEntity extends DomainEntity<any>> {
mapToPersistence(source: TEntity, params?: MapperParamsType): Result<TModelAttributes, Error>; mapToPersistence(source: TEntity, params?: MapperParamsType): TModelAttributes;
mapCollectionToPersistence( mapCollectionToPersistence(
source: Collection<TEntity>, source: Collection<TEntity>,
params?: MapperParamsType params?: MapperParamsType
): Result<TModelAttributes[], Error>; ): TModelAttributes[];
} }
export interface ISequelizeMapper< export interface ISequelizeMapper<
@ -59,23 +59,13 @@ export abstract class SequelizeMapper<
} }
} }
public abstract mapToPersistence( public abstract mapToPersistence(source: TEntity, params?: MapperParamsType): TModelAttributes;
source: TEntity,
params?: MapperParamsType
): Result<TModelAttributes, Error>;
public mapCollectionToPersistence( public mapCollectionToPersistence(
source: Collection<TEntity>, source: Collection<TEntity>,
params?: MapperParamsType params?: MapperParamsType
): Result<TModelAttributes[], Error> { ): TModelAttributes[] {
try { return source.map((value, index) => this.mapToPersistence(value, { index, ...params }));
const result = source.map(
(value, index) => this.mapToPersistence(value, { index, ...params }).data
);
return Result.ok(result);
} catch (error) {
return Result.fail(error as Error);
}
} }
protected safeMap<T>(operation: () => T, key: string): Result<T, Error> { protected safeMap<T>(operation: () => T, key: string): Result<T, Error> {

View File

@ -11,7 +11,7 @@ export const sequelize = new Sequelize(
process.env.DB_PASSWORD as string, // password process.env.DB_PASSWORD as string, // password
{ {
host: process.env.DB_HOST as string, host: process.env.DB_HOST as string,
dialect: "mariadb", dialect: "mysql",
port: parseInt(process.env.DB_PORT || "3306", 10), port: parseInt(process.env.DB_PORT || "3306", 10),
dialectOptions: { dialectOptions: {
multipleStatements: true, multipleStatements: true,

View File

@ -0,0 +1,88 @@
import { EmailAddress, PhoneNumber, PostalAddress, TINNumber, UniqueID } from "@common/domain";
import { Maybe, Result } from "@common/helpers";
import { ITransactionManager } from "@common/infrastructure/database";
import { logger } from "@common/infrastructure/logger";
import { Account, AccountStatus, IAccountProps, IAccountService } from "@contexts/accounts/domain";
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_freelancer,
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.lang_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

@ -0,0 +1,23 @@
import { UniqueID } from "@common/domain";
import { Result } from "@common/helpers";
import { ITransactionManager } from "@common/infrastructure/database";
import { logger } from "@common/infrastructure/logger";
import { Account, IAccountService } from "@contexts/accounts/domain";
export class GetAccountsUseCase {
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 +1,4 @@
export * from "./list-accounts"; 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

@ -0,0 +1,22 @@
import { Collection, Result } from "@common/helpers";
import { ITransactionManager } from "@common/infrastructure/database";
import { logger } from "@common/infrastructure/logger";
import { Account, IAccountService } from "@contexts/accounts/domain";
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 +0,0 @@
export * from "./list-accounts.use-case";

View File

@ -1,17 +0,0 @@
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);
});
}
}

View File

@ -0,0 +1,46 @@
import { UniqueID } from "@common/domain";
import { Result } from "@common/helpers";
import { TransactionManager } from "@common/infrastructure/database";
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

@ -0,0 +1,121 @@
import { EmailAddress, PhoneNumber, PostalAddress, TINNumber, UniqueID } from "@common/domain";
import { Maybe, Result } from "@common/helpers";
import { ITransactionManager } from "@common/infrastructure/database";
import { logger } from "@common/infrastructure/logger";
import { Account, IAccountProps, IAccountService } from "@contexts/accounts/domain";
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_freelancer) {
validatedData.isFreelancer = dto.is_freelancer;
}
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.lang_code) {
validatedData.langCode = dto.lang_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

@ -7,8 +7,11 @@ import {
UniqueID, UniqueID,
} from "@common/domain"; } from "@common/domain";
import { Maybe, Result } from "@common/helpers"; import { Maybe, Result } from "@common/helpers";
import { AccountStatus } from "../value-objects";
export interface IAccountProps { export interface IAccountProps {
status: AccountStatus;
isFreelancer: boolean; isFreelancer: boolean;
name: string; name: string;
tin: TINNumber; tin: TINNumber;
@ -17,7 +20,7 @@ export interface IAccountProps {
phone: PhoneNumber; phone: PhoneNumber;
legalRecord: string; legalRecord: string;
defaultTax: number; defaultTax: number;
status: string;
langCode: string; langCode: string;
currencyCode: string; currencyCode: string;
@ -29,6 +32,7 @@ export interface IAccountProps {
export interface IAccount { export interface IAccount {
id: UniqueID; id: UniqueID;
status: AccountStatus;
name: string; name: string;
tin: TINNumber; tin: TINNumber;
address: PostalAddress; address: PostalAddress;
@ -36,6 +40,7 @@ export interface IAccount {
phone: PhoneNumber; phone: PhoneNumber;
legalRecord: string; legalRecord: string;
defaultTax: number; defaultTax: number;
langCode: string; langCode: string;
currencyCode: string; currencyCode: string;
@ -47,6 +52,9 @@ export interface IAccount {
isAccount: boolean; isAccount: boolean;
isFreelancer: boolean; isFreelancer: boolean;
isActive: boolean; isActive: boolean;
activate(): boolean;
deactivate(): boolean;
} }
export class Account extends AggregateRoot<IAccountProps> implements IAccount { export class Account extends AggregateRoot<IAccountProps> implements IAccount {
@ -64,6 +72,55 @@ export class Account extends AggregateRoot<IAccountProps> implements IAccount {
return Result.ok(account); 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() { get name() {
return this.props.name; return this.props.name;
} }
@ -125,6 +182,6 @@ export class Account extends AggregateRoot<IAccountProps> implements IAccount {
} }
get isActive(): boolean { get isActive(): boolean {
return this.props.status === "active"; return this.props.status.equals(AccountStatus.createActive());
} }
} }

View File

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

View File

@ -3,7 +3,11 @@ import { Collection, Result } from "@common/helpers";
import { Account } from "../aggregates"; import { Account } from "../aggregates";
export interface IAccountRepository { export interface IAccountRepository {
accountExists(id: UniqueID, transaction?: any): Promise<Result<boolean, Error>>;
findAll(transaction?: any): Promise<Result<Collection<Account>, Error>>; findAll(transaction?: any): Promise<Result<Collection<Account>, Error>>;
findById(id: UniqueID, transaction?: any): Promise<Result<Account, Error>>; findById(id: UniqueID, transaction?: any): Promise<Result<Account, Error>>;
findByEmail(email: EmailAddress, 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

@ -0,0 +1,20 @@
import { UniqueID } from "@common/domain";
import { Collection, Result } from "@common/helpers";
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

@ -0,0 +1,41 @@
// Pruebas unitarias: AccountService.test.ts
import { Account } from "../domain/Account";
import { IAccountRepository } from "../repositories/AccountRepository";
import { AccountService } from "../services/AccountService";
const mockAccountRepository: IAccountRepository = {
findById: jest.fn(),
save: jest.fn(),
};
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.save as jest.Mock).mockResolvedValue(undefined);
const result = await accountService.updateAccountById("123", { 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("123", { name: "Nuevo Nombre" });
expect(result.isFailure).toBe(true);
expect(result.error.message).toBe("Account not found");
});
});

View File

@ -0,0 +1,104 @@
import { UniqueID } from "@common/domain";
import { Collection, Result } from "@common/helpers";
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,8 +0,0 @@
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>>;
}

View File

@ -1,23 +0,0 @@
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);
}
}

View File

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

View File

@ -0,0 +1,59 @@
import { ValueObject } from "@common/domain";
import { Result } from "@common/helpers";
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);
}
toString(): string {
return this.getValue();
}
}

View File

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

View File

@ -5,7 +5,7 @@ import {
MapperParamsType, MapperParamsType,
SequelizeMapper, SequelizeMapper,
} from "@common/infrastructure/sequelize/sequelize-mapper"; } from "@common/infrastructure/sequelize/sequelize-mapper";
import { Account } from "@contexts/accounts/domain/"; import { Account, AccountStatus } from "@contexts/accounts/domain/";
import { AccountCreationAttributes, AccountModel } from "../sequelize/account.model"; import { AccountCreationAttributes, AccountModel } from "../sequelize/account.model";
export interface IAccountMapper export interface IAccountMapper
@ -17,6 +17,7 @@ export class AccountMapper
{ {
public mapToDomain(source: AccountModel, params?: MapperParamsType): Result<Account, Error> { public mapToDomain(source: AccountModel, params?: MapperParamsType): Result<Account, Error> {
const idOrError = UniqueID.create(source.id); const idOrError = UniqueID.create(source.id);
const statusOrError = AccountStatus.create(source.status);
const tinOrError = TINNumber.create(source.tin); const tinOrError = TINNumber.create(source.tin);
const emailOrError = EmailAddress.create(source.email); const emailOrError = EmailAddress.create(source.email);
const phoneOrError = PhoneNumber.create(source.phone); const phoneOrError = PhoneNumber.create(source.phone);
@ -31,6 +32,7 @@ export class AccountMapper
const result = Result.combine([ const result = Result.combine([
idOrError, idOrError,
statusOrError,
tinOrError, tinOrError,
emailOrError, emailOrError,
phoneOrError, phoneOrError,
@ -44,6 +46,7 @@ export class AccountMapper
return Account.create( return Account.create(
{ {
status: statusOrError.data,
isFreelancer: source.is_freelancer, isFreelancer: source.is_freelancer,
name: source.name, name: source.name,
tradeName: source.trade_name ? Maybe.some(source.trade_name) : Maybe.none(), tradeName: source.trade_name ? Maybe.some(source.trade_name) : Maybe.none(),
@ -55,7 +58,6 @@ export class AccountMapper
website: source.website ? Maybe.some(source.website) : Maybe.none(), website: source.website ? Maybe.some(source.website) : Maybe.none(),
legalRecord: source.legal_record, legalRecord: source.legal_record,
defaultTax: source.default_tax, defaultTax: source.default_tax,
status: source.status,
langCode: source.lang_code, langCode: source.lang_code,
currencyCode: source.currency_code, currencyCode: source.currency_code,
logo: source.logo ? Maybe.some(source.logo) : Maybe.none(), logo: source.logo ? Maybe.some(source.logo) : Maybe.none(),
@ -64,11 +66,8 @@ export class AccountMapper
); );
} }
public mapToPersistence( public mapToPersistence(source: Account, params?: MapperParamsType): AccountCreationAttributes {
source: Account, return {
params?: MapperParamsType
): Result<AccountCreationAttributes, Error> {
return Result.ok({
id: source.id.toString(), id: source.id.toString(),
is_freelancer: source.isFreelancer, is_freelancer: source.isFreelancer,
name: source.name, name: source.name,
@ -92,7 +91,7 @@ export class AccountMapper
lang_code: source.langCode, lang_code: source.langCode,
currency_code: source.currencyCode, currency_code: source.currencyCode,
logo: source.logo.getOrUndefined(), logo: source.logo.getOrUndefined(),
}); };
} }
} }

View File

@ -63,6 +63,7 @@ export default (sequelize: Sequelize) => {
trade_name: { trade_name: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true, allowNull: true,
defaultValue: null,
}, },
tin: { tin: {
type: DataTypes.STRING, type: DataTypes.STRING,
@ -104,10 +105,12 @@ export default (sequelize: Sequelize) => {
fax: { fax: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true, allowNull: true,
defaultValue: null,
}, },
website: { website: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true, allowNull: true,
defaultValue: null,
validate: { validate: {
isUrl: true, isUrl: true,
}, },
@ -126,6 +129,7 @@ export default (sequelize: Sequelize) => {
logo: { logo: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true, allowNull: true,
defaultValue: null,
}, },
lang_code: { lang_code: {

View File

@ -2,7 +2,7 @@ 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 { Account } from "@contexts/accounts/domain"; import { Account } from "@contexts/accounts/domain";
import { IAccountRepository } from "@contexts/accounts/domain/repositories/company-repository.interface"; import { IAccountRepository } from "@contexts/accounts/domain/repositories/account-repository.interface";
import { Transaction } from "sequelize"; import { Transaction } from "sequelize";
import { accountMapper, IAccountMapper } from "../mappers/account.mapper"; import { accountMapper, IAccountMapper } from "../mappers/account.mapper";
import { AccountModel } from "./account.model"; import { AccountModel } from "./account.model";
@ -26,6 +26,16 @@ class AccountRepository extends SequelizeRepository<Account> implements IAccount
this._mapper = mapper; 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>> { async findAll(transaction?: Transaction): Promise<Result<Collection<Account>, Error>> {
try { try {
const rawAccounts: any = await this._findAll(AccountModel, {}, transaction); const rawAccounts: any = await this._findAll(AccountModel, {}, transaction);
@ -76,6 +86,16 @@ class AccountRepository extends SequelizeRepository<Account> implements IAccount
return this._handleDatabaseError(error, this._customErrorMapper); 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); const accountRepository = new AccountRepository(accountMapper);

View File

@ -1,8 +1,10 @@
import { IAccountRepository } from "@contexts/accounts/domain/repositories/company-repository.interface"; import { IAccountRepository } from "@contexts/accounts/domain/repositories/account-repository.interface";
import { accountRepository } from "./account.repository"; import { accountRepository } from "./account.repository";
export * from "./account.model"; export * from "./account.model";
export * from "./account.repository";
export const createAccountRepository = (): IAccountRepository => { export const createAccountRepository = (): IAccountRepository => {
return accountRepository; return accountRepository;
}; };

View File

@ -0,0 +1,45 @@
import { UniqueID } from "@common/domain";
import { ExpressController } from "@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

@ -0,0 +1,37 @@
import { ensureBoolean, ensureNumber, ensureString } from "@common/helpers";
import { Account } from "@contexts/accounts/domain";
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_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()),
}),
};

View File

@ -0,0 +1,16 @@
import { SequelizeTransactionManager } from "@common/infrastructure";
import { CreateAccountUseCase } from "@contexts/accounts/application/create-account.use-case";
import { AccountService } from "@contexts/accounts/domain";
import { accountRepository } from "@contexts/accounts/infraestructure";
import { CreateAccountController } from "./create-account.controller";
import { createAccountPresenter } from "./create-account.presenter";
export const createAccountController = () => {
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

@ -0,0 +1,44 @@
import { UniqueID } from "@common/domain";
import { ExpressController } from "@common/presentation";
import { GetAccountsUseCase } from "@contexts/accounts/application";
import { IGetAccountPresenter } from "./get-account.presenter";
export class GetAccountController extends ExpressController {
public constructor(
private readonly getAccount: GetAccountsUseCase,
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

@ -0,0 +1,37 @@
import { ensureBoolean, ensureNumber, ensureString } from "@common/helpers";
import { Account } from "@contexts/accounts/domain";
import { IGetAccountResponseDTO } from "../../dto";
export interface IGetAccountPresenter {
toDTO: (account: Account) => IGetAccountResponseDTO;
}
export const getAccountPresenter: IGetAccountPresenter = {
toDTO: (account: Account): IGetAccountResponseDTO => ({
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()),
}),
};

View File

@ -0,0 +1,16 @@
import { SequelizeTransactionManager } from "@common/infrastructure";
import { GetAccountsUseCase } from "@contexts/accounts/application";
import { AccountService } from "@contexts/accounts/domain";
import { accountRepository } from "@contexts/accounts/infraestructure";
import { GetAccountController } from "./get-account.controller";
import { getAccountPresenter } from "./get-account.presenter";
export const getAccountController = () => {
const transactionManager = new SequelizeTransactionManager();
const accountService = new AccountService(accountRepository);
const useCase = new GetAccountsUseCase(accountService, transactionManager);
const presenter = getAccountPresenter;
return new GetAccountController(useCase, presenter);
};

View File

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

View File

@ -1,7 +1,7 @@
import { SequelizeTransactionManager } from "@common/infrastructure"; import { SequelizeTransactionManager } from "@common/infrastructure";
import { ListAccountsUseCase } from "@contexts/accounts/application/list-accounts/list-accounts.use-case"; import { ListAccountsUseCase } from "@contexts/accounts/application";
import { AccountService } from "@contexts/accounts/domain/services/company.service"; import { AccountService } from "@contexts/accounts/domain";
import { accountRepository } from "@contexts/accounts/infraestructure/sequelize/account.repository"; import { accountRepository } from "@contexts/accounts/infraestructure";
import { ListAccountsController } from "./list-accounts.controller"; import { ListAccountsController } from "./list-accounts.controller";
import { listAccountsPresenter } from "./list-accounts.presenter"; import { listAccountsPresenter } from "./list-accounts.presenter";

View File

@ -1,5 +1,5 @@
import { ExpressController } from "@common/presentation"; import { ExpressController } from "@common/presentation";
import { ListAccountsUseCase } from "@contexts/accounts/application/list-accounts/list-accounts.use-case"; import { ListAccountsUseCase } from "@contexts/accounts/application";
import { IListAccountsPresenter } from "./list-accounts.presenter"; import { IListAccountsPresenter } from "./list-accounts.presenter";
export class ListAccountsController extends ExpressController { export class ListAccountsController extends ExpressController {

View File

@ -0,0 +1,16 @@
import { SequelizeTransactionManager } from "@common/infrastructure";
import { UpdateAccountUseCase } from "@contexts/accounts/application";
import { AccountService } from "@contexts/accounts/domain";
import { accountRepository } from "@contexts/accounts/infraestructure";
import { UpdateAccountController } from "./update-account.controller";
import { updateAccountPresenter } from "./update-account.presenter";
export const updateAccountController = () => {
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

@ -0,0 +1,46 @@
import { UniqueID } from "@common/domain";
import { ExpressController } from "@common/presentation";
import { UpdateAccountUseCase } from "@contexts/accounts/application/update-account.use-case";
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

@ -0,0 +1,37 @@
import { ensureBoolean, ensureNumber, ensureString } from "@common/helpers";
import { Account } from "@contexts/accounts/domain";
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_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()),
}),
};

View File

@ -1 +1,52 @@
export interface IListAccountsRequestDTO {} export interface IListAccountsRequestDTO {}
export interface ICreateAccountRequestDTO {
id: 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;
lang_code: string;
currency_code: string;
logo: string;
}
export interface IUpdateAccountRequestDTO {
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;
lang_code: string;
currency_code: string;
logo: string;
}

View File

@ -25,3 +25,90 @@ export interface IListAccountsResponseDTO {
currency_code: string; currency_code: string;
logo: string; logo: string;
} }
export interface IGetAccountResponseDTO {
id: 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;
logo: string;
}
export interface ICreateAccountResponseDTO {
id: 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;
logo: string;
}
// Inferir el tipo en TypeScript desde el esquema Zod
//export type IUpdateAcccountResponseDTO = z.infer<typeof IUpdateAcccountResponseDTOSchema>;
export interface IUpdateAccountResponseDTO {
id: 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;
logo: string;
}

View File

@ -1,3 +1,87 @@
import { z } from "zod"; import { z } from "zod";
export const ListAccountsSchema = z.object({}); export const ListAccountsSchema = z.object({});
export const IGetAcccountResponseDTOSchema = z.object({
id: z.string(),
is_freelancer: 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(),
lang_code: z.string(),
currency_code: z.string(),
logo: z.string(),
});
export const ICreateAcccountResponseDTOSchema = z.object({
id: z.string(),
is_freelancer: 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(),
lang_code: z.string(),
currency_code: z.string(),
logo: z.string(),
});
export const IUpdateAcccountResponseDTOSchema = z.object({
id: z.string(),
is_freelancer: 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(),
lang_code: z.string(),
currency_code: z.string(),
logo: z.string(),
});

View File

@ -67,6 +67,7 @@ export default (sequelize: Sequelize) => {
trade_name: { trade_name: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true, allowNull: true,
defaultValue: null,
}, },
tin: { tin: {
type: DataTypes.STRING, type: DataTypes.STRING,
@ -108,10 +109,12 @@ export default (sequelize: Sequelize) => {
fax: { fax: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true, allowNull: true,
defaultValue: null,
}, },
website: { website: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true, allowNull: true,
defaultValue: null,
validate: { validate: {
isUrl: true, isUrl: true,
}, },

View File

@ -1 +0,0 @@
export * from "./get-customer-invoice.use-case";

View File

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

View File

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

View File

@ -1,17 +0,0 @@
import { Collection, Result } from "@common/helpers";
import { ITransactionManager } from "@common/infrastructure/database";
import { Customer } from "@contexts/customer-billing/domain/aggregates";
import { ICustomerService } from "@contexts/customer-billing/domain/services";
export class ListCustomersUseCase {
constructor(
private readonly customerService: ICustomerService,
private readonly transactionManager: ITransactionManager
) {}
public execute(): Promise<Result<Collection<Customer>, Error>> {
return this.transactionManager.complete((transaction) => {
return this.customerService.findCustomer(transaction);
});
}
}

View File

@ -1,2 +1,2 @@
export * from "./customer-invoices"; export * from "./get-customer-invoice.use-case";
export * from "./customers"; export * from "./list-customer-invoices-use-case";

View File

@ -0,0 +1,16 @@
import { Collection, Result } from "@common/helpers";
import { ITransactionManager } from "@common/infrastructure/database";
import { CustomerInvoice, ICustomerInvoiceService } from "../domain";
export class ListCustomerInvoicesUseCase {
constructor(
private readonly invoiceService: ICustomerInvoiceService,
private readonly transactionManager: ITransactionManager
) {}
public execute(): Promise<Result<Collection<CustomerInvoice>, Error>> {
return this.transactionManager.complete((transaction) => {
return this.invoiceService.findCustomerInvoices(transaction);
});
}
}

View File

@ -1,5 +1,5 @@
import { AggregateRoot, UniqueID, UtcDate } from "@common/domain"; import { AggregateRoot, UniqueID, UtcDate } from "@common/domain";
import { Result } from "@common/helpers"; import { Maybe, Result } from "@common/helpers";
import { Customer, CustomerInvoiceItem } from "../entities"; import { Customer, CustomerInvoiceItem } from "../entities";
import { InvoiceStatus } from "../value-objetcs"; import { InvoiceStatus } from "../value-objetcs";
@ -8,7 +8,7 @@ export interface ICustomerInvoiceProps {
issueDate: UtcDate; issueDate: UtcDate;
invoiceNumber: string; invoiceNumber: string;
invoiceType: string; invoiceType: string;
invoiceCustomerReference: string; invoiceCustomerReference: Maybe<string>;
customer: Customer; customer: Customer;
items: CustomerInvoiceItem[]; items: CustomerInvoiceItem[];
@ -20,7 +20,7 @@ export interface ICustomerInvoice {
issueDate: UtcDate; issueDate: UtcDate;
invoiceNumber: string; invoiceNumber: string;
invoiceType: string; invoiceType: string;
invoiceCustomerReference: string; invoiceCustomerReference: Maybe<string>;
customer: Customer; customer: Customer;
items: CustomerInvoiceItem[]; items: CustomerInvoiceItem[];
@ -63,7 +63,7 @@ export class CustomerInvoice
return this.props.invoiceType; return this.props.invoiceType;
} }
get invoiceCustomerReference(): string { get invoiceCustomerReference(): Maybe<string> {
return this.props.invoiceCustomerReference; return this.props.invoiceCustomerReference;
} }

View File

@ -1,2 +1 @@
export * from "./customer-invoice-repository.interface"; export * from "./customer-invoice-repository.interface";
export * from "./customer-repository.interface";

View File

@ -1,4 +1,2 @@
export * from "./customer-invoice-service.interface"; export * from "./customer-invoice-service.interface";
export * from "./customer-invoice.service"; export * from "./customer-invoice.service";
export * from "./customer-service.interface";
export * from "./customer.service";

View File

@ -1,5 +1,5 @@
import { PostalAddress, TINNumber, UniqueID, UtcDate } from "@common/domain"; import { PostalAddress, TINNumber, UniqueID, UtcDate } from "@common/domain";
import { Result } from "@common/helpers"; import { Maybe, Result } from "@common/helpers";
import { import {
ISequelizeMapper, ISequelizeMapper,
MapperParamsType, MapperParamsType,
@ -70,7 +70,7 @@ export class CustomerInvoiceMapper
issueDate: issueDateOrError.data, issueDate: issueDateOrError.data,
invoiceNumber: source.invoice_number, invoiceNumber: source.invoice_number,
invoiceType: source.invoice_type, invoiceType: source.invoice_type,
invoiceCustomerReference: source.invoice_customer_reference, invoiceCustomerReference: Maybe.fromNullable(source.invoice_customer_reference),
customer: customerOrError.data, customer: customerOrError.data,
items: [], items: [],
}, },
@ -89,7 +89,7 @@ export class CustomerInvoiceMapper
issue_date: source.issueDate.toDateString(), issue_date: source.issueDate.toDateString(),
invoice_number: source.invoiceNumber, invoice_number: source.invoiceNumber,
invoice_type: source.invoiceType, invoice_type: source.invoiceType,
invoice_customer_reference: source.invoiceCustomerReference, invoice_customer_reference: source.invoiceCustomerReference.getOrUndefined(),
lang_code: "es", lang_code: "es",
currency_code: "EUR", currency_code: "EUR",

View File

@ -56,6 +56,7 @@ export default (sequelize: Sequelize) => {
id_article: { id_article: {
type: DataTypes.BIGINT().UNSIGNED, type: DataTypes.BIGINT().UNSIGNED,
allowNull: true, allowNull: true,
defaultValue: null,
}, },
position: { position: {
type: new DataTypes.MEDIUMINT(), type: new DataTypes.MEDIUMINT(),
@ -65,26 +66,32 @@ export default (sequelize: Sequelize) => {
description: { description: {
type: new DataTypes.TEXT(), type: new DataTypes.TEXT(),
allowNull: true, allowNull: true,
defaultValue: null,
}, },
quantity: { quantity: {
type: DataTypes.BIGINT(), type: DataTypes.BIGINT(),
allowNull: true, allowNull: true,
defaultValue: null,
}, },
unit_price: { unit_price: {
type: new DataTypes.BIGINT(), type: new DataTypes.BIGINT(),
allowNull: true, allowNull: true,
defaultValue: null,
}, },
subtotal_price: { subtotal_price: {
type: new DataTypes.BIGINT(), type: new DataTypes.BIGINT(),
allowNull: true, allowNull: true,
defaultValue: null,
}, },
discount: { discount: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: true, allowNull: true,
defaultValue: null,
}, },
total_price: { total_price: {
type: new DataTypes.BIGINT(), type: new DataTypes.BIGINT(),
allowNull: true, allowNull: true,
defaultValue: null,
}, },
}, },
{ {

View File

@ -126,6 +126,7 @@ export default (sequelize: Sequelize) => {
customer_id: { customer_id: {
type: new DataTypes.UUID(), type: new DataTypes.UUID(),
allowNull: false,
}, },
customer_name: { customer_name: {
@ -146,6 +147,7 @@ export default (sequelize: Sequelize) => {
customer_street2: { customer_street2: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true, allowNull: true,
defaultValue: null,
}, },
customer_city: { customer_city: {
@ -168,56 +170,67 @@ export default (sequelize: Sequelize) => {
subtotal_price: { subtotal_price: {
type: new DataTypes.BIGINT(), type: new DataTypes.BIGINT(),
allowNull: true, allowNull: true,
defaultValue: null,
}, },
discount: { discount: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: true, allowNull: true,
defaultValue: null,
}, },
discount_price: { discount_price: {
type: new DataTypes.BIGINT(), type: new DataTypes.BIGINT(),
allowNull: true, allowNull: true,
defaultValue: null,
}, },
before_tax_price: { before_tax_price: {
type: new DataTypes.BIGINT(), type: new DataTypes.BIGINT(),
allowNull: true, allowNull: true,
defaultValue: null,
}, },
tax: { tax: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: true, allowNull: true,
defaultValue: null,
}, },
tax_price: { tax_price: {
type: new DataTypes.BIGINT(), type: new DataTypes.BIGINT(),
allowNull: true, allowNull: true,
defaultValue: null,
}, },
total_price: { total_price: {
type: new DataTypes.BIGINT(), type: new DataTypes.BIGINT(),
allowNull: true, allowNull: true,
defaultValue: null,
}, },
notes: { notes: {
type: DataTypes.TEXT, type: DataTypes.TEXT,
allowNull: true, allowNull: true,
defaultValue: null,
}, },
integrity_hash: { integrity_hash: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true, allowNull: true,
defaultValue: null,
comment: "Hash criptográfico para asegurar integridad", comment: "Hash criptográfico para asegurar integridad",
}, },
previous_invoice_id: { previous_invoice_id: {
type: DataTypes.UUID, type: DataTypes.UUID,
allowNull: true, allowNull: true,
defaultValue: null,
comment: "Referencia a la factura anterior (si aplica)", comment: "Referencia a la factura anterior (si aplica)",
}, },
signed_at: { signed_at: {
type: DataTypes.DATE, type: DataTypes.DATE,
allowNull: true, allowNull: true,
defaultValue: null,
comment: "Fecha en que la factura fue firmada digitalmente", comment: "Fecha en que la factura fue firmada digitalmente",
}, },
}, },

View File

@ -67,6 +67,7 @@ export default (sequelize: Sequelize) => {
trade_name: { trade_name: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true, allowNull: true,
defaultValue: null,
}, },
tin: { tin: {
type: DataTypes.STRING, type: DataTypes.STRING,
@ -108,10 +109,12 @@ export default (sequelize: Sequelize) => {
fax: { fax: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true, allowNull: true,
defaultValue: null,
}, },
website: { website: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true, allowNull: true,
defaultValue: null,
validate: { validate: {
isUrl: true, isUrl: true,
}, },

View File

@ -1,81 +0,0 @@
import { EmailAddress, UniqueID } from "@common/domain";
import { Collection, Result } from "@common/helpers";
import { SequelizeRepository } from "@common/infrastructure";
import { Customer, ICustomerRepository } from "@contexts/customer-billing/domain";
import { Transaction } from "sequelize";
import { customerMapper, ICustomerMapper } from "../mappers";
import { CustomerModel } from "./customer.model";
class CustomerRepository extends SequelizeRepository<Customer> implements ICustomerRepository {
private readonly _mapper!: ICustomerMapper;
/**
* 🔹 Función personalizada para mapear errores de unicidad en autenticación
*/
private _customErrorMapper(error: Error): string | null {
if (error.name === "SequelizeUniqueConstraintError") {
return "Customer with this email already exists";
}
return null;
}
constructor(mapper: ICustomerMapper) {
super();
this._mapper = mapper;
}
async findAll(transaction?: Transaction): Promise<Result<Collection<Customer>, Error>> {
try {
const rawCustomers: any = await this._findAll(CustomerModel, {}, transaction);
if (!rawCustomers === true) {
return Result.fail(new Error("Customer with email not exists"));
}
return this._mapper.mapArrayToDomain(rawCustomers);
} catch (error: any) {
return this._handleDatabaseError(error, this._customErrorMapper);
}
}
async findById(id: UniqueID, transaction?: Transaction): Promise<Result<Customer, Error>> {
try {
const rawCustomer: any = await this._getById(CustomerModel, id, {}, transaction);
if (!rawCustomer === true) {
return Result.fail(new Error(`Customer with id ${id.toString()} not exists`));
}
return this._mapper.mapToDomain(rawCustomer);
} catch (error: any) {
return this._handleDatabaseError(error, this._customErrorMapper);
}
}
async findByEmail(
email: EmailAddress,
transaction?: Transaction
): Promise<Result<Customer, Error>> {
try {
const rawCustomer: any = await this._getBy(
CustomerModel,
"email",
email.toString(),
{},
transaction
);
if (!rawCustomer === true) {
return Result.fail(new Error(`Customer with email ${email.toString()} not exists`));
}
return this._mapper.mapToDomain(rawCustomer);
} catch (error: any) {
return this._handleDatabaseError(error, this._customErrorMapper);
}
}
}
const customerRepository = new CustomerRepository(customerMapper);
export { customerRepository };

View File

@ -1,17 +1,15 @@
import { ICustomerRepository } from "@contexts/customer-billing/domain";
import { ICustomerInvoiceRepository } from "@contexts/customer-billing/domain/"; import { ICustomerInvoiceRepository } from "@contexts/customer-billing/domain/";
import { customerRepository } from "./customer.repository"; import { customerInvoiceRepository } from "./customer-invoice.repository";
export * from "./customer.model";
export * from "./customer.repository";
export * from "./customer-invoice.model"; export * from "./customer-invoice.model";
export * from "./customer.model";
export * from "./customer-invoice.repository"; export * from "./customer-invoice.repository";
export const createCustomerRepository = (): ICustomerRepository => { /*export const createCustomerRepository = (): ICustomerRepository => {
return customerRepository; return customerRepository;
}; };*/
export const createCustomerInvoiceRepository = (): ICustomerInvoiceRepository => { export const createCustomerInvoiceRepository = (): ICustomerInvoiceRepository => {
return customerRepository; return customerInvoiceRepository;
}; };

View File

@ -1,6 +1,7 @@
import { SequelizeTransactionManager } from "@common/infrastructure"; import { SequelizeTransactionManager } from "@common/infrastructure";
import { CustomerInvoiceService } from "@contexts/customer-billing/domain"; import { CustomerInvoiceService } from "@contexts/customer-billing/domain";
import { customerInvoiceRepository } from "@contexts/customer-billing/infraestructure"; import { customerInvoiceRepository } from "@contexts/customer-billing/infraestructure";
import { ListCustomerInvoicesUseCase } from "../../../../application";
import { ListCustomerInvoicesController } from "./list-customer-invoices.controller"; import { ListCustomerInvoicesController } from "./list-customer-invoices.controller";
import { listCustomerInvoicesPresenter } from "./list-customer-invoices.presenter"; import { listCustomerInvoicesPresenter } from "./list-customer-invoices.presenter";

View File

@ -1,4 +1,4 @@
import { Collection, ensureBoolean, ensureNumber, ensureString } from "@common/helpers"; import { Collection, ensureString } from "@common/helpers";
import { CustomerInvoice } from "@contexts/customer-billing/domain"; import { CustomerInvoice } from "@contexts/customer-billing/domain";
import { IListCustomerInvoicesResponseDTO } from "../../../dto"; import { IListCustomerInvoicesResponseDTO } from "../../../dto";
@ -8,10 +8,10 @@ export interface IListCustomerInvoicesPresenter {
} }
export const listCustomerInvoicesPresenter: IListCustomerInvoicesPresenter = { export const listCustomerInvoicesPresenter: IListCustomerInvoicesPresenter = {
toDTO: (customers: Collection<CustomerInvoice>): IListCustomerInvoicesResponseDTO[] => toDTO: (invoice: Collection<CustomerInvoice>): IListCustomerInvoicesResponseDTO[] =>
customers.map((customer) => ({ invoice.map((customer) => ({
id: ensureString(customer.id.toString()), id: ensureString(customer.id.toString()),
reference: ensureString(customer.reference), /*reference: ensureString(customer.),
is_freelancer: ensureBoolean(customer.isFreelancer), is_freelancer: ensureBoolean(customer.isFreelancer),
name: ensureString(customer.name), name: ensureString(customer.name),
@ -34,6 +34,6 @@ export const listCustomerInvoicesPresenter: IListCustomerInvoicesPresenter = {
default_tax: ensureNumber(customer.defaultTax), default_tax: ensureNumber(customer.defaultTax),
status: ensureString(customer.isActive ? "active" : "inactive"), status: ensureString(customer.isActive ? "active" : "inactive"),
lang_code: ensureString(customer.langCode), lang_code: ensureString(customer.langCode),
currency_code: ensureString(customer.currencyCode), currency_code: ensureString(customer.currencyCode),*/
})), })),
}; };

View File

@ -1,16 +0,0 @@
import { SequelizeTransactionManager } from "@common/infrastructure";
import { ListCustomersUseCase } from "@contexts/customer-billing/application/customers/list-customers";
import { CustomerService } from "@contexts/customer-billing/domain";
import { customerRepository } from "@contexts/customer-billing/infraestructure";
import { ListCustomersController } from "./list-customers.controller";
import { listCustomersPresenter } from "./list-customers.presenter";
export const listCustomersController = () => {
const transactionManager = new SequelizeTransactionManager();
const customerService = new CustomerService(customerRepository);
const useCase = new ListCustomersUseCase(customerService, transactionManager);
const presenter = listCustomersPresenter;
return new ListCustomersController(useCase, presenter);
};

View File

@ -1,37 +0,0 @@
import { ExpressController } from "@common/presentation";
import { ListCustomersUseCase } from "@contexts/customer-billing/application";
import { IListCustomersPresenter } from "./list-customers.presenter";
export class ListCustomersController extends ExpressController {
public constructor(
private readonly listCustomers: ListCustomersUseCase,
private readonly presenter: IListCustomersPresenter
) {
super();
}
protected async executeImpl() {
const customersOrError = await this.listCustomers.execute();
if (customersOrError.isFailure) {
return this.handleError(customersOrError.error);
}
return this.ok(this.presenter.toDTO(customersOrError.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 { Collection, ensureBoolean, ensureNumber, ensureString } from "@common/helpers";
import { Customer } from "@contexts/customer-billing/domain";
import { IListCustomersResponseDTO } from "../../../dto";
export interface IListCustomersPresenter {
toDTO: (customers: Collection<Customer>) => IListCustomersResponseDTO[];
}
export const listCustomersPresenter: IListCustomersPresenter = {
toDTO: (customers: Collection<Customer>): IListCustomersResponseDTO[] =>
customers.map((customer) => ({
id: ensureString(customer.id.toString()),
reference: ensureString(customer.reference),
is_freelancer: ensureBoolean(customer.isFreelancer),
name: ensureString(customer.name),
trade_name: ensureString(customer.tradeName.getValue()),
tin: ensureString(customer.tin.toString()),
street: ensureString(customer.address.street),
city: ensureString(customer.address.city),
state: ensureString(customer.address.state),
postal_code: ensureString(customer.address.postalCode),
country: ensureString(customer.address.country),
email: ensureString(customer.email.toString()),
phone: ensureString(customer.phone.toString()),
fax: ensureString(customer.fax.getValue()?.toString()),
website: ensureString(customer.website.getValue()),
legal_record: ensureString(customer.legalRecord),
default_tax: ensureNumber(customer.defaultTax),
status: ensureString(customer.isActive ? "active" : "inactive"),
lang_code: ensureString(customer.langCode),
currency_code: ensureString(customer.currencyCode),
})),
};

View File

@ -1,2 +1 @@
export * from "./customer-invoices"; export * from "./customer-invoices";
export * from "./customers";

View File

@ -1,6 +1,6 @@
export interface IListCustomerInvoicesResponseDTO { export interface IListCustomerInvoicesResponseDTO {
id: string; id: string;
reference: string; /*reference: string;
is_freelancer: boolean; is_freelancer: boolean;
name: string; name: string;
@ -23,7 +23,7 @@ export interface IListCustomerInvoicesResponseDTO {
default_tax: number; default_tax: number;
status: string; status: string;
lang_code: string; lang_code: string;
currency_code: string; currency_code: string;*/
} }
export interface IGetCustomerInvoiceResponseDTO {} export interface IGetCustomerInvoiceResponseDTO {}

View File

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

View File

@ -1,6 +1,16 @@
import { validateRequestDTO } from "@common/presentation"; import { validateRequestDTO } from "@common/presentation";
import { ListAccountsSchema } from "@contexts/accounts/presentation"; import {
import { listAccountsController } from "@contexts/accounts/presentation/controllers/list-accounts"; ICreateAcccountResponseDTOSchema,
IGetAcccountResponseDTOSchema,
IUpdateAcccountResponseDTOSchema,
ListAccountsSchema,
} from "@contexts/accounts/presentation";
import {
createAccountController,
getAccountController,
listAccountsController,
updateAccountController,
} from "@contexts/accounts/presentation/controllers";
import { checkTabContext } from "@contexts/auth/infraestructure"; import { checkTabContext } from "@contexts/auth/infraestructure";
import { NextFunction, Request, Response, Router } from "express"; import { NextFunction, Request, Response, Router } from "express";
@ -17,5 +27,35 @@ export const accountsRouter = (appRouter: Router) => {
} }
); );
routes.get(
"/:accountId",
validateRequestDTO(IGetAcccountResponseDTOSchema),
checkTabContext,
//checkUser,
(req: Request, res: Response, next: NextFunction) => {
getAccountController().execute(req, res, next);
}
);
routes.post(
"/",
validateRequestDTO(ICreateAcccountResponseDTOSchema),
checkTabContext,
//checkUser,
(req: Request, res: Response, next: NextFunction) => {
createAccountController().execute(req, res, next);
}
);
routes.put(
"/:accountId",
validateRequestDTO(IUpdateAcccountResponseDTOSchema),
checkTabContext,
//checkUser,
(req: Request, res: Response, next: NextFunction) => {
updateAccountController().execute(req, res, next);
}
);
appRouter.use("/accounts", routes); appRouter.use("/accounts", routes);
}; };

View File

@ -65,9 +65,6 @@ importers:
luxon: luxon:
specifier: ^3.5.0 specifier: ^3.5.0
version: 3.5.0 version: 3.5.0
mariadb:
specifier: ^3.4.0
version: 3.4.0
module-alias: module-alias:
specifier: ^2.2.3 specifier: ^2.2.3
version: 2.2.3 version: 2.2.3
@ -94,16 +91,13 @@ importers:
version: 2.3.3 version: 2.3.3
sequelize: sequelize:
specifier: ^6.37.5 specifier: ^6.37.5
version: 6.37.5(mariadb@3.4.0)(mysql2@3.12.0) version: 6.37.5(mysql2@3.12.0)
shallow-equal-object: shallow-equal-object:
specifier: ^1.1.1 specifier: ^1.1.1
version: 1.1.1 version: 1.1.1
ts-node: ts-node:
specifier: ^10.9.1 specifier: ^10.9.1
version: 10.9.2(@types/node@22.12.0)(typescript@5.7.3) version: 10.9.2(@types/node@22.12.0)(typescript@5.7.3)
tsconfig-paths:
specifier: ^4.2.0
version: 4.2.0
uuid: uuid:
specifier: ^11.0.5 specifier: ^11.0.5
version: 11.0.5 version: 11.0.5
@ -201,6 +195,9 @@ importers:
ts-node-dev: ts-node-dev:
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.0.0(@types/node@22.12.0)(typescript@5.7.3) version: 2.0.0(@types/node@22.12.0)(typescript@5.7.3)
tsconfig-paths:
specifier: ^4.2.0
version: 4.2.0
typescript: typescript:
specifier: ^5.7.3 specifier: ^5.7.3
version: 5.7.3 version: 5.7.3
@ -1289,9 +1286,6 @@ packages:
'@types/express@4.17.21': '@types/express@4.17.21':
resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==}
'@types/geojson@7946.0.16':
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
'@types/glob@8.1.0': '@types/glob@8.1.0':
resolution: {integrity: sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==} resolution: {integrity: sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==}
@ -3215,10 +3209,6 @@ packages:
makeerror@1.0.12: makeerror@1.0.12:
resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}
mariadb@3.4.0:
resolution: {integrity: sha512-hdRPcAzs+MTxK5VG1thBW18gGTlw6yWBe9YnLB65GLo7q0fO5DWsgomIevV/pXSaWRmD3qi6ka4oSFRTExRiEQ==}
engines: {node: '>= 14'}
math-intrinsics@1.1.0: math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -5538,8 +5528,6 @@ snapshots:
'@types/qs': 6.9.18 '@types/qs': 6.9.18
'@types/serve-static': 1.15.7 '@types/serve-static': 1.15.7
'@types/geojson@7946.0.16': {}
'@types/glob@8.1.0': '@types/glob@8.1.0':
dependencies: dependencies:
'@types/minimatch': 5.1.2 '@types/minimatch': 5.1.2
@ -8128,14 +8116,6 @@ snapshots:
dependencies: dependencies:
tmpl: 1.0.5 tmpl: 1.0.5
mariadb@3.4.0:
dependencies:
'@types/geojson': 7946.0.16
'@types/node': 22.12.0
denque: 2.1.0
iconv-lite: 0.6.3
lru-cache: 10.4.3
math-intrinsics@1.1.0: {} math-intrinsics@1.1.0: {}
media-typer@0.3.0: {} media-typer@0.3.0: {}
@ -8746,7 +8726,7 @@ snapshots:
sequelize-pool@7.1.0: {} sequelize-pool@7.1.0: {}
sequelize@6.37.5(mariadb@3.4.0)(mysql2@3.12.0): sequelize@6.37.5(mysql2@3.12.0):
dependencies: dependencies:
'@types/debug': 4.1.12 '@types/debug': 4.1.12
'@types/validator': 13.12.2 '@types/validator': 13.12.2
@ -8765,7 +8745,6 @@ snapshots:
validator: 13.12.0 validator: 13.12.0
wkx: 0.5.0 wkx: 0.5.0
optionalDependencies: optionalDependencies:
mariadb: 3.4.0
mysql2: 3.12.0 mysql2: 3.12.0
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color