v0.6.0 - Subida a producción

This commit is contained in:
David Arranz 2026-04-21 15:51:57 +02:00
parent 67dc91eced
commit 330240deaa
56 changed files with 320 additions and 239 deletions

2
.vscode/launch.json vendored
View File

@ -1,5 +1,5 @@
{ {
"version": "0.5.0", "version": "0.6.0",
"configurations": [ "configurations": [
{ {
"name": "WEB: Vite (Chrome)", "name": "WEB: Vite (Chrome)",

View File

@ -1,6 +1,6 @@
{ {
"name": "@erp/factuges-server", "name": "@erp/factuges-server",
"version": "0.5.0", "version": "0.6.0",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "tsup src/index.ts --config tsup.config.ts", "build": "tsup src/index.ts --config tsup.config.ts",
@ -37,8 +37,6 @@
"@erp/core": "workspace:*", "@erp/core": "workspace:*",
"@erp/customer-invoices": "workspace:*", "@erp/customer-invoices": "workspace:*",
"@erp/customers": "workspace:*", "@erp/customers": "workspace:*",
"@erp/factuges": "workspace:*",
"@erp/suppliers": "workspace:*",
"@repo/rdx-logger": "workspace:*", "@repo/rdx-logger": "workspace:*",
"@repo/rdx-utils": "workspace:*", "@repo/rdx-utils": "workspace:*",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",

View File

@ -1,7 +1,7 @@
import customerInvoicesAPIModule from "@erp/customer-invoices/api"; import customerInvoicesAPIModule from "@erp/customer-invoices/api";
import customersAPIModule from "@erp/customers/api"; import customersAPIModule from "@erp/customers/api";
import factuGESAPIModule from "@erp/factuges/api";
import suppliersAPIModule from "@erp/suppliers/api"; //import suppliersAPIModule from "@erp/suppliers/api";
import { registerModule } from "./lib"; import { registerModule } from "./lib";
@ -9,6 +9,5 @@ export const registerModules = () => {
//registerModule(authAPIModule); //registerModule(authAPIModule);
registerModule(customersAPIModule); registerModule(customersAPIModule);
registerModule(customerInvoicesAPIModule); registerModule(customerInvoicesAPIModule);
registerModule(factuGESAPIModule); //registerModule(suppliersAPIModule);
registerModule(suppliersAPIModule);
}; };

View File

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

View File

@ -1,6 +1,5 @@
{ {
"compilerOptions": { "compilerOptions": {
"baseUrl": ".",
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
}, },

View File

@ -4,7 +4,6 @@
"compilerOptions": { "compilerOptions": {
"resolveJsonModule": true, "resolveJsonModule": true,
"esModuleInterop": true, "esModuleInterop": true,
"baseUrl": ".",
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
} }

View File

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

View File

@ -1,7 +1,6 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"baseUrl": ".",
"paths": { "paths": {
"@erp/auth/*": ["./src/*"] "@erp/auth/*": ["./src/*"]
}, },

View File

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

View File

@ -1,3 +0,0 @@
export const formatDate = (value: string) => {
return new Date(value).toLocaleDateString();
};

View File

@ -1,3 +1,2 @@
export * from "./date-func";
export * from "./form-utils"; export * from "./form-utils";
export * from "./http-url-utils"; export * from "./http-url-utils";

View File

@ -1,7 +1,6 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"baseUrl": ".",
"paths": { "paths": {
"@erp/core/*": ["./src/*"] "@erp/core/*": ["./src/*"]
}, },

View File

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

View File

@ -1,5 +1,5 @@
import type { UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import type { Maybe, Result } from "@repo/rdx-utils"; import type { Result } from "@repo/rdx-utils";
import type { InvoiceNumber, InvoiceSerie } from "../../../domain"; import type { InvoiceNumber, InvoiceSerie } from "../../../domain";
@ -13,7 +13,7 @@ export interface IIssuedInvoiceNumberGenerator {
*/ */
getNextForCompany( getNextForCompany(
companyId: UniqueID, companyId: UniqueID,
series: Maybe<InvoiceSerie>, series: InvoiceSerie,
transaction: unknown transaction: unknown
): Promise<Result<InvoiceNumber, Error>>; ): Promise<Result<InvoiceNumber, Error>>;
} }

View File

@ -98,15 +98,15 @@ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoic
companyId: proforma.companyId, companyId: proforma.companyId,
status: InvoiceStatus.issued(), status: InvoiceStatus.issued(),
series: proforma.series, series: proforma.series.getOrUndefined()!,
proformaId: proforma.id, linkedProformaId: proforma.id,
invoiceNumber: proforma.invoiceNumber, invoiceNumber: proforma.invoiceNumber,
invoiceDate: UtcDate.today(), // La fecha de la factura es la fecha de emisión, no la de la proforma invoiceDate: UtcDate.today(), // La fecha de la factura es la fecha de emisión, no la de la proforma
operationDate: proforma.operationDate, operationDate: proforma.operationDate,
description: proforma.description, description: proforma.description.getOrUndefined()!,
languageCode: proforma.languageCode, languageCode: proforma.languageCode,
currencyCode: proforma.currencyCode, currencyCode: proforma.currencyCode,
@ -116,7 +116,7 @@ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoic
paymentMethod: proforma.paymentMethod, paymentMethod: proforma.paymentMethod,
customerId: proforma.customerId, customerId: proforma.customerId,
recipient: proforma.recipient, recipient: proforma.recipient.getOrUndefined()!,
items: issuedItems, items: issuedItems,

View File

@ -53,9 +53,7 @@ export class IssuedInvoiceFullSnapshotBuilder implements IIssuedInvoiceFullSnaps
customer_id: invoice.customerId.toString(), customer_id: invoice.customerId.toString(),
recipient, recipient,
linked_proforma_id: maybeToNullable(invoice.linkedProformaId, (linkedId) => linked_proforma_id: invoice.linkedProformaId.toString(),
linkedId.toString()
),
taxes, taxes,

View File

@ -1,2 +1 @@
export * from "./proforma-summary-snapshot.interface";
export * from "./proforma-summary-snapshot-builder"; export * from "./proforma-summary-snapshot-builder";

View File

@ -1,15 +1,14 @@
import type { ISnapshotBuilder } from "@erp/core/api"; import type { ISnapshotBuilder } from "@erp/core/api";
import { maybeToEmptyString, maybeToNullable } from "@repo/rdx-ddd"; import { maybeToEmptyString, maybeToNullable } from "@repo/rdx-ddd";
import type { ProformaSummaryDTO } from "../../../../../common";
import type { ProformaSummary } from "../../models"; import type { ProformaSummary } from "../../models";
import type { IProformaSummarySnapshot } from "./proforma-summary-snapshot.interface";
export interface IProformaSummarySnapshotBuilder export interface IProformaSummarySnapshotBuilder
extends ISnapshotBuilder<ProformaSummary, IProformaSummarySnapshot> {} extends ISnapshotBuilder<ProformaSummary, ProformaSummaryDTO> {}
export class ProformaSummarySnapshotBuilder implements IProformaSummarySnapshotBuilder { export class ProformaSummarySnapshotBuilder implements IProformaSummarySnapshotBuilder {
toOutput(proforma: ProformaSummary): IProformaSummarySnapshot { toOutput(proforma: ProformaSummary): ProformaSummaryDTO {
const recipient = proforma.recipient.toObjectString(); const recipient = proforma.recipient.toObjectString();
return { return {
@ -18,7 +17,7 @@ export class ProformaSummarySnapshotBuilder implements IProformaSummarySnapshotB
is_proforma: proforma.isProforma, is_proforma: proforma.isProforma,
invoice_number: proforma.invoiceNumber.toString(), invoice_number: proforma.invoiceNumber.toString(),
status: proforma.status.toPrimitive(), status: proforma.status.toPrimitive() as ProformaSummaryDTO["status"],
series: maybeToNullable(proforma.series, (value) => value.toString()), series: maybeToNullable(proforma.series, (value) => value.toString()),
invoice_date: proforma.invoiceDate.toDateString(), invoice_date: proforma.invoiceDate.toDateString(),

View File

@ -1,42 +0,0 @@
/**
* Fijarse en ListProformasResponseDTO["items"]
*/
export interface IProformaSummarySnapshot {
id: string;
company_id: string;
is_proforma: boolean;
invoice_number: string;
status: string;
series: string | null;
invoice_date: string;
operation_date: string | null;
language_code: string;
currency_code: string;
reference: string | null;
description: string | null;
customer_id: string;
recipient: {
id: string;
tin: string;
name: string;
street: string | null;
street2: string | null;
city: string | null;
postal_code: string | null;
province: string | null;
country: string | null;
};
subtotal_amount: { value: string; scale: string; currency_code: string };
total_discount_amount: { value: string; scale: string; currency_code: string };
taxable_amount: { value: string; scale: string; currency_code: string };
taxes_amount: { value: string; scale: string; currency_code: string };
total_amount: { value: string; scale: string; currency_code: string };
linked_invoice_id: string | null;
}

View File

@ -32,7 +32,7 @@ export interface IIssuedInvoiceCreateProps {
companyId: UniqueID; companyId: UniqueID;
status: InvoiceStatus; status: InvoiceStatus;
linkedProformaId: Maybe<UniqueID>; // <- id de la proforma padre en caso de issue linkedProformaId: UniqueID; // <- id de la proforma padre en caso de issue
series: InvoiceSerie; series: InvoiceSerie;
invoiceNumber: InvoiceNumber; invoiceNumber: InvoiceNumber;
@ -178,7 +178,7 @@ export class IssuedInvoice
return this.props.customerId; return this.props.customerId;
} }
public get linkedProformaId(): Maybe<UniqueID> { public get linkedProformaId(): UniqueID {
return this.props.linkedProformaId; return this.props.linkedProformaId;
} }

View File

@ -304,6 +304,97 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
); );
} }
if (this.series.isNone()) {
return Result.fail(
new DomainValidationError(
"MISSING_SERIES",
"series",
"Series is required to issue the proforma"
)
);
}
if (this.description.isNone()) {
return Result.fail(
new DomainValidationError(
"MISSING_DESCRIPTION",
"description",
"Description is required to issue the proforma"
)
);
}
if (this.recipient.isNone()) {
return Result.fail(
new DomainValidationError(
"MISSING_RECIPIENT",
"recipient",
"Recipient is required to issue the proforma"
)
);
}
if (this.items.size() === 0) {
return Result.fail(
new DomainValidationError(
"NO_ITEMS",
"items",
"At least one item is required to issue the proforma"
)
);
}
const invalidItem = this.items.find((item) => !item.isValued());
if (invalidItem) {
return Result.fail(
new DomainValidationError(
"INVALID_ITEM",
"items",
`Item at position ${invalidItem.id} is not valid for invoicing`
)
);
}
if (this.paymentMethod.isNone()) {
return Result.fail(
new DomainValidationError(
"MISSING_PAYMENT_METHOD",
"paymentMethod",
"Payment method is required to issue the proforma"
)
);
}
/*if (this.operationDate.isSome() && this.operationDate.unwrap() > new Date()) {
return Result.fail(
new DomainValidationError(
"INVALID_OPERATION_DATE",
"operationDate",
"Operation date cannot be in the future"
)
);
}
if (this.operationDate.isSome() && this.operationDate.unwrap() < this.invoiceDate) {
return Result.fail(
new DomainValidationError(
"INVALID_OPERATION_DATE",
"operationDate",
"Operation date cannot be before invoice date"
)
);
}*/
if (this.linkedInvoiceId.isSome()) {
return Result.fail(
new DomainValidationError(
"LINKED_INVOICE_NOT_ALLOWED",
"linkedInvoiceId",
"Proforma cannot be linked to an invoice"
)
);
}
this.props.status = InvoiceStatus.issued(); this.props.status = InvoiceStatus.issued();
return Result.ok(); return Result.ok();
} }

View File

@ -64,7 +64,7 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
const customerId = extractOrPushError(UniqueID.create(raw.customer_id), "customer_id", errors); const customerId = extractOrPushError(UniqueID.create(raw.customer_id), "customer_id", errors);
const linkedProformaId = extractOrPushError( const linkedProformaId = extractOrPushError(
maybeFromNullableResult(raw.proforma_id, (v) => UniqueID.create(String(v))), UniqueID.create(String(raw.proforma_id)),
"proforma_id", "proforma_id",
errors errors
); );
@ -435,6 +435,7 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
} }
// 3) Cliente // 3) Cliente
console.debug(source.recipient);
const recipient = this._recipientMapper.mapToPersistence(source.recipient, { const recipient = this._recipientMapper.mapToPersistence(source.recipient, {
errors, errors,
parent: source, parent: source,
@ -450,6 +451,7 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
// 5) Si hubo errores de mapeo, devolvemos colección de validación // 5) Si hubo errores de mapeo, devolvemos colección de validación
if (errors.length > 0) { if (errors.length > 0) {
console.error("Errors mapping issued invoice to persistence:", errors);
return Result.fail( return Result.fail(
new ValidationErrorCollection("Customer invoice mapping to persistence failed", errors) new ValidationErrorCollection("Customer invoice mapping to persistence failed", errors)
); );
@ -467,7 +469,7 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
// Flags / estado / serie / número // Flags / estado / serie / número
is_proforma: false, is_proforma: false,
status: source.status.toPrimitive(), status: source.status.toPrimitive(),
proforma_id: maybeToNullable(source.linkedProformaId, (v) => v.toPrimitive()), proforma_id: source.linkedProformaId.toPrimitive(),
series: source.series.toPrimitive(), series: source.series.toPrimitive(),
invoice_number: source.invoiceNumber.toPrimitive(), invoice_number: source.invoiceNumber.toPrimitive(),

View File

@ -15,11 +15,7 @@ import {
} from "@repo/rdx-ddd"; } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { import { type IIssuedInvoiceCreateProps, InvoiceRecipient } from "../../../../../../domain";
type IIssuedInvoiceCreateProps,
InvoiceRecipient,
type IssuedInvoice,
} from "../../../../../../domain";
import type { CustomerInvoiceModel } from "../../../../../common"; import type { CustomerInvoiceModel } from "../../../../../common";
export class SequelizeIssuedInvoiceRecipientDomainMapper { export class SequelizeIssuedInvoiceRecipientDomainMapper {
@ -118,27 +114,10 @@ export class SequelizeIssuedInvoiceRecipientDomainMapper {
* En caso contrario, se agrega un error de validación. * En caso contrario, se agrega un error de validación.
*/ */
mapToPersistence(source: InvoiceRecipient, params?: MapperParamsType) { mapToPersistence(source: InvoiceRecipient, params?: MapperParamsType) {
const { errors, parent } = params as { /*const { errors, parent } = params as {
parent: IssuedInvoice; parent: IssuedInvoice;
errors: ValidationErrorDetail[]; errors: ValidationErrorDetail[];
}; };*/
const { hasRecipient } = parent;
// Validación: facturas emitidas deben tener destinatario.
if (!hasRecipient) {
errors.push({
path: "recipient",
message: "[InvoiceRecipientDomainMapper] Issued customer invoice without recipient data",
});
}
// Si hay errores previos, devolvemos fallo de validación inmediatamente.
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("Customer invoice mapping to persistence failed", errors)
);
}
const recipient = source; const recipient = source;

View File

@ -1,5 +1,5 @@
import type { UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { type Maybe, Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { type Transaction, type WhereOptions, literal } from "sequelize"; import { type Transaction, type WhereOptions, literal } from "sequelize";
import type { IIssuedInvoiceNumberGenerator } from "../../../../../application/issued-invoices"; import type { IIssuedInvoiceNumberGenerator } from "../../../../../application/issued-invoices";
@ -12,7 +12,7 @@ import { CustomerInvoiceModel } from "../../../../common/persistence";
export class SequelizeIssuedInvoiceNumberGenerator implements IIssuedInvoiceNumberGenerator { export class SequelizeIssuedInvoiceNumberGenerator implements IIssuedInvoiceNumberGenerator {
public async getNextForCompany( public async getNextForCompany(
companyId: UniqueID, companyId: UniqueID,
series: Maybe<InvoiceSerie>, series: InvoiceSerie,
transaction: Transaction transaction: Transaction
): Promise<Result<InvoiceNumber, Error>> { ): Promise<Result<InvoiceNumber, Error>> {
const where: WhereOptions = { const where: WhereOptions = {
@ -20,14 +20,7 @@ export class SequelizeIssuedInvoiceNumberGenerator implements IIssuedInvoiceNumb
is_proforma: false, is_proforma: false,
}; };
series.match( where.series = series.toString();
(serieVO) => {
where.series = serieVO.toString();
},
() => {
where.series = null;
}
);
try { try {
const lastInvoice = await CustomerInvoiceModel.findOne({ const lastInvoice = await CustomerInvoiceModel.findOne({

View File

@ -1,46 +1,8 @@
import { import { createPaginatedListSchema } from "@erp/core";
CurrencyCodeSchema, import type { z } from "zod/v4";
IsoDateSchema,
LanguageCodeSchema,
MoneySchema,
PercentageSchema,
createPaginatedListSchema,
} from "@erp/core";
import { z } from "zod/v4";
import { ProformaRecipientSummarySchema, ProformaStatusSchema } from "../../shared/proforma"; import { ProformaSummarySchema } from "../../shared/proforma";
export const ListProformasResponseSchema = createPaginatedListSchema( export const ListProformasResponseSchema = createPaginatedListSchema(ProformaSummarySchema);
z.object({
id: z.uuid(),
company_id: z.uuid(),
is_proforma: z.boolean(),
invoice_number: z.string(),
status: ProformaStatusSchema,
series: z.string().nullable(),
invoice_date: IsoDateSchema,
operation_date: IsoDateSchema.nullable(),
language_code: LanguageCodeSchema,
currency_code: CurrencyCodeSchema,
reference: z.string().nullable(),
description: z.string().nullable(),
customer_id: z.uuid(),
recipient: ProformaRecipientSummarySchema,
subtotal_amount: MoneySchema,
discount_percentage: PercentageSchema,
discount_amount: MoneySchema,
taxable_amount: MoneySchema,
taxes_amount: MoneySchema,
total_amount: MoneySchema,
linked_invoice_id: z.uuid().nullable(),
})
);
export type ListProformasResponseDTO = z.infer<typeof ListProformasResponseSchema>; export type ListProformasResponseDTO = z.infer<typeof ListProformasResponseSchema>;

View File

@ -1,3 +1,4 @@
export * from "./proforma-item-detail.dto"; export * from "./proforma-item-detail.dto";
export * from "./proforma-recipient-summary.dto"; export * from "./proforma-recipient-summary.dto";
export * from "./proforma-status.dto"; export * from "./proforma-status.dto";
export * from "./proforma-summary.dto";

View File

@ -0,0 +1,37 @@
import { CurrencyCodeSchema, IsoDateSchema, LanguageCodeSchema, MoneySchema } from "@erp/core";
import { z } from "zod/v4";
import { ProformaRecipientSummarySchema } from "./proforma-recipient-summary.dto";
import { ProformaStatusSchema } from "./proforma-status.dto";
export const ProformaSummarySchema = z.object({
id: z.uuid(),
company_id: z.uuid(),
is_proforma: z.boolean(),
invoice_number: z.string(),
status: ProformaStatusSchema,
series: z.string().nullable(),
invoice_date: IsoDateSchema,
operation_date: IsoDateSchema.nullable(),
language_code: LanguageCodeSchema,
currency_code: CurrencyCodeSchema,
reference: z.string().nullable(),
description: z.string().nullable(),
customer_id: z.uuid(),
recipient: ProformaRecipientSummarySchema,
subtotal_amount: MoneySchema,
total_discount_amount: MoneySchema,
taxable_amount: MoneySchema,
taxes_amount: MoneySchema,
total_amount: MoneySchema,
linked_invoice_id: z.uuid().nullable(),
});
export type ProformaSummaryDTO = z.infer<typeof ProformaSummarySchema>;

View File

@ -1,4 +1,4 @@
import { formatDate } from "@erp/core/client"; import { DateHelper } from "@erp/core";
import { ReactQRCode } from "@lglab/react-qr-code"; import { ReactQRCode } from "@lglab/react-qr-code";
import { DataTableColumnHeader } from "@repo/rdx-ui/components"; import { DataTableColumnHeader } from "@repo/rdx-ui/components";
import { import {
@ -205,7 +205,7 @@ export function useIssuedInvoicesGridColumns(
), ),
cell: ({ row }) => ( cell: ({ row }) => (
<div className="font-medium text-left tabular-nums"> <div className="font-medium text-left tabular-nums">
{formatDate(row.original.invoiceDate)} {DateHelper.format(row.original.invoiceDate)}
</div> </div>
), ),
enableSorting: false, enableSorting: false,
@ -227,7 +227,7 @@ export function useIssuedInvoicesGridColumns(
), ),
cell: ({ row }) => ( cell: ({ row }) => (
<div className="font-medium text-left tabular-nums"> <div className="font-medium text-left tabular-nums">
{formatDate(row.original.operationDate)} {DateHelper.format(row.original.operationDate)}
</div> </div>
), ),
enableSorting: false, enableSorting: false,
@ -284,6 +284,27 @@ export function useIssuedInvoicesGridColumns(
}, },
}, },
// Taxable amount
{
accessorKey: "taxableAmountFmt",
header: ({ column }) => (
<DataTableColumnHeader
className="text-right tabular-nums"
column={column}
title={t("pages.issued_invoices.list.grid_columns.taxable_amount", "Base imponible")}
/>
),
cell: ({ row }) => (
<div className="font-medium text-right tabular-nums">{row.original.taxableAmountFmt}</div>
),
enableSorting: false,
size: 120,
minSize: 100,
meta: {
title: t("pages.issued_invoices.list.grid_columns.taxable_amount"),
},
},
// Taxes amount // Taxes amount
{ {
accessorKey: "taxes_amount_fmt", accessorKey: "taxes_amount_fmt",

View File

@ -1,3 +1,4 @@
import { DateHelper } from "@erp/core";
import { import {
Button, Button,
Tooltip, Tooltip,
@ -159,6 +160,10 @@ export function useProformasGridColumns(
accessorKey: "series", accessorKey: "series",
header: "Serie", header: "Serie",
}, },
{
accessorKey: "reference",
header: "Referencia",
},
{ {
accessorKey: "invoiceDate", accessorKey: "invoiceDate",
header: ({ column }) => { header: ({ column }) => {
@ -173,6 +178,17 @@ export function useProformasGridColumns(
</Button> </Button>
); );
}, },
cell: ({ row }) => (
<div className="font-medium text-left tabular-nums">
{DateHelper.format(row.original.invoiceDate)}
</div>
),
enableSorting: false,
size: 140,
minSize: 120,
meta: {
title: t("pages.issued_invoices.list.grid_columns.invoice_date"),
},
}, },
{ {
accessorKey: "operationDate", accessorKey: "operationDate",
@ -193,30 +209,43 @@ export function useProformasGridColumns(
accessorKey: "subtotalAmountFmt", accessorKey: "subtotalAmountFmt",
header: () => <div className="text-right">Subtotal</div>, header: () => <div className="text-right">Subtotal</div>,
cell: ({ row }) => ( cell: ({ row }) => (
<div className="text-right tabular-nums">{row.getValue("subtotalAmountFmt")}</div> <div className="text-right tabular-nums font-medium">
{row.getValue("subtotalAmountFmt")}
</div>
), ),
}, },
{ {
accessorKey: "discountAmountFmt", accessorKey: "totalDiscountAmountFmt",
header: () => <div className="text-right">Descuentos</div>, header: () => <div className="text-right">Descuentos</div>,
cell: ({ row }) => ( cell: ({ row }) => (
<div className="text-right tabular-nums">{row.getValue("discountAmountFmt")}</div> <div className="text-right tabular-nums font-medium">
{row.getValue("totalDiscountAmountFmt")}
</div>
),
},
{
accessorKey: "taxableAmountFmt",
header: () => <div className="text-right">Base imponible</div>,
cell: ({ row }) => (
<div className="text-right tabular-nums font-medium">
{row.getValue("taxableAmountFmt")}
</div>
), ),
}, },
{ {
accessorKey: "taxesAmountFmt", accessorKey: "taxesAmountFmt",
header: () => <div className="text-right">Impuestos</div>, header: () => <div className="text-right">Impuestos</div>,
cell: ({ row }) => ( cell: ({ row }) => (
<div className="text-right tabular-nums">{row.getValue("taxesAmountFmt")}</div> <div className="text-right tabular-nums font-medium">
{row.getValue("taxesAmountFmt")}
</div>
), ),
}, },
{ {
accessorKey: "totalAmountFmt", accessorKey: "totalAmountFmt",
header: () => <div className="text-right">Importe total</div>, header: () => <div className="text-right">Importe total</div>,
cell: ({ row }) => ( cell: ({ row }) => (
<div className="text-right tabular-nums font-medium"> <div className="text-right tabular-nums font-bold">{row.getValue("totalAmountFmt")}</div>
{row.getValue("totalAmountFmt")}
</div>
), ),
}, },
{ {

View File

@ -1,28 +1,52 @@
import { MoneyDTOHelper, PercentageDTOHelper, formatCurrency } from "@erp/core"; import { MoneyDTOHelper, formatCurrency } from "@erp/core";
import type { ListProformasResponseDTO } from "../../../../common"; import type { ListProformasResponseDTO } from "../../../../common";
import type { ListProformasResult } from "../api";
import type { ProformaList, ProformaListRow, ProformaStatus } from "../entities"; import type { ProformaList, ProformaListRow, ProformaStatus } from "../entities";
/**
* Adaptador para transformar los datos de la API de ListProformasResponseDTO
* a la entidad ProformaList utilizada en la aplicación.
* Reglas de adaptación:
* - page, per_page, total_pages, total_items se asignan directamente.
* - items se transforma utilizando ProformaListRowAdapter para cada elemento.
*
* @param pageDto - lista de proformas desde la API.
* @param context - Contexto adicional opcional para la adaptación.
* @returns {ProformaList} Objeto adaptado a ProformaList.
*/
export const ListProformasAdapter = { export const ListProformasAdapter = {
fromDto(dto: ListProformasResponseDTO): ProformaList { fromDto(dto: ListProformasResult, context?: unknown): ProformaList {
return { return {
items: dto.items.map(ProformaListRowAdapter.fromDto),
page: dto.page, page: dto.page,
perPage: dto.per_page, perPage: dto.per_page,
totalPages: dto.total_pages, totalPages: dto.total_pages,
totalItems: dto.total_items, totalItems: dto.total_items,
items: dto.items.map((rowDto) => ProformaListRowAdapter.fromDto(rowDto, context)),
}; };
}, },
}; };
type ListProformasItemDTO = ListProformasResponseDTO["items"][number]; /**
* Adaptador para transformar los items de la API de ListProformasResult a la entidad ProformaListRow.
* Reglas de adaptación:
* - id, company_id, status, reference se asignan directamente.
* - is_proforma se convierte a booleano (true si es "1", false si es "0").
*
* @param rowDto - item de proforma desde la API.
* @param context - Contexto adicional opcional para la adaptación.
* @returns {ProformaListRow} Objeto adaptado a ProformaListRow.
*/
type ListProformasItemOutput = ListProformasResponseDTO["items"][number];
const ProformaListRowAdapter = { const ProformaListRowAdapter = {
fromDto(dto: ListProformasItemDTO): ProformaListRow { fromDto(dto: ListProformasItemOutput, context?: unknown): ProformaListRow {
return { return {
id: dto.id, id: dto.id,
companyId: dto.company_id, companyId: dto.company_id,
isProforma: dto.is_proforma === "1", isProforma: dto.is_proforma,
invoiceNumber: dto.invoice_number, invoiceNumber: dto.invoice_number,
status: dto.status as ProformaStatus, status: dto.status as ProformaStatus,
@ -57,12 +81,9 @@ const ProformaListRowAdapter = {
dto.language_code dto.language_code
), ),
discountPercentage: PercentageDTOHelper.toNumber(dto.discount_percentage), totalDiscountAmount: MoneyDTOHelper.toNumber(dto.total_discount_amount),
discountPercentageFmt: PercentageDTOHelper.toNumericString(dto.discount_percentage), totalDiscountAmountFmt: formatCurrency(
MoneyDTOHelper.toNumber(dto.total_discount_amount),
discountAmount: MoneyDTOHelper.toNumber(dto.discount_amount),
discountAmountFmt: formatCurrency(
MoneyDTOHelper.toNumber(dto.discount_amount),
Number(dto.total_amount.scale || 2), Number(dto.total_amount.scale || 2),
dto.currency_code, dto.currency_code,
dto.language_code dto.language_code

View File

@ -15,27 +15,24 @@ export interface ProformaListRow {
invoiceNumber: string; invoiceNumber: string;
status: ProformaStatus; status: ProformaStatus;
series: string; series: string | null;
invoiceDate: string; invoiceDate: string;
operationDate: string; operationDate: string | null;
languageCode: string; languageCode: string;
currencyCode: string; currencyCode: string;
reference: string; reference: string | null;
description: string; description: string | null;
recipient: ProformaRecipient; recipient: ProformaRecipient;
subtotalAmount: number; subtotalAmount: number;
subtotalAmountFmt: string; subtotalAmountFmt: string;
discountPercentage: number; totalDiscountAmount: number;
discountPercentageFmt: string; totalDiscountAmountFmt: string;
discountAmount: number;
discountAmountFmt: string;
taxableAmount: number; taxableAmount: number;
taxableAmountFmt: string; taxableAmountFmt: string;
@ -46,5 +43,5 @@ export interface ProformaListRow {
totalAmount: number; totalAmount: number;
totalAmountFmt: string; totalAmountFmt: string;
linkedInvoiceId: string; linkedInvoiceId: string | null;
} }

View File

@ -5,14 +5,14 @@
export interface ProformaRecipient { export interface ProformaRecipient {
id: string; id: string;
name: string; name: string | null;
tin: string; tin: string | null;
street: string; street: string | null;
street2: string; street2: string | null;
city: string; city: string | null;
province: string; province: string | null;
postalCode: string; postalCode: string | null;
country: string; country: string | null;
} }

View File

@ -1,7 +1,8 @@
import type { CustomerSelectionOption } from "@erp/customers"; import type { CustomerSelectionOption } from "@erp/customers";
import { FormSectionCard } from "@repo/rdx-ui/components";
import { useTranslation } from "../../../../i18n"; import { useTranslation } from "../../../../i18n";
import { FormSectionCard, SelectedRecipientSummary } from "../blocks"; import { SelectedRecipientSummary } from "../blocks";
interface ProformaUpdateRecipientEditorProps { interface ProformaUpdateRecipientEditorProps {
disabled?: boolean; disabled?: boolean;

View File

@ -1,7 +1,6 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"baseUrl": ".",
"paths": { "paths": {
"@erp/customer-invoices/*": ["./src/*"] "@erp/customer-invoices/*": ["./src/*"]
}, },

View File

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

View File

@ -1,3 +1,4 @@
import { DateHelper } from "@erp/core";
import { Badge, Button } from "@repo/shadcn-ui/components"; import { Badge, Button } from "@repo/shadcn-ui/components";
import { FileTextIcon } from "lucide-react"; import { FileTextIcon } from "lucide-react";
@ -69,7 +70,7 @@ export const CustomerProformasSection = ({
> >
<div className="min-w-0"> <div className="min-w-0">
<p className="text-sm font-medium text-foreground truncate">{pro.number}</p> <p className="text-sm font-medium text-foreground truncate">{pro.number}</p>
<p className="text-xs text-muted-foreground">{formatDate(pro.date)}</p> <p className="text-xs text-muted-foreground">{DateHelper.format(pro.date)}</p>
</div> </div>
<div className="flex items-center gap-2 shrink-0"> <div className="flex items-center gap-2 shrink-0">
<span className="text-sm font-medium">{formatCurrency(pro.amount)}</span> <span className="text-sm font-medium">{formatCurrency(pro.amount)}</span>

View File

@ -1,7 +1,6 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"baseUrl": ".",
"paths": { "paths": {
"@erp/customers/*": ["./src/*"] "@erp/customers/*": ["./src/*"]
}, },
@ -28,11 +27,6 @@
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true
}, },
"include": [ "include": ["src"],
"src",
"../customer-invoices/src/web/proformas/update/ui/blocks/selected-recipient/customer-view-dialog.tsx",
"../customer-invoices/src/web/proformas/update/ui/blocks/selected-recipient/customer-card.tsx",
"../customer-invoices/src/web/proformas/update/ui/blocks/selected-recipient/selected-recipient-empty-card.tsx"
],
"exclude": ["node_modules"] "exclude": ["node_modules"]
} }

View File

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

View File

@ -4,10 +4,10 @@ import {
requireAuthenticatedGuard, requireAuthenticatedGuard,
requireCompanyContextGuard, requireCompanyContextGuard,
} from "@erp/core/api"; } from "@erp/core/api";
import type { CreateProformaFromFactugesUseCase } from "@erp/factuges/api/application";
import type { CreateProformaFromFactugesRequestDTO } from "@erp/factuges/common";
import type { CreateProformaFromFactugesRequestDTO } from "../../../../common/dto/request/create-proforma-from-factuges.request.dto.js"; import { factugesApiErrorMapper } from "../factuges-api-error-mapper";
import type { CreateProformaFromFactugesUseCase } from "../../../application/use-cases/create-proforma-from-factuges.use-case.js";
import { factugesApiErrorMapper } from "../factuges-api-error-mapper.js";
export class CreateProformaFromFactugesController extends ExpressController { export class CreateProformaFromFactugesController extends ExpressController {
public constructor(private readonly useCase: CreateProformaFromFactugesUseCase) { public constructor(private readonly useCase: CreateProformaFromFactugesUseCase) {

View File

@ -1,7 +1,6 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"baseUrl": ".",
"paths": { "paths": {
"@erp/factuges/*": ["./src/*"] "@erp/factuges/*": ["./src/*"]
}, },

View File

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

View File

@ -1,7 +1,6 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"baseUrl": ".",
"paths": { "paths": {
"@erp/supplier-invoices/*": ["./src/*"] "@erp/supplier-invoices/*": ["./src/*"]
}, },

View File

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

View File

@ -1,7 +1,6 @@
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"baseUrl": ".",
"paths": { "paths": {
"@erp/suppliers/*": ["./src/*"] "@erp/suppliers/*": ["./src/*"]
}, },

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import { ZodError } from "zod/v4"; import type { ZodError } from "zod/v4";
import { ValidationErrorCollection, ValidationErrorDetail } from "../errors";
import { ValidationErrorCollection, type ValidationErrorDetail } from "../errors";
export function translateZodValidationError<T>( export function translateZodValidationError<T>(
message: string, message: string,

View File

@ -1,6 +1,8 @@
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { z } from "zod/v4"; import { z } from "zod/v4";
import { translateZodValidationError } from "../helpers"; import { translateZodValidationError } from "../helpers";
import { ValueObject } from "./value-object"; import { ValueObject } from "./value-object";
interface UtcDateProps { interface UtcDateProps {
@ -101,4 +103,15 @@ export class UtcDate extends ValueObject<UtcDateProps> {
equals(other: UtcDate): boolean { equals(other: UtcDate): boolean {
return this.toISOString() === other.toISOString(); return this.toISOString() === other.toISOString();
} }
/**
* Determina si la fecha representada
* es una fecha futura en comparación con la fecha actual.
* @returns
*/
isFuture(currentDate?: UtcDate): boolean {
const now = currentDate ? currentDate : UtcDate.today();
return this.date.getTime() > now.getTime();
}
} }

View File

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

View File

@ -1,7 +1,6 @@
{ {
"extends": "@repo/typescript-config/react-library.json", "extends": "@repo/typescript-config/react-library.json",
"compilerOptions": { "compilerOptions": {
"baseUrl": ".",
"paths": { "paths": {
"@repo/rdx-ui/*": ["./src/*"] "@repo/rdx-ui/*": ["./src/*"]
}, },

View File

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

View File

@ -1,8 +1,9 @@
{ {
"extends": "@repo/typescript-config/buildless.json", "extends": "@repo/typescript-config/buildless.json",
"compilerOptions": { "compilerOptions": {
"types": ["node"],
"rootDir": "src" "rootDir": "src"
}, },
"include": ["src", "../../modules/core/src/api/application/mappers/patch-collector.ts"], "include": ["src"],
"exclude": ["node_modules", "dist", "**/*.test.ts"] "exclude": ["node_modules", "dist", "**/*.test.ts"]
} }

View File

@ -1,16 +1,21 @@
{ {
"$schema": "https://json.schemastore.org/tsconfig", "$schema": "https://json.schemastore.org/tsconfig",
"display": "Root", "display": "Root",
"compilerOptions": { "compilerOptions": {
"baseUrl": ".",
"paths": { "paths": {
"@erp/core/*": ["modules/core/src/*"], "@erp/core/*": [
"@erp/auth/*": ["modules/auth/src/*"], "modules/core/src/*"
"@erp/customers/*": ["modules/customers/src/*"], ],
"@erp/customer-invoices/*": ["modules/customer-invoices/src/*"] "@erp/auth/*": [
"modules/auth/src/*"
],
"@erp/customers/*": [
"modules/customers/src/*"
],
"@erp/customer-invoices/*": [
"modules/customer-invoices/src/*"
]
}, },
"target": "ES2021", "target": "ES2021",
"module": "CommonJS", "module": "CommonJS",
"moduleResolution": "Node", "moduleResolution": "Node",
@ -20,4 +25,4 @@
"skipLibCheck": true, "skipLibCheck": true,
"allowUnreachableCode": true "allowUnreachableCode": true
} }
} }

View File

@ -65,12 +65,6 @@ importers:
'@erp/customers': '@erp/customers':
specifier: workspace:* specifier: workspace:*
version: link:../../modules/customers version: link:../../modules/customers
'@erp/factuges':
specifier: workspace:*
version: link:../../modules/factuges
'@erp/suppliers':
specifier: workspace:*
version: link:../../modules/supplier
'@repo/rdx-logger': '@repo/rdx-logger':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/rdx-logger version: link:../../packages/rdx-logger