This commit is contained in:
David Arranz 2026-05-06 13:10:47 +02:00
parent 0fc0717822
commit 2c6cac4859
26 changed files with 171 additions and 119 deletions

View File

@ -19,5 +19,12 @@
"factuges_id": "15",
"description": "TRANSFERENCIA BANCARIA",
"group": "General"
},
{
"id": "126e477f-9260-4cb7-b6fd-76f3b088a395",
"company_id": "5e4dc5b3-96b9-4968-9490-14bd032fec5f",
"factuges_id": "21",
"description": "30/60/90/120",
"group": "General"
}
]

View File

@ -29,6 +29,13 @@ const toNumericString = (dto?: MoneyDTO | null, fallbackScale = 2): string => {
return toNumber(dto, fallbackScale).toString();
};
const toNumericNulleable = (dto?: MoneyDTO | null, fallbackScale = 2): number | null => {
if (isEmptyMoneyDTO(dto)) {
return null;
}
return toNumber(dto, fallbackScale);
};
/**
* Formatea un MoneyDTO según un locale.
*
@ -117,6 +124,7 @@ export const MoneyDTOHelper = {
isEmpty: isEmptyMoneyDTO,
toNumber,
toNumericString,
toNumericNulleable,
fromNumber,
fromNumericString,
format,

View File

@ -25,6 +25,13 @@ const toNumericString = (dto?: PercentageDTO | null, fallbackScale = 2): string
return toNumber(dto, fallbackScale).toString();
};
const toNumericNulleable = (dto?: PercentageDTO | null, fallbackScale = 2): number | null => {
if (isEmptyPercentageDTO(dto)) {
return null;
}
return toNumber(dto, fallbackScale);
};
/**
* Formatea un PercentageDTO según un locale.
*
@ -97,6 +104,7 @@ export const PercentageDTOHelper = {
isEmpty: isEmptyPercentageDTO,
toNumber,
toNumericString,
toNumericNulleable,
fromNumber,
fromNumericString,
format,

View File

@ -25,6 +25,13 @@ const toNumericString = (dto?: QuantityDTO | null, fallbackScale = 2): string =>
return toNumber(dto, fallbackScale).toString();
};
const toNumericNulleable = (dto?: QuantityDTO | null, fallbackScale = 2): number | null => {
if (isEmptyQuantityDTO(dto)) {
return null;
}
return toNumber(dto, fallbackScale);
};
/**
* Formatea un QuantityDTO según un locale.
*
@ -91,6 +98,7 @@ export const QuantityDTOHelper = {
isEmpty: isEmptyQuantityDTO,
toNumber,
toNumericString,
toNumericNulleable,
fromNumber,
fromNumericString,
format,

View File

@ -1,4 +1,4 @@
import { useDebounce } from "@repo/rdx-ui/components";
import { useDebounce } from "@erp/core/hooks";
import { useMemo, useState } from "react";
import type {

View File

@ -40,7 +40,7 @@ export const GetProformaByIdAdapter = {
currencyCode: dto.currency_code,
customerId: dto.customer_id,
recipient: mapRecipient(dto.recipient),
recipient: mapRecipient(dto),
taxes: dto.taxes.map(mapTaxSummary),
paymentMethod: dto.payment_method?.id,
@ -48,8 +48,8 @@ export const GetProformaByIdAdapter = {
subtotalAmount: MoneyDTOHelper.toNumber(dto.subtotal_amount),
itemsDiscountAmount: MoneyDTOHelper.toNumber(dto.items_discount_amount),
globalDiscountPercentage: PercentageDTOHelper.toNumber(dto.discount_percentage),
discountAmount: MoneyDTOHelper.toNumber(dto.discount_amount),
globalDiscountPercentage: PercentageDTOHelper.toNumber(dto.global_discount_percentage),
totalDiscountAmount: MoneyDTOHelper.toNumber(dto.total_discount_amount),
taxableAmount: MoneyDTOHelper.toNumber(dto.taxable_amount),
ivaAmount: MoneyDTOHelper.toNumber(dto.iva_amount),
@ -65,19 +65,19 @@ export const GetProformaByIdAdapter = {
};
const mapItem = (dto: GetProformaByIdResponseDTO["items"][number]): ProformaItem => {
return {
const item: ProformaItem = {
id: dto.id,
position: Number(dto.position),
isValued: dto.is_valued,
description: dto.description,
quantity: QuantityDTOHelper.toNumber(dto.quantity),
unitAmount: MoneyDTOHelper.toNumber(dto.unit_amount),
quantity: QuantityDTOHelper.toNumericNulleable(dto.quantity),
unitAmount: MoneyDTOHelper.toNumericNulleable(dto.unit_amount),
subtotalAmount: MoneyDTOHelper.toNumber(dto.subtotal_amount),
itemDiscountPercentage: PercentageDTOHelper.toNumber(dto.item_discount_percentage),
itemDiscountPercentage: PercentageDTOHelper.toNumericNulleable(dto.item_discount_percentage),
itemDiscountAmount: MoneyDTOHelper.toNumber(dto.item_discount_amount),
globalDiscountPercentage: PercentageDTOHelper.toNumber(dto.global_discount_percentage),
@ -104,18 +104,20 @@ const mapItem = (dto: GetProformaByIdResponseDTO["items"][number]): ProformaItem
// Dejamos fallback explícito para no inventar parseos.
taxes: "",
};
return item;
};
const mapRecipient = (dto: GetProformaByIdResponseDTO["recipient"]): ProformaRecipient => {
const mapRecipient = (dto: GetProformaByIdResponseDTO): ProformaRecipient => {
return {
id: dto.id,
name: dto.name,
tin: dto.tin,
street: dto.street,
street2: dto.street2,
city: dto.city,
province: dto.province,
postalCode: dto.postal_code,
country: dto.country,
id: dto.customer_id,
name: dto.recipient.name,
tin: dto.recipient.tin,
street: dto.recipient.street,
street2: dto.recipient.street2,
city: dto.recipient.city,
province: dto.recipient.province,
postalCode: dto.recipient.postal_code,
country: dto.recipient.country,
};
};

View File

@ -40,8 +40,7 @@ export const ProformaToListRowPatchAdapter = {
country: proforma.recipient.country,
},
subtotalAmount: proforma.subtotalAmount,
discountPercentage: proforma.globalDiscountPercentage,
discountAmount: proforma.discountAmount,
totalDiscountAmount: proforma.totalDiscountAmount,
taxableAmount: proforma.taxableAmount,
taxesAmount: proforma.taxesAmount,
totalAmount: proforma.totalAmount,

View File

@ -9,14 +9,14 @@ export interface ProformaItem {
position: number;
isValued: boolean;
description: string;
description: string | null;
quantity: number;
unitAmount: number;
quantity: number | null;
unitAmount: number | null;
subtotalAmount: number;
itemDiscountPercentage: number;
itemDiscountPercentage: number | null;
itemDiscountAmount: number;
globalDiscountPercentage: number;
@ -28,11 +28,11 @@ export interface ProformaItem {
ivaPercentage: number;
ivaAmount: number;
recCode: string;
recCode: string | null;
recPercentage: number;
recAmount: number;
retentionCode: string;
retentionCode: string | null;
retentionPercentage: number;
retentionAmount: number;

View File

@ -11,11 +11,11 @@ export interface ProformaTaxSummary {
ivaPercentage: number;
ivaAmount: number;
recCode: string;
recCode: string | null;
recPercentage: number;
recAmount: number;
retentionCode: string;
retentionCode: string | null;
retentionPercentage: number;
retentionAmount: number;

View File

@ -13,18 +13,17 @@ import type { ProformaTaxSummary } from "./proforma-tax-summary.entity";
export interface Proforma {
id: string;
companyId: string;
isProforma: boolean;
invoiceNumber: string;
status: ProformaStatus;
series: string;
series: string | null;
invoiceDate: string;
operationDate: string;
operationDate: string | null;
reference: string;
description: string;
notes: string;
reference: string | null;
description: string | null;
notes: string | null;
languageCode: string;
currencyCode: string;
@ -40,7 +39,7 @@ export interface Proforma {
itemsDiscountAmount: number;
globalDiscountPercentage: number;
discountAmount: number;
totalDiscountAmount: number;
taxableAmount: number;
ivaAmount: number;

View File

@ -11,6 +11,7 @@ import type { ProformaItemUpdateForm } from "../entities";
export const mapProformaItemsToProformaItemsUpdateForm = (
item: ProformaItem
): ProformaItemUpdateForm => {
console.log(item);
return {
id: item.id,
position: item.position,

View File

@ -74,6 +74,8 @@ export const useUpdateProformaController = (
const initialValues = useMemo<ProformaUpdateForm>(() => {
if (!proformaData) return buildProformaUpdateDefault();
console.log("initialValues", proformaData);
return mapProformaToProformaUpdateForm(proformaData);
}, [proformaData]);

View File

@ -178,7 +178,7 @@ export const LineEditor = <TLine,>({
}
/>
<DropdownMenuContent align="end">
<DropdownMenuContent align="end" className="min-w-48">
<DropdownMenuItem onClick={() => onAddBefore(index)}>
<PlusCircle className="mr-2 h-4 w-4" />
{insertBeforeLabel}

View File

@ -8,7 +8,7 @@ export const buildProformaItemUpdateDefault = (position: number): ProformaItemUp
position,
isValued: false,
description: "",
description: null,
quantity: null,
unitAmount: null,

View File

@ -1,4 +1,5 @@
import type { ICatalogs, ITransactionManager } from "@erp/core/api";
import type { IProformaPublicServices } from "@erp/customer-invoices/api";
import type { ICustomerPublicServices } from "@erp/customers/api";
import type { ICreateProformaFromFactugesInputMapper } from "../mappers";
@ -10,6 +11,7 @@ export function buildCreateProformaFromFactugesUseCase(deps: {
finder: IFactuGESProformaFinder;
publicServices: {
customerServices: ICustomerPublicServices;
proformaServices: IProformaPublicServices;
};
dtoMapper: ICreateProformaFromFactugesInputMapper;
catalogs: ICatalogs;
@ -17,18 +19,21 @@ export function buildCreateProformaFromFactugesUseCase(deps: {
}) {
const {
linker,
finder,
dtoMapper,
transactionManager,
publicServices: { customerServices },
publicServices: { customerServices, proformaServices },
} = deps;
const { taxCatalog } = deps.catalogs;
const { taxCatalog, paymentCatalog } = deps.catalogs;
return new CreateProformaFromFactugesUseCase({
linker,
finder,
customerServices,
proformaServices,
dtoMapper,
taxCatalog,
paymentCatalog,
transactionManager,
});
}

View File

@ -1,2 +1,2 @@
export * from "./factuges-input-mappers.di";
export * from "./factuges-use-cases.di";
export * from "./factuges-input-mappers.di"

View File

@ -1,3 +1,2 @@
export * from "./mappers";
export * from "./repositories";
export * from "./use-cases";

View File

@ -164,8 +164,6 @@ export class CreateProformaFromFactugesInputMapper
errors,
});
console.log(paymentProps);
this.throwIfValidationErrors(errors);
return Result.ok({
@ -173,7 +171,7 @@ export class CreateProformaFromFactugesInputMapper
tin: customerProps.tin,
},
paymentLookup: {
factuges_id: paymentProps.factuges_id,
factuges_id: paymentProps?.factuges_id,
},
customerDraft: customerProps,
proformaDraft: proformaProps,
@ -195,25 +193,25 @@ export class CreateProformaFromFactugesInputMapper
currencyCode: CurrencyCode;
errors: ValidationErrorDetail[];
}
): ProformaPaymentDraft {
): ProformaPaymentDraft | undefined {
const errors: ValidationErrorDetail[] = [];
const { companyId } = params;
const factuges_id = String(dto.payment_method_id);
const paymentOrNot = this.paymentCatalog.findByFactuGESId(factuges_id);
const payment_method_id = String(dto.payment_method_id);
const paymentOrNot = this.paymentCatalog.findByFactuGESId(payment_method_id);
if (paymentOrNot.isNone()) {
errors.push({
path: "payment_method_id",
message: "Forma de pago no encontrada",
});
} else {
return {
payment_id: paymentOrNot.unwrap().id,
factuges_id: paymentOrNot.unwrap().factuges_id,
description: paymentOrNot.unwrap().description,
};
}
return {
payment_id: paymentOrNot.unwrap().id,
factuges_id: paymentOrNot.unwrap().factuges_id,
description: paymentOrNot.unwrap().description,
};
}
private mapProformaProps(

View File

@ -1,4 +1,4 @@
import type { JsonTaxCatalogProvider } from "@erp/core";
import type { JsonPaymentCatalogProvider, JsonTaxCatalogProvider } from "@erp/core";
import { type ITransactionManager, isEntityNotFoundError } from "@erp/core/api";
import type { IProformaPublicServices } from "@erp/customer-invoices/api";
import {
@ -30,19 +30,31 @@ import type { CreateProformaFromFactugesRequestDTO } from "../../../common";
import type { FactugesProformaPayload, ICreateProformaFromFactugesInputMapper } from "../mappers";
import type { IFactuGESProformaFinder, IFactuGESProformaLinker } from "../services";
import paymentsCatalog from "./payments.json";
type FakePaymentMethod = {
id: UniqueID;
description: string;
factuges_id: string;
};
type CreateProformaFromFactugesUseCaseInput = {
companyId: UniqueID;
dto: CreateProformaFromFactugesRequestDTO;
};
export type CreateProformaFromFactugesResponseDTO = {
proforma_id: string;
};
export type ResolvedPaymentMethod = {
id: UniqueID;
description: string;
factugesId: string;
};
export interface IFactugesPaymentMethodResolver {
resolve(
lookup: FactugesProformaPayload["paymentLookup"],
context: {
companyId: UniqueID;
transaction: Transaction;
}
): Promise<Result<ResolvedPaymentMethod, Error>>;
}
type CreateProformaFromFactugesUseCaseDeps = {
linker: IFactuGESProformaLinker;
finder: IFactuGESProformaFinder;
@ -50,17 +62,19 @@ type CreateProformaFromFactugesUseCaseDeps = {
proformaServices: IProformaPublicServices;
dtoMapper: ICreateProformaFromFactugesInputMapper;
taxCatalog: JsonTaxCatalogProvider;
paymentCatalog: JsonPaymentCatalogProvider;
transactionManager: ITransactionManager;
};
type CreateProformaProps = Parameters<IProformaPublicServices["createProforma"]>["1"];
export class CreateProformaFromFactugesUseCase {
private readonly dtoMapper: ICreateProformaFromFactugesInputMapper;
private readonly linker: IFactuGESProformaLinker;
private readonly finder: IFactuGESProformaFinder;
private readonly dtoMapper: ICreateProformaFromFactugesInputMapper;
private readonly customerServices: ICustomerPublicServices;
private readonly proformaServices: IProformaPublicServices;
private readonly paymentCatalog: JsonPaymentCatalogProvider;
private readonly taxCatalog: JsonTaxCatalogProvider;
private readonly transactionManager: ITransactionManager;
@ -70,11 +84,14 @@ export class CreateProformaFromFactugesUseCase {
this.customerServices = deps.customerServices;
this.proformaServices = deps.proformaServices;
this.dtoMapper = deps.dtoMapper;
this.paymentCatalog = deps.paymentCatalog;
this.taxCatalog = deps.taxCatalog;
this.transactionManager = deps.transactionManager;
}
public async execute(params: CreateProformaFromFactugesUseCaseInput) {
public async execute(
params: CreateProformaFromFactugesUseCaseInput
): Promise<Result<CreateProformaFromFactugesResponseDTO, Error>> {
const { dto, companyId } = params;
// 1) Mapear DTO → props
@ -87,16 +104,25 @@ export class CreateProformaFromFactugesUseCase {
mappedPropsResult.data;
// 2) Comprobar si la proforma ya existe (idempotencia)
const proformaIdResult = await this.finder.findProformaIdByFactuGESId(
const proformaExists = await this.finder.existsProformaByFactuGESId(
companyId,
proformaDraft.factugesID
);
if (proformaIdResult.isSuccess) {
const existingProforma = proformaIdResult.data;
if (proformaExists.isSuccess && proformaExists.data) {
// 2.1) Recuperar el ID de la proforma que ya existe
const proformaIdResult = await this.finder.findProformaIdByFactuGESId(
companyId,
proformaDraft.factugesID
);
if (proformaIdResult.isFailure) {
return Result.fail(proformaIdResult.error);
}
const proformaId = proformaIdResult.data;
return Result.ok({
proforma_id: existingProforma.toString(),
proforma_id: proformaId.toString(),
});
}
@ -113,7 +139,7 @@ export class CreateProformaFromFactugesUseCase {
const customer = customerResult.data;
const paymentResult = await this.resolvePayment(paymentLookup, paymentDraft, {
const paymentResult = await this.resolvePayment(paymentLookup, {
companyId,
transaction,
});
@ -338,7 +364,7 @@ export class CreateProformaFromFactugesUseCase {
private buildProformaCreateProps(deps: {
proformaDraft: FactugesProformaPayload["proformaDraft"];
customerId: UniqueID;
payment: FakePaymentMethod;
payment: ResolvedPaymentMethod;
context: {
companyId: UniqueID;
transaction: Transaction;
@ -410,31 +436,35 @@ export class CreateProformaFromFactugesUseCase {
});
}
private async resolvePayment(
private resolvePayment(
paymentLookup: FactugesProformaPayload["paymentLookup"],
paymentDraft: FactugesProformaPayload["paymentDraft"],
context: {
companyId: UniqueID;
transaction: Transaction;
}
): Promise<Result<FakePaymentMethod, Error>> {
const { companyId, transaction } = context;
context: { companyId: UniqueID; transaction: Transaction }
): Result<ResolvedPaymentMethod, Error> {
const payment = this.paymentCatalog.findByFactuGESId(paymentLookup.factuges_id);
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,
});
if (payment.isNone()) {
return Result.fail(new Error("La forma de pago de FactuGES no existe."));
}
return Result.fail(new Error("Forma de pago no existe!!!"));
const paymentItem = payment.unwrap();
if (paymentItem.company_id !== context.companyId.toString()) {
return Result.fail(
new Error("La forma de pago de FactuGES no pertenece a la compañía actual.")
);
}
const idResult = UniqueID.create(paymentItem.id);
if (idResult.isFailure) {
return Result.fail(idResult.error);
}
return Result.ok({
id: idResult.data,
description: paymentItem.description,
factugesId: paymentItem.factuges_id,
});
}
private buildCustomerCreateProps(

View File

@ -1,23 +0,0 @@
[
{
"id": "019c2834-a766-7787-a626-fa89cac3a8a1",
"company_id": "5e4dc5b3-96b9-4968-9490-14bd032fec5f",
"factuges_id": "6",
"description": "TRANSFERENCIA",
"group": "General"
},
{
"id": "57ed228f-88bd-431d-b5e6-0ed9cff01684",
"company_id": "5e4dc5b3-96b9-4968-9490-14bd032fec5f",
"factuges_id": "14",
"description": "DOMICILIACION BANCARIA",
"group": "General"
},
{
"id": "336e477f-9260-4cb7-b6fd-76f3b088a395",
"company_id": "5e4dc5b3-96b9-4968-9490-14bd032fec5f",
"factuges_id": "15",
"description": "TRANSFERENCIA BANCARIA",
"group": "General"
}
]

View File

@ -1,6 +1,6 @@
import type { IModuleServer } from "@erp/core/api";
import { factugesRouter } from "./infraestructure";
import { factugesRouter, models } from "./infraestructure";
import { buildFactugesDependencies } from "./infraestructure/di";
export const factugesAPIModule: IModuleServer = {
@ -28,7 +28,7 @@ export const factugesAPIModule: IModuleServer = {
return {
// Modelos Sequelize del módulo
models: [],
models,
// Servicios expuestos a otros módulos
services: {},

View File

@ -41,7 +41,10 @@ export function buildFactugesDependencies(params: SetupParams): FactugesInternal
// Internal use cases (factories)
return {
useCases: {
createProforma: (publicServices: { customerServices: ICustomerPublicServices }) =>
createProforma: (publicServices: {
customerServices: ICustomerPublicServices;
proformaServices: IProformaPublicServices;
}) =>
buildCreateProformaFromFactugesUseCase({
linker,
finder,

View File

@ -29,6 +29,11 @@ export class CreateProformaFromFactugesController extends ExpressController {
}
const dto = this.req.body as CreateProformaFromFactugesRequestDTO;
// Request mapper
// Command for use case
// Execute use case
const result = await this.useCase.execute({ dto, companyId });
return result.match(

View File

@ -40,7 +40,7 @@ export function extractOrPushError<T>(
if (isValidationErrorCollection(error)) {
// Copiar todos los detalles, rellenando path si falta
console.log(error);
error.details?.forEach((detail) => {
errors.push({
...detail,

View File

@ -99,7 +99,7 @@ export class Collection<T> {
* @param callbackfn A function that accepts up to three arguments. forEach calls the callbackfn function one time for each element in the array.
* @param thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value.
*/
forEach<U>(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void {
forEach<U>(callbackfn: (value: T, index: number, array: T[]) => void, _thisArg?: unknown): void {
this.items.forEach(callbackfn);
}