diff --git a/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts b/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts index 442f2cb7..01100b8f 100644 --- a/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts +++ b/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts @@ -1,6 +1,13 @@ -import { AggregateRoot, UniqueID, UtcDate } from "@repo/rdx-ddd"; -import { Collection, Result } from "@repo/rdx-utils"; -import { CustomerInvoiceCustomer, CustomerInvoiceItem, CustomerInvoiceItems } from "../entities"; +import { + AggregateRoot, + CurrencyCode, + LanguageCode, + TextValue, + UniqueID, + UtcDate, +} from "@repo/rdx-ddd"; +import { Maybe, Result } from "@repo/rdx-utils"; +import { CustomerInvoiceItems } from "../entities"; import { CustomerInvoiceNumber, CustomerInvoiceSerie, @@ -8,20 +15,19 @@ import { } from "../value-objects"; export interface CustomerInvoiceProps { - invoiceNumber: CustomerInvoiceNumber; - invoiceSeries: CustomerInvoiceSerie; - + companyId: UniqueID; status: CustomerInvoiceStatus; + series: Maybe; + invoiceNumber: CustomerInvoiceNumber; issueDate: UtcDate; - operationDate: UtcDate; + operationDate: Maybe; + + notes: Maybe; //dueDate: UtcDate; // ? --> depende de la forma de pago //tax: Tax; // ? --> detalles? - currency: string; - - //language: Language; //purchareOrderNumber: string; //notes: Note; @@ -31,53 +37,27 @@ export interface CustomerInvoiceProps { //paymentInstructions: Note; //paymentTerms: string; - customer?: CustomerInvoiceCustomer; + languageCode: LanguageCode; + currencyCode: CurrencyCode; + + //customer?: CustomerInvoiceCustomer; items?: CustomerInvoiceItems; } -export interface ICustomerInvoice { - id: UniqueID; - invoiceNumber: CustomerInvoiceNumber; - invoiceSeries: CustomerInvoiceSerie; +export type CustomerInvoicePatchProps = Partial>; - status: CustomerInvoiceStatus; - - issueDate: UtcDate; - operationDate: UtcDate; - - //senderId: UniqueID; - - customer?: CustomerInvoiceCustomer; - - //dueDate - - //tax: Tax; - //language: Language; - currency: string; - - //purchareOrderNumber: string; - //notes: Note; - - //paymentInstructions: Note; - //paymentTerms: string; - - items: CustomerInvoiceItems; - - calculateSubtotal: () => MoneyValue; - calculateTaxTotal: () => MoneyValue; - calculateTotal: () => MoneyValue; -} - -export class CustomerInvoice - extends AggregateRoot - implements ICustomerInvoice -{ - private _items!: Collection; +export class CustomerInvoice extends AggregateRoot { + private _items!: CustomerInvoiceItems; //protected _status: CustomerInvoiceStatus; protected constructor(props: CustomerInvoiceProps, id?: UniqueID) { super(props, id); - this._items = props.items || CustomerInvoiceItems.create(); + this._items = + props.items || + CustomerInvoiceItems.create({ + languageCode: props.languageCode, + currencyCode: props.currencyCode, + }); } static create(props: CustomerInvoiceProps, id?: UniqueID): Result { @@ -94,50 +74,55 @@ export class CustomerInvoice return Result.ok(customerInvoice); } - get invoiceNumber() { + public update(partialInvoice: CustomerInvoicePatchProps): Result { + throw new Error("Not implemented"); + } + + public get companyId(): UniqueID { + return this.props.companyId; + } + + public get series(): Maybe { + return this.props.series; + } + + public get invoiceNumber() { return this.props.invoiceNumber; } - get invoiceSeries() { - return this.props.invoiceSeries; + public get issueDate(): UtcDate { + return this.props.issueDate; } - get issueDate() { - return this.props.issueDate; + public get operationDate(): Maybe { + return this.props.operationDate; + } + + public get notes(): Maybe { + return this.props.notes; + } + + public get languageCode(): LanguageCode { + return this.props.languageCode; + } + + public get currencyCode(): CurrencyCode { + return this.props.currencyCode; + } + + // Method to get the complete list of line items + get lineItems(): CustomerInvoiceItems { + return this._items; } /*get senderId(): UniqueID { return this.props.senderId; }*/ - get customer(): CustomerInvoiceCustomer | undefined { + /* get customer(): CustomerInvoiceCustomer | undefined { return this.props.customer; - } - - get operationDate() { - return this.props.operationDate; - } - - /*get language() { - return this.props.language; }*/ - get dueDate() { - return undefined; - } - - get tax() { - return undefined; - } - - get status() { - return this.props.status; - } - - get items() { - return this._items; - } - /*get purchareOrderNumber() { return this.props.purchareOrderNumber; } @@ -158,19 +143,7 @@ export class CustomerInvoice return this.props.shipTo; }*/ - get currency() { - return this.props.currency; - } - - /*get notes() { - return this.props.notes; - }*/ - - // Method to get the complete list of line items - /*get lineItems(): CustomerInvoiceLineItem[] { - return this._lineItems; - } - + /* addLineItem(lineItem: CustomerInvoiceLineItem, position?: number): void { if (position === undefined) { this._lineItems.push(lineItem); @@ -179,7 +152,7 @@ export class CustomerInvoice } }*/ - calculateSubtotal(): MoneyValue { + /*calculateSubtotal(): MoneyValue { const customerInvoiceSubtotal = MoneyValue.create({ amount: 0, currency_code: this.props.currency, @@ -189,10 +162,10 @@ export class CustomerInvoice return this._items.getAll().reduce((subtotal, item) => { return subtotal.add(item.calculateTotal()); }, customerInvoiceSubtotal); - } + }*/ // Method to calculate the total tax in the customerInvoice - calculateTaxTotal(): MoneyValue { + /*calculateTaxTotal(): MoneyValue { const taxTotal = MoneyValue.create({ amount: 0, currency_code: this.props.currency, @@ -200,10 +173,10 @@ export class CustomerInvoice }).data; return taxTotal; - } + }*/ // Method to calculate the total customerInvoice amount, including taxes - calculateTotal(): MoneyValue { + /*calculateTotal(): MoneyValue { return this.calculateSubtotal().add(this.calculateTaxTotal()); - } + }*/ } diff --git a/modules/customer-invoices/src/api/domain/entities/invoice-items/invoice-item.test.ts b/modules/customer-invoices/src/api/domain/entities/invoice-items/customer-invoice-item.test.ts similarity index 93% rename from modules/customer-invoices/src/api/domain/entities/invoice-items/invoice-item.test.ts rename to modules/customer-invoices/src/api/domain/entities/invoice-items/customer-invoice-item.test.ts index fc725e8c..7698626d 100644 --- a/modules/customer-invoices/src/api/domain/entities/invoice-items/invoice-item.test.ts +++ b/modules/customer-invoices/src/api/domain/entities/invoice-items/customer-invoice-item.test.ts @@ -1,4 +1,4 @@ -import { MoneyValue, Percentage, Quantity } from "@/core/common/domain"; +import { CurrencyCode, LanguageCode, MoneyValue, Percentage, Quantity } from "@repo/rdx-ddd"; import { CustomerInvoiceItemDescription } from "../../value-objects"; import { CustomerInvoiceItem } from "./customer-invoice-item"; @@ -9,6 +9,8 @@ describe("CustomerInvoiceItem", () => { quantity: Quantity.create({ amount: 200, scale: 2 }), unitPrice: MoneyValue.create(50), discount: Percentage.create(0), + languageCode: LanguageCode.create("es"), + currencyCode: CurrencyCode.create("EUR"), }; const result = CustomerInvoiceItem.create(props); diff --git a/modules/customer-invoices/src/api/domain/entities/invoice-items/customer-invoice-item.ts b/modules/customer-invoices/src/api/domain/entities/invoice-items/customer-invoice-item.ts new file mode 100644 index 00000000..79ee7ca1 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/entities/invoice-items/customer-invoice-item.ts @@ -0,0 +1,102 @@ +import { CurrencyCode, DomainEntity, LanguageCode, UniqueID } from "@repo/rdx-ddd"; +import { Maybe, Result } from "@repo/rdx-utils"; +import { + CustomerInvoiceItemDescription, + CustomerInvoiceItemDiscount, + CustomerInvoiceItemQuantity, + CustomerInvoiceItemSubtotalPrice, + CustomerInvoiceItemTotalPrice, + CustomerInvoiceItemUnitPrice, +} from "../../value-objects"; + +export interface CustomerInvoiceItemProps { + description: Maybe; + quantity: Maybe; // Cantidad de unidades + unitPrice: Maybe; // Precio unitario en la moneda de la factura + discount: Maybe; // % descuento + + languageCode: LanguageCode; + currencyCode: CurrencyCode; +} + +export class CustomerInvoiceItem extends DomainEntity { + private _subtotalPrice!: CustomerInvoiceItemSubtotalPrice; + private _totalPrice!: CustomerInvoiceItemTotalPrice; + + public static create( + props: CustomerInvoiceItemProps, + id?: UniqueID + ): Result { + const item = new CustomerInvoiceItem(props, id); + + // Reglas de negocio / validaciones + // ... + // ... + + // 🔹 Disparar evento de dominio "CustomerInvoiceItemCreatedEvent" + //const { customerInvoice } = props; + //user.addDomainEvent(new CustomerInvoiceAuthenticatedEvent(id, customerInvoice.toString())); + + return Result.ok(item); + } + + get description(): Maybe { + return this.props.description; + } + + get quantity(): Maybe { + return this.props.quantity; + } + + get unitPrice(): Maybe { + return this.props.unitPrice; + } + + get subtotalPrice(): CustomerInvoiceItemSubtotalPrice { + if (!this._subtotalPrice) { + this._subtotalPrice = this.calculateSubtotal(); + } + return this._subtotalPrice; + } + + get discount(): Maybe { + return this.props.discount; + } + + get totalPrice(): CustomerInvoiceItemTotalPrice { + if (!this._totalPrice) { + this._totalPrice = this.calculateTotal(); + } + return this._totalPrice; + } + + public get languageCode(): LanguageCode { + return this.props.languageCode; + } + + public get currencyCode(): CurrencyCode { + return this.props.currencyCode; + } + + getValue(): CustomerInvoiceItemProps { + return this.props; + } + + toPrimitive() { + return this.getValue(); + } + + calculateSubtotal(): CustomerInvoiceItemSubtotalPrice { + throw new Error("Not implemented"); + + /*const unitPrice = this.unitPrice.isSome() + ? this.unitPrice.unwrap() + : CustomerInvoiceItemUnitPrice.zero(); + return this.unitPrice.multiply(this.quantity.toNumber()); // Precio unitario * Cantidad*/ + } + + calculateTotal(): CustomerInvoiceItemTotalPrice { + throw new Error("Not implemented"); + //return this.subtotalPrice.subtract(this.subtotalPrice.percentage(this.discount.toNumber())); + } +} diff --git a/modules/customer-invoices/src/api/domain/entities/invoice-items/customer-invoice-items.ts b/modules/customer-invoices/src/api/domain/entities/invoice-items/customer-invoice-items.ts new file mode 100644 index 00000000..911cbaf9 --- /dev/null +++ b/modules/customer-invoices/src/api/domain/entities/invoice-items/customer-invoice-items.ts @@ -0,0 +1,25 @@ +import { CurrencyCode, LanguageCode } from "@repo/rdx-ddd"; +import { Collection } from "@repo/rdx-utils"; +import { CustomerInvoiceItem } from "./customer-invoice-item"; + +export interface CustomerInvoiceItemsProps { + items?: CustomerInvoiceItem[]; + languageCode: LanguageCode; + currencyCode: CurrencyCode; +} + +export class CustomerInvoiceItems extends Collection { + private _languageCode!: LanguageCode; + private _currencyCode!: CurrencyCode; + + constructor(props: CustomerInvoiceItemsProps) { + const { items, languageCode, currencyCode } = props; + super(items); + this._languageCode = languageCode; + this._currencyCode = currencyCode; + } + + public static create(props: CustomerInvoiceItemsProps): CustomerInvoiceItems { + return new CustomerInvoiceItems(props); + } +} diff --git a/modules/customer-invoices/src/api/domain/entities/invoice-items/index.ts b/modules/customer-invoices/src/api/domain/entities/invoice-items/index.ts index f95bd889..32b0f623 100644 --- a/modules/customer-invoices/src/api/domain/entities/invoice-items/index.ts +++ b/modules/customer-invoices/src/api/domain/entities/invoice-items/index.ts @@ -1,2 +1,2 @@ -export * from "./invoice-item"; -export * from "./invoice-items"; +export * from "./customer-invoice-item"; +export * from "./customer-invoice-items"; diff --git a/modules/customer-invoices/src/api/domain/entities/invoice-items/invoice-item.ts b/modules/customer-invoices/src/api/domain/entities/invoice-items/invoice-item.ts deleted file mode 100644 index 81a04d27..00000000 --- a/modules/customer-invoices/src/api/domain/entities/invoice-items/invoice-item.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { DomainEntity, UniqueID } from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; -import { - CustomerInvoiceItemDescription, - CustomerInvoiceItemDiscount, - CustomerInvoiceItemQuantity, - CustomerInvoiceItemSubtotalPrice, - CustomerInvoiceItemTotalPrice, - CustomerInvoiceItemUnitPrice, -} from "../../value-objects"; - -export interface ICustomerInvoiceItemProps { - description: CustomerInvoiceItemDescription; - quantity: CustomerInvoiceItemQuantity; // Cantidad de unidades - unitPrice: CustomerInvoiceItemUnitPrice; // Precio unitario en la moneda de la factura - //subtotalPrice?: MoneyValue; // Precio unitario * Cantidad - discount: CustomerInvoiceItemDiscount; // % descuento - //totalPrice?: MoneyValue; -} - -export interface ICustomerInvoiceItem { - id: UniqueID; - description: CustomerInvoiceItemDescription; - quantity: CustomerInvoiceItemQuantity; - unitPrice: CustomerInvoiceItemUnitPrice; - subtotalPrice: CustomerInvoiceItemSubtotalPrice; - discount: CustomerInvoiceItemDiscount; - totalPrice: CustomerInvoiceItemTotalPrice; -} - -export class CustomerInvoiceItem - extends DomainEntity - implements ICustomerInvoiceItem -{ - private _subtotalPrice!: CustomerInvoiceItemSubtotalPrice; - private _totalPrice!: CustomerInvoiceItemTotalPrice; - - public static create( - props: ICustomerInvoiceItemProps, - id?: UniqueID - ): Result { - const item = new CustomerInvoiceItem(props, id); - - // Reglas de negocio / validaciones - // ... - // ... - - // 🔹 Disparar evento de dominio "CustomerInvoiceItemCreatedEvent" - //const { customerInvoice } = props; - //user.addDomainEvent(new CustomerInvoiceAuthenticatedEvent(id, customerInvoice.toString())); - - return Result.ok(item); - } - - get description(): CustomerInvoiceItemDescription { - return this.props.description; - } - - get quantity(): CustomerInvoiceItemQuantity { - return this.props.quantity; - } - - get unitPrice(): CustomerInvoiceItemUnitPrice { - return this.props.unitPrice; - } - - get subtotalPrice(): CustomerInvoiceItemSubtotalPrice { - if (!this._subtotalPrice) { - this._subtotalPrice = this.calculateSubtotal(); - } - return this._subtotalPrice; - } - - get discount(): CustomerInvoiceItemDiscount { - return this.props.discount; - } - - get totalPrice(): CustomerInvoiceItemTotalPrice { - if (!this._totalPrice) { - this._totalPrice = this.calculateTotal(); - } - return this._totalPrice; - } - - getValue() { - return this.props; - } - - toPrimitive() { - return { - description: this.description.toPrimitive(), - quantity: this.quantity.toPrimitive(), - unit_price: this.unitPrice.toPrimitive(), - subtotal_price: this.subtotalPrice.toPrimitive(), - discount: this.discount.toPrimitive(), - total_price: this.totalPrice.toPrimitive(), - }; - } - - calculateSubtotal(): CustomerInvoiceItemSubtotalPrice { - return this.unitPrice.multiply(this.quantity.toNumber()); // Precio unitario * Cantidad - } - - calculateTotal(): CustomerInvoiceItemTotalPrice { - return this.subtotalPrice.subtract(this.subtotalPrice.percentage(this.discount.toNumber())); - } -} diff --git a/modules/customer-invoices/src/api/domain/entities/invoice-items/invoice-items.ts b/modules/customer-invoices/src/api/domain/entities/invoice-items/invoice-items.ts deleted file mode 100644 index 02ce2168..00000000 --- a/modules/customer-invoices/src/api/domain/entities/invoice-items/invoice-items.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Collection } from "@repo/rdx-utils"; -import { CustomerInvoiceItem } from "./customer-invoice-item"; - -export class CustomerInvoiceItems extends Collection { - public static create(items?: CustomerInvoiceItem[]): CustomerInvoiceItems { - return new CustomerInvoiceItems(items); - } -} diff --git a/modules/customer-invoices/src/api/domain/repositories/customer-invoice-repository.interface.ts b/modules/customer-invoices/src/api/domain/repositories/customer-invoice-repository.interface.ts index df230096..85aa5aec 100644 --- a/modules/customer-invoices/src/api/domain/repositories/customer-invoice-repository.interface.ts +++ b/modules/customer-invoices/src/api/domain/repositories/customer-invoice-repository.interface.ts @@ -3,12 +3,15 @@ import { UniqueID } from "@repo/rdx-ddd"; import { Collection, Result } from "@repo/rdx-utils"; import { CustomerInvoice } from "../aggregates"; +/** + * Interfaz del repositorio para el agregado `CustomerInvoice`. + * El escopado multitenant está representado por `companyId`. + */ export interface ICustomerInvoiceRepository { - existsById(id: UniqueID, transaction?: any): Promise>; - /** * * Persiste una nueva factura o actualiza una existente. + * Retorna el objeto actualizado tras la operación. * * @param invoice - El agregado a guardar. * @param transaction - Transacción activa para la operación. @@ -17,34 +20,55 @@ export interface ICustomerInvoiceRepository { save(invoice: CustomerInvoice, transaction: any): Promise>; /** - * - * Busca una factura por su identificador único. - * @param id - UUID de la factura. - * @param transaction - Transacción activa para la operación. - * @returns Result + * Comprueba si existe una factura con un `id` dentro de una `company`. */ - findById(id: UniqueID, transaction: any): Promise>; + existsByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction?: any + ): Promise>; + + /** + * Recupera una factura por su ID y companyId. + * Devuelve un `NotFoundError` si no se encuentra. + */ + getByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction?: any + ): Promise>; /** * - * Consulta facturas usando un objeto Criteria (filtros, orden, paginación). + * Consulta facturas dentro de una empresa usando un + * objeto Criteria (filtros, orden, paginación). + * El resultado está encapsulado en un objeto `Collection`. + * + * @param companyId - ID de la empresa. * @param criteria - Criterios de búsqueda. * @param transaction - Transacción activa para la operación. * @returns Result * * @see Criteria */ - findByCriteria( + findByCriteriaInCompany( + companyId: UniqueID, criteria: Criteria, transaction: any ): Promise, Error>>; /** * - * Elimina o marca como eliminada una factura. + * Elimina o marca como eliminada una factura dentro de una empresa. + * + * @param companyId - ID de la empresa. * @param id - UUID de la factura a eliminar. * @param transaction - Transacción activa para la operación. * @returns Result */ - deleteById(id: UniqueID, transaction: any): Promise>; + deleteByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction: any + ): Promise>; } diff --git a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-address-type.ts b/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-address-type.ts index c5d83463..3a2225c2 100644 --- a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-address-type.ts +++ b/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-address-type.ts @@ -14,10 +14,10 @@ export class CustomerInvoiceAddressType extends ValueObject { - if (!this.ALLOWED_TYPES.includes(value)) { + if (!CustomerInvoiceAddressType.ALLOWED_TYPES.includes(value)) { return Result.fail( new Error( - `Invalid address type: ${value}. Allowed types are: ${this.ALLOWED_TYPES.join(", ")}` + `Invalid address type: ${value}. Allowed types are: ${CustomerInvoiceAddressType.ALLOWED_TYPES.join(", ")}` ) ); } diff --git a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-item-description.ts b/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-item-description.ts index 5f1131c2..bfc4ac33 100644 --- a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-item-description.ts +++ b/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-item-description.ts @@ -3,11 +3,11 @@ import { ValueObject } from "@repo/rdx-ddd"; import { Maybe, Result } from "@repo/rdx-utils"; import * as z from "zod/v4"; -interface ICustomerInvoiceItemDescriptionProps { +interface CustomerInvoiceItemDescriptionProps { value: string; } -export class CustomerInvoiceItemDescription extends ValueObject { +export class CustomerInvoiceItemDescription extends ValueObject { private static readonly MAX_LENGTH = 255; private static readonly FIELD = "invoiceItemDescription"; private static readonly ERROR_CODE = "INVALID_INVOICE_ITEM_DESCRIPTION"; @@ -23,10 +23,10 @@ export class CustomerInvoiceItemDescription extends ValueObject("contacts"); const { logger } = params; customerInvoicesRouter(params); - logger.info("🚀 CustomerInvoices module initialized", { label: "customer-invoices" }); + logger.info("🚀 CustomerInvoices module initialized", { label: this.name }); }, async registerDependencies(params) { const { database, logger } = params; logger.info("🚀 CustomerInvoices module dependencies registered", { - label: "customer-invoices", + label: this.name, }); return { models, diff --git a/modules/customer-invoices/src/api/infrastructure/dependencies.ts b/modules/customer-invoices/src/api/infrastructure/dependencies.ts index 0949b729..5693e5b3 100644 --- a/modules/customer-invoices/src/api/infrastructure/dependencies.ts +++ b/modules/customer-invoices/src/api/infrastructure/dependencies.ts @@ -8,6 +8,8 @@ import { GetCustomerInvoiceUseCase, ListCustomerInvoicesAssembler, ListCustomerInvoicesUseCase, + UpdateCustomerInvoiceAssembler, + UpdateCustomerInvoiceUseCase, } from "../application"; import { CustomerInvoiceService, ICustomerInvoiceService } from "../domain"; import { CustomerInvoiceMapper } from "./mappers"; @@ -22,13 +24,13 @@ type InvoiceDeps = { list: ListCustomerInvoicesAssembler; get: GetCustomerInvoiceAssembler; create: CreateCustomerInvoicesAssembler; - //update: UpdateCustomerInvoiceAssembler; + update: UpdateCustomerInvoiceAssembler; }; build: { list: () => ListCustomerInvoicesUseCase; get: () => GetCustomerInvoiceUseCase; create: () => CreateCustomerInvoiceUseCase; - //update: () => UpdateCustomerInvoiceUseCase; + update: () => UpdateCustomerInvoiceUseCase; delete: () => DeleteCustomerInvoiceUseCase; }; presenters: { @@ -54,7 +56,7 @@ export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps { list: new ListCustomerInvoicesAssembler(), // transforma domain → ListDTO get: new GetCustomerInvoiceAssembler(), // transforma domain → DetailDTO create: new CreateCustomerInvoicesAssembler(), // transforma domain → CreatedDTO - //update: new UpdateCustomerInvoiceAssembler(), // transforma domain -> UpdateDTO + update: new UpdateCustomerInvoiceAssembler(), // transforma domain -> UpdateDTO }; } @@ -70,8 +72,8 @@ export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps { get: () => new GetCustomerInvoiceUseCase(_service!, transactionManager!, _assemblers!.get), create: () => new CreateCustomerInvoiceUseCase(_service!, transactionManager!, _assemblers!.create), - /*update: () => - new UpdateCustomerInvoiceUseCase(_service!, transactionManager!, _assemblers!.update),*/ + update: () => + new UpdateCustomerInvoiceUseCase(_service!, transactionManager!, _assemblers!.update), delete: () => new DeleteCustomerInvoiceUseCase(_service!, transactionManager!), }, presenters: { diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/customer-invoice.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/customer-invoice.mapper.ts index 50859ade..5161b84a 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/customer-invoice.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/customer-invoice.mapper.ts @@ -1,9 +1,17 @@ -import { ISequelizeMapper, MapperParamsType, SequelizeMapper } from "@erp/core/api"; -import { UniqueID, UtcDate } from "@repo/rdx-ddd"; +import { + ISequelizeMapper, + MapperParamsType, + SequelizeMapper, + ValidationErrorCollection, + ValidationErrorDetail, + extractOrPushError, +} from "@erp/core/api"; +import { CurrencyCode, LanguageCode, UniqueID, UtcDate, maybeFromNullableVO } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { CustomerInvoice, CustomerInvoiceNumber, + CustomerInvoiceProps, CustomerInvoiceSerie, CustomerInvoiceStatus, } from "../../domain"; @@ -32,50 +40,75 @@ export class CustomerInvoiceMapper source: CustomerInvoiceModel, params?: MapperParamsType ): Result { - const idOrError = UniqueID.create(source.id); - const statusOrError = CustomerInvoiceStatus.create(source.invoice_status); - const customerInvoiceSeriesOrError = CustomerInvoiceSerie.create(source.invoice_series); - const customerInvoiceNumberOrError = CustomerInvoiceNumber.create(source.invoice_number); - const issueDateOrError = UtcDate.createFromISO(source.issue_date); - const operationDateOrError = UtcDate.createFromISO(source.operation_date); + const errors: ValidationErrorDetail[] = []; - const result = Result.combine([ - idOrError, - statusOrError, - customerInvoiceSeriesOrError, - customerInvoiceNumberOrError, - issueDateOrError, - operationDateOrError, - ]); + const invoiceId = extractOrPushError(UniqueID.create(source.id), "id", errors); + const companyId = extractOrPushError(UniqueID.create(source.company_id), "company_id", errors); - if (result.isFailure) { - return Result.fail(result.error); + const status = extractOrPushError( + CustomerInvoiceStatus.create(source.status), + "status", + errors + ); + const series = extractOrPushError(CustomerInvoiceSerie.create(source.series), "series", errors); + const invoiceNumber = extractOrPushError( + CustomerInvoiceNumber.create(source.invoice_number), + "invoice_number", + errors + ); + const issueDate = extractOrPushError( + UtcDate.createFromISO(source.issue_date), + "issue_date", + errors + ); + + const operationDate = extractOrPushError( + maybeFromNullableVO(source.operation_date, (value) => UtcDate.createFromISO(value)), + "operation_date", + errors + ); + + const languageCode = extractOrPushError( + LanguageCode.create(source.language_code), + "language_code", + errors + ); + + const currencyCode = extractOrPushError( + CurrencyCode.create(source.currency_code), + "currency_code", + errors + ); + + if (errors.length > 0) { + return Result.fail( + new ValidationErrorCollection("Customer invoice props mapping failed", errors) + ); } // Mapear los items de la factura - const itemsOrErrors = this.customerInvoiceItemMapper.mapArrayToDomain(source.items, { + /*const itemsOrErrors = this.customerInvoiceItemMapper.mapArrayToDomain(source.items, { sourceParent: source, ...params, }); if (itemsOrErrors.isFailure) { return Result.fail(itemsOrErrors.error); - } + }*/ - const customerInvoiceCurrency = source.invoice_currency || "EUR"; + const invoiceProps: CustomerInvoiceProps = { + status: status!, + series: series!, + invoiceNumber: invoiceNumber!, + issueDate: issueDate!, + operationDate: operationDate!, - return CustomerInvoice.create( - { - status: statusOrError.data, - invoiceSeries: customerInvoiceSeriesOrError.data, - invoiceNumber: customerInvoiceNumberOrError.data, - issueDate: issueDateOrError.data, - operationDate: operationDateOrError.data, - currency: customerInvoiceCurrency, - items: itemsOrErrors.data, - }, - idOrError.data - ); + languageCode: languageCode!, + currencyCode: currencyCode!, + //items: itemsOrErrors.data, + }; + + return CustomerInvoice.create(invoiceProps, invoiceId); } public mapToPersistence( diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice-item.model.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice-item.model.ts index ea553dc7..8ac37181 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice-item.model.ts +++ b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice-item.model.ts @@ -1,5 +1,4 @@ import { - CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, @@ -21,26 +20,26 @@ export class CustomerInvoiceItemModel extends Model< declare item_id: string; declare invoice_id: string; - declare parent_id: CreationOptional; + declare parent_id: string; declare position: number; declare item_type: string; - declare description: CreationOptional; + declare description: string; - declare quantity_amount: CreationOptional; - declare quantity_scale: CreationOptional; + declare quantity_amount: number; + declare quantity_scale: number; - declare unit_price_amount: CreationOptional; - declare unit_price_scale: CreationOptional; + declare unit_price_amount: number; + declare unit_price_scale: number; - declare subtotal_amount: CreationOptional; - declare subtotal_scale: CreationOptional; + declare subtotal_amount: number; + declare subtotal_scale: number; - declare discount_amount: CreationOptional; - declare discount_scale: CreationOptional; + declare discount_amount: number; + declare discount_scale: number; - declare total_amount: CreationOptional; - declare total_scale: CreationOptional; + declare total_amount: number; + declare total_scale: number; declare invoice: NonAttribute; @@ -65,14 +64,14 @@ export default (database: Sequelize) => { }, invoice_id: { type: new DataTypes.UUID(), - primaryKey: true, + allowNull: false, }, parent_id: { type: new DataTypes.UUID(), allowNull: true, // Puede ser nulo para elementos de nivel superior }, position: { - type: new DataTypes.MEDIUMINT(), + type: new DataTypes.MEDIUMINT().UNSIGNED, autoIncrement: false, allowNull: false, }, @@ -84,6 +83,7 @@ export default (database: Sequelize) => { description: { type: new DataTypes.TEXT(), allowNull: true, + defaultValue: null, }, quantity_amount: { @@ -160,9 +160,17 @@ export default (database: Sequelize) => { }, { sequelize: database, - underscored: true, tableName: "customer_invoice_items", + underscored: true, + + indexes: [ + { name: "invoice_idx", fields: ["invoice_id"], unique: false }, + { name: "parent_idx", fields: ["parent_id"], unique: false }, + ], + + whereMergeStrategy: "and", // <- cómo tratar el merge de un scope + defaultScope: {}, scopes: {}, diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.model.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.model.ts index 9da987fa..cb5f9fed 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.model.ts +++ b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.model.ts @@ -1,5 +1,4 @@ import { - CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, @@ -24,22 +23,23 @@ export class CustomerInvoiceModel extends Model< InferCreationAttributes > { declare id: string; + declare company_id: string; - declare invoice_status: string; - declare invoice_series: CreationOptional; - declare invoice_number: CreationOptional; - declare issue_date: CreationOptional; - declare operation_date: CreationOptional; - declare invoice_language: string; - declare invoice_currency: string; + declare status: string; + declare series: string; + declare invoice_number: string; + declare issue_date: string; + declare operation_date: string; + declare language_code: string; + declare currency_code: string; // Subtotal - declare subtotal_amount: CreationOptional; - declare subtotal_scale: CreationOptional; + declare subtotal_amount: number; + declare subtotal_scale: number; // Total - declare total_amount: CreationOptional; - declare total_scale: CreationOptional; + declare total_amount: number; + declare total_scale: number; // Relaciones declare items: NonAttribute; @@ -59,14 +59,14 @@ export class CustomerInvoiceModel extends Model< static hooks(database: Sequelize) { // Soft-cascade manual: al borrar una factura, marcamos items como borrados (paranoid). - CustomerInvoiceModel.addHook("afterDestroy", async (invoice, options) => { + /*CustomerInvoiceModel.addHook("afterDestroy", async (invoice, options) => { if (!invoice?.id) return; await CustomerInvoiceItemModel.destroy({ - where: { invoiceId: invoice.id }, + where: { invoice_id: invoice.id }, individualHooks: true, transaction: options.transaction, }); - }); + });*/ } } @@ -78,12 +78,18 @@ export default (database: Sequelize) => { primaryKey: true, }, - invoice_status: { - type: new DataTypes.STRING(), + company_id: { + type: DataTypes.UUID, allowNull: false, }, - invoice_series: { + status: { + type: new DataTypes.STRING(), + allowNull: false, + defaultValue: "draft", + }, + + series: { type: new DataTypes.STRING(), allowNull: true, defaultValue: null, @@ -107,14 +113,16 @@ export default (database: Sequelize) => { defaultValue: null, }, - invoice_language: { - type: new DataTypes.STRING(), + language_code: { + type: DataTypes.STRING(2), allowNull: false, + defaultValue: "es", }, - invoice_currency: { - type: new DataTypes.STRING(3), // ISO 4217 + currency_code: { + type: new DataTypes.STRING(3), allowNull: false, + defaultValue: "EUR", }, subtotal_amount: { @@ -151,7 +159,11 @@ export default (database: Sequelize) => { updatedAt: "updated_at", deletedAt: "deleted_at", - indexes: [{ unique: true, fields: ["invoice_number"] }], + indexes: [ + { name: "company_idx", fields: ["company_id"], unique: false }, + { name: "idx_company_idx", fields: ["id", "company_id"], unique: true }, + { unique: true, fields: ["invoice_number"] }, + ], whereMergeStrategy: "and", // <- cómo tratar el merge de un scope diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts index d0c40194..7863dd26 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts +++ b/modules/customer-invoices/src/api/infrastructure/sequelize/customer-invoice.repository.ts @@ -1,4 +1,4 @@ -import { SequelizeRepository } from "@erp/core/api"; +import { EntityNotFoundError, SequelizeRepository, translateSequelizeError } from "@erp/core/api"; import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server"; import { UniqueID } from "@repo/rdx-ddd"; import { Collection, Result } from "@repo/rdx-utils"; @@ -11,7 +11,6 @@ export class CustomerInvoiceRepository extends SequelizeRepository implements ICustomerInvoiceRepository { - //private readonly model: typeof CustomerInvoiceModel; private readonly mapper!: ICustomerInvoiceMapper; constructor(mapper: ICustomerInvoiceMapper) { @@ -52,16 +51,6 @@ export class CustomerInvoiceRepository }; } */ - async existsById(id: UniqueID, transaction?: Transaction): Promise> { - try { - const result = await this._exists(CustomerInvoiceModel, "id", id.toString(), transaction); - - return Result.ok(Boolean(result)); - } catch (err: unknown) { - return Result.fail(translateSequelizeError(err)); - } - } - /** * * Persiste una nueva factura o actualiza una existente. @@ -76,29 +65,64 @@ export class CustomerInvoiceRepository ): Promise> { try { const data = this.mapper.mapToPersistence(invoice); - await CustomerInvoiceModel.upsert(data, { transaction }); - return Result.ok(invoice); + const [instance] = await CustomerInvoiceModel.upsert(data, { transaction, returning: true }); + const savedInvoice = this.mapper.mapToDomain(instance); + return savedInvoice; } catch (err: unknown) { return Result.fail(translateSequelizeError(err)); } } + /** + * Comprueba si existe una factura con un `id` dentro de una `company`. + * + * @param companyId - Identificador UUID de la empresa a la que pertenece la factura. + * @param id - Identificador UUID de la factura. + * @param transaction - Transacción activa para la operación. + * @returns Result + */ + async existsByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction?: Transaction + ): Promise> { + try { + const count = await CustomerInvoiceModel.count({ + where: { id: id.toString(), company_id: companyId.toString() }, + transaction, + }); + return Result.ok(Boolean(count > 0)); + } catch (error: any) { + return Result.fail(translateSequelizeError(error)); + } + } + /** * * Busca una factura por su identificador único. + * + * @param companyId - Identificador UUID de la empresa a la que pertenece la factura. * @param id - UUID de la factura. * @param transaction - Transacción activa para la operación. * @returns Result */ - async findById(id: UniqueID, transaction: Transaction): Promise> { + async getByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction: Transaction + ): Promise> { try { - const rawData = await this._findById(CustomerInvoiceModel, id.toString(), { transaction }); + const row = await CustomerInvoiceModel.findOne({ + where: { id: id.toString(), company_id: companyId.toString() }, + transaction, + }); - if (!rawData) { - return Result.fail(new Error(`Invoice with id ${id} not found.`)); + if (!row) { + return Result.fail(new EntityNotFoundError("CustomerInvoice", "id", id.toString())); } - return this.mapper.mapToDomain(rawData); + const customer = this.mapper.mapToDomain(row); + return customer; } catch (err: unknown) { return Result.fail(translateSequelizeError(err)); } @@ -107,13 +131,16 @@ export class CustomerInvoiceRepository /** * * Consulta facturas usando un objeto Criteria (filtros, orden, paginación). + * + * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. * @param criteria - Criterios de búsqueda. * @param transaction - Transacción activa para la operación. * @returns Result * * @see Criteria */ - public async findByCriteria( + public async findByCriteriaInCompany( + companyId: UniqueID, criteria: Criteria, transaction: Transaction ): Promise, Error>> { @@ -121,6 +148,11 @@ export class CustomerInvoiceRepository const converter = new CriteriaToSequelizeConverter(); const query = converter.convert(criteria); + query.where = { + ...query.where, + company_id: companyId.toString(), + }; + const instances = await CustomerInvoiceModel.findAll({ ...query, transaction, @@ -135,13 +167,23 @@ export class CustomerInvoiceRepository /** * * Elimina o marca como eliminada una factura. + * + * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente. * @param id - UUID de la factura a eliminar. * @param transaction - Transacción activa para la operación. * @returns Result */ - async deleteById(id: UniqueID, transaction: any): Promise> { + async deleteByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction: any + ): Promise> { try { - await this._deleteById(CustomerInvoiceModel, id, false, transaction); + const deleted = await CustomerInvoiceModel.destroy({ + where: { id: id.toString(), company_id: companyId.toString() }, + transaction, + }); + return Result.ok(); } catch (err: unknown) { return Result.fail(translateSequelizeError(err)); diff --git a/modules/customers/src/api/domain/aggregates/customer.ts b/modules/customers/src/api/domain/aggregates/customer.ts index 2edefd16..b46c76e9 100644 --- a/modules/customers/src/api/domain/aggregates/customer.ts +++ b/modules/customers/src/api/domain/aggregates/customer.ts @@ -7,7 +7,6 @@ import { PhoneNumber, PostalAddress, PostalAddressPatchProps, - PostalAddressSnapshot, TINNumber, TaxCode, TextValue, @@ -41,32 +40,6 @@ export interface CustomerProps { currencyCode: CurrencyCode; } -export interface CustomerSnapshot { - id: string; - companyId: string; - status: string; - reference: string | null; - - isCompany: boolean; - name: string; - tradeName: string | null; - - tin: string | null; - - address: PostalAddressSnapshot; // snapshot serializable del VO PostalAddress - - email: string | null; - phone: string | null; - fax: string | null; - website: string | null; - - legalRecord: string | null; - defaultTaxes: string[]; - - languageCode: string; - currencyCode: string; -} - export type CustomerPatchProps = Partial> & { address?: PostalAddressPatchProps; }; diff --git a/modules/customers/src/api/domain/repositories/customer-repository.interface.ts b/modules/customers/src/api/domain/repositories/customer-repository.interface.ts index f8501c88..a3c4d170 100644 --- a/modules/customers/src/api/domain/repositories/customer-repository.interface.ts +++ b/modules/customers/src/api/domain/repositories/customer-repository.interface.ts @@ -4,8 +4,7 @@ import { Collection, Result } from "@repo/rdx-utils"; import { Customer } from "../aggregates"; /** - * Contrato del repositorio de Customers. - * Define la interfaz de persistencia para el agregado `Customer`. + * Interfaz del repositorio para el agregado `Customer`. * El escopado multitenant está representado por `companyId`. */ export interface ICustomerRepository { @@ -35,7 +34,8 @@ export interface ICustomerRepository { ): Promise>; /** - * Recupera múltiples customers dentro de una empresa según un criterio dinámico (búsqueda, paginación, etc.). + * Recupera múltiples customers dentro de una empresa + * según un criterio dinámico (búsqueda, paginación, etc.). * El resultado está encapsulado en un objeto `Collection`. */ findByCriteriaInCompany( @@ -47,6 +47,11 @@ export interface ICustomerRepository { /** * Elimina un Customer por su ID, dentro de una empresa. * Retorna `void` si se elimina correctamente, o `NotFoundError` si no existía. + * */ - deleteByIdInCompany(companyId: UniqueID, id: UniqueID, transaction: any): Promise>; + deleteByIdInCompany( + companyId: UniqueID, + id: UniqueID, + transaction: any + ): Promise>; } diff --git a/modules/customers/src/api/index.ts b/modules/customers/src/api/index.ts index 4d95288d..11e17aac 100644 --- a/modules/customers/src/api/index.ts +++ b/modules/customers/src/api/index.ts @@ -10,12 +10,12 @@ export const customersAPIModule: IModuleServer = { // const contacts = getService("contacts"); const { logger } = params; customersRouter(params); - logger.info("🚀 Customers module initialized", { label: "customers" }); + logger.info("🚀 Customers module initialized", { label: this.name }); }, async registerDependencies(params) { const { database, logger } = params; logger.info("🚀 Customers module dependencies registered", { - label: "customers", + label: this.name, }); return { models, diff --git a/modules/customers/src/api/infrastructure/mappers/customer-item.mapper.ts b/modules/customers/src/api/infrastructure/mappers/customer-item.mapper.ts deleted file mode 100644 index b50b15d8..00000000 --- a/modules/customers/src/api/infrastructure/mappers/customer-item.mapper.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { ISequelizeMapper, MapperParamsType, SequelizeMapper } from "@erp/core/api"; -import { UniqueID } from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; -import { InferCreationAttributes } from "sequelize"; -import { - Customer, - CustomerItem, - CustomerItemDescription, - CustomerItemDiscount, - CustomerItemQuantity, - CustomerItemUnitPrice, -} from "../../domain"; -import { CustomerItemCreationAttributes, CustomerItemModel, CustomerModel } from "../sequelize"; - -export interface ICustomerItemMapper - extends ISequelizeMapper {} - -export class CustomerItemMapper - extends SequelizeMapper - implements ICustomerItemMapper -{ - public mapToDomain( - source: CustomerItemModel, - params?: MapperParamsType - ): Result { - const { sourceParent } = params as { sourceParent: CustomerModel }; - - // Validación y creación de ID único - const idOrError = UniqueID.create(source.item_id); - if (idOrError.isFailure) { - return Result.fail(idOrError.error); - } - - // Validación y creación de descripción - const descriptionOrError = CustomerItemDescription.create(source.description || ""); - if (descriptionOrError.isFailure) { - return Result.fail(descriptionOrError.error); - } - - // Validación y creación de cantidad - const quantityOrError = CustomerItemQuantity.create({ - amount: source.quantity_amount, - scale: source.quantity_scale, - }); - if (quantityOrError.isFailure) { - return Result.fail(quantityOrError.error); - } - - // Validación y creación de precio unitario - const unitPriceOrError = CustomerItemUnitPrice.create({ - amount: source.unit_price_amount, - scale: source.unit_price_scale, - currency_code: sourceParent.invoice_currency, - }); - if (unitPriceOrError.isFailure) { - return Result.fail(unitPriceOrError.error); - } - - // Validación y creación de descuento - const discountOrError = CustomerItemDiscount.create({ - amount: source.discount_amount || 0, - scale: source.discount_scale || 0, - }); - if (discountOrError.isFailure) { - return Result.fail(discountOrError.error); - } - - // Combinación de resultados - const result = Result.combine([ - idOrError, - descriptionOrError, - quantityOrError, - unitPriceOrError, - discountOrError, - ]); - - if (result.isFailure) { - return Result.fail(result.error); - } - - // Creación del objeto de dominio - return CustomerItem.create( - { - description: descriptionOrError.data, - quantity: quantityOrError.data, - unitPrice: unitPriceOrError.data, - discount: discountOrError.data, - }, - idOrError.data - ); - } - - public mapToPersistence( - source: CustomerItem, - params?: MapperParamsType - ): InferCreationAttributes { - const { index, sourceParent } = params as { - index: number; - sourceParent: Customer; - }; - - const lineData = { - parent_id: undefined, - invoice_id: sourceParent.id.toPrimitive(), - item_type: "simple", - position: index, - - item_id: source.id.toPrimitive(), - description: source.description.toPrimitive(), - - quantity_amount: source.quantity.toPrimitive().amount, - quantity_scale: source.quantity.toPrimitive().scale, - - unit_price_amount: source.unitPrice.toPrimitive().amount, - unit_price_scale: source.unitPrice.toPrimitive().scale, - - subtotal_amount: source.subtotalPrice.toPrimitive().amount, - subtotal_scale: source.subtotalPrice.toPrimitive().scale, - - discount_amount: source.discount.toPrimitive().amount, - discount_scale: source.discount.toPrimitive().scale, - - total_amount: source.totalPrice.toPrimitive().amount, - total_scale: source.totalPrice.toPrimitive().scale, - }; - return lineData; - } -} diff --git a/modules/customers/src/api/infrastructure/sequelize/customer.model.ts b/modules/customers/src/api/infrastructure/sequelize/customer.model.ts index 0f8b5603..7fa0149f 100644 --- a/modules/customers/src/api/infrastructure/sequelize/customer.model.ts +++ b/modules/customers/src/api/infrastructure/sequelize/customer.model.ts @@ -171,6 +171,7 @@ export default (database: Sequelize) => { sequelize: database, tableName: "customers", + underscored: true, paranoid: true, // softs deletes timestamps: true, diff --git a/modules/customers/src/api/infrastructure/sequelize/customer.repository.ts b/modules/customers/src/api/infrastructure/sequelize/customer.repository.ts index e2892ea3..8b5ae4d0 100644 --- a/modules/customers/src/api/infrastructure/sequelize/customer.repository.ts +++ b/modules/customers/src/api/infrastructure/sequelize/customer.repository.ts @@ -11,7 +11,6 @@ export class CustomerRepository extends SequelizeRepository implements ICustomerRepository { - //private readonly model: typeof CustomerModel; private readonly mapper!: ICustomerMapper; constructor(mapper: ICustomerMapper) { @@ -149,7 +148,6 @@ export class CustomerRepository return Result.ok(); } catch (err: unknown) { - // , `Error deleting customer ${id} in company ${companyId}` return Result.fail(translateSequelizeError(err)); } }