diff --git a/modules/customer-invoices/src/api/application/proformas/di/index.ts b/modules/customer-invoices/src/api/application/proformas/di/index.ts index 43c8a3f1..99d4b6f4 100644 --- a/modules/customer-invoices/src/api/application/proformas/di/index.ts +++ b/modules/customer-invoices/src/api/application/proformas/di/index.ts @@ -3,4 +3,5 @@ export * from "./proforma-finder.di"; export * from "./proforma-input-mappers.di"; export * from "./proforma-issuer.di"; export * from "./proforma-snapshot-builders.di"; +export * from "./proforma-updater.di"; export * from "./proforma-use-cases.di"; 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 fba0a410..7bf77e48 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 @@ -1,19 +1,26 @@ import type { ICatalogs } from "@erp/core/api"; -import { CreateProformaInputMapper, type ICreateProformaInputMapper } from "../mappers"; +import { + CreateProformaInputMapper, + type ICreateProformaInputMapper, + type IUpdateProformaInputMapper, + UpdateProformaInputMapper, +} from "../mappers"; export interface IProformaInputMappers { createInputMapper: ICreateProformaInputMapper; + updateInputMapper: IUpdateProformaInputMapper; } export const buildProformaInputMappers = (catalogs: ICatalogs): IProformaInputMappers => { const { taxCatalog } = catalogs; - // Mappers el DTO a las props validadas (CustomerProps) y luego construir agregado + // Mappers el DTO a las props validadas (ProformaProps) y luego construir agregado const createInputMapper = new CreateProformaInputMapper({ taxCatalog }); - //const updateProformaInputMapper = new UpdateProformaInputMapper(); + const updateInputMapper = new UpdateProformaInputMapper(); return { createInputMapper, + updateInputMapper, }; }; diff --git a/modules/customer-invoices/src/api/application/proformas/di/proforma-updater.di.ts b/modules/customer-invoices/src/api/application/proformas/di/proforma-updater.di.ts new file mode 100644 index 00000000..5f746d44 --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/di/proforma-updater.di.ts @@ -0,0 +1,12 @@ +import type { IProformaRepository } from "../repositories"; +import { type IProformaUpdater, ProformaUpdater } from "../services"; + +export const buildProformaUpdater = (params: { + repository: IProformaRepository; +}): IProformaUpdater => { + const { repository } = params; + + return new ProformaUpdater({ + repository, + }); +}; 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 35cdd861..941b5f53 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,11 +1,12 @@ import type { ITransactionManager } from "@erp/core/api"; import type { IIssuedInvoicePublicServices } from "../../issued-invoices"; -import type { ICreateProformaInputMapper } from "../mappers"; +import type { ICreateProformaInputMapper, IUpdateProformaInputMapper } from "../mappers"; import type { IProformaCreator, IProformaFinder, IProformaIssuer, + IProformaUpdater, ProformaDocumentGeneratorService, } from "../services"; import type { @@ -20,6 +21,7 @@ import { ListProformasUseCase, ReportProformaUseCase, } from "../use-cases"; +import { UpdateProformaUseCase } from "../use-cases/update-proforma.use-case"; export function buildGetProformaByIdUseCase(deps: { finder: IProformaFinder; @@ -93,18 +95,25 @@ export function buildIssueProformaUseCase(deps: { }); } -/*export function buildUpdateProformaUseCase(deps: { - finder: IProformaFinder; +export function buildUpdateProformaUseCase(deps: { + updater: IProformaUpdater; + dtoMapper: IUpdateProformaInputMapper; fullSnapshotBuilder: IProformaFullSnapshotBuilder; + transactionManager: ITransactionManager; }) { - return new UpdateProformaUseCase(deps.finder, deps.fullSnapshotBuilder); + return new UpdateProformaUseCase({ + dtoMapper: deps.dtoMapper, + updater: deps.updater, + fullSnapshotBuilder: deps.fullSnapshotBuilder, + transactionManager: deps.transactionManager, + }); } +/* export function buildDeleteProformaUseCase(deps: { finder: IProformaFinder }) { return new DeleteProformaUseCase(deps.finder); } - export function buildChangeStatusProformaUseCase(deps: { finder: IProformaFinder; 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 76ccc0a3..32d2a3cb 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 @@ -17,7 +17,7 @@ import { InvoiceSerie, type ProformaPatchProps } from "../../../domain"; /** * UpdateProformaPropsMapper - * Convierte el DTO a las props validadas (CustomerInvoiceProps). + * Convierte el DTO a las props validadas (ProformaInvoiceProps). * No construye directamente el agregado. * Tri-estado: * - campo omitido → no se cambia @@ -29,105 +29,114 @@ import { InvoiceSerie, type ProformaPatchProps } from "../../../domain"; * */ -export function UpdateProformaInputMapper(dto: UpdateProformaByIdRequestDTO) { - try { - const errors: ValidationErrorDetail[] = []; - const props: ProformaPatchProps = {}; +export interface IUpdateProformaInputMapper { + map( + dto: UpdateProformaByIdRequestDTO, + params: { companyId: UniqueID } + ): Result; +} - toPatchField(dto.series).ifSet((series) => { - props.series = extractOrPushError( - maybeFromNullableResult(series, (value) => InvoiceSerie.create(value)), - "reference", - errors - ); - }); +export class UpdateProformaInputMapper implements IUpdateProformaInputMapper { + public map(dto: UpdateProformaByIdRequestDTO, params: { companyId: UniqueID }) { + try { + const errors: ValidationErrorDetail[] = []; + const props: ProformaPatchProps = {}; - toPatchField(dto.invoice_date).ifSet((invoice_date) => { - if (isNullishOrEmpty(invoice_date)) { - errors.push({ path: "invoice_date", message: "Invoice date cannot be empty" }); - return; - } - props.invoiceDate = extractOrPushError( - UtcDate.createFromISO(invoice_date!), - "invoice_date", - errors - ); - }); + toPatchField(dto.series).ifSet((series) => { + props.series = extractOrPushError( + maybeFromNullableResult(series, (value) => InvoiceSerie.create(value)), + "reference", + errors + ); + }); - toPatchField(dto.operation_date).ifSet((operation_date) => { - props.operationDate = extractOrPushError( - maybeFromNullableResult(operation_date, (value) => UtcDate.createFromISO(value)), - "operation_date", - errors - ); - }); + toPatchField(dto.invoice_date).ifSet((invoice_date) => { + if (isNullishOrEmpty(invoice_date)) { + errors.push({ path: "invoice_date", message: "Invoice date cannot be empty" }); + return; + } + props.invoiceDate = extractOrPushError( + UtcDate.createFromISO(invoice_date!), + "invoice_date", + errors + ); + }); - toPatchField(dto.customer_id).ifSet((customer_id) => { - if (isNullishOrEmpty(customer_id)) { - errors.push({ path: "customer_id", message: "Customer cannot be empty" }); - return; - } - props.customerId = extractOrPushError(UniqueID.create(customer_id!), "customer_id", errors); - }); + toPatchField(dto.operation_date).ifSet((operation_date) => { + props.operationDate = extractOrPushError( + maybeFromNullableResult(operation_date, (value) => UtcDate.createFromISO(value)), + "operation_date", + errors + ); + }); - toPatchField(dto.reference).ifSet((reference) => { - props.reference = extractOrPushError( - maybeFromNullableResult(reference, (value) => Result.ok(String(value))), - "reference", - errors - ); - }); + toPatchField(dto.customer_id).ifSet((customer_id) => { + if (isNullishOrEmpty(customer_id)) { + errors.push({ path: "customer_id", message: "Proforma cannot be empty" }); + return; + } + props.customerId = extractOrPushError(UniqueID.create(customer_id!), "customer_id", errors); + }); - toPatchField(dto.description).ifSet((description) => { - props.description = extractOrPushError( - maybeFromNullableResult(description, (value) => Result.ok(String(value))), - "description", - errors - ); - }); + toPatchField(dto.reference).ifSet((reference) => { + props.reference = extractOrPushError( + maybeFromNullableResult(reference, (value) => Result.ok(String(value))), + "reference", + errors + ); + }); - toPatchField(dto.notes).ifSet((notes) => { - props.notes = extractOrPushError( - maybeFromNullableResult(notes, (value) => TextValue.create(value)), - "notes", - errors - ); - }); + toPatchField(dto.description).ifSet((description) => { + props.description = extractOrPushError( + maybeFromNullableResult(description, (value) => Result.ok(String(value))), + "description", + errors + ); + }); - toPatchField(dto.language_code).ifSet((languageCode) => { - if (isNullishOrEmpty(languageCode)) { - errors.push({ path: "language_code", message: "Language code cannot be empty" }); - return; + toPatchField(dto.notes).ifSet((notes) => { + props.notes = extractOrPushError( + maybeFromNullableResult(notes, (value) => TextValue.create(value)), + "notes", + errors + ); + }); + + toPatchField(dto.language_code).ifSet((languageCode) => { + if (isNullishOrEmpty(languageCode)) { + errors.push({ path: "language_code", message: "Language code cannot be empty" }); + return; + } + + props.languageCode = extractOrPushError( + LanguageCode.create(languageCode!), + "language_code", + errors + ); + }); + + toPatchField(dto.currency_code).ifSet((currencyCode) => { + if (isNullishOrEmpty(currencyCode)) { + errors.push({ path: "currency_code", message: "Currency code cannot be empty" }); + return; + } + + props.currencyCode = extractOrPushError( + CurrencyCode.create(currencyCode!), + "currency_code", + errors + ); + }); + + if (errors.length > 0) { + return Result.fail( + new ValidationErrorCollection("Proforma invoice props mapping failed (update)", errors) + ); } - props.languageCode = extractOrPushError( - LanguageCode.create(languageCode!), - "language_code", - errors - ); - }); - - toPatchField(dto.currency_code).ifSet((currencyCode) => { - if (isNullishOrEmpty(currencyCode)) { - errors.push({ path: "currency_code", message: "Currency code cannot be empty" }); - return; - } - - props.currencyCode = extractOrPushError( - CurrencyCode.create(currencyCode!), - "currency_code", - errors - ); - }); - - if (errors.length > 0) { - return Result.fail( - new ValidationErrorCollection("Customer invoice props mapping failed (update)", errors) - ); + return Result.ok(props); + } catch (err: unknown) { + return Result.fail(new DomainError("Proforma invoice props mapping failed", { cause: err })); } - - return Result.ok(props); - } catch (err: unknown) { - return Result.fail(new DomainError("Customer invoice props mapping failed", { cause: err })); } } diff --git a/modules/customer-invoices/src/api/application/proformas/services/index.ts b/modules/customer-invoices/src/api/application/proformas/services/index.ts index 843ab5fe..74c53181 100644 --- a/modules/customer-invoices/src/api/application/proformas/services/index.ts +++ b/modules/customer-invoices/src/api/application/proformas/services/index.ts @@ -6,3 +6,4 @@ export * from "./proforma-finder"; export * from "./proforma-issuer"; export * from "./proforma-number-generator.interface"; export * from "./proforma-public-services.interface"; +export * from "./proforma-updater"; 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 new file mode 100644 index 00000000..128c685a --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/services/proforma-updater.ts @@ -0,0 +1,64 @@ +import type { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import type { Proforma, ProformaPatchProps } from "../../../domain"; +import type { IProformaRepository } from "../repositories"; + +export interface IProformaUpdater { + update(params: { + companyId: UniqueID; + id: UniqueID; + props: ProformaPatchProps; + transaction: unknown; + }): Promise>; +} + +type ProformaUpdaterDeps = { + repository: IProformaRepository; +}; + +export class ProformaUpdater implements IProformaUpdater { + private readonly repository: IProformaRepository; + + constructor(deps: ProformaUpdaterDeps) { + this.repository = deps.repository; + } + + async update(params: { + companyId: UniqueID; + id: UniqueID; + props: ProformaPatchProps; + transaction: unknown; + }): Promise> { + const { companyId, id, props, transaction } = params; + + console.log("props => ", props); + + // Recuperar agregado existente + const existingResult = await this.repository.getByIdInCompany(companyId, id, transaction); + + if (existingResult.isFailure) { + return Result.fail(existingResult.error); + } + + const proforma = existingResult.data; + + // Aplicar cambios en el agregado + const updateResult = proforma.update(props); + + if (updateResult.isFailure) { + return Result.fail(updateResult.error); + } + + console.log(proforma.operationDate); + + // Persistir cambios + const saveResult = await this.repository.update(proforma, transaction); + + if (saveResult.isFailure) { + return Result.fail(saveResult.error); + } + + return Result.ok(proforma); + } +} diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/index.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/index.ts index 6107e7cf..71215eb1 100644 --- a/modules/customer-invoices/src/api/application/proformas/use-cases/index.ts +++ b/modules/customer-invoices/src/api/application/proformas/use-cases/index.ts @@ -5,4 +5,4 @@ export * from "./get-proforma-by-id.use-case"; export * from "./issue-proforma.use-case"; export * from "./list-proformas.use-case"; export * from "./report-proforma.use-case"; -//export * from "./update-proforma"; +export * from "./update-proforma.use-case"; diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/issue-proforma.use-case.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/issue-proforma.use-case.ts index 4c29f829..b2997aa3 100644 --- a/modules/customer-invoices/src/api/application/proformas/use-cases/issue-proforma.use-case.ts +++ b/modules/customer-invoices/src/api/application/proformas/use-cases/issue-proforma.use-case.ts @@ -48,7 +48,7 @@ export class IssueProformaUseCase { return this.transactionManager.complete(async (transaction) => { try { - // 1. Recuperamos la issuedinvoice + // 1. Recuperamos la proforma const proformaResult = await this.finder.findProformaById( companyId, proformaId, 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 new file mode 100644 index 00000000..b394007c --- /dev/null +++ b/modules/customer-invoices/src/api/application/proformas/use-cases/update-proforma.use-case.ts @@ -0,0 +1,124 @@ +import type { ITransactionManager } from "@erp/core/api"; +import { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; + +import type { UpdateProformaByIdRequestDTO } from "../../../../common"; +import type { ProformaPatchProps } from "../../../domain"; +import type { IUpdateProformaInputMapper } from "../mappers"; +import type { IProformaUpdater } from "../services"; +import type { IProformaFullSnapshotBuilder } from "../snapshot-builders"; + +type UpdateProformaUseCaseInput = { + companyId: UniqueID; + proforma_id: string; + dto: UpdateProformaByIdRequestDTO; +}; + +type UpdateProformaUseCaseDeps = { + dtoMapper: IUpdateProformaInputMapper; + updater: IProformaUpdater; + fullSnapshotBuilder: IProformaFullSnapshotBuilder; + transactionManager: ITransactionManager; +}; + +export class UpdateProformaUseCase { + private readonly dtoMapper: IUpdateProformaInputMapper; + private readonly updater: IProformaUpdater; + private readonly fullSnapshotBuilder: IProformaFullSnapshotBuilder; + private readonly transactionManager: ITransactionManager; + + constructor(deps: UpdateProformaUseCaseDeps) { + this.dtoMapper = deps.dtoMapper; + this.updater = deps.updater; + this.fullSnapshotBuilder = deps.fullSnapshotBuilder; + this.transactionManager = deps.transactionManager; + } + + public execute(params: UpdateProformaUseCaseInput) { + const { companyId, proforma_id, dto } = params; + + const proformaIdOrError = UniqueID.create(proforma_id); + if (proformaIdOrError.isFailure) { + return Result.fail(proformaIdOrError.error); + } + + const proformaId = proformaIdOrError.data; + + // Mapear DTO → props de dominio + const patchPropsResult = this.dtoMapper.map(dto, { companyId }); + if (patchPropsResult.isFailure) { + return patchPropsResult; + } + + const patchProps: ProformaPatchProps = patchPropsResult.data; + + console.log("dto => ", dto); + console.log("patchProps => ", patchProps); + + return this.transactionManager.complete(async (transaction) => { + try { + const updateResult = await this.updater.update({ + companyId, + id: proformaId, + props: patchProps, + transaction, + }); + + if (updateResult.isFailure) { + return Result.fail(updateResult.error); + } + + return Result.ok(this.fullSnapshotBuilder.toOutput(updateResult.data)); + } catch (error: unknown) { + return Result.fail(error as Error); + } + }); + } +} + +/* + + const presenter = this.presenterRegistry.getPresenter({ + resource: "proforma", + projection: "FULL", + }) as ProformaFullPresenter; + + // Mapear DTO → props de dominio + const patchPropsResult = mapDTOToUpdateProformaInvoicePatchProps(dto); + if (patchPropsResult.isFailure) { + return Result.fail(patchPropsResult.error); + } + + const patchProps: ProformaPatchProps = patchPropsResult.data; + + return this.transactionManager.complete(async (transaction: unknown) => { + try { + const updatedInvoice = await this.service.patchProformaByIdInCompany( + companyId, + invoiceId, + patchProps, + transaction + ); + + if (updatedInvoice.isFailure) { + return Result.fail(updatedInvoice.error); + } + + const invoiceOrError = await this.service.updateProformaInCompany( + companyId, + updatedInvoice.data, + transaction + ); + if (invoiceOrError.isFailure) return Result.fail(invoiceOrError.error); + + const invoice = invoiceOrError.data; + const dto = presenter.toOutput(invoice); + return Result.ok(dto); + } catch (error: unknown) { + return Result.fail(error as Error); + } + }); + } +} + +*/ diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/update-proforma/index.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/update-proforma/index.ts deleted file mode 100644 index 5af0ad37..00000000 --- a/modules/customer-invoices/src/api/application/proformas/use-cases/update-proforma/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./update-proforma.use-case"; diff --git a/modules/customer-invoices/src/api/application/proformas/use-cases/update-proforma/update-proforma.use-case.ts b/modules/customer-invoices/src/api/application/proformas/use-cases/update-proforma/update-proforma.use-case.ts deleted file mode 100644 index e451ae3b..00000000 --- a/modules/customer-invoices/src/api/application/proformas/use-cases/update-proforma/update-proforma.use-case.ts +++ /dev/null @@ -1,73 +0,0 @@ -import type { IPresenterRegistry, ITransactionManager } from "@erp/core/api"; -import { UniqueID } from "@repo/rdx-ddd"; -import { Result } from "@repo/rdx-utils"; - -import type { UpdateProformaByIdRequestDTO } from "../../../../../common"; -import type { ProformaPatchProps } from "../../../../domain"; -import type { CustomerInvoiceApplicationService } from "../../../services/customer-invoice-application.service"; -import type { ProformaFullPresenter } from "../../../snapshot-builders"; - -type UpdateProformaUseCaseInput = { - companyId: UniqueID; - proforma_id: string; - dto: UpdateProformaByIdRequestDTO; -}; - -export class UpdateProformaUseCase { - constructor( - private readonly service: CustomerInvoiceApplicationService, - private readonly transactionManager: ITransactionManager, - private readonly presenterRegistry: IPresenterRegistry - ) {} - - public execute(params: UpdateProformaUseCaseInput) { - const { companyId, proforma_id, dto } = params; - - const idOrError = UniqueID.create(proforma_id); - if (idOrError.isFailure) { - return Result.fail(idOrError.error); - } - - const invoiceId = idOrError.data; - const presenter = this.presenterRegistry.getPresenter({ - resource: "proforma", - projection: "FULL", - }) as ProformaFullPresenter; - - // Mapear DTO → props de dominio - const patchPropsResult = mapDTOToUpdateCustomerInvoicePatchProps(dto); - if (patchPropsResult.isFailure) { - return Result.fail(patchPropsResult.error); - } - - const patchProps: ProformaPatchProps = patchPropsResult.data; - - return this.transactionManager.complete(async (transaction: unknown) => { - try { - const updatedInvoice = await this.service.patchProformaByIdInCompany( - companyId, - invoiceId, - patchProps, - transaction - ); - - if (updatedInvoice.isFailure) { - return Result.fail(updatedInvoice.error); - } - - const invoiceOrError = await this.service.updateProformaInCompany( - companyId, - updatedInvoice.data, - transaction - ); - if (invoiceOrError.isFailure) return Result.fail(invoiceOrError.error); - - const invoice = invoiceOrError.data; - const dto = presenter.toOutput(invoice); - return Result.ok(dto); - } catch (error: unknown) { - return Result.fail(error as Error); - } - }); - } -} diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts b/modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts index 3397fd07..8fe62db3 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/di/proformas.di.ts @@ -7,6 +7,7 @@ import { type IssueProformaUseCase, type ListProformasUseCase, type ReportProformaUseCase, + type UpdateProformaUseCase, buildCreateProformaUseCase, buildGetProformaByIdUseCase, buildIssueProformaUseCase, @@ -17,7 +18,9 @@ import { buildProformaIssuer, buildProformaSnapshotBuilders, buildProformaToIssuedInvoicePropsConverter, + buildProformaUpdater, buildReportProformaUseCase, + buildUpdateProformaUseCase, } from "../../../application"; import { buildProformaDocumentService } from "./proforma-documents.di"; @@ -34,9 +37,9 @@ export type ProformasInternalDeps = { issueProforma: (publicServices: { issuedInvoiceServices: IIssuedInvoicePublicServices; }) => IssueProformaUseCase; + updateProforma: () => UpdateProformaUseCase; /* - updateProforma: () => UpdateProformaUseCase; deleteProforma: () => DeleteProformaUseCase; issueProforma: () => IssueProformaUseCase; changeStatusProforma: () => ChangeStatusProformaUseCase;*/ @@ -65,6 +68,8 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter repository, }); + const updater = buildProformaUpdater({ repository }); + const snapshotBuilders = buildProformaSnapshotBuilders(); const documentGeneratorPipeline = buildProformaDocumentService(params); @@ -102,6 +107,14 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter transactionManager, }), + updateProforma: () => + buildUpdateProformaUseCase({ + updater, + dtoMapper: inputMappers.updateInputMapper, + fullSnapshotBuilder: snapshotBuilders.full, + transactionManager, + }), + issueProforma: (publicServices: { issuedInvoiceServices: IIssuedInvoicePublicServices }) => buildIssueProformaUseCase({ publicServices, diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas.routes.ts b/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas.routes.ts index 38c45bd2..8a3fab22 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas.routes.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/express/proformas.routes.ts @@ -17,10 +17,13 @@ import { ListProformasRequestSchema, ReportProformaByIdParamsRequestSchema, ReportProformaByIdQueryRequestSchema, + UpdateProformaByIdParamsRequestSchema, + UpdateProformaByIdRequestSchema, } from "../../../../common"; import type { IIssuedInvoicePublicServices } from "../../../application"; import { CreateProformaController } from "./controllers/create-proforma.controller"; +import { UpdateProformaController } from "./controllers/update-proforma.controller"; export const proformasRouter = (params: StartParams) => { const { app, config, getService, getInternal } = params; @@ -102,7 +105,6 @@ export const proformasRouter = (params: StartParams) => { } ); - /* router.put( "/:proforma_id", //checkTabContext, @@ -110,13 +112,13 @@ export const proformasRouter = (params: StartParams) => { validateRequest(UpdateProformaByIdParamsRequestSchema, "params"), validateRequest(UpdateProformaByIdRequestSchema, "body"), (req: Request, res: Response, next: NextFunction) => { - const useCase = deps.useCases.update_proforma(); + const useCase = deps.useCases.updateProforma(); const controller = new UpdateProformaController(useCase); return controller.execute(req, res, next); } ); - router.delete( + /*router.delete( "/:proforma_id", //checkTabContext, diff --git a/modules/customer-invoices/src/web/customer-invoice-routes.tsx b/modules/customer-invoices/src/web/customer-invoice-routes.tsx index 5d0fa96b..cbf0efc7 100644 --- a/modules/customer-invoices/src/web/customer-invoice-routes.tsx +++ b/modules/customer-invoices/src/web/customer-invoice-routes.tsx @@ -14,9 +14,9 @@ const ProformasListPage = lazy(() => import("./proformas/create").then((m) => ({ default: m.ProformaCreatePage })) );*/ -/*const InvoiceUpdatePage = lazy(() => - import("./pages").then((m) => ({ default: m.InvoiceUpdatePage })) -);*/ +const ProformaUpdatePage = lazy(() => + import("./proformas/update").then((m) => ({ default: m.ProformaUpdatePage })) +); const IssuedInvoicesLayout = lazy(() => import("./issued-invoices/ui").then((m) => ({ default: m.IssuedInvoicesLayout })) @@ -39,7 +39,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[] { path: "", index: true, element: }, // index { path: "list", element: }, //{ path: "create", element: }, - //{ path: ":id/edit", element: }, + { path: ":id/edit", element: }, ], }, { @@ -55,7 +55,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[] /* { path: "create", element: }, { path: ":id", element: }, - { path: ":id/edit", element: }, + { path: ":id/delete", element: }, { path: ":id/view", element: }, { path: ":id/print", element: }, diff --git a/modules/customer-invoices/src/web/pages/index.ts b/modules/customer-invoices/src/web/pages/index.ts index cea734d0..e30e9eb1 100644 --- a/modules/customer-invoices/src/web/pages/index.ts +++ b/modules/customer-invoices/src/web/pages/index.ts @@ -2,4 +2,3 @@ export * from "../proformas"; export * from "./create"; export * from "./list"; -export * from "./update"; diff --git a/modules/customer-invoices/src/web/proformas/create/entities/index.ts b/modules/customer-invoices/src/web/proformas/create/entities/index.ts index 3a31f995..a1605cb3 100644 --- a/modules/customer-invoices/src/web/proformas/create/entities/index.ts +++ b/modules/customer-invoices/src/web/proformas/create/entities/index.ts @@ -1,3 +1,3 @@ export * from "./proforma-create-form.entity"; export * from "./proforma-create-form.schema"; -export * from "./proforma-create-form-default"; +export * from "./proforma-create-form-default.entity"; diff --git a/modules/customer-invoices/src/web/proformas/create/entities/proforma-create-form-default.ts b/modules/customer-invoices/src/web/proformas/create/entities/proforma-create-form-default.entity.ts similarity index 100% rename from modules/customer-invoices/src/web/proformas/create/entities/proforma-create-form-default.ts rename to modules/customer-invoices/src/web/proformas/create/entities/proforma-create-form-default.entity.ts diff --git a/modules/customer-invoices/src/web/proformas/update/adapters/index.ts b/modules/customer-invoices/src/web/proformas/update/adapters/index.ts new file mode 100644 index 00000000..197d88cc --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/update/adapters/index.ts @@ -0,0 +1 @@ +export * from "./proforma-to-proforma-update-form.adapter"; diff --git a/modules/customer-invoices/src/web/proformas/update/adapters/proforma-to-proforma-update-form.adapter.ts b/modules/customer-invoices/src/web/proformas/update/adapters/proforma-to-proforma-update-form.adapter.ts new file mode 100644 index 00000000..869c160a --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/update/adapters/proforma-to-proforma-update-form.adapter.ts @@ -0,0 +1,31 @@ +import type { Proforma } from "../../shared"; +import type { ProformaUpdateForm } from "../entities"; + +/** + * Mapea un cliente a un formulario de actualización de cliente. + * + * @param proforma + * @returns + */ + +export const mapProformaToProformaUpdateForm = (proforma: Proforma): ProformaUpdateForm => { + return { + series: proforma.series ?? "", + + invoiceDate: proforma.invoiceDate ?? "", + operationDate: proforma.operationDate ?? "", + + customerId: proforma.customerId ?? "", + + description: proforma.description ?? "", + reference: proforma.reference ?? "", + notes: proforma.notes ?? "", + + languageCode: proforma.languageCode ?? "es", + currencyCode: proforma.currencyCode ?? "EUR", + + globalDiscountPercentage: proforma.globalDiscountPercentage ?? 0, + + paymentMethod: proforma.paymentMethod ?? "", + }; +}; diff --git a/modules/customer-invoices/src/web/proformas/update/controllers/index.ts b/modules/customer-invoices/src/web/proformas/update/controllers/index.ts index 37eb5fd2..f8022c89 100644 --- a/modules/customer-invoices/src/web/proformas/update/controllers/index.ts +++ b/modules/customer-invoices/src/web/proformas/update/controllers/index.ts @@ -1 +1,2 @@ -export * from "./use-proforma-update-page.controller"; +export * from "./use-update-proforma-controller"; +export * from "./use-update-proforma-page-controller"; diff --git a/modules/customer-invoices/src/web/proformas/update/controllers/use-proforma-update-page.controller.ts b/modules/customer-invoices/src/web/proformas/update/controllers/use-proforma-update-page.controller.ts.bak similarity index 100% rename from modules/customer-invoices/src/web/proformas/update/controllers/use-proforma-update-page.controller.ts rename to modules/customer-invoices/src/web/proformas/update/controllers/use-proforma-update-page.controller.ts.bak 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 new file mode 100644 index 00000000..5ca7e4ae --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-controller.ts @@ -0,0 +1,171 @@ +import { useHookForm } from "@erp/core/hooks"; +import { showErrorToast, showSuccessToast, showWarningToast } from "@repo/rdx-ui/helpers"; +import { useEffect, useId, useMemo } from "react"; +import type { FieldErrors } from "react-hook-form"; + +import { useTranslation } from "../../../i18n"; +import type { UpdateProformaByIdParams } from "../../shared"; +import type { Proforma } from "../../shared/entities"; +import { useProformaGetQuery, useProformaUpdateMutation } from "../../shared/hooks"; +import { mapProformaToProformaUpdateForm } from "../adapters"; +import { + type ProformaUpdateForm, + ProformaUpdateFormSchema, + defaultProformaUpdateForm, +} from "../entities"; +import { + buildProformaUpdatePatch, + buildUpdateProformaByIdParams, + focusFirstProformaUpdateError, +} from "../utils"; + +export interface UseUpdateProformaControllerOptions { + onUpdated?(updated: Proforma): void; + successToasts?: boolean; + + onError?(error: Error, params: UpdateProformaByIdParams): void; + errorToasts?: boolean; +} + +export const useUpdateProformaController = ( + proformaId?: string, + options?: UseUpdateProformaControllerOptions +) => { + const { t } = useTranslation(); + const formId = useId(); + + // 1) Estado de carga de la proforma (query) + const { + data: proformaData, + isLoading, + isError: isLoadError, + error: loadError, + } = useProformaGetQuery({ id: proformaId, enabled: Boolean(proformaId) }); + + // 2) Estado de creación (mutación) + const { + mutateAsync, + isPending: isUpdating, + isError: isUpdateError, + error: updateError, + } = useProformaUpdateMutation(); + + const initialValues = useMemo(() => { + if (!proformaData) return defaultProformaUpdateForm; + + return mapProformaToProformaUpdateForm(proformaData); + }, [proformaData]); + + // 3) Form hook + const form = useHookForm({ + resolverSchema: ProformaUpdateFormSchema, + initialValues, + disabled: isLoading || isUpdating, + }); + + useEffect(() => { + if (!proformaData) return; + + console.log("Reseteando form con datos de la proforma:", proformaData); + form.reset(mapProformaToProformaUpdateForm(proformaData), { + keepDirty: false, // <-- importante: no marca el form como "dirty" al cargar los datos reales + }); + }, [proformaData, form]); + + /** Handlers */ + + const resetForm = () => { + const initialData = proformaData + ? mapProformaToProformaUpdateForm(proformaData) + : defaultProformaUpdateForm; + + form.reset(initialData, { keepDirty: false }); + }; + + const submitHandler = form.handleSubmit( + async (formData: ProformaUpdateForm) => { + if (!proformaId) { + showErrorToast(t("proformas.update.error.title"), t("proformas.update.error.missing_id")); + return; + } + + const previousData = proformaData; + + const patchData = buildProformaUpdatePatch(formData, form.formState.dirtyFields); + const params = buildUpdateProformaByIdParams(proformaId, patchData); + + try { + // Enviamos cambios al servidor + const updated = await mutateAsync(params); + + // Ha ido bien -> actualizamos form con datos reales + // keepDirty = false -> deja el formulario sin cambios sin tener que esperar al siguiente render. + form.reset(mapProformaToProformaUpdateForm(updated), { + keepDirty: false, + }); + + if (options?.successToasts !== false) { + showSuccessToast( + t("proformas.update.success.title"), + t("proformas.update.success.message") + ); + } + + options?.onUpdated?.(updated); + } catch (error: unknown) { + const normalizedError = + error instanceof Error ? error : new Error(t("pages.update.error.unknown")); + + form.reset( + previousData ? mapProformaToProformaUpdateForm(previousData) : defaultProformaUpdateForm, + { keepDirty: false } + ); + + if (options?.errorToasts !== false) { + showErrorToast(t("proformas.update.error.title"), normalizedError.message); + } + + options?.onError?.(normalizedError, params); + } + }, + (errors: FieldErrors) => { + focusFirstProformaUpdateError(errors); + + showWarningToast( + t("proformas.update.validation.title"), + t("proformas.update.validation.message") + ); + } + ); + + // Evento onSubmit ya preparado para el
+ const onSubmit = (event: React.FormEvent) => { + event.stopPropagation(); // <-- evita que el submit se propage por los padre en el árbol DOM + submitHandler(event); + }; + + return { + // form + formId, + form, + + // handlers del form + onSubmit, + resetForm, + + // carga de datos + proformaData, + isLoading, + isLoadError, + loadError, + + // mutation + isUpdating, + isUpdateError, + updateError, + + // No devolver FormProvider, así el controller es más + // flexible y reusable (p.ej. para un modal) + // FormProvider, + }; +}; diff --git a/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-page-controller.ts b/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-page-controller.ts new file mode 100644 index 00000000..efa06674 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/update/controllers/use-update-proforma-page-controller.ts @@ -0,0 +1,13 @@ +import { useUrlParamId } from "@erp/core/hooks"; + +import { useUpdateProformaController } from "./use-update-proforma-controller"; + +export const useUpdateProformaPageController = () => { + const proformaId = useUrlParamId(); + + const updateCtrl = useUpdateProformaController(proformaId); + + return { + updateCtrl, + }; +}; diff --git a/modules/customer-invoices/src/web/proformas/update/entities/index.ts b/modules/customer-invoices/src/web/proformas/update/entities/index.ts index 08b24fd7..19bad70d 100644 --- a/modules/customer-invoices/src/web/proformas/update/entities/index.ts +++ b/modules/customer-invoices/src/web/proformas/update/entities/index.ts @@ -1,4 +1,5 @@ +export * from "./proforma-item-update-form.entity"; export * from "./proforma-update-form.entity"; export * from "./proforma-update-form.schema"; -export * from "./proforma-update-form-defaults"; +export * from "./proforma-update-form-default.entity"; export * from "./proforma-update-patch.entity"; diff --git a/modules/customer-invoices/src/web/proformas/update/entities/proforma-update-form-default.entity.ts b/modules/customer-invoices/src/web/proformas/update/entities/proforma-update-form-default.entity.ts new file mode 100644 index 00000000..e3ec1aa1 --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/update/entities/proforma-update-form-default.entity.ts @@ -0,0 +1,21 @@ +import type { ProformaUpdateForm } from "."; + +export const defaultProformaUpdateForm: ProformaUpdateForm = { + series: "", + + invoiceDate: "", + operationDate: "", + + customerId: "", + + description: "", + reference: "", + notes: "", + + languageCode: "es", + currencyCode: "EUR", + + paymentMethod: "", + + globalDiscountPercentage: 0, +}; diff --git a/modules/customer-invoices/src/web/proformas/update/entities/proforma-update-form-defaults.ts b/modules/customer-invoices/src/web/proformas/update/entities/proforma-update-form-defaults.ts deleted file mode 100644 index 9e045cd1..00000000 --- a/modules/customer-invoices/src/web/proformas/update/entities/proforma-update-form-defaults.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { CustomerUpdateForm } from "./proforma-update-form.entity"; - -export const defaultCustomerUpdateForm: CustomerUpdateForm = { - reference: "", - isCompany: true, - name: "", - tradeName: "", - tin: "", - - defaultTaxes: [], - - street: "", - street2: "", - city: "", - province: "", - postalCode: "", - country: "es", - - primaryEmail: "", - secondaryEmail: "", - primaryPhone: "", - secondaryPhone: "", - primaryMobile: "", - secondaryMobile: "", - - fax: "", - website: "", - - legalRecord: "", - - languageCode: "es", - currencyCode: "EUR", -}; diff --git a/modules/customer-invoices/src/web/proformas/update/entities/proforma-update-form.entity.ts b/modules/customer-invoices/src/web/proformas/update/entities/proforma-update-form.entity.ts index 72a4be89..610b3eb5 100644 --- a/modules/customer-invoices/src/web/proformas/update/entities/proforma-update-form.entity.ts +++ b/modules/customer-invoices/src/web/proformas/update/entities/proforma-update-form.entity.ts @@ -13,8 +13,6 @@ * - sin detalles impuestos por el widget */ -import type { ProformaItemForm } from "../../shared/entities"; - export interface ProformaUpdateForm { series: string; @@ -23,6 +21,7 @@ export interface ProformaUpdateForm { customerId: string; + description: string; reference: string; notes: string; @@ -33,5 +32,5 @@ export interface ProformaUpdateForm { paymentMethod: string; - items: ProformaItemForm[]; + //items: ProformaItemForm[]; } diff --git a/modules/customer-invoices/src/web/proformas/update/entities/proforma-update-form.schema.ts b/modules/customer-invoices/src/web/proformas/update/entities/proforma-update-form.schema.ts index 3849da9f..4b6b7156 100644 --- a/modules/customer-invoices/src/web/proformas/update/entities/proforma-update-form.schema.ts +++ b/modules/customer-invoices/src/web/proformas/update/entities/proforma-update-form.schema.ts @@ -14,7 +14,29 @@ import { z } from "zod/v4"; * - sin detalles impuestos por el widget */ -export const ProformaItemFormSchema = z.object({ +export const ProformaUpdateFormSchema = z.object({ + series: z.string().default(""), + + invoiceDate: z.string().default(""), + operationDate: z.string().default(""), + + customerId: z.string().default(""), + + description: z.string().default(""), + reference: z.string().default(""), + notes: z.string().default(""), + + languageCode: z.string().min(1, "Debe indicar un idioma").default("es"), + currencyCode: z.string().min(1, "Debe indicar una moneda").default("EUR"), + + globalDiscountPercentage: z.number().default(0), + + paymentMethod: z.string().default(""), +}); + +export type ProformaUpdateFormSchemaType = z.infer; + +const ProformaItemFormSchema = z.object({ description: z.string().max(2000).optional().default(""), quantity: z.any(), //NumericStringSchema.optional(), unit_amount: z.any(), //NumericStringSchema.optional(), @@ -30,7 +52,7 @@ export const ProformaItemFormSchema = z.object({ total_amount: z.number(), }); -export const ProformaFormSchema = z.object({ +const ProformaFormSchema = z.object({ invoice_number: z.string().optional(), series: z.string().optional(), diff --git a/modules/customer-invoices/src/web/proformas/update/types/index.ts b/modules/customer-invoices/src/web/proformas/update/types/index.ts deleted file mode 100644 index eea524d6..00000000 --- a/modules/customer-invoices/src/web/proformas/update/types/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./types"; diff --git a/modules/customer-invoices/src/web/proformas/update/types/types.ts b/modules/customer-invoices/src/web/proformas/update/types/types.ts deleted file mode 100644 index 99f2f05d..00000000 --- a/modules/customer-invoices/src/web/proformas/update/types/types.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { z } from "zod/v4"; - -export const ProformaItemFormSchema = z.object({ - description: z.string().max(2000).optional().default(""), - quantity: z.any(), //NumericStringSchema.optional(), - unit_amount: z.any(), //NumericStringSchema.optional(), - - subtotal_amount: z.any(), //z.number(), - discount_percentage: z.any(), //NumericStringSchema.optional(), - discount_amount: z.number(), - taxable_amount: z.number(), - - tax_codes: z.array(z.string()).default([]), - - taxes_amount: z.number(), - total_amount: z.number(), -}); - -export const ProformaFormSchema = z.object({ - invoice_number: z.string().optional(), - series: z.string().optional(), - - invoice_date: z.string().optional(), - operation_date: z.string().optional(), - - customer_id: z.string().optional(), - recipient: z - .object({ - id: z.string().optional(), - name: z.string().optional(), - tin: z.string().optional(), - street: z.string().optional(), - street2: z.string().optional(), - city: z.string().optional(), - province: z.string().optional(), - postal_code: z.string().optional(), - country: z.string().optional(), - }) - .optional(), - - reference: z.string().optional(), - description: z.string().optional(), - notes: z.string().optional(), - - language_code: z - .string({ - error: "El idioma es obligatorio", - }) - .min(1, "Debe indicar un idioma") - .toUpperCase() // asegura mayúsculas - .default("es"), - - currency_code: z - .string({ - error: "La moneda es obligatoria", - }) - .min(1, "La moneda no puede estar vacía") - .toUpperCase() // asegura mayúsculas - .default("EUR"), - - taxes: z - .array( - z.object({ - tax_code: z.string(), - tax_label: z.string(), - taxable_amount: z.number(), - taxes_amount: z.number(), - }) - ) - .optional(), - - items: z.array(ProformaItemFormSchema).optional(), - - subtotal_amount: z.number(), - items_discount_amount: z.number(), - discount_percentage: z.number(), - discount_amount: z.number(), - taxable_amount: z.number(), - taxes_amount: z.number(), - total_amount: z.number(), -}); - -export type ProformaFormData = z.infer; -export type ProformaItemFormData = z.infer; - -export const defaultProformaItemFormData: ProformaItemFormData = { - description: "", - quantity: "", - unit_amount: "", - subtotal_amount: 0, - discount_percentage: "", - discount_amount: 0, - taxable_amount: 0, - tax_codes: ["iva_21"], - taxes_amount: 0, - total_amount: 0, -}; - -export const defaultProformaFormData: ProformaFormData = { - invoice_number: "", - series: "", - - invoice_date: "", - operation_date: "", - - reference: "", - description: "", - notes: "", - - language_code: "es", - currency_code: "EUR", - - items: [], - - subtotal_amount: 0, - items_discount_amount: 0, - discount_amount: 0, - discount_percentage: 0, - taxable_amount: 0, - taxes_amount: 0, - total_amount: 0, -}; diff --git a/modules/customer-invoices/src/web/proformas/update/ui/blocks/index.ts b/modules/customer-invoices/src/web/proformas/update/ui/blocks/index.ts new file mode 100644 index 00000000..a0ac584a --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/update/ui/blocks/index.ts @@ -0,0 +1,2 @@ +export * from "./proforma-header-fields-card"; +export * from "./proforma-update-editor"; diff --git a/modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-header-fields-card.tsx b/modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-header-fields-card.tsx new file mode 100644 index 00000000..6a2c742a --- /dev/null +++ b/modules/customer-invoices/src/web/proformas/update/ui/blocks/proforma-header-fields-card.tsx @@ -0,0 +1,119 @@ +import { + Card, + CardContent, + CardHeader, + CardTitle, + Input, + Textarea, +} from "@repo/shadcn-ui/components"; +import { useFormContext } from "react-hook-form"; + +import { useTranslation } from "../../../../i18n"; +import type { ProformaUpdateForm } from "../../entities"; + +export const ProformaHeaderFieldsCard = () => { + const { t } = useTranslation(); + const { register, formState } = useFormContext(); + + return ( + + + {t("proformas.update.header.title", "Cabecera")} + + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ +