.
This commit is contained in:
parent
42987d688f
commit
2e397900e8
7
.vscode/launch.json
vendored
7
.vscode/launch.json
vendored
@ -14,7 +14,6 @@
|
||||
"name": "Launch Chrome localhost",
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"reAttach": true,
|
||||
"url": "http://localhost:5173",
|
||||
"webRoot": "${workspaceFolder}/client"
|
||||
},
|
||||
@ -30,10 +29,12 @@
|
||||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "SERVER: Attach to dev:debug",
|
||||
"name": "Attach to ts-node-dev",
|
||||
"port": 4321,
|
||||
"restart": true,
|
||||
"cwd": "${workspaceRoot}"
|
||||
"timeout": 10000,
|
||||
"sourceMaps": true,
|
||||
"resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"]
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@ -2,7 +2,6 @@ DB_HOST=localhost
|
||||
DB_USER=rodax
|
||||
DB_PASSWORD=rodax
|
||||
DB_NAME=uecko_erp
|
||||
DB_DIALECT=mariadb
|
||||
DB_PORT=3306
|
||||
|
||||
PORT=3002
|
||||
|
||||
@ -4,8 +4,8 @@
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "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:nodebug": "ts-node-dev -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",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"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",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -63,7 +64,6 @@
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"libphonenumber-js": "^1.11.20",
|
||||
"luxon": "^3.5.0",
|
||||
"mariadb": "^3.4.0",
|
||||
"module-alias": "^2.2.3",
|
||||
"mysql2": "^3.12.0",
|
||||
"passport": "^0.7.0",
|
||||
@ -75,7 +75,6 @@
|
||||
"sequelize": "^6.37.5",
|
||||
"shallow-equal-object": "^1.1.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"uuid": "^11.0.5",
|
||||
"winston": "^3.17.0",
|
||||
"winston-daily-rotate-file": "^5.0.0",
|
||||
|
||||
@ -11,14 +11,11 @@ const postalCodeSchema = z
|
||||
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 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 {
|
||||
street: string;
|
||||
@ -37,7 +34,7 @@ export class PostalAddress extends ValueObject<IPostalAddressProps> {
|
||||
street2: street2Schema,
|
||||
city: citySchema,
|
||||
postalCode: postalCodeSchema,
|
||||
state: provinceSchema,
|
||||
state: stateSchema,
|
||||
country: countrySchema,
|
||||
})
|
||||
.safeParse(values);
|
||||
@ -60,6 +57,20 @@ export class PostalAddress extends ValueObject<IPostalAddressProps> {
|
||||
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 {
|
||||
return this.props.street;
|
||||
}
|
||||
|
||||
@ -15,11 +15,11 @@ interface IDomainMapper<TModel extends Model, 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(
|
||||
source: Collection<TEntity>,
|
||||
params?: MapperParamsType
|
||||
): Result<TModelAttributes[], Error>;
|
||||
): TModelAttributes[];
|
||||
}
|
||||
|
||||
export interface ISequelizeMapper<
|
||||
@ -59,23 +59,13 @@ export abstract class SequelizeMapper<
|
||||
}
|
||||
}
|
||||
|
||||
public abstract mapToPersistence(
|
||||
source: TEntity,
|
||||
params?: MapperParamsType
|
||||
): Result<TModelAttributes, Error>;
|
||||
public abstract mapToPersistence(source: TEntity, params?: MapperParamsType): TModelAttributes;
|
||||
|
||||
public mapCollectionToPersistence(
|
||||
source: Collection<TEntity>,
|
||||
params?: MapperParamsType
|
||||
): Result<TModelAttributes[], Error> {
|
||||
try {
|
||||
const result = source.map(
|
||||
(value, index) => this.mapToPersistence(value, { index, ...params }).data
|
||||
);
|
||||
return Result.ok(result);
|
||||
} catch (error) {
|
||||
return Result.fail(error as Error);
|
||||
}
|
||||
): TModelAttributes[] {
|
||||
return source.map((value, index) => this.mapToPersistence(value, { index, ...params }));
|
||||
}
|
||||
|
||||
protected safeMap<T>(operation: () => T, key: string): Result<T, Error> {
|
||||
|
||||
@ -11,7 +11,7 @@ export const sequelize = new Sequelize(
|
||||
process.env.DB_PASSWORD as string, // password
|
||||
{
|
||||
host: process.env.DB_HOST as string,
|
||||
dialect: "mariadb",
|
||||
dialect: "mysql",
|
||||
port: parseInt(process.env.DB_PORT || "3306", 10),
|
||||
dialectOptions: {
|
||||
multipleStatements: true,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export * from "./list-accounts.use-case";
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -7,8 +7,11 @@ import {
|
||||
UniqueID,
|
||||
} from "@common/domain";
|
||||
import { Maybe, Result } from "@common/helpers";
|
||||
import { AccountStatus } from "../value-objects";
|
||||
|
||||
export interface IAccountProps {
|
||||
status: AccountStatus;
|
||||
|
||||
isFreelancer: boolean;
|
||||
name: string;
|
||||
tin: TINNumber;
|
||||
@ -17,7 +20,7 @@ export interface IAccountProps {
|
||||
phone: PhoneNumber;
|
||||
legalRecord: string;
|
||||
defaultTax: number;
|
||||
status: string;
|
||||
|
||||
langCode: string;
|
||||
currencyCode: string;
|
||||
|
||||
@ -29,6 +32,7 @@ export interface IAccountProps {
|
||||
|
||||
export interface IAccount {
|
||||
id: UniqueID;
|
||||
status: AccountStatus;
|
||||
name: string;
|
||||
tin: TINNumber;
|
||||
address: PostalAddress;
|
||||
@ -36,6 +40,7 @@ export interface IAccount {
|
||||
phone: PhoneNumber;
|
||||
legalRecord: string;
|
||||
defaultTax: number;
|
||||
|
||||
langCode: string;
|
||||
currencyCode: string;
|
||||
|
||||
@ -47,6 +52,9 @@ export interface IAccount {
|
||||
isAccount: boolean;
|
||||
isFreelancer: boolean;
|
||||
isActive: boolean;
|
||||
|
||||
activate(): boolean;
|
||||
deactivate(): boolean;
|
||||
}
|
||||
|
||||
export class Account extends AggregateRoot<IAccountProps> implements IAccount {
|
||||
@ -64,6 +72,55 @@ export class Account extends AggregateRoot<IAccountProps> implements IAccount {
|
||||
return Result.ok(account);
|
||||
}
|
||||
|
||||
static update(oldAccount: Account, data: Partial<IAccountProps>): Result<Account, Error> {
|
||||
const updatedPostalAddress = PostalAddress.update(oldAccount.address, data.address ?? {}).data;
|
||||
|
||||
return Account.create(
|
||||
{
|
||||
isFreelancer: data.isFreelancer ?? oldAccount.isFreelancer,
|
||||
|
||||
name: data.name ?? oldAccount.name,
|
||||
tin: data.tin ?? oldAccount.tin,
|
||||
|
||||
address: updatedPostalAddress,
|
||||
email: data.email ?? oldAccount.email,
|
||||
phone: data.phone ?? oldAccount.phone,
|
||||
|
||||
legalRecord: data.legalRecord ?? oldAccount.legalRecord,
|
||||
defaultTax: data.defaultTax ?? oldAccount.defaultTax,
|
||||
status: oldAccount.props.status,
|
||||
langCode: data.langCode ?? oldAccount.langCode,
|
||||
currencyCode: data.currencyCode ?? oldAccount.currencyCode,
|
||||
|
||||
tradeName: data.tradeName ?? oldAccount.tradeName,
|
||||
website: data.website ?? oldAccount.website,
|
||||
fax: data.fax ?? oldAccount.fax,
|
||||
logo: data.logo ?? oldAccount.logo,
|
||||
},
|
||||
oldAccount.id
|
||||
).getOrElse(this);
|
||||
}
|
||||
|
||||
activate() {
|
||||
if (!this.props.status.canTransitionTo("active")) {
|
||||
return false;
|
||||
}
|
||||
this.props.status = AccountStatus.createActive();
|
||||
return true;
|
||||
}
|
||||
|
||||
deactivate() {
|
||||
if (!this.props.status.canTransitionTo("inactive")) {
|
||||
return false;
|
||||
}
|
||||
this.props.status = AccountStatus.createInactive();
|
||||
return true;
|
||||
}
|
||||
|
||||
get status() {
|
||||
return this.props.status;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.props.name;
|
||||
}
|
||||
@ -125,6 +182,6 @@ export class Account extends AggregateRoot<IAccountProps> implements IAccount {
|
||||
}
|
||||
|
||||
get isActive(): boolean {
|
||||
return this.props.status === "active";
|
||||
return this.props.status.equals(AccountStatus.createActive());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
export * from "./aggregates";
|
||||
|
||||
export * from "./repositories";
|
||||
export * from "./services";
|
||||
export * from "./value-objects";
|
||||
|
||||
@ -3,7 +3,11 @@ import { Collection, Result } from "@common/helpers";
|
||||
import { Account } from "../aggregates";
|
||||
|
||||
export interface IAccountRepository {
|
||||
accountExists(id: UniqueID, transaction?: any): Promise<Result<boolean, Error>>;
|
||||
findAll(transaction?: any): Promise<Result<Collection<Account>, Error>>;
|
||||
findById(id: UniqueID, transaction?: any): Promise<Result<Account, Error>>;
|
||||
findByEmail(email: EmailAddress, transaction?: any): Promise<Result<Account, Error>>;
|
||||
|
||||
create(account: Account, transaction?: any): Promise<void>;
|
||||
update(account: Account, transaction?: any): Promise<void>;
|
||||
}
|
||||
@ -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>>;
|
||||
}
|
||||
@ -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");
|
||||
});
|
||||
});
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
@ -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>>;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./account-service.interface";
|
||||
export * from "./account.service";
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from "./account-status";
|
||||
@ -5,7 +5,7 @@ import {
|
||||
MapperParamsType,
|
||||
SequelizeMapper,
|
||||
} 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";
|
||||
|
||||
export interface IAccountMapper
|
||||
@ -17,6 +17,7 @@ export class AccountMapper
|
||||
{
|
||||
public mapToDomain(source: AccountModel, params?: MapperParamsType): Result<Account, Error> {
|
||||
const idOrError = UniqueID.create(source.id);
|
||||
const statusOrError = AccountStatus.create(source.status);
|
||||
const tinOrError = TINNumber.create(source.tin);
|
||||
const emailOrError = EmailAddress.create(source.email);
|
||||
const phoneOrError = PhoneNumber.create(source.phone);
|
||||
@ -31,6 +32,7 @@ export class AccountMapper
|
||||
|
||||
const result = Result.combine([
|
||||
idOrError,
|
||||
statusOrError,
|
||||
tinOrError,
|
||||
emailOrError,
|
||||
phoneOrError,
|
||||
@ -44,6 +46,7 @@ export class AccountMapper
|
||||
|
||||
return Account.create(
|
||||
{
|
||||
status: statusOrError.data,
|
||||
isFreelancer: source.is_freelancer,
|
||||
name: source.name,
|
||||
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(),
|
||||
legalRecord: source.legal_record,
|
||||
defaultTax: source.default_tax,
|
||||
status: source.status,
|
||||
langCode: source.lang_code,
|
||||
currencyCode: source.currency_code,
|
||||
logo: source.logo ? Maybe.some(source.logo) : Maybe.none(),
|
||||
@ -64,11 +66,8 @@ export class AccountMapper
|
||||
);
|
||||
}
|
||||
|
||||
public mapToPersistence(
|
||||
source: Account,
|
||||
params?: MapperParamsType
|
||||
): Result<AccountCreationAttributes, Error> {
|
||||
return Result.ok({
|
||||
public mapToPersistence(source: Account, params?: MapperParamsType): AccountCreationAttributes {
|
||||
return {
|
||||
id: source.id.toString(),
|
||||
is_freelancer: source.isFreelancer,
|
||||
name: source.name,
|
||||
@ -92,7 +91,7 @@ export class AccountMapper
|
||||
lang_code: source.langCode,
|
||||
currency_code: source.currencyCode,
|
||||
logo: source.logo.getOrUndefined(),
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -63,6 +63,7 @@ export default (sequelize: Sequelize) => {
|
||||
trade_name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
tin: {
|
||||
type: DataTypes.STRING,
|
||||
@ -104,10 +105,12 @@ export default (sequelize: Sequelize) => {
|
||||
fax: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
website: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
validate: {
|
||||
isUrl: true,
|
||||
},
|
||||
@ -126,6 +129,7 @@ export default (sequelize: Sequelize) => {
|
||||
logo: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
|
||||
lang_code: {
|
||||
|
||||
@ -2,7 +2,7 @@ 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 { IAccountRepository } from "@contexts/accounts/domain/repositories/account-repository.interface";
|
||||
import { Transaction } from "sequelize";
|
||||
import { accountMapper, IAccountMapper } from "../mappers/account.mapper";
|
||||
import { AccountModel } from "./account.model";
|
||||
@ -26,6 +26,16 @@ class AccountRepository extends SequelizeRepository<Account> implements IAccount
|
||||
this._mapper = mapper;
|
||||
}
|
||||
|
||||
async accountExists(id: UniqueID, transaction?: Transaction): Promise<Result<boolean, Error>> {
|
||||
try {
|
||||
const _account = await this._getById(AccountModel, id, {}, transaction);
|
||||
|
||||
return Result.ok(Boolean(id.equals(_account.id)));
|
||||
} catch (error: any) {
|
||||
return this._handleDatabaseError(error, this._customErrorMapper);
|
||||
}
|
||||
}
|
||||
|
||||
async findAll(transaction?: Transaction): Promise<Result<Collection<Account>, Error>> {
|
||||
try {
|
||||
const rawAccounts: any = await this._findAll(AccountModel, {}, transaction);
|
||||
@ -76,6 +86,16 @@ class AccountRepository extends SequelizeRepository<Account> implements IAccount
|
||||
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);
|
||||
|
||||
@ -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";
|
||||
|
||||
export * from "./account.model";
|
||||
|
||||
export * from "./account.repository";
|
||||
|
||||
export const createAccountRepository = (): IAccountRepository => {
|
||||
return accountRepository;
|
||||
};
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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()),
|
||||
}),
|
||||
};
|
||||
@ -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);
|
||||
};
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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()),
|
||||
}),
|
||||
};
|
||||
@ -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);
|
||||
};
|
||||
@ -1 +1,4 @@
|
||||
export * from "./create-account";
|
||||
export * from "./get-account";
|
||||
export * from "./list-accounts";
|
||||
export * from "./update-account";
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
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 { ListAccountsUseCase } from "@contexts/accounts/application";
|
||||
import { AccountService } from "@contexts/accounts/domain";
|
||||
import { accountRepository } from "@contexts/accounts/infraestructure";
|
||||
import { ListAccountsController } from "./list-accounts.controller";
|
||||
import { listAccountsPresenter } from "./list-accounts.presenter";
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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";
|
||||
|
||||
export class ListAccountsController extends ExpressController {
|
||||
|
||||
@ -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);
|
||||
};
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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()),
|
||||
}),
|
||||
};
|
||||
@ -1 +1,52 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@ -25,3 +25,90 @@ export interface IListAccountsResponseDTO {
|
||||
currency_code: 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;
|
||||
}
|
||||
|
||||
@ -1,3 +1,87 @@
|
||||
import { z } from "zod";
|
||||
|
||||
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(),
|
||||
});
|
||||
|
||||
@ -67,6 +67,7 @@ export default (sequelize: Sequelize) => {
|
||||
trade_name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
tin: {
|
||||
type: DataTypes.STRING,
|
||||
@ -108,10 +109,12 @@ export default (sequelize: Sequelize) => {
|
||||
fax: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
website: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
validate: {
|
||||
isUrl: true,
|
||||
},
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export * from "./get-customer-invoice.use-case";
|
||||
@ -1 +0,0 @@
|
||||
export * from "./list-customers";
|
||||
@ -1 +0,0 @@
|
||||
export * from "./list-customers.use-case";
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,2 +1,2 @@
|
||||
export * from "./customer-invoices";
|
||||
export * from "./customers";
|
||||
export * from "./get-customer-invoice.use-case";
|
||||
export * from "./list-customer-invoices-use-case";
|
||||
|
||||
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import { AggregateRoot, UniqueID, UtcDate } from "@common/domain";
|
||||
import { Result } from "@common/helpers";
|
||||
import { Maybe, Result } from "@common/helpers";
|
||||
import { Customer, CustomerInvoiceItem } from "../entities";
|
||||
import { InvoiceStatus } from "../value-objetcs";
|
||||
|
||||
@ -8,7 +8,7 @@ export interface ICustomerInvoiceProps {
|
||||
issueDate: UtcDate;
|
||||
invoiceNumber: string;
|
||||
invoiceType: string;
|
||||
invoiceCustomerReference: string;
|
||||
invoiceCustomerReference: Maybe<string>;
|
||||
|
||||
customer: Customer;
|
||||
items: CustomerInvoiceItem[];
|
||||
@ -20,7 +20,7 @@ export interface ICustomerInvoice {
|
||||
issueDate: UtcDate;
|
||||
invoiceNumber: string;
|
||||
invoiceType: string;
|
||||
invoiceCustomerReference: string;
|
||||
invoiceCustomerReference: Maybe<string>;
|
||||
|
||||
customer: Customer;
|
||||
items: CustomerInvoiceItem[];
|
||||
@ -63,7 +63,7 @@ export class CustomerInvoice
|
||||
return this.props.invoiceType;
|
||||
}
|
||||
|
||||
get invoiceCustomerReference(): string {
|
||||
get invoiceCustomerReference(): Maybe<string> {
|
||||
return this.props.invoiceCustomerReference;
|
||||
}
|
||||
|
||||
|
||||
@ -1,2 +1 @@
|
||||
export * from "./customer-invoice-repository.interface";
|
||||
export * from "./customer-repository.interface";
|
||||
|
||||
@ -1,4 +1,2 @@
|
||||
export * from "./customer-invoice-service.interface";
|
||||
export * from "./customer-invoice.service";
|
||||
export * from "./customer-service.interface";
|
||||
export * from "./customer.service";
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { PostalAddress, TINNumber, UniqueID, UtcDate } from "@common/domain";
|
||||
import { Result } from "@common/helpers";
|
||||
import { Maybe, Result } from "@common/helpers";
|
||||
import {
|
||||
ISequelizeMapper,
|
||||
MapperParamsType,
|
||||
@ -70,7 +70,7 @@ export class CustomerInvoiceMapper
|
||||
issueDate: issueDateOrError.data,
|
||||
invoiceNumber: source.invoice_number,
|
||||
invoiceType: source.invoice_type,
|
||||
invoiceCustomerReference: source.invoice_customer_reference,
|
||||
invoiceCustomerReference: Maybe.fromNullable(source.invoice_customer_reference),
|
||||
customer: customerOrError.data,
|
||||
items: [],
|
||||
},
|
||||
@ -89,7 +89,7 @@ export class CustomerInvoiceMapper
|
||||
issue_date: source.issueDate.toDateString(),
|
||||
invoice_number: source.invoiceNumber,
|
||||
invoice_type: source.invoiceType,
|
||||
invoice_customer_reference: source.invoiceCustomerReference,
|
||||
invoice_customer_reference: source.invoiceCustomerReference.getOrUndefined(),
|
||||
|
||||
lang_code: "es",
|
||||
currency_code: "EUR",
|
||||
|
||||
@ -56,6 +56,7 @@ export default (sequelize: Sequelize) => {
|
||||
id_article: {
|
||||
type: DataTypes.BIGINT().UNSIGNED,
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
position: {
|
||||
type: new DataTypes.MEDIUMINT(),
|
||||
@ -65,26 +66,32 @@ export default (sequelize: Sequelize) => {
|
||||
description: {
|
||||
type: new DataTypes.TEXT(),
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
quantity: {
|
||||
type: DataTypes.BIGINT(),
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
unit_price: {
|
||||
type: new DataTypes.BIGINT(),
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
subtotal_price: {
|
||||
type: new DataTypes.BIGINT(),
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
discount: {
|
||||
type: new DataTypes.SMALLINT(),
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
total_price: {
|
||||
type: new DataTypes.BIGINT(),
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -126,6 +126,7 @@ export default (sequelize: Sequelize) => {
|
||||
|
||||
customer_id: {
|
||||
type: new DataTypes.UUID(),
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
customer_name: {
|
||||
@ -146,6 +147,7 @@ export default (sequelize: Sequelize) => {
|
||||
customer_street2: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
|
||||
customer_city: {
|
||||
@ -168,56 +170,67 @@ export default (sequelize: Sequelize) => {
|
||||
subtotal_price: {
|
||||
type: new DataTypes.BIGINT(),
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
|
||||
discount: {
|
||||
type: new DataTypes.SMALLINT(),
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
|
||||
discount_price: {
|
||||
type: new DataTypes.BIGINT(),
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
|
||||
before_tax_price: {
|
||||
type: new DataTypes.BIGINT(),
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
|
||||
tax: {
|
||||
type: new DataTypes.SMALLINT(),
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
|
||||
tax_price: {
|
||||
type: new DataTypes.BIGINT(),
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
|
||||
total_price: {
|
||||
type: new DataTypes.BIGINT(),
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
|
||||
notes: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
|
||||
integrity_hash: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
comment: "Hash criptográfico para asegurar integridad",
|
||||
},
|
||||
previous_invoice_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
comment: "Referencia a la factura anterior (si aplica)",
|
||||
},
|
||||
signed_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
comment: "Fecha en que la factura fue firmada digitalmente",
|
||||
},
|
||||
},
|
||||
|
||||
@ -67,6 +67,7 @@ export default (sequelize: Sequelize) => {
|
||||
trade_name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
tin: {
|
||||
type: DataTypes.STRING,
|
||||
@ -108,10 +109,12 @@ export default (sequelize: Sequelize) => {
|
||||
fax: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
website: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
validate: {
|
||||
isUrl: true,
|
||||
},
|
||||
|
||||
@ -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 };
|
||||
@ -1,17 +1,15 @@
|
||||
import { ICustomerRepository } from "@contexts/customer-billing/domain";
|
||||
import { ICustomerInvoiceRepository } from "@contexts/customer-billing/domain/";
|
||||
import { customerRepository } from "./customer.repository";
|
||||
|
||||
export * from "./customer.model";
|
||||
export * from "./customer.repository";
|
||||
import { customerInvoiceRepository } from "./customer-invoice.repository";
|
||||
|
||||
export * from "./customer-invoice.model";
|
||||
export * from "./customer.model";
|
||||
|
||||
export * from "./customer-invoice.repository";
|
||||
|
||||
export const createCustomerRepository = (): ICustomerRepository => {
|
||||
/*export const createCustomerRepository = (): ICustomerRepository => {
|
||||
return customerRepository;
|
||||
};
|
||||
};*/
|
||||
|
||||
export const createCustomerInvoiceRepository = (): ICustomerInvoiceRepository => {
|
||||
return customerRepository;
|
||||
return customerInvoiceRepository;
|
||||
};
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { SequelizeTransactionManager } from "@common/infrastructure";
|
||||
import { CustomerInvoiceService } from "@contexts/customer-billing/domain";
|
||||
import { customerInvoiceRepository } from "@contexts/customer-billing/infraestructure";
|
||||
import { ListCustomerInvoicesUseCase } from "../../../../application";
|
||||
import { ListCustomerInvoicesController } from "./list-customer-invoices.controller";
|
||||
import { listCustomerInvoicesPresenter } from "./list-customer-invoices.presenter";
|
||||
|
||||
|
||||
@ -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 { IListCustomerInvoicesResponseDTO } from "../../../dto";
|
||||
@ -8,10 +8,10 @@ export interface IListCustomerInvoicesPresenter {
|
||||
}
|
||||
|
||||
export const listCustomerInvoicesPresenter: IListCustomerInvoicesPresenter = {
|
||||
toDTO: (customers: Collection<CustomerInvoice>): IListCustomerInvoicesResponseDTO[] =>
|
||||
customers.map((customer) => ({
|
||||
toDTO: (invoice: Collection<CustomerInvoice>): IListCustomerInvoicesResponseDTO[] =>
|
||||
invoice.map((customer) => ({
|
||||
id: ensureString(customer.id.toString()),
|
||||
reference: ensureString(customer.reference),
|
||||
/*reference: ensureString(customer.),
|
||||
|
||||
is_freelancer: ensureBoolean(customer.isFreelancer),
|
||||
name: ensureString(customer.name),
|
||||
@ -34,6 +34,6 @@ export const listCustomerInvoicesPresenter: IListCustomerInvoicesPresenter = {
|
||||
default_tax: ensureNumber(customer.defaultTax),
|
||||
status: ensureString(customer.isActive ? "active" : "inactive"),
|
||||
lang_code: ensureString(customer.langCode),
|
||||
currency_code: ensureString(customer.currencyCode),
|
||||
currency_code: ensureString(customer.currencyCode),*/
|
||||
})),
|
||||
};
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export * from "./list";
|
||||
@ -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);
|
||||
};
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
})),
|
||||
};
|
||||
@ -1,2 +1 @@
|
||||
export * from "./customer-invoices";
|
||||
export * from "./customers";
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
export interface IListCustomerInvoicesResponseDTO {
|
||||
id: string;
|
||||
reference: string;
|
||||
/*reference: string;
|
||||
|
||||
is_freelancer: boolean;
|
||||
name: string;
|
||||
@ -23,7 +23,7 @@ export interface IListCustomerInvoicesResponseDTO {
|
||||
default_tax: number;
|
||||
status: string;
|
||||
lang_code: string;
|
||||
currency_code: string;
|
||||
currency_code: string;*/
|
||||
}
|
||||
|
||||
export interface IGetCustomerInvoiceResponseDTO {}
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
export * from "./controllers/customers";
|
||||
export * from "./controllers";
|
||||
export * from "./dto";
|
||||
|
||||
@ -1,6 +1,16 @@
|
||||
import { validateRequestDTO } from "@common/presentation";
|
||||
import { ListAccountsSchema } from "@contexts/accounts/presentation";
|
||||
import { listAccountsController } from "@contexts/accounts/presentation/controllers/list-accounts";
|
||||
import {
|
||||
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 { 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);
|
||||
};
|
||||
|
||||
@ -65,9 +65,6 @@ importers:
|
||||
luxon:
|
||||
specifier: ^3.5.0
|
||||
version: 3.5.0
|
||||
mariadb:
|
||||
specifier: ^3.4.0
|
||||
version: 3.4.0
|
||||
module-alias:
|
||||
specifier: ^2.2.3
|
||||
version: 2.2.3
|
||||
@ -94,16 +91,13 @@ importers:
|
||||
version: 2.3.3
|
||||
sequelize:
|
||||
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:
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1
|
||||
ts-node:
|
||||
specifier: ^10.9.1
|
||||
version: 10.9.2(@types/node@22.12.0)(typescript@5.7.3)
|
||||
tsconfig-paths:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
uuid:
|
||||
specifier: ^11.0.5
|
||||
version: 11.0.5
|
||||
@ -201,6 +195,9 @@ importers:
|
||||
ts-node-dev:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0(@types/node@22.12.0)(typescript@5.7.3)
|
||||
tsconfig-paths:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
typescript:
|
||||
specifier: ^5.7.3
|
||||
version: 5.7.3
|
||||
@ -1289,9 +1286,6 @@ packages:
|
||||
'@types/express@4.17.21':
|
||||
resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==}
|
||||
|
||||
'@types/geojson@7946.0.16':
|
||||
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
|
||||
|
||||
'@types/glob@8.1.0':
|
||||
resolution: {integrity: sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==}
|
||||
|
||||
@ -3215,10 +3209,6 @@ packages:
|
||||
makeerror@1.0.12:
|
||||
resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}
|
||||
|
||||
mariadb@3.4.0:
|
||||
resolution: {integrity: sha512-hdRPcAzs+MTxK5VG1thBW18gGTlw6yWBe9YnLB65GLo7q0fO5DWsgomIevV/pXSaWRmD3qi6ka4oSFRTExRiEQ==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
math-intrinsics@1.1.0:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -5538,8 +5528,6 @@ snapshots:
|
||||
'@types/qs': 6.9.18
|
||||
'@types/serve-static': 1.15.7
|
||||
|
||||
'@types/geojson@7946.0.16': {}
|
||||
|
||||
'@types/glob@8.1.0':
|
||||
dependencies:
|
||||
'@types/minimatch': 5.1.2
|
||||
@ -8128,14 +8116,6 @@ snapshots:
|
||||
dependencies:
|
||||
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: {}
|
||||
|
||||
media-typer@0.3.0: {}
|
||||
@ -8746,7 +8726,7 @@ snapshots:
|
||||
|
||||
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:
|
||||
'@types/debug': 4.1.12
|
||||
'@types/validator': 13.12.2
|
||||
@ -8765,7 +8745,6 @@ snapshots:
|
||||
validator: 13.12.0
|
||||
wkx: 0.5.0
|
||||
optionalDependencies:
|
||||
mariadb: 3.4.0
|
||||
mysql2: 3.12.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
Loading…
Reference in New Issue
Block a user