.
This commit is contained in:
parent
d3fac62898
commit
0171f51c56
@ -1,5 +1,7 @@
|
|||||||
import { Result } from "@common/helpers";
|
import { Result } from "@common/helpers";
|
||||||
import DineroFactory, { Currency, Dinero } from "dinero.js";
|
import DineroFactory, { Currency, Dinero } from "dinero.js";
|
||||||
|
import { Percentage } from "./percentage";
|
||||||
|
import { Quantity } from "./quantity";
|
||||||
import { ValueObject } from "./value-object";
|
import { ValueObject } from "./value-object";
|
||||||
|
|
||||||
const DEFAULT_SCALE = 2;
|
const DEFAULT_SCALE = 2;
|
||||||
@ -30,14 +32,17 @@ interface IMoneyValue {
|
|||||||
convertScale(newScale: number): MoneyValue;
|
convertScale(newScale: number): MoneyValue;
|
||||||
add(addend: MoneyValue): MoneyValue;
|
add(addend: MoneyValue): MoneyValue;
|
||||||
subtract(subtrahend: MoneyValue): MoneyValue;
|
subtract(subtrahend: MoneyValue): MoneyValue;
|
||||||
multiply(multiplier: number): MoneyValue;
|
multiply(multiplier: number | Quantity, roundingMode?: RoundingMode): MoneyValue;
|
||||||
divide(divisor: number): MoneyValue;
|
divide(divisor: number, roundingMode?: RoundingMode): MoneyValue;
|
||||||
|
percentage(percentage: number, roundingMode?: RoundingMode): MoneyValue;
|
||||||
equalsTo(comparator: MoneyValue): boolean;
|
equalsTo(comparator: MoneyValue): boolean;
|
||||||
greaterThan(comparator: MoneyValue): boolean;
|
greaterThan(comparator: MoneyValue): boolean;
|
||||||
lessThan(comparator: MoneyValue): boolean;
|
lessThan(comparator: MoneyValue): boolean;
|
||||||
isZero(): boolean;
|
isZero(): boolean;
|
||||||
isPositive(): boolean;
|
isPositive(): boolean;
|
||||||
isNegative(): boolean;
|
isNegative(): boolean;
|
||||||
|
hasSameCurrency(comparator: MoneyValue): boolean;
|
||||||
|
hasSameAmount(comparator: MoneyValue): boolean;
|
||||||
format(locale: string): string;
|
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({
|
return new MoneyValue({
|
||||||
amount: this.dinero.multiply(multiplier).getAmount(),
|
amount: _newDinero.getAmount(),
|
||||||
scale: this.scale,
|
scale: _newDinero.getPrecision(),
|
||||||
currency_code: this.currency,
|
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({
|
return new MoneyValue({
|
||||||
amount: this.dinero.divide(divisor).getAmount(),
|
amount: _newDinero.getAmount(),
|
||||||
scale: this.scale,
|
scale: _newDinero.getPrecision(),
|
||||||
currency_code: this.currency,
|
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;
|
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 {
|
format(locale: string): string {
|
||||||
const amount = this.amount;
|
const amount = this.amount;
|
||||||
const currency = this.currency;
|
const currency = this.currency;
|
||||||
|
|||||||
@ -12,6 +12,13 @@ describe("UniqueID", () => {
|
|||||||
expect(result.data.toString()).toBe(id);
|
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", () => {
|
test("should fail to create UniqueID with an invalid UUID", () => {
|
||||||
const result = UniqueID.create("invalid-uuid");
|
const result = UniqueID.create("invalid-uuid");
|
||||||
|
|
||||||
|
|||||||
@ -19,8 +19,8 @@ export class UniqueID extends ValueObject<string> {
|
|||||||
: Result.fail(new Error(result.error.errors[0].message));
|
: Result.fail(new Error(result.error.errors[0].message));
|
||||||
}
|
}
|
||||||
|
|
||||||
static generate(): UniqueID {
|
static generate(): Result<UniqueID, never> {
|
||||||
return new UniqueID(uuidv4());
|
return Result.ok(new UniqueID(uuidv4()));
|
||||||
}
|
}
|
||||||
|
|
||||||
static validate(id: string) {
|
static validate(id: string) {
|
||||||
|
|||||||
@ -20,12 +20,4 @@ export abstract class ValueObject<T> {
|
|||||||
|
|
||||||
return shallowEqual(this.props, other.props);
|
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) : "";
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,7 +72,7 @@ export class CompanyMapper
|
|||||||
id: source.id.toString(),
|
id: source.id.toString(),
|
||||||
is_freelancer: source.isFreelancer,
|
is_freelancer: source.isFreelancer,
|
||||||
name: source.name,
|
name: source.name,
|
||||||
trade_name: source.tradeName.isSome() ? source.tradeName.getValue() : undefined,
|
trade_name: source.tradeName.getOrUndefined(),
|
||||||
tin: source.tin.toString(),
|
tin: source.tin.toString(),
|
||||||
|
|
||||||
street: source.address.street,
|
street: source.address.street,
|
||||||
@ -83,15 +83,15 @@ export class CompanyMapper
|
|||||||
|
|
||||||
email: source.email.toString(),
|
email: source.email.toString(),
|
||||||
phone: source.phone.toString(),
|
phone: source.phone.toString(),
|
||||||
fax: source.fax.isSome() ? source.fax.getValue()?.toString() : undefined,
|
fax: source.fax.isSome() ? source.fax.getOrUndefined()?.toString() : undefined,
|
||||||
website: source.website.isSome() ? source.website.getValue() : undefined,
|
website: source.website.getOrUndefined(),
|
||||||
|
|
||||||
legal_record: source.legalRecord,
|
legal_record: source.legalRecord,
|
||||||
default_tax: source.defaultTax,
|
default_tax: source.defaultTax,
|
||||||
status: source.isActive ? "active" : "inactive",
|
status: source.isActive ? "active" : "inactive",
|
||||||
lang_code: source.langCode,
|
lang_code: source.langCode,
|
||||||
currency_code: source.currencyCode,
|
currency_code: source.currencyCode,
|
||||||
logo: source.logo.isSome() ? source.logo.getValue() : undefined,
|
logo: source.logo.getOrUndefined(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ export const listCompaniesPresenter: IListCompaniesPresenter = {
|
|||||||
|
|
||||||
is_freelancer: ensureBoolean(company.isFreelancer),
|
is_freelancer: ensureBoolean(company.isFreelancer),
|
||||||
name: ensureString(company.name),
|
name: ensureString(company.name),
|
||||||
trade_name: ensureString(company.tradeName.getValue()),
|
trade_name: ensureString(company.tradeName.getOrUndefined()),
|
||||||
tin: ensureString(company.tin.toString()),
|
tin: ensureString(company.tin.toString()),
|
||||||
|
|
||||||
street: ensureString(company.address.street),
|
street: ensureString(company.address.street),
|
||||||
@ -24,8 +24,8 @@ export const listCompaniesPresenter: IListCompaniesPresenter = {
|
|||||||
|
|
||||||
email: ensureString(company.email.toString()),
|
email: ensureString(company.email.toString()),
|
||||||
phone: ensureString(company.phone.toString()),
|
phone: ensureString(company.phone.toString()),
|
||||||
fax: ensureString(company.fax.getValue()?.toString()),
|
fax: ensureString(company.fax.getOrUndefined()?.toString()),
|
||||||
website: ensureString(company.website.getValue()),
|
website: ensureString(company.website.getOrUndefined()),
|
||||||
|
|
||||||
legal_record: ensureString(company.legalRecord),
|
legal_record: ensureString(company.legalRecord),
|
||||||
|
|
||||||
@ -33,6 +33,6 @@ export const listCompaniesPresenter: IListCompaniesPresenter = {
|
|||||||
status: ensureString(company.isActive ? "active" : "inactive"),
|
status: ensureString(company.isActive ? "active" : "inactive"),
|
||||||
lang_code: ensureString(company.langCode),
|
lang_code: ensureString(company.langCode),
|
||||||
currency_code: ensureString(company.currencyCode),
|
currency_code: ensureString(company.currencyCode),
|
||||||
logo: ensureString(company.logo.getValue()),
|
logo: ensureString(company.logo.getOrUndefined()),
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from "./get-customer-invoice.use-case";
|
||||||
@ -11,7 +11,7 @@ export class ListCustomersUseCase {
|
|||||||
|
|
||||||
public execute(): Promise<Result<Collection<Customer>, Error>> {
|
public execute(): Promise<Result<Collection<Customer>, Error>> {
|
||||||
return this.transactionManager.complete((transaction) => {
|
return this.transactionManager.complete((transaction) => {
|
||||||
return this.customerService.findCustomers(transaction);
|
return this.customerService.findCustomer(transaction);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./customer-invoice-item";
|
||||||
|
export * from "./tax";
|
||||||
|
export * from "./tax-collection";
|
||||||
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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>>;
|
||||||
|
}
|
||||||
@ -1 +1,2 @@
|
|||||||
|
export * from "./customer-invoice-repository.interface";
|
||||||
export * from "./customer-repository.interface";
|
export * from "./customer-repository.interface";
|
||||||
|
|||||||
@ -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>>;
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,6 +3,6 @@ import { Collection, Result } from "@common/helpers";
|
|||||||
import { Customer } from "../aggregates";
|
import { Customer } from "../aggregates";
|
||||||
|
|
||||||
export interface ICustomerService {
|
export interface ICustomerService {
|
||||||
findCustomers(transaction?: any): Promise<Result<Collection<Customer>, Error>>;
|
findCustomer(transaction?: any): Promise<Result<Collection<Customer>, Error>>;
|
||||||
findCustomerById(userId: UniqueID, transaction?: any): Promise<Result<Customer>>;
|
findCustomerById(customerId: UniqueID, transaction?: any): Promise<Result<Customer>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { ICustomerService } from "./customer-service.interface";
|
|||||||
export class CustomerService implements ICustomerService {
|
export class CustomerService implements ICustomerService {
|
||||||
constructor(private readonly customerRepository: ICustomerRepository) {}
|
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);
|
const customersOrError = await this.customerRepository.findAll(transaction);
|
||||||
if (customersOrError.isFailure) {
|
if (customersOrError.isFailure) {
|
||||||
return Result.fail(customersOrError.error);
|
return Result.fail(customersOrError.error);
|
||||||
|
|||||||
@ -1,2 +1,4 @@
|
|||||||
|
export * from "./customer-invoice-service.interface";
|
||||||
|
export * from "./customer-invoice.service";
|
||||||
export * from "./customer-service.interface";
|
export * from "./customer-service.interface";
|
||||||
export * from "./customer.service";
|
export * from "./customer.service";
|
||||||
|
|||||||
@ -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 };
|
||||||
@ -73,7 +73,7 @@ export class CustomerMapper
|
|||||||
reference: source.reference,
|
reference: source.reference,
|
||||||
is_freelancer: source.isFreelancer,
|
is_freelancer: source.isFreelancer,
|
||||||
name: source.name,
|
name: source.name,
|
||||||
trade_name: source.tradeName.isSome() ? source.tradeName.getValue() : undefined,
|
trade_name: source.tradeName.getOrUndefined(),
|
||||||
tin: source.tin.toString(),
|
tin: source.tin.toString(),
|
||||||
|
|
||||||
street: source.address.street,
|
street: source.address.street,
|
||||||
@ -84,8 +84,8 @@ export class CustomerMapper
|
|||||||
|
|
||||||
email: source.email.toString(),
|
email: source.email.toString(),
|
||||||
phone: source.phone.toString(),
|
phone: source.phone.toString(),
|
||||||
fax: source.fax.isSome() ? source.fax.getValue()?.toString() : undefined,
|
fax: source.fax.isSome() ? source.fax.getOrUndefined()?.toString() : undefined,
|
||||||
website: source.website.isSome() ? source.website.getValue() : undefined,
|
website: source.website.getOrUndefined(),
|
||||||
|
|
||||||
legal_record: source.legalRecord,
|
legal_record: source.legalRecord,
|
||||||
default_tax: source.defaultTax,
|
default_tax: source.defaultTax,
|
||||||
|
|||||||
@ -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 };
|
||||||
@ -1,9 +1,17 @@
|
|||||||
import { ICustomerRepository } from "@contexts/customer-billing/domain";
|
import { ICustomerRepository } from "@contexts/customer-billing/domain";
|
||||||
|
import { ICustomerInvoiceRepository } from "@contexts/customer-billing/domain/";
|
||||||
import { customerRepository } from "./customer.repository";
|
import { customerRepository } from "./customer.repository";
|
||||||
|
|
||||||
export * from "./customer.model";
|
export * from "./customer.model";
|
||||||
export * from "./customer.repository";
|
export * from "./customer.repository";
|
||||||
|
|
||||||
|
export * from "./customer-invoice.model";
|
||||||
|
export * from "./customer-invoice.repository";
|
||||||
|
|
||||||
export const createCustomerRepository = (): ICustomerRepository => {
|
export const createCustomerRepository = (): ICustomerRepository => {
|
||||||
return customerRepository;
|
return customerRepository;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createCustomerInvoiceRepository = (): ICustomerInvoiceRepository => {
|
||||||
|
return customerRepository;
|
||||||
|
};
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -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);
|
||||||
|
};
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./get";
|
||||||
|
export * from "./list";
|
||||||
@ -1,2 +1,15 @@
|
|||||||
export * from "./list-customer-invoices.controller";
|
import { SequelizeTransactionManager } from "@common/infrastructure";
|
||||||
export * from "./list-customer-invoices.presenter";
|
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);
|
||||||
|
};
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Collection, ensureBoolean, ensureNumber, ensureString } from "@common/helpers";
|
import { Collection, ensureBoolean, ensureNumber, ensureString } from "@common/helpers";
|
||||||
|
|
||||||
|
import { CustomerInvoice } from "@contexts/customer-billing/domain";
|
||||||
import { IListCustomerInvoicesResponseDTO } from "../../../dto";
|
import { IListCustomerInvoicesResponseDTO } from "../../../dto";
|
||||||
|
|
||||||
export interface IListCustomerInvoicesPresenter {
|
export interface IListCustomerInvoicesPresenter {
|
||||||
|
|||||||
@ -25,3 +25,5 @@ export interface IListCustomerInvoicesResponseDTO {
|
|||||||
lang_code: string;
|
lang_code: string;
|
||||||
currency_code: string;
|
currency_code: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IGetCustomerInvoiceResponseDTO {}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const ListCustomerInvoicesSchema = z.object({});
|
export const ListCustomerInvoicesSchema = z.object({});
|
||||||
|
export const GetCustomerInvoiceSchema = z.object({});
|
||||||
|
|||||||
@ -1,6 +1,13 @@
|
|||||||
import { validateRequestDTO } from "@common/presentation";
|
import { validateRequestDTO } from "@common/presentation";
|
||||||
import { checkTabContext, checkUser } from "@contexts/auth/infraestructure";
|
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";
|
import { NextFunction, Request, Response, Router } from "express";
|
||||||
|
|
||||||
export const customerInvoicesRouter = (appRouter: Router) => {
|
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);
|
appRouter.use("/customer-invoices", routes);
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user