This commit is contained in:
David Arranz 2026-05-04 20:33:24 +02:00
parent 4beb7aa207
commit 92faca9bfa
70 changed files with 394 additions and 157 deletions

View File

@ -5,6 +5,12 @@
// Enable Font Ligatures
"editor.fontLigatures": true,
// Lint
"eslint.workingDirectories": [{ "mode": "auto" }],
"eslint.run": "onType",
"eslint.validate": ["javascript", "typescript", "typescriptreact"],
"eslint.lintTask.enable": true,
// Javascript and TypeScript settings
"js/ts.suggest.enabled": true,
"js/ts.suggest.autoImports": true,

View File

@ -1,6 +1,6 @@
{
"name": "@erp/factuges-server",
"version": "0.6.3",
"version": "0.6.4",
"private": true,
"scripts": {
"build": "tsup src/index.ts --config tsup.config.ts",

View File

@ -125,14 +125,14 @@ async function setupModule(name: string, params: ModuleParams, stack: string[])
// 4) models
if (pkgApi?.models) {
await withPhase(name, "registerModels", async () => {
await Promise.resolve(registerModels(pkgApi.models, params, { moduleName: name }));
await Promise.resolve(registerModels(pkgApi.models!, params, { moduleName: name }));
});
}
// 5) services (namespaced)
if (pkgApi?.services) {
await withPhase(name, "registerServices", async () => {
validateModuleServices(name, pkgApi.services);
validateModuleServices(name, pkgApi.services!);
for (const [serviceKey, serviceApi] of Object.entries(pkgApi.services!)) {
const fullName = buildServiceName(name, serviceKey);

View File

@ -1,7 +1,7 @@
{
"name": "@erp/factuges-web",
"private": true,
"version": "0.6.3",
"version": "0.6.4",
"type": "module",
"scripts": {
"dev": "vite --host --clearScreen false",

View File

@ -1,18 +1,18 @@
import { Button } from "@repo/shadcn-ui/components";
import * as React from "react";
import { FallbackProps } from "react-error-boundary";
import type { FallbackProps } from "react-error-boundary";
/**
* 1) Fallback simple
*/
export function SimpleFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div role='alert' className='p-4 rounded-md border bg-red-50 text-red-700'>
<p className='font-medium'> Algo salió mal</p>
<pre className='mt-2 text-sm whitespace-pre-wrap'>{error?.message}</pre>
<div className="p-4 rounded-md border bg-red-50 text-red-700" role="alert">
<p className="font-medium"> Algo salió mal</p>
<pre className="mt-2 text-sm whitespace-pre-wrap">{error?.message}</pre>
<Button
className="mt-3 px-3 py-1.5 rounded-md bg-red-600 text-white"
onClick={resetErrorBoundary}
className='mt-3 px-3 py-1.5 rounded-md bg-red-600 text-white'
>
Reintentar
</Button>
@ -25,22 +25,22 @@ export function SimpleFallback({ error, resetErrorBoundary }: FallbackProps) {
*/
export function SectionCardFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div role='alert' className='rounded-2xl border shadow-sm p-5 bg-white'>
<div className='flex items-start gap-3'>
<div className='shrink-0 rounded-full p-2 bg-red-100'></div>
<div className='grow'>
<h3 className='font-semibold text-gray-900'>No se pudo cargar esta sección</h3>
<p className='mt-1 text-sm text-gray-600'>{error?.message}</p>
<div className='mt-3 flex gap-2'>
<div className="rounded-2xl border shadow-sm p-5 bg-white" role="alert">
<div className="flex items-start gap-3">
<div className="shrink-0 rounded-full p-2 bg-red-100"></div>
<div className="grow">
<h3 className="font-semibold text-gray-900">No se pudo cargar esta sección</h3>
<p className="mt-1 text-sm text-gray-600">{error?.message}</p>
<div className="mt-3 flex gap-2">
<Button
className="px-3 py-1.5 rounded-md bg-gray-900 text-white"
onClick={resetErrorBoundary}
className='px-3 py-1.5 rounded-md bg-gray-900 text-white'
>
Reintentar
</Button>
<Button
className="px-3 py-1.5 rounded-md border"
onClick={() => window.location.reload()}
className='px-3 py-1.5 rounded-md border'
>
Refrescar página
</Button>
@ -56,15 +56,15 @@ export function SectionCardFallback({ error, resetErrorBoundary }: FallbackProps
*/
export function FullPageFallback({ error }: FallbackProps) {
return (
<div className='min-h-screen flex flex-col items-center justify-center px-6 bg-gray-50 text-center'>
<div className='text-5xl'>😵</div>
<h1 className='mt-4 text-2xl font-bold text-gray-900'>Ocurrió un error inesperado</h1>
<p className='mt-2 text-gray-600'>{error?.message}</p>
<div className='mt-6 flex flex-wrap items-center justify-center gap-3'>
<a href='/' className='px-4 py-2 rounded-md bg-blue-600 text-white'>
<div className="min-h-screen flex flex-col items-center justify-center px-6 bg-gray-50 text-center">
<div className="text-5xl">😵</div>
<h1 className="mt-4 text-2xl font-bold text-gray-900">Ocurrió un error inesperado</h1>
<p className="mt-2 text-gray-600">{error?.message}</p>
<div className="mt-6 flex flex-wrap items-center justify-center gap-3">
<a className="px-4 py-2 rounded-md bg-blue-600 text-white" href="/">
Volver al inicio
</a>
<Button onClick={() => window.location.reload()} className='px-4 py-2 rounded-md border'>
<Button className="px-4 py-2 rounded-md border" onClick={() => window.location.reload()}>
Recargar
</Button>
</div>
@ -77,22 +77,22 @@ export function FullPageFallback({ error }: FallbackProps) {
*/
export function ListFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div role='alert' className='p-4 rounded-md border bg-amber-50'>
<div className='flex items-start gap-3'>
<span className='text-xl'>🗂</span>
<div className="p-4 rounded-md border bg-amber-50" role="alert">
<div className="flex items-start gap-3">
<span className="text-xl">🗂</span>
<div>
<p className='font-medium text-amber-900'>No pudimos cargar la lista.</p>
<p className='text-sm text-amber-800 mt-1'>{error?.message}</p>
<div className='mt-3 flex gap-2'>
<p className="font-medium text-amber-900">No pudimos cargar la lista.</p>
<p className="text-sm text-amber-800 mt-1">{error?.message}</p>
<div className="mt-3 flex gap-2">
<Button
className="px-3 py-1.5 rounded-md bg-amber-700 text-white"
onClick={resetErrorBoundary}
className='px-3 py-1.5 rounded-md bg-amber-700 text-white'
>
Reintentar
</Button>
<Button
className="px-3 py-1.5 rounded-md border"
onClick={() => window.location.reload()}
className='px-3 py-1.5 rounded-md border'
>
Recargar
</Button>
@ -108,17 +108,17 @@ export function ListFallback({ error, resetErrorBoundary }: FallbackProps) {
*/
export function FormFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div role='alert' className='rounded-md border p-4 bg-red-50'>
<h4 className='font-semibold text-red-800'>No se pudo mostrar el formulario</h4>
<p className='mt-1 text-sm text-red-700'>{error?.message}</p>
<div className='mt-3 flex gap-2'>
<div className="rounded-md border p-4 bg-red-50" role="alert">
<h4 className="font-semibold text-red-800">No se pudo mostrar el formulario</h4>
<p className="mt-1 text-sm text-red-700">{error?.message}</p>
<div className="mt-3 flex gap-2">
<Button
className="px-3 py-1.5 rounded-md bg-red-600 text-white"
onClick={resetErrorBoundary}
className='px-3 py-1.5 rounded-md bg-red-600 text-white'
>
Reintentar
</Button>
<Button onClick={() => history.back()} className='px-3 py-1.5 rounded-md border'>
<Button className="px-3 py-1.5 rounded-md border" onClick={() => history.back()}>
Volver atrás
</Button>
</div>
@ -135,18 +135,18 @@ export function NetworkFallback({ error, resetErrorBoundary }: FallbackProps) {
: "Estás sin conexión. Revisa tu red y reintenta.";
return (
<div role='alert' className='rounded-md border p-4 bg-blue-50 text-blue-900'>
<div className='font-medium'>No pudimos obtener los datos</div>
<p className='mt-1 text-sm'>{error?.message}</p>
<p className='mt-1 text-xs opacity-80'>{note}</p>
<div className='mt-3 flex gap-2'>
<div className="rounded-md border p-4 bg-blue-50 text-blue-900" role="alert">
<div className="font-medium">No pudimos obtener los datos</div>
<p className="mt-1 text-sm">{error?.message}</p>
<p className="mt-1 text-xs opacity-80">{note}</p>
<div className="mt-3 flex gap-2">
<Button
className="px-3 py-1.5 rounded-md bg-blue-700 text-white"
onClick={resetErrorBoundary}
className='px-3 py-1.5 rounded-md bg-blue-700 text-white'
>
Reintentar
</Button>
<Button onClick={() => window.location.reload()} className='px-3 py-1.5 rounded-md border'>
<Button className="px-3 py-1.5 rounded-md border" onClick={() => window.location.reload()}>
Recargar
</Button>
</div>
@ -160,23 +160,23 @@ export function NetworkFallback({ error, resetErrorBoundary }: FallbackProps) {
export function DevDetailsFallback({ error, resetErrorBoundary }: FallbackProps) {
const [open, setOpen] = React.useState(false);
return (
<div role='alert' className='rounded-md border p-4 bg-gray-50'>
<div className='flex items-center justify-between'>
<p className='font-medium text-gray-900'>Algo falló</p>
<div className='flex gap-2'>
<div className="rounded-md border p-4 bg-gray-50" role="alert">
<div className="flex items-center justify-between">
<p className="font-medium text-gray-900">Algo falló</p>
<div className="flex gap-2">
<Button
className="px-3 py-1.5 rounded-md bg-gray-900 text-white"
onClick={resetErrorBoundary}
className='px-3 py-1.5 rounded-md bg-gray-900 text-white'
>
Reintentar
</Button>
<Button onClick={() => setOpen((v) => !v)} className='px-3 py-1.5 rounded-md border'>
<Button className="px-3 py-1.5 rounded-md border" onClick={() => setOpen((v) => !v)}>
{open ? "Ocultar detalles" : "Ver detalles"}
</Button>
</div>
</div>
{open && (
<pre className='mt-3 text-xs whitespace-pre-wrap bg-white p-3 rounded-md border overflow-auto'>
<pre className="mt-3 text-xs whitespace-pre-wrap bg-white p-3 rounded-md border overflow-auto">
{error?.stack || error?.message}
</pre>
)}
@ -189,22 +189,22 @@ export function DevDetailsFallback({ error, resetErrorBoundary }: FallbackProps)
*/
export function SupportFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div role='alert' className='rounded-md border p-4 bg-white'>
<h4 className='font-semibold text-gray-900'>No pudimos completar la acción</h4>
<p className='mt-1 text-gray-700'>{error?.message}</p>
<div className='mt-3 flex flex-wrap gap-2'>
<div className="rounded-md border p-4 bg-white" role="alert">
<h4 className="font-semibold text-gray-900">No pudimos completar la acción</h4>
<p className="mt-1 text-gray-700">{error?.message}</p>
<div className="mt-3 flex flex-wrap gap-2">
<Button
className="px-3 py-1.5 rounded-md bg-gray-900 text-white"
onClick={resetErrorBoundary}
className='px-3 py-1.5 rounded-md bg-gray-900 text-white'
>
Intentar de nuevo
</Button>
<a href='/ayuda' className='px-3 py-1.5 rounded-md border'>
<a className="px-3 py-1.5 rounded-md border" href="/ayuda">
Ir a Ayuda
</a>
<a
href='mailto:soporte@tuapp.com?subject=Error%20en%20la%20aplicaci%C3%B3n'
className='px-3 py-1.5 rounded-md border'
className="px-3 py-1.5 rounded-md border"
href="mailto:soporte@tuapp.com?subject=Error%20en%20la%20aplicaci%C3%B3n"
>
Contactar soporte
</a>

View File

@ -1,6 +1,6 @@
import { IModuleClient, ModuleClientParams } from "@erp/core/client";
import { JSX } from "react";
import { RouteObject, useRoutes } from "react-router-dom";
import type { IModuleClient, ModuleClientParams } from "@erp/core/client";
import type { JSX } from "react";
import { type RouteObject, useRoutes } from "react-router-dom";
interface ModuleRoutesProps {
modules: IModuleClient[];

View File

@ -1,4 +1,4 @@
import { AxiosInstance } from "axios";
import type { AxiosInstance } from "axios";
/**
* Datos requeridos para iniciar sesión.

View File

@ -5,9 +5,14 @@ export default [
{
files: ["**/*.ts", "**/*.tsx"],
ignores: [
"**/docs/**",
"**/dist/**",
"**/out/**",
"**/.turbo/**",
"**/node_modules/**"
"**/.vscode/**",
"**/node_modules/**",
"**/scripts/**",
"**/tools/**"
],
languageOptions: {
parser,

View File

@ -1,6 +1,6 @@
{
"name": "@erp/auth",
"version": "0.6.3",
"version": "0.6.4",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,6 +1,6 @@
{
"name": "@erp/core",
"version": "0.6.3",
"version": "0.6.4",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,13 +1,21 @@
import { type JsonTaxCatalogProvider, SpainTaxCatalogProvider } from "../../../common";
import {
FactuGESPaymentCatalogProvider,
type JsonPaymentCatalogProvider,
type JsonTaxCatalogProvider,
SpainTaxCatalogProvider,
} from "../../../common";
export interface ICatalogs {
taxCatalog: JsonTaxCatalogProvider;
paymentCatalog: JsonPaymentCatalogProvider;
}
export const buildCatalogs = (): ICatalogs => {
const taxCatalog = SpainTaxCatalogProvider();
const paymentCatalog = FactuGESPaymentCatalogProvider();
return {
taxCatalog,
paymentCatalog,
};
};

View File

@ -1 +1,2 @@
export * from "./payments";
export * from "./taxes";

View File

@ -0,0 +1,23 @@
[
{
"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

@ -0,0 +1,5 @@
import factugesPaymentCatalog from "./factuges-payment-catalog.json";
import { JsonPaymentCatalogProvider } from "./json-payment-catalog.provider";
export const FactuGESPaymentCatalogProvider = () =>
new JsonPaymentCatalogProvider(factugesPaymentCatalog);

View File

@ -0,0 +1,4 @@
export * from "./factuges-payment-catalog.provider";
export * from "./json-payment-catalog.provider";
export * from "./payment-catalog.provider";
export * from "./payment-catalog-types";

View File

@ -0,0 +1,64 @@
// --- Adaptador que carga el catálogo JSON en memoria e indexa por code ---
import { Maybe } from "@repo/rdx-utils";
import type { PaymentCatalogProvider } from "./payment-catalog.provider";
import type {
PaymentCatalogType,
PaymentItemType,
PaymentLookupItems,
} from "./payment-catalog-types";
export class JsonPaymentCatalogProvider implements PaymentCatalogProvider {
// Índice por código normalizado
private readonly catalog: Map<string, PaymentItemType>;
/**
* @param catalog Catálogo ya parseado (p.ej. import JSON o fetch)
*/
constructor(catalog: PaymentCatalogType) {
this.catalog = new Map<string, PaymentItemType>();
// Normalizamos códigos a minúsculas y sin espacios
for (const item of catalog) {
const key = item.factuges_id;
// En caso de duplicados, el último gana (o lanza error si prefieres)
this.catalog.set(key, item);
}
}
static normalizeCode(code: string): string {
return (code ?? "").trim().toLowerCase();
}
findByFactuGESId(factuges_id: string): Maybe<PaymentItemType> {
const found = this.catalog.get(factuges_id);
return found ? Maybe.some(found) : Maybe.none<PaymentItemType>();
}
findById(id: string): Maybe<PaymentItemType> {
for (const value of this.catalog.values()) {
if (value.id === id) {
return Maybe.some(value);
}
}
return Maybe.none<PaymentItemType>();
}
getAll(): PaymentItemType[] {
return Array.from(this.catalog.values());
}
/** Devuelve un objeto indexado por código, compatible con PaymentMultiSelectField */
toOptionLookup(): PaymentLookupItems {
return this.getAll().map((item) => ({
label: item.description,
value: item.id,
group: item.group,
}));
}
/** Devuelve la lista única de grupos disponibles */
groups(): string[] {
return Array.from(new Set(Array.from(this.catalog.values()).map((i) => i.group)));
}
}

View File

@ -0,0 +1,17 @@
// --- DTOs del catálogo (comparten contrato entre frontend/backend) ---
export type PaymentItemType = {
id: string;
company_id: string;
factuges_id: string;
description: string;
group: string;
};
export type PaymentCatalogType = PaymentItemType[];
export type PaymentLookupItems = {
label: string;
value: string;
group: string;
}[];

View File

@ -0,0 +1,19 @@
import type { Maybe } from "@repo/rdx-utils"; // Usa tu implementación real de Maybe
import type {
PaymentCatalogType,
PaymentItemType,
PaymentLookupItems,
} from "./payment-catalog-types";
export interface PaymentCatalogProvider {
findByFactuGESId(factuges_id: string): Maybe<PaymentItemType>;
findById(id: string): Maybe<PaymentItemType>;
// devuelve el catálogo completo como array
getAll(): PaymentCatalogType;
toOptionLookup(): PaymentLookupItems;
groups(): string[]; //Devuelve una lista con los grupos
}

View File

@ -1,6 +1,6 @@
{
"name": "@erp/customer-invoices",
"version": "0.6.3",
"version": "0.6.4",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -401,6 +401,7 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
this.props.status = InvoiceStatus.issued();
return Result.ok();
}
// Cálculos
/**

View File

@ -258,6 +258,8 @@ export class ProformaItem extends DomainEntity<InternalProformaItemProps> implem
// Calcular impuestos individuales a partir de la base imponible
const { ivaAmount, recAmount, retentionAmount } = this.taxes.totals(taxableAmount);
// El importe de la retención ya va en negativo (-1) y
// no hace falta indicarlo como resta.
const taxesAmount = ivaAmount.add(recAmount).add(retentionAmount);
const totalAmount = taxableAmount.add(taxesAmount);

View File

@ -18,6 +18,8 @@ type TaxGroupState = {
retentionCode: Maybe<string>;
retentionPercentage: Maybe<TaxPercentage>;
retentionAmount: ItemAmount;
taxesAmount: ItemAmount;
};
/**
@ -56,6 +58,8 @@ export function proformaComputeTaxGroups(items: IProformaItems): Map<string, Tax
? Maybe.some(retention.unwrap().percentage)
: Maybe.none(),
retentionAmount: ItemAmount.zero(currency.code),
taxesAmount: ItemAmount.zero(currency.code),
});
}
@ -67,6 +71,7 @@ export function proformaComputeTaxGroups(items: IProformaItems): Map<string, Tax
g.ivaAmount = g.ivaAmount.add(itemTotals.ivaAmount);
g.recAmount = g.recAmount.add(itemTotals.recAmount);
g.retentionAmount = g.retentionAmount.add(itemTotals.retentionAmount);
g.taxesAmount = g.taxesAmount.add(itemTotals.taxesAmount);
}
return map;
}

View File

@ -19,8 +19,8 @@ export interface IProformaItemsTotals {
}
/**
* Calcula los totales (scale 4) a partir de las líneas valoradas.
* La lógica fiscal está en ProformaItem; aquí solo se agregan resultados.
* Acumula los totales (scale 4) a partir de los totales de las líneas valoradas.
* Aquí no se hace ningúna operación de cálculo.
*/
export class ProformaItemsTotalsCalculator {
constructor(private readonly items: ProformaItems) {}

View File

@ -30,16 +30,14 @@ export class ProformaTaxesCalculator {
public calculate(): Collection<IProformaTaxTotals> {
const groups = proformaComputeTaxGroups(this.items); // <- devuelve en escala 4
//const currencyCode = this.items.currencyCode;
// Vamos acumulando los importes, redondeando previamente a 2 decimales
const rows = Array.from(groups.values()).map((g) => {
const taxableAmount = this.toInvoiceAmount(g.taxableAmount);
const ivaAmount = this.toInvoiceAmount(g.ivaAmount);
const recAmount = this.toInvoiceAmount(g.recAmount);
const retentionAmount = this.toInvoiceAmount(g.retentionAmount);
//const taxesAmount = this.toInvoiceAmount(g.taxesAmount);
const taxesAmount = ivaAmount.add(recAmount).subtract(retentionAmount);
const taxesAmount = this.toInvoiceAmount(g.taxesAmount);
return {
taxableAmount,

View File

@ -1,3 +1,3 @@
export * from "./common/persistence";
export * from "./common";
export * from "./issued-invoices";
export * from "./proformas";

View File

@ -13,16 +13,17 @@ export interface IProformaPersistenceMappers {
export const buildProformaPersistenceMappers = (
catalogs: ICatalogs
): IProformaPersistenceMappers => {
const { taxCatalog } = catalogs;
const { taxCatalog, paymentCatalog } = catalogs;
// Mappers para el repositorio
const domainMapper = new SequelizeProformaDomainMapper({
taxCatalog,
paymentCatalog,
});
const listMapper = new SequelizeProformaSummaryMapper();
// Mappers el DTO a las props validadas (CustomerProps) y luego construir agregado
const createMapper = new CreateProformaInputMapper({ taxCatalog });
const createMapper = new CreateProformaInputMapper();
return {
domainMapper,

View File

@ -1,3 +1,4 @@
import type { JsonPaymentCatalogProvider } from "@erp/core";
import { DiscountPercentage, type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
import {
CurrencyCode,
@ -11,11 +12,10 @@ import {
maybeFromNullableResult,
maybeToNullable,
} from "@repo/rdx-ddd";
import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils";
import { Result } from "@repo/rdx-utils";
import {
InvoiceNumber,
InvoicePaymentMethod,
InvoiceSerie,
InvoiceStatus,
Proforma,
@ -40,9 +40,21 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
private _recipientMapper: SequelizeProformaRecipientDomainMapper;
private _taxesMapper: SequelizeProformaTaxesDomainMapper;
private _paymentCatalog: JsonPaymentCatalogProvider;
constructor(params: MapperParamsType) {
super();
const { paymentCatalog } = params as {
paymentCatalog: JsonPaymentCatalogProvider;
};
this._paymentCatalog = paymentCatalog;
if (!this._paymentCatalog) {
throw new Error('paymentCatalog not defined ("SequelizeProformaDomainMapper")');
}
this._itemsMapper = new SequelizeProformaItemDomainMapper(params);
this._recipientMapper = new SequelizeProformaRecipientDomainMapper();
this._taxesMapper = new SequelizeProformaTaxesDomainMapper(params);
@ -118,7 +130,7 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
);
// Método de pago (VO opcional con id + descripción)
let paymentMethod = Maybe.none<InvoicePaymentMethod>();
/*let paymentMethod = Maybe.none<InvoicePaymentMethod>();
if (!isNullishOrEmpty(raw.payment_method_id)) {
const paymentId = extractOrPushError(
@ -127,19 +139,34 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
errors
);
const paymentVO = extractOrPushError(
InvoicePaymentMethod.create(
{ paymentDescription: String(raw.payment_method_description ?? "") },
paymentId ?? undefined
),
"payment_method_description",
errors
);
if (paymentId) {
const paymentOrNot = this._paymentCatalog.findById(paymentId.toString());
if (paymentVO) {
paymentMethod = Maybe.some(paymentVO);
if (paymentOrNot.isSome()) {
const paymentCatalogItem = paymentOrNot.unwrap();
const paymentVO = extractOrPushError(
InvoicePaymentMethod.create(
{ paymentDescription: paymentCatalogItem.description ?? "" },
paymentId
),
"paymentMethod",
errors
);
if (paymentVO) {
paymentMethod = Maybe.some(paymentVO);
}
}
}
}
}*/
// Método de pago (ID)
const paymentMethodId = extractOrPushError(
maybeFromNullableResult(raw.payment_method_id, (value) => UniqueID.create(String(value))),
"payment_method_id",
errors
);
// % descuento global (VO)
const globalDiscountPercentage = extractOrPushError(
@ -170,7 +197,7 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
notes,
languageCode,
currencyCode,
paymentMethod,
paymentMethodId,
globalDiscountPercentage,
linkedInvoiceId,
@ -241,7 +268,7 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
globalDiscountPercentage: attributes.globalDiscountPercentage!,
paymentMethod: attributes.paymentMethod!,
paymentMethodId: attributes.paymentMethodId!,
linkedInvoiceId: attributes.linkedInvoiceId!, // El id de la factura emitida (linked_invoice) se asigna al hacer issue() desde la proforma, no viene en el modelo de persistencia.
};
@ -296,7 +323,27 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
...params,
});
// 4) Si hubo errores de mapeo, devolvemos colección de validación
// 4) Payment
let payment: {
id: string | null;
description: string | null;
} = { id: null, description: null };
if (source.hasPaymentMethod) {
const paymentId = source.paymentMethodId.unwrap();
const paymentOrNot = this._paymentCatalog.findById(paymentId.toString());
if (paymentOrNot.isSome()) {
const paymentItem = paymentOrNot.unwrap();
payment = {
id: paymentItem.id ?? null,
description: paymentItem.description ?? null,
};
}
}
// 5) Si hubo errores de mapeo, devolvemos colección de validación
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("Customer invoice mapping to persistence failed", errors)
@ -329,14 +376,8 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
description: maybeToNullable(source.description, (description) => description),
notes: maybeToNullable(source.notes, (v) => v.toPrimitive()),
payment_method_id: maybeToNullable(
source.paymentMethod,
(payment) => payment.toObjectString().id
),
payment_method_description: maybeToNullable(
source.paymentMethod,
(payment) => payment.toObjectString().payment_description
),
payment_method_id: payment.id,
payment_method_description: payment.description,
subtotal_amount_value: allAmounts.subtotalAmount.value,
subtotal_amount_scale: allAmounts.subtotalAmount.scale,

View File

@ -1,7 +1,7 @@
{
"name": "@erp/customers",
"description": "Customers",
"version": "0.6.3",
"version": "0.6.4",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,6 +1,6 @@
{
"name": "@erp/factuges",
"version": "0.6.3",
"version": "0.6.4",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,15 +1,22 @@
import type { ICatalogs } from "@erp/core/api";
import { CreateProformaFromFactugesInputMapper, ICreateProformaFromFactugesInputMapper } from '../mappers';
import {
CreateProformaFromFactugesInputMapper,
type ICreateProformaFromFactugesInputMapper,
} from "../mappers";
export interface IFactugesInputMappers {
createInputMapper: ICreateProformaFromFactugesInputMapper;
}
export const buildFactugesInputMappers = (catalogs: ICatalogs): IFactugesInputMappers => {
const { taxCatalog } = catalogs;
const { taxCatalog, paymentCatalog } = catalogs;
// Mappers el DTO a las props validadas (FactugesProps) y luego construir agregado
const createInputMapper = new CreateProformaFromFactugesInputMapper({ taxCatalog });
const createInputMapper = new CreateProformaFromFactugesInputMapper({
taxCatalog,
paymentCatalog,
});
return {
createInputMapper,

View File

@ -1,4 +1,4 @@
import type { JsonTaxCatalogProvider } from "@erp/core";
import type { JsonPaymentCatalogProvider, JsonTaxCatalogProvider } from "@erp/core";
import { DiscountPercentage, Tax } from "@erp/core/api";
import {
InvoiceAmount,
@ -103,6 +103,7 @@ export type ProformaDraft = {
};
export type ProformaPaymentDraft = {
payment_id: string;
factuges_id: string;
description: string;
};
@ -126,9 +127,14 @@ export class CreateProformaFromFactugesInputMapper
implements ICreateProformaFromFactugesInputMapper
{
private readonly taxCatalog: JsonTaxCatalogProvider;
private readonly paymentCatalog: JsonPaymentCatalogProvider;
constructor(params: { taxCatalog: JsonTaxCatalogProvider }) {
constructor(params: {
taxCatalog: JsonTaxCatalogProvider;
paymentCatalog: JsonPaymentCatalogProvider;
}) {
this.taxCatalog = params.taxCatalog;
this.paymentCatalog = params.paymentCatalog;
}
public map(
@ -190,9 +196,20 @@ export class CreateProformaFromFactugesInputMapper
const errors: ValidationErrorDetail[] = [];
const { companyId } = params;
const factuges_id = String(dto.payment_method_id);
const paymentOrNot = this.paymentCatalog.findByFactuGESId(factuges_id);
if (paymentOrNot.isNone()) {
errors.push({
path: "payment_method_id",
message: "Forma de pago no encontrada",
});
}
return {
factuges_id: String(dto.payment_method_id),
description: String(dto.payment_method_description),
payment_id: paymentOrNot.unwrap().id,
factuges_id: paymentOrNot.unwrap().factuges_id,
description: paymentOrNot.unwrap().description,
};
}

View File

@ -340,6 +340,15 @@ export class CreateProformaFromFactugesUseCase {
InvoicePaymentMethod.create({ paymentDescription: payment.description }, payment.id).data
);
console.log({
...proformaDraft,
companyId,
customerId,
status: defaultStatus,
paymentMethod,
recipient,
});
return Result.ok({
...proformaDraft,
companyId,

View File

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

View File

@ -1,6 +1,6 @@
import { type SetupParams, buildCatalogs, buildTransactionManager } from "@erp/core/api";
import type { ProformaPublicServices } from "@erp/customer-invoices/api";
import type { CustomerPublicServices } from "@erp/customers/api";
import type { IProformaPublicServices } from "@erp/customer-invoices/api";
import type { ICustomerPublicServices } from "@erp/customers/api";
import {
buildCreateProformaFromFactugesUseCase,
@ -11,8 +11,8 @@ import type { CreateProformaFromFactugesUseCase } from "../../application/use-ca
export type FactugesInternalDeps = {
useCases: {
createProforma: (publicServices: {
customerServices: CustomerPublicServices;
proformaServices: ProformaPublicServices;
customerServices: ICustomerPublicServices;
proformaServices: IProformaPublicServices;
}) => CreateProformaFromFactugesUseCase;
};
};
@ -31,8 +31,8 @@ export function buildFactugesDependencies(params: SetupParams): FactugesInternal
return {
useCases: {
createProforma: (publicServices: {
customerServices: CustomerPublicServices;
proformaServices: ProformaPublicServices;
customerServices: ICustomerPublicServices;
proformaServices: IProformaPublicServices;
}) =>
buildCreateProformaFromFactugesUseCase({
dtoMapper: inputMappers.createInputMapper,

View File

@ -1,7 +1,7 @@
{
"name": "@erp/supplier-invoices",
"description": "Supplier invoices",
"version": "0.6.3",
"version": "0.6.4",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,7 +1,7 @@
{
"name": "@erp/suppliers",
"description": "Suppliers",
"version": "0.6.3",
"version": "0.6.4",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,13 +1,14 @@
{
"name": "uecko-erp-2025",
"private": true,
"version": "0.6.3",
"version": "0.6.4",
"workspaces": [
"apps/*",
"modules/*",
"packages/*"
],
"scripts": {
"lint": "turbo run lint",
"build": "turbo build",
"build:templates": "bash scripts/build-templates.sh",
"build:api": "bash scripts/build-api.sh rodax --api",

View File

@ -1,6 +1,6 @@
{
"name": "@repo/rdx-criteria",
"version": "0.6.3",
"version": "0.6.4",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,6 +1,6 @@
{
"name": "@repo/rdx-ddd",
"version": "0.6.3",
"version": "0.6.4",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,6 +1,6 @@
{
"name": "@repo/rdx-logger",
"version": "0.6.3",
"version": "0.6.4",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,6 +1,6 @@
{
"name": "@repo/rdx-ui",
"version": "0.6.3",
"version": "0.6.4",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,6 +1,6 @@
{
"name": "@repo/rdx-utils",
"version": "0.6.3",
"version": "0.6.4",
"private": true,
"type": "module",
"sideEffects": false,

View File

@ -1,6 +1,6 @@
"use client"
import * as React from "react"
import type * as React from "react"
import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog"
import { cn } from "@repo/shadcn-ui/lib/utils"

View File

@ -1,4 +1,4 @@
import * as React from "react"
import type * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@repo/shadcn-ui/lib/utils"

View File

@ -1,4 +1,4 @@
import * as React from "react"
import type * as React from "react"
import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar"
import { cn } from "@repo/shadcn-ui/lib/utils"

View File

@ -1,4 +1,4 @@
import * as React from "react"
import type * as React from "react"
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"

View File

@ -1,4 +1,4 @@
import * as React from "react"
import type * as React from "react"
import { cn } from "@repo/shadcn-ui/lib/utils"

View File

@ -89,7 +89,7 @@ function Carousel({
)
React.useEffect(() => {
if (!api || !setApi) return
if (!(api && setApi)) return
setApi(api)
}, [api, setApi])

View File

@ -180,7 +180,7 @@ function ChartTooltipContent({
labelKey,
])
if (!active || !payload?.length) {
if (!(active && payload?.length)) {
return null
}
@ -193,7 +193,7 @@ function ChartTooltipContent({
className
)}
>
{!nestLabel ? tooltipLabel : null}
{nestLabel ? null : tooltipLabel}
<div className="grid gap-1.5">
{payload
.filter((item) => item.type !== "none")

View File

@ -1,6 +1,6 @@
"use client"
import * as React from "react"
import type * as React from "react"
import { Command as CommandPrimitive } from "cmdk"
import { cn } from "@repo/shadcn-ui/lib/utils"

View File

@ -1,6 +1,6 @@
"use client"
import * as React from "react"
import type * as React from "react"
import { ContextMenu as ContextMenuPrimitive } from "@base-ui/react/context-menu"
import { cn } from "@repo/shadcn-ui/lib/utils"

View File

@ -1,4 +1,4 @@
import * as React from "react"
import type * as React from "react"
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
import { cn } from "@repo/shadcn-ui/lib/utils"

View File

@ -1,6 +1,6 @@
"use client"
import * as React from "react"
import type * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
import { cn } from "@repo/shadcn-ui/lib/utils"

View File

@ -1,4 +1,4 @@
import * as React from "react"
import type * as React from "react"
import { Menu as MenuPrimitive } from "@base-ui/react/menu"
import { cn } from "@repo/shadcn-ui/lib/utils"

View File

@ -1,6 +1,6 @@
"use client"
import * as React from "react"
import type * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@repo/shadcn-ui/lib/utils"

View File

@ -1,4 +1,4 @@
import * as React from "react"
import type * as React from "react"
import { Input as InputPrimitive } from "@base-ui/react/input"
import { cn } from "@repo/shadcn-ui/lib/utils"

View File

@ -1,4 +1,4 @@
import * as React from "react"
import type * as React from "react"
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"

View File

@ -1,6 +1,6 @@
"use client"
import * as React from "react"
import type * as React from "react"
import { cn } from "@repo/shadcn-ui/lib/utils"

View File

@ -1,6 +1,6 @@
"use client"
import * as React from "react"
import type * as React from "react"
import { Menu as MenuPrimitive } from "@base-ui/react/menu"
import { Menubar as MenubarPrimitive } from "@base-ui/react/menubar"

View File

@ -1,4 +1,4 @@
import * as React from "react"
import type * as React from "react"
import { cn } from "@repo/shadcn-ui/lib/utils"
import { ChevronDownIcon } from "lucide-react"

View File

@ -1,4 +1,4 @@
import * as React from "react"
import type * as React from "react"
import { cn } from "@repo/shadcn-ui/lib/utils"
import { Button } from "@repo/shadcn-ui/components/button"

View File

@ -1,4 +1,4 @@
import * as React from "react"
import type * as React from "react"
import { Popover as PopoverPrimitive } from "@base-ui/react/popover"
import { cn } from "@repo/shadcn-ui/lib/utils"

View File

@ -1,6 +1,6 @@
"use client"
import * as React from "react"
import type * as React from "react"
import { Select as SelectPrimitive } from "@base-ui/react/select"
import { cn } from "@repo/shadcn-ui/lib/utils"

View File

@ -1,6 +1,6 @@
"use client"
import * as React from "react"
import type * as React from "react"
import { Dialog as SheetPrimitive } from "@base-ui/react/dialog"
import { cn } from "@repo/shadcn-ui/lib/utils"

View File

@ -518,7 +518,7 @@ function SidebarMenuButton({
},
props
),
render: !tooltip ? render : <TooltipTrigger render={render} />,
render: tooltip ? <TooltipTrigger render={render} /> : render,
state: {
slot: "sidebar-menu-button",
sidebar: "menu-button",

View File

@ -1,6 +1,6 @@
"use client"
import * as React from "react"
import type * as React from "react"
import { cn } from "@repo/shadcn-ui/lib/utils"

View File

@ -1,4 +1,4 @@
import * as React from "react"
import type * as React from "react"
import { cn } from "@repo/shadcn-ui/lib/utils"

View File

@ -1,7 +1,7 @@
import * as React from "react"
import { Toggle as TogglePrimitive } from "@base-ui/react/toggle"
import { ToggleGroup as ToggleGroupPrimitive } from "@base-ui/react/toggle-group"
import { type VariantProps } from "class-variance-authority"
import type { VariantProps } from "class-variance-authority"
import { cn } from "@repo/shadcn-ui/lib/utils"
import { toggleVariants } from "@repo/shadcn-ui/components/toggle"