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 {
CurrencyCode,
DomainError,
LanguageCode,
Percentage,
TextValue,
UniqueID,
UtcDate,
@ -20,7 +19,6 @@ import {
type IProformaCreateProps,
type IProformaItemCreateProps,
InvoiceNumber,
InvoicePaymentMethod,
type InvoiceRecipient,
InvoiceSerie,
InvoiceStatus,
@ -31,23 +29,6 @@ import {
type ProformaItemTaxesProps,
} 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 {
map(
dto: CreateProformaRequestDTO,
@ -55,23 +36,21 @@ export interface ICreateProformaInputMapper {
): Result<{ id: UniqueID; props: IProformaCreateProps }>;
}
export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/ {
private readonly taxCatalog: JsonTaxCatalogProvider;
constructor(params: { taxCatalog: JsonTaxCatalogProvider }) {
this.taxCatalog = params.taxCatalog;
}
/**
* @summary Convierte el DTO de creación de proforma en props de dominio.
* @remarks
* No construye el agregado. Solo valida y convierte primitivas de transporte
* a Value Objects y props necesarias para `Proforma.create`.
*/
export class CreateProformaInputMapper implements ICreateProformaInputMapper {
public map(
dto: CreateProformaRequestDTO,
params: { companyId: UniqueID }
): Result<{ id: UniqueID; props: IProformaCreateProps }> {
const errors: ValidationErrorDetail[] = [];
const { companyId } = params;
try {
const defaultStatus = InvoiceStatus.draft();
const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
const customerId = extractOrPushError(
@ -80,9 +59,7 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
errors
);
const recipient = Maybe.none<InvoiceRecipient>();
const proformaNumber = extractOrPushError(
const invoiceNumber = extractOrPushError(
InvoiceNumber.create(dto.invoice_number),
"invoice_number",
errors
@ -113,7 +90,7 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
);
const description = extractOrPushError(
maybeFromNullableResult(dto.reference, (value) => Result.ok(String(value))),
maybeFromNullableResult(dto.description, (value) => Result.ok(String(value))),
"description",
errors
);
@ -136,24 +113,21 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
errors
);
const paymentMethod = extractOrPushError(
maybeFromNullableResult(dto.payment_method, (value) =>
InvoicePaymentMethod.create({ paymentDescription: value })
),
"payment_method",
errors
);
const globalDiscountPercentage = extractOrPushError(
Percentage.create({
value: Number(dto.global_discount_percentage.value),
scale: Number(dto.global_discount_percentage.scale),
DiscountPercentage.create({
value: NumberHelper.toSafeNumber(dto.global_discount_percentage.value),
}),
"discount_percentage",
"global_discount_percentage",
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!,
currencyCode: currencyCode!,
globalDiscountPercentage: globalDiscountPercentage!,
@ -163,17 +137,17 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
this.throwIfValidationErrors(errors);
const props: IProformaCreateProps = {
companyId,
status: defaultStatus,
companyId: params.companyId,
status: InvoiceStatus.draft(),
invoiceNumber: proformaNumber!,
invoiceNumber: invoiceNumber!,
series: series!,
invoiceDate: invoiceDate!,
operationDate: operationDate!,
customerId: customerId!,
recipient,
recipient: Maybe.none<InvoiceRecipient>(),
reference: reference!,
description: description!,
@ -182,10 +156,12 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
languageCode: languageCode!,
currencyCode: currencyCode!,
paymentMethod: paymentMethod!,
globalDiscountPercentage: globalDiscountPercentage!,
linkedInvoiceId: Maybe.none(),
items: itemsProps, // ← IProformaItemProps[]
paymentMethodId: paymentMethodId!,
globalDiscountPercentage: globalDiscountPercentage!,
items,
};
return Result.ok({
@ -193,18 +169,12 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
props,
});
} catch (err: unknown) {
return Result.fail(new DomainError("Customer invoice props mapping failed", { cause: err }));
}
}
private throwIfValidationErrors(errors: ValidationErrorDetail[]): void {
if (errors.length > 0) {
throw new ValidationErrorCollection("Customer proforma props mapping failed", errors);
return Result.fail(new DomainError("Proforma props mapping failed", { cause: err }));
}
}
private mapItemsProps(
dto: CreateProformaRequestDTO,
itemsDTO: CreateProformaRequestDTO["items"],
params: {
languageCode: LanguageCode;
currencyCode: CurrencyCode;
@ -212,132 +182,78 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
errors: ValidationErrorDetail[];
}
): IProformaItemCreateProps[] {
const itemsProps: IProformaItemCreateProps[] = [];
dto.items.forEach((item, index) => {
return itemsDTO.map((item, index) => {
const description = extractOrPushError(
maybeFromNullableResult(item.description, (v) => ItemDescription.create(v)),
maybeFromNullableResult(item.description, (value) => ItemDescription.create(value)),
`items[${index}].description`,
params.errors
);
const quantity = extractOrPushError(
maybeFromNullableResult(item.quantity, (v) =>
ItemQuantity.create({ value: NumberHelper.toSafeNumber(v) })
maybeFromNullableResult(item.quantity, (value) =>
ItemQuantity.create({ value: NumberHelper.toSafeNumber(value) })
),
`items[${index}].quantity`,
params.errors
);
const unitAmount = extractOrPushError(
maybeFromNullableResult(item.unit_amount, (v) =>
ItemAmount.create({ value: NumberHelper.toSafeNumber(v) })
maybeFromNullableResult(item.unit_amount, (value) =>
ItemAmount.create({ value: NumberHelper.toSafeNumber(value) })
),
`items[${index}].unit_amount`,
params.errors
);
const discountPercentage = extractOrPushError(
maybeFromNullableResult(item.item_discount_percentage, (v) =>
DiscountPercentage.create({ value: NumberHelper.toSafeNumber(v.value) })
const itemDiscountPercentage = extractOrPushError(
maybeFromNullableResult(item.item_discount_percentage, (value) =>
DiscountPercentage.create({
value: NumberHelper.toSafeNumber(value.value),
})
),
`items[${index}].discount_percentage`,
`items[${index}].item_discount_percentage`,
params.errors
);
const taxes = this.mapTaxesProps(item.taxes, {
itemIndex: index,
errors: params.errors,
});
this.throwIfValidationErrors(params.errors);
itemsProps.push({
globalDiscountPercentage: params.globalDiscountPercentage,
languageCode: params.languageCode,
currencyCode: params.currencyCode,
return {
position: item.position,
description: description!,
quantity: quantity!,
unitAmount: unitAmount!,
itemDiscountPercentage: discountPercentage!,
taxes,
});
itemDiscountPercentage: itemDiscountPercentage!,
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(
taxesDTO: NonNullable<CreateProformaRequestDTO["items"]>[number]["taxes"],
taxesDTO: CreateProformaRequestDTO["items"][number]["taxes"],
params: { itemIndex: number; errors: ValidationErrorDetail[] }
): ProformaItemTaxesProps {
// TODO: POR AHORA SE QUEDA ASÍ
if (taxesDTO === "#;#;#") {
return ProformaItemTaxes.empty().getProps();
}
return ProformaItemTaxes.empty().getProps();
/*const { itemIndex, errors } = params;
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);
}
params.errors.push({
path: `items[${params.itemIndex}].taxes`,
message: "Tax combination mapping is not implemented yet",
});
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,
} 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 {
map(
dto: UpdateProformaByIdRequestDTO,
@ -47,6 +33,20 @@ export interface IUpdateProformaInputMapper {
): 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 {
public map(
dto: UpdateProformaByIdRequestDTO,
@ -54,46 +54,58 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
): Result<ProformaPatchProps> {
try {
const errors: ValidationErrorDetail[] = [];
const props: ProformaPatchProps = {};
const proformaPatchProps: ProformaPatchProps = {};
toPatchField(dto.series).ifSet((series) => {
props.series = extractOrPushError(
proformaPatchProps.series = extractOrPushError(
maybeFromNullableResult(series, (value) => InvoiceSerie.create(value)),
"reference",
"series",
errors
);
});
toPatchField(dto.invoice_date).ifSet((invoice_date) => {
if (isNullishOrEmpty(invoice_date)) {
errors.push({ path: "invoice_date", message: "Invoice date cannot be empty" });
toPatchField(dto.invoice_date).ifSet((invoiceDate) => {
if (isNullishOrEmpty(invoiceDate)) {
errors.push({
path: "invoice_date",
message: "Invoice date cannot be empty",
});
return;
}
props.invoiceDate = extractOrPushError(
UtcDate.createFromISO(invoice_date!),
proformaPatchProps.invoiceDate = extractOrPushError(
UtcDate.createFromISO(invoiceDate),
"invoice_date",
errors
);
});
toPatchField(dto.operation_date).ifSet((operation_date) => {
props.operationDate = extractOrPushError(
maybeFromNullableResult(operation_date, (value) => UtcDate.createFromISO(value)),
toPatchField(dto.operation_date).ifSet((operationDate) => {
proformaPatchProps.operationDate = extractOrPushError(
maybeFromNullableResult(operationDate, (value) => UtcDate.createFromISO(value)),
"operation_date",
errors
);
});
toPatchField(dto.customer_id).ifSet((customer_id) => {
if (isNullishOrEmpty(customer_id)) {
errors.push({ path: "customer_id", message: "Proforma cannot be empty" });
toPatchField(dto.customer_id).ifSet((customerId) => {
if (isNullishOrEmpty(customerId)) {
errors.push({
path: "customer_id",
message: "Customer id cannot be empty",
});
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) => {
props.reference = extractOrPushError(
proformaPatchProps.reference = extractOrPushError(
maybeFromNullableResult(reference, (value) => Result.ok(String(value))),
"reference",
errors
@ -101,7 +113,7 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
});
toPatchField(dto.description).ifSet((description) => {
props.description = extractOrPushError(
proformaPatchProps.description = extractOrPushError(
maybeFromNullableResult(description, (value) => Result.ok(String(value))),
"description",
errors
@ -109,7 +121,7 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
});
toPatchField(dto.notes).ifSet((notes) => {
props.notes = extractOrPushError(
proformaPatchProps.notes = extractOrPushError(
maybeFromNullableResult(notes, (value) => TextValue.create(value)),
"notes",
errors
@ -118,12 +130,15 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
toPatchField(dto.language_code).ifSet((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;
}
props.languageCode = extractOrPushError(
LanguageCode.create(languageCode!),
proformaPatchProps.languageCode = extractOrPushError(
LanguageCode.create(languageCode),
"language_code",
errors
);
@ -131,72 +146,84 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
toPatchField(dto.currency_code).ifSet((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;
}
props.currencyCode = extractOrPushError(
CurrencyCode.create(currencyCode!),
proformaPatchProps.currencyCode = extractOrPushError(
CurrencyCode.create(currencyCode),
"currency_code",
errors
);
});
if (dto.items) {
const itemsProps = this.mapItemsProps(dto, { errors });
props.items = itemsProps;
toPatchField(dto.global_discount_percentage).ifSet((globalDiscountPercentage) => {
proformaPatchProps.globalDiscountPercentage = extractOrPushError(
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);
return Result.ok(props);
return Result.ok(proformaPatchProps);
} catch (err: unknown) {
return Result.fail(new DomainError("Proforma proforma props mapping failed", { cause: err }));
}
}
private throwIfValidationErrors(errors: ValidationErrorDetail[]): void {
if (errors.length > 0) {
throw new ValidationErrorCollection("Customer proforma props mapping failed", errors);
return Result.fail(new DomainError("Proforma props mapping failed", { cause: err }));
}
}
private mapItemsProps(
dto: UpdateProformaByIdRequestDTO,
params: {
errors: ValidationErrorDetail[];
}
itemsDTO: NonNullable<UpdateProformaByIdRequestDTO["items"]>,
params: { errors: ValidationErrorDetail[] }
): ProformaItemPatchProps[] {
const itemsProps: ProformaItemPatchProps[] = [];
dto.items?.forEach((item, index) => {
return itemsDTO.map((item, index) => {
const description = extractOrPushError(
maybeFromNullableResult(item.description, (v) => ItemDescription.create(v)),
maybeFromNullableResult(item.description, (value) => ItemDescription.create(value)),
`items[${index}].description`,
params.errors
);
const quantity = extractOrPushError(
maybeFromNullableResult(item.quantity, (v) =>
ItemQuantity.create({ value: NumberHelper.toSafeNumber(v) })
maybeFromNullableResult(item.quantity, (value) =>
ItemQuantity.create({ value: NumberHelper.toSafeNumber(value) })
),
`items[${index}].quantity`,
params.errors
);
const unitAmount = extractOrPushError(
maybeFromNullableResult(item.unit_amount, (v) =>
ItemAmount.create({ value: NumberHelper.toSafeNumber(v) })
maybeFromNullableResult(item.unit_amount, (value) =>
ItemAmount.create({ value: NumberHelper.toSafeNumber(value) })
),
`items[${index}].unit_amount`,
params.errors
);
const discountPercentage = extractOrPushError(
maybeFromNullableResult(item.item_discount_percentage, (v) =>
DiscountPercentage.create({ value: NumberHelper.toSafeNumber(v.value) })
const itemDiscountPercentage = extractOrPushError(
maybeFromNullableResult(item.item_discount_percentage, (value) =>
DiscountPercentage.create({
value: NumberHelper.toSafeNumber(value.value),
})
),
`items[${index}].discount_percentage`,
`items[${index}].item_discount_percentage`,
params.errors
);
@ -205,89 +232,44 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
errors: params.errors,
});
this.throwIfValidationErrors(params.errors);
itemsProps.push({
return {
position: item.position,
description: description!,
quantity: quantity!,
unitAmount: unitAmount!,
itemDiscountPercentage: discountPercentage!,
itemDiscountPercentage: itemDiscountPercentage!,
taxes,
});
};
});
return itemsProps;
}
/* Devuelve las propiedades de los impuestos de una línea de detalle */
private mapTaxesProps(
taxesDTO: NonNullable<UpdateProformaByIdRequestDTO["items"]>[number]["taxes"],
params: { itemIndex: number; errors: ValidationErrorDetail[] }
): ProformaItemTaxesProps {
// TODO: POR AHORA SE QUEDA ASÍ
if (taxesDTO === "#;#;#") {
return ProformaItemTaxes.empty().getProps();
}
return ProformaItemTaxes.empty().getProps();
/*const { itemIndex, errors } = params;
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);
}
/**
* Pendiente: resolver códigos contra catálogo fiscal.
*
* taxesDTO llega como:
* - iva_21;#;retention_10
* - iva_10;rec_5_2;#
* - #;#;#
*/
params.errors.push({
path: `items[${params.itemIndex}].taxes`,
message: "Tax combination mapping is not implemented yet",
});
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-item-full-snapshot.interface";
export * from "./proforma-items-full-snapshot-builder";
export * from "./proforma-recipient-full-snapshot.interface";
export * from "./proforma-recipient-full-snapshot-builder";
export * from "./proforma-tax-full-snapshot-interface";
export * from "./proforma-taxes-full-snapshot-builder";

View File

@ -1,15 +1,15 @@
import type { ISnapshotBuilder } from "@erp/core/api";
import { maybeToNullable } from "@repo/rdx-ddd";
import type { GetProformaByIdResponseDTO } from "../../../../../common";
import type { Proforma } from "../../../../domain";
import type { IProformaFullSnapshot } from "./proforma-full-snapshot.interface";
import type { IProformaItemsFullSnapshotBuilder } from "./proforma-items-full-snapshot-builder";
import type { IProformaRecipientFullSnapshotBuilder } from "./proforma-recipient-full-snapshot-builder";
import type { IProformaTaxesFullSnapshotBuilder } from "./proforma-taxes-full-snapshot-builder";
export interface IProformaFullSnapshotBuilder
extends ISnapshotBuilder<Proforma, IProformaFullSnapshot> {}
extends ISnapshotBuilder<Proforma, GetProformaByIdResponseDTO> {}
export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder {
constructor(
@ -18,7 +18,7 @@ export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder
private readonly taxesBuilder: IProformaTaxesFullSnapshotBuilder
) {}
toOutput(proforma: Proforma): IProformaFullSnapshot {
toOutput(proforma: Proforma): GetProformaByIdResponseDTO {
const items = this.itemsBuilder.toOutput(proforma.items);
const recipient = this.recipientBuilder.toOutput(proforma);
const taxes = this.taxesBuilder.toOutput(proforma.taxes());
@ -27,8 +27,8 @@ export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder
(payment) => {
const { id, payment_description } = payment.toObjectString();
return {
payment_id: id,
payment_description,
id: id,
description: payment_description,
};
},
() => null
@ -40,9 +40,8 @@ export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder
id: proforma.id.toString(),
company_id: proforma.companyId.toString(),
is_proforma: true,
invoice_number: proforma.invoiceNumber.toString(),
status: proforma.status.toPrimitive(),
status: proforma.status.toPrimitive() as GetProformaByIdResponseDTO["status"],
series: maybeToNullable(proforma.series, (value) => value.toString()),
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 { ProformaItemDetailDTO } from "@erp/customer-invoices/common";
import {
maybeToEmptyMoneyObjectString,
maybeToEmptyPercentageObjectString,
@ -9,13 +10,11 @@ import {
import { ItemAmount, type ProformaItem, type ProformaItems } from "../../../../domain";
import type { IProformaItemFullSnapshot } from "./proforma-item-full-snapshot.interface";
export interface IProformaItemsFullSnapshotBuilder
extends ISnapshotBuilder<ProformaItems, IProformaItemFullSnapshot[]> {}
extends ISnapshotBuilder<ProformaItems, ProformaItemDetailDTO[]> {}
export class ProformaItemsFullSnapshotBuilder implements IProformaItemsFullSnapshotBuilder {
private mapItem(proformaItem: ProformaItem, index: number): IProformaItemFullSnapshot {
private mapItem(proformaItem: ProformaItem, index: number): ProformaItemDetailDTO {
const allAmounts = proformaItem.totals();
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));
}
}

View File

@ -1,15 +1,14 @@
import type { ISnapshotBuilder } from "@erp/core/api";
import { DomainValidationError, maybeToNullable } from "@repo/rdx-ddd";
import type { ProformaRecipientSummaryDTO } from "../../../../../common";
import type { InvoiceRecipient, Proforma } from "../../../../domain";
import type { IProformaRecipientFullSnapshot } from "./proforma-recipient-full-snapshot.interface";
export interface IProformaRecipientFullSnapshotBuilder
extends ISnapshotBuilder<Proforma, IProformaRecipientFullSnapshot> {}
extends ISnapshotBuilder<Proforma, ProformaRecipientSummaryDTO> {}
export class ProformaRecipientFullSnapshotBuilder implements IProformaRecipientFullSnapshotBuilder {
toOutput(proforma: Proforma): IProformaRecipientFullSnapshot {
toOutput(proforma: Proforma): ProformaRecipientSummaryDTO {
if (!proforma.recipient) {
throw DomainValidationError.requiredValue("recipient", {
cause: proforma,
@ -39,7 +38,7 @@ export class ProformaRecipientFullSnapshotBuilder implements IProformaRecipientF
province: null,
postal_code: 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 type { Collection } from "@repo/rdx-utils";
import type { TaxesBreakdownDTO } from "../../../../../common";
import type { IProformaTaxTotals } from "../../../../domain";
import type { IProformaTaxFullSnapshot } from "./proforma-tax-full-snapshot-interface";
export interface IProformaTaxesFullSnapshotBuilder
extends ISnapshotBuilder<Collection<IProformaTaxTotals>, IProformaTaxFullSnapshot[]> {}
extends ISnapshotBuilder<Collection<IProformaTaxTotals>, TaxesBreakdownDTO[]> {}
export class ProformaTaxesFullSnapshotBuilder implements IProformaTaxesFullSnapshotBuilder {
private mapItem(proformaTax: IProformaTaxTotals, index: number): IProformaTaxFullSnapshot {
private mapItem(proformaTax: IProformaTaxTotals, index: number): TaxesBreakdownDTO {
return {
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));
}
}

View File

@ -14,7 +14,6 @@ export class ProformaSummarySnapshotBuilder implements IProformaSummarySnapshotB
return {
id: proforma.id.toString(),
company_id: proforma.companyId.toString(),
is_proforma: proforma.isProforma,
invoice_number: proforma.invoiceNumber.toString(),
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 { ProformaPatchProps } from "../../../domain";
import type { UpdateProformaInputMapper } from "../mappers";
import type { IUpdateProformaInputMapper } from "../mappers";
import type { IProformaUpdater } from "../services";
import type { IProformaFullSnapshotBuilder } from "../snapshot-builders";
@ -15,14 +15,14 @@ type UpdateProformaUseCaseInput = {
};
type UpdateProformaUseCaseDeps = {
dtoMapper: UpdateProformaInputMapper;
dtoMapper: IUpdateProformaInputMapper;
updater: IProformaUpdater;
fullSnapshotBuilder: IProformaFullSnapshotBuilder;
transactionManager: ITransactionManager;
};
export class UpdateProformaUseCase {
private readonly dtoMapper: UpdateProformaInputMapper;
private readonly dtoMapper: IUpdateProformaInputMapper;
private readonly updater: IProformaUpdater;
private readonly fullSnapshotBuilder: IProformaFullSnapshotBuilder;
private readonly transactionManager: ITransactionManager;

View File

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

View File

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

View File

@ -11,7 +11,6 @@ import {
} from "@repo/rdx-ddd";
import { type Collection, type Maybe, Result } from "@repo/rdx-utils";
import type { InvoicePaymentMethod } from "../../common/entities";
import {
InvoiceAmount,
type InvoiceNumber,
@ -53,14 +52,14 @@ export interface IProformaCreateProps {
linkedInvoiceId: Maybe<UniqueID>;
paymentMethod: Maybe<InvoicePaymentMethod>;
paymentMethodId: Maybe<UniqueID>;
items: IProformaItemCreateProps[];
globalDiscountPercentage: DiscountPercentage;
}
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 {
@ -100,7 +99,7 @@ export interface IProforma {
languageCode: LanguageCode;
currencyCode: CurrencyCode;
paymentMethod: Maybe<InvoicePaymentMethod>;
paymentMethodId: Maybe<UniqueID>;
linkedInvoiceId: Maybe<UniqueID>;
@ -176,8 +175,12 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
Object.assign(this.props, candidateProps);
// Reemplazo de items (si se proporciona)
if (items) {
this.initializeItems(items);
if (items !== undefined) {
const initializeResult = this.initializeItems(items);
if (initializeResult.isFailure) {
return Result.fail(initializeResult.error);
}
}
return Result.ok();
@ -189,18 +192,11 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
this._items.reset();
for (const [index, itemProps] of itemsProps.entries()) {
const { languageCode, currencyCode, globalDiscountPercentage, ...restProps } = {
const itemResult = ProformaItem.create({
...itemProps,
languageCode: this.languageCode,
currencyCode: this.currencyCode,
globalDiscountPercentage: this.globalDiscountPercentage,
...itemProps,
};
const itemResult = ProformaItem.create({
...restProps,
languageCode,
currencyCode,
globalDiscountPercentage,
});
if (itemResult.isFailure) {
@ -261,8 +257,8 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
return this.props.recipient;
}
public get paymentMethod(): Maybe<InvoicePaymentMethod> {
return this.props.paymentMethod;
public get paymentMethodId(): Maybe<UniqueID> {
return this.props.paymentMethodId;
}
public get linkedInvoiceId(): Maybe<UniqueID> {
@ -290,7 +286,7 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
}
public get hasPaymentMethod() {
return this.paymentMethod.isSome();
return this.paymentMethodId.isSome();
}
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(
new DomainValidationError(
"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";
export const CreateProformaItemRequestSchema = z.object({
id: z.uuid(),
position: z.string(),
description: z.string().default(""),
quantity: NumericStringSchema.default(""),
unit_amount: NumericStringSchema.default(""),
item_discount_percentage: PercentageSchema.default({
value: "0",
scale: "2",
}),
taxes: z.string().default(""),
});
import { ItemPositionSchema, TaxCombinationCodeSchema } from "../../shared";
export const CreateProformaItemRequestSchema = z
.object({
position: ItemPositionSchema,
is_valued: z.boolean(),
description: z.string().nullable(),
quantity: NumericStringSchema.nullable(),
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 const CreateProformaRequestSchema = z.object({
id: z.uuid(),
invoice_number: z.string(),
series: z.string().default(""),
series: z.string().nullable(),
invoice_date: z.string(),
operation_date: z.string().default(""),
invoice_date: IsoDateSchema,
operation_date: IsoDateSchema.nullable().optional(),
customer_id: z.uuid(),
reference: z.string().default(""),
notes: z.string().default(""),
reference: z.string().nullable().optional(),
description: z.string().nullable().optional(),
notes: z.string().nullable().optional(),
language_code: z.string().toLowerCase().default("es"),
currency_code: z.string().toUpperCase().default("EUR"),
language_code: LanguageCodeSchema,
currency_code: CurrencyCodeSchema,
global_discount_percentage: PercentageSchema.default({
value: "0",
scale: "2",
}),
global_discount_percentage: PercentageSchema,
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>;

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";
export const UpdateProformaItemRequestSchema = z.object({
id: z.uuid(),
position: z.string(),
description: z.string().default(""),
quantity: NumericStringSchema.default(""),
unit_amount: NumericStringSchema.default(""),
item_discount_percentage: PercentageSchema.default({
value: "0",
scale: "2",
}),
taxes: z.string().default(""),
});
import { ItemPositionSchema, TaxCombinationCodeSchema } from "../../shared";
export const UpdateProformaItemRequestSchema = z
.object({
position: ItemPositionSchema,
is_valued: z.boolean(),
description: z.string().nullable(),
quantity: NumericStringSchema.nullable(),
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({
proforma_id: z.string(),
proforma_id: z.uuid(),
});
export const UpdateProformaByIdRequestSchema = z.object({
series: z.string().optional(),
series: z.string().nullable().optional(),
invoice_date: z.string().optional(),
operation_date: z.string().optional(),
invoice_date: IsoDateSchema.optional(),
operation_date: IsoDateSchema.nullable().optional(),
customer_id: z.uuid().optional(),
reference: z.string().optional(),
description: z.string().optional(),
notes: z.string().optional(),
reference: z.string().nullable().optional(),
description: z.string().nullable().optional(),
notes: z.string().nullable().optional(),
language_code: z.string().optional(),
currency_code: z.string().optional(),
language_code: LanguageCodeSchema.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(),
company_id: z.uuid(),
is_proforma: z.boolean(),
invoice_number: z.string(),
status: ProformaStatusSchema,
series: z.string().nullable(),
@ -39,7 +38,7 @@ export const GetProformaByIdResponseSchema = z.object({
linked_invoice_id: z.uuid().nullable(),
taxes: TaxesBreakdownSchema,
taxes: z.array(TaxesBreakdownSchema),
payment_method: PaymentMethodRefSchema.nullable(),
@ -47,6 +46,7 @@ export const GetProformaByIdResponseSchema = z.object({
items_discount_amount: MoneySchema,
global_discount_percentage: PercentageSchema,
global_discount_amount: MoneySchema,
total_discount_amount: MoneySchema,
taxable_amount: MoneySchema,
iva_amount: MoneySchema,
rec_amount: MoneySchema,

View File

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

View File

@ -1,12 +1,13 @@
import { MoneySchema, PercentageSchema, QuantitySchema } from "@erp/core";
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({
id: z.uuid(),
is_valued: z.boolean(),
position: z.number(),
position: ItemPositionSchema,
description: z.string().nullable(),
quantity: QuantitySchema,
@ -20,7 +21,7 @@ export const IssuedInvoiceItemDetailSchema = z.object({
global_discount_percentage: PercentageSchema,
global_discount_amount: MoneySchema,
...ItemTaxesBreakdownSchema.shape,
...TaxesBreakdownSchema.shape,
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";
export const PaymentMethodRefSchema = z.object({
payment_id: z.uuid(),
payment_description: z.string(),
id: z.uuid(),
description: z.string(),
});
export type PaymentMethodRefDTO = z.infer<typeof PaymentMethodRefSchema>;

View File

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

View File

@ -7,7 +7,6 @@ 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,

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),
taxes: dto.taxes.map(mapTaxSummary),
paymentMethod: dto.payment_method?.payment_id,
paymentMethod: dto.payment_method?.id,
subtotalAmount: MoneyDTOHelper.toNumber(dto.subtotal_amount),

View File

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

View File

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

View File

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

View File

@ -1,2 +1,3 @@
export * from "./customer-status.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(),
default_taxes: z.array(z.string()),
default_taxes: TaxCombinationCodeSchema,
status: z.string(),
language_code: LanguageCodeSchema,
currency_code: CurrencyCodeSchema,