This commit is contained in:
David Arranz 2026-04-24 20:53:05 +02:00
parent e87e3ec609
commit c9ba2d0370
34 changed files with 409 additions and 543 deletions

View File

@ -1,10 +1,9 @@
import { type JsonTaxCatalogProvider, NumberHelper } from "@erp/core"; import { NumberHelper } from "@erp/core";
import { DiscountPercentage } from "@erp/core/api"; import { DiscountPercentage } from "@erp/core/api";
import { import {
CurrencyCode, CurrencyCode,
DomainError, DomainError,
LanguageCode, LanguageCode,
Percentage,
TextValue, TextValue,
UniqueID, UniqueID,
UtcDate, UtcDate,
@ -20,7 +19,6 @@ import {
type IProformaCreateProps, type IProformaCreateProps,
type IProformaItemCreateProps, type IProformaItemCreateProps,
InvoiceNumber, InvoiceNumber,
InvoicePaymentMethod,
type InvoiceRecipient, type InvoiceRecipient,
InvoiceSerie, InvoiceSerie,
InvoiceStatus, InvoiceStatus,
@ -31,23 +29,6 @@ import {
type ProformaItemTaxesProps, type ProformaItemTaxesProps,
} from "../../../domain"; } from "../../../domain";
/**
* CreateProformaPropsMapper
* Convierte el DTO a las props validadas (CustomerProps).
* No construye directamente el agregado.
*
* @param dto - DTO con los datos de la factura de cliente
* @returns
*
*/
/*export interface ICreateProformaInputMapper
extends IDTOInputToPropsMapper<
CreateProformaRequestDTO,
{ id: UniqueID; props: Omit<IProformaProps, "items"> & { items: IProformaItemProps[] } }
> {}*/
export interface ICreateProformaInputMapper { export interface ICreateProformaInputMapper {
map( map(
dto: CreateProformaRequestDTO, dto: CreateProformaRequestDTO,
@ -55,23 +36,21 @@ export interface ICreateProformaInputMapper {
): Result<{ id: UniqueID; props: IProformaCreateProps }>; ): Result<{ id: UniqueID; props: IProformaCreateProps }>;
} }
export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/ { /**
private readonly taxCatalog: JsonTaxCatalogProvider; * @summary Convierte el DTO de creación de proforma en props de dominio.
* @remarks
constructor(params: { taxCatalog: JsonTaxCatalogProvider }) { * No construye el agregado. Solo valida y convierte primitivas de transporte
this.taxCatalog = params.taxCatalog; * a Value Objects y props necesarias para `Proforma.create`.
} */
export class CreateProformaInputMapper implements ICreateProformaInputMapper {
public map( public map(
dto: CreateProformaRequestDTO, dto: CreateProformaRequestDTO,
params: { companyId: UniqueID } params: { companyId: UniqueID }
): Result<{ id: UniqueID; props: IProformaCreateProps }> { ): Result<{ id: UniqueID; props: IProformaCreateProps }> {
const errors: ValidationErrorDetail[] = []; const errors: ValidationErrorDetail[] = [];
const { companyId } = params;
try { try {
const defaultStatus = InvoiceStatus.draft();
const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", errors); const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
const customerId = extractOrPushError( const customerId = extractOrPushError(
@ -80,9 +59,7 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
errors errors
); );
const recipient = Maybe.none<InvoiceRecipient>(); const invoiceNumber = extractOrPushError(
const proformaNumber = extractOrPushError(
InvoiceNumber.create(dto.invoice_number), InvoiceNumber.create(dto.invoice_number),
"invoice_number", "invoice_number",
errors errors
@ -113,7 +90,7 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
); );
const description = extractOrPushError( const description = extractOrPushError(
maybeFromNullableResult(dto.reference, (value) => Result.ok(String(value))), maybeFromNullableResult(dto.description, (value) => Result.ok(String(value))),
"description", "description",
errors errors
); );
@ -136,24 +113,21 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
errors errors
); );
const paymentMethod = extractOrPushError(
maybeFromNullableResult(dto.payment_method, (value) =>
InvoicePaymentMethod.create({ paymentDescription: value })
),
"payment_method",
errors
);
const globalDiscountPercentage = extractOrPushError( const globalDiscountPercentage = extractOrPushError(
Percentage.create({ DiscountPercentage.create({
value: Number(dto.global_discount_percentage.value), value: NumberHelper.toSafeNumber(dto.global_discount_percentage.value),
scale: Number(dto.global_discount_percentage.scale),
}), }),
"discount_percentage", "global_discount_percentage",
errors errors
); );
const itemsProps = this.mapItemsProps(dto, { const paymentMethodId = extractOrPushError(
maybeFromNullableResult(dto.payment_method_id, (value) => UniqueID.create(value)),
"payment_method_id",
errors
);
const items = this.mapItemsProps(dto.items, {
languageCode: languageCode!, languageCode: languageCode!,
currencyCode: currencyCode!, currencyCode: currencyCode!,
globalDiscountPercentage: globalDiscountPercentage!, globalDiscountPercentage: globalDiscountPercentage!,
@ -163,17 +137,17 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
this.throwIfValidationErrors(errors); this.throwIfValidationErrors(errors);
const props: IProformaCreateProps = { const props: IProformaCreateProps = {
companyId, companyId: params.companyId,
status: defaultStatus, status: InvoiceStatus.draft(),
invoiceNumber: proformaNumber!, invoiceNumber: invoiceNumber!,
series: series!, series: series!,
invoiceDate: invoiceDate!, invoiceDate: invoiceDate!,
operationDate: operationDate!, operationDate: operationDate!,
customerId: customerId!, customerId: customerId!,
recipient, recipient: Maybe.none<InvoiceRecipient>(),
reference: reference!, reference: reference!,
description: description!, description: description!,
@ -182,10 +156,12 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
languageCode: languageCode!, languageCode: languageCode!,
currencyCode: currencyCode!, currencyCode: currencyCode!,
paymentMethod: paymentMethod!, linkedInvoiceId: Maybe.none(),
globalDiscountPercentage: globalDiscountPercentage!,
items: itemsProps, // ← IProformaItemProps[] paymentMethodId: paymentMethodId!,
globalDiscountPercentage: globalDiscountPercentage!,
items,
}; };
return Result.ok({ return Result.ok({
@ -193,18 +169,12 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
props, props,
}); });
} catch (err: unknown) { } catch (err: unknown) {
return Result.fail(new DomainError("Customer invoice props mapping failed", { cause: err })); return Result.fail(new DomainError("Proforma props mapping failed", { cause: err }));
}
}
private throwIfValidationErrors(errors: ValidationErrorDetail[]): void {
if (errors.length > 0) {
throw new ValidationErrorCollection("Customer proforma props mapping failed", errors);
} }
} }
private mapItemsProps( private mapItemsProps(
dto: CreateProformaRequestDTO, itemsDTO: CreateProformaRequestDTO["items"],
params: { params: {
languageCode: LanguageCode; languageCode: LanguageCode;
currencyCode: CurrencyCode; currencyCode: CurrencyCode;
@ -212,132 +182,78 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
errors: ValidationErrorDetail[]; errors: ValidationErrorDetail[];
} }
): IProformaItemCreateProps[] { ): IProformaItemCreateProps[] {
const itemsProps: IProformaItemCreateProps[] = []; return itemsDTO.map((item, index) => {
dto.items.forEach((item, index) => {
const description = extractOrPushError( const description = extractOrPushError(
maybeFromNullableResult(item.description, (v) => ItemDescription.create(v)), maybeFromNullableResult(item.description, (value) => ItemDescription.create(value)),
`items[${index}].description`, `items[${index}].description`,
params.errors params.errors
); );
const quantity = extractOrPushError( const quantity = extractOrPushError(
maybeFromNullableResult(item.quantity, (v) => maybeFromNullableResult(item.quantity, (value) =>
ItemQuantity.create({ value: NumberHelper.toSafeNumber(v) }) ItemQuantity.create({ value: NumberHelper.toSafeNumber(value) })
), ),
`items[${index}].quantity`, `items[${index}].quantity`,
params.errors params.errors
); );
const unitAmount = extractOrPushError( const unitAmount = extractOrPushError(
maybeFromNullableResult(item.unit_amount, (v) => maybeFromNullableResult(item.unit_amount, (value) =>
ItemAmount.create({ value: NumberHelper.toSafeNumber(v) }) ItemAmount.create({ value: NumberHelper.toSafeNumber(value) })
), ),
`items[${index}].unit_amount`, `items[${index}].unit_amount`,
params.errors params.errors
); );
const discountPercentage = extractOrPushError( const itemDiscountPercentage = extractOrPushError(
maybeFromNullableResult(item.item_discount_percentage, (v) => maybeFromNullableResult(item.item_discount_percentage, (value) =>
DiscountPercentage.create({ value: NumberHelper.toSafeNumber(v.value) }) DiscountPercentage.create({
value: NumberHelper.toSafeNumber(value.value),
})
), ),
`items[${index}].discount_percentage`, `items[${index}].item_discount_percentage`,
params.errors params.errors
); );
const taxes = this.mapTaxesProps(item.taxes, { return {
itemIndex: index, position: item.position,
errors: params.errors,
});
this.throwIfValidationErrors(params.errors);
itemsProps.push({
globalDiscountPercentage: params.globalDiscountPercentage,
languageCode: params.languageCode,
currencyCode: params.currencyCode,
description: description!, description: description!,
quantity: quantity!, quantity: quantity!,
unitAmount: unitAmount!, unitAmount: unitAmount!,
itemDiscountPercentage: discountPercentage!, itemDiscountPercentage: itemDiscountPercentage!,
taxes,
}); taxes: this.mapTaxesProps(item.taxes, {
itemIndex: index,
errors: params.errors,
}),
languageCode: params.languageCode,
currencyCode: params.currencyCode,
globalDiscountPercentage: params.globalDiscountPercentage,
};
}); });
return itemsProps;
} }
/* Devuelve las propiedades de los impustos de una línea de detalle */
private mapTaxesProps( private mapTaxesProps(
taxesDTO: NonNullable<CreateProformaRequestDTO["items"]>[number]["taxes"], taxesDTO: CreateProformaRequestDTO["items"][number]["taxes"],
params: { itemIndex: number; errors: ValidationErrorDetail[] } params: { itemIndex: number; errors: ValidationErrorDetail[] }
): ProformaItemTaxesProps { ): ProformaItemTaxesProps {
// TODO: POR AHORA SE QUEDA ASÍ if (taxesDTO === "#;#;#") {
return ProformaItemTaxes.empty().getProps();
}
return ProformaItemTaxes.empty().getProps(); params.errors.push({
path: `items[${params.itemIndex}].taxes`,
/*const { itemIndex, errors } = params; message: "Tax combination mapping is not implemented yet",
const taxesProps: ProformaItemTaxesProps = {
iva: Maybe.none(),
retention: Maybe.none(),
rec: Maybe.none(),
};
const taxStrCodes = taxesDTO
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0);
taxStrCodes.forEach((strCode, taxIndex) => {
const taxResult = Tax.createFromCode(strCode, this.taxCatalog);
if (!taxResult.isSuccess) {
errors.push({
path: `items[${itemIndex}].taxes[${taxIndex}]`,
message: taxResult.error.message,
});
return;
}
const tax = taxResult.data;
if (tax.isVATLike()) {
if (taxesProps.iva.isSome()) {
errors.push({
path: `items[${itemIndex}].taxes`,
message: "Multiple taxes for group VAT are not allowed",
});
}
taxesProps.iva = Maybe.some(tax);
}
if (tax.isRetention()) {
if (taxesProps.retention.isSome()) {
errors.push({
path: `items[${itemIndex}].taxes`,
message: "Multiple taxes for group retention are not allowed",
});
}
taxesProps.retention = Maybe.some(tax);
}
if (tax.isRec()) {
if (taxesProps.rec.isSome()) {
errors.push({
path: `items[${itemIndex}].taxes`,
message: "Multiple taxes for group rec are not allowed",
});
}
taxesProps.rec = Maybe.some(tax);
}
}); });
this.throwIfValidationErrors(errors); return ProformaItemTaxes.empty().getProps();
}
return taxesProps; private throwIfValidationErrors(errors: ValidationErrorDetail[]): void {
*/ if (errors.length > 0) {
throw new ValidationErrorCollection("Proforma props mapping failed", errors);
}
} }
} }

View File

@ -26,20 +26,6 @@ import {
type ProformaPatchProps, type ProformaPatchProps,
} from "../../../domain"; } from "../../../domain";
/**
* UpdateProformaPropsMapper
* Convierte el DTO a las props validadas (ProformaInvoiceProps).
* No construye directamente el agregado.
* Tri-estado:
* - campo omitido no se cambia
* - campo con valor null/"" se quita el valor -> set(None()),
* - campo con valor no-vacío se pone el nuevo valor -> set(Some(VO)).
*
* @param dto - DTO con los datos a cambiar en la factura de cliente
* @returns Cambios en las propiedades de la factura de cliente
*
*/
export interface IUpdateProformaInputMapper { export interface IUpdateProformaInputMapper {
map( map(
dto: UpdateProformaByIdRequestDTO, dto: UpdateProformaByIdRequestDTO,
@ -47,6 +33,20 @@ export interface IUpdateProformaInputMapper {
): Result<ProformaPatchProps>; ): Result<ProformaPatchProps>;
} }
/**
* @summary Convierte el DTO de update de proforma en props de dominio.
* @remarks
* Respeta semántica PATCH en cabecera:
* - omitido: no modificar
* - null: limpiar valor cuando el campo lo permite
* - valor: asignar nuevo valor
*
* Para `items`, no aplica patch granular:
* - undefined: no tocar líneas
* - []: borrar todas las líneas
* - [...]: reemplazar colección completa
*/
export class UpdateProformaInputMapper implements IUpdateProformaInputMapper { export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
public map( public map(
dto: UpdateProformaByIdRequestDTO, dto: UpdateProformaByIdRequestDTO,
@ -54,46 +54,58 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
): Result<ProformaPatchProps> { ): Result<ProformaPatchProps> {
try { try {
const errors: ValidationErrorDetail[] = []; const errors: ValidationErrorDetail[] = [];
const props: ProformaPatchProps = {}; const proformaPatchProps: ProformaPatchProps = {};
toPatchField(dto.series).ifSet((series) => { toPatchField(dto.series).ifSet((series) => {
props.series = extractOrPushError( proformaPatchProps.series = extractOrPushError(
maybeFromNullableResult(series, (value) => InvoiceSerie.create(value)), maybeFromNullableResult(series, (value) => InvoiceSerie.create(value)),
"reference", "series",
errors errors
); );
}); });
toPatchField(dto.invoice_date).ifSet((invoice_date) => { toPatchField(dto.invoice_date).ifSet((invoiceDate) => {
if (isNullishOrEmpty(invoice_date)) { if (isNullishOrEmpty(invoiceDate)) {
errors.push({ path: "invoice_date", message: "Invoice date cannot be empty" }); errors.push({
path: "invoice_date",
message: "Invoice date cannot be empty",
});
return; return;
} }
props.invoiceDate = extractOrPushError(
UtcDate.createFromISO(invoice_date!), proformaPatchProps.invoiceDate = extractOrPushError(
UtcDate.createFromISO(invoiceDate),
"invoice_date", "invoice_date",
errors errors
); );
}); });
toPatchField(dto.operation_date).ifSet((operation_date) => { toPatchField(dto.operation_date).ifSet((operationDate) => {
props.operationDate = extractOrPushError( proformaPatchProps.operationDate = extractOrPushError(
maybeFromNullableResult(operation_date, (value) => UtcDate.createFromISO(value)), maybeFromNullableResult(operationDate, (value) => UtcDate.createFromISO(value)),
"operation_date", "operation_date",
errors errors
); );
}); });
toPatchField(dto.customer_id).ifSet((customer_id) => { toPatchField(dto.customer_id).ifSet((customerId) => {
if (isNullishOrEmpty(customer_id)) { if (isNullishOrEmpty(customerId)) {
errors.push({ path: "customer_id", message: "Proforma cannot be empty" }); errors.push({
path: "customer_id",
message: "Customer id cannot be empty",
});
return; return;
} }
props.customerId = extractOrPushError(UniqueID.create(customer_id!), "customer_id", errors);
proformaPatchProps.customerId = extractOrPushError(
UniqueID.create(customerId),
"customer_id",
errors
);
}); });
toPatchField(dto.reference).ifSet((reference) => { toPatchField(dto.reference).ifSet((reference) => {
props.reference = extractOrPushError( proformaPatchProps.reference = extractOrPushError(
maybeFromNullableResult(reference, (value) => Result.ok(String(value))), maybeFromNullableResult(reference, (value) => Result.ok(String(value))),
"reference", "reference",
errors errors
@ -101,7 +113,7 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
}); });
toPatchField(dto.description).ifSet((description) => { toPatchField(dto.description).ifSet((description) => {
props.description = extractOrPushError( proformaPatchProps.description = extractOrPushError(
maybeFromNullableResult(description, (value) => Result.ok(String(value))), maybeFromNullableResult(description, (value) => Result.ok(String(value))),
"description", "description",
errors errors
@ -109,7 +121,7 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
}); });
toPatchField(dto.notes).ifSet((notes) => { toPatchField(dto.notes).ifSet((notes) => {
props.notes = extractOrPushError( proformaPatchProps.notes = extractOrPushError(
maybeFromNullableResult(notes, (value) => TextValue.create(value)), maybeFromNullableResult(notes, (value) => TextValue.create(value)),
"notes", "notes",
errors errors
@ -118,12 +130,15 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
toPatchField(dto.language_code).ifSet((languageCode) => { toPatchField(dto.language_code).ifSet((languageCode) => {
if (isNullishOrEmpty(languageCode)) { if (isNullishOrEmpty(languageCode)) {
errors.push({ path: "language_code", message: "Language code cannot be empty" }); errors.push({
path: "language_code",
message: "Language code cannot be empty",
});
return; return;
} }
props.languageCode = extractOrPushError( proformaPatchProps.languageCode = extractOrPushError(
LanguageCode.create(languageCode!), LanguageCode.create(languageCode),
"language_code", "language_code",
errors errors
); );
@ -131,72 +146,84 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
toPatchField(dto.currency_code).ifSet((currencyCode) => { toPatchField(dto.currency_code).ifSet((currencyCode) => {
if (isNullishOrEmpty(currencyCode)) { if (isNullishOrEmpty(currencyCode)) {
errors.push({ path: "currency_code", message: "Currency code cannot be empty" }); errors.push({
path: "currency_code",
message: "Currency code cannot be empty",
});
return; return;
} }
props.currencyCode = extractOrPushError( proformaPatchProps.currencyCode = extractOrPushError(
CurrencyCode.create(currencyCode!), CurrencyCode.create(currencyCode),
"currency_code", "currency_code",
errors errors
); );
}); });
if (dto.items) { toPatchField(dto.global_discount_percentage).ifSet((globalDiscountPercentage) => {
const itemsProps = this.mapItemsProps(dto, { errors }); proformaPatchProps.globalDiscountPercentage = extractOrPushError(
props.items = itemsProps; DiscountPercentage.create({
value: NumberHelper.toSafeNumber(globalDiscountPercentage.value),
}),
"global_discount_percentage",
errors
);
});
toPatchField(dto.payment_method_id).ifSet((paymentMethodId) => {
proformaPatchProps.paymentMethodId = extractOrPushError(
maybeFromNullableResult(paymentMethodId, (value) => UniqueID.create(value)),
"payment_method_id",
errors
);
});
if (dto.items !== undefined) {
proformaPatchProps.items = this.mapItemsProps(dto.items, { errors });
} }
this.throwIfValidationErrors(errors); this.throwIfValidationErrors(errors);
return Result.ok(props); return Result.ok(proformaPatchProps);
} catch (err: unknown) { } catch (err: unknown) {
return Result.fail(new DomainError("Proforma proforma props mapping failed", { cause: err })); return Result.fail(new DomainError("Proforma props mapping failed", { cause: err }));
}
}
private throwIfValidationErrors(errors: ValidationErrorDetail[]): void {
if (errors.length > 0) {
throw new ValidationErrorCollection("Customer proforma props mapping failed", errors);
} }
} }
private mapItemsProps( private mapItemsProps(
dto: UpdateProformaByIdRequestDTO, itemsDTO: NonNullable<UpdateProformaByIdRequestDTO["items"]>,
params: { params: { errors: ValidationErrorDetail[] }
errors: ValidationErrorDetail[];
}
): ProformaItemPatchProps[] { ): ProformaItemPatchProps[] {
const itemsProps: ProformaItemPatchProps[] = []; return itemsDTO.map((item, index) => {
dto.items?.forEach((item, index) => {
const description = extractOrPushError( const description = extractOrPushError(
maybeFromNullableResult(item.description, (v) => ItemDescription.create(v)), maybeFromNullableResult(item.description, (value) => ItemDescription.create(value)),
`items[${index}].description`, `items[${index}].description`,
params.errors params.errors
); );
const quantity = extractOrPushError( const quantity = extractOrPushError(
maybeFromNullableResult(item.quantity, (v) => maybeFromNullableResult(item.quantity, (value) =>
ItemQuantity.create({ value: NumberHelper.toSafeNumber(v) }) ItemQuantity.create({ value: NumberHelper.toSafeNumber(value) })
), ),
`items[${index}].quantity`, `items[${index}].quantity`,
params.errors params.errors
); );
const unitAmount = extractOrPushError( const unitAmount = extractOrPushError(
maybeFromNullableResult(item.unit_amount, (v) => maybeFromNullableResult(item.unit_amount, (value) =>
ItemAmount.create({ value: NumberHelper.toSafeNumber(v) }) ItemAmount.create({ value: NumberHelper.toSafeNumber(value) })
), ),
`items[${index}].unit_amount`, `items[${index}].unit_amount`,
params.errors params.errors
); );
const discountPercentage = extractOrPushError( const itemDiscountPercentage = extractOrPushError(
maybeFromNullableResult(item.item_discount_percentage, (v) => maybeFromNullableResult(item.item_discount_percentage, (value) =>
DiscountPercentage.create({ value: NumberHelper.toSafeNumber(v.value) }) DiscountPercentage.create({
value: NumberHelper.toSafeNumber(value.value),
})
), ),
`items[${index}].discount_percentage`, `items[${index}].item_discount_percentage`,
params.errors params.errors
); );
@ -205,89 +232,44 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
errors: params.errors, errors: params.errors,
}); });
this.throwIfValidationErrors(params.errors); return {
position: item.position,
itemsProps.push({
description: description!, description: description!,
quantity: quantity!, quantity: quantity!,
unitAmount: unitAmount!, unitAmount: unitAmount!,
itemDiscountPercentage: discountPercentage!, itemDiscountPercentage: itemDiscountPercentage!,
taxes, taxes,
}); };
}); });
return itemsProps;
} }
/* Devuelve las propiedades de los impuestos de una línea de detalle */
private mapTaxesProps( private mapTaxesProps(
taxesDTO: NonNullable<UpdateProformaByIdRequestDTO["items"]>[number]["taxes"], taxesDTO: NonNullable<UpdateProformaByIdRequestDTO["items"]>[number]["taxes"],
params: { itemIndex: number; errors: ValidationErrorDetail[] } params: { itemIndex: number; errors: ValidationErrorDetail[] }
): ProformaItemTaxesProps { ): ProformaItemTaxesProps {
// TODO: POR AHORA SE QUEDA ASÍ if (taxesDTO === "#;#;#") {
return ProformaItemTaxes.empty().getProps();
}
return ProformaItemTaxes.empty().getProps(); /**
* Pendiente: resolver códigos contra catálogo fiscal.
/*const { itemIndex, errors } = params; *
* taxesDTO llega como:
const taxesProps: ProformaItemTaxesProps = { * - iva_21;#;retention_10
iva: Maybe.none(), * - iva_10;rec_5_2;#
retention: Maybe.none(), * - #;#;#
rec: Maybe.none(), */
}; params.errors.push({
path: `items[${params.itemIndex}].taxes`,
const taxStrCodes = taxesDTO message: "Tax combination mapping is not implemented yet",
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0);
taxStrCodes.forEach((strCode, taxIndex) => {
const taxResult = Tax.createFromCode(strCode, this.taxCatalog);
if (!taxResult.isSuccess) {
errors.push({
path: `items[${itemIndex}].taxes[${taxIndex}]`,
message: taxResult.error.message,
});
return;
}
const tax = taxResult.data;
if (tax.isVATLike()) {
if (taxesProps.iva.isSome()) {
errors.push({
path: `items[${itemIndex}].taxes`,
message: "Multiple taxes for group VAT are not allowed",
});
}
taxesProps.iva = Maybe.some(tax);
}
if (tax.isRetention()) {
if (taxesProps.retention.isSome()) {
errors.push({
path: `items[${itemIndex}].taxes`,
message: "Multiple taxes for group retention are not allowed",
});
}
taxesProps.retention = Maybe.some(tax);
}
if (tax.isRec()) {
if (taxesProps.rec.isSome()) {
errors.push({
path: `items[${itemIndex}].taxes`,
message: "Multiple taxes for group rec are not allowed",
});
}
taxesProps.rec = Maybe.some(tax);
}
}); });
this.throwIfValidationErrors(errors); return ProformaItemTaxes.empty().getProps();
}
return taxesProps;*/ private throwIfValidationErrors(errors: ValidationErrorDetail[]): void {
if (errors.length > 0) {
throw new ValidationErrorCollection("Proforma props mapping failed", errors);
}
} }
} }

View File

@ -1,8 +1,4 @@
export * from "./proforma-full-snapshot.interface";
export * from "./proforma-full-snapshot-builder"; export * from "./proforma-full-snapshot-builder";
export * from "./proforma-item-full-snapshot.interface";
export * from "./proforma-items-full-snapshot-builder"; export * from "./proforma-items-full-snapshot-builder";
export * from "./proforma-recipient-full-snapshot.interface";
export * from "./proforma-recipient-full-snapshot-builder"; export * from "./proforma-recipient-full-snapshot-builder";
export * from "./proforma-tax-full-snapshot-interface";
export * from "./proforma-taxes-full-snapshot-builder"; export * from "./proforma-taxes-full-snapshot-builder";

View File

@ -1,15 +1,15 @@
import type { ISnapshotBuilder } from "@erp/core/api"; import type { ISnapshotBuilder } from "@erp/core/api";
import { maybeToNullable } from "@repo/rdx-ddd"; import { maybeToNullable } from "@repo/rdx-ddd";
import type { GetProformaByIdResponseDTO } from "../../../../../common";
import type { Proforma } from "../../../../domain"; import type { Proforma } from "../../../../domain";
import type { IProformaFullSnapshot } from "./proforma-full-snapshot.interface";
import type { IProformaItemsFullSnapshotBuilder } from "./proforma-items-full-snapshot-builder"; import type { IProformaItemsFullSnapshotBuilder } from "./proforma-items-full-snapshot-builder";
import type { IProformaRecipientFullSnapshotBuilder } from "./proforma-recipient-full-snapshot-builder"; import type { IProformaRecipientFullSnapshotBuilder } from "./proforma-recipient-full-snapshot-builder";
import type { IProformaTaxesFullSnapshotBuilder } from "./proforma-taxes-full-snapshot-builder"; import type { IProformaTaxesFullSnapshotBuilder } from "./proforma-taxes-full-snapshot-builder";
export interface IProformaFullSnapshotBuilder export interface IProformaFullSnapshotBuilder
extends ISnapshotBuilder<Proforma, IProformaFullSnapshot> {} extends ISnapshotBuilder<Proforma, GetProformaByIdResponseDTO> {}
export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder { export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder {
constructor( constructor(
@ -18,7 +18,7 @@ export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder
private readonly taxesBuilder: IProformaTaxesFullSnapshotBuilder private readonly taxesBuilder: IProformaTaxesFullSnapshotBuilder
) {} ) {}
toOutput(proforma: Proforma): IProformaFullSnapshot { toOutput(proforma: Proforma): GetProformaByIdResponseDTO {
const items = this.itemsBuilder.toOutput(proforma.items); const items = this.itemsBuilder.toOutput(proforma.items);
const recipient = this.recipientBuilder.toOutput(proforma); const recipient = this.recipientBuilder.toOutput(proforma);
const taxes = this.taxesBuilder.toOutput(proforma.taxes()); const taxes = this.taxesBuilder.toOutput(proforma.taxes());
@ -27,8 +27,8 @@ export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder
(payment) => { (payment) => {
const { id, payment_description } = payment.toObjectString(); const { id, payment_description } = payment.toObjectString();
return { return {
payment_id: id, id: id,
payment_description, description: payment_description,
}; };
}, },
() => null () => null
@ -40,9 +40,8 @@ export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder
id: proforma.id.toString(), id: proforma.id.toString(),
company_id: proforma.companyId.toString(), company_id: proforma.companyId.toString(),
is_proforma: true,
invoice_number: proforma.invoiceNumber.toString(), invoice_number: proforma.invoiceNumber.toString(),
status: proforma.status.toPrimitive(), status: proforma.status.toPrimitive() as GetProformaByIdResponseDTO["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,60 +0,0 @@
import type { IProformaItemFullSnapshot } from "./proforma-item-full-snapshot.interface";
import type { IProformaRecipientFullSnapshot } from "./proforma-recipient-full-snapshot.interface";
import type { IProformaTaxFullSnapshot } from "./proforma-tax-full-snapshot-interface";
/**
* Fijarse en GetProformaByIdResponseDTO
*/
export interface IProformaFullSnapshot {
id: string;
company_id: string;
is_proforma: boolean;
invoice_number: string;
status: string;
series: string | null;
invoice_date: string;
operation_date: string | null;
reference: string | null;
description: string | null;
notes: string | null;
language_code: string;
currency_code: string;
customer_id: string;
recipient: IProformaRecipientFullSnapshot;
linked_invoice_id: string | null;
taxes: IProformaTaxFullSnapshot[];
payment_method: {
payment_id: string;
payment_description: string;
} | null;
subtotal_amount: { value: string; scale: string; currency_code: string };
items_discount_amount: { value: string; scale: string; currency_code: string };
global_discount_percentage: { value: string; scale: string };
global_discount_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 };
iva_amount: { value: string; scale: string; currency_code: string };
rec_amount: { value: string; scale: string; currency_code: string };
retention_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 };
items: IProformaItemFullSnapshot[];
metadata: Record<string, string> | null;
}

View File

@ -1,36 +0,0 @@
export interface IProformaItemFullSnapshot {
id: string;
is_valued: boolean;
position: number;
description: string | null;
quantity: { value: string; scale: string };
unit_amount: { value: string; scale: string; currency_code: string };
subtotal_amount: { value: string; scale: string; currency_code: string };
item_discount_percentage: { value: string; scale: string };
item_discount_amount: { value: string; scale: string; currency_code: string };
global_discount_percentage: { value: string; scale: string };
global_discount_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 };
iva_code: string;
iva_percentage: { value: string; scale: string };
iva_amount: { value: string; scale: string; currency_code: string };
rec_code: string;
rec_percentage: { value: string; scale: string };
rec_amount: { value: string; scale: string; currency_code: string };
retention_code: string;
retention_percentage: { value: string; scale: string };
retention_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 };
}

View File

@ -1,4 +1,5 @@
import type { ISnapshotBuilder } from "@erp/core/api"; import type { ISnapshotBuilder } from "@erp/core/api";
import type { ProformaItemDetailDTO } from "@erp/customer-invoices/common";
import { import {
maybeToEmptyMoneyObjectString, maybeToEmptyMoneyObjectString,
maybeToEmptyPercentageObjectString, maybeToEmptyPercentageObjectString,
@ -9,13 +10,11 @@ import {
import { ItemAmount, type ProformaItem, type ProformaItems } from "../../../../domain"; import { ItemAmount, type ProformaItem, type ProformaItems } from "../../../../domain";
import type { IProformaItemFullSnapshot } from "./proforma-item-full-snapshot.interface";
export interface IProformaItemsFullSnapshotBuilder export interface IProformaItemsFullSnapshotBuilder
extends ISnapshotBuilder<ProformaItems, IProformaItemFullSnapshot[]> {} extends ISnapshotBuilder<ProformaItems, ProformaItemDetailDTO[]> {}
export class ProformaItemsFullSnapshotBuilder implements IProformaItemsFullSnapshotBuilder { export class ProformaItemsFullSnapshotBuilder implements IProformaItemsFullSnapshotBuilder {
private mapItem(proformaItem: ProformaItem, index: number): IProformaItemFullSnapshot { private mapItem(proformaItem: ProformaItem, index: number): ProformaItemDetailDTO {
const allAmounts = proformaItem.totals(); const allAmounts = proformaItem.totals();
const isValued = proformaItem.isValued(); const isValued = proformaItem.isValued();
@ -77,7 +76,7 @@ export class ProformaItemsFullSnapshotBuilder implements IProformaItemsFullSnaps
}; };
} }
toOutput(invoiceItems: ProformaItems): IProformaItemFullSnapshot[] { toOutput(invoiceItems: ProformaItems): ProformaItemDetailDTO[] {
return invoiceItems.map((item, index) => this.mapItem(item, index)); return invoiceItems.map((item, index) => this.mapItem(item, index));
} }
} }

View File

@ -1,15 +1,14 @@
import type { ISnapshotBuilder } from "@erp/core/api"; import type { ISnapshotBuilder } from "@erp/core/api";
import { DomainValidationError, maybeToNullable } from "@repo/rdx-ddd"; import { DomainValidationError, maybeToNullable } from "@repo/rdx-ddd";
import type { ProformaRecipientSummaryDTO } from "../../../../../common";
import type { InvoiceRecipient, Proforma } from "../../../../domain"; import type { InvoiceRecipient, Proforma } from "../../../../domain";
import type { IProformaRecipientFullSnapshot } from "./proforma-recipient-full-snapshot.interface";
export interface IProformaRecipientFullSnapshotBuilder export interface IProformaRecipientFullSnapshotBuilder
extends ISnapshotBuilder<Proforma, IProformaRecipientFullSnapshot> {} extends ISnapshotBuilder<Proforma, ProformaRecipientSummaryDTO> {}
export class ProformaRecipientFullSnapshotBuilder implements IProformaRecipientFullSnapshotBuilder { export class ProformaRecipientFullSnapshotBuilder implements IProformaRecipientFullSnapshotBuilder {
toOutput(proforma: Proforma): IProformaRecipientFullSnapshot { toOutput(proforma: Proforma): ProformaRecipientSummaryDTO {
if (!proforma.recipient) { if (!proforma.recipient) {
throw DomainValidationError.requiredValue("recipient", { throw DomainValidationError.requiredValue("recipient", {
cause: proforma, cause: proforma,
@ -39,7 +38,7 @@ export class ProformaRecipientFullSnapshotBuilder implements IProformaRecipientF
province: null, province: null,
postal_code: null, postal_code: null,
country: null, country: null,
}) as IProformaRecipientFullSnapshot }) as ProformaRecipientSummaryDTO
); );
} }
} }

View File

@ -1,15 +0,0 @@
/**
* Fijarse en ProformaRecipientSummarySchema
*/
export interface IProformaRecipientFullSnapshot {
id: string | null;
name: string | null;
tin: string | null;
street: string | null;
street2: string | null;
city: string | null;
province: string | null;
postal_code: string | null;
country: string | null;
}

View File

@ -1,17 +0,0 @@
export interface IProformaTaxFullSnapshot {
taxable_amount: { value: string; scale: string; currency_code: string };
iva_code: string;
iva_percentage: { value: string; scale: string };
iva_amount: { value: string; scale: string; currency_code: string };
rec_code: string;
rec_percentage: { value: string; scale: string };
rec_amount: { value: string; scale: string; currency_code: string };
retention_code: string;
retention_percentage: { value: string; scale: string };
retention_amount: { value: string; scale: string; currency_code: string };
taxes_amount: { value: string; scale: string; currency_code: string };
}

View File

@ -2,15 +2,14 @@ import type { ISnapshotBuilder } from "@erp/core/api";
import { maybeToEmptyPercentageObjectString, maybeToEmptyString } from "@repo/rdx-ddd"; import { maybeToEmptyPercentageObjectString, maybeToEmptyString } from "@repo/rdx-ddd";
import type { Collection } from "@repo/rdx-utils"; import type { Collection } from "@repo/rdx-utils";
import type { TaxesBreakdownDTO } from "../../../../../common";
import type { IProformaTaxTotals } from "../../../../domain"; import type { IProformaTaxTotals } from "../../../../domain";
import type { IProformaTaxFullSnapshot } from "./proforma-tax-full-snapshot-interface";
export interface IProformaTaxesFullSnapshotBuilder export interface IProformaTaxesFullSnapshotBuilder
extends ISnapshotBuilder<Collection<IProformaTaxTotals>, IProformaTaxFullSnapshot[]> {} extends ISnapshotBuilder<Collection<IProformaTaxTotals>, TaxesBreakdownDTO[]> {}
export class ProformaTaxesFullSnapshotBuilder implements IProformaTaxesFullSnapshotBuilder { export class ProformaTaxesFullSnapshotBuilder implements IProformaTaxesFullSnapshotBuilder {
private mapItem(proformaTax: IProformaTaxTotals, index: number): IProformaTaxFullSnapshot { private mapItem(proformaTax: IProformaTaxTotals, index: number): TaxesBreakdownDTO {
return { return {
taxable_amount: proformaTax.taxableAmount.toObjectString(), taxable_amount: proformaTax.taxableAmount.toObjectString(),
@ -30,7 +29,7 @@ export class ProformaTaxesFullSnapshotBuilder implements IProformaTaxesFullSnaps
}; };
} }
toOutput(invoiceTaxes: Collection<IProformaTaxTotals>): IProformaTaxFullSnapshot[] { toOutput(invoiceTaxes: Collection<IProformaTaxTotals>): TaxesBreakdownDTO[] {
return invoiceTaxes.map((item, index) => this.mapItem(item, index)); return invoiceTaxes.map((item, index) => this.mapItem(item, index));
} }
} }

View File

@ -14,7 +14,6 @@ export class ProformaSummarySnapshotBuilder implements IProformaSummarySnapshotB
return { return {
id: proforma.id.toString(), id: proforma.id.toString(),
company_id: proforma.companyId.toString(), company_id: proforma.companyId.toString(),
is_proforma: proforma.isProforma,
invoice_number: proforma.invoiceNumber.toString(), invoice_number: proforma.invoiceNumber.toString(),
status: proforma.status.toPrimitive() as ProformaSummaryDTO["status"], status: proforma.status.toPrimitive() as ProformaSummaryDTO["status"],

View File

@ -4,7 +4,7 @@ import { Result } from "@repo/rdx-utils";
import type { UpdateProformaByIdRequestDTO } from "../../../../common"; import type { UpdateProformaByIdRequestDTO } from "../../../../common";
import type { ProformaPatchProps } from "../../../domain"; import type { ProformaPatchProps } from "../../../domain";
import type { UpdateProformaInputMapper } from "../mappers"; import type { IUpdateProformaInputMapper } from "../mappers";
import type { IProformaUpdater } from "../services"; import type { IProformaUpdater } from "../services";
import type { IProformaFullSnapshotBuilder } from "../snapshot-builders"; import type { IProformaFullSnapshotBuilder } from "../snapshot-builders";
@ -15,14 +15,14 @@ type UpdateProformaUseCaseInput = {
}; };
type UpdateProformaUseCaseDeps = { type UpdateProformaUseCaseDeps = {
dtoMapper: UpdateProformaInputMapper; dtoMapper: IUpdateProformaInputMapper;
updater: IProformaUpdater; updater: IProformaUpdater;
fullSnapshotBuilder: IProformaFullSnapshotBuilder; fullSnapshotBuilder: IProformaFullSnapshotBuilder;
transactionManager: ITransactionManager; transactionManager: ITransactionManager;
}; };
export class UpdateProformaUseCase { export class UpdateProformaUseCase {
private readonly dtoMapper: UpdateProformaInputMapper; private readonly dtoMapper: IUpdateProformaInputMapper;
private readonly updater: IProformaUpdater; private readonly updater: IProformaUpdater;
private readonly fullSnapshotBuilder: IProformaFullSnapshotBuilder; private readonly fullSnapshotBuilder: IProformaFullSnapshotBuilder;
private readonly transactionManager: ITransactionManager; private readonly transactionManager: ITransactionManager;

View File

@ -14,7 +14,7 @@ export class IssuedInvoiceReportPresenter extends Presenter<
return ""; return "";
} }
return paymentMethod.payment_description ?? ""; return paymentMethod.description ?? "";
} }
toOutput(issuedInvoiceDTO: GetIssuedInvoiceByIdResponseDTO) { toOutput(issuedInvoiceDTO: GetIssuedInvoiceByIdResponseDTO) {

View File

@ -9,7 +9,7 @@ export class ProformaReportPresenter extends Presenter<GetProformaByIdResponseDT
return ""; return "";
} }
return paymentMethod.payment_description ?? ""; return paymentMethod.description ?? "";
} }
toOutput(proformaDTO: GetProformaByIdResponseDTO) { toOutput(proformaDTO: GetProformaByIdResponseDTO) {

View File

@ -11,7 +11,6 @@ import {
} from "@repo/rdx-ddd"; } from "@repo/rdx-ddd";
import { type Collection, type Maybe, Result } from "@repo/rdx-utils"; import { type Collection, type Maybe, Result } from "@repo/rdx-utils";
import type { InvoicePaymentMethod } from "../../common/entities";
import { import {
InvoiceAmount, InvoiceAmount,
type InvoiceNumber, type InvoiceNumber,
@ -53,14 +52,14 @@ export interface IProformaCreateProps {
linkedInvoiceId: Maybe<UniqueID>; linkedInvoiceId: Maybe<UniqueID>;
paymentMethod: Maybe<InvoicePaymentMethod>; paymentMethodId: Maybe<UniqueID>;
items: IProformaItemCreateProps[]; items: IProformaItemCreateProps[];
globalDiscountPercentage: DiscountPercentage; globalDiscountPercentage: DiscountPercentage;
} }
export type ProformaPatchProps = Partial<Omit<IProformaCreateProps, "companyId" | "items">> & { export type ProformaPatchProps = Partial<Omit<IProformaCreateProps, "companyId" | "items">> & {
items?: ProformaItemPatchProps[]; items?: ProformaItemPatchProps[]; // update no es patch granular, sino reemplazo completo si se proporciona
}; };
export interface IProformaTotals { export interface IProformaTotals {
@ -100,7 +99,7 @@ export interface IProforma {
languageCode: LanguageCode; languageCode: LanguageCode;
currencyCode: CurrencyCode; currencyCode: CurrencyCode;
paymentMethod: Maybe<InvoicePaymentMethod>; paymentMethodId: Maybe<UniqueID>;
linkedInvoiceId: Maybe<UniqueID>; linkedInvoiceId: Maybe<UniqueID>;
@ -176,8 +175,12 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
Object.assign(this.props, candidateProps); Object.assign(this.props, candidateProps);
// Reemplazo de items (si se proporciona) // Reemplazo de items (si se proporciona)
if (items) { if (items !== undefined) {
this.initializeItems(items); const initializeResult = this.initializeItems(items);
if (initializeResult.isFailure) {
return Result.fail(initializeResult.error);
}
} }
return Result.ok(); return Result.ok();
@ -189,18 +192,11 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
this._items.reset(); this._items.reset();
for (const [index, itemProps] of itemsProps.entries()) { for (const [index, itemProps] of itemsProps.entries()) {
const { languageCode, currencyCode, globalDiscountPercentage, ...restProps } = { const itemResult = ProformaItem.create({
...itemProps,
languageCode: this.languageCode, languageCode: this.languageCode,
currencyCode: this.currencyCode, currencyCode: this.currencyCode,
globalDiscountPercentage: this.globalDiscountPercentage, globalDiscountPercentage: this.globalDiscountPercentage,
...itemProps,
};
const itemResult = ProformaItem.create({
...restProps,
languageCode,
currencyCode,
globalDiscountPercentage,
}); });
if (itemResult.isFailure) { if (itemResult.isFailure) {
@ -261,8 +257,8 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
return this.props.recipient; return this.props.recipient;
} }
public get paymentMethod(): Maybe<InvoicePaymentMethod> { public get paymentMethodId(): Maybe<UniqueID> {
return this.props.paymentMethod; return this.props.paymentMethodId;
} }
public get linkedInvoiceId(): Maybe<UniqueID> { public get linkedInvoiceId(): Maybe<UniqueID> {
@ -290,7 +286,7 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
} }
public get hasPaymentMethod() { public get hasPaymentMethod() {
return this.paymentMethod.isSome(); return this.paymentMethodId.isSome();
} }
public issue(): Result<void, Error> { public issue(): Result<void, Error> {
@ -355,7 +351,7 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
); );
}*/ }*/
if (this.paymentMethod.isNone()) { if (this.paymentMethodId.isNone()) {
return Result.fail( return Result.fail(
new DomainValidationError( new DomainValidationError(
"MISSING_PAYMENT_METHOD", "MISSING_PAYMENT_METHOD",

View File

@ -1,44 +1,68 @@
import { NumericStringSchema, PercentageSchema } from "@erp/core"; import {
CurrencyCodeSchema,
IsoDateSchema,
LanguageCodeSchema,
NumericStringSchema,
PercentageSchema,
} from "@erp/core";
import { z } from "zod/v4"; import { z } from "zod/v4";
export const CreateProformaItemRequestSchema = z.object({ import { ItemPositionSchema, TaxCombinationCodeSchema } from "../../shared";
id: z.uuid(),
position: z.string(), export const CreateProformaItemRequestSchema = z
description: z.string().default(""), .object({
quantity: NumericStringSchema.default(""), position: ItemPositionSchema,
unit_amount: NumericStringSchema.default(""),
item_discount_percentage: PercentageSchema.default({ is_valued: z.boolean(),
value: "0", description: z.string().nullable(),
scale: "2",
}), quantity: NumericStringSchema.nullable(),
taxes: z.string().default(""), unit_amount: NumericStringSchema.nullable(),
});
item_discount_percentage: PercentageSchema.nullable(),
taxes: TaxCombinationCodeSchema,
})
.refine(
(item) => {
if (!item.is_valued) {
return item.quantity === null && item.unit_amount === null;
}
return item.quantity !== null && item.unit_amount !== null;
},
{
message:
"quantity and unit_amount must be null when is_valued is false and non-null when is_valued is true",
path: ["is_valued"],
}
);
export type CreateProformaItemRequestDTO = z.infer<typeof CreateProformaItemRequestSchema>; export type CreateProformaItemRequestDTO = z.infer<typeof CreateProformaItemRequestSchema>;
export const CreateProformaRequestSchema = z.object({ export const CreateProformaRequestSchema = z.object({
id: z.uuid(), id: z.uuid(),
invoice_number: z.string(), invoice_number: z.string(),
series: z.string().default(""), series: z.string().nullable(),
invoice_date: z.string(), invoice_date: IsoDateSchema,
operation_date: z.string().default(""), operation_date: IsoDateSchema.nullable().optional(),
customer_id: z.uuid(), customer_id: z.uuid(),
reference: z.string().default(""), reference: z.string().nullable().optional(),
notes: z.string().default(""), description: z.string().nullable().optional(),
notes: z.string().nullable().optional(),
language_code: z.string().toLowerCase().default("es"), language_code: LanguageCodeSchema,
currency_code: z.string().toUpperCase().default("EUR"), currency_code: CurrencyCodeSchema,
global_discount_percentage: PercentageSchema.default({ global_discount_percentage: PercentageSchema,
value: "0",
scale: "2",
}),
payment_method: z.string().default(""), payment_method_id: z.uuid().nullable().optional(),
items: z.array(CreateProformaItemRequestSchema).default([]), items: z.array(CreateProformaItemRequestSchema),
}); });
export type CreateProformaRequestDTO = z.infer<typeof CreateProformaRequestSchema>; export type CreateProformaRequestDTO = z.infer<typeof CreateProformaRequestSchema>;

View File

@ -1,39 +1,71 @@
import { NumericStringSchema, PercentageSchema } from "@erp/core"; import {
CurrencyCodeSchema,
IsoDateSchema,
LanguageCodeSchema,
NumericStringSchema,
PercentageSchema,
} from "@erp/core";
import { z } from "zod/v4"; import { z } from "zod/v4";
export const UpdateProformaItemRequestSchema = z.object({ import { ItemPositionSchema, TaxCombinationCodeSchema } from "../../shared";
id: z.uuid(),
position: z.string(), export const UpdateProformaItemRequestSchema = z
description: z.string().default(""), .object({
quantity: NumericStringSchema.default(""), position: ItemPositionSchema,
unit_amount: NumericStringSchema.default(""), is_valued: z.boolean(),
item_discount_percentage: PercentageSchema.default({
value: "0", description: z.string().nullable(),
scale: "2",
}), quantity: NumericStringSchema.nullable(),
taxes: z.string().default(""), unit_amount: NumericStringSchema.nullable(),
});
item_discount_percentage: PercentageSchema.nullable(),
taxes: TaxCombinationCodeSchema,
})
.refine(
(item) => {
if (!item.is_valued) {
return item.quantity === null && item.unit_amount === null;
}
return item.quantity !== null && item.unit_amount !== null;
},
{
message:
"quantity and unit_amount must be null when is_valued is false and non-null when is_valued is true",
path: ["is_valued"],
}
);
export const UpdateProformaByIdParamsRequestSchema = z.object({ export const UpdateProformaByIdParamsRequestSchema = z.object({
proforma_id: z.string(), proforma_id: z.uuid(),
}); });
export const UpdateProformaByIdRequestSchema = z.object({ export const UpdateProformaByIdRequestSchema = z.object({
series: z.string().optional(), series: z.string().nullable().optional(),
invoice_date: z.string().optional(), invoice_date: IsoDateSchema.optional(),
operation_date: z.string().optional(), operation_date: IsoDateSchema.nullable().optional(),
customer_id: z.uuid().optional(), customer_id: z.uuid().optional(),
reference: z.string().optional(), reference: z.string().nullable().optional(),
description: z.string().optional(), description: z.string().nullable().optional(),
notes: z.string().optional(), notes: z.string().nullable().optional(),
language_code: z.string().optional(), language_code: LanguageCodeSchema.optional(),
currency_code: z.string().optional(), currency_code: CurrencyCodeSchema.optional(),
items: z.array(UpdateProformaItemRequestSchema).default([]), global_discount_percentage: PercentageSchema.optional(),
payment_method_id: z.uuid().nullable().optional(),
items: z.array(UpdateProformaItemRequestSchema).optional(),
}); });
export type UpdateProformaByIdRequestDTO = Partial<z.infer<typeof UpdateProformaByIdRequestSchema>>; export type UpdateProformaByIdRequestDTO = z.infer<typeof UpdateProformaByIdRequestSchema>;
export type UpdateProformaByIdParamsRequestDTO = z.infer<
typeof UpdateProformaByIdParamsRequestSchema
>;

View File

@ -19,7 +19,6 @@ export const GetProformaByIdResponseSchema = z.object({
id: z.uuid(), id: z.uuid(),
company_id: z.uuid(), company_id: z.uuid(),
is_proforma: z.boolean(),
invoice_number: z.string(), invoice_number: z.string(),
status: ProformaStatusSchema, status: ProformaStatusSchema,
series: z.string().nullable(), series: z.string().nullable(),
@ -39,7 +38,7 @@ export const GetProformaByIdResponseSchema = z.object({
linked_invoice_id: z.uuid().nullable(), linked_invoice_id: z.uuid().nullable(),
taxes: TaxesBreakdownSchema, taxes: z.array(TaxesBreakdownSchema),
payment_method: PaymentMethodRefSchema.nullable(), payment_method: PaymentMethodRefSchema.nullable(),
@ -47,6 +46,7 @@ export const GetProformaByIdResponseSchema = z.object({
items_discount_amount: MoneySchema, items_discount_amount: MoneySchema,
global_discount_percentage: PercentageSchema, global_discount_percentage: PercentageSchema,
global_discount_amount: MoneySchema, global_discount_amount: MoneySchema,
total_discount_amount: MoneySchema,
taxable_amount: MoneySchema, taxable_amount: MoneySchema,
iva_amount: MoneySchema, iva_amount: MoneySchema,
rec_amount: MoneySchema, rec_amount: MoneySchema,

View File

@ -1,5 +1,6 @@
export * from "./issued-invoices"; export * from "./issued-invoices";
export * from "./item-taxes-breakdown.dto"; export * from "./item-position.dto";
export * from "./payment-methof-ref.dto"; export * from "./payment-method-ref.dto";
export * from "./proforma"; export * from "./proforma";
export * from "./tax-combination-code.dto";
export * from "./taxes-breakdown.dto"; export * from "./taxes-breakdown.dto";

View File

@ -1,12 +1,13 @@
import { MoneySchema, PercentageSchema, QuantitySchema } from "@erp/core"; import { MoneySchema, PercentageSchema, QuantitySchema } from "@erp/core";
import { z } from "zod/v4"; import { z } from "zod/v4";
import { ItemTaxesBreakdownSchema } from "../item-taxes-breakdown.dto"; import { ItemPositionSchema } from "../item-position.dto";
import { TaxesBreakdownSchema } from "../taxes-breakdown.dto";
export const IssuedInvoiceItemDetailSchema = z.object({ export const IssuedInvoiceItemDetailSchema = z.object({
id: z.uuid(), id: z.uuid(),
is_valued: z.boolean(), is_valued: z.boolean(),
position: z.number(), position: ItemPositionSchema,
description: z.string().nullable(), description: z.string().nullable(),
quantity: QuantitySchema, quantity: QuantitySchema,
@ -20,7 +21,7 @@ export const IssuedInvoiceItemDetailSchema = z.object({
global_discount_percentage: PercentageSchema, global_discount_percentage: PercentageSchema,
global_discount_amount: MoneySchema, global_discount_amount: MoneySchema,
...ItemTaxesBreakdownSchema.shape, ...TaxesBreakdownSchema.shape,
total_amount: MoneySchema, total_amount: MoneySchema,
}); });

View File

@ -0,0 +1,5 @@
import { z } from "zod/v4";
export const ItemPositionSchema = z.number().int().nonnegative();
export type ItemPositionDTO = z.infer<typeof ItemPositionSchema>;

View File

@ -1,6 +0,0 @@
import type { z } from "zod/v4";
import { TaxesBreakdownSchema } from "./taxes-breakdown.dto";
export const ItemTaxesBreakdownSchema = TaxesBreakdownSchema;
export type ItemTaxesBreakdownDTO = z.infer<typeof ItemTaxesBreakdownSchema>;

View File

@ -1,8 +1,8 @@
import { z } from "zod/v4"; import { z } from "zod/v4";
export const PaymentMethodRefSchema = z.object({ export const PaymentMethodRefSchema = z.object({
payment_id: z.uuid(), id: z.uuid(),
payment_description: z.string(), description: z.string(),
}); });
export type PaymentMethodRefDTO = z.infer<typeof PaymentMethodRefSchema>; export type PaymentMethodRefDTO = z.infer<typeof PaymentMethodRefSchema>;

View File

@ -1,12 +1,13 @@
import { MoneySchema, PercentageSchema, QuantitySchema } from "@erp/core"; import { MoneySchema, PercentageSchema, QuantitySchema } from "@erp/core";
import { z } from "zod/v4"; import { z } from "zod/v4";
import { ItemTaxesBreakdownSchema } from "../item-taxes-breakdown.dto"; import { ItemPositionSchema } from "../item-position.dto";
import { TaxesBreakdownSchema } from "../taxes-breakdown.dto";
export const ProformaItemDetailSchema = z.object({ export const ProformaItemDetailSchema = z.object({
id: z.uuid(), id: z.uuid(),
is_valued: z.boolean(), is_valued: z.boolean(),
position: z.number(), position: ItemPositionSchema,
description: z.string().nullable(), description: z.string().nullable(),
quantity: QuantitySchema, quantity: QuantitySchema,
@ -20,7 +21,9 @@ export const ProformaItemDetailSchema = z.object({
global_discount_percentage: PercentageSchema, global_discount_percentage: PercentageSchema,
global_discount_amount: MoneySchema, global_discount_amount: MoneySchema,
...ItemTaxesBreakdownSchema.shape, total_discount_amount: MoneySchema,
...TaxesBreakdownSchema.shape,
total_amount: MoneySchema, total_amount: MoneySchema,
}); });

View File

@ -7,7 +7,6 @@ import { ProformaStatusSchema } from "./proforma-status.dto";
export const ProformaSummarySchema = z.object({ export const ProformaSummarySchema = z.object({
id: z.uuid(), id: z.uuid(),
company_id: z.uuid(), company_id: z.uuid(),
is_proforma: z.boolean(),
invoice_number: z.string(), invoice_number: z.string(),
status: ProformaStatusSchema, status: ProformaStatusSchema,

View File

@ -0,0 +1,23 @@
import { z } from "zod/v4";
const TAX_CODE_PATTERN = /^[a-z0-9_]+$/i;
const EMPTY_TAX_SLOT = "#";
export const TaxCombinationCodeSchema = z.string().refine(
(value) => {
const parts = value.split(";");
if (parts.length !== 3) {
return false;
}
return parts.every((part) => {
return part === EMPTY_TAX_SLOT || TAX_CODE_PATTERN.test(part);
});
},
{
message: "taxes must use format '<iva_code|#>;<rec_code|#>;<retention_code|#>'",
}
);
export type TaxCombinationCodeDTO = z.infer<typeof TaxCombinationCodeSchema>;

View File

@ -44,7 +44,7 @@ export const GetProformaByIdAdapter = {
recipient: mapRecipient(dto.recipient), recipient: mapRecipient(dto.recipient),
taxes: dto.taxes.map(mapTaxSummary), taxes: dto.taxes.map(mapTaxSummary),
paymentMethod: dto.payment_method?.payment_id, paymentMethod: dto.payment_method?.id,
subtotalAmount: MoneyDTOHelper.toNumber(dto.subtotal_amount), subtotalAmount: MoneyDTOHelper.toNumber(dto.subtotal_amount),

View File

@ -55,8 +55,6 @@ export class UpdateCustomerInputMapper implements IUpdateCustomerInputMapper {
dto: UpdateCustomerByIdRequestDTO, dto: UpdateCustomerByIdRequestDTO,
params: { companyId: UniqueID } params: { companyId: UniqueID }
): Result<CustomerPatchProps, Error> { ): Result<CustomerPatchProps, Error> {
console.log("Mapping UpdateCustomerByIdRequestDTO to CustomerPatchProps:", dto);
try { try {
const errors: ValidationErrorDetail[] = []; const errors: ValidationErrorDetail[] = [];
const customerPatchProps: CustomerPatchProps = {}; const customerPatchProps: CustomerPatchProps = {};

View File

@ -1,7 +1,9 @@
import { import {
CountryCodeSchema, CountryCodeSchema,
CurrencyCodeSchema,
EmailSchema, EmailSchema,
LandPhoneSchema, LandPhoneSchema,
LanguageCodeSchema,
MobilePhoneSchema, MobilePhoneSchema,
PostalCodeSchema, PostalCodeSchema,
TinSchema, TinSchema,
@ -9,6 +11,8 @@ import {
} from "@erp/core"; } from "@erp/core";
import { z } from "zod/v4"; import { z } from "zod/v4";
import { TaxCombinationCodeSchema } from "../shared";
export const UpdateCustomerByIdParamsRequestSchema = z.object({ export const UpdateCustomerByIdParamsRequestSchema = z.object({
customer_id: z.uuid(), customer_id: z.uuid(),
}); });
@ -45,15 +49,15 @@ export const UpdateCustomerByIdRequestSchema = z.object({
trade_name: z.string().nullable().optional(), trade_name: z.string().nullable().optional(),
tin: TinSchema.nullable().optional(), tin: TinSchema.nullable().optional(),
default_taxes: z.string().nullable().optional(), default_taxes: TaxCombinationCodeSchema.optional(),
address: UpdateCustomerAddressPatchRequestSchema.optional(), address: UpdateCustomerAddressPatchRequestSchema.optional(),
contact: UpdateCustomerContactPatchRequestSchema.optional(), contact: UpdateCustomerContactPatchRequestSchema.optional(),
legal_record: z.string().nullable().optional(), legal_record: z.string().nullable().optional(),
language_code: z.string().optional(), language_code: LanguageCodeSchema.optional(),
currency_code: z.string().optional(), currency_code: CurrencyCodeSchema.optional(),
}); });
export type UpdateCustomerAddressPatchRequestDTO = z.infer< export type UpdateCustomerAddressPatchRequestDTO = z.infer<

View File

@ -12,6 +12,7 @@ import {
} from "@erp/core"; } from "@erp/core";
import { z } from "zod/v4"; import { z } from "zod/v4";
import { TaxCombinationCodeSchema } from "../shared";
import { CustomerStatusSchema } from "../shared/customer-status.dto"; import { CustomerStatusSchema } from "../shared/customer-status.dto";
export const GetCustomerByIdResponseSchema = z.object({ export const GetCustomerByIdResponseSchema = z.object({
@ -48,7 +49,7 @@ export const GetCustomerByIdResponseSchema = z.object({
legal_record: z.string().nullable(), legal_record: z.string().nullable(),
default_taxes: z.string().nullable(), default_taxes: TaxCombinationCodeSchema,
language_code: LanguageCodeSchema, language_code: LanguageCodeSchema,
currency_code: CurrencyCodeSchema, currency_code: CurrencyCodeSchema,

View File

@ -1,2 +1,3 @@
export * from "./customer-status.dto"; export * from "./customer-status.dto";
export * from "./customer-summary.dto"; export * from "./customer-summary.dto";
export * from "./tax-combination-code.dto";

View File

@ -0,0 +1,23 @@
import { z } from "zod/v4";
const TAX_CODE_PATTERN = /^[a-z0-9_]+$/i;
const EMPTY_TAX_SLOT = "#";
export const TaxCombinationCodeSchema = z.string().refine(
(value) => {
const parts = value.split(";");
if (parts.length !== 3) {
return false;
}
return parts.every((part) => {
return part === EMPTY_TAX_SLOT || TAX_CODE_PATTERN.test(part);
});
},
{
message: "taxes must use format '<iva_code|#>;<rec_code|#>;<retention_code|#>'",
}
);
export type TaxCombinationCodeDTO = z.infer<typeof TaxCombinationCodeSchema>;

View File

@ -36,7 +36,7 @@ export const GetSupplierByIdResponseSchema = z.object({
legal_record: z.string(), legal_record: z.string(),
default_taxes: z.array(z.string()), default_taxes: TaxCombinationCodeSchema,
status: z.string(), status: z.string(),
language_code: LanguageCodeSchema, language_code: LanguageCodeSchema,
currency_code: CurrencyCodeSchema, currency_code: CurrencyCodeSchema,