diff --git a/modules/core/src/common/helpers/number-helper.ts b/modules/core/src/common/helpers/number-helper.ts index e7d95c50..eba54410 100644 --- a/modules/core/src/common/helpers/number-helper.ts +++ b/modules/core/src/common/helpers/number-helper.ts @@ -1,5 +1,5 @@ -const toSafeNumber = (value: number | null | undefined): number => { - return value ?? 0; +const toSafeNumber = (value: string | number | null | undefined): number => { + return Number(value ?? 0); }; export const NumberHelper = { diff --git a/modules/core/src/web/lib/helpers/form-utils.ts b/modules/core/src/web/lib/helpers/form-utils.ts index bc85ba3e..98cb9f4b 100644 --- a/modules/core/src/web/lib/helpers/form-utils.ts +++ b/modules/core/src/web/lib/helpers/form-utils.ts @@ -1,28 +1,54 @@ +type PickFormDirtyValuesOptions> = { + replaceTopLevelArrayKeys?: Array; +}; + /** * Extrae solo los valores marcados como "dirty" por react-hook-form, * respetando la estructura anidada de dirtyFields. + * + * Regla especial opcional: Si una key de primer nivel + * está configurada en `replaceTopLevelArrayKeys` y tiene + * cualquier dirty anidado, se incluye el array completo + * en lugar de intentar hacer patch recursivo */ export function pickFormDirtyValues>( values: T, - dirtyFields: Partial> + dirtyFields: Partial>, + options?: PickFormDirtyValuesOptions ): Partial { const result: Partial = {}; + const replaceTopLevelArrayKeys = options?.replaceTopLevelArrayKeys ?? []; for (const key in dirtyFields) { if (!Object.hasOwn(dirtyFields, key)) continue; - const isDirty = dirtyFields[key]; - const value = values[key]; + const typedKey = key as keyof T; + const isDirty = dirtyFields[typedKey]; + const value = values[typedKey]; if (isDirty === true) { - // 🔹 Campo "leaf": se ha tocado → copiar valor - result[key] = value; - } else if (typeof isDirty === "object" && isDirty !== null) { - // 🔹 Campo anidado: recursión - const nested = pickFormDirtyValues(value, isDirty); - if (Object.keys(nested).length > 0) { - result[key] = nested as any; - } + result[typedKey] = value; + continue; + } + + if (typeof isDirty !== "object" || isDirty === null) { + continue; + } + + const shouldReplaceTopLevelArray = + replaceTopLevelArrayKeys.includes(typedKey) && + Array.isArray(value) && + formHasAnyDirty(isDirty); + + if (shouldReplaceTopLevelArray) { + result[typedKey] = value; + continue; + } + + const nested = pickFormDirtyValues(value, isDirty as Partial>); + + if (Object.keys(nested).length > 0) { + result[typedKey] = nested as T[keyof T]; } } @@ -37,7 +63,7 @@ export function formHasAnyDirty(dirtyFields: Partial> | bool if (dirtyFields === false || dirtyFields == null) return false; if (typeof dirtyFields === "object") { - return Object.values(dirtyFields).some((v) => formHasAnyDirty(v)); + return Object.values(dirtyFields).some((value) => formHasAnyDirty(value)); } return false; diff --git a/modules/customer-invoices/src/api/application/proformas/di/proforma-input-mappers.di.ts b/modules/customer-invoices/src/api/application/proformas/di/proforma-input-mappers.di.ts index 7bf77e48..0d81dace 100644 --- a/modules/customer-invoices/src/api/application/proformas/di/proforma-input-mappers.di.ts +++ b/modules/customer-invoices/src/api/application/proformas/di/proforma-input-mappers.di.ts @@ -3,13 +3,12 @@ import type { ICatalogs } from "@erp/core/api"; import { CreateProformaInputMapper, type ICreateProformaInputMapper, - type IUpdateProformaInputMapper, UpdateProformaInputMapper, } from "../mappers"; export interface IProformaInputMappers { createInputMapper: ICreateProformaInputMapper; - updateInputMapper: IUpdateProformaInputMapper; + updateInputMapper: UpdateProformaInputMapper; } export const buildProformaInputMappers = (catalogs: ICatalogs): IProformaInputMappers => { diff --git a/modules/customer-invoices/src/api/application/proformas/di/proforma-use-cases.di.ts b/modules/customer-invoices/src/api/application/proformas/di/proforma-use-cases.di.ts index 941b5f53..19f92eae 100644 --- a/modules/customer-invoices/src/api/application/proformas/di/proforma-use-cases.di.ts +++ b/modules/customer-invoices/src/api/application/proformas/di/proforma-use-cases.di.ts @@ -1,7 +1,7 @@ import type { ITransactionManager } from "@erp/core/api"; import type { IIssuedInvoicePublicServices } from "../../issued-invoices"; -import type { ICreateProformaInputMapper, IUpdateProformaInputMapper } from "../mappers"; +import type { ICreateProformaInputMapper, UpdateProformaInputMapper } from "../mappers"; import type { IProformaCreator, IProformaFinder, @@ -97,7 +97,7 @@ export function buildIssueProformaUseCase(deps: { export function buildUpdateProformaUseCase(deps: { updater: IProformaUpdater; - dtoMapper: IUpdateProformaInputMapper; + dtoMapper: UpdateProformaInputMapper; fullSnapshotBuilder: IProformaFullSnapshotBuilder; transactionManager: ITransactionManager; }) { 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 32d2a3cb..860124ee 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 @@ -1,3 +1,5 @@ +import { NumberHelper } from "@erp/core"; +import { DiscountPercentage } from "@erp/core/api"; import { CurrencyCode, DomainError, @@ -13,7 +15,14 @@ import { import { Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils"; import type { UpdateProformaByIdRequestDTO } from "../../../../common/dto"; -import { InvoiceSerie, type ProformaPatchProps } from "../../../domain"; +import { + InvoiceSerie, + ItemAmount, + ItemDescription, + ItemQuantity, + type ProformaItemPatchProps, + type ProformaPatchProps, +} from "../../../domain"; /** * UpdateProformaPropsMapper @@ -37,7 +46,10 @@ export interface IUpdateProformaInputMapper { } export class UpdateProformaInputMapper implements IUpdateProformaInputMapper { - public map(dto: UpdateProformaByIdRequestDTO, params: { companyId: UniqueID }) { + public map( + dto: UpdateProformaByIdRequestDTO, + params: { companyId: UniqueID } + ): Result { try { const errors: ValidationErrorDetail[] = []; const props: ProformaPatchProps = {}; @@ -128,15 +140,149 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper { ); }); - if (errors.length > 0) { - return Result.fail( - new ValidationErrorCollection("Proforma invoice props mapping failed (update)", errors) - ); + if (dto.items) { + const itemsProps = this.mapItemsProps(dto, { errors }); + props.items = itemsProps; } + this.throwIfValidationErrors(errors); + return Result.ok(props); } catch (err: unknown) { - return Result.fail(new DomainError("Proforma invoice props mapping failed", { cause: err })); + 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); + } + } + + private mapItemsProps( + dto: UpdateProformaByIdRequestDTO, + params: { + errors: ValidationErrorDetail[]; + } + ): ProformaItemPatchProps[] { + const itemsProps: ProformaItemPatchProps[] = []; + + dto.items?.forEach((item, index) => { + const description = extractOrPushError( + maybeFromNullableResult(item.description, (v) => ItemDescription.create(v)), + `items[${index}].description`, + params.errors + ); + + const quantity = extractOrPushError( + maybeFromNullableResult(item.quantity, (v) => + ItemQuantity.create({ value: NumberHelper.toSafeNumber(v) }) + ), + `items[${index}].quantity`, + params.errors + ); + + const unitAmount = extractOrPushError( + maybeFromNullableResult(item.unit_amount, (v) => + ItemAmount.create({ value: NumberHelper.toSafeNumber(v) }) + ), + `items[${index}].unit_amount`, + params.errors + ); + + const discountPercentage = extractOrPushError( + maybeFromNullableResult(item.item_discount_percentage, (v) => + DiscountPercentage.create({ value: NumberHelper.toSafeNumber(v.value) }) + ), + `items[${index}].discount_percentage`, + params.errors + ); + + /*const taxes = this.mapTaxesProps(item.taxes, { + itemIndex: index, + errors: params.errors, + });*/ + + this.throwIfValidationErrors(params.errors); + + itemsProps.push({ + description: description!, + quantity: quantity!, + unitAmount: unitAmount!, + itemDiscountPercentage: discountPercentage!, + //taxes, + }); + }); + + return itemsProps; + } + + /* Devuelve las propiedades de los impustos de una línea de detalle */ + + /*private mapTaxesProps( + taxesDTO: Pick["taxes"], + params: { itemIndex: number; errors: ValidationErrorDetail[] } + ): ProformaItemTaxesProps { + const { itemIndex, errors } = params; + + const taxesProps: ProformaItemTaxesProps = { + iva: Maybe.none(), + retention: Maybe.none(), + rec: Maybe.none(), + }; + + // Normaliza: "" -> [] + 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); + } + }); + + this.throwIfValidationErrors(errors); + + return taxesProps; + }*/ } diff --git a/modules/customer-invoices/src/api/application/proformas/services/proforma-creator.ts b/modules/customer-invoices/src/api/application/proformas/services/proforma-creator.ts index 702a216e..8c3ef4bd 100644 --- a/modules/customer-invoices/src/api/application/proformas/services/proforma-creator.ts +++ b/modules/customer-invoices/src/api/application/proformas/services/proforma-creator.ts @@ -1,7 +1,7 @@ import type { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import { type IProformaCreateProps, Proforma } from "../../../domain"; +import { Proforma, type ProformaCreateProps } from "../../../domain"; import type { IProformaRepository } from "../repositories"; import type { IProformaNumberGenerator } from "./proforma-number-generator.interface"; @@ -9,7 +9,7 @@ import type { IProformaNumberGenerator } from "./proforma-number-generator.inter export interface IProformaCreatorParams { companyId: UniqueID; id: UniqueID; - props: Omit; + props: Omit; transaction: unknown; } diff --git a/modules/customer-invoices/src/api/application/proformas/services/proforma-updater.ts b/modules/customer-invoices/src/api/application/proformas/services/proforma-updater.ts index 128c685a..f45b9068 100644 --- a/modules/customer-invoices/src/api/application/proformas/services/proforma-updater.ts +++ b/modules/customer-invoices/src/api/application/proformas/services/proforma-updater.ts @@ -8,7 +8,7 @@ export interface IProformaUpdater { update(params: { companyId: UniqueID; id: UniqueID; - props: ProformaPatchProps; + patchProps: ProformaPatchProps; transaction: unknown; }): Promise>; } @@ -27,12 +27,12 @@ export class ProformaUpdater implements IProformaUpdater { async update(params: { companyId: UniqueID; id: UniqueID; - props: ProformaPatchProps; + patchProps: ProformaPatchProps; transaction: unknown; }): Promise> { - const { companyId, id, props, transaction } = params; + const { companyId, id, patchProps, transaction } = params; - console.log("props => ", props); + console.log("patchProps => ", patchProps); // Recuperar agregado existente const existingResult = await this.repository.getByIdInCompany(companyId, id, transaction); @@ -44,14 +44,12 @@ export class ProformaUpdater implements IProformaUpdater { const proforma = existingResult.data; // Aplicar cambios en el agregado - const updateResult = proforma.update(props); + const updateResult = proforma.update(patchProps); if (updateResult.isFailure) { return Result.fail(updateResult.error); } - console.log(proforma.operationDate); - // Persistir cambios const saveResult = await this.repository.update(proforma, transaction); 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 b394007c..7bf201a3 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 { IUpdateProformaInputMapper } from "../mappers"; +import type { UpdateProformaInputMapper } from "../mappers"; import type { IProformaUpdater } from "../services"; import type { IProformaFullSnapshotBuilder } from "../snapshot-builders"; @@ -15,14 +15,14 @@ type UpdateProformaUseCaseInput = { }; type UpdateProformaUseCaseDeps = { - dtoMapper: IUpdateProformaInputMapper; + dtoMapper: UpdateProformaInputMapper; updater: IProformaUpdater; fullSnapshotBuilder: IProformaFullSnapshotBuilder; transactionManager: ITransactionManager; }; export class UpdateProformaUseCase { - private readonly dtoMapper: IUpdateProformaInputMapper; + private readonly dtoMapper: UpdateProformaInputMapper; private readonly updater: IProformaUpdater; private readonly fullSnapshotBuilder: IProformaFullSnapshotBuilder; private readonly transactionManager: ITransactionManager; @@ -60,7 +60,7 @@ export class UpdateProformaUseCase { const updateResult = await this.updater.update({ companyId, id: proformaId, - props: patchProps, + patchProps, transaction, }); diff --git a/modules/customer-invoices/src/api/domain/proformas/aggregates/proforma.aggregate.ts b/modules/customer-invoices/src/api/domain/proformas/aggregates/proforma.aggregate.ts index ed6410a8..6f683e9f 100644 --- a/modules/customer-invoices/src/api/domain/proformas/aggregates/proforma.aggregate.ts +++ b/modules/customer-invoices/src/api/domain/proformas/aggregates/proforma.aggregate.ts @@ -24,6 +24,7 @@ import { type IProformaItemCreateProps, type IProformaItems, ProformaItem, + type ProformaItemPatchProps, ProformaItems, } from "../entities"; import { ProformaItemMismatch } from "../errors"; @@ -56,6 +57,10 @@ export interface IProformaCreateProps { globalDiscountPercentage: DiscountPercentage; } +export type ProformaPatchProps = Partial> & { + items?: ProformaItemPatchProps[]; +}; + export interface IProformaTotals { subtotalAmount: InvoiceAmount; @@ -100,10 +105,6 @@ export interface IProforma { totals(): IProformaTotals; } -export type ProformaPatchProps = Partial> & { - //items?: ProformaItems; -}; - export type InternalProformaProps = Omit; export class Proforma extends AggregateRoot implements IProforma { @@ -156,9 +157,47 @@ export class Proforma extends AggregateRoot implements IP return new Proforma(props, items, id); } - private initializeItems(itemsProps: IProformaItemCreateProps[]): Result { + // Mutabilidad + public update(patchProps: ProformaPatchProps): Result { + const { items, ...otherProps } = patchProps; + + const candidateProps: InternalProformaProps = { + ...this.props, + ...otherProps, + }; + + // Validacciones + + // Aplicar cambios + Object.assign(this.props, candidateProps); + + // Reemplazo de items (si se proporciona) + if (items) { + this.initializeItems(items); + } + + return Result.ok(); + } + + private initializeItems( + itemsProps: IProformaItemCreateProps[] | ProformaItemPatchProps[] + ): Result { + this._items.reset(); + for (const [index, itemProps] of itemsProps.entries()) { - const itemResult = ProformaItem.create(itemProps); + const { languageCode, currencyCode, globalDiscountPercentage, ...restProps } = { + languageCode: this.languageCode, + currencyCode: this.currencyCode, + globalDiscountPercentage: this.globalDiscountPercentage, + ...itemProps, + }; + + const itemResult = ProformaItem.create({ + ...restProps, + languageCode, + currencyCode, + globalDiscountPercentage, + }); if (itemResult.isFailure) { return Result.fail(itemResult.error); @@ -246,20 +285,6 @@ export class Proforma extends AggregateRoot implements IP return this.paymentMethod.isSome(); } - // Mutabilidad - public update(patch: ProformaPatchProps): Result { - const candidateProps: InternalProformaProps = { - ...this.props, - ...patch, - }; - - // Validacciones - - Object.assign(this.props, candidateProps); - - return Result.ok(); - } - public issue(): Result { if (!this.props.status.canTransitionTo("issued")) { return Result.fail( @@ -274,7 +299,6 @@ export class Proforma extends AggregateRoot implements IP this.props.status = InvoiceStatus.issued(); return Result.ok(); } - // Cálculos /** @@ -326,25 +350,6 @@ export class Proforma extends AggregateRoot implements IP return Result.ok(); } - /*public updateItem(itemId: UniqueID, props: IProformaItemProps): Result { - const item = this._items.find((i) => i.id.equals(itemId)); - if (!item) { - return Result.fail(new Error("Item not found")); - } - - return item.update(props); - }*/ - - /*public removeItem(itemId: UniqueID): Result { - const removed = this._items.removeWhere(i => i.id.equals(itemId)); - - if (!removed) { - return Result.fail(new Error("Item not found")); - } - - return Result.ok(); - }*/ - // Helpers /** diff --git a/modules/customer-invoices/src/api/domain/proformas/entities/proforma-items/proforma-item.entity.ts b/modules/customer-invoices/src/api/domain/proformas/entities/proforma-items/proforma-item.entity.ts index 1987e136..9384e36d 100644 --- a/modules/customer-invoices/src/api/domain/proformas/entities/proforma-items/proforma-item.entity.ts +++ b/modules/customer-invoices/src/api/domain/proformas/entities/proforma-items/proforma-item.entity.ts @@ -42,6 +42,11 @@ export interface IProformaItemCreateProps { currencyCode: CurrencyCode; // Para cálculos y formateos de moneda } +export type ProformaItemPatchProps = Omit< + IProformaItemCreateProps, + "globalDiscountPercentage" | "languageCode" | "currencyCode" +>; + export interface IProformaItemTotals { subtotalAmount: ItemAmount; diff --git a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/customer-invoice-item.mapper.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/customer-invoice-item.mapper.ts index 2c425240..e10675f0 100644 --- a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/customer-invoice-item.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/customer-invoice-item.mapper.ts @@ -17,12 +17,12 @@ import { import { Result } from "@repo/rdx-utils"; import { - type IProformaCreateProps, IssuedInvoiceItem, ItemAmount, ItemDescription, ItemQuantity, type Proforma, + type ProformaCreateProps, } from "../../../../../../domain"; export interface ICustomerInvoiceItemDomainMapper @@ -62,7 +62,7 @@ export class CustomerInvoiceItemDomainMapper const { errors, index, attributes } = params as { index: number; errors: ValidationErrorDetail[]; - attributes: Partial; + attributes: Partial; }; const itemId = extractOrPushError( @@ -157,7 +157,7 @@ export class CustomerInvoiceItemDomainMapper const { errors, index } = params as { index: number; errors: ValidationErrorDetail[]; - attributes: Partial; + attributes: Partial; }; // 1) Valores escalares (atributos generales) diff --git a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/customer-invoice.mapper.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/customer-invoice.mapper.ts index 216679da..42d782ad 100644 --- a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/customer-invoice.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/customer-invoice.mapper.ts @@ -20,12 +20,12 @@ import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils"; import { CustomerInvoiceItems, - type IProformaCreateProps, InvoiceNumber, InvoicePaymentMethod, InvoiceSerie, InvoiceStatus, Proforma, + type ProformaCreateProps, } from "../../../../../../domain"; import type { CustomerInvoiceCreationAttributes, @@ -249,7 +249,7 @@ export class CustomerInvoiceDomainMapper items: itemsResults.data.getAll(), }); - const invoiceProps: IProformaCreateProps = { + const invoiceProps: ProformaCreateProps = { companyId: attributes.companyId!, isProforma: attributes.isProforma, diff --git a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/invoice-recipient.mapper.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/invoice-recipient.mapper.ts index 1bb03df2..74e63989 100644 --- a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/invoice-recipient.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/invoice-recipient.mapper.ts @@ -16,9 +16,9 @@ import { import { Maybe, Result } from "@repo/rdx-utils"; import { - type IProformaCreateProps, InvoiceRecipient, type Proforma, + type ProformaCreateProps, } from "../../../../../../domain"; import type { CustomerInvoiceModel } from "../../../../sequelize"; @@ -34,7 +34,7 @@ export class InvoiceRecipientDomainMapper { const { errors, attributes } = params as { errors: ValidationErrorDetail[]; - attributes: Partial; + attributes: Partial; }; const { isProforma } = attributes; diff --git a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/invoice-verifactu.mapper.ts b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/invoice-verifactu.mapper.ts index a08d33e2..bdfe1d90 100644 --- a/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/invoice-verifactu.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/common/persistence/sequelize/mappers/domain/invoice-verifactu.mapper.ts @@ -12,8 +12,8 @@ import { import { Maybe, Result } from "@repo/rdx-utils"; import { - type IProformaCreateProps, type Proforma, + type ProformaCreateProps, VerifactuRecord, VerifactuRecordEstado, } from "../../../../../../domain"; @@ -43,7 +43,7 @@ export class CustomerInvoiceVerifactuDomainMapper ): Result, Error> { const { errors, attributes } = params as { errors: ValidationErrorDetail[]; - attributes: Partial; + attributes: Partial; }; if (!source) { diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/express/mappers/create-proforma-request-mapper.ts b/modules/customer-invoices/src/api/infrastructure/proformas/express/mappers/create-proforma-request-mapper.ts index 9db40437..cad11824 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/express/mappers/create-proforma-request-mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/express/mappers/create-proforma-request-mapper.ts @@ -17,7 +17,6 @@ import { Maybe, Result } from "@repo/rdx-utils"; import type { CreateProformaItemRequestDTO, CreateProformaRequestDTO } from "../../../../../common"; import { - type IProformaCreateProps, type IProformaItemCreateProps, InvoiceNumber, InvoicePaymentMethod, @@ -29,6 +28,7 @@ import { ItemAmount, ItemDescription, ItemQuantity, + type ProformaCreateProps, } from "../../../../domain"; /** @@ -149,7 +149,7 @@ export class CreateProformaRequestMapper { ); } - const proformaProps: Omit & { items: IProformaItemCreateProps[] } = { + const proformaProps: Omit & { items: IProformaItemCreateProps[] } = { companyId, status: defaultStatus!, diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-item-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-item-domain.mapper.ts index a0afe5e6..801602fd 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-item-domain.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-item-domain.mapper.ts @@ -16,12 +16,12 @@ import { import { Result } from "@repo/rdx-utils"; import { - type IProformaCreateProps, type IProformaItemCreateProps, ItemAmount, ItemDescription, ItemQuantity, type Proforma, + type ProformaCreateProps, ProformaItem, ProformaItemTaxes, type ProformaItemTaxesProps, @@ -58,7 +58,7 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper< const { errors, index, parent } = params as { index: number; errors: ValidationErrorDetail[]; - parent: Partial; + parent: Partial; }; const itemId = extractOrPushError( @@ -139,7 +139,7 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper< const { errors, index } = params as { index: number; errors: ValidationErrorDetail[]; - parent: Partial; + parent: Partial; }; // 1) Valores escalares (atributos generales) diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-recipient-domain.mapper.ts b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-recipient-domain.mapper.ts index e839e524..508c96f2 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-recipient-domain.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-recipient-domain.mapper.ts @@ -14,7 +14,7 @@ import { } from "@repo/rdx-ddd"; import { Maybe, Result } from "@repo/rdx-utils"; -import { type IProformaCreateProps, InvoiceRecipient } from "../../../../../../domain"; +import { InvoiceRecipient, type ProformaCreateProps } from "../../../../../../domain"; import type { CustomerInvoiceModel } from "../../../../../common"; export class SequelizeProformaRecipientDomainMapper { @@ -28,7 +28,7 @@ export class SequelizeProformaRecipientDomainMapper { const { errors, parent } = params as { errors: ValidationErrorDetail[]; - parent: Partial; + parent: Partial; }; const _name = source.current_customer.name; 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 422a5729..86e03ea6 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 @@ -4,11 +4,14 @@ import { z } from "zod/v4"; export const UpdateProformaItemRequestSchema = z.object({ id: z.uuid(), position: z.string(), - description: z.string().optional(), - quantity: NumericStringSchema.optional(), - unit_amount: NumericStringSchema.optional(), - item_discount_percentage: PercentageSchema.optional(), - taxes: z.string().optional(), + description: z.string().default(""), + quantity: NumericStringSchema.default(""), + unit_amount: NumericStringSchema.default(""), + item_discount_percentage: PercentageSchema.default({ + value: "0", + scale: "2", + }), + taxes: z.string().default(""), }); export const UpdateProformaByIdParamsRequestSchema = z.object({ diff --git a/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-controller.ts b/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-controller.ts index 9695d1f0..0ed38981 100644 --- a/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-controller.ts +++ b/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-controller.ts @@ -1,3 +1,4 @@ +import { formHasAnyDirty } from "@erp/core/client"; import { useHookForm } from "@erp/core/hooks"; import type { CustomerSelectionOption } from "@erp/customers"; import { showErrorToast, showSuccessToast, showWarningToast } from "@repo/rdx-ui/helpers"; @@ -114,12 +115,19 @@ export const useUpdateProformaController = ( return; } + console.log(form.formState.dirtyFields); + + if (!formHasAnyDirty(form.formState.dirtyFields)) { + showWarningToast( + t("proformas.update.no_changes.title"), + t("proformas.update.no_changes.message") + ); + return; + } + const previousData = proformaData; const patchData = buildProformaUpdatePatch(formData, form.formState.dirtyFields); - - console.log(patchData); - const params = buildUpdateProformaByIdParams(proformaId, patchData); try { @@ -132,7 +140,7 @@ export const useUpdateProformaController = ( keepDirty: false, }); - setSelectedCustomer(mapProformaToSelectedCustomer(proformaData)); + setSelectedCustomer(mapProformaToSelectedCustomer(updated)); if (options?.successToasts !== false) { showSuccessToast( @@ -153,6 +161,8 @@ export const useUpdateProformaController = ( { keepDirty: false } ); + setSelectedCustomer(previousData ? mapProformaToSelectedCustomer(previousData) : null); + if (options?.errorToasts !== false) { showErrorToast(t("proformas.update.error.title"), normalizedError.message); } diff --git a/modules/customer-invoices/src/web/proformas/update/utils/build-proforma-items-update-patch.ts b/modules/customer-invoices/src/web/proformas/update/utils/build-proforma-items-update-patch.ts deleted file mode 100644 index 34d799a0..00000000 --- a/modules/customer-invoices/src/web/proformas/update/utils/build-proforma-items-update-patch.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { ProformaItemUpdateForm, ProformaItemUpdatePatch } from "../entities"; - -export const buildProformaItemsUpdatePatch = ( - items: ProformaItemUpdateForm[] -): ProformaItemUpdatePatch[] => { - return items.map((item, index) => ({ - id: item.id, - position: index, - description: item.description.trim(), - quantity: item.quantity, - unitAmount: item.unitAmount, - itemDiscountPercentage: item.itemDiscountPercentage, - })); -}; diff --git a/modules/customer-invoices/src/web/proformas/update/utils/build-proforma-update-patch.ts b/modules/customer-invoices/src/web/proformas/update/utils/build-proforma-update-patch.ts index 0ea7a82c..ee6256f7 100644 --- a/modules/customer-invoices/src/web/proformas/update/utils/build-proforma-update-patch.ts +++ b/modules/customer-invoices/src/web/proformas/update/utils/build-proforma-update-patch.ts @@ -3,8 +3,6 @@ import type { FieldNamesMarkedBoolean } from "react-hook-form"; import type { ProformaUpdateForm, ProformaUpdatePatch } from "../entities"; -import { buildProformaItemsUpdatePatch } from "./build-proforma-items-update-patch"; - export const buildProformaUpdatePatch = ( formData: ProformaUpdateForm, dirtyFields: FieldNamesMarkedBoolean @@ -13,10 +11,7 @@ export const buildProformaUpdatePatch = ( return {}; } - const itemsPatch = buildProformaItemsUpdatePatch(formData.items); - - return { - ...pickFormDirtyValues(formData, dirtyFields), - items: itemsPatch, - } as ProformaUpdatePatch; + return pickFormDirtyValues(formData, dirtyFields, { + replaceTopLevelArrayKeys: ["items"], + }) satisfies ProformaUpdatePatch; }; diff --git a/modules/customer-invoices/src/web/proformas/update/utils/build-update-proforma-by-id-params.ts b/modules/customer-invoices/src/web/proformas/update/utils/build-update-proforma-by-id-params.ts index 5c0ed6ea..9ed6975a 100644 --- a/modules/customer-invoices/src/web/proformas/update/utils/build-update-proforma-by-id-params.ts +++ b/modules/customer-invoices/src/web/proformas/update/utils/build-update-proforma-by-id-params.ts @@ -23,12 +23,24 @@ export const buildUpdateProformaByIdParams = ( language_code: patch.languageCode, currency_code: patch.currencyCode, + + items: patch.items?.map((item) => ({ + id: item.id, + position: String(item.position), + description: item.description, + quantity: item.quantity === null ? undefined : String(item.quantity), + unit_amount: item.unitAmount === null ? undefined : String(item.unitAmount), + item_discount_percentage: + item.itemDiscountPercentage === null + ? undefined + : { value: String(item.itemDiscountPercentage), scale: "2" }, + })), }; return { id, - data: { - ...Object.fromEntries(Object.entries(data).filter(([, value]) => value !== undefined)), - }, + data: Object.fromEntries( + Object.entries(data).filter(([, value]) => value !== undefined) + ) satisfies UpdateProformaByIdParams["data"], }; };