This commit is contained in:
David Arranz 2025-02-25 16:25:30 +01:00
parent d3fac62898
commit 0171f51c56
34 changed files with 593 additions and 99 deletions

View File

@ -1,5 +1,7 @@
import { Result } from "@common/helpers";
import DineroFactory, { Currency, Dinero } from "dinero.js";
import { Percentage } from "./percentage";
import { Quantity } from "./quantity";
import { ValueObject } from "./value-object";
const DEFAULT_SCALE = 2;
@ -30,14 +32,17 @@ interface IMoneyValue {
convertScale(newScale: number): MoneyValue;
add(addend: MoneyValue): MoneyValue;
subtract(subtrahend: MoneyValue): MoneyValue;
multiply(multiplier: number): MoneyValue;
divide(divisor: number): MoneyValue;
multiply(multiplier: number | Quantity, roundingMode?: RoundingMode): MoneyValue;
divide(divisor: number, roundingMode?: RoundingMode): MoneyValue;
percentage(percentage: number, roundingMode?: RoundingMode): MoneyValue;
equalsTo(comparator: MoneyValue): boolean;
greaterThan(comparator: MoneyValue): boolean;
lessThan(comparator: MoneyValue): boolean;
isZero(): boolean;
isPositive(): boolean;
isNegative(): boolean;
hasSameCurrency(comparator: MoneyValue): boolean;
hasSameAmount(comparator: MoneyValue): boolean;
format(locale: string): string;
}
@ -101,19 +106,36 @@ export class MoneyValue extends ValueObject<IMoneyValueProps> implements IMoneyV
});
}
multiply(multiplier: number): MoneyValue {
multiply(multiplier: number | Quantity, roundingMode?: RoundingMode): MoneyValue {
const _multiplier = typeof multiplier === "number" ? multiplier : multiplier.toNumber();
const _newDinero = this.dinero.multiply(_multiplier, roundingMode);
return new MoneyValue({
amount: this.dinero.multiply(multiplier).getAmount(),
scale: this.scale,
currency_code: this.currency,
amount: _newDinero.getAmount(),
scale: _newDinero.getPrecision(),
currency_code: _newDinero.getCurrency(),
});
}
divide(divisor: number): MoneyValue {
divide(divisor: number | Quantity, roundingMode?: RoundingMode): MoneyValue {
const _divisor = typeof divisor === "number" ? divisor : divisor.toNumber();
const _newDinero = this.dinero.divide(_divisor, roundingMode);
return new MoneyValue({
amount: this.dinero.divide(divisor).getAmount(),
scale: this.scale,
currency_code: this.currency,
amount: _newDinero.getAmount(),
scale: _newDinero.getPrecision(),
currency_code: _newDinero.getCurrency(),
});
}
percentage(percentage: number | Percentage, roundingMode?: RoundingMode): MoneyValue {
const _percentage = typeof percentage === "number" ? percentage : percentage.toNumber();
const _newDinero = this.dinero.percentage(_percentage, roundingMode);
return new MoneyValue({
amount: _newDinero.getAmount(),
scale: _newDinero.getPrecision(),
currency_code: _newDinero.getCurrency(),
});
}
@ -141,6 +163,14 @@ export class MoneyValue extends ValueObject<IMoneyValueProps> implements IMoneyV
return this.amount < 0;
}
hasSameCurrency(comparator: MoneyValue): boolean {
return this.dinero.hasSameCurrency(comparator.dinero);
}
hasSameAmount(comparator: MoneyValue): boolean {
return this.dinero.hasSameAmount(comparator.dinero);
}
format(locale: string): string {
const amount = this.amount;
const currency = this.currency;

View File

@ -12,6 +12,13 @@ describe("UniqueID", () => {
expect(result.data.toString()).toBe(id);
});
test("should generate a UniqueID with a valid UUID", () => {
const result = UniqueID.generate();
expect(result.isSuccess).toBe(true);
expect(result.data.toString()).toBeTruthy();
});
test("should fail to create UniqueID with an invalid UUID", () => {
const result = UniqueID.create("invalid-uuid");

View File

@ -19,8 +19,8 @@ export class UniqueID extends ValueObject<string> {
: Result.fail(new Error(result.error.errors[0].message));
}
static generate(): UniqueID {
return new UniqueID(uuidv4());
static generate(): Result<UniqueID, never> {
return Result.ok(new UniqueID(uuidv4()));
}
static validate(id: string) {

View File

@ -20,12 +20,4 @@ export abstract class ValueObject<T> {
return shallowEqual(this.props, other.props);
}
/*isEmpty(): boolean {
return this.props === null || this.props === undefined;
}*/
/*toString(): string {
return this.props !== null && this.props !== undefined ? String(this.props) : "";
}*/
}

View File

@ -72,7 +72,7 @@ export class CompanyMapper
id: source.id.toString(),
is_freelancer: source.isFreelancer,
name: source.name,
trade_name: source.tradeName.isSome() ? source.tradeName.getValue() : undefined,
trade_name: source.tradeName.getOrUndefined(),
tin: source.tin.toString(),
street: source.address.street,
@ -83,15 +83,15 @@ export class CompanyMapper
email: source.email.toString(),
phone: source.phone.toString(),
fax: source.fax.isSome() ? source.fax.getValue()?.toString() : undefined,
website: source.website.isSome() ? source.website.getValue() : undefined,
fax: source.fax.isSome() ? source.fax.getOrUndefined()?.toString() : undefined,
website: source.website.getOrUndefined(),
legal_record: source.legalRecord,
default_tax: source.defaultTax,
status: source.isActive ? "active" : "inactive",
lang_code: source.langCode,
currency_code: source.currencyCode,
logo: source.logo.isSome() ? source.logo.getValue() : undefined,
logo: source.logo.getOrUndefined(),
});
}
}

View File

@ -13,7 +13,7 @@ export const listCompaniesPresenter: IListCompaniesPresenter = {
is_freelancer: ensureBoolean(company.isFreelancer),
name: ensureString(company.name),
trade_name: ensureString(company.tradeName.getValue()),
trade_name: ensureString(company.tradeName.getOrUndefined()),
tin: ensureString(company.tin.toString()),
street: ensureString(company.address.street),
@ -24,8 +24,8 @@ export const listCompaniesPresenter: IListCompaniesPresenter = {
email: ensureString(company.email.toString()),
phone: ensureString(company.phone.toString()),
fax: ensureString(company.fax.getValue()?.toString()),
website: ensureString(company.website.getValue()),
fax: ensureString(company.fax.getOrUndefined()?.toString()),
website: ensureString(company.website.getOrUndefined()),
legal_record: ensureString(company.legalRecord),
@ -33,6 +33,6 @@ export const listCompaniesPresenter: IListCompaniesPresenter = {
status: ensureString(company.isActive ? "active" : "inactive"),
lang_code: ensureString(company.langCode),
currency_code: ensureString(company.currencyCode),
logo: ensureString(company.logo.getValue()),
logo: ensureString(company.logo.getOrUndefined()),
})),
};

View File

@ -0,0 +1,18 @@
import { UniqueID } from "@common/domain";
import { Result } from "@common/helpers";
import { ITransactionManager } from "@common/infrastructure/database";
import { ICustomerInvoiceService } from "@contexts/customer-billing/domain";
import { CustomerInvoice } from "@contexts/customer-billing/domain/aggregates";
export class GetCustomerInvoiceUseCase {
constructor(
private readonly invoiceService: ICustomerInvoiceService,
private readonly transactionManager: ITransactionManager
) {}
public execute(invoiceId: UniqueID): Promise<Result<CustomerInvoice, Error>> {
return this.transactionManager.complete((transaction) => {
return this.invoiceService.findCustomerInvoiceById(invoiceId, transaction);
});
}
}

View File

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

View File

@ -11,7 +11,7 @@ export class ListCustomersUseCase {
public execute(): Promise<Result<Collection<Customer>, Error>> {
return this.transactionManager.complete((transaction) => {
return this.customerService.findCustomers(transaction);
return this.customerService.findCustomer(transaction);
});
}
}

View File

@ -0,0 +1,107 @@
import { DomainEntity, MoneyValue, Percentage, UniqueID } from "@common/domain";
import { Quantity } from "@common/domain/value-objects/quantity";
import { Maybe, Result } from "@common/helpers";
export interface ICustomerInvoiceItemProps {
description: Maybe<string>; // Descripción del artículo o servicio
quantity: Maybe<Quantity>; // Cantidad de unidades
unitPrice: Maybe<MoneyValue>; // Precio unitario en la moneda de la factura
// subtotalPrice: MoneyValue; // Precio unitario * Cantidad
discount: Maybe<Percentage>; // % descuento
// totalPrice: MoneyValue;
}
export interface ICustomerInvoiceItem {
description: Maybe<string>;
quantity: Maybe<Quantity>;
unitPrice: Maybe<MoneyValue>;
subtotalPrice: Maybe<MoneyValue>;
discount: Maybe<Percentage>;
totalPrice: Maybe<MoneyValue>;
isEmptyLine(): Boolean;
}
export class CustomerInvoiceItem
extends DomainEntity<ICustomerInvoiceItemProps>
implements ICustomerInvoiceItem
{
private readonly _subtotalPrice!: Maybe<MoneyValue>;
private readonly _totalPrice!: Maybe<MoneyValue>;
static validate(props: ICustomerInvoiceItemProps) {
return Result.ok(props);
}
static create(
props: ICustomerInvoiceItemProps,
id?: UniqueID
): Result<CustomerInvoiceItem, Error> {
const validation = CustomerInvoiceItem.validate(props);
if (!validation.isSuccess) {
Result.fail(new Error("Invalid invoice line data"));
}
return Result.ok(new CustomerInvoiceItem(props, id));
}
private constructor(props: ICustomerInvoiceItemProps, id?: UniqueID) {
super(props, id);
this._subtotalPrice = this.calculateSubtotal();
this._totalPrice = this.calculateTotal();
}
isEmptyLine(): boolean {
return this.quantity.isNone() && this.unitPrice.isNone() && this.discount.isNone();
}
calculateSubtotal(): Maybe<MoneyValue> {
if (this.quantity.isNone() || this.unitPrice.isNone()) {
return Maybe.None();
}
const _quantity = this.quantity.getOrUndefined()!;
const _unitPrice = this.unitPrice.getOrUndefined()!;
const _subtotal = _unitPrice.multiply(_quantity);
return Maybe.Some(_subtotal);
}
calculateTotal(): Maybe<MoneyValue> {
const subtotal = this.calculateSubtotal();
if (subtotal.isNone()) {
return Maybe.None();
}
const _subtotal = subtotal.getOrUndefined()!;
const _discount = this.discount.getOrUndefined()!;
const _total = _subtotal.subtract(_subtotal.percentage(_discount));
return Maybe.Some(_total);
}
get description(): Maybe<string> {
return this.props.description;
}
get quantity(): Maybe<Quantity> {
return this.props.quantity;
}
get unitPrice(): Maybe<MoneyValue> {
return this.props.unitPrice;
}
get subtotalPrice(): Maybe<MoneyValue> {
return this._subtotalPrice;
}
get discount(): Maybe<Percentage> {
return this.props.discount;
}
get totalPrice(): Maybe<MoneyValue> {
return this._totalPrice;
}
}

View File

@ -0,0 +1,3 @@
export * from "./customer-invoice-item";
export * from "./tax";
export * from "./tax-collection";

View File

@ -0,0 +1,33 @@
import { Slug } from "@common/domain";
import { Collection } from "@common/helpers";
import { Tax } from "./tax";
export class TaxCollection extends Collection<Tax> {
constructor(items: Tax[] = []) {
super(items);
}
/**
Agrega un impuesto a la colección garantizando que el slug sea único. */
add(tax: Tax): void {
if (this.exists(tax.slug)) {
throw new Error(`(El impuesto con slug "${tax.slug.toString()}" ya existe.`);
}
this.add(tax);
}
/**
Verifica si un slug ya existe en la colección. */
exists(slug: Slug): boolean {
return this.some((tax) => tax.slug.equals(slug));
}
/**
Encuentra un impuesto por su slug. */
findBySlug(slug: Slug): Tax | undefined {
return this.find((tax) => tax.slug.equals(slug));
}
}

View File

@ -0,0 +1,32 @@
import { DomainEntity, Percentage, Slug, UniqueID } from "@common/domain";
import { Result } from "@common/helpers";
interface ITaxProps {
slug: Slug;
name: string;
taxValue: Percentage;
}
interface ITax {
slug: Slug;
name: string;
taxValue: Percentage;
}
export class Tax extends DomainEntity<ITaxProps> implements ITax {
static create(props: ITaxProps, id?: UniqueID): Result<Tax, Error> {
return Result.ok(new Tax(props, id));
}
get slug(): Slug {
return this.props.slug;
}
get name(): string {
return this.props.name;
}
get taxValue(): Percentage {
return this.props.taxValue;
}
}

View File

@ -1,61 +0,0 @@
import { DomainEntity, MoneyValue, Percentage, UniqueID } from "@common/domain";
import { Quantity } from "@common/domain/value-objects/quantity";
import { Maybe, Result } from "@common/helpers";
export interface ICustomerInvoiceItemProps {
description: Maybe<string>; // Descripción del artículo o servicio
quantity: Quantity; // Cantidad de unidades
unitPrice: MoneyValue; // Precio unitario en la moneda de la factura
// subtotalPrice: MoneyValue; // Precio unitario * Cantidad
discount: Percentage; // % descuento
// totalPrice: MoneyValue;
}
export interface ICustomerInvoiceItem {
description: Maybe<string>;
quantity: Quantity;
unitPrice: MoneyValue;
subtotalPrice: MoneyValue;
discount: Percentage;
totalPrice: MoneyValue;
}
export class CustomerInvoiceItem
extends DomainEntity<ICustomerInvoiceItemProps>
implements ICustomerInvoiceItem
{
public static create(
props: ICustomerInvoiceItemProps,
id?: UniqueID
): Result<CustomerInvoiceItem, Error> {
return Result.ok(new CustomerInvoiceItem(props, id));
}
get description(): Maybe<string> {
return this.props.description;
}
get quantity(): Quantity {
return this.props.quantity;
}
get unitPrice(): MoneyValue {
return this.props.unitPrice;
}
get subtotalPrice(): MoneyValue {
return this.quantity.isNull() || this.unitPrice.isNull()
? MoneyValue.create({ amount: null, scale: 2 }).object
: this.unitPrice.multiply(this.quantity.toNumber());
}
get discount(): Percentage {
return this.props.discount;
}
get totalPrice(): MoneyValue {
return this.subtotalPrice.isNull()
? MoneyValue.create({ amount: null, scale: 2 }).object
: this.subtotalPrice.subtract(this.subtotalPrice.percentage(this.discount.toNumber()));
}
}

View File

@ -0,0 +1,8 @@
import { UniqueID } from "@common/domain";
import { Collection, Result } from "@common/helpers";
import { CustomerInvoice } from "../aggregates";
export interface ICustomerInvoiceRepository {
findAll(transaction?: any): Promise<Result<Collection<CustomerInvoice>, Error>>;
findById(id: UniqueID, transaction?: any): Promise<Result<CustomerInvoice, Error>>;
}

View File

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

View File

@ -0,0 +1,8 @@
import { UniqueID } from "@common/domain";
import { Collection, Result } from "@common/helpers";
import { CustomerInvoice } from "../aggregates";
export interface ICustomerInvoiceService {
findCustomerInvoices(transaction?: any): Promise<Result<Collection<CustomerInvoice>, Error>>;
findCustomerInvoiceById(invoiceId: UniqueID, transaction?: any): Promise<Result<CustomerInvoice>>;
}

View File

@ -0,0 +1,27 @@
import { UniqueID } from "@common/domain";
import { Collection, Result } from "@common/helpers";
import { CustomerInvoice } from "../aggregates";
import { ICustomerInvoiceRepository } from "../repositories";
import { ICustomerInvoiceService } from "./customer-invoice-service.interface";
export class CustomerInvoiceService implements ICustomerInvoiceService {
constructor(private readonly invoiceRepository: ICustomerInvoiceRepository) {}
async findCustomerInvoices(
transaction?: any
): Promise<Result<Collection<CustomerInvoice>, Error>> {
const invoicesOrError = await this.invoiceRepository.findAll(transaction);
if (invoicesOrError.isFailure) {
return Result.fail(invoicesOrError.error);
}
return Result.ok(invoicesOrError.data);
}
async findCustomerInvoiceById(
invoiceId: UniqueID,
transaction?: any
): Promise<Result<CustomerInvoice>> {
return await this.invoiceRepository.findById(invoiceId, transaction);
}
}

View File

@ -3,6 +3,6 @@ import { Collection, Result } from "@common/helpers";
import { Customer } from "../aggregates";
export interface ICustomerService {
findCustomers(transaction?: any): Promise<Result<Collection<Customer>, Error>>;
findCustomerById(userId: UniqueID, transaction?: any): Promise<Result<Customer>>;
findCustomer(transaction?: any): Promise<Result<Collection<Customer>, Error>>;
findCustomerById(customerId: UniqueID, transaction?: any): Promise<Result<Customer>>;
}

View File

@ -7,7 +7,7 @@ import { ICustomerService } from "./customer-service.interface";
export class CustomerService implements ICustomerService {
constructor(private readonly customerRepository: ICustomerRepository) {}
async findCustomers(transaction?: any): Promise<Result<Collection<Customer>, Error>> {
async findCustomer(transaction?: any): Promise<Result<Collection<Customer>, Error>> {
const customersOrError = await this.customerRepository.findAll(transaction);
if (customersOrError.isFailure) {
return Result.fail(customersOrError.error);

View File

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

View File

@ -0,0 +1,109 @@
import { Result } from "@common/helpers";
import {
ISequelizeMapper,
MapperParamsType,
SequelizeMapper,
} from "@common/infrastructure/sequelize/sequelize-mapper";
import { CustomerInvoice } from "@contexts/customer-billing/domain";
import {
CustomerInvoiceCreationAttributes,
CustomerInvoiceModel,
} from "../sequelize/customer-invoice.model";
export interface ICustomerInvoiceMapper
extends ISequelizeMapper<
CustomerInvoiceModel,
CustomerInvoiceCreationAttributes,
CustomerInvoice
> {}
export class CustomerInvoiceMapper
extends SequelizeMapper<CustomerInvoiceModel, CustomerInvoiceCreationAttributes, CustomerInvoice>
implements ICustomerInvoiceMapper
{
public mapToDomain(
source: CustomerInvoiceModel,
params?: MapperParamsType
): Result<CustomerInvoice, Error> {
/*const idOrError = UniqueID.create(source.id);
const tinOrError = TINNumber.create(source.tin);
const emailOrError = EmailAddress.create(source.email);
const phoneOrError = PhoneNumber.create(source.phone);
const faxOrError = PhoneNumber.createNullable(source.fax);
const postalAddressOrError = PostalAddress.create({
street: source.street,
city: source.city,
state: source.state,
postalCode: source.postal_code,
country: source.country,
});
const result = Result.combine([
idOrError,
tinOrError,
emailOrError,
phoneOrError,
faxOrError,
postalAddressOrError,
]);
if (result.isFailure) {
return Result.fail(result.error);
}
return Customer.create(
{
isFreelancer: source.is_freelancer,
reference: source.reference,
name: source.name,
tradeName: source.trade_name ? Maybe.Some(source.trade_name) : Maybe.None(),
tin: tinOrError.data,
address: postalAddressOrError.data,
email: emailOrError.data,
phone: phoneOrError.data,
fax: faxOrError.data,
website: source.website ? Maybe.Some(source.website) : Maybe.None(),
legalRecord: source.legal_record,
defaultTax: source.default_tax,
status: source.status,
langCode: source.lang_code,
currencyCode: source.currency_code,
},
idOrError.data
);*/
}
public mapToPersistence(
source: CustomerInvoice,
params?: MapperParamsType
): Result<CustomerInvoiceCreationAttributes, Error> {
/*return Result.ok({
id: source.id.toString(),
reference: source.reference,
is_freelancer: source.isFreelancer,
name: source.name,
trade_name: source.tradeName.isSome() ? source.tradeName.getValue() : undefined,
tin: source.tin.toString(),
street: source.address.street,
city: source.address.city,
state: source.address.state,
postal_code: source.address.postalCode,
country: source.address.country,
email: source.email.toString(),
phone: source.phone.toString(),
fax: source.fax.isSome() ? source.fax.getValue()?.toString() : undefined,
website: source.website.isSome() ? source.website.getValue() : undefined,
legal_record: source.legalRecord,
default_tax: source.defaultTax,
status: source.isActive ? "active" : "inactive",
lang_code: source.langCode,
currency_code: source.currencyCode,
});*/
}
}
const customerInvoiceMapper: CustomerInvoiceMapper = new CustomerInvoiceMapper();
export { customerInvoiceMapper };

View File

@ -73,7 +73,7 @@ export class CustomerMapper
reference: source.reference,
is_freelancer: source.isFreelancer,
name: source.name,
trade_name: source.tradeName.isSome() ? source.tradeName.getValue() : undefined,
trade_name: source.tradeName.getOrUndefined(),
tin: source.tin.toString(),
street: source.address.street,
@ -84,8 +84,8 @@ export class CustomerMapper
email: source.email.toString(),
phone: source.phone.toString(),
fax: source.fax.isSome() ? source.fax.getValue()?.toString() : undefined,
website: source.website.isSome() ? source.website.getValue() : undefined,
fax: source.fax.isSome() ? source.fax.getOrUndefined()?.toString() : undefined,
website: source.website.getOrUndefined(),
legal_record: source.legalRecord,
default_tax: source.defaultTax,

View File

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

View File

@ -1,9 +1,17 @@
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";
export * from "./customer-invoice.model";
export * from "./customer-invoice.repository";
export const createCustomerRepository = (): ICustomerRepository => {
return customerRepository;
};
export const createCustomerInvoiceRepository = (): ICustomerInvoiceRepository => {
return customerRepository;
};

View File

@ -0,0 +1,44 @@
import { UniqueID } from "@common/domain";
import { ExpressController } from "@common/presentation";
import { GetCustomerInvoiceUseCase } from "@contexts/customer-billing/application";
import { IGetCustomerInvoicePresenter } from "./get-customer-invoice.presenter";
export class GetCustomerInvoiceController extends ExpressController {
public constructor(
private readonly getCustomerInvoice: GetCustomerInvoiceUseCase,
private readonly presenter: IGetCustomerInvoicePresenter
) {
super();
}
protected async executeImpl() {
const { invoiceId } = this.req.params;
// Validar ID
const invoiceIdOrError = UniqueID.create(invoiceId);
if (invoiceIdOrError.isFailure) return this.invalidInputError("Invoice ID not valid");
const invoiceOrError = await this.getCustomerInvoice.execute(invoiceIdOrError.data);
if (invoiceOrError.isFailure) {
return this.handleError(invoiceOrError.error);
}
return this.ok(this.presenter.toDTO(invoiceOrError.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,12 @@
import { CustomerInvoice } from "@contexts/customer-billing/domain";
import { IGetCustomerInvoiceResponseDTO } from "@contexts/customer-billing/presentation/dto";
export interface IGetCustomerInvoicePresenter {
toDTO: (invoice: CustomerInvoice) => IGetCustomerInvoiceResponseDTO;
}
export const getCustomerInvoicesPresenter: IGetCustomerInvoicePresenter = {
toDTO: (invoice: CustomerInvoice): IGetCustomerInvoiceResponseDTO => {
return {} as IGetCustomerInvoiceResponseDTO;
},
};

View File

@ -0,0 +1,16 @@
import { SequelizeTransactionManager } from "@common/infrastructure";
import { GetCustomerInvoiceUseCase } from "@contexts/customer-billing/application/";
import { CustomerInvoiceService } from "@contexts/customer-billing/domain";
import { customerInvoiceRepository } from "@contexts/customer-billing/infraestructure";
import { GetCustomerInvoiceController } from "./get-customer-invoice.controller";
import { getCustomerInvoicesPresenter } from "./get-customer-invoice.presenter";
export const getCustomerInvoiceController = () => {
const transactionManager = new SequelizeTransactionManager();
const customerInvoiceService = new CustomerInvoiceService(customerInvoiceRepository);
const useCase = new GetCustomerInvoiceUseCase(customerInvoiceService, transactionManager);
const presenter = getCustomerInvoicesPresenter;
return new GetCustomerInvoiceController(useCase, presenter);
};

View File

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

View File

@ -1,2 +1,15 @@
export * from "./list-customer-invoices.controller";
export * from "./list-customer-invoices.presenter";
import { SequelizeTransactionManager } from "@common/infrastructure";
import { CustomerInvoiceService } from "@contexts/customer-billing/domain";
import { customerInvoiceRepository } from "@contexts/customer-billing/infraestructure";
import { ListCustomerInvoicesController } from "./list-customer-invoices.controller";
import { listCustomerInvoicesPresenter } from "./list-customer-invoices.presenter";
export const listCustomerInvoicesController = () => {
const transactionManager = new SequelizeTransactionManager();
const customerInvoiceService = new CustomerInvoiceService(customerInvoiceRepository);
const useCase = new ListCustomerInvoicesUseCase(customerInvoiceService, transactionManager);
const presenter = listCustomerInvoicesPresenter;
return new ListCustomerInvoicesController(useCase, presenter);
};

View File

@ -1,5 +1,6 @@
import { Collection, ensureBoolean, ensureNumber, ensureString } from "@common/helpers";
import { CustomerInvoice } from "@contexts/customer-billing/domain";
import { IListCustomerInvoicesResponseDTO } from "../../../dto";
export interface IListCustomerInvoicesPresenter {

View File

@ -25,3 +25,5 @@ export interface IListCustomerInvoicesResponseDTO {
lang_code: string;
currency_code: string;
}
export interface IGetCustomerInvoiceResponseDTO {}

View File

@ -1,3 +1,4 @@
import { z } from "zod";
export const ListCustomerInvoicesSchema = z.object({});
export const GetCustomerInvoiceSchema = z.object({});

View File

@ -1,6 +1,13 @@
import { validateRequestDTO } from "@common/presentation";
import { checkTabContext, checkUser } from "@contexts/auth/infraestructure";
import { ListCustomerInvoicesSchema } from "@contexts/customer-billing/presentation";
import {
GetCustomerInvoiceSchema,
ListCustomerInvoicesSchema,
} from "@contexts/customer-billing/presentation";
import {
getCustomerInvoiceController,
listCustomerInvoicesController,
} from "@contexts/customer-billing/presentation/controllers";
import { NextFunction, Request, Response, Router } from "express";
export const customerInvoicesRouter = (appRouter: Router) => {
@ -16,5 +23,15 @@ export const customerInvoicesRouter = (appRouter: Router) => {
}
);
routes.get(
"/:invoiceId",
validateRequestDTO(GetCustomerInvoiceSchema),
checkTabContext,
checkUser,
(req: Request, res: Response, next: NextFunction) => {
getCustomerInvoiceController().execute(req, res, next);
}
);
appRouter.use("/customer-invoices", routes);
};