From 0171f51c560b317b0a038e407591410a055f4eab Mon Sep 17 00:00:00 2001 From: david Date: Tue, 25 Feb 2025 16:25:30 +0100 Subject: [PATCH] . --- .../domain/value-objects/money-value.ts | 50 ++++++-- .../domain/value-objects/unique-id.spec.ts | 7 ++ .../common/domain/value-objects/unique-id.ts | 4 +- .../domain/value-objects/value-object.ts | 8 -- .../infraestructure/mappers/company.mapper.ts | 8 +- .../list-companies.presenter.ts | 8 +- .../get-customer-invoice.use-case.ts | 18 +++ .../application/customer-invoices/index.ts | 1 + .../list-customers/list-customers.use-case.ts | 2 +- .../domain/entities/customer-invoice-item.ts | 107 +++++++++++++++++ .../customer-billing/domain/entities/index.ts | 3 + .../domain/entities/tax-collection.ts | 33 ++++++ .../customer-billing/domain/entities/tax.ts | 32 +++++ .../domain/events/customer-invoice-item.ts | 61 ---------- .../customer-invoice-repository.interface.ts | 8 ++ .../domain/repositories/index.ts | 1 + .../customer-invoice-service.interface.ts | 8 ++ .../services/customer-invoice.service.ts | 27 +++++ .../services/customer-service.interface.ts | 4 +- .../domain/services/customer.service.ts | 2 +- .../customer-billing/domain/services/index.ts | 2 + .../mappers/customer-invoice.mapper.ts | 109 ++++++++++++++++++ .../mappers/customer.mapper.ts | 6 +- .../sequelize/customer-invoice.repository.ts | 61 ++++++++++ .../infraestructure/sequelize/index.ts | 8 ++ .../get/get-customer-invoice.controller.ts | 44 +++++++ .../get/get-customer-invoice.presenter.ts | 12 ++ .../customer-invoices/get/index.ts | 16 +++ .../controllers/customer-invoices/index.ts | 2 + .../customer-invoices/list/index.ts | 17 ++- .../list/list-customer-invoices.presenter.ts | 1 + .../customer-invoices.response.dto.ts | 2 + .../customer-invoices.validation.dto.ts | 1 + .../src/routes/customer-invoices.routes.ts | 19 ++- 34 files changed, 593 insertions(+), 99 deletions(-) create mode 100644 apps/server/src/contexts/customer-billing/application/customer-invoices/get-customer-invoice.use-case.ts create mode 100644 apps/server/src/contexts/customer-billing/domain/entities/customer-invoice-item.ts create mode 100644 apps/server/src/contexts/customer-billing/domain/entities/index.ts create mode 100644 apps/server/src/contexts/customer-billing/domain/entities/tax-collection.ts create mode 100644 apps/server/src/contexts/customer-billing/domain/entities/tax.ts delete mode 100644 apps/server/src/contexts/customer-billing/domain/events/customer-invoice-item.ts create mode 100644 apps/server/src/contexts/customer-billing/domain/repositories/customer-invoice-repository.interface.ts create mode 100644 apps/server/src/contexts/customer-billing/domain/services/customer-invoice-service.interface.ts create mode 100644 apps/server/src/contexts/customer-billing/domain/services/customer-invoice.service.ts create mode 100644 apps/server/src/contexts/customer-billing/infraestructure/mappers/customer-invoice.mapper.ts create mode 100644 apps/server/src/contexts/customer-billing/infraestructure/sequelize/customer-invoice.repository.ts create mode 100644 apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/get/get-customer-invoice.controller.ts create mode 100644 apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/get/get-customer-invoice.presenter.ts create mode 100644 apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/get/index.ts create mode 100644 apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/index.ts diff --git a/apps/server/src/common/domain/value-objects/money-value.ts b/apps/server/src/common/domain/value-objects/money-value.ts index 430ff198..300bdb08 100644 --- a/apps/server/src/common/domain/value-objects/money-value.ts +++ b/apps/server/src/common/domain/value-objects/money-value.ts @@ -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 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 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; diff --git a/apps/server/src/common/domain/value-objects/unique-id.spec.ts b/apps/server/src/common/domain/value-objects/unique-id.spec.ts index f85f793b..c656b0aa 100644 --- a/apps/server/src/common/domain/value-objects/unique-id.spec.ts +++ b/apps/server/src/common/domain/value-objects/unique-id.spec.ts @@ -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"); diff --git a/apps/server/src/common/domain/value-objects/unique-id.ts b/apps/server/src/common/domain/value-objects/unique-id.ts index 34c24b13..74116189 100644 --- a/apps/server/src/common/domain/value-objects/unique-id.ts +++ b/apps/server/src/common/domain/value-objects/unique-id.ts @@ -19,8 +19,8 @@ export class UniqueID extends ValueObject { : Result.fail(new Error(result.error.errors[0].message)); } - static generate(): UniqueID { - return new UniqueID(uuidv4()); + static generate(): Result { + return Result.ok(new UniqueID(uuidv4())); } static validate(id: string) { diff --git a/apps/server/src/common/domain/value-objects/value-object.ts b/apps/server/src/common/domain/value-objects/value-object.ts index 3737f5b1..8804b617 100644 --- a/apps/server/src/common/domain/value-objects/value-object.ts +++ b/apps/server/src/common/domain/value-objects/value-object.ts @@ -20,12 +20,4 @@ export abstract class ValueObject { 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) : ""; - }*/ } diff --git a/apps/server/src/contexts/companies/infraestructure/mappers/company.mapper.ts b/apps/server/src/contexts/companies/infraestructure/mappers/company.mapper.ts index 3658cf2d..eb1c463f 100644 --- a/apps/server/src/contexts/companies/infraestructure/mappers/company.mapper.ts +++ b/apps/server/src/contexts/companies/infraestructure/mappers/company.mapper.ts @@ -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(), }); } } diff --git a/apps/server/src/contexts/companies/presentation/controllers/list-companies/list-companies.presenter.ts b/apps/server/src/contexts/companies/presentation/controllers/list-companies/list-companies.presenter.ts index 244e2ddb..7c70f667 100644 --- a/apps/server/src/contexts/companies/presentation/controllers/list-companies/list-companies.presenter.ts +++ b/apps/server/src/contexts/companies/presentation/controllers/list-companies/list-companies.presenter.ts @@ -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()), })), }; diff --git a/apps/server/src/contexts/customer-billing/application/customer-invoices/get-customer-invoice.use-case.ts b/apps/server/src/contexts/customer-billing/application/customer-invoices/get-customer-invoice.use-case.ts new file mode 100644 index 00000000..e9fe8f77 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/application/customer-invoices/get-customer-invoice.use-case.ts @@ -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> { + return this.transactionManager.complete((transaction) => { + return this.invoiceService.findCustomerInvoiceById(invoiceId, transaction); + }); + } +} diff --git a/apps/server/src/contexts/customer-billing/application/customer-invoices/index.ts b/apps/server/src/contexts/customer-billing/application/customer-invoices/index.ts index e69de29b..960446a8 100644 --- a/apps/server/src/contexts/customer-billing/application/customer-invoices/index.ts +++ b/apps/server/src/contexts/customer-billing/application/customer-invoices/index.ts @@ -0,0 +1 @@ +export * from "./get-customer-invoice.use-case"; diff --git a/apps/server/src/contexts/customer-billing/application/customers/list-customers/list-customers.use-case.ts b/apps/server/src/contexts/customer-billing/application/customers/list-customers/list-customers.use-case.ts index 66fec103..e15a4bf2 100644 --- a/apps/server/src/contexts/customer-billing/application/customers/list-customers/list-customers.use-case.ts +++ b/apps/server/src/contexts/customer-billing/application/customers/list-customers/list-customers.use-case.ts @@ -11,7 +11,7 @@ export class ListCustomersUseCase { public execute(): Promise, Error>> { return this.transactionManager.complete((transaction) => { - return this.customerService.findCustomers(transaction); + return this.customerService.findCustomer(transaction); }); } } diff --git a/apps/server/src/contexts/customer-billing/domain/entities/customer-invoice-item.ts b/apps/server/src/contexts/customer-billing/domain/entities/customer-invoice-item.ts new file mode 100644 index 00000000..de02f17f --- /dev/null +++ b/apps/server/src/contexts/customer-billing/domain/entities/customer-invoice-item.ts @@ -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; // Descripción del artículo o servicio + quantity: Maybe; // Cantidad de unidades + unitPrice: Maybe; // Precio unitario en la moneda de la factura + // subtotalPrice: MoneyValue; // Precio unitario * Cantidad + discount: Maybe; // % descuento + // totalPrice: MoneyValue; +} + +export interface ICustomerInvoiceItem { + description: Maybe; + quantity: Maybe; + unitPrice: Maybe; + subtotalPrice: Maybe; + discount: Maybe; + totalPrice: Maybe; + + isEmptyLine(): Boolean; +} + +export class CustomerInvoiceItem + extends DomainEntity + implements ICustomerInvoiceItem +{ + private readonly _subtotalPrice!: Maybe; + private readonly _totalPrice!: Maybe; + + static validate(props: ICustomerInvoiceItemProps) { + return Result.ok(props); + } + + static create( + props: ICustomerInvoiceItemProps, + id?: UniqueID + ): Result { + 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 { + 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 { + 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 { + return this.props.description; + } + + get quantity(): Maybe { + return this.props.quantity; + } + + get unitPrice(): Maybe { + return this.props.unitPrice; + } + + get subtotalPrice(): Maybe { + return this._subtotalPrice; + } + + get discount(): Maybe { + return this.props.discount; + } + + get totalPrice(): Maybe { + return this._totalPrice; + } +} diff --git a/apps/server/src/contexts/customer-billing/domain/entities/index.ts b/apps/server/src/contexts/customer-billing/domain/entities/index.ts new file mode 100644 index 00000000..11bac671 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/domain/entities/index.ts @@ -0,0 +1,3 @@ +export * from "./customer-invoice-item"; +export * from "./tax"; +export * from "./tax-collection"; diff --git a/apps/server/src/contexts/customer-billing/domain/entities/tax-collection.ts b/apps/server/src/contexts/customer-billing/domain/entities/tax-collection.ts new file mode 100644 index 00000000..ccb6050b --- /dev/null +++ b/apps/server/src/contexts/customer-billing/domain/entities/tax-collection.ts @@ -0,0 +1,33 @@ +import { Slug } from "@common/domain"; +import { Collection } from "@common/helpers"; +import { Tax } from "./tax"; + +export class TaxCollection extends Collection { + 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)); + } +} diff --git a/apps/server/src/contexts/customer-billing/domain/entities/tax.ts b/apps/server/src/contexts/customer-billing/domain/entities/tax.ts new file mode 100644 index 00000000..eff80751 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/domain/entities/tax.ts @@ -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 implements ITax { + static create(props: ITaxProps, id?: UniqueID): Result { + 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; + } +} diff --git a/apps/server/src/contexts/customer-billing/domain/events/customer-invoice-item.ts b/apps/server/src/contexts/customer-billing/domain/events/customer-invoice-item.ts deleted file mode 100644 index 753a5c29..00000000 --- a/apps/server/src/contexts/customer-billing/domain/events/customer-invoice-item.ts +++ /dev/null @@ -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; // 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; - quantity: Quantity; - unitPrice: MoneyValue; - subtotalPrice: MoneyValue; - discount: Percentage; - totalPrice: MoneyValue; -} - -export class CustomerInvoiceItem - extends DomainEntity - implements ICustomerInvoiceItem -{ - public static create( - props: ICustomerInvoiceItemProps, - id?: UniqueID - ): Result { - return Result.ok(new CustomerInvoiceItem(props, id)); - } - - get description(): Maybe { - 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())); - } -} diff --git a/apps/server/src/contexts/customer-billing/domain/repositories/customer-invoice-repository.interface.ts b/apps/server/src/contexts/customer-billing/domain/repositories/customer-invoice-repository.interface.ts new file mode 100644 index 00000000..4fde6c01 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/domain/repositories/customer-invoice-repository.interface.ts @@ -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, Error>>; + findById(id: UniqueID, transaction?: any): Promise>; +} diff --git a/apps/server/src/contexts/customer-billing/domain/repositories/index.ts b/apps/server/src/contexts/customer-billing/domain/repositories/index.ts index 2ae98271..f7ab9f55 100644 --- a/apps/server/src/contexts/customer-billing/domain/repositories/index.ts +++ b/apps/server/src/contexts/customer-billing/domain/repositories/index.ts @@ -1 +1,2 @@ +export * from "./customer-invoice-repository.interface"; export * from "./customer-repository.interface"; diff --git a/apps/server/src/contexts/customer-billing/domain/services/customer-invoice-service.interface.ts b/apps/server/src/contexts/customer-billing/domain/services/customer-invoice-service.interface.ts new file mode 100644 index 00000000..20ee38ca --- /dev/null +++ b/apps/server/src/contexts/customer-billing/domain/services/customer-invoice-service.interface.ts @@ -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, Error>>; + findCustomerInvoiceById(invoiceId: UniqueID, transaction?: any): Promise>; +} diff --git a/apps/server/src/contexts/customer-billing/domain/services/customer-invoice.service.ts b/apps/server/src/contexts/customer-billing/domain/services/customer-invoice.service.ts new file mode 100644 index 00000000..8ea2dbbb --- /dev/null +++ b/apps/server/src/contexts/customer-billing/domain/services/customer-invoice.service.ts @@ -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, 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> { + return await this.invoiceRepository.findById(invoiceId, transaction); + } +} diff --git a/apps/server/src/contexts/customer-billing/domain/services/customer-service.interface.ts b/apps/server/src/contexts/customer-billing/domain/services/customer-service.interface.ts index a47da4f6..73d19547 100644 --- a/apps/server/src/contexts/customer-billing/domain/services/customer-service.interface.ts +++ b/apps/server/src/contexts/customer-billing/domain/services/customer-service.interface.ts @@ -3,6 +3,6 @@ import { Collection, Result } from "@common/helpers"; import { Customer } from "../aggregates"; export interface ICustomerService { - findCustomers(transaction?: any): Promise, Error>>; - findCustomerById(userId: UniqueID, transaction?: any): Promise>; + findCustomer(transaction?: any): Promise, Error>>; + findCustomerById(customerId: UniqueID, transaction?: any): Promise>; } diff --git a/apps/server/src/contexts/customer-billing/domain/services/customer.service.ts b/apps/server/src/contexts/customer-billing/domain/services/customer.service.ts index 6beb6bd7..926f67a7 100644 --- a/apps/server/src/contexts/customer-billing/domain/services/customer.service.ts +++ b/apps/server/src/contexts/customer-billing/domain/services/customer.service.ts @@ -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, Error>> { + async findCustomer(transaction?: any): Promise, Error>> { const customersOrError = await this.customerRepository.findAll(transaction); if (customersOrError.isFailure) { return Result.fail(customersOrError.error); diff --git a/apps/server/src/contexts/customer-billing/domain/services/index.ts b/apps/server/src/contexts/customer-billing/domain/services/index.ts index fd8abcbb..04045c87 100644 --- a/apps/server/src/contexts/customer-billing/domain/services/index.ts +++ b/apps/server/src/contexts/customer-billing/domain/services/index.ts @@ -1,2 +1,4 @@ +export * from "./customer-invoice-service.interface"; +export * from "./customer-invoice.service"; export * from "./customer-service.interface"; export * from "./customer.service"; diff --git a/apps/server/src/contexts/customer-billing/infraestructure/mappers/customer-invoice.mapper.ts b/apps/server/src/contexts/customer-billing/infraestructure/mappers/customer-invoice.mapper.ts new file mode 100644 index 00000000..686d52b5 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/infraestructure/mappers/customer-invoice.mapper.ts @@ -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 + implements ICustomerInvoiceMapper +{ + public mapToDomain( + source: CustomerInvoiceModel, + params?: MapperParamsType + ): Result { + /*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 { + /*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 }; diff --git a/apps/server/src/contexts/customer-billing/infraestructure/mappers/customer.mapper.ts b/apps/server/src/contexts/customer-billing/infraestructure/mappers/customer.mapper.ts index f94d33c9..2778b7c4 100644 --- a/apps/server/src/contexts/customer-billing/infraestructure/mappers/customer.mapper.ts +++ b/apps/server/src/contexts/customer-billing/infraestructure/mappers/customer.mapper.ts @@ -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, diff --git a/apps/server/src/contexts/customer-billing/infraestructure/sequelize/customer-invoice.repository.ts b/apps/server/src/contexts/customer-billing/infraestructure/sequelize/customer-invoice.repository.ts new file mode 100644 index 00000000..53bbefd3 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/infraestructure/sequelize/customer-invoice.repository.ts @@ -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 + 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, 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> { + 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 }; diff --git a/apps/server/src/contexts/customer-billing/infraestructure/sequelize/index.ts b/apps/server/src/contexts/customer-billing/infraestructure/sequelize/index.ts index 38e74d82..8092c8b0 100644 --- a/apps/server/src/contexts/customer-billing/infraestructure/sequelize/index.ts +++ b/apps/server/src/contexts/customer-billing/infraestructure/sequelize/index.ts @@ -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; +}; diff --git a/apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/get/get-customer-invoice.controller.ts b/apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/get/get-customer-invoice.controller.ts new file mode 100644 index 00000000..9afe2197 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/get/get-customer-invoice.controller.ts @@ -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); + } +} diff --git a/apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/get/get-customer-invoice.presenter.ts b/apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/get/get-customer-invoice.presenter.ts new file mode 100644 index 00000000..27438f5f --- /dev/null +++ b/apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/get/get-customer-invoice.presenter.ts @@ -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; + }, +}; diff --git a/apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/get/index.ts b/apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/get/index.ts new file mode 100644 index 00000000..4d58fa07 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/get/index.ts @@ -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); +}; diff --git a/apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/index.ts b/apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/index.ts new file mode 100644 index 00000000..e04b56ea --- /dev/null +++ b/apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/index.ts @@ -0,0 +1,2 @@ +export * from "./get"; +export * from "./list"; diff --git a/apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/list/index.ts b/apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/list/index.ts index 1758ef67..98857d6a 100644 --- a/apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/list/index.ts +++ b/apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/list/index.ts @@ -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); +}; diff --git a/apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/list/list-customer-invoices.presenter.ts b/apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/list/list-customer-invoices.presenter.ts index fa95e929..a67bb67d 100644 --- a/apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/list/list-customer-invoices.presenter.ts +++ b/apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/list/list-customer-invoices.presenter.ts @@ -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 { diff --git a/apps/server/src/contexts/customer-billing/presentation/dto/customer-invoices/customer-invoices.response.dto.ts b/apps/server/src/contexts/customer-billing/presentation/dto/customer-invoices/customer-invoices.response.dto.ts index c19599c1..1f27e65b 100644 --- a/apps/server/src/contexts/customer-billing/presentation/dto/customer-invoices/customer-invoices.response.dto.ts +++ b/apps/server/src/contexts/customer-billing/presentation/dto/customer-invoices/customer-invoices.response.dto.ts @@ -25,3 +25,5 @@ export interface IListCustomerInvoicesResponseDTO { lang_code: string; currency_code: string; } + +export interface IGetCustomerInvoiceResponseDTO {} diff --git a/apps/server/src/contexts/customer-billing/presentation/dto/customer-invoices/customer-invoices.validation.dto.ts b/apps/server/src/contexts/customer-billing/presentation/dto/customer-invoices/customer-invoices.validation.dto.ts index a4bf02d4..e133753f 100644 --- a/apps/server/src/contexts/customer-billing/presentation/dto/customer-invoices/customer-invoices.validation.dto.ts +++ b/apps/server/src/contexts/customer-billing/presentation/dto/customer-invoices/customer-invoices.validation.dto.ts @@ -1,3 +1,4 @@ import { z } from "zod"; export const ListCustomerInvoicesSchema = z.object({}); +export const GetCustomerInvoiceSchema = z.object({}); diff --git a/apps/server/src/routes/customer-invoices.routes.ts b/apps/server/src/routes/customer-invoices.routes.ts index 3690e005..1f24f6f9 100644 --- a/apps/server/src/routes/customer-invoices.routes.ts +++ b/apps/server/src/routes/customer-invoices.routes.ts @@ -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); };