Líneas de proformas

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
David Arranz 2026-05-07 17:56:25 +02:00
parent 2c6cac4859
commit a248e8cdc0
41 changed files with 342 additions and 947 deletions

View File

@ -1,16 +1,26 @@
import { Percentage, type PercentageProps } from "@repo/rdx-ddd";
import type { Result } from "@repo/rdx-utils";
import { Percentage, type PercentageProps, ValidationErrorCollection } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
type DiscountPercentageProps = Pick<PercentageProps, "value">;
type DiscountPercentageProps = PercentageProps;
export class DiscountPercentage extends Percentage {
static DEFAULT_SCALE = 2;
static create({ value }: DiscountPercentageProps): Result<Percentage> {
return Percentage.create({
value,
scale: DiscountPercentage.DEFAULT_SCALE,
});
static create({ value, scale }: DiscountPercentageProps): Result<Percentage> {
if (scale && scale !== DiscountPercentage.DEFAULT_SCALE) {
return Result.fail(
new ValidationErrorCollection("InvalidScale", [
{ message: `DiscountPercentage scale must be ${DiscountPercentage.DEFAULT_SCALE}` },
])
);
}
return Result.ok(
new DiscountPercentage({
value,
scale: DiscountPercentage.DEFAULT_SCALE,
})
);
}
static zero() {

View File

@ -92,6 +92,21 @@ const fromNumber = (amount: number, currency = "EUR", scale = 2): MoneyDTO => {
};
};
const fromNumberNulleable = (
amount: number | null,
currency = "EUR",
scale = 2
): MoneyDTO | null => {
if (amount === null) {
return null;
}
return {
value: String(Math.round(amount * 10 ** scale)),
scale: String(scale),
currency_code: currency,
};
};
/**
* Convierte cadena numérica a MoneyDTO.
*/
@ -126,6 +141,7 @@ export const MoneyDTOHelper = {
toNumericString,
toNumericNulleable,
fromNumber,
fromNumberNulleable,
fromNumericString,
format,
};

View File

@ -84,6 +84,16 @@ const fromNumber = (amount: number, scale = 2): PercentageDTO => {
};
};
const fromNumberNulleable = (amount: number | null, scale = 2): PercentageDTO | null => {
if (amount === null) {
return null;
}
return {
value: String(Math.round(amount * 10 ** scale)),
scale: String(scale),
};
};
/**
* Convierte cadena numérica a PercentageDTO.
*/
@ -106,6 +116,7 @@ export const PercentageDTOHelper = {
toNumericString,
toNumericNulleable,
fromNumber,
fromNumberNulleable,
fromNumericString,
format,
};

View File

@ -78,6 +78,16 @@ const fromNumber = (amount: number, scale = 2): QuantityDTO => {
};
};
const fromNumberNulleable = (amount: number | null, scale = 2): QuantityDTO | null => {
if (amount === null) {
return null;
}
return {
value: String(Math.round(amount * 10 ** scale)),
scale: String(scale),
};
};
/**
* Convierte cadena numérica a QuantityDTO.
*/
@ -100,6 +110,7 @@ export const QuantityDTOHelper = {
toNumericString,
toNumericNulleable,
fromNumber,
fromNumberNulleable,
fromNumericString,
format,
};

View File

@ -11,12 +11,10 @@ export interface IProformaInputMappers {
updateInputMapper: UpdateProformaInputMapper;
}
export const buildProformaInputMappers = (_catalogs: ICatalogs): IProformaInputMappers => {
//const { taxCatalog } = catalogs;
export const buildProformaInputMappers = (catalogs: ICatalogs): IProformaInputMappers => {
// Mappers el DTO a las props validadas (ProformaProps) y luego construir agregado
const createInputMapper = new CreateProformaInputMapper();
const updateInputMapper = new UpdateProformaInputMapper();
const createInputMapper = new CreateProformaInputMapper(catalogs);
const updateInputMapper = new UpdateProformaInputMapper(catalogs);
return {
createInputMapper,

View File

@ -1,4 +1,5 @@
import { DiscountPercentage } from "@erp/core/api";
import type { JsonTaxCatalogProvider } from "@erp/core";
import { DiscountPercentage, type ICatalogs, Tax } from "@erp/core/api";
import {
CurrencyCode,
DomainError,
@ -43,6 +44,11 @@ export interface ICreateProformaInputMapper {
*/
export class CreateProformaInputMapper implements ICreateProformaInputMapper {
private readonly taxCatalog: JsonTaxCatalogProvider;
constructor(catalogs: ICatalogs) {
this.taxCatalog = catalogs.taxCatalog;
}
public map(
dto: CreateProformaRequestDTO,
params: { companyId: UniqueID }
@ -238,16 +244,35 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
taxesDTO: CreateProformaRequestDTO["items"][number]["taxes"],
params: { itemIndex: number; errors: ValidationErrorDetail[] }
): ProformaItemTaxesProps {
if (taxesDTO === "#;#;#") {
const parts = taxesDTO.split(";");
if (parts.length !== 3) {
params.errors.push({
path: `items[${params.itemIndex}].taxes`,
message: "Tax combination must contain exactly three elements",
});
return ProformaItemTaxes.empty().getProps();
}
params.errors.push({
path: `items[${params.itemIndex}].taxes`,
message: "Tax combination mapping is not implemented yet",
});
const [ivaCode, recCode, retentionCode] = parts;
return ProformaItemTaxes.empty().getProps();
const iva = this.mapTaxCode(ivaCode, `items[${params.itemIndex}].taxes.iva`, params.errors);
const rec = this.mapTaxCode(recCode, `items[${params.itemIndex}].taxes.rec`, params.errors);
const retention = this.mapTaxCode(
retentionCode,
`items[${params.itemIndex}].taxes.retention`,
params.errors
);
return ProformaItemTaxes.create({ iva, rec, retention }).data.getProps();
}
private mapTaxCode(code: string, path: string, errors: ValidationErrorDetail[]): Maybe<Tax> {
if (code === "#") {
return Maybe.none();
}
const tax = extractOrPushError(Tax.createFromCode(code, this.taxCatalog), path, errors);
return tax ? Maybe.some(tax) : Maybe.none();
}
private throwIfValidationErrors(errors: ValidationErrorDetail[]): void {

View File

@ -1,4 +1,5 @@
import { DiscountPercentage } from "@erp/core/api";
import type { JsonTaxCatalogProvider } from "@erp/core";
import { DiscountPercentage, type ICatalogs, Tax } from "@erp/core/api";
import {
CurrencyCode,
DomainError,
@ -11,7 +12,7 @@ import {
extractOrPushError,
maybeFromNullableResult,
} from "@repo/rdx-ddd";
import { NumberHelper, Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils";
import { Maybe, NumberHelper, Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils";
import type { UpdateProformaByIdRequestDTO } from "../../../../common/dto";
import {
@ -47,6 +48,11 @@ export interface IUpdateProformaInputMapper {
*/
export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
private readonly taxCatalog: JsonTaxCatalogProvider;
constructor(catalogs: ICatalogs) {
this.taxCatalog = catalogs.taxCatalog;
}
public map(
dto: UpdateProformaByIdRequestDTO,
_params: { companyId: UniqueID }
@ -201,26 +207,20 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
);
const quantity = extractOrPushError(
maybeFromNullableResult(item.quantity, (value) =>
ItemQuantity.create({ value: NumberHelper.toSafeNumber(value) })
),
maybeFromNullableResult(item.quantity, (dto) => ItemQuantity.fromObjectString(dto)),
`items[${index}].quantity`,
params.errors
);
const unitAmount = extractOrPushError(
maybeFromNullableResult(item.unit_amount, (value) =>
ItemAmount.create({ value: NumberHelper.toSafeNumber(value) })
),
maybeFromNullableResult(item.unit_amount, (dto) => ItemAmount.fromObjectString(dto)),
`items[${index}].unit_amount`,
params.errors
);
const itemDiscountPercentage = extractOrPushError(
maybeFromNullableResult(item.item_discount_percentage, (value) =>
DiscountPercentage.create({
value: NumberHelper.toSafeNumber(value.value),
})
maybeFromNullableResult(item.item_discount_percentage, (dto) =>
DiscountPercentage.fromObjectString(dto)
),
`items[${index}].item_discount_percentage`,
params.errors
@ -246,24 +246,35 @@ export class UpdateProformaInputMapper implements IUpdateProformaInputMapper {
taxesDTO: NonNullable<UpdateProformaByIdRequestDTO["items"]>[number]["taxes"],
params: { itemIndex: number; errors: ValidationErrorDetail[] }
): ProformaItemTaxesProps {
if (taxesDTO === "#;#;#") {
const parts = taxesDTO.split(";");
if (parts.length !== 3) {
params.errors.push({
path: `items[${params.itemIndex}].taxes`,
message: "Tax combination must contain exactly three elements",
});
return ProformaItemTaxes.empty().getProps();
}
/**
* 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",
});
const [ivaCode, recCode, retentionCode] = parts;
return ProformaItemTaxes.empty().getProps();
const iva = this.mapTaxCode(ivaCode, `items[${params.itemIndex}].taxes.iva`, params.errors);
const rec = this.mapTaxCode(recCode, `items[${params.itemIndex}].taxes.rec`, params.errors);
const retention = this.mapTaxCode(
retentionCode,
`items[${params.itemIndex}].taxes.retention`,
params.errors
);
return ProformaItemTaxes.create({ iva, rec, retention }).data.getProps();
}
private mapTaxCode(code: string, path: string, errors: ValidationErrorDetail[]): Maybe<Tax> {
if (code === "#") {
return Maybe.none();
}
const tax = extractOrPushError(Tax.createFromCode(code, this.taxCatalog), path, errors);
return tax ? Maybe.some(tax) : Maybe.none();
}
private throwIfValidationErrors(errors: ValidationErrorDetail[]): void {

View File

@ -1 +0,0 @@
//export * from "./proformas";

View File

@ -1 +0,0 @@
//export * from "./proforma.full.presenter";

View File

@ -1,93 +0,0 @@
import { SnapshotBuilder } from "@erp/core/api";
import type { GetProformaByIdResponseDTO } from "@erp/customer-invoices/common";
import { maybeToEmptyString } from "@repo/rdx-ddd";
import type { ArrayElement } from "@repo/rdx-utils";
import type { CustomerInvoiceItems, IssuedInvoiceItem } from "../../../../domain";
type GetProformaItemByIdResponseDTO = ArrayElement<GetProformaByIdResponseDTO["items"]>;
export class ProformaItemsFullPresenter extends SnapshotBuilder {
private _mapItem(proformaItem: IssuedInvoiceItem, index: number): GetProformaItemByIdResponseDTO {
const allAmounts = proformaItem.calculateAllAmounts();
return {
id: proformaItem.id.toPrimitive(),
is_valued: String(proformaItem.isValued),
position: String(index),
description: maybeToEmptyString(proformaItem.description, (value) => value.toPrimitive()),
quantity: proformaItem.quantity.match(
(quantity) => quantity.toObjectString(),
() => ({ value: "", scale: "" })
),
unit_amount: proformaItem.unitAmount.match(
(unitAmount) => unitAmount.toObjectString(),
() => ({ value: "", scale: "", currency_code: "" })
),
subtotal_amount: allAmounts.subtotalAmount.toObjectString(),
discount_percentage: proformaItem.itemDiscountPercentage.match(
(discountPercentage) => discountPercentage.toObjectString(),
() => ({ value: "", scale: "" })
),
discount_amount: allAmounts.itemDiscountAmount.toObjectString(),
global_discount_percentage: proformaItem.globalDiscountPercentage.match(
(discountPercentage) => discountPercentage.toObjectString(),
() => ({ value: "", scale: "" })
),
global_discount_amount: allAmounts.globalDiscountAmount.toObjectString(),
taxable_amount: allAmounts.taxableAmount.toObjectString(),
iva_code: proformaItem.taxes.iva.match(
(iva) => iva.code,
() => ""
),
iva_percentage: proformaItem.taxes.iva.match(
(iva) => iva.percentage.toObjectString(),
() => ({ value: "", scale: "" })
),
iva_amount: allAmounts.ivaAmount.toObjectString(),
rec_code: proformaItem.taxes.rec.match(
(rec) => rec.code,
() => ""
),
rec_percentage: proformaItem.taxes.rec.match(
(rec) => rec.percentage.toObjectString(),
() => ({ value: "", scale: "" })
),
rec_amount: allAmounts.recAmount.toObjectString(),
retention_code: proformaItem.taxes.retention.match(
(retention) => retention.code,
() => ""
),
retention_percentage: proformaItem.taxes.retention.match(
(retention) => retention.percentage.toObjectString(),
() => ({ value: "", scale: "" })
),
retention_amount: allAmounts.retentionAmount.toObjectString(),
taxes_amount: allAmounts.taxesAmount.toObjectString(),
total_amount: allAmounts.totalAmount.toObjectString(),
};
}
toOutput(proformaItems: CustomerInvoiceItems): GetProformaByIdResponseDTO["items"] {
return proformaItems.map(this._mapItem);
}
}

View File

@ -1,46 +0,0 @@
import { SnapshotBuilder } from "@erp/core/api";
import { DomainValidationError, maybeToEmptyString } from "@repo/rdx-ddd";
import type { GetIssuedInvoiceByIdResponseDTO as GetProformaByIdResponseDTO } from "../../../../../common/dto";
import type { InvoiceRecipient, Proforma } from "../../../../domain";
type GetProformaRecipientByIdResponseDTO = GetProformaByIdResponseDTO["recipient"];
export class ProformaRecipientFullPresenter extends SnapshotBuilder {
toOutput(proforma: Proforma): GetProformaRecipientByIdResponseDTO {
if (!proforma.recipient) {
throw DomainValidationError.requiredValue("recipient", {
cause: proforma,
});
}
return proforma.recipient.match(
(recipient: InvoiceRecipient) => {
return {
id: proforma.customerId.toString(),
name: recipient.name.toString(),
tin: recipient.tin.toString(),
street: maybeToEmptyString(recipient.street, (value) => value.toString()),
street2: maybeToEmptyString(recipient.street2, (value) => value.toString()),
city: maybeToEmptyString(recipient.city, (value) => value.toString()),
province: maybeToEmptyString(recipient.province, (value) => value.toString()),
postal_code: maybeToEmptyString(recipient.postalCode, (value) => value.toString()),
country: maybeToEmptyString(recipient.country, (value) => value.toString()),
};
},
() => {
return {
id: "",
name: "",
tin: "",
street: "",
street2: "",
city: "",
province: "",
postal_code: "",
country: "",
};
}
);
}
}

View File

@ -1,130 +0,0 @@
import { Presenter } from "@erp/core/api";
import { maybeToEmptyString } from "@repo/rdx-ddd";
import type { GetProformaByIdResponseDTO } from "../../../../../common/dto";
import { InvoiceAmount, type Proforma } from "../../../../domain";
import type { ProformaItemsFullPresenter } from "./proforma-items.full.presenter";
import type { ProformaRecipientFullPresenter } from "./proforma-recipient.full.presenter";
export class ProformaFullPresenter extends Presenter<Proforma, GetProformaByIdResponseDTO> {
toOutput(proforma: Proforma): GetProformaByIdResponseDTO {
const itemsPresenter = this.presenterRegistry.getPresenter({
resource: "proforma-items",
projection: "FULL",
}) as ProformaItemsFullPresenter;
const recipientPresenter = this.presenterRegistry.getPresenter({
resource: "proforma-recipient",
projection: "FULL",
}) as ProformaRecipientFullPresenter;
const recipient = recipientPresenter.toOutput(proforma);
const items = itemsPresenter.toOutput(proforma.items);
const allAmounts = proforma.calculateAllAmounts();
const payment = proforma.paymentMethod.match(
(payment) => {
const { id, payment_description } = payment.toObjectString();
return {
payment_id: id,
payment_description,
};
},
() => undefined
);
let totalIvaAmount = InvoiceAmount.zero(proforma.currencyCode.code);
let totalRecAmount = InvoiceAmount.zero(proforma.currencyCode.code);
let totalRetentionAmount = InvoiceAmount.zero(proforma.currencyCode.code);
const invoiceTaxes = proforma.getTaxes().map((taxGroup) => {
const { ivaAmount, recAmount, retentionAmount, totalAmount } = taxGroup.calculateAmounts();
totalIvaAmount = totalIvaAmount.add(ivaAmount);
totalRecAmount = totalRecAmount.add(recAmount);
totalRetentionAmount = totalRetentionAmount.add(retentionAmount);
return {
taxable_amount: taxGroup.taxableAmount.toObjectString(),
iva_code: taxGroup.iva.code,
iva_percentage: taxGroup.iva.percentage.toObjectString(),
iva_amount: ivaAmount.toObjectString(),
rec_code: taxGroup.rec.match(
(rec) => rec.code,
() => ""
),
rec_percentage: taxGroup.rec.match(
(rec) => rec.percentage.toObjectString(),
() => ({ value: "", scale: "" })
),
rec_amount: recAmount.toObjectString(),
retention_code: taxGroup.retention.match(
(retention) => retention.code,
() => ""
),
retention_percentage: taxGroup.retention.match(
(retention) => retention.percentage.toObjectString(),
() => ({ value: "", scale: "" })
),
retention_amount: retentionAmount.toObjectString(),
taxes_amount: totalAmount.toObjectString(),
};
});
return {
id: proforma.id.toString(),
company_id: proforma.companyId.toString(),
invoice_number: proforma.invoiceNumber.toString(),
status: proforma.status.toPrimitive(),
series: maybeToEmptyString(proforma.series, (value) => value.toString()),
invoice_date: proforma.invoiceDate.toDateString(),
operation_date: maybeToEmptyString(proforma.operationDate, (value) => value.toDateString()),
reference: maybeToEmptyString(proforma.reference, (value) => value.toString()),
description: maybeToEmptyString(proforma.description, (value) => value.toString()),
notes: maybeToEmptyString(proforma.notes, (value) => value.toString()),
language_code: proforma.languageCode.toString(),
currency_code: proforma.currencyCode.toString(),
customer_id: proforma.customerId.toString(),
recipient,
taxes: invoiceTaxes,
payment_method: payment,
subtotal_amount: allAmounts.subtotalAmount.toObjectString(),
items_discount_amount: allAmounts.itemDiscountAmount.toObjectString(),
discount_percentage: proforma.globalDiscountPercentage.toObjectString(),
discount_amount: allAmounts.globalDiscountAmount.toObjectString(),
taxable_amount: allAmounts.taxableAmount.toObjectString(),
iva_amount: totalIvaAmount.toObjectString(),
rec_amount: totalRecAmount.toObjectString(),
retention_amount: totalRetentionAmount.toObjectString(),
taxes_amount: allAmounts.taxesAmount.toObjectString(),
total_amount: allAmounts.totalAmount.toObjectString(),
items,
metadata: {
entity: "proforma",
},
};
}
}

View File

@ -1,3 +0,0 @@
//export * from "./domain";
//export * from "./queries";
//export * from "./reports";

View File

@ -1,2 +0,0 @@
export * from "./issued-invoices";
export * from "./proformas";

View File

@ -1 +0,0 @@
export * from "./issued-invoice.list.presenter";

View File

@ -1,82 +0,0 @@
import type { Criteria } from "@repo/rdx-criteria/server";
import { maybeToEmptyString } from "@repo/rdx-ddd";
import type { ArrayElement, Collection } from "@repo/rdx-utils";
import type { ListIssuedInvoicesResponseDTO } from "../../../../../common/dto";
export class IssuedInvoiceListPresenter extends Presenter {
protected _mapInvoice(invoice: CustomerInvoiceListDTO) {
const recipientDTO = invoice.recipient.toObjectString();
const verifactuDTO = invoice.verifactu.match(
(verifactu) => verifactu.toObjectString(),
() => ({
status: "",
url: "",
qr_code: "",
})
);
const invoiceDTO: ArrayElement<ListIssuedInvoicesResponseDTO["items"]> = {
id: invoice.id.toString(),
company_id: invoice.companyId.toString(),
customer_id: invoice.customerId.toString(),
invoice_number: invoice.invoiceNumber.toString(),
status: invoice.status.toPrimitive(),
series: maybeToEmptyString(invoice.series, (value) => value.toString()),
invoice_date: invoice.invoiceDate.toDateString(),
operation_date: maybeToEmptyString(invoice.operationDate, (value) => value.toDateString()),
reference: maybeToEmptyString(invoice.reference, (value) => value.toString()),
description: maybeToEmptyString(invoice.description, (value) => value.toString()),
recipient: recipientDTO,
language_code: invoice.languageCode.code,
currency_code: invoice.currencyCode.code,
subtotal_amount: invoice.subtotalAmount.toObjectString(),
discount_percentage: invoice.discountPercentage.toObjectString(),
discount_amount: invoice.discountAmount.toObjectString(),
taxable_amount: invoice.taxableAmount.toObjectString(),
taxes_amount: invoice.taxesAmount.toObjectString(),
total_amount: invoice.totalAmount.toObjectString(),
verifactu: verifactuDTO,
metadata: {
entity: "issued-invoice",
},
};
return invoiceDTO;
}
toOutput(params: {
invoices: Collection<CustomerInvoiceListDTO>;
criteria: Criteria;
}): ListIssuedInvoicesResponseDTO {
const { invoices, criteria } = params;
const _invoices = invoices.map((invoice) => this._mapInvoice(invoice));
const _totalItems = invoices.total();
return {
page: criteria.pageNumber,
per_page: criteria.pageSize,
total_pages: Math.ceil(_totalItems / criteria.pageSize),
total_items: _totalItems,
items: _invoices,
metadata: {
entity: "issued-invoices",
criteria: criteria.toJSON(),
//links: {
// self: `/api/customer-invoices?page=${criteria.pageNumber}&per_page=${criteria.pageSize}`,
// first: `/api/customer-invoices?page=1&per_page=${criteria.pageSize}`,
// last: `/api/customer-invoices?page=${Math.ceil(totalItems / criteria.pageSize)}&per_page=${criteria.pageSize}`,
//},
},
};
}
}

View File

@ -1 +0,0 @@
export * from "./proforma.list.presenter";

View File

@ -1,74 +0,0 @@
import { Presenter } from "@erp/core/api";
import type { Criteria } from "@repo/rdx-criteria/server";
import { maybeToEmptyString } from "@repo/rdx-ddd";
import type { ArrayElement, Collection } from "@repo/rdx-utils";
import type { ListProformasResponseDTO } from "../../../../../common/dto";
import type { CustomerInvoiceListDTO } from "../../../../infrastructure";
export class ProformaListPresenter extends Presenter {
protected _mapProforma(proforma: CustomerInvoiceListDTO) {
const recipientDTO = proforma.recipient.toObjectString();
const invoiceDTO: ArrayElement<ListProformasResponseDTO["items"]> = {
id: proforma.id.toString(),
company_id: proforma.companyId.toString(),
is_proforma: proforma.isProforma,
customer_id: proforma.customerId.toString(),
invoice_number: proforma.invoiceNumber.toString(),
status: proforma.status.toPrimitive(),
series: maybeToEmptyString(proforma.series, (value) => value.toString()),
invoice_date: proforma.invoiceDate.toDateString(),
operation_date: maybeToEmptyString(proforma.operationDate, (value) => value.toDateString()),
reference: maybeToEmptyString(proforma.reference, (value) => value.toString()),
description: maybeToEmptyString(proforma.description, (value) => value.toString()),
recipient: recipientDTO,
language_code: proforma.languageCode.code,
currency_code: proforma.currencyCode.code,
subtotal_amount: proforma.subtotalAmount.toObjectString(),
discount_percentage: proforma.discountPercentage.toObjectString(),
discount_amount: proforma.discountAmount.toObjectString(),
taxable_amount: proforma.taxableAmount.toObjectString(),
taxes_amount: proforma.taxesAmount.toObjectString(),
total_amount: proforma.totalAmount.toObjectString(),
metadata: {
entity: "proforma",
},
};
return invoiceDTO;
}
toOutput(params: {
proformas: Collection<CustomerInvoiceListDTO>;
criteria: Criteria;
}): ListProformasResponseDTO {
const { proformas, criteria } = params;
const _proformas = proformas.map((proforma) => this._mapProforma(proforma));
const _totalItems = proformas.total();
return {
page: criteria.pageNumber,
per_page: criteria.pageSize,
total_pages: Math.ceil(_totalItems / criteria.pageSize),
total_items: _totalItems,
items: _proformas,
metadata: {
entity: "proformas",
criteria: criteria.toJSON(),
//links: {
// self: `/api/customer-invoices?page=${criteria.pageNumber}&per_page=${criteria.pageSize}`,
// first: `/api/customer-invoices?page=1&per_page=${criteria.pageSize}`,
// last: `/api/customer-invoices?page=${Math.ceil(totalItems / criteria.pageSize)}&per_page=${criteria.pageSize}`,
//},
},
};
}
}

View File

@ -1,2 +0,0 @@
export * from "./issued-invoices";
//export * from "./proformas";

View File

@ -1,3 +0,0 @@
//export * from "./issued-invoice.report.presenter";
//export * from "./issued-invoice-items.report.presenter";
//export * from "./issued-invoice-taxes.report.presenter";

View File

@ -1,59 +0,0 @@
import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/core";
import { type ISnapshotBuilderParams, Presenter } from "@erp/core/api";
import type { GetIssuedInvoiceByIdResponseDTO } from "@erp/customer-invoices/common";
import type { ArrayElement } from "@repo/rdx-utils";
type IssuedInvoiceItemsDTO = GetIssuedInvoiceByIdResponseDTO["items"];
type IssuedInvoiceItemDTO = ArrayElement<IssuedInvoiceItemsDTO>;
export class IssuedInvoiceItemsReportPresenter extends Presenter<IssuedInvoiceItemsDTO, unknown> {
private _locale!: string;
private _mapItem(invoiceItem: IssuedInvoiceItemDTO, _index: number) {
const moneyOptions = {
hideZeros: true,
minimumFractionDigits: 2,
};
return {
...invoiceItem,
quantity: QuantityDTOHelper.format(invoiceItem.quantity, this._locale, {
minimumFractionDigits: 0,
}),
unit_amount: MoneyDTOHelper.format(invoiceItem.unit_amount, this._locale, moneyOptions),
subtotal_amount: MoneyDTOHelper.format(
invoiceItem.subtotal_amount,
this._locale,
moneyOptions
),
discount_percentage: PercentageDTOHelper.format(
invoiceItem.discount_percentage,
this._locale,
{
minimumFractionDigits: 0,
}
),
discount_amount: MoneyDTOHelper.format(
invoiceItem.discount_amount,
this._locale,
moneyOptions
),
taxable_amount: MoneyDTOHelper.format(invoiceItem.taxable_amount, this._locale, moneyOptions),
taxes_amount: MoneyDTOHelper.format(invoiceItem.taxes_amount, this._locale, moneyOptions),
total_amount: MoneyDTOHelper.format(invoiceItem.total_amount, this._locale, moneyOptions),
};
}
toOutput(issuedInvoiceItems: IssuedInvoiceItemsDTO, params: ISnapshotBuilderParams): unknown {
const { locale } = params as {
locale: string;
};
this._locale = locale;
return issuedInvoiceItems.map((item, index) => {
return this._mapItem(item, index);
});
}
}

View File

@ -1,57 +0,0 @@
import {
type JsonTaxCatalogProvider,
MoneyDTOHelper,
PercentageDTOHelper,
SpainTaxCatalogProvider,
} from "@erp/core";
import { type ISnapshotBuilderParams, Presenter } from "@erp/core/api";
import type { GetIssuedInvoiceByIdResponseDTO } from "@erp/customer-invoices/common";
import type { ArrayElement } from "@repo/rdx-utils";
type IssuedInvoiceTaxesDTO = GetIssuedInvoiceByIdResponseDTO["taxes"];
type IssuedInvoiceTaxDTO = ArrayElement<IssuedInvoiceTaxesDTO>;
export class IssuedInvoiceTaxesReportPresenter extends Presenter<IssuedInvoiceTaxesDTO, unknown> {
private _locale!: string;
private _taxCatalog!: JsonTaxCatalogProvider;
private _mapTax(taxItem: IssuedInvoiceTaxDTO) {
const moneyOptions = {
hideZeros: true,
minimumFractionDigits: 2,
};
//const taxCatalogItem = this._taxCatalog.findByCode(taxItem.tax_code);
return {
taxable_amount: MoneyDTOHelper.format(taxItem.taxable_amount, this._locale, moneyOptions),
iva_code: taxItem.iva_code,
iva_percentage: PercentageDTOHelper.format(taxItem.iva_percentage, this._locale),
iva_amount: MoneyDTOHelper.format(taxItem.iva_amount, this._locale, moneyOptions),
rec_code: taxItem.rec_code,
rec_percentage: PercentageDTOHelper.format(taxItem.rec_percentage, this._locale),
rec_amount: MoneyDTOHelper.format(taxItem.rec_amount, this._locale, moneyOptions),
retention_code: taxItem.retention_code,
retention_percentage: PercentageDTOHelper.format(taxItem.retention_percentage, this._locale),
retention_amount: MoneyDTOHelper.format(taxItem.rec_amount, this._locale, moneyOptions),
taxes_amount: MoneyDTOHelper.format(taxItem.taxes_amount, this._locale, moneyOptions),
};
}
toOutput(taxes: IssuedInvoiceTaxesDTO, params: ISnapshotBuilderParams): unknown {
const { locale } = params as {
locale: string;
};
this._locale = locale;
this._taxCatalog = SpainTaxCatalogProvider();
return taxes?.map((item, _index) => {
return this._mapTax(item);
});
}
}

View File

@ -1,118 +0,0 @@
import { MoneyDTOHelper, PercentageDTOHelper } from "@erp/core";
import { Presenter } from "@erp/core/api";
import { DateHelper } from "@repo/rdx-utils";
import type { GetIssuedInvoiceByIdResponseDTO } from "../../../../../common/dto";
export class IssuedInvoiceReportPresenter extends Presenter<
GetIssuedInvoiceByIdResponseDTO,
unknown
> {
private _formatPaymentMethodDTO(
paymentMethod?: GetIssuedInvoiceByIdResponseDTO["payment_method"]
) {
if (!paymentMethod) {
return "";
}
return paymentMethod.description ?? "";
}
toOutput(issuedInvoiceDTO: GetIssuedInvoiceByIdResponseDTO) {
const itemsPresenter = this.presenterRegistry.getPresenter({
resource: "issued-invoice-items",
projection: "REPORT",
format: "DTO",
});
const taxesPresenter = this.presenterRegistry.getPresenter({
resource: "issued-invoice-taxes",
projection: "REPORT",
format: "DTO",
});
const locale = issuedInvoiceDTO.language_code;
const itemsDTO = itemsPresenter.toOutput(issuedInvoiceDTO.items, {
locale,
});
const taxesDTO = taxesPresenter.toOutput(issuedInvoiceDTO.taxes, {
locale,
});
const moneyOptions = {
hideZeros: true,
minimumFractionDigits: 2,
};
return {
...issuedInvoiceDTO,
taxes: taxesDTO,
items: itemsDTO,
recipient: {
...issuedInvoiceDTO.recipient,
format_address: this.formatAddress(issuedInvoiceDTO.recipient),
},
invoice_date: DateHelper.format(issuedInvoiceDTO.invoice_date, locale),
subtotal_amount: MoneyDTOHelper.format(
issuedInvoiceDTO.subtotal_amount,
locale,
moneyOptions
),
discount_percentage: PercentageDTOHelper.format(
issuedInvoiceDTO.discount_percentage,
locale,
{ hideZeros: true }
),
discount_amount: MoneyDTOHelper.format(
issuedInvoiceDTO.discount_amount,
locale,
moneyOptions
),
taxable_amount: MoneyDTOHelper.format(issuedInvoiceDTO.taxable_amount, locale, moneyOptions),
taxes_amount: MoneyDTOHelper.format(issuedInvoiceDTO.taxes_amount, locale, moneyOptions),
total_amount: MoneyDTOHelper.format(issuedInvoiceDTO.total_amount, locale, moneyOptions),
payment_method: this._formatPaymentMethodDTO(issuedInvoiceDTO.payment_method),
verifactu: {
...issuedInvoiceDTO.verifactu,
qr_code: issuedInvoiceDTO.verifactu.qr_code.replace("data:image/png;base64,", ""),
},
};
}
protected formatAddress(recipient: GetIssuedInvoiceByIdResponseDTO["recipient"]): string {
const lines: string[] = [];
// Líneas de calle
if (recipient.street) {
lines.push(recipient.street);
}
if (recipient.street2) {
lines.push(recipient.street2);
}
// Ciudad + código postal
const cityLine = [recipient.postal_code, recipient.city].filter(Boolean).join(" ");
if (cityLine) {
lines.push(cityLine);
}
// Provincia
if (recipient.province && recipient.province !== recipient.city) {
lines.push(recipient.province);
}
// País
if (recipient.country && recipient.country !== "es") {
lines.push(recipient.country);
}
return lines.join("\n");
}
}

View File

@ -1,3 +0,0 @@
export * from "./proforma.report.presenter";
export * from "./proforma-items.report.presenter";
export * from "./proforma-taxes.report.presenter";

View File

@ -1,63 +0,0 @@
import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/core";
import { type ISnapshotBuilderParams, Presenter } from "@erp/core/api";
import type { GetProformaByIdResponseDTO } from "@erp/customer-invoices/common";
import type { ArrayElement } from "@repo/rdx-utils";
type ProformaItemsDTO = GetProformaByIdResponseDTO["items"];
type ProformaItemDTO = ArrayElement<ProformaItemsDTO>;
export class ProformaItemsReportPresenter extends Presenter<ProformaItemsDTO, unknown> {
private _locale!: string;
private _mapItem(proformaItem: ProformaItemDTO, _index: number) {
const moneyOptions = {
hideZeros: true,
minimumFractionDigits: 0,
};
return {
...proformaItem,
quantity: QuantityDTOHelper.format(proformaItem.quantity, this._locale, {
minimumFractionDigits: 0,
}),
unit_amount: MoneyDTOHelper.format(proformaItem.unit_amount, this._locale, moneyOptions),
subtotal_amount: MoneyDTOHelper.format(
proformaItem.subtotal_amount,
this._locale,
moneyOptions
),
discount_percentage: PercentageDTOHelper.format(
proformaItem.discount_percentage,
this._locale,
{
minimumFractionDigits: 0,
}
),
discount_amount: MoneyDTOHelper.format(
proformaItem.discount_amount,
this._locale,
moneyOptions
),
taxable_amount: MoneyDTOHelper.format(
proformaItem.taxable_amount,
this._locale,
moneyOptions
),
taxes_amount: MoneyDTOHelper.format(proformaItem.taxes_amount, this._locale, moneyOptions),
total_amount: MoneyDTOHelper.format(proformaItem.total_amount, this._locale, moneyOptions),
};
}
toOutput(proformaItems: ProformaItemsDTO, params: ISnapshotBuilderParams): unknown {
const { locale } = params as {
locale: string;
};
this._locale = locale;
return proformaItems.map((item, index) => {
return this._mapItem(item, index);
});
}
}

View File

@ -1,57 +0,0 @@
import {
type JsonTaxCatalogProvider,
MoneyDTOHelper,
PercentageDTOHelper,
SpainTaxCatalogProvider,
} from "@erp/core";
import { type ISnapshotBuilderParams, Presenter } from "@erp/core/api";
import type { GetProformaByIdResponseDTO } from "@erp/customer-invoices/common";
import type { ArrayElement } from "@repo/rdx-utils";
type ProformaTaxesDTO = GetProformaByIdResponseDTO["taxes"];
type ProformaTaxDTO = ArrayElement<ProformaTaxesDTO>;
export class ProformaTaxesReportPresenter extends Presenter<ProformaTaxesDTO, unknown> {
private _locale!: string;
private _taxCatalog!: JsonTaxCatalogProvider;
private _mapTax(taxItem: ProformaTaxDTO) {
const moneyOptions = {
hideZeros: true,
minimumFractionDigits: 0,
};
//const taxCatalogItem = this._taxCatalog.findByCode(taxItem.tax_code);
return {
taxable_amount: MoneyDTOHelper.format(taxItem.taxable_amount, this._locale, moneyOptions),
iva_code: taxItem.iva_code,
iva_percentage: PercentageDTOHelper.format(taxItem.iva_percentage, this._locale),
iva_amount: MoneyDTOHelper.format(taxItem.iva_amount, this._locale, moneyOptions),
rec_code: taxItem.rec_code,
rec_percentage: PercentageDTOHelper.format(taxItem.rec_percentage, this._locale),
rec_amount: MoneyDTOHelper.format(taxItem.rec_amount, this._locale, moneyOptions),
retention_code: taxItem.retention_code,
retention_percentage: PercentageDTOHelper.format(taxItem.retention_percentage, this._locale),
retention_amount: MoneyDTOHelper.format(taxItem.rec_amount, this._locale, moneyOptions),
taxes_amount: MoneyDTOHelper.format(taxItem.taxes_amount, this._locale, moneyOptions),
};
}
toOutput(taxes: ProformaTaxesDTO, params: ISnapshotBuilderParams): unknown {
const { locale } = params as {
locale: string;
};
this._locale = locale;
this._taxCatalog = SpainTaxCatalogProvider();
return taxes.map((item, _index) => {
return this._mapTax(item);
});
}
}

View File

@ -1,58 +0,0 @@
import { DateHelper, MoneyDTOHelper, PercentageDTOHelper } from "@erp/core";
import { Presenter } from "@erp/core/api";
import type { GetProformaByIdResponseDTO } from "../../../../../common/dto";
export class ProformaReportPresenter extends Presenter<GetProformaByIdResponseDTO, unknown> {
private _formatPaymentMethodDTO(paymentMethod?: GetProformaByIdResponseDTO["payment_method"]) {
if (!paymentMethod) {
return "";
}
return paymentMethod.description ?? "";
}
toOutput(proformaDTO: GetProformaByIdResponseDTO) {
const itemsPresenter = this.presenterRegistry.getPresenter({
resource: "proforma-items",
projection: "REPORT",
format: "JSON",
});
const taxesPresenter = this.presenterRegistry.getPresenter({
resource: "proforma-taxes",
projection: "REPORT",
format: "JSON",
});
const locale = proformaDTO.language_code;
const itemsDTO = itemsPresenter.toOutput(proformaDTO.items, {
locale,
});
const taxesDTO = taxesPresenter.toOutput(proformaDTO.taxes, {
locale,
});
const moneyOptions = {
hideZeros: true,
minimumFractionDigits: 0,
};
return {
...proformaDTO,
taxes: taxesDTO,
items: itemsDTO,
invoice_date: DateHelper.format(proformaDTO.invoice_date, locale),
subtotal_amount: MoneyDTOHelper.format(proformaDTO.subtotal_amount, locale, moneyOptions),
discount_percentage: PercentageDTOHelper.format(proformaDTO.discount_percentage, locale),
discount_amount: MoneyDTOHelper.format(proformaDTO.discount_amount, locale, moneyOptions),
taxable_amount: MoneyDTOHelper.format(proformaDTO.taxable_amount, locale, moneyOptions),
taxes_amount: MoneyDTOHelper.format(proformaDTO.taxes_amount, locale, moneyOptions),
total_amount: MoneyDTOHelper.format(proformaDTO.total_amount, locale, moneyOptions),
payment_method: this._formatPaymentMethodDTO(proformaDTO.payment_method),
};
}
}

View File

@ -1,12 +1,32 @@
import { MoneyValue, type MoneyValueProps, type Percentage, type Quantity } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import {
MoneyValue,
type MoneyValueProps,
type Percentage,
type Quantity,
ValidationErrorCollection,
} from "@repo/rdx-ddd";
import { NumberHelper, Result } from "@repo/rdx-utils";
type InvoiceAmountProps = Pick<MoneyValueProps, "value" | "currency_code">;
type InvoiceAmountProps = MoneyValueProps;
type InvoiceAmountObjectString = {
value: string;
scale: string;
currency_code: string;
};
export class InvoiceAmount extends MoneyValue {
public static DEFAULT_SCALE = 2;
static create({ value, currency_code }: InvoiceAmountProps) {
static create({ value, currency_code, scale }: InvoiceAmountProps) {
if (scale && scale !== InvoiceAmount.DEFAULT_SCALE) {
return Result.fail(
new ValidationErrorCollection("InvalidScale", [
{ message: `InvoiceAmount scale must be ${InvoiceAmount.DEFAULT_SCALE}` },
])
);
}
const props = {
value: Number(value),
scale: InvoiceAmount.DEFAULT_SCALE,
@ -23,7 +43,26 @@ export class InvoiceAmount extends MoneyValue {
return InvoiceAmount.create(props).data;
}
toObjectString() {
static fromObjectString(dto: InvoiceAmountObjectString) {
const value = NumberHelper.toSafeNumber(dto.value);
const scale = dto.scale ? NumberHelper.toSafeNumber(dto.scale) : InvoiceAmount.DEFAULT_SCALE;
if (!(Number.isFinite(value) && Number.isInteger(scale))) {
return Result.fail(
new ValidationErrorCollection("InvalidNumericValues", [
{ message: "InvoiceAmount payload contains invalid numeric values" },
])
);
}
return InvoiceAmount.create({
value,
scale,
currency_code: dto.currency_code,
});
}
toObjectString(): InvoiceAmountObjectString {
return {
value: String(this.value),
scale: String(this.scale),

View File

@ -1,14 +1,34 @@
import { MoneyValue, type MoneyValueProps, type Percentage, type Quantity } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import {
MoneyValue,
type MoneyValueProps,
type Percentage,
type Quantity,
ValidationErrorCollection,
} from "@repo/rdx-ddd";
import { NumberHelper, Result } from "@repo/rdx-utils";
type ItemAmountProps = Pick<MoneyValueProps, "value" | "currency_code">;
type ItemAmountProps = MoneyValueProps;
type ItemAmountObjectString = {
value: string;
scale: string;
currency_code: string;
};
export class ItemAmount extends MoneyValue {
public static DEFAULT_SCALE = 4;
static create({ value, currency_code }: ItemAmountProps) {
static create({ value, currency_code, scale }: ItemAmountProps) {
if (scale && scale !== ItemAmount.DEFAULT_SCALE) {
return Result.fail(
new ValidationErrorCollection("InvalidScale", [
{ message: `ItemAmount scale must be ${ItemAmount.DEFAULT_SCALE}` },
])
);
}
const props = {
value: Number(value),
value,
scale: ItemAmount.DEFAULT_SCALE,
currency_code,
};
@ -23,7 +43,26 @@ export class ItemAmount extends MoneyValue {
return ItemAmount.create(props).data;
}
toObjectString() {
static fromObjectString(dto: ItemAmountObjectString) {
const value = NumberHelper.toSafeNumber(dto.value);
const scale = dto.scale ? NumberHelper.toSafeNumber(dto.scale) : ItemAmount.DEFAULT_SCALE;
if (!(Number.isFinite(value) && Number.isInteger(scale))) {
return Result.fail(
new ValidationErrorCollection("InvalidNumericValues", [
{ message: "ItemAmount payload contains invalid numeric values" },
])
);
}
return ItemAmount.create({
value,
scale,
currency_code: dto.currency_code,
});
}
toObjectString(): ItemAmountObjectString {
return {
value: String(this.value),
scale: String(this.scale),

View File

@ -1,15 +1,26 @@
import { Quantity, type QuantityProps } from "@repo/rdx-ddd";
import { Quantity, type QuantityProps, ValidationErrorCollection } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
type ItemQuantityProps = Pick<QuantityProps, "value">;
type ItemQuantityProps = QuantityProps;
export class ItemQuantity extends Quantity {
public static DEFAULT_SCALE = 2;
static create({ value }: ItemQuantityProps) {
return Quantity.create({
value,
scale: ItemQuantity.DEFAULT_SCALE,
});
static create({ value, scale }: ItemQuantityProps) {
if (scale && scale !== ItemQuantity.DEFAULT_SCALE) {
return Result.fail(
new ValidationErrorCollection("InvalidScale", [
{ message: `ItemQuantity scale must be ${ItemQuantity.DEFAULT_SCALE}` },
])
);
}
return Result.ok(
new ItemQuantity({
value,
scale: ItemQuantity.DEFAULT_SCALE,
})
);
}
static zero() {

View File

@ -23,7 +23,7 @@ export const buildProformaPersistenceMappers = (
const listMapper = new SequelizeProformaSummaryMapper();
// Mappers el DTO a las props validadas (CustomerProps) y luego construir agregado
const createMapper = new CreateProformaInputMapper();
const createMapper = new CreateProformaInputMapper(catalogs);
return {
domainMapper,

View File

@ -61,6 +61,8 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
parent: Partial<IProformaCreateProps>;
};
const { currencyCode } = parent;
const itemId = extractOrPushError(
UniqueID.create(raw.item_id),
`items[${index}].item_id`,
@ -74,14 +76,20 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
);
const quantity = extractOrPushError(
maybeFromNullableResult(raw.quantity_value, (v) => ItemQuantity.create({ value: v })),
maybeFromNullableResult(raw.quantity_value, (v) =>
ItemQuantity.create({ value: v, scale: raw.quantity_scale })
),
`items[${index}].quantity_value`,
errors
);
const unitAmount = extractOrPushError(
maybeFromNullableResult(raw.unit_amount_value, (value) =>
ItemAmount.create({ value, currency_code: parent.currencyCode?.code })
ItemAmount.create({
value,
currency_code: currencyCode?.code,
scale: raw.unit_amount_scale,
})
),
`items[${index}].unit_amount_value`,
errors
@ -89,7 +97,10 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
const itemDiscountPercentage = extractOrPushError(
maybeFromNullableResult(raw.item_discount_percentage_value, (v) =>
DiscountPercentage.create({ value: v })
DiscountPercentage.create({
value: v,
scale: raw.item_discount_percentage_scale,
})
),
`items[${index}].item_discount_percentage`,
errors

View File

@ -2,8 +2,9 @@ import {
CurrencyCodeSchema,
IsoDateSchema,
LanguageCodeSchema,
NumericStringSchema,
MoneySchema,
PercentageSchema,
QuantitySchema,
} from "@erp/core";
import { z } from "zod/v4";
@ -15,8 +16,8 @@ export const UpdateProformaItemRequestSchema = z.object({
description: z.string().nullable(),
quantity: NumericStringSchema.nullable(),
unit_amount: NumericStringSchema.nullable(),
quantity: QuantitySchema.nullable(),
unit_amount: MoneySchema.nullable(),
item_discount_percentage: PercentageSchema.nullable(),

View File

@ -6,8 +6,9 @@ import { TaxesBreakdownSchema } from "../taxes-breakdown.dto";
export const ProformaItemDetailSchema = z.object({
id: z.uuid(),
is_valued: z.boolean(),
position: ItemPositionSchema,
is_valued: z.boolean(),
description: z.string().nullable(),
quantity: QuantitySchema.nullable(),

View File

@ -11,7 +11,6 @@ import type { ProformaItemUpdateForm } from "../entities";
export const mapProformaItemsToProformaItemsUpdateForm = (
item: ProformaItem
): ProformaItemUpdateForm => {
console.log(item);
return {
id: item.id,
position: item.position,

View File

@ -74,8 +74,6 @@ export const useUpdateProformaController = (
const initialValues = useMemo<ProformaUpdateForm>(() => {
if (!proformaData) return buildProformaUpdateDefault();
console.log("initialValues", proformaData);
return mapProformaToProformaUpdateForm(proformaData);
}, [proformaData]);
@ -160,7 +158,7 @@ export const useUpdateProformaController = (
console.log("Parche de actualización construido:", patchData);
const params = buildUpdateProformaByIdParams(proformaId, patchData);
const params = buildUpdateProformaByIdParams(proformaId, patchData, formData);
console.log("Enviando actualización con params:", params);

View File

@ -2,7 +2,7 @@ import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/cor
import { ObjectHelper } from "@repo/rdx-utils";
import type { UpdateProformaByIdParams } from "../../shared/api";
import type { ProformaItemUpdatePatch, ProformaUpdatePatch } from "../entities";
import type { ProformaItemUpdatePatch, ProformaUpdateForm, ProformaUpdatePatch } from "../entities";
/**
* Convierte el patch del formulario de actualización de proforma
@ -21,12 +21,18 @@ import type { ProformaItemUpdatePatch, ProformaUpdatePatch } from "../entities";
export const buildUpdateProformaByIdParams = (
id: string,
patch: ProformaUpdatePatch
patch: ProformaUpdatePatch,
formData: ProformaUpdateForm
): UpdateProformaByIdParams => {
if (!id) {
throw new Error("proformaId is required");
}
//const currencyCode = formData.currencyCode;
//const languageCode = formData.languageCode;
console.log("PATCH => ", patch);
const data: UpdateProformaByIdParams["data"] = {};
if (ObjectHelper.hasOwn(patch, "series")) {
@ -73,9 +79,11 @@ export const buildUpdateProformaByIdParams = (
}
if (ObjectHelper.hasOwn(patch, "items")) {
data.items = patch.items?.map(toProformaItemUpdateDTO);
data.items = patch.items?.map((item, index) => toProformaItemUpdateDTO(item, index, formData));
}
console.log("DATA => ", data);
return {
id,
data,
@ -83,13 +91,20 @@ export const buildUpdateProformaByIdParams = (
};
const toProformaItemUpdateDTO = (
item: ProformaItemUpdatePatch
item: ProformaItemUpdatePatch,
_index: number,
formData: ProformaUpdateForm
): NonNullable<UpdateProformaByIdParams["data"]["items"]>[number] => {
const currencyCode = formData.currencyCode;
//const languageCode = formData.languageCode;
const quantity =
item.quantity === null ? null : QuantityDTOHelper.fromNumber(item.quantity, 4).value;
item.quantity === null ? null : QuantityDTOHelper.fromNumberNulleable(item.quantity, 2);
const unit_amount =
item.unitAmount === null ? null : MoneyDTOHelper.fromNumber(item.unitAmount, "EUR", 2).value;
item.unitAmount === null
? null
: MoneyDTOHelper.fromNumberNulleable(item.unitAmount, currencyCode, 4);
const is_valued = item.isValued;
@ -105,8 +120,8 @@ const toProformaItemUpdateDTO = (
item_discount_percentage:
item.itemDiscountPercentage === null
? null
: PercentageDTOHelper.fromNumber(item.itemDiscountPercentage, 2),
: PercentageDTOHelper.fromNumber(item.itemDiscountPercentage),
taxes: "#;#;#",
taxes: "#;#;#", // TODO: CAMBIAR!!!!
};
};

View File

@ -1,6 +1,8 @@
import { Result } from "@repo/rdx-utils";
import DineroFactory, { type Currency, type Dinero } from "dinero.js";
import type { DomainError } from "../errors";
import type { Percentage } from "./percentage";
import type { Quantity } from "./quantity";
import { ValueObject } from "./value-object";
@ -55,7 +57,7 @@ export class MoneyValue extends ValueObject<MoneyValueProps> implements IMoneyVa
static DEFAULT_CURRENCY_CODE = DEFAULT_CURRENCY_CODE;
static EMPTY_MONEY_OBJECT = { value: "", scale: "", currency_code: "" };
static create({ value, currency_code, scale }: MoneyValueProps) {
static create({ value, currency_code, scale }: MoneyValueProps): Result<MoneyValue, DomainError> {
const props = {
value: Number(value),
scale: scale ?? MoneyValue.DEFAULT_SCALE,

View File

@ -1,6 +1,7 @@
import { Result } from "@repo/rdx-utils";
import { NumberHelper, Result } from "@repo/rdx-utils";
import { z } from "zod/v4";
import { ValidationErrorCollection } from "../errors";
import { translateZodValidationError } from "../helpers";
import { ValueObject } from "./value-object";
@ -15,9 +16,14 @@ const DEFAULT_MAX_SCALE = 2;
export interface PercentageProps {
value: number;
scale: number;
scale?: number;
}
type PercentageObjectString = {
value: string;
scale: string;
};
export class Percentage extends ValueObject<PercentageProps> {
static DEFAULT_SCALE = DEFAULT_SCALE;
static MIN_VALUE = DEFAULT_MIN_VALUE;
@ -70,12 +76,30 @@ export class Percentage extends ValueObject<PercentageProps> {
return Percentage.create({ value: 0, scale: Percentage.DEFAULT_SCALE }).data;
}
static fromObjectString(dto: PercentageObjectString) {
const value = NumberHelper.toSafeNumber(dto.value);
const scale = dto.scale ? NumberHelper.toSafeNumber(dto.scale) : Percentage.DEFAULT_SCALE;
if (!(Number.isFinite(value) && Number.isInteger(scale))) {
return Result.fail(
new ValidationErrorCollection("InvalidNumericValues", [
{ message: "Percentage payload contains invalid numeric values" },
])
);
}
return Percentage.create({
value,
scale,
});
}
get value(): number {
return this.props.value;
}
get scale(): number {
return this.props.scale;
return this.props.scale ?? Percentage.DEFAULT_SCALE;
}
getProps(): PercentageProps {

View File

@ -1,6 +1,7 @@
import { Result } from "@repo/rdx-utils";
import { NumberHelper, Result } from "@repo/rdx-utils";
import { z } from "zod/v4";
import { ValidationErrorCollection } from "../errors";
import { translateZodValidationError } from "../helpers";
import { ValueObject } from "./value-object";
@ -11,9 +12,14 @@ const DEFAULT_MAX_SCALE = 2;
export interface QuantityProps {
value: number;
scale: number;
scale?: number;
}
type QuantityObjectString = {
value: string;
scale: string;
};
export class Quantity extends ValueObject<QuantityProps> {
static EMPTY_QUANTITY_OBJECT = { value: "", scale: "" };
@ -43,12 +49,30 @@ export class Quantity extends ValueObject<QuantityProps> {
return Result.ok(new Quantity({ ...(checkProps.data as QuantityProps) }));
}
static fromObjectString(dto: QuantityObjectString) {
const value = NumberHelper.toSafeNumber(dto.value);
const scale = dto.scale ? NumberHelper.toSafeNumber(dto.scale) : Quantity.DEFAULT_SCALE;
if (!(Number.isFinite(value) && Number.isInteger(scale))) {
return Result.fail(
new ValidationErrorCollection("InvalidNumericValues", [
{ message: "Quantity payload contains invalid numeric values" },
])
);
}
return Quantity.create({
value,
scale,
});
}
get value(): number {
return this.props.value;
}
get scale(): number {
return this.props.scale;
return this.props.scale ?? Quantity.DEFAULT_SCALE;
}
getProps(): QuantityProps {
@ -67,7 +91,7 @@ export class Quantity extends ValueObject<QuantityProps> {
return this.toNumber().toFixed(this.scale);
}
toObjectString() {
toObjectString(): QuantityObjectString {
return {
value: String(this.value),
scale: String(this.scale),

View File

@ -5,6 +5,9 @@
}
],
"settings": {
"chatgpt.openOnStartup": true
"chatgpt.openOnStartup": true,
"chat.tools.terminal.autoApprove": {
"pnpm": true
}
}
}