From c9ba2d0370e8206c8baf163e7a9f4872df0a5c2c Mon Sep 17 00:00:00 2001 From: david Date: Fri, 24 Apr 2026 20:53:05 +0200 Subject: [PATCH] . --- .../mappers/create-proforma-input.mapper.ts | 222 +++++----------- .../mappers/update-proforma-input.mapper.ts | 250 ++++++++---------- .../proformas/snapshot-builders/full/index.ts | 4 - .../full/proforma-full-snapshot-builder.ts | 13 +- .../full/proforma-full-snapshot.interface.ts | 60 ----- .../proforma-item-full-snapshot.interface.ts | 36 --- .../proforma-items-full-snapshot-builder.ts | 9 +- ...roforma-recipient-full-snapshot-builder.ts | 9 +- ...forma-recipient-full-snapshot.interface.ts | 15 -- .../proforma-tax-full-snapshot-interface.ts | 17 -- .../proforma-taxes-full-snapshot-builder.ts | 9 +- .../proforma-summary-snapshot-builder.ts | 1 - .../use-cases/update-proforma.use-case.ts | 6 +- .../issued-invoice.report.presenter.ts | 2 +- .../proformas/proforma.report.presenter.ts | 2 +- .../aggregates/proforma.aggregate.ts | 34 ++- .../proformas/create-proforma.request.dto.ts | 76 ++++-- .../update-proforma-by-id.request.dto.ts | 80 ++++-- .../get-proforma-by-id.response.dto.ts | 4 +- .../src/common/dto/shared/index.ts | 5 +- .../issued-invoice-item-detail.dto.ts | 7 +- .../common/dto/shared/item-position.dto.ts | 5 + .../dto/shared/item-taxes-breakdown.dto.ts | 6 - ...f-ref.dto.ts => payment-method-ref.dto.ts} | 4 +- .../proforma/proforma-item-detail.dto.ts | 9 +- .../shared/proforma/proforma-summary.dto.ts | 1 - .../dto/shared/tax-combination-code.dto.ts | 23 ++ .../adapters/get-proforma-by-id.adapter.ts | 2 +- .../mappers/update-customer-input.mapper.ts | 2 - .../update-customer-by-id.request.dto.ts | 10 +- .../get-customer-by-id.response.dto.ts | 3 +- .../customers/src/common/dto/shared/index.ts | 1 + .../dto/shared/tax-combination-code.dto.ts | 23 ++ .../get-supplier-by-id.response.dto.ts | 2 +- 34 files changed, 409 insertions(+), 543 deletions(-) delete mode 100644 modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-full-snapshot.interface.ts delete mode 100644 modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-item-full-snapshot.interface.ts delete mode 100644 modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-recipient-full-snapshot.interface.ts delete mode 100644 modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-tax-full-snapshot-interface.ts create mode 100644 modules/customer-invoices/src/common/dto/shared/item-position.dto.ts delete mode 100644 modules/customer-invoices/src/common/dto/shared/item-taxes-breakdown.dto.ts rename modules/customer-invoices/src/common/dto/shared/{payment-methof-ref.dto.ts => payment-method-ref.dto.ts} (72%) create mode 100644 modules/customer-invoices/src/common/dto/shared/tax-combination-code.dto.ts create mode 100644 modules/customers/src/common/dto/shared/tax-combination-code.dto.ts diff --git a/modules/customer-invoices/src/api/application/proformas/mappers/create-proforma-input.mapper.ts b/modules/customer-invoices/src/api/application/proformas/mappers/create-proforma-input.mapper.ts index 67bcf7af..3bceabf0 100644 --- a/modules/customer-invoices/src/api/application/proformas/mappers/create-proforma-input.mapper.ts +++ b/modules/customer-invoices/src/api/application/proformas/mappers/create-proforma-input.mapper.ts @@ -1,10 +1,9 @@ -import { type JsonTaxCatalogProvider, NumberHelper } from "@erp/core"; +import { NumberHelper } from "@erp/core"; import { DiscountPercentage } from "@erp/core/api"; import { CurrencyCode, DomainError, LanguageCode, - Percentage, TextValue, UniqueID, UtcDate, @@ -20,7 +19,6 @@ import { type IProformaCreateProps, type IProformaItemCreateProps, InvoiceNumber, - InvoicePaymentMethod, type InvoiceRecipient, InvoiceSerie, InvoiceStatus, @@ -31,23 +29,6 @@ import { type ProformaItemTaxesProps, } from "../../../domain"; -/** - * CreateProformaPropsMapper - * Convierte el DTO a las props validadas (CustomerProps). - * No construye directamente el agregado. - * - * @param dto - DTO con los datos de la factura de cliente - * @returns - - * - */ - -/*export interface ICreateProformaInputMapper - extends IDTOInputToPropsMapper< - CreateProformaRequestDTO, - { id: UniqueID; props: Omit & { items: IProformaItemProps[] } } - > {}*/ - export interface ICreateProformaInputMapper { map( dto: CreateProformaRequestDTO, @@ -55,23 +36,21 @@ export interface ICreateProformaInputMapper { ): Result<{ id: UniqueID; props: IProformaCreateProps }>; } -export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/ { - private readonly taxCatalog: JsonTaxCatalogProvider; - - constructor(params: { taxCatalog: JsonTaxCatalogProvider }) { - this.taxCatalog = params.taxCatalog; - } +/** + * @summary Convierte el DTO de creación de proforma en props de dominio. + * @remarks + * No construye el agregado. Solo valida y convierte primitivas de transporte + * a Value Objects y props necesarias para `Proforma.create`. + */ +export class CreateProformaInputMapper implements ICreateProformaInputMapper { public map( dto: CreateProformaRequestDTO, params: { companyId: UniqueID } ): Result<{ id: UniqueID; props: IProformaCreateProps }> { const errors: ValidationErrorDetail[] = []; - const { companyId } = params; try { - const defaultStatus = InvoiceStatus.draft(); - const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", errors); const customerId = extractOrPushError( @@ -80,9 +59,7 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/ errors ); - const recipient = Maybe.none(); - - const proformaNumber = extractOrPushError( + const invoiceNumber = extractOrPushError( InvoiceNumber.create(dto.invoice_number), "invoice_number", errors @@ -113,7 +90,7 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/ ); const description = extractOrPushError( - maybeFromNullableResult(dto.reference, (value) => Result.ok(String(value))), + maybeFromNullableResult(dto.description, (value) => Result.ok(String(value))), "description", errors ); @@ -136,24 +113,21 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/ errors ); - const paymentMethod = extractOrPushError( - maybeFromNullableResult(dto.payment_method, (value) => - InvoicePaymentMethod.create({ paymentDescription: value }) - ), - "payment_method", - errors - ); - const globalDiscountPercentage = extractOrPushError( - Percentage.create({ - value: Number(dto.global_discount_percentage.value), - scale: Number(dto.global_discount_percentage.scale), + DiscountPercentage.create({ + value: NumberHelper.toSafeNumber(dto.global_discount_percentage.value), }), - "discount_percentage", + "global_discount_percentage", errors ); - const itemsProps = this.mapItemsProps(dto, { + const paymentMethodId = extractOrPushError( + maybeFromNullableResult(dto.payment_method_id, (value) => UniqueID.create(value)), + "payment_method_id", + errors + ); + + const items = this.mapItemsProps(dto.items, { languageCode: languageCode!, currencyCode: currencyCode!, globalDiscountPercentage: globalDiscountPercentage!, @@ -163,17 +137,17 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/ this.throwIfValidationErrors(errors); const props: IProformaCreateProps = { - companyId, - status: defaultStatus, + companyId: params.companyId, + status: InvoiceStatus.draft(), - invoiceNumber: proformaNumber!, + invoiceNumber: invoiceNumber!, series: series!, invoiceDate: invoiceDate!, operationDate: operationDate!, customerId: customerId!, - recipient, + recipient: Maybe.none(), reference: reference!, description: description!, @@ -182,10 +156,12 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/ languageCode: languageCode!, currencyCode: currencyCode!, - paymentMethod: paymentMethod!, - globalDiscountPercentage: globalDiscountPercentage!, + linkedInvoiceId: Maybe.none(), - items: itemsProps, // ← IProformaItemProps[] + paymentMethodId: paymentMethodId!, + + globalDiscountPercentage: globalDiscountPercentage!, + items, }; return Result.ok({ @@ -193,18 +169,12 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/ props, }); } catch (err: unknown) { - return Result.fail(new DomainError("Customer invoice props mapping failed", { cause: err })); - } - } - - private throwIfValidationErrors(errors: ValidationErrorDetail[]): void { - if (errors.length > 0) { - throw new ValidationErrorCollection("Customer proforma props mapping failed", errors); + return Result.fail(new DomainError("Proforma props mapping failed", { cause: err })); } } private mapItemsProps( - dto: CreateProformaRequestDTO, + itemsDTO: CreateProformaRequestDTO["items"], params: { languageCode: LanguageCode; currencyCode: CurrencyCode; @@ -212,132 +182,78 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/ errors: ValidationErrorDetail[]; } ): IProformaItemCreateProps[] { - const itemsProps: IProformaItemCreateProps[] = []; - - dto.items.forEach((item, index) => { + return itemsDTO.map((item, index) => { const description = extractOrPushError( - maybeFromNullableResult(item.description, (v) => ItemDescription.create(v)), + maybeFromNullableResult(item.description, (value) => ItemDescription.create(value)), `items[${index}].description`, params.errors ); const quantity = extractOrPushError( - maybeFromNullableResult(item.quantity, (v) => - ItemQuantity.create({ value: NumberHelper.toSafeNumber(v) }) + maybeFromNullableResult(item.quantity, (value) => + ItemQuantity.create({ value: NumberHelper.toSafeNumber(value) }) ), `items[${index}].quantity`, params.errors ); const unitAmount = extractOrPushError( - maybeFromNullableResult(item.unit_amount, (v) => - ItemAmount.create({ value: NumberHelper.toSafeNumber(v) }) + maybeFromNullableResult(item.unit_amount, (value) => + ItemAmount.create({ value: NumberHelper.toSafeNumber(value) }) ), `items[${index}].unit_amount`, params.errors ); - const discountPercentage = extractOrPushError( - maybeFromNullableResult(item.item_discount_percentage, (v) => - DiscountPercentage.create({ value: NumberHelper.toSafeNumber(v.value) }) + const itemDiscountPercentage = extractOrPushError( + maybeFromNullableResult(item.item_discount_percentage, (value) => + DiscountPercentage.create({ + value: NumberHelper.toSafeNumber(value.value), + }) ), - `items[${index}].discount_percentage`, + `items[${index}].item_discount_percentage`, params.errors ); - const taxes = this.mapTaxesProps(item.taxes, { - itemIndex: index, - errors: params.errors, - }); - - this.throwIfValidationErrors(params.errors); - - itemsProps.push({ - globalDiscountPercentage: params.globalDiscountPercentage, - languageCode: params.languageCode, - currencyCode: params.currencyCode, + return { + position: item.position, description: description!, quantity: quantity!, unitAmount: unitAmount!, - itemDiscountPercentage: discountPercentage!, - taxes, - }); + itemDiscountPercentage: itemDiscountPercentage!, + + taxes: this.mapTaxesProps(item.taxes, { + itemIndex: index, + errors: params.errors, + }), + + languageCode: params.languageCode, + currencyCode: params.currencyCode, + globalDiscountPercentage: params.globalDiscountPercentage, + }; }); - - return itemsProps; } - /* Devuelve las propiedades de los impustos de una línea de detalle */ - private mapTaxesProps( - taxesDTO: NonNullable[number]["taxes"], + taxesDTO: CreateProformaRequestDTO["items"][number]["taxes"], params: { itemIndex: number; errors: ValidationErrorDetail[] } ): ProformaItemTaxesProps { - // TODO: POR AHORA SE QUEDA ASÍ + if (taxesDTO === "#;#;#") { + return ProformaItemTaxes.empty().getProps(); + } - return ProformaItemTaxes.empty().getProps(); - - /*const { itemIndex, errors } = params; - - const taxesProps: ProformaItemTaxesProps = { - iva: Maybe.none(), - retention: Maybe.none(), - rec: Maybe.none(), - }; - - const taxStrCodes = taxesDTO - .split(",") - .map((s) => s.trim()) - .filter((s) => s.length > 0); - - taxStrCodes.forEach((strCode, taxIndex) => { - const taxResult = Tax.createFromCode(strCode, this.taxCatalog); - - if (!taxResult.isSuccess) { - errors.push({ - path: `items[${itemIndex}].taxes[${taxIndex}]`, - message: taxResult.error.message, - }); - return; - } - - const tax = taxResult.data; - - if (tax.isVATLike()) { - if (taxesProps.iva.isSome()) { - errors.push({ - path: `items[${itemIndex}].taxes`, - message: "Multiple taxes for group VAT are not allowed", - }); - } - taxesProps.iva = Maybe.some(tax); - } - - if (tax.isRetention()) { - if (taxesProps.retention.isSome()) { - errors.push({ - path: `items[${itemIndex}].taxes`, - message: "Multiple taxes for group retention are not allowed", - }); - } - taxesProps.retention = Maybe.some(tax); - } - - if (tax.isRec()) { - if (taxesProps.rec.isSome()) { - errors.push({ - path: `items[${itemIndex}].taxes`, - message: "Multiple taxes for group rec are not allowed", - }); - } - taxesProps.rec = Maybe.some(tax); - } + params.errors.push({ + path: `items[${params.itemIndex}].taxes`, + message: "Tax combination mapping is not implemented yet", }); - this.throwIfValidationErrors(errors); + return ProformaItemTaxes.empty().getProps(); + } - return taxesProps; - */ + private throwIfValidationErrors(errors: ValidationErrorDetail[]): void { + if (errors.length > 0) { + throw new ValidationErrorCollection("Proforma props mapping failed", errors); + } } } diff --git a/modules/customer-invoices/src/api/application/proformas/mappers/update-proforma-input.mapper.ts b/modules/customer-invoices/src/api/application/proformas/mappers/update-proforma-input.mapper.ts index 2dacb0f2..0c2d98e4 100644 --- a/modules/customer-invoices/src/api/application/proformas/mappers/update-proforma-input.mapper.ts +++ b/modules/customer-invoices/src/api/application/proformas/mappers/update-proforma-input.mapper.ts @@ -26,20 +26,6 @@ import { type ProformaPatchProps, } from "../../../domain"; -/** - * UpdateProformaPropsMapper - * Convierte el DTO a las props validadas (ProformaInvoiceProps). - * No construye directamente el agregado. - * Tri-estado: - * - campo omitido → no se cambia - * - campo con valor null/"" → se quita el valor -> set(None()), - * - campo con valor no-vacío → se pone el nuevo valor -> set(Some(VO)). - * - * @param dto - DTO con los datos a cambiar en la factura de cliente - * @returns Cambios en las propiedades de la factura de cliente - * - */ - export interface IUpdateProformaInputMapper { map( dto: UpdateProformaByIdRequestDTO, @@ -47,6 +33,20 @@ export interface IUpdateProformaInputMapper { ): Result; } +/** + * @summary Convierte el DTO de update de proforma en props de dominio. + * @remarks + * Respeta semántica PATCH en cabecera: + * - omitido: no modificar + * - null: limpiar valor cuando el campo lo permite + * - valor: asignar nuevo valor + * + * Para `items`, no aplica patch granular: + * - undefined: no tocar líneas + * - []: borrar todas las líneas + * - [...]: reemplazar colección completa + */ + export class UpdateProformaInputMapper implements IUpdateProformaInputMapper { public map( dto: UpdateProformaByIdRequestDTO, @@ -54,46 +54,58 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper { ): Result { try { const errors: ValidationErrorDetail[] = []; - const props: ProformaPatchProps = {}; + const proformaPatchProps: ProformaPatchProps = {}; toPatchField(dto.series).ifSet((series) => { - props.series = extractOrPushError( + proformaPatchProps.series = extractOrPushError( maybeFromNullableResult(series, (value) => InvoiceSerie.create(value)), - "reference", + "series", errors ); }); - toPatchField(dto.invoice_date).ifSet((invoice_date) => { - if (isNullishOrEmpty(invoice_date)) { - errors.push({ path: "invoice_date", message: "Invoice date cannot be empty" }); + toPatchField(dto.invoice_date).ifSet((invoiceDate) => { + if (isNullishOrEmpty(invoiceDate)) { + errors.push({ + path: "invoice_date", + message: "Invoice date cannot be empty", + }); return; } - props.invoiceDate = extractOrPushError( - UtcDate.createFromISO(invoice_date!), + + proformaPatchProps.invoiceDate = extractOrPushError( + UtcDate.createFromISO(invoiceDate), "invoice_date", errors ); }); - toPatchField(dto.operation_date).ifSet((operation_date) => { - props.operationDate = extractOrPushError( - maybeFromNullableResult(operation_date, (value) => UtcDate.createFromISO(value)), + toPatchField(dto.operation_date).ifSet((operationDate) => { + proformaPatchProps.operationDate = extractOrPushError( + maybeFromNullableResult(operationDate, (value) => UtcDate.createFromISO(value)), "operation_date", errors ); }); - toPatchField(dto.customer_id).ifSet((customer_id) => { - if (isNullishOrEmpty(customer_id)) { - errors.push({ path: "customer_id", message: "Proforma cannot be empty" }); + toPatchField(dto.customer_id).ifSet((customerId) => { + if (isNullishOrEmpty(customerId)) { + errors.push({ + path: "customer_id", + message: "Customer id cannot be empty", + }); return; } - props.customerId = extractOrPushError(UniqueID.create(customer_id!), "customer_id", errors); + + proformaPatchProps.customerId = extractOrPushError( + UniqueID.create(customerId), + "customer_id", + errors + ); }); toPatchField(dto.reference).ifSet((reference) => { - props.reference = extractOrPushError( + proformaPatchProps.reference = extractOrPushError( maybeFromNullableResult(reference, (value) => Result.ok(String(value))), "reference", errors @@ -101,7 +113,7 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper { }); toPatchField(dto.description).ifSet((description) => { - props.description = extractOrPushError( + proformaPatchProps.description = extractOrPushError( maybeFromNullableResult(description, (value) => Result.ok(String(value))), "description", errors @@ -109,7 +121,7 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper { }); toPatchField(dto.notes).ifSet((notes) => { - props.notes = extractOrPushError( + proformaPatchProps.notes = extractOrPushError( maybeFromNullableResult(notes, (value) => TextValue.create(value)), "notes", errors @@ -118,12 +130,15 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper { toPatchField(dto.language_code).ifSet((languageCode) => { if (isNullishOrEmpty(languageCode)) { - errors.push({ path: "language_code", message: "Language code cannot be empty" }); + errors.push({ + path: "language_code", + message: "Language code cannot be empty", + }); return; } - props.languageCode = extractOrPushError( - LanguageCode.create(languageCode!), + proformaPatchProps.languageCode = extractOrPushError( + LanguageCode.create(languageCode), "language_code", errors ); @@ -131,72 +146,84 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper { toPatchField(dto.currency_code).ifSet((currencyCode) => { if (isNullishOrEmpty(currencyCode)) { - errors.push({ path: "currency_code", message: "Currency code cannot be empty" }); + errors.push({ + path: "currency_code", + message: "Currency code cannot be empty", + }); return; } - props.currencyCode = extractOrPushError( - CurrencyCode.create(currencyCode!), + proformaPatchProps.currencyCode = extractOrPushError( + CurrencyCode.create(currencyCode), "currency_code", errors ); }); - if (dto.items) { - const itemsProps = this.mapItemsProps(dto, { errors }); - props.items = itemsProps; + toPatchField(dto.global_discount_percentage).ifSet((globalDiscountPercentage) => { + proformaPatchProps.globalDiscountPercentage = extractOrPushError( + DiscountPercentage.create({ + value: NumberHelper.toSafeNumber(globalDiscountPercentage.value), + }), + "global_discount_percentage", + errors + ); + }); + + toPatchField(dto.payment_method_id).ifSet((paymentMethodId) => { + proformaPatchProps.paymentMethodId = extractOrPushError( + maybeFromNullableResult(paymentMethodId, (value) => UniqueID.create(value)), + "payment_method_id", + errors + ); + }); + + if (dto.items !== undefined) { + proformaPatchProps.items = this.mapItemsProps(dto.items, { errors }); } this.throwIfValidationErrors(errors); - return Result.ok(props); + return Result.ok(proformaPatchProps); } catch (err: unknown) { - return Result.fail(new DomainError("Proforma proforma props mapping failed", { cause: err })); - } - } - - private throwIfValidationErrors(errors: ValidationErrorDetail[]): void { - if (errors.length > 0) { - throw new ValidationErrorCollection("Customer proforma props mapping failed", errors); + return Result.fail(new DomainError("Proforma props mapping failed", { cause: err })); } } private mapItemsProps( - dto: UpdateProformaByIdRequestDTO, - params: { - errors: ValidationErrorDetail[]; - } + itemsDTO: NonNullable, + params: { errors: ValidationErrorDetail[] } ): ProformaItemPatchProps[] { - const itemsProps: ProformaItemPatchProps[] = []; - - dto.items?.forEach((item, index) => { + return itemsDTO.map((item, index) => { const description = extractOrPushError( - maybeFromNullableResult(item.description, (v) => ItemDescription.create(v)), + maybeFromNullableResult(item.description, (value) => ItemDescription.create(value)), `items[${index}].description`, params.errors ); const quantity = extractOrPushError( - maybeFromNullableResult(item.quantity, (v) => - ItemQuantity.create({ value: NumberHelper.toSafeNumber(v) }) + maybeFromNullableResult(item.quantity, (value) => + ItemQuantity.create({ value: NumberHelper.toSafeNumber(value) }) ), `items[${index}].quantity`, params.errors ); const unitAmount = extractOrPushError( - maybeFromNullableResult(item.unit_amount, (v) => - ItemAmount.create({ value: NumberHelper.toSafeNumber(v) }) + maybeFromNullableResult(item.unit_amount, (value) => + ItemAmount.create({ value: NumberHelper.toSafeNumber(value) }) ), `items[${index}].unit_amount`, params.errors ); - const discountPercentage = extractOrPushError( - maybeFromNullableResult(item.item_discount_percentage, (v) => - DiscountPercentage.create({ value: NumberHelper.toSafeNumber(v.value) }) + const itemDiscountPercentage = extractOrPushError( + maybeFromNullableResult(item.item_discount_percentage, (value) => + DiscountPercentage.create({ + value: NumberHelper.toSafeNumber(value.value), + }) ), - `items[${index}].discount_percentage`, + `items[${index}].item_discount_percentage`, params.errors ); @@ -205,89 +232,44 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper { errors: params.errors, }); - this.throwIfValidationErrors(params.errors); - - itemsProps.push({ + return { + position: item.position, description: description!, quantity: quantity!, unitAmount: unitAmount!, - itemDiscountPercentage: discountPercentage!, + itemDiscountPercentage: itemDiscountPercentage!, taxes, - }); + }; }); - - return itemsProps; } - /* Devuelve las propiedades de los impuestos de una línea de detalle */ - private mapTaxesProps( taxesDTO: NonNullable[number]["taxes"], params: { itemIndex: number; errors: ValidationErrorDetail[] } ): ProformaItemTaxesProps { - // TODO: POR AHORA SE QUEDA ASÍ + if (taxesDTO === "#;#;#") { + return ProformaItemTaxes.empty().getProps(); + } - return ProformaItemTaxes.empty().getProps(); - - /*const { itemIndex, errors } = params; - - const taxesProps: ProformaItemTaxesProps = { - iva: Maybe.none(), - retention: Maybe.none(), - rec: Maybe.none(), - }; - - const taxStrCodes = taxesDTO - .split(",") - .map((s) => s.trim()) - .filter((s) => s.length > 0); - - taxStrCodes.forEach((strCode, taxIndex) => { - const taxResult = Tax.createFromCode(strCode, this.taxCatalog); - - if (!taxResult.isSuccess) { - errors.push({ - path: `items[${itemIndex}].taxes[${taxIndex}]`, - message: taxResult.error.message, - }); - return; - } - - const tax = taxResult.data; - - if (tax.isVATLike()) { - if (taxesProps.iva.isSome()) { - errors.push({ - path: `items[${itemIndex}].taxes`, - message: "Multiple taxes for group VAT are not allowed", - }); - } - taxesProps.iva = Maybe.some(tax); - } - - if (tax.isRetention()) { - if (taxesProps.retention.isSome()) { - errors.push({ - path: `items[${itemIndex}].taxes`, - message: "Multiple taxes for group retention are not allowed", - }); - } - taxesProps.retention = Maybe.some(tax); - } - - if (tax.isRec()) { - if (taxesProps.rec.isSome()) { - errors.push({ - path: `items[${itemIndex}].taxes`, - message: "Multiple taxes for group rec are not allowed", - }); - } - taxesProps.rec = Maybe.some(tax); - } + /** + * Pendiente: resolver códigos contra catálogo fiscal. + * + * taxesDTO llega como: + * - iva_21;#;retention_10 + * - iva_10;rec_5_2;# + * - #;#;# + */ + params.errors.push({ + path: `items[${params.itemIndex}].taxes`, + message: "Tax combination mapping is not implemented yet", }); - this.throwIfValidationErrors(errors); + return ProformaItemTaxes.empty().getProps(); + } - return taxesProps;*/ + private throwIfValidationErrors(errors: ValidationErrorDetail[]): void { + if (errors.length > 0) { + throw new ValidationErrorCollection("Proforma props mapping failed", errors); + } } } diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/index.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/index.ts index e9181206..c824a6ad 100644 --- a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/index.ts +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/index.ts @@ -1,8 +1,4 @@ -export * from "./proforma-full-snapshot.interface"; export * from "./proforma-full-snapshot-builder"; -export * from "./proforma-item-full-snapshot.interface"; export * from "./proforma-items-full-snapshot-builder"; -export * from "./proforma-recipient-full-snapshot.interface"; export * from "./proforma-recipient-full-snapshot-builder"; -export * from "./proforma-tax-full-snapshot-interface"; export * from "./proforma-taxes-full-snapshot-builder"; diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-full-snapshot-builder.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-full-snapshot-builder.ts index adb8fabb..498aba81 100644 --- a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-full-snapshot-builder.ts +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-full-snapshot-builder.ts @@ -1,15 +1,15 @@ import type { ISnapshotBuilder } from "@erp/core/api"; import { maybeToNullable } from "@repo/rdx-ddd"; +import type { GetProformaByIdResponseDTO } from "../../../../../common"; import type { Proforma } from "../../../../domain"; -import type { IProformaFullSnapshot } from "./proforma-full-snapshot.interface"; import type { IProformaItemsFullSnapshotBuilder } from "./proforma-items-full-snapshot-builder"; import type { IProformaRecipientFullSnapshotBuilder } from "./proforma-recipient-full-snapshot-builder"; import type { IProformaTaxesFullSnapshotBuilder } from "./proforma-taxes-full-snapshot-builder"; export interface IProformaFullSnapshotBuilder - extends ISnapshotBuilder {} + extends ISnapshotBuilder {} export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder { constructor( @@ -18,7 +18,7 @@ export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder private readonly taxesBuilder: IProformaTaxesFullSnapshotBuilder ) {} - toOutput(proforma: Proforma): IProformaFullSnapshot { + toOutput(proforma: Proforma): GetProformaByIdResponseDTO { const items = this.itemsBuilder.toOutput(proforma.items); const recipient = this.recipientBuilder.toOutput(proforma); const taxes = this.taxesBuilder.toOutput(proforma.taxes()); @@ -27,8 +27,8 @@ export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder (payment) => { const { id, payment_description } = payment.toObjectString(); return { - payment_id: id, - payment_description, + id: id, + description: payment_description, }; }, () => null @@ -40,9 +40,8 @@ export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder id: proforma.id.toString(), company_id: proforma.companyId.toString(), - is_proforma: true, invoice_number: proforma.invoiceNumber.toString(), - status: proforma.status.toPrimitive(), + status: proforma.status.toPrimitive() as GetProformaByIdResponseDTO["status"], series: maybeToNullable(proforma.series, (value) => value.toString()), invoice_date: proforma.invoiceDate.toDateString(), diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-full-snapshot.interface.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-full-snapshot.interface.ts deleted file mode 100644 index 8e460941..00000000 --- a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-full-snapshot.interface.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { IProformaItemFullSnapshot } from "./proforma-item-full-snapshot.interface"; -import type { IProformaRecipientFullSnapshot } from "./proforma-recipient-full-snapshot.interface"; -import type { IProformaTaxFullSnapshot } from "./proforma-tax-full-snapshot-interface"; - -/** - * Fijarse en GetProformaByIdResponseDTO - */ - -export interface IProformaFullSnapshot { - id: string; - company_id: string; - - is_proforma: boolean; - invoice_number: string; - status: string; - series: string | null; - - invoice_date: string; - operation_date: string | null; - - reference: string | null; - description: string | null; - notes: string | null; - - language_code: string; - currency_code: string; - - customer_id: string; - recipient: IProformaRecipientFullSnapshot; - - linked_invoice_id: string | null; - - taxes: IProformaTaxFullSnapshot[]; - - payment_method: { - payment_id: string; - payment_description: string; - } | null; - - subtotal_amount: { value: string; scale: string; currency_code: string }; - items_discount_amount: { value: string; scale: string; currency_code: string }; - - global_discount_percentage: { value: string; scale: string }; - global_discount_amount: { value: string; scale: string; currency_code: string }; - - total_discount_amount: { value: string; scale: string; currency_code: string }; - - taxable_amount: { value: string; scale: string; currency_code: string }; - - iva_amount: { value: string; scale: string; currency_code: string }; - rec_amount: { value: string; scale: string; currency_code: string }; - retention_amount: { value: string; scale: string; currency_code: string }; - - taxes_amount: { value: string; scale: string; currency_code: string }; - total_amount: { value: string; scale: string; currency_code: string }; - - items: IProformaItemFullSnapshot[]; - - metadata: Record | null; -} diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-item-full-snapshot.interface.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-item-full-snapshot.interface.ts deleted file mode 100644 index 8c8d098d..00000000 --- a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-item-full-snapshot.interface.ts +++ /dev/null @@ -1,36 +0,0 @@ -export interface IProformaItemFullSnapshot { - id: string; - is_valued: boolean; - position: number; - description: string | null; - - quantity: { value: string; scale: string }; - unit_amount: { value: string; scale: string; currency_code: string }; - - subtotal_amount: { value: string; scale: string; currency_code: string }; - - item_discount_percentage: { value: string; scale: string }; - item_discount_amount: { value: string; scale: string; currency_code: string }; - - global_discount_percentage: { value: string; scale: string }; - global_discount_amount: { value: string; scale: string; currency_code: string }; - - total_discount_amount: { value: string; scale: string; currency_code: string }; - - taxable_amount: { value: string; scale: string; currency_code: string }; - - iva_code: string; - iva_percentage: { value: string; scale: string }; - iva_amount: { value: string; scale: string; currency_code: string }; - - rec_code: string; - rec_percentage: { value: string; scale: string }; - rec_amount: { value: string; scale: string; currency_code: string }; - - retention_code: string; - retention_percentage: { value: string; scale: string }; - retention_amount: { value: string; scale: string; currency_code: string }; - - taxes_amount: { value: string; scale: string; currency_code: string }; - total_amount: { value: string; scale: string; currency_code: string }; -} diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-items-full-snapshot-builder.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-items-full-snapshot-builder.ts index a5570dbc..f30e2203 100644 --- a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-items-full-snapshot-builder.ts +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-items-full-snapshot-builder.ts @@ -1,4 +1,5 @@ import type { ISnapshotBuilder } from "@erp/core/api"; +import type { ProformaItemDetailDTO } from "@erp/customer-invoices/common"; import { maybeToEmptyMoneyObjectString, maybeToEmptyPercentageObjectString, @@ -9,13 +10,11 @@ import { import { ItemAmount, type ProformaItem, type ProformaItems } from "../../../../domain"; -import type { IProformaItemFullSnapshot } from "./proforma-item-full-snapshot.interface"; - export interface IProformaItemsFullSnapshotBuilder - extends ISnapshotBuilder {} + extends ISnapshotBuilder {} export class ProformaItemsFullSnapshotBuilder implements IProformaItemsFullSnapshotBuilder { - private mapItem(proformaItem: ProformaItem, index: number): IProformaItemFullSnapshot { + private mapItem(proformaItem: ProformaItem, index: number): ProformaItemDetailDTO { const allAmounts = proformaItem.totals(); const isValued = proformaItem.isValued(); @@ -77,7 +76,7 @@ export class ProformaItemsFullSnapshotBuilder implements IProformaItemsFullSnaps }; } - toOutput(invoiceItems: ProformaItems): IProformaItemFullSnapshot[] { + toOutput(invoiceItems: ProformaItems): ProformaItemDetailDTO[] { return invoiceItems.map((item, index) => this.mapItem(item, index)); } } diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-recipient-full-snapshot-builder.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-recipient-full-snapshot-builder.ts index 6d3637cb..07cd295d 100644 --- a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-recipient-full-snapshot-builder.ts +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-recipient-full-snapshot-builder.ts @@ -1,15 +1,14 @@ import type { ISnapshotBuilder } from "@erp/core/api"; import { DomainValidationError, maybeToNullable } from "@repo/rdx-ddd"; +import type { ProformaRecipientSummaryDTO } from "../../../../../common"; import type { InvoiceRecipient, Proforma } from "../../../../domain"; -import type { IProformaRecipientFullSnapshot } from "./proforma-recipient-full-snapshot.interface"; - export interface IProformaRecipientFullSnapshotBuilder - extends ISnapshotBuilder {} + extends ISnapshotBuilder {} export class ProformaRecipientFullSnapshotBuilder implements IProformaRecipientFullSnapshotBuilder { - toOutput(proforma: Proforma): IProformaRecipientFullSnapshot { + toOutput(proforma: Proforma): ProformaRecipientSummaryDTO { if (!proforma.recipient) { throw DomainValidationError.requiredValue("recipient", { cause: proforma, @@ -39,7 +38,7 @@ export class ProformaRecipientFullSnapshotBuilder implements IProformaRecipientF province: null, postal_code: null, country: null, - }) as IProformaRecipientFullSnapshot + }) as ProformaRecipientSummaryDTO ); } } diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-recipient-full-snapshot.interface.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-recipient-full-snapshot.interface.ts deleted file mode 100644 index 606d806f..00000000 --- a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-recipient-full-snapshot.interface.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * Fijarse en ProformaRecipientSummarySchema - */ - -export interface IProformaRecipientFullSnapshot { - id: string | null; - name: string | null; - tin: string | null; - street: string | null; - street2: string | null; - city: string | null; - province: string | null; - postal_code: string | null; - country: string | null; -} diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-tax-full-snapshot-interface.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-tax-full-snapshot-interface.ts deleted file mode 100644 index 09407b8f..00000000 --- a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-tax-full-snapshot-interface.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface IProformaTaxFullSnapshot { - taxable_amount: { value: string; scale: string; currency_code: string }; - - iva_code: string; - iva_percentage: { value: string; scale: string }; - iva_amount: { value: string; scale: string; currency_code: string }; - - rec_code: string; - rec_percentage: { value: string; scale: string }; - rec_amount: { value: string; scale: string; currency_code: string }; - - retention_code: string; - retention_percentage: { value: string; scale: string }; - retention_amount: { value: string; scale: string; currency_code: string }; - - taxes_amount: { value: string; scale: string; currency_code: string }; -} diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-taxes-full-snapshot-builder.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-taxes-full-snapshot-builder.ts index 5880c728..e130671b 100644 --- a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-taxes-full-snapshot-builder.ts +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/full/proforma-taxes-full-snapshot-builder.ts @@ -2,15 +2,14 @@ import type { ISnapshotBuilder } from "@erp/core/api"; import { maybeToEmptyPercentageObjectString, maybeToEmptyString } from "@repo/rdx-ddd"; import type { Collection } from "@repo/rdx-utils"; +import type { TaxesBreakdownDTO } from "../../../../../common"; import type { IProformaTaxTotals } from "../../../../domain"; -import type { IProformaTaxFullSnapshot } from "./proforma-tax-full-snapshot-interface"; - export interface IProformaTaxesFullSnapshotBuilder - extends ISnapshotBuilder, IProformaTaxFullSnapshot[]> {} + extends ISnapshotBuilder, TaxesBreakdownDTO[]> {} export class ProformaTaxesFullSnapshotBuilder implements IProformaTaxesFullSnapshotBuilder { - private mapItem(proformaTax: IProformaTaxTotals, index: number): IProformaTaxFullSnapshot { + private mapItem(proformaTax: IProformaTaxTotals, index: number): TaxesBreakdownDTO { return { taxable_amount: proformaTax.taxableAmount.toObjectString(), @@ -30,7 +29,7 @@ export class ProformaTaxesFullSnapshotBuilder implements IProformaTaxesFullSnaps }; } - toOutput(invoiceTaxes: Collection): IProformaTaxFullSnapshot[] { + toOutput(invoiceTaxes: Collection): TaxesBreakdownDTO[] { return invoiceTaxes.map((item, index) => this.mapItem(item, index)); } } diff --git a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/summary/proforma-summary-snapshot-builder.ts b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/summary/proforma-summary-snapshot-builder.ts index bc836a80..18c9fb61 100644 --- a/modules/customer-invoices/src/api/application/proformas/snapshot-builders/summary/proforma-summary-snapshot-builder.ts +++ b/modules/customer-invoices/src/api/application/proformas/snapshot-builders/summary/proforma-summary-snapshot-builder.ts @@ -14,7 +14,6 @@ export class ProformaSummarySnapshotBuilder implements IProformaSummarySnapshotB return { id: proforma.id.toString(), company_id: proforma.companyId.toString(), - is_proforma: proforma.isProforma, invoice_number: proforma.invoiceNumber.toString(), status: proforma.status.toPrimitive() as ProformaSummaryDTO["status"], diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/update-proforma.use-case.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/update-proforma.use-case.ts index 7bf201a3..3ea4a801 100644 --- a/modules/customer-invoices/src/api/application/proformas/use-cases/update-proforma.use-case.ts +++ b/modules/customer-invoices/src/api/application/proformas/use-cases/update-proforma.use-case.ts @@ -4,7 +4,7 @@ import { Result } from "@repo/rdx-utils"; import type { UpdateProformaByIdRequestDTO } from "../../../../common"; import type { ProformaPatchProps } from "../../../domain"; -import type { UpdateProformaInputMapper } from "../mappers"; +import type { IUpdateProformaInputMapper } from "../mappers"; import type { IProformaUpdater } from "../services"; import type { IProformaFullSnapshotBuilder } from "../snapshot-builders"; @@ -15,14 +15,14 @@ type UpdateProformaUseCaseInput = { }; type UpdateProformaUseCaseDeps = { - dtoMapper: UpdateProformaInputMapper; + dtoMapper: IUpdateProformaInputMapper; updater: IProformaUpdater; fullSnapshotBuilder: IProformaFullSnapshotBuilder; transactionManager: ITransactionManager; }; export class UpdateProformaUseCase { - private readonly dtoMapper: UpdateProformaInputMapper; + private readonly dtoMapper: IUpdateProformaInputMapper; private readonly updater: IProformaUpdater; private readonly fullSnapshotBuilder: IProformaFullSnapshotBuilder; private readonly transactionManager: ITransactionManager; diff --git a/modules/customer-invoices/src/api/application/snapshot-builders/reports/issued-invoices/issued-invoice.report.presenter.ts b/modules/customer-invoices/src/api/application/snapshot-builders/reports/issued-invoices/issued-invoice.report.presenter.ts index 3a40e217..44699b82 100644 --- a/modules/customer-invoices/src/api/application/snapshot-builders/reports/issued-invoices/issued-invoice.report.presenter.ts +++ b/modules/customer-invoices/src/api/application/snapshot-builders/reports/issued-invoices/issued-invoice.report.presenter.ts @@ -14,7 +14,7 @@ export class IssuedInvoiceReportPresenter extends Presenter< return ""; } - return paymentMethod.payment_description ?? ""; + return paymentMethod.description ?? ""; } toOutput(issuedInvoiceDTO: GetIssuedInvoiceByIdResponseDTO) { diff --git a/modules/customer-invoices/src/api/application/snapshot-builders/reports/proformas/proforma.report.presenter.ts b/modules/customer-invoices/src/api/application/snapshot-builders/reports/proformas/proforma.report.presenter.ts index 58276915..b876c0ed 100644 --- a/modules/customer-invoices/src/api/application/snapshot-builders/reports/proformas/proforma.report.presenter.ts +++ b/modules/customer-invoices/src/api/application/snapshot-builders/reports/proformas/proforma.report.presenter.ts @@ -9,7 +9,7 @@ export class ProformaReportPresenter extends Presenter; - paymentMethod: Maybe; + paymentMethodId: Maybe; items: IProformaItemCreateProps[]; globalDiscountPercentage: DiscountPercentage; } export type ProformaPatchProps = Partial> & { - items?: ProformaItemPatchProps[]; + items?: ProformaItemPatchProps[]; // update no es patch granular, sino reemplazo completo si se proporciona }; export interface IProformaTotals { @@ -100,7 +99,7 @@ export interface IProforma { languageCode: LanguageCode; currencyCode: CurrencyCode; - paymentMethod: Maybe; + paymentMethodId: Maybe; linkedInvoiceId: Maybe; @@ -176,8 +175,12 @@ export class Proforma extends AggregateRoot implements IP Object.assign(this.props, candidateProps); // Reemplazo de items (si se proporciona) - if (items) { - this.initializeItems(items); + if (items !== undefined) { + const initializeResult = this.initializeItems(items); + + if (initializeResult.isFailure) { + return Result.fail(initializeResult.error); + } } return Result.ok(); @@ -189,18 +192,11 @@ export class Proforma extends AggregateRoot implements IP this._items.reset(); for (const [index, itemProps] of itemsProps.entries()) { - const { languageCode, currencyCode, globalDiscountPercentage, ...restProps } = { + const itemResult = ProformaItem.create({ + ...itemProps, languageCode: this.languageCode, currencyCode: this.currencyCode, globalDiscountPercentage: this.globalDiscountPercentage, - ...itemProps, - }; - - const itemResult = ProformaItem.create({ - ...restProps, - languageCode, - currencyCode, - globalDiscountPercentage, }); if (itemResult.isFailure) { @@ -261,8 +257,8 @@ export class Proforma extends AggregateRoot implements IP return this.props.recipient; } - public get paymentMethod(): Maybe { - return this.props.paymentMethod; + public get paymentMethodId(): Maybe { + return this.props.paymentMethodId; } public get linkedInvoiceId(): Maybe { @@ -290,7 +286,7 @@ export class Proforma extends AggregateRoot implements IP } public get hasPaymentMethod() { - return this.paymentMethod.isSome(); + return this.paymentMethodId.isSome(); } public issue(): Result { @@ -355,7 +351,7 @@ export class Proforma extends AggregateRoot implements IP ); }*/ - if (this.paymentMethod.isNone()) { + if (this.paymentMethodId.isNone()) { return Result.fail( new DomainValidationError( "MISSING_PAYMENT_METHOD", diff --git a/modules/customer-invoices/src/common/dto/request/proformas/create-proforma.request.dto.ts b/modules/customer-invoices/src/common/dto/request/proformas/create-proforma.request.dto.ts index 30e7d5a7..e85d8c39 100644 --- a/modules/customer-invoices/src/common/dto/request/proformas/create-proforma.request.dto.ts +++ b/modules/customer-invoices/src/common/dto/request/proformas/create-proforma.request.dto.ts @@ -1,44 +1,68 @@ -import { NumericStringSchema, PercentageSchema } from "@erp/core"; +import { + CurrencyCodeSchema, + IsoDateSchema, + LanguageCodeSchema, + NumericStringSchema, + PercentageSchema, +} from "@erp/core"; import { z } from "zod/v4"; -export const CreateProformaItemRequestSchema = z.object({ - id: z.uuid(), - position: z.string(), - description: z.string().default(""), - quantity: NumericStringSchema.default(""), - unit_amount: NumericStringSchema.default(""), - item_discount_percentage: PercentageSchema.default({ - value: "0", - scale: "2", - }), - taxes: z.string().default(""), -}); +import { ItemPositionSchema, TaxCombinationCodeSchema } from "../../shared"; + +export const CreateProformaItemRequestSchema = z + .object({ + position: ItemPositionSchema, + + is_valued: z.boolean(), + description: z.string().nullable(), + + quantity: NumericStringSchema.nullable(), + unit_amount: NumericStringSchema.nullable(), + + item_discount_percentage: PercentageSchema.nullable(), + + taxes: TaxCombinationCodeSchema, + }) + .refine( + (item) => { + if (!item.is_valued) { + return item.quantity === null && item.unit_amount === null; + } + + return item.quantity !== null && item.unit_amount !== null; + }, + { + message: + "quantity and unit_amount must be null when is_valued is false and non-null when is_valued is true", + path: ["is_valued"], + } + ); + export type CreateProformaItemRequestDTO = z.infer; export const CreateProformaRequestSchema = z.object({ id: z.uuid(), invoice_number: z.string(), - series: z.string().default(""), + series: z.string().nullable(), - invoice_date: z.string(), - operation_date: z.string().default(""), + invoice_date: IsoDateSchema, + operation_date: IsoDateSchema.nullable().optional(), customer_id: z.uuid(), - reference: z.string().default(""), - notes: z.string().default(""), + reference: z.string().nullable().optional(), + description: z.string().nullable().optional(), + notes: z.string().nullable().optional(), - language_code: z.string().toLowerCase().default("es"), - currency_code: z.string().toUpperCase().default("EUR"), + language_code: LanguageCodeSchema, + currency_code: CurrencyCodeSchema, - global_discount_percentage: PercentageSchema.default({ - value: "0", - scale: "2", - }), + global_discount_percentage: PercentageSchema, - payment_method: z.string().default(""), + payment_method_id: z.uuid().nullable().optional(), - items: z.array(CreateProformaItemRequestSchema).default([]), + items: z.array(CreateProformaItemRequestSchema), }); + export type CreateProformaRequestDTO = z.infer; diff --git a/modules/customer-invoices/src/common/dto/request/proformas/update-proforma-by-id.request.dto.ts b/modules/customer-invoices/src/common/dto/request/proformas/update-proforma-by-id.request.dto.ts index 86e03ea6..f7c6e9f1 100644 --- a/modules/customer-invoices/src/common/dto/request/proformas/update-proforma-by-id.request.dto.ts +++ b/modules/customer-invoices/src/common/dto/request/proformas/update-proforma-by-id.request.dto.ts @@ -1,39 +1,71 @@ -import { NumericStringSchema, PercentageSchema } from "@erp/core"; +import { + CurrencyCodeSchema, + IsoDateSchema, + LanguageCodeSchema, + NumericStringSchema, + PercentageSchema, +} from "@erp/core"; import { z } from "zod/v4"; -export const UpdateProformaItemRequestSchema = z.object({ - id: z.uuid(), - position: z.string(), - description: z.string().default(""), - quantity: NumericStringSchema.default(""), - unit_amount: NumericStringSchema.default(""), - item_discount_percentage: PercentageSchema.default({ - value: "0", - scale: "2", - }), - taxes: z.string().default(""), -}); +import { ItemPositionSchema, TaxCombinationCodeSchema } from "../../shared"; + +export const UpdateProformaItemRequestSchema = z + .object({ + position: ItemPositionSchema, + is_valued: z.boolean(), + + description: z.string().nullable(), + + quantity: NumericStringSchema.nullable(), + unit_amount: NumericStringSchema.nullable(), + + item_discount_percentage: PercentageSchema.nullable(), + + taxes: TaxCombinationCodeSchema, + }) + .refine( + (item) => { + if (!item.is_valued) { + return item.quantity === null && item.unit_amount === null; + } + + return item.quantity !== null && item.unit_amount !== null; + }, + { + message: + "quantity and unit_amount must be null when is_valued is false and non-null when is_valued is true", + path: ["is_valued"], + } + ); export const UpdateProformaByIdParamsRequestSchema = z.object({ - proforma_id: z.string(), + proforma_id: z.uuid(), }); export const UpdateProformaByIdRequestSchema = z.object({ - series: z.string().optional(), + series: z.string().nullable().optional(), - invoice_date: z.string().optional(), - operation_date: z.string().optional(), + invoice_date: IsoDateSchema.optional(), + operation_date: IsoDateSchema.nullable().optional(), customer_id: z.uuid().optional(), - reference: z.string().optional(), - description: z.string().optional(), - notes: z.string().optional(), + reference: z.string().nullable().optional(), + description: z.string().nullable().optional(), + notes: z.string().nullable().optional(), - language_code: z.string().optional(), - currency_code: z.string().optional(), + language_code: LanguageCodeSchema.optional(), + currency_code: CurrencyCodeSchema.optional(), - items: z.array(UpdateProformaItemRequestSchema).default([]), + global_discount_percentage: PercentageSchema.optional(), + + payment_method_id: z.uuid().nullable().optional(), + + items: z.array(UpdateProformaItemRequestSchema).optional(), }); -export type UpdateProformaByIdRequestDTO = Partial>; +export type UpdateProformaByIdRequestDTO = z.infer; + +export type UpdateProformaByIdParamsRequestDTO = z.infer< + typeof UpdateProformaByIdParamsRequestSchema +>; diff --git a/modules/customer-invoices/src/common/dto/response/proformas/get-proforma-by-id.response.dto.ts b/modules/customer-invoices/src/common/dto/response/proformas/get-proforma-by-id.response.dto.ts index 5c1ad40f..88a00661 100644 --- a/modules/customer-invoices/src/common/dto/response/proformas/get-proforma-by-id.response.dto.ts +++ b/modules/customer-invoices/src/common/dto/response/proformas/get-proforma-by-id.response.dto.ts @@ -19,7 +19,6 @@ export const GetProformaByIdResponseSchema = z.object({ id: z.uuid(), company_id: z.uuid(), - is_proforma: z.boolean(), invoice_number: z.string(), status: ProformaStatusSchema, series: z.string().nullable(), @@ -39,7 +38,7 @@ export const GetProformaByIdResponseSchema = z.object({ linked_invoice_id: z.uuid().nullable(), - taxes: TaxesBreakdownSchema, + taxes: z.array(TaxesBreakdownSchema), payment_method: PaymentMethodRefSchema.nullable(), @@ -47,6 +46,7 @@ export const GetProformaByIdResponseSchema = z.object({ items_discount_amount: MoneySchema, global_discount_percentage: PercentageSchema, global_discount_amount: MoneySchema, + total_discount_amount: MoneySchema, taxable_amount: MoneySchema, iva_amount: MoneySchema, rec_amount: MoneySchema, diff --git a/modules/customer-invoices/src/common/dto/shared/index.ts b/modules/customer-invoices/src/common/dto/shared/index.ts index be9eb5ab..7e736965 100644 --- a/modules/customer-invoices/src/common/dto/shared/index.ts +++ b/modules/customer-invoices/src/common/dto/shared/index.ts @@ -1,5 +1,6 @@ export * from "./issued-invoices"; -export * from "./item-taxes-breakdown.dto"; -export * from "./payment-methof-ref.dto"; +export * from "./item-position.dto"; +export * from "./payment-method-ref.dto"; export * from "./proforma"; +export * from "./tax-combination-code.dto"; export * from "./taxes-breakdown.dto"; diff --git a/modules/customer-invoices/src/common/dto/shared/issued-invoices/issued-invoice-item-detail.dto.ts b/modules/customer-invoices/src/common/dto/shared/issued-invoices/issued-invoice-item-detail.dto.ts index 81edc691..73ae88a8 100644 --- a/modules/customer-invoices/src/common/dto/shared/issued-invoices/issued-invoice-item-detail.dto.ts +++ b/modules/customer-invoices/src/common/dto/shared/issued-invoices/issued-invoice-item-detail.dto.ts @@ -1,12 +1,13 @@ import { MoneySchema, PercentageSchema, QuantitySchema } from "@erp/core"; import { z } from "zod/v4"; -import { ItemTaxesBreakdownSchema } from "../item-taxes-breakdown.dto"; +import { ItemPositionSchema } from "../item-position.dto"; +import { TaxesBreakdownSchema } from "../taxes-breakdown.dto"; export const IssuedInvoiceItemDetailSchema = z.object({ id: z.uuid(), is_valued: z.boolean(), - position: z.number(), + position: ItemPositionSchema, description: z.string().nullable(), quantity: QuantitySchema, @@ -20,7 +21,7 @@ export const IssuedInvoiceItemDetailSchema = z.object({ global_discount_percentage: PercentageSchema, global_discount_amount: MoneySchema, - ...ItemTaxesBreakdownSchema.shape, + ...TaxesBreakdownSchema.shape, total_amount: MoneySchema, }); diff --git a/modules/customer-invoices/src/common/dto/shared/item-position.dto.ts b/modules/customer-invoices/src/common/dto/shared/item-position.dto.ts new file mode 100644 index 00000000..d5c493d3 --- /dev/null +++ b/modules/customer-invoices/src/common/dto/shared/item-position.dto.ts @@ -0,0 +1,5 @@ +import { z } from "zod/v4"; + +export const ItemPositionSchema = z.number().int().nonnegative(); + +export type ItemPositionDTO = z.infer; diff --git a/modules/customer-invoices/src/common/dto/shared/item-taxes-breakdown.dto.ts b/modules/customer-invoices/src/common/dto/shared/item-taxes-breakdown.dto.ts deleted file mode 100644 index df8b70e2..00000000 --- a/modules/customer-invoices/src/common/dto/shared/item-taxes-breakdown.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { z } from "zod/v4"; - -import { TaxesBreakdownSchema } from "./taxes-breakdown.dto"; - -export const ItemTaxesBreakdownSchema = TaxesBreakdownSchema; -export type ItemTaxesBreakdownDTO = z.infer; diff --git a/modules/customer-invoices/src/common/dto/shared/payment-methof-ref.dto.ts b/modules/customer-invoices/src/common/dto/shared/payment-method-ref.dto.ts similarity index 72% rename from modules/customer-invoices/src/common/dto/shared/payment-methof-ref.dto.ts rename to modules/customer-invoices/src/common/dto/shared/payment-method-ref.dto.ts index b48c2d08..1ab009e9 100644 --- a/modules/customer-invoices/src/common/dto/shared/payment-methof-ref.dto.ts +++ b/modules/customer-invoices/src/common/dto/shared/payment-method-ref.dto.ts @@ -1,8 +1,8 @@ import { z } from "zod/v4"; export const PaymentMethodRefSchema = z.object({ - payment_id: z.uuid(), - payment_description: z.string(), + id: z.uuid(), + description: z.string(), }); export type PaymentMethodRefDTO = z.infer; diff --git a/modules/customer-invoices/src/common/dto/shared/proforma/proforma-item-detail.dto.ts b/modules/customer-invoices/src/common/dto/shared/proforma/proforma-item-detail.dto.ts index 49077843..c924ef1f 100644 --- a/modules/customer-invoices/src/common/dto/shared/proforma/proforma-item-detail.dto.ts +++ b/modules/customer-invoices/src/common/dto/shared/proforma/proforma-item-detail.dto.ts @@ -1,12 +1,13 @@ import { MoneySchema, PercentageSchema, QuantitySchema } from "@erp/core"; import { z } from "zod/v4"; -import { ItemTaxesBreakdownSchema } from "../item-taxes-breakdown.dto"; +import { ItemPositionSchema } from "../item-position.dto"; +import { TaxesBreakdownSchema } from "../taxes-breakdown.dto"; export const ProformaItemDetailSchema = z.object({ id: z.uuid(), is_valued: z.boolean(), - position: z.number(), + position: ItemPositionSchema, description: z.string().nullable(), quantity: QuantitySchema, @@ -20,7 +21,9 @@ export const ProformaItemDetailSchema = z.object({ global_discount_percentage: PercentageSchema, global_discount_amount: MoneySchema, - ...ItemTaxesBreakdownSchema.shape, + total_discount_amount: MoneySchema, + + ...TaxesBreakdownSchema.shape, total_amount: MoneySchema, }); diff --git a/modules/customer-invoices/src/common/dto/shared/proforma/proforma-summary.dto.ts b/modules/customer-invoices/src/common/dto/shared/proforma/proforma-summary.dto.ts index 7e3c670a..63ae5ce7 100644 --- a/modules/customer-invoices/src/common/dto/shared/proforma/proforma-summary.dto.ts +++ b/modules/customer-invoices/src/common/dto/shared/proforma/proforma-summary.dto.ts @@ -7,7 +7,6 @@ import { ProformaStatusSchema } from "./proforma-status.dto"; export const ProformaSummarySchema = z.object({ id: z.uuid(), company_id: z.uuid(), - is_proforma: z.boolean(), invoice_number: z.string(), status: ProformaStatusSchema, diff --git a/modules/customer-invoices/src/common/dto/shared/tax-combination-code.dto.ts b/modules/customer-invoices/src/common/dto/shared/tax-combination-code.dto.ts new file mode 100644 index 00000000..b34b9cb8 --- /dev/null +++ b/modules/customer-invoices/src/common/dto/shared/tax-combination-code.dto.ts @@ -0,0 +1,23 @@ +import { z } from "zod/v4"; + +const TAX_CODE_PATTERN = /^[a-z0-9_]+$/i; +const EMPTY_TAX_SLOT = "#"; + +export const TaxCombinationCodeSchema = z.string().refine( + (value) => { + const parts = value.split(";"); + + if (parts.length !== 3) { + return false; + } + + return parts.every((part) => { + return part === EMPTY_TAX_SLOT || TAX_CODE_PATTERN.test(part); + }); + }, + { + message: "taxes must use format ';;'", + } +); + +export type TaxCombinationCodeDTO = z.infer; diff --git a/modules/customer-invoices/src/web/proformas/shared/adapters/get-proforma-by-id.adapter.ts b/modules/customer-invoices/src/web/proformas/shared/adapters/get-proforma-by-id.adapter.ts index 04530f56..f7216764 100644 --- a/modules/customer-invoices/src/web/proformas/shared/adapters/get-proforma-by-id.adapter.ts +++ b/modules/customer-invoices/src/web/proformas/shared/adapters/get-proforma-by-id.adapter.ts @@ -44,7 +44,7 @@ export const GetProformaByIdAdapter = { recipient: mapRecipient(dto.recipient), taxes: dto.taxes.map(mapTaxSummary), - paymentMethod: dto.payment_method?.payment_id, + paymentMethod: dto.payment_method?.id, subtotalAmount: MoneyDTOHelper.toNumber(dto.subtotal_amount), diff --git a/modules/customers/src/api/application/mappers/update-customer-input.mapper.ts b/modules/customers/src/api/application/mappers/update-customer-input.mapper.ts index fa3a7b64..cf391084 100644 --- a/modules/customers/src/api/application/mappers/update-customer-input.mapper.ts +++ b/modules/customers/src/api/application/mappers/update-customer-input.mapper.ts @@ -55,8 +55,6 @@ export class UpdateCustomerInputMapper implements IUpdateCustomerInputMapper { dto: UpdateCustomerByIdRequestDTO, params: { companyId: UniqueID } ): Result { - console.log("Mapping UpdateCustomerByIdRequestDTO to CustomerPatchProps:", dto); - try { const errors: ValidationErrorDetail[] = []; const customerPatchProps: CustomerPatchProps = {}; diff --git a/modules/customers/src/common/dto/request/update-customer-by-id.request.dto.ts b/modules/customers/src/common/dto/request/update-customer-by-id.request.dto.ts index 7d21e9d9..a60a53a1 100644 --- a/modules/customers/src/common/dto/request/update-customer-by-id.request.dto.ts +++ b/modules/customers/src/common/dto/request/update-customer-by-id.request.dto.ts @@ -1,7 +1,9 @@ import { CountryCodeSchema, + CurrencyCodeSchema, EmailSchema, LandPhoneSchema, + LanguageCodeSchema, MobilePhoneSchema, PostalCodeSchema, TinSchema, @@ -9,6 +11,8 @@ import { } from "@erp/core"; import { z } from "zod/v4"; +import { TaxCombinationCodeSchema } from "../shared"; + export const UpdateCustomerByIdParamsRequestSchema = z.object({ customer_id: z.uuid(), }); @@ -45,15 +49,15 @@ export const UpdateCustomerByIdRequestSchema = z.object({ trade_name: z.string().nullable().optional(), tin: TinSchema.nullable().optional(), - default_taxes: z.string().nullable().optional(), + default_taxes: TaxCombinationCodeSchema.optional(), address: UpdateCustomerAddressPatchRequestSchema.optional(), contact: UpdateCustomerContactPatchRequestSchema.optional(), legal_record: z.string().nullable().optional(), - language_code: z.string().optional(), - currency_code: z.string().optional(), + language_code: LanguageCodeSchema.optional(), + currency_code: CurrencyCodeSchema.optional(), }); export type UpdateCustomerAddressPatchRequestDTO = z.infer< diff --git a/modules/customers/src/common/dto/response/get-customer-by-id.response.dto.ts b/modules/customers/src/common/dto/response/get-customer-by-id.response.dto.ts index 3132b3eb..81fbf17b 100644 --- a/modules/customers/src/common/dto/response/get-customer-by-id.response.dto.ts +++ b/modules/customers/src/common/dto/response/get-customer-by-id.response.dto.ts @@ -12,6 +12,7 @@ import { } from "@erp/core"; import { z } from "zod/v4"; +import { TaxCombinationCodeSchema } from "../shared"; import { CustomerStatusSchema } from "../shared/customer-status.dto"; export const GetCustomerByIdResponseSchema = z.object({ @@ -48,7 +49,7 @@ export const GetCustomerByIdResponseSchema = z.object({ legal_record: z.string().nullable(), - default_taxes: z.string().nullable(), + default_taxes: TaxCombinationCodeSchema, language_code: LanguageCodeSchema, currency_code: CurrencyCodeSchema, diff --git a/modules/customers/src/common/dto/shared/index.ts b/modules/customers/src/common/dto/shared/index.ts index 806f8523..ea0d72f9 100644 --- a/modules/customers/src/common/dto/shared/index.ts +++ b/modules/customers/src/common/dto/shared/index.ts @@ -1,2 +1,3 @@ export * from "./customer-status.dto"; export * from "./customer-summary.dto"; +export * from "./tax-combination-code.dto"; diff --git a/modules/customers/src/common/dto/shared/tax-combination-code.dto.ts b/modules/customers/src/common/dto/shared/tax-combination-code.dto.ts new file mode 100644 index 00000000..b34b9cb8 --- /dev/null +++ b/modules/customers/src/common/dto/shared/tax-combination-code.dto.ts @@ -0,0 +1,23 @@ +import { z } from "zod/v4"; + +const TAX_CODE_PATTERN = /^[a-z0-9_]+$/i; +const EMPTY_TAX_SLOT = "#"; + +export const TaxCombinationCodeSchema = z.string().refine( + (value) => { + const parts = value.split(";"); + + if (parts.length !== 3) { + return false; + } + + return parts.every((part) => { + return part === EMPTY_TAX_SLOT || TAX_CODE_PATTERN.test(part); + }); + }, + { + message: "taxes must use format ';;'", + } +); + +export type TaxCombinationCodeDTO = z.infer; diff --git a/modules/supplier/src/common/dto/response/get-supplier-by-id.response.dto.ts b/modules/supplier/src/common/dto/response/get-supplier-by-id.response.dto.ts index 31c5fcab..b6e0e4af 100644 --- a/modules/supplier/src/common/dto/response/get-supplier-by-id.response.dto.ts +++ b/modules/supplier/src/common/dto/response/get-supplier-by-id.response.dto.ts @@ -36,7 +36,7 @@ export const GetSupplierByIdResponseSchema = z.object({ legal_record: z.string(), - default_taxes: z.array(z.string()), + default_taxes: TaxCombinationCodeSchema, status: z.string(), language_code: LanguageCodeSchema, currency_code: CurrencyCodeSchema,