From 41f30cde9d8772f739e2fdc4f062794286d48348 Mon Sep 17 00:00:00 2001 From: david Date: Wed, 25 Mar 2026 10:34:17 +0100 Subject: [PATCH] Paso de factuges a proforma --- .../proformas/di/proforma-public-services.ts | 46 ++++++- .../domain/aggregates/customer.aggregate.ts | 19 ++- .../domain/value-objects/customer-taxes.vo.ts | 114 +++++++++++++++++- .../di/customer-persistence-mappers.di.ts | 10 +- .../di/customer-public-services.ts | 5 +- .../src/api/infrastructure/di/customers.di.ts | 2 +- .../domain/sequelize-customer.mapper.ts | 35 +++--- .../sequelize/models/customer.model.ts | 2 +- ...ate-proforma-from-factuges-input.mapper.ts | 54 ++++++--- .../create-proforma-from-factuges.use-case.ts | 107 +++++++++++++--- .../api/application/use-cases/payments.json | 20 +++ .../express/factuges.routes.ts | 1 - ...eate-proforma-from-factuges.request.dto.ts | 40 ++++-- 13 files changed, 379 insertions(+), 76 deletions(-) create mode 100644 modules/factuges/src/api/application/use-cases/payments.json diff --git a/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-public-services.ts b/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-public-services.ts index bb920930..2236f449 100644 --- a/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-public-services.ts +++ b/modules/customer-invoices/src/api/infrastructure/proformas/di/proforma-public-services.ts @@ -3,7 +3,13 @@ import type { UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import type { Transaction } from "sequelize"; -import { type IProformaCreatorParams, buildProformaCreator } from "../../../application"; +import { + type IProformaCreatorParams, + type IProformaFullSnapshot, + buildProformaCreator, + buildProformaFinder, + buildProformaSnapshotBuilders, +} from "../../../application"; import type { Proforma } from "../../../domain"; import { buildProformaNumberGenerator } from "./proforma-number-generator.di"; @@ -24,7 +30,16 @@ export type ProformaPublicServices = { ) => Promise>; listProformas: (filters: unknown, context: unknown) => null; - getProformaById: (id: unknown, context: unknown) => null; + getProformaById: ( + id: UniqueID, + context: ProformaServicesContext + ) => Promise>; + + getProformaSnapshotById: ( + id: UniqueID, + context: ProformaServicesContext + ) => Promise>; + generateProformaReport: (id: unknown, options: unknown, context: unknown) => null; }; @@ -40,9 +55,11 @@ export function buildProformaServices( const repository = buildProformaRepository({ database, mappers: persistenceMappers }); const numberService = buildProformaNumberGenerator(); + const snapshotsBuilder = buildProformaSnapshotBuilders(); // Application helpers const creator = buildProformaCreator({ numberService, repository }); + const finder = buildProformaFinder(repository); return { createProforma: async ( @@ -64,8 +81,29 @@ export function buildProformaServices( listProformas: (filters, context) => null, //internal.useCases.listProformas().execute(filters, context), - getProformaById: (id, context) => null, - //internal.useCases.getProformaById().execute(id, context), + getProformaById: async (id: UniqueID, context: ProformaServicesContext) => { + const { transaction, companyId } = context; + const proformaResult = await finder.findProformaById(companyId, id, transaction); + + if (proformaResult.isFailure) { + return Result.fail(proformaResult.error); + } + + return Result.ok(proformaResult.data); + }, + + getProformaSnapshotById: async (id: UniqueID, context: ProformaServicesContext) => { + const { transaction, companyId } = context; + const proformaResult = await finder.findProformaById(companyId, id, transaction); + + if (proformaResult.isFailure) { + return Result.fail(proformaResult.error); + } + + const fullSnapshot = snapshotsBuilder.full.toOutput(proformaResult.data); + + return Result.ok(fullSnapshot); + }, generateProformaReport: (id, options, context) => null, //internal.useCases.reportProforma().execute(id, options, context), diff --git a/modules/customers/src/api/domain/aggregates/customer.aggregate.ts b/modules/customers/src/api/domain/aggregates/customer.aggregate.ts index b51c5c2a..f433318c 100644 --- a/modules/customers/src/api/domain/aggregates/customer.aggregate.ts +++ b/modules/customers/src/api/domain/aggregates/customer.aggregate.ts @@ -15,7 +15,7 @@ import { } from "@repo/rdx-ddd"; import { type Maybe, Result } from "@repo/rdx-utils"; -import type { CustomerStatus, CustomerTaxesProps } from "../value-objects"; +import { type CustomerStatus, CustomerTaxes } from "../value-objects"; export interface ICustomerCreateProps { companyId: UniqueID; @@ -43,7 +43,7 @@ export interface ICustomerCreateProps { legalRecord: Maybe; - defaultTaxes: CustomerTaxesProps; + defaultTaxes: CustomerTaxes; languageCode: LanguageCode; currencyCode: CurrencyCode; @@ -86,19 +86,20 @@ export interface ICustomer { readonly website: Maybe; readonly legalRecord: Maybe; - readonly defaultTaxes: CustomerTaxesProps; + readonly defaultTaxes: CustomerTaxes; readonly languageCode: LanguageCode; readonly currencyCode: CurrencyCode; } -type CustomerInternalProps = Omit & { +type CustomerInternalProps = Omit & { readonly address: PostalAddress; + readonly defaultTaxes: CustomerTaxes; }; export class Customer extends AggregateRoot implements ICustomer { static create(props: ICustomerCreateProps, id?: UniqueID): Result { - const { address, ...internalProps } = props; + const { address, defaultTaxes, ...internalProps } = props; const postalAddressResult = PostalAddress.create(address); @@ -106,9 +107,15 @@ export class Customer extends AggregateRoot implements IC return Result.fail(postalAddressResult.error); } + const taxes = CustomerTaxes.create(defaultTaxes); + if (taxes.isFailure) { + return Result.fail(taxes.error); + } + const contact = new Customer( { ...internalProps, + defaultTaxes: taxes.data, address: postalAddressResult.data, }, id @@ -220,7 +227,7 @@ export class Customer extends AggregateRoot implements IC return this.props.legalRecord; } - public get defaultTaxes(): CustomerTaxesProps { + public get defaultTaxes(): CustomerTaxes { return this.props.defaultTaxes; } diff --git a/modules/customers/src/api/domain/value-objects/customer-taxes.vo.ts b/modules/customers/src/api/domain/value-objects/customer-taxes.vo.ts index e3d6b380..e73797ae 100644 --- a/modules/customers/src/api/domain/value-objects/customer-taxes.vo.ts +++ b/modules/customers/src/api/domain/value-objects/customer-taxes.vo.ts @@ -1,6 +1,7 @@ -import type { Tax } from "@erp/core/api"; +import type { TaxCatalogProvider } from "@erp/core"; +import { Tax } from "@erp/core/api"; import { ValueObject } from "@repo/rdx-ddd"; -import { type Maybe, Result } from "@repo/rdx-utils"; +import { Maybe, Result } from "@repo/rdx-utils"; export type CustomerTaxesProps = { iva: Maybe; // si existe @@ -16,30 +17,131 @@ export interface ICustomerItemTaxes { toKey(): string; // Clave para representar un trío. } -export class CustomerTaxes - extends ValueObject - implements ICustomerItemTaxes -{ +export class CustomerTaxes extends ValueObject implements ICustomerItemTaxes { static create(props: CustomerTaxesProps) { return Result.ok(new CustomerTaxes(props)); } + /** + * Reconstruye una instancia de CustomerTaxes a partir de una clave serializada. + * Este método es la operación inversa de toKey(). + * + * @param key Clave en formato "ivaCode;recCode;retentionCode" + * Donde cada código puede ser "#" si el impuesto no existe + * Ejemplo: "iva_21;rec_02;#" o "#;#;#" + * @param provider Proveedor del catálogo de impuestos (ej: SpainTaxCatalogProvider) + * @returns Result Éxito con la instancia creada o fallo con mensaje de error + * + * @example + * const key = "iva_21;rec_02;ret_1500"; + * const result = CustomerTaxes.fromKey(key, SpainTaxCatalogProvider()); + * if (result.isOk()) { + * const customerTaxes = result.value; + * } + */ + static fromKey(key: string, provider: TaxCatalogProvider): Result { + // Validar que la clave no esté vacía + if (!key || typeof key !== "string") { + return Result.fail(new Error("La clave debe ser una cadena no vacía.")); + } + + // Dividir la clave por punto y coma para obtener los tres códigos + const codes = key.split(";"); + + // Validar que la clave tiene exactamente 3 partes (IVA, REC, Retención) + if (codes.length !== 3) { + return Result.fail( + new Error( + `Formato de clave inválido. Se esperaban 3 códigos separados por ";", se recibieron ${codes.length}.` + ) + ); + } + + const [ivaCode, recCode, retentionCode] = codes; + + // Función auxiliar para resolver un código a un impuesto (Maybe) + // Si el código es "#", retorna Maybe.none(), si no, busca en el catálogo + const resolveTaxFromCode = (code: string): Result> => { + const trimmedCode = code.trim(); + + // Si el código es "#", significa que no existe este tipo de impuesto + if (trimmedCode === "#") { + return Result.ok(Maybe.none()); + } + + // Si el código no es "#", buscamos en el catálogo usando Tax.createFromCode + const taxResult = Tax.createFromCode(trimmedCode, provider); + + // Si hay un error creando el impuesto, propagamos el error + if (taxResult.isFailure) { + return Result.fail(taxResult.error); + } + + // Si se creó exitosamente, lo envolvemos en Maybe.some() + return Result.ok(Maybe.some(taxResult.data)); + }; + + // Resolver el IVA desde su código + const ivaResult = resolveTaxFromCode(ivaCode); + if (ivaResult.isFailure) { + return Result.fail( + new Error(`Error al resolver IVA desde código "${ivaCode}": ${ivaResult.error.message}`) + ); + } + + // Resolver el REC desde su código + const recResult = resolveTaxFromCode(recCode); + if (recResult.isFailure) { + return Result.fail( + new Error(`Error al resolver REC desde código "${recCode}": ${recResult.error.message}`) + ); + } + + // Resolver la Retención desde su código + const retentionResult = resolveTaxFromCode(retentionCode); + if (retentionResult.isFailure) { + return Result.fail( + new Error( + `Error al resolver Retención desde código "${retentionCode}": ${retentionResult.error.message}` + ) + ); + } + + // Crear la instancia de CustomerTaxes con los impuestos resueltos + return Result.ok( + new CustomerTaxes({ + iva: ivaResult.data, + rec: recResult.data, + retention: retentionResult.data, + }) + ); + } + + /** + * Genera una clave única que representa la combinación de impuestos. + * Extrae el código de cada impuesto o usa "#" si no existe. + * @returns {string} Clave en formato: "ivaCode;recCode;retentionCode" + */ toKey(): string { + // Extrae el código del IVA, o "#" si no existe const ivaCode = this.props.iva.match( (iva) => iva.code, () => "#" ); + // Extrae el código de la retención de cliente (REC), o "#" si no existe const recCode = this.props.rec.match( (rec) => rec.code, () => "#" ); + // Extrae el código de la retención, o "#" si no existe const retentionCode = this.props.retention.match( (retention) => retention.code, () => "#" ); + // Retorna la clave combinada separada por punto y coma return `${ivaCode};${recCode};${retentionCode}`; } diff --git a/modules/customers/src/api/infrastructure/di/customer-persistence-mappers.di.ts b/modules/customers/src/api/infrastructure/di/customer-persistence-mappers.di.ts index 8b1f658c..1c669298 100644 --- a/modules/customers/src/api/infrastructure/di/customer-persistence-mappers.di.ts +++ b/modules/customers/src/api/infrastructure/di/customer-persistence-mappers.di.ts @@ -1,3 +1,5 @@ +import type { ICatalogs } from "@erp/core/api"; + import { SequelizeCustomerDomainMapper, SequelizeCustomerSummaryMapper } from "../mappers"; export interface ICustomerPersistenceMappers { @@ -7,9 +9,13 @@ export interface ICustomerPersistenceMappers { //createMapper: CreateCustomerInputMapper; } -export const buildCustomerPersistenceMappers = (): ICustomerPersistenceMappers => { +export const buildCustomerPersistenceMappers = ( + catalogs: ICatalogs +): ICustomerPersistenceMappers => { + const { taxCatalog } = catalogs; + // Mappers para el repositorio - const domainMapper = new SequelizeCustomerDomainMapper(); + const domainMapper = new SequelizeCustomerDomainMapper({ taxCatalog }); const summaryMapper = new SequelizeCustomerSummaryMapper(); // Mappers el DTO a las props validadas (CustomerProps) y luego construir agregado diff --git a/modules/customers/src/api/infrastructure/di/customer-public-services.ts b/modules/customers/src/api/infrastructure/di/customer-public-services.ts index 999c2900..a1bb929f 100644 --- a/modules/customers/src/api/infrastructure/di/customer-public-services.ts +++ b/modules/customers/src/api/infrastructure/di/customer-public-services.ts @@ -1,4 +1,4 @@ -import type { SetupParams } from "@erp/core/api"; +import { type SetupParams, buildCatalogs } from "@erp/core/api"; import type { TINNumber, UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import type { Transaction } from "sequelize"; @@ -34,9 +34,10 @@ export function buildCustomerServices( deps: CustomersInternalDeps ): CustomerPublicServices { const { database } = params; + const catalogs = buildCatalogs(); // Infrastructure - const persistenceMappers = buildCustomerPersistenceMappers(); + const persistenceMappers = buildCustomerPersistenceMappers(catalogs); const repository = buildCustomerRepository({ database, mappers: persistenceMappers }); const finder = buildCustomerFinder({ repository }); diff --git a/modules/customers/src/api/infrastructure/di/customers.di.ts b/modules/customers/src/api/infrastructure/di/customers.di.ts index a8331c4e..b333f68c 100644 --- a/modules/customers/src/api/infrastructure/di/customers.di.ts +++ b/modules/customers/src/api/infrastructure/di/customers.di.ts @@ -42,7 +42,7 @@ export function buildCustomersDependencies(params: ModuleParams): CustomersInter // Infrastructure const transactionManager = buildTransactionManager(database); const catalogs = buildCatalogs(); - const persistenceMappers = buildCustomerPersistenceMappers(); + const persistenceMappers = buildCustomerPersistenceMappers(catalogs); const repository = buildCustomerRepository({ database, mappers: persistenceMappers }); //const numberService = buildCustomerNumberGenerator(); diff --git a/modules/customers/src/api/infrastructure/mappers/domain/sequelize-customer.mapper.ts b/modules/customers/src/api/infrastructure/mappers/domain/sequelize-customer.mapper.ts index 1b005d85..aee1a2d2 100644 --- a/modules/customers/src/api/infrastructure/mappers/domain/sequelize-customer.mapper.ts +++ b/modules/customers/src/api/infrastructure/mappers/domain/sequelize-customer.mapper.ts @@ -1,3 +1,4 @@ +import type { TaxCatalogProvider } from "@erp/core"; import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api"; import { City, @@ -12,7 +13,6 @@ import { Province, Street, TINNumber, - type TaxCode, TextValue, URLAddress, UniqueID, @@ -22,9 +22,14 @@ import { maybeFromNullableResult, maybeToNullable, } from "@repo/rdx-ddd"; -import { Collection, Result } from "@repo/rdx-utils"; +import { Result } from "@repo/rdx-utils"; -import { Customer, CustomerStatus, type ICustomerCreateProps } from "../../../domain"; +import { + Customer, + CustomerStatus, + CustomerTaxes, + type ICustomerCreateProps, +} from "../../../domain"; import type { CustomerCreationAttributes, CustomerModel } from "../../sequelize"; export class SequelizeCustomerDomainMapper extends SequelizeDomainMapper< @@ -32,6 +37,13 @@ export class SequelizeCustomerDomainMapper extends SequelizeDomainMapper< CustomerCreationAttributes, Customer > { + private readonly taxCatalog: TaxCatalogProvider; + + constructor(params: { taxCatalog: TaxCatalogProvider }) { + super(); + this.taxCatalog = params.taxCatalog; + } + public mapToDomain(source: CustomerModel, params?: MapperParamsType): Result { try { const errors: ValidationErrorDetail[] = []; @@ -167,16 +179,11 @@ export class SequelizeCustomerDomainMapper extends SequelizeDomainMapper< errors ); - // source.default_taxes is stored as a comma-separated string - const defaultTaxes = new Collection(); - /*if (!isNullishOrEmpty(source.default_taxes)) { - source.default_taxes!.split(",").map((taxCode, index) => { - const tax = extractOrPushError(TaxCode.create(taxCode), `default_taxes.${index}`, errors); - if (tax) { - defaultTaxes.add(tax!); - } - }); - }*/ + const defaultTaxes = extractOrPushError( + CustomerTaxes.fromKey(source.default_taxes, this.taxCatalog), + "default_taxes", + errors + ); // Now, create the PostalAddress VO const postalAddressProps = { @@ -256,7 +263,7 @@ export class SequelizeCustomerDomainMapper extends SequelizeDomainMapper< website: maybeToNullable(source.website, (website) => website.toPrimitive()), legal_record: maybeToNullable(source.legalRecord, (legalRecord) => legalRecord.toPrimitive()), - default_taxes: source.defaultTaxes.map((taxItem) => taxItem.toPrimitive()).join(", "), + default_taxes: source.defaultTaxes.toKey(), status: source.isActive ? "active" : "inactive", language_code: source.languageCode.toPrimitive(), diff --git a/modules/customers/src/api/infrastructure/sequelize/models/customer.model.ts b/modules/customers/src/api/infrastructure/sequelize/models/customer.model.ts index 6fb796f4..dceb64e7 100644 --- a/modules/customers/src/api/infrastructure/sequelize/models/customer.model.ts +++ b/modules/customers/src/api/infrastructure/sequelize/models/customer.model.ts @@ -51,7 +51,7 @@ export class CustomerModel extends Model< declare legal_record: CreationOptional; - declare default_taxes: CreationOptional; + declare default_taxes: string; declare status: string; declare language_code: CreationOptional; declare currency_code: CreationOptional; diff --git a/modules/factuges/src/api/application/mappers/create-proforma-from-factuges-input.mapper.ts b/modules/factuges/src/api/application/mappers/create-proforma-from-factuges-input.mapper.ts index 4b58cf84..f67b48a4 100644 --- a/modules/factuges/src/api/application/mappers/create-proforma-from-factuges-input.mapper.ts +++ b/modules/factuges/src/api/application/mappers/create-proforma-from-factuges-input.mapper.ts @@ -2,7 +2,6 @@ import type { JsonTaxCatalogProvider } from "@erp/core"; import { DiscountPercentage, Tax } from "@erp/core/api"; import { type IProformaItemCreateProps, - InvoicePaymentMethod, InvoiceSerie, ItemAmount, ItemDescription, @@ -45,6 +44,10 @@ export interface IProformaFromFactuGESProps { tin: TINNumber; }; + paymentLookup: { + factuges_id: string; + }; + customerDraft: { //reference: Maybe; @@ -88,11 +91,14 @@ export interface IProformaFromFactuGESProps { languageCode: LanguageCode; currencyCode: CurrencyCode; - paymentMethod: Maybe; - items: IProformaItemCreateProps[]; globalDiscountPercentage: DiscountPercentage; }; + + paymentDraft: { + factuges_id: string; + description: string; + }; } export interface ICreateProformaFromFactugesInputMapper { @@ -132,14 +138,24 @@ export class CreateProformaFromFactugesInputMapper errors, }); + const paymentProps = this.mapPaymentProps(dto, { + companyId, + currencyCode, + errors, + }); + this.throwIfValidationErrors(errors); return Result.ok({ customerLookup: { tin: customerProps.tin, }, + paymentLookup: { + factuges_id: paymentProps.factuges_id, + }, customerDraft: customerProps, proformaDraft: proformaProps, + paymentDraft: paymentProps, }); } catch (err: unknown) { const error = isValidationErrorCollection(err) @@ -149,7 +165,24 @@ export class CreateProformaFromFactugesInputMapper } } - public mapProformaProps( + private mapPaymentProps( + dto: CreateProformaFromFactugesRequestDTO, + params: { + companyId: UniqueID; + currencyCode: CurrencyCode; + errors: ValidationErrorDetail[]; + } + ): IProformaFromFactuGESProps["paymentDraft"] { + const errors: ValidationErrorDetail[] = []; + const { companyId } = params; + + return { + factuges_id: String(dto.payment_method_id), + description: String(dto.payment_method_description), + }; + } + + private mapProformaProps( dto: CreateProformaFromFactugesRequestDTO, params: { companyId: UniqueID; @@ -208,14 +241,6 @@ export class CreateProformaFromFactugesInputMapper errors ); - const paymentMethod = extractOrPushError( - maybeFromNullableResult(dto.payment_method, (value) => - InvoicePaymentMethod.create({ paymentDescription: value }) - ), - "payment_method", - errors - ); - const globalDiscountPercentage = extractOrPushError( DiscountPercentage.create({ value: Number(dto.global_discount_percentage_value) }), "global_discount_percentage_value", @@ -257,7 +282,6 @@ export class CreateProformaFromFactugesInputMapper languageCode: languageCode!, currencyCode: currencyCode!, - paymentMethod: paymentMethod!, globalDiscountPercentage: globalDiscountPercentage!, items: itemsProps, // ← IProformaItemProps[] @@ -443,10 +467,10 @@ export class CreateProformaFromFactugesInputMapper ); const discountPercentage = extractOrPushError( - maybeFromNullableResult(item.discount_percentage_value, (value) => + maybeFromNullableResult(item.item_discount_percentage_value, (value) => DiscountPercentage.create({ value: Number(value) }) ), - `items[${index}].discount_percentage_value`, + `items[${index}].item_discount_percentage_value`, params.errors ); diff --git a/modules/factuges/src/api/application/use-cases/create-proforma-from-factuges.use-case.ts b/modules/factuges/src/api/application/use-cases/create-proforma-from-factuges.use-case.ts index 720100c9..7e2250e1 100644 --- a/modules/factuges/src/api/application/use-cases/create-proforma-from-factuges.use-case.ts +++ b/modules/factuges/src/api/application/use-cases/create-proforma-from-factuges.use-case.ts @@ -1,7 +1,11 @@ import type { JsonTaxCatalogProvider } from "@erp/core"; import { type ITransactionManager, Tax, isEntityNotFoundError } from "@erp/core/api"; import type { ProformaPublicServices } from "@erp/customer-invoices/api"; -import { type InvoiceRecipient, InvoiceStatus } from "@erp/customer-invoices/api/domain"; +import { + InvoicePaymentMethod, + type InvoiceRecipient, + InvoiceStatus, +} from "@erp/customer-invoices/api/domain"; import type { CustomerPublicServices } from "@erp/customers/api"; import { type Customer, @@ -11,7 +15,6 @@ import { } from "@erp/customers/api/domain"; import { type Name, type PhoneNumber, type TextValue, UniqueID } from "@repo/rdx-ddd"; import { Maybe, Result } from "@repo/rdx-utils"; -import type { IProformaCreatorParams } from "node_modules/@erp/customer-invoices/src/api/application"; import type { Transaction } from "sequelize"; import type { CreateProformaFromFactugesRequestDTO } from "../../../common"; @@ -20,6 +23,14 @@ import type { IProformaFromFactuGESProps, } from "../mappers"; +import paymentsCatalog from "./payments.json"; + +type FakePaymentMethod = { + id: UniqueID; + description: string; + factuges_id: string; +}; + type CreateProformaFromFactugesUseCaseInput = { companyId: UniqueID; dto: CreateProformaFromFactugesRequestDTO; @@ -33,6 +44,8 @@ type CreateProformaFromFactugesUseCaseDeps = { transactionManager: ITransactionManager; }; +type CreateProformaProps = Parameters["1"]; + export class CreateProformaFromFactugesUseCase { private readonly dtoMapper: ICreateProformaFromFactugesInputMapper; private readonly customerServices: CustomerPublicServices; @@ -57,7 +70,8 @@ export class CreateProformaFromFactugesUseCase { return Result.fail(mappedPropsResult.error); } - const { customerLookup, customerDraft, proformaDraft } = mappedPropsResult.data; + const { customerLookup, paymentLookup, customerDraft, proformaDraft, paymentDraft } = + mappedPropsResult.data; return this.transactionManager.complete(async (transaction: Transaction) => { try { @@ -71,17 +85,35 @@ export class CreateProformaFromFactugesUseCase { const customer = customerResult.data; - // Crear la proforma para ese cliente - const createPropsResult = this.buildProformaCreateProps(proformaDraft, customer.id, { + const paymentResult = await this.resolvePayment(paymentLookup, paymentDraft, { companyId, transaction, }); + if (customerResult.isFailure) { + return Result.fail(customerResult.error); + } + + const payment = paymentResult.data; + + // Crear la proforma para ese cliente + const createPropsResult = this.buildProformaCreateProps({ + proformaDraft, + payment, + customerId: customer.id, + context: { + companyId, + transaction, + }, + }); + if (createPropsResult.isFailure) { return Result.fail(createPropsResult.error); } + const newId = UniqueID.generateNewID(); + const createResult = await this.proformaServices.createProforma( - UniqueID.generateNewID(), + newId, createPropsResult.data, { companyId, @@ -93,38 +125,56 @@ export class CreateProformaFromFactugesUseCase { return Result.fail(createResult.error); } - const proforma = createResult.data; + const readResult = await this.proformaServices.getProformaSnapshotById( + createResult.data.id, + { + companyId, + transaction, + } + ); - const snapshot = { + if (readResult.isFailure) { + return Result.fail(readResult.error); + } + + const snapshot = readResult.data; + + const result = { customer_id: customer.id.toString(), - proforma_id: proforma.id.toString(), + proforma_id: snapshot.id.toString(), }; - return Result.ok(snapshot); + return Result.ok(result); } catch (error: unknown) { return Result.fail(error as Error); } }); } - private buildProformaCreateProps( - proformaDraft: IProformaFromFactuGESProps["proformaDraft"], - customerId: UniqueID, + private buildProformaCreateProps(deps: { + proformaDraft: IProformaFromFactuGESProps["proformaDraft"]; + customerId: UniqueID; + payment: FakePaymentMethod; context: { companyId: UniqueID; transaction: Transaction; - } - ): Result { + }; + }): Result { + const { proformaDraft, payment, customerId, context } = deps; const { companyId } = context; const defaultStatus = InvoiceStatus.fromApproved(); const recipient = Maybe.none(); + const paymentMethod = Maybe.some( + InvoicePaymentMethod.create({ paymentDescription: payment.description }, payment.id).data + ); return Result.ok({ ...proformaDraft, companyId, customerId, status: defaultStatus, + paymentMethod, recipient, }); } @@ -176,6 +226,33 @@ export class CreateProformaFromFactugesUseCase { }); } + private async resolvePayment( + paymentLookup: IProformaFromFactuGESProps["paymentLookup"], + paymentDraft: IProformaFromFactuGESProps["paymentDraft"], + context: { + companyId: UniqueID; + transaction: Transaction; + } + ): Promise> { + const { companyId, transaction } = context; + + const existingPaymentResult = paymentsCatalog.find( + (payment) => + payment.factuges_id === paymentLookup.factuges_id && + payment.company_id === companyId.toString() + ); + + if (existingPaymentResult) { + return Result.ok({ + id: UniqueID.create(existingPaymentResult.id).data, + description: existingPaymentResult.description, + factuges_id: existingPaymentResult.factuges_id, + }); + } + + return Result.fail(new Error("Forma de pago no existe!!!")); + } + private buildCustomerCreateProps( customerDraft: IProformaFromFactuGESProps["customerDraft"], context: { diff --git a/modules/factuges/src/api/application/use-cases/payments.json b/modules/factuges/src/api/application/use-cases/payments.json new file mode 100644 index 00000000..2a203755 --- /dev/null +++ b/modules/factuges/src/api/application/use-cases/payments.json @@ -0,0 +1,20 @@ +[ + { + "id": "019c2834-a766-7787-a626-fa89cac3a8a1", + "company_id": "5e4dc5b3-96b9-4968-9490-14bd032fec5f", + "factuges_id": "6", + "description": "TRANSFERENCIA" + }, + { + "id": "57ed228f-88bd-431d-b5e6-0ed9cff01684", + "company_id": "5e4dc5b3-96b9-4968-9490-14bd032fec5f", + "factuges_id": "14", + "description": "DOMICILIACION BANCARIA" + }, + { + "id": "336e477f-9260-4cb7-b6fd-76f3b088a395", + "company_id": "5e4dc5b3-96b9-4968-9490-14bd032fec5f", + "factuges_id": "15", + "description": "TRANSFERENCIA BANCARIA" + } +] \ No newline at end of file diff --git a/modules/factuges/src/api/infraestructure/express/factuges.routes.ts b/modules/factuges/src/api/infraestructure/express/factuges.routes.ts index cc3d4f8f..57556714 100644 --- a/modules/factuges/src/api/infraestructure/express/factuges.routes.ts +++ b/modules/factuges/src/api/infraestructure/express/factuges.routes.ts @@ -43,7 +43,6 @@ export const factugesRouter = ( router.post( "/", //checkTabContext, - validateRequest(CreateProformaFromFactugesRequestSchema, "body"), (req: Request, res: Response, next: NextFunction) => { const useCase = deps.useCases.createProforma(publicServices); diff --git a/modules/factuges/src/common/dto/request/create-proforma-from-factuges.request.dto.ts b/modules/factuges/src/common/dto/request/create-proforma-from-factuges.request.dto.ts index 032b6bc7..256b5988 100644 --- a/modules/factuges/src/common/dto/request/create-proforma-from-factuges.request.dto.ts +++ b/modules/factuges/src/common/dto/request/create-proforma-from-factuges.request.dto.ts @@ -7,16 +7,31 @@ export const CreateProformaItemFromFactugesRequestSchema = z.object({ quantity_value: NumericStringSchema.default(""), // Ya viene escalado unit_value: NumericStringSchema.default(""), - discount_percentage_value: NumericStringSchema.default(""), + subtotal_amuount_value: NumericStringSchema.default(""), + item_discount_percentage_value: NumericStringSchema.default(""), + item_discount_amount_value: NumericStringSchema.default(""), + + global_discount_percentage_value: NumericStringSchema.default(""), + global_discount_amount_value: NumericStringSchema.default(""), + + total_discount_amount_value: NumericStringSchema.default(""), + taxable_amount_value: NumericStringSchema.default(""), + + total_value: NumericStringSchema.default(""), iva_code: z.string().default(""), iva_percentage_value: NumericStringSchema.default(""), + iva_amount_value: NumericStringSchema.default(""), rec_code: z.string().default(""), rec_percentage_value: NumericStringSchema.default(""), + rec_amount_value: NumericStringSchema.default(""), retention_code: z.string().default(""), retention_percentage_value: NumericStringSchema.default(""), + retention_amount_value: NumericStringSchema.default(""), + + taxes_amount_value: NumericStringSchema.default(""), }); export type CreateProformaItemFromFactugesRequestDTO = z.infer< @@ -24,23 +39,23 @@ export type CreateProformaItemFromFactugesRequestDTO = z.infer< >; export const CreateProformaFromFactugesRequestSchema = z.object({ - //factuges_id: z.string(), - //id: z.uuid(), + factuges_id: z.string(), + //company_id: z.uuid(), + //is_proforma: z.string().default("1"), + //status: z.string(), series: z.string(), - //invoice_number: z.string(), - reference: z.string().default(""), + description: z.string().default(""), invoice_date: z.string(), operation_date: z.string().default(""), - description: z.string().default(""), notes: z.string().default(""), + language_code: z.string().default("es"), customer: z.object({ - //factuges_id: z.string(), is_company: z.string(), name: z.string(), tin: z.string(), @@ -51,7 +66,7 @@ export const CreateProformaFromFactugesRequestSchema = z.object({ postal_code: z.string(), country: z.string(), - language_code: z.string(), + language_code: z.string().default("es"), phone_primary: z.string(), phone_secondary: z.string(), @@ -63,9 +78,16 @@ export const CreateProformaFromFactugesRequestSchema = z.object({ website: z.string(), }), + subtotal_amount_value: NumericStringSchema, global_discount_percentage_value: NumericStringSchema, - payment_method: z.string().default(""), + discount_amount_value: NumericStringSchema, + taxable_amount_value: NumericStringSchema, + taxes_amount_value: NumericStringSchema, + total_amount_value: NumericStringSchema, + + payment_method_id: z.string(), + payment_method_description: z.string(), items: z.array(CreateProformaItemFromFactugesRequestSchema), });