Facturas de cliente

This commit is contained in:
David Arranz 2025-09-18 10:49:32 +02:00
parent 096abdccb2
commit ac11eaffdf
11 changed files with 84 additions and 26 deletions

View File

@ -0,0 +1,12 @@
import { MoneyDTO } from "@erp/core";
import { MoneyValue } from "@repo/rdx-ddd";
export function formatMoneyDTO(amount: MoneyDTO, locale: string) {
const money = MoneyValue.create({
value: Number(amount.value),
currency_code: amount.currency_code,
scale: Number(amount.scale),
}).data;
return money.format(locale);
}

View File

@ -1 +1,2 @@
export * from "./format-money-dto";
export * from "./map-dto-to-customer-invoice-props"; export * from "./map-dto-to-customer-invoice-props";

View File

@ -1,16 +1,21 @@
import { ValidationErrorCollection, ValidationErrorDetail } from "@repo/rdx-ddd"; import {
ValidationErrorCollection,
ValidationErrorDetail,
extractOrPushError,
} from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { CreateCustomerInvoiceRequestDTO } from "../../../common";
import { import {
CustomerInvoiceItem, CustomerInvoiceItem,
CustomerInvoiceItemDescription, CustomerInvoiceItemDescription,
ItemAmount, ItemAmount,
ItemDiscount, ItemDiscount,
ItemQuantity,
} from "../../domain"; } from "../../domain";
import { extractOrPushError } from "./extract-or-push-error";
import { hasNoUndefinedFields } from "./has-no-undefined-fields"; import { hasNoUndefinedFields } from "./has-no-undefined-fields";
export function mapDTOToCustomerInvoiceItemsProps( export function mapDTOToCustomerInvoiceItemsProps(
dtoItems: Pick<CreateCustomerInvoiceCommandDTO, "items">["items"] dtoItems: Pick<CreateCustomerInvoiceRequestDTO, "items">["items"]
): Result<CustomerInvoiceItem[], ValidationErrorCollection> { ): Result<CustomerInvoiceItem[], ValidationErrorCollection> {
const errors: ValidationErrorDetail[] = []; const errors: ValidationErrorDetail[] = [];
const items: CustomerInvoiceItem[] = []; const items: CustomerInvoiceItem[] = [];
@ -25,9 +30,8 @@ export function mapDTOToCustomerInvoiceItemsProps(
); );
const quantity = extractOrPushError( const quantity = extractOrPushError(
CustomerInvoiceItemQuantity.create({ ItemQuantity.create({
amount: item.quantity.amount, value: Number(item.quantity),
scale: item.quantity.scale,
}), }),
path("quantity"), path("quantity"),
errors errors

View File

@ -1,14 +1,20 @@
import { ValidationErrorCollection, ValidationErrorDetail } from "@erp/core/api"; import {
import { UniqueID, UtcDate } from "@repo/rdx-ddd"; CurrencyCode,
UniqueID,
UtcDate,
ValidationErrorCollection,
ValidationErrorDetail,
extractOrPushError,
maybeFromNullableVO,
} from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { CreateCustomerInvoiceCommandDTO } from "../../../common/dto"; import { CreateCustomerInvoiceRequestDTO } from "../../../common";
import { import {
CustomerInvoiceNumber, CustomerInvoiceNumber,
CustomerInvoiceProps, CustomerInvoiceProps,
CustomerInvoiceSerie, CustomerInvoiceSerie,
CustomerInvoiceStatus, CustomerInvoiceStatus,
} from "../../domain"; } from "../../domain";
import { extractOrPushError } from "./extract-or-push-error";
import { mapDTOToCustomerInvoiceItemsProps } from "./map-dto-to-customer-invoice-items-props"; import { mapDTOToCustomerInvoiceItemsProps } from "./map-dto-to-customer-invoice-items-props";
/** /**
@ -21,7 +27,7 @@ import { mapDTOToCustomerInvoiceItemsProps } from "./map-dto-to-customer-invoice
* *
*/ */
export function mapDTOToCustomerInvoiceProps(dto: CreateCustomerInvoiceCommandDTO) { export function mapDTOToCustomerInvoiceProps(dto: CreateCustomerInvoiceRequestDTO) {
const errors: ValidationErrorDetail[] = []; const errors: ValidationErrorDetail[] = [];
const invoiceId = extractOrPushError(UniqueID.create(dto.id), "id", errors); const invoiceId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
@ -32,7 +38,7 @@ export function mapDTOToCustomerInvoiceProps(dto: CreateCustomerInvoiceCommandDT
errors errors
); );
const invoiceSeries = extractOrPushError( const invoiceSeries = extractOrPushError(
CustomerInvoiceSerie.create(dto.invoice_series), maybeFromNullableVO(dto.invoice_series, (value) => CustomerInvoiceSerie.create(value)),
"invoice_series", "invoice_series",
errors errors
); );
@ -42,13 +48,16 @@ export function mapDTOToCustomerInvoiceProps(dto: CreateCustomerInvoiceCommandDT
errors errors
); );
const operationDate = extractOrPushError( const operationDate = extractOrPushError(
UtcDate.createFromISO(dto.operation_date), maybeFromNullableVO(dto.operation_date, (value) => UtcDate.createFromISO(value)),
"operation_date", "operation_date",
errors errors
); );
//const currency = extractOrPushError(Currency.(dto.currency), "currency", errors); const currencyCode = extractOrPushError(
const currency = dto.currency; CurrencyCode.create(dto.currency_code),
"currency",
errors
);
// 🔄 Validar y construir los items de factura con helper especializado // 🔄 Validar y construir los items de factura con helper especializado
const itemsResult = mapDTOToCustomerInvoiceItemsProps(dto.items); const itemsResult = mapDTOToCustomerInvoiceItemsProps(dto.items);
@ -57,16 +66,16 @@ export function mapDTOToCustomerInvoiceProps(dto: CreateCustomerInvoiceCommandDT
} }
if (errors.length > 0) { if (errors.length > 0) {
return Result.fail(new ValidationErrorCollection(errors)); return Result.fail(new ValidationErrorCollection("Customer dto mapping failed", errors));
} }
const invoiceProps: CustomerInvoiceProps = { const invoiceProps: CustomerInvoiceProps = {
invoiceNumber: invoiceNumber!, invoiceNumber: invoiceNumber!,
invoiceSeries: invoiceSeries!, series: invoiceSeries!,
invoiceDate: invoiceDate!, invoiceDate: invoiceDate!,
operationDate: operationDate!, operationDate: operationDate!,
status: CustomerInvoiceStatus.createDraft(), status: CustomerInvoiceStatus.createDraft(),
currency, currencyCode: currencyCode!,
}; };
return Result.ok({ id: invoiceId!, props: invoiceProps }); return Result.ok({ id: invoiceId!, props: invoiceProps });

View File

@ -0,0 +1,17 @@
import { Presenter } from "@erp/core/api";
import { GetCustomerInvoiceByIdResponseDTO } from "../../../../common/dto";
import { formatMoneyDTO } from "../../helpers";
export class CustomerInvoiceReportPresenter extends Presenter<
GetCustomerInvoiceByIdResponseDTO,
unknown
> {
toOutput(invoiceDTO: GetCustomerInvoiceByIdResponseDTO) {
const locale = invoiceDTO.language_code;
return {
...invoiceDTO,
total_amount: formatMoneyDTO(invoiceDTO.total_amount, locale),
};
}
}

View File

@ -1 +1,2 @@
export * from "./customer-invoice.report.presenter";
export * from "./list-customer-invoices.presenter"; export * from "./list-customer-invoices.presenter";

View File

@ -11,13 +11,20 @@ export class CustomerInvoiceReportHTMLPresenter extends Presenter {
projection: "FULL", projection: "FULL",
}); });
const prePresenter = this.presenterRegistry.getPresenter({
resource: "customer-invoice",
projection: "REPORT",
format: "JSON",
});
const invoiceDTO = dtoPresenter.toOutput(customerInvoice); const invoiceDTO = dtoPresenter.toOutput(customerInvoice);
const prettyDTO = prePresenter.toOutput(invoiceDTO);
// Obtener y compilar la plantilla HTML // Obtener y compilar la plantilla HTML
const templateHtml = readFileSync( const templateHtml = readFileSync(
path.join(__dirname, "./templates/customer-invoice/template.hbs") path.join(__dirname, "./templates/customer-invoice/template.hbs")
).toString(); ).toString();
const template = handlebars.compile(templateHtml, {}); const template = handlebars.compile(templateHtml, {});
return template(invoiceDTO); return template(prettyDTO);
} }
} }

View File

@ -139,7 +139,7 @@
<!-- Badge TOTAL superpuesto --> <!-- Badge TOTAL superpuesto -->
<div class="absolute -top-7 right-0"> <div class="absolute -top-7 right-0">
<div class="relative bg-[#f08119] text-white text-sm font-semibold px-3 py-1 shadow"> <div class="relative bg-[#f08119] text-white text-sm font-semibold px-3 py-1 shadow">
TOTAL: {{total_amount.value}} TOTAL: {{total_amount}}
</div> </div>
</div> </div>
</div> </div>

View File

@ -14,6 +14,7 @@ import {
CustomerInvoiceItemsFullPresenter, CustomerInvoiceItemsFullPresenter,
CustomerInvoiceReportHTMLPresenter, CustomerInvoiceReportHTMLPresenter,
CustomerInvoiceReportPDFPresenter, CustomerInvoiceReportPDFPresenter,
CustomerInvoiceReportPresenter,
GetCustomerInvoiceUseCase, GetCustomerInvoiceUseCase,
ListCustomerInvoicesPresenter, ListCustomerInvoicesPresenter,
ListCustomerInvoicesUseCase, ListCustomerInvoicesUseCase,
@ -99,6 +100,14 @@ export function buildCustomerInvoiceDependencies(params: ModuleParams): Customer
}, },
presenter: new ListCustomerInvoicesPresenter(presenterRegistry), presenter: new ListCustomerInvoicesPresenter(presenterRegistry),
}, },
{
key: {
resource: "customer-invoice",
projection: "REPORT",
format: "JSON",
},
presenter: new CustomerInvoiceReportPresenter(presenterRegistry),
},
{ {
key: { key: {
resource: "customer-invoice", resource: "customer-invoice",

View File

@ -119,7 +119,7 @@ export class CustomerInvoiceDomainMapper
const discountPercentage = extractOrPushError( const discountPercentage = extractOrPushError(
Percentage.create({ Percentage.create({
value: source.discount_amount_scale, value: source.discount_percentage_value,
scale: source.discount_percentage_scale, scale: source.discount_percentage_scale,
}), }),
"discount_percentage_value", "discount_percentage_value",
@ -206,9 +206,7 @@ export class CustomerInvoiceDomainMapper
// 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) {
return Result.fail( return Result.fail(new ValidationErrorCollection("Customer mapping failed", errors));
new ValidationErrorCollection("Customer invoice mapping failed", errors)
);
} }
// 6) Construcción del agregado (Dominio) // 6) Construcción del agregado (Dominio)

View File

@ -207,7 +207,7 @@ export class MoneyValue extends ValueObject<MoneyValueProps> implements IMoneyVa
/** /**
* Devuelve una cadena con el importe formateado. * Devuelve una cadena con el importe formateado.
* Ejemplo: 123456 -> 1,234.56 * Ejemplo: 123456 -> 1.234,56
* @param locale Código de idioma y país (ej. "es-ES") * @param locale Código de idioma y país (ej. "es-ES")
* @returns Importe formateado * @returns Importe formateado
*/ */
@ -222,6 +222,6 @@ export class MoneyValue extends ValueObject<MoneyValueProps> implements IMoneyVa
minimumFractionDigits: scale, minimumFractionDigits: scale,
maximumFractionDigits: scale, maximumFractionDigits: scale,
useGrouping: true, useGrouping: true,
}).format(value); }).format(this.formattedValue);
} }
} }