Paso de factuges a proforma

This commit is contained in:
David Arranz 2026-03-25 10:34:17 +01:00
parent 4786eb189e
commit 41f30cde9d
13 changed files with 379 additions and 76 deletions

View File

@ -3,7 +3,13 @@ import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize"; 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 type { Proforma } from "../../../domain";
import { buildProformaNumberGenerator } from "./proforma-number-generator.di"; import { buildProformaNumberGenerator } from "./proforma-number-generator.di";
@ -24,7 +30,16 @@ export type ProformaPublicServices = {
) => Promise<Result<Proforma, Error>>; ) => Promise<Result<Proforma, Error>>;
listProformas: (filters: unknown, context: unknown) => null; listProformas: (filters: unknown, context: unknown) => null;
getProformaById: (id: unknown, context: unknown) => null; getProformaById: (
id: UniqueID,
context: ProformaServicesContext
) => Promise<Result<Proforma, Error>>;
getProformaSnapshotById: (
id: UniqueID,
context: ProformaServicesContext
) => Promise<Result<IProformaFullSnapshot, Error>>;
generateProformaReport: (id: unknown, options: unknown, context: unknown) => null; generateProformaReport: (id: unknown, options: unknown, context: unknown) => null;
}; };
@ -40,9 +55,11 @@ export function buildProformaServices(
const repository = buildProformaRepository({ database, mappers: persistenceMappers }); const repository = buildProformaRepository({ database, mappers: persistenceMappers });
const numberService = buildProformaNumberGenerator(); const numberService = buildProformaNumberGenerator();
const snapshotsBuilder = buildProformaSnapshotBuilders();
// Application helpers // Application helpers
const creator = buildProformaCreator({ numberService, repository }); const creator = buildProformaCreator({ numberService, repository });
const finder = buildProformaFinder(repository);
return { return {
createProforma: async ( createProforma: async (
@ -64,8 +81,29 @@ export function buildProformaServices(
listProformas: (filters, context) => null, listProformas: (filters, context) => null,
//internal.useCases.listProformas().execute(filters, context), //internal.useCases.listProformas().execute(filters, context),
getProformaById: (id, context) => null, getProformaById: async (id: UniqueID, context: ProformaServicesContext) => {
//internal.useCases.getProformaById().execute(id, context), 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, generateProformaReport: (id, options, context) => null,
//internal.useCases.reportProforma().execute(id, options, context), //internal.useCases.reportProforma().execute(id, options, context),

View File

@ -15,7 +15,7 @@ import {
} from "@repo/rdx-ddd"; } from "@repo/rdx-ddd";
import { type Maybe, Result } from "@repo/rdx-utils"; 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 { export interface ICustomerCreateProps {
companyId: UniqueID; companyId: UniqueID;
@ -43,7 +43,7 @@ export interface ICustomerCreateProps {
legalRecord: Maybe<TextValue>; legalRecord: Maybe<TextValue>;
defaultTaxes: CustomerTaxesProps; defaultTaxes: CustomerTaxes;
languageCode: LanguageCode; languageCode: LanguageCode;
currencyCode: CurrencyCode; currencyCode: CurrencyCode;
@ -86,19 +86,20 @@ export interface ICustomer {
readonly website: Maybe<URLAddress>; readonly website: Maybe<URLAddress>;
readonly legalRecord: Maybe<TextValue>; readonly legalRecord: Maybe<TextValue>;
readonly defaultTaxes: CustomerTaxesProps; readonly defaultTaxes: CustomerTaxes;
readonly languageCode: LanguageCode; readonly languageCode: LanguageCode;
readonly currencyCode: CurrencyCode; readonly currencyCode: CurrencyCode;
} }
type CustomerInternalProps = Omit<ICustomerCreateProps, "address"> & { type CustomerInternalProps = Omit<ICustomerCreateProps, "address" | "defaultTaxes"> & {
readonly address: PostalAddress; readonly address: PostalAddress;
readonly defaultTaxes: CustomerTaxes;
}; };
export class Customer extends AggregateRoot<CustomerInternalProps> implements ICustomer { export class Customer extends AggregateRoot<CustomerInternalProps> implements ICustomer {
static create(props: ICustomerCreateProps, id?: UniqueID): Result<Customer, Error> { static create(props: ICustomerCreateProps, id?: UniqueID): Result<Customer, Error> {
const { address, ...internalProps } = props; const { address, defaultTaxes, ...internalProps } = props;
const postalAddressResult = PostalAddress.create(address); const postalAddressResult = PostalAddress.create(address);
@ -106,9 +107,15 @@ export class Customer extends AggregateRoot<CustomerInternalProps> implements IC
return Result.fail(postalAddressResult.error); return Result.fail(postalAddressResult.error);
} }
const taxes = CustomerTaxes.create(defaultTaxes);
if (taxes.isFailure) {
return Result.fail(taxes.error);
}
const contact = new Customer( const contact = new Customer(
{ {
...internalProps, ...internalProps,
defaultTaxes: taxes.data,
address: postalAddressResult.data, address: postalAddressResult.data,
}, },
id id
@ -220,7 +227,7 @@ export class Customer extends AggregateRoot<CustomerInternalProps> implements IC
return this.props.legalRecord; return this.props.legalRecord;
} }
public get defaultTaxes(): CustomerTaxesProps { public get defaultTaxes(): CustomerTaxes {
return this.props.defaultTaxes; return this.props.defaultTaxes;
} }

View File

@ -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 { ValueObject } from "@repo/rdx-ddd";
import { type Maybe, Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
export type CustomerTaxesProps = { export type CustomerTaxesProps = {
iva: Maybe<Tax>; // si existe iva: Maybe<Tax>; // si existe
@ -16,30 +17,131 @@ export interface ICustomerItemTaxes {
toKey(): string; // Clave para representar un trío. toKey(): string; // Clave para representar un trío.
} }
export class CustomerTaxes export class CustomerTaxes extends ValueObject<CustomerTaxesProps> implements ICustomerItemTaxes {
extends ValueObject<CustomerTaxesProps>
implements ICustomerItemTaxes
{
static create(props: CustomerTaxesProps) { static create(props: CustomerTaxesProps) {
return Result.ok(new CustomerTaxes(props)); 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<CustomerTaxes> É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<CustomerTaxes, Error> {
// 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<Tax>)
// Si el código es "#", retorna Maybe.none(), si no, busca en el catálogo
const resolveTaxFromCode = (code: string): Result<Maybe<Tax>> => {
const trimmedCode = code.trim();
// Si el código es "#", significa que no existe este tipo de impuesto
if (trimmedCode === "#") {
return Result.ok(Maybe.none<Tax>());
}
// 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 { toKey(): string {
// Extrae el código del IVA, o "#" si no existe
const ivaCode = this.props.iva.match( const ivaCode = this.props.iva.match(
(iva) => iva.code, (iva) => iva.code,
() => "#" () => "#"
); );
// Extrae el código de la retención de cliente (REC), o "#" si no existe
const recCode = this.props.rec.match( const recCode = this.props.rec.match(
(rec) => rec.code, (rec) => rec.code,
() => "#" () => "#"
); );
// Extrae el código de la retención, o "#" si no existe
const retentionCode = this.props.retention.match( const retentionCode = this.props.retention.match(
(retention) => retention.code, (retention) => retention.code,
() => "#" () => "#"
); );
// Retorna la clave combinada separada por punto y coma
return `${ivaCode};${recCode};${retentionCode}`; return `${ivaCode};${recCode};${retentionCode}`;
} }

View File

@ -1,3 +1,5 @@
import type { ICatalogs } from "@erp/core/api";
import { SequelizeCustomerDomainMapper, SequelizeCustomerSummaryMapper } from "../mappers"; import { SequelizeCustomerDomainMapper, SequelizeCustomerSummaryMapper } from "../mappers";
export interface ICustomerPersistenceMappers { export interface ICustomerPersistenceMappers {
@ -7,9 +9,13 @@ export interface ICustomerPersistenceMappers {
//createMapper: CreateCustomerInputMapper; //createMapper: CreateCustomerInputMapper;
} }
export const buildCustomerPersistenceMappers = (): ICustomerPersistenceMappers => { export const buildCustomerPersistenceMappers = (
catalogs: ICatalogs
): ICustomerPersistenceMappers => {
const { taxCatalog } = catalogs;
// Mappers para el repositorio // Mappers para el repositorio
const domainMapper = new SequelizeCustomerDomainMapper(); const domainMapper = new SequelizeCustomerDomainMapper({ taxCatalog });
const summaryMapper = new SequelizeCustomerSummaryMapper(); const summaryMapper = new SequelizeCustomerSummaryMapper();
// Mappers el DTO a las props validadas (CustomerProps) y luego construir agregado // Mappers el DTO a las props validadas (CustomerProps) y luego construir agregado

View File

@ -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 type { TINNumber, UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize"; import type { Transaction } from "sequelize";
@ -34,9 +34,10 @@ export function buildCustomerServices(
deps: CustomersInternalDeps deps: CustomersInternalDeps
): CustomerPublicServices { ): CustomerPublicServices {
const { database } = params; const { database } = params;
const catalogs = buildCatalogs();
// Infrastructure // Infrastructure
const persistenceMappers = buildCustomerPersistenceMappers(); const persistenceMappers = buildCustomerPersistenceMappers(catalogs);
const repository = buildCustomerRepository({ database, mappers: persistenceMappers }); const repository = buildCustomerRepository({ database, mappers: persistenceMappers });
const finder = buildCustomerFinder({ repository }); const finder = buildCustomerFinder({ repository });

View File

@ -42,7 +42,7 @@ export function buildCustomersDependencies(params: ModuleParams): CustomersInter
// Infrastructure // Infrastructure
const transactionManager = buildTransactionManager(database); const transactionManager = buildTransactionManager(database);
const catalogs = buildCatalogs(); const catalogs = buildCatalogs();
const persistenceMappers = buildCustomerPersistenceMappers(); const persistenceMappers = buildCustomerPersistenceMappers(catalogs);
const repository = buildCustomerRepository({ database, mappers: persistenceMappers }); const repository = buildCustomerRepository({ database, mappers: persistenceMappers });
//const numberService = buildCustomerNumberGenerator(); //const numberService = buildCustomerNumberGenerator();

View File

@ -1,3 +1,4 @@
import type { TaxCatalogProvider } from "@erp/core";
import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api"; import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
import { import {
City, City,
@ -12,7 +13,6 @@ import {
Province, Province,
Street, Street,
TINNumber, TINNumber,
type TaxCode,
TextValue, TextValue,
URLAddress, URLAddress,
UniqueID, UniqueID,
@ -22,9 +22,14 @@ import {
maybeFromNullableResult, maybeFromNullableResult,
maybeToNullable, maybeToNullable,
} from "@repo/rdx-ddd"; } 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"; import type { CustomerCreationAttributes, CustomerModel } from "../../sequelize";
export class SequelizeCustomerDomainMapper extends SequelizeDomainMapper< export class SequelizeCustomerDomainMapper extends SequelizeDomainMapper<
@ -32,6 +37,13 @@ export class SequelizeCustomerDomainMapper extends SequelizeDomainMapper<
CustomerCreationAttributes, CustomerCreationAttributes,
Customer Customer
> { > {
private readonly taxCatalog: TaxCatalogProvider;
constructor(params: { taxCatalog: TaxCatalogProvider }) {
super();
this.taxCatalog = params.taxCatalog;
}
public mapToDomain(source: CustomerModel, params?: MapperParamsType): Result<Customer, Error> { public mapToDomain(source: CustomerModel, params?: MapperParamsType): Result<Customer, Error> {
try { try {
const errors: ValidationErrorDetail[] = []; const errors: ValidationErrorDetail[] = [];
@ -167,16 +179,11 @@ export class SequelizeCustomerDomainMapper extends SequelizeDomainMapper<
errors errors
); );
// source.default_taxes is stored as a comma-separated string const defaultTaxes = extractOrPushError(
const defaultTaxes = new Collection<TaxCode>(); CustomerTaxes.fromKey(source.default_taxes, this.taxCatalog),
/*if (!isNullishOrEmpty(source.default_taxes)) { "default_taxes",
source.default_taxes!.split(",").map((taxCode, index) => { errors
const tax = extractOrPushError(TaxCode.create(taxCode), `default_taxes.${index}`, errors); );
if (tax) {
defaultTaxes.add(tax!);
}
});
}*/
// Now, create the PostalAddress VO // Now, create the PostalAddress VO
const postalAddressProps = { const postalAddressProps = {
@ -256,7 +263,7 @@ export class SequelizeCustomerDomainMapper extends SequelizeDomainMapper<
website: maybeToNullable(source.website, (website) => website.toPrimitive()), website: maybeToNullable(source.website, (website) => website.toPrimitive()),
legal_record: maybeToNullable(source.legalRecord, (legalRecord) => legalRecord.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", status: source.isActive ? "active" : "inactive",
language_code: source.languageCode.toPrimitive(), language_code: source.languageCode.toPrimitive(),

View File

@ -51,7 +51,7 @@ export class CustomerModel extends Model<
declare legal_record: CreationOptional<string | null>; declare legal_record: CreationOptional<string | null>;
declare default_taxes: CreationOptional<string | null>; declare default_taxes: string;
declare status: string; declare status: string;
declare language_code: CreationOptional<string>; declare language_code: CreationOptional<string>;
declare currency_code: CreationOptional<string>; declare currency_code: CreationOptional<string>;

View File

@ -2,7 +2,6 @@ import type { JsonTaxCatalogProvider } from "@erp/core";
import { DiscountPercentage, Tax } from "@erp/core/api"; import { DiscountPercentage, Tax } from "@erp/core/api";
import { import {
type IProformaItemCreateProps, type IProformaItemCreateProps,
InvoicePaymentMethod,
InvoiceSerie, InvoiceSerie,
ItemAmount, ItemAmount,
ItemDescription, ItemDescription,
@ -45,6 +44,10 @@ export interface IProformaFromFactuGESProps {
tin: TINNumber; tin: TINNumber;
}; };
paymentLookup: {
factuges_id: string;
};
customerDraft: { customerDraft: {
//reference: Maybe<Name>; //reference: Maybe<Name>;
@ -88,11 +91,14 @@ export interface IProformaFromFactuGESProps {
languageCode: LanguageCode; languageCode: LanguageCode;
currencyCode: CurrencyCode; currencyCode: CurrencyCode;
paymentMethod: Maybe<InvoicePaymentMethod>;
items: IProformaItemCreateProps[]; items: IProformaItemCreateProps[];
globalDiscountPercentage: DiscountPercentage; globalDiscountPercentage: DiscountPercentage;
}; };
paymentDraft: {
factuges_id: string;
description: string;
};
} }
export interface ICreateProformaFromFactugesInputMapper { export interface ICreateProformaFromFactugesInputMapper {
@ -132,14 +138,24 @@ export class CreateProformaFromFactugesInputMapper
errors, errors,
}); });
const paymentProps = this.mapPaymentProps(dto, {
companyId,
currencyCode,
errors,
});
this.throwIfValidationErrors(errors); this.throwIfValidationErrors(errors);
return Result.ok({ return Result.ok({
customerLookup: { customerLookup: {
tin: customerProps.tin, tin: customerProps.tin,
}, },
paymentLookup: {
factuges_id: paymentProps.factuges_id,
},
customerDraft: customerProps, customerDraft: customerProps,
proformaDraft: proformaProps, proformaDraft: proformaProps,
paymentDraft: paymentProps,
}); });
} catch (err: unknown) { } catch (err: unknown) {
const error = isValidationErrorCollection(err) 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, dto: CreateProformaFromFactugesRequestDTO,
params: { params: {
companyId: UniqueID; companyId: UniqueID;
@ -208,14 +241,6 @@ export class CreateProformaFromFactugesInputMapper
errors errors
); );
const paymentMethod = extractOrPushError(
maybeFromNullableResult(dto.payment_method, (value) =>
InvoicePaymentMethod.create({ paymentDescription: value })
),
"payment_method",
errors
);
const globalDiscountPercentage = extractOrPushError( const globalDiscountPercentage = extractOrPushError(
DiscountPercentage.create({ value: Number(dto.global_discount_percentage_value) }), DiscountPercentage.create({ value: Number(dto.global_discount_percentage_value) }),
"global_discount_percentage_value", "global_discount_percentage_value",
@ -257,7 +282,6 @@ export class CreateProformaFromFactugesInputMapper
languageCode: languageCode!, languageCode: languageCode!,
currencyCode: currencyCode!, currencyCode: currencyCode!,
paymentMethod: paymentMethod!,
globalDiscountPercentage: globalDiscountPercentage!, globalDiscountPercentage: globalDiscountPercentage!,
items: itemsProps, // ← IProformaItemProps[] items: itemsProps, // ← IProformaItemProps[]
@ -443,10 +467,10 @@ export class CreateProformaFromFactugesInputMapper
); );
const discountPercentage = extractOrPushError( const discountPercentage = extractOrPushError(
maybeFromNullableResult(item.discount_percentage_value, (value) => maybeFromNullableResult(item.item_discount_percentage_value, (value) =>
DiscountPercentage.create({ value: Number(value) }) DiscountPercentage.create({ value: Number(value) })
), ),
`items[${index}].discount_percentage_value`, `items[${index}].item_discount_percentage_value`,
params.errors params.errors
); );

View File

@ -1,7 +1,11 @@
import type { JsonTaxCatalogProvider } from "@erp/core"; import type { JsonTaxCatalogProvider } from "@erp/core";
import { type ITransactionManager, Tax, isEntityNotFoundError } from "@erp/core/api"; import { type ITransactionManager, Tax, isEntityNotFoundError } from "@erp/core/api";
import type { ProformaPublicServices } from "@erp/customer-invoices/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 { CustomerPublicServices } from "@erp/customers/api";
import { import {
type Customer, type Customer,
@ -11,7 +15,6 @@ import {
} from "@erp/customers/api/domain"; } from "@erp/customers/api/domain";
import { type Name, type PhoneNumber, type TextValue, UniqueID } from "@repo/rdx-ddd"; import { type Name, type PhoneNumber, type TextValue, UniqueID } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils"; 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 { Transaction } from "sequelize";
import type { CreateProformaFromFactugesRequestDTO } from "../../../common"; import type { CreateProformaFromFactugesRequestDTO } from "../../../common";
@ -20,6 +23,14 @@ import type {
IProformaFromFactuGESProps, IProformaFromFactuGESProps,
} from "../mappers"; } from "../mappers";
import paymentsCatalog from "./payments.json";
type FakePaymentMethod = {
id: UniqueID;
description: string;
factuges_id: string;
};
type CreateProformaFromFactugesUseCaseInput = { type CreateProformaFromFactugesUseCaseInput = {
companyId: UniqueID; companyId: UniqueID;
dto: CreateProformaFromFactugesRequestDTO; dto: CreateProformaFromFactugesRequestDTO;
@ -33,6 +44,8 @@ type CreateProformaFromFactugesUseCaseDeps = {
transactionManager: ITransactionManager; transactionManager: ITransactionManager;
}; };
type CreateProformaProps = Parameters<ProformaPublicServices["createProforma"]>["1"];
export class CreateProformaFromFactugesUseCase { export class CreateProformaFromFactugesUseCase {
private readonly dtoMapper: ICreateProformaFromFactugesInputMapper; private readonly dtoMapper: ICreateProformaFromFactugesInputMapper;
private readonly customerServices: CustomerPublicServices; private readonly customerServices: CustomerPublicServices;
@ -57,7 +70,8 @@ export class CreateProformaFromFactugesUseCase {
return Result.fail(mappedPropsResult.error); 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) => { return this.transactionManager.complete(async (transaction: Transaction) => {
try { try {
@ -71,17 +85,35 @@ export class CreateProformaFromFactugesUseCase {
const customer = customerResult.data; const customer = customerResult.data;
// Crear la proforma para ese cliente const paymentResult = await this.resolvePayment(paymentLookup, paymentDraft, {
const createPropsResult = this.buildProformaCreateProps(proformaDraft, customer.id, {
companyId, companyId,
transaction, 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) { if (createPropsResult.isFailure) {
return Result.fail(createPropsResult.error); return Result.fail(createPropsResult.error);
} }
const newId = UniqueID.generateNewID();
const createResult = await this.proformaServices.createProforma( const createResult = await this.proformaServices.createProforma(
UniqueID.generateNewID(), newId,
createPropsResult.data, createPropsResult.data,
{ {
companyId, companyId,
@ -93,38 +125,56 @@ export class CreateProformaFromFactugesUseCase {
return Result.fail(createResult.error); 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(), 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) { } catch (error: unknown) {
return Result.fail(error as Error); return Result.fail(error as Error);
} }
}); });
} }
private buildProformaCreateProps( private buildProformaCreateProps(deps: {
proformaDraft: IProformaFromFactuGESProps["proformaDraft"], proformaDraft: IProformaFromFactuGESProps["proformaDraft"];
customerId: UniqueID, customerId: UniqueID;
payment: FakePaymentMethod;
context: { context: {
companyId: UniqueID; companyId: UniqueID;
transaction: Transaction; transaction: Transaction;
} };
): Result<IProformaCreatorParams["props"], Error> { }): Result<CreateProformaProps, Error> {
const { proformaDraft, payment, customerId, context } = deps;
const { companyId } = context; const { companyId } = context;
const defaultStatus = InvoiceStatus.fromApproved(); const defaultStatus = InvoiceStatus.fromApproved();
const recipient = Maybe.none<InvoiceRecipient>(); const recipient = Maybe.none<InvoiceRecipient>();
const paymentMethod = Maybe.some(
InvoicePaymentMethod.create({ paymentDescription: payment.description }, payment.id).data
);
return Result.ok({ return Result.ok({
...proformaDraft, ...proformaDraft,
companyId, companyId,
customerId, customerId,
status: defaultStatus, status: defaultStatus,
paymentMethod,
recipient, recipient,
}); });
} }
@ -176,6 +226,33 @@ export class CreateProformaFromFactugesUseCase {
}); });
} }
private async resolvePayment(
paymentLookup: IProformaFromFactuGESProps["paymentLookup"],
paymentDraft: IProformaFromFactuGESProps["paymentDraft"],
context: {
companyId: UniqueID;
transaction: Transaction;
}
): Promise<Result<FakePaymentMethod, Error>> {
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( private buildCustomerCreateProps(
customerDraft: IProformaFromFactuGESProps["customerDraft"], customerDraft: IProformaFromFactuGESProps["customerDraft"],
context: { context: {

View File

@ -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"
}
]

View File

@ -43,7 +43,6 @@ export const factugesRouter = (
router.post( router.post(
"/", "/",
//checkTabContext, //checkTabContext,
validateRequest(CreateProformaFromFactugesRequestSchema, "body"), validateRequest(CreateProformaFromFactugesRequestSchema, "body"),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.createProforma(publicServices); const useCase = deps.useCases.createProforma(publicServices);

View File

@ -7,16 +7,31 @@ export const CreateProformaItemFromFactugesRequestSchema = z.object({
quantity_value: NumericStringSchema.default(""), // Ya viene escalado quantity_value: NumericStringSchema.default(""), // Ya viene escalado
unit_value: NumericStringSchema.default(""), 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_code: z.string().default(""),
iva_percentage_value: NumericStringSchema.default(""), iva_percentage_value: NumericStringSchema.default(""),
iva_amount_value: NumericStringSchema.default(""),
rec_code: z.string().default(""), rec_code: z.string().default(""),
rec_percentage_value: NumericStringSchema.default(""), rec_percentage_value: NumericStringSchema.default(""),
rec_amount_value: NumericStringSchema.default(""),
retention_code: z.string().default(""), retention_code: z.string().default(""),
retention_percentage_value: NumericStringSchema.default(""), retention_percentage_value: NumericStringSchema.default(""),
retention_amount_value: NumericStringSchema.default(""),
taxes_amount_value: NumericStringSchema.default(""),
}); });
export type CreateProformaItemFromFactugesRequestDTO = z.infer< export type CreateProformaItemFromFactugesRequestDTO = z.infer<
@ -24,23 +39,23 @@ export type CreateProformaItemFromFactugesRequestDTO = z.infer<
>; >;
export const CreateProformaFromFactugesRequestSchema = z.object({ export const CreateProformaFromFactugesRequestSchema = z.object({
//factuges_id: z.string(),
//id: z.uuid(), //id: z.uuid(),
factuges_id: z.string(),
//company_id: z.uuid(),
//is_proforma: z.string().default("1"),
//status: z.string(),
series: z.string(), series: z.string(),
//invoice_number: z.string(),
reference: z.string().default(""), reference: z.string().default(""),
description: z.string().default(""),
invoice_date: z.string(), invoice_date: z.string(),
operation_date: z.string().default(""), operation_date: z.string().default(""),
description: z.string().default(""),
notes: z.string().default(""), notes: z.string().default(""),
language_code: z.string().default("es"),
customer: z.object({ customer: z.object({
//factuges_id: z.string(),
is_company: z.string(), is_company: z.string(),
name: z.string(), name: z.string(),
tin: z.string(), tin: z.string(),
@ -51,7 +66,7 @@ export const CreateProformaFromFactugesRequestSchema = z.object({
postal_code: z.string(), postal_code: z.string(),
country: z.string(), country: z.string(),
language_code: z.string(), language_code: z.string().default("es"),
phone_primary: z.string(), phone_primary: z.string(),
phone_secondary: z.string(), phone_secondary: z.string(),
@ -63,9 +78,16 @@ export const CreateProformaFromFactugesRequestSchema = z.object({
website: z.string(), website: z.string(),
}), }),
subtotal_amount_value: NumericStringSchema,
global_discount_percentage_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), items: z.array(CreateProformaItemFromFactugesRequestSchema),
}); });