Importación desde FactuGES con validación
This commit is contained in:
parent
2ae118d1ff
commit
964565a6fe
@ -60,7 +60,7 @@ export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder
|
|||||||
payment_method: payment,
|
payment_method: payment,
|
||||||
|
|
||||||
subtotal_amount: allTotals.subtotalAmount.toObjectString(),
|
subtotal_amount: allTotals.subtotalAmount.toObjectString(),
|
||||||
items_discount_amount: allTotals.itemDiscountAmount.toObjectString(),
|
items_discount_amount: allTotals.itemsDiscountAmount.toObjectString(),
|
||||||
|
|
||||||
global_discount_percentage: proforma.globalDiscountPercentage.toObjectString(),
|
global_discount_percentage: proforma.globalDiscountPercentage.toObjectString(),
|
||||||
global_discount_amount: allTotals.globalDiscountAmount.toObjectString(),
|
global_discount_amount: allTotals.globalDiscountAmount.toObjectString(),
|
||||||
|
|||||||
@ -59,7 +59,7 @@ export interface IProformaCreateProps {
|
|||||||
export interface IProformaTotals {
|
export interface IProformaTotals {
|
||||||
subtotalAmount: InvoiceAmount;
|
subtotalAmount: InvoiceAmount;
|
||||||
|
|
||||||
itemDiscountAmount: InvoiceAmount;
|
itemsDiscountAmount: InvoiceAmount;
|
||||||
globalDiscountAmount: InvoiceAmount;
|
globalDiscountAmount: InvoiceAmount;
|
||||||
totalDiscountAmount: InvoiceAmount;
|
totalDiscountAmount: InvoiceAmount;
|
||||||
|
|
||||||
@ -276,7 +276,7 @@ export class Proforma extends AggregateRoot<InternalProformaProps> implements IP
|
|||||||
return {
|
return {
|
||||||
subtotalAmount: this.toInvoiceAmount(itemsTotals.subtotalAmount),
|
subtotalAmount: this.toInvoiceAmount(itemsTotals.subtotalAmount),
|
||||||
|
|
||||||
itemDiscountAmount: this.toInvoiceAmount(itemsTotals.itemDiscountAmount),
|
itemsDiscountAmount: this.toInvoiceAmount(itemsTotals.itemDiscountAmount),
|
||||||
globalDiscountAmount: this.toInvoiceAmount(itemsTotals.globalDiscountAmount),
|
globalDiscountAmount: this.toInvoiceAmount(itemsTotals.globalDiscountAmount),
|
||||||
totalDiscountAmount: this.toInvoiceAmount(itemsTotals.totalDiscountAmount),
|
totalDiscountAmount: this.toInvoiceAmount(itemsTotals.totalDiscountAmount),
|
||||||
|
|
||||||
|
|||||||
@ -339,8 +339,8 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
|
|||||||
subtotal_amount_value: allAmounts.subtotalAmount.value,
|
subtotal_amount_value: allAmounts.subtotalAmount.value,
|
||||||
subtotal_amount_scale: allAmounts.subtotalAmount.scale,
|
subtotal_amount_scale: allAmounts.subtotalAmount.scale,
|
||||||
|
|
||||||
items_discount_amount_value: allAmounts.itemDiscountAmount.value,
|
items_discount_amount_value: allAmounts.itemsDiscountAmount.value,
|
||||||
items_discount_amount_scale: allAmounts.itemDiscountAmount.scale,
|
items_discount_amount_scale: allAmounts.itemsDiscountAmount.scale,
|
||||||
|
|
||||||
global_discount_percentage_value: source.globalDiscountPercentage.toPrimitive().value,
|
global_discount_percentage_value: source.globalDiscountPercentage.toPrimitive().value,
|
||||||
global_discount_percentage_scale: source.globalDiscountPercentage.toPrimitive().scale,
|
global_discount_percentage_scale: source.globalDiscountPercentage.toPrimitive().scale,
|
||||||
|
|||||||
@ -91,13 +91,13 @@ export type ProformaDraft = {
|
|||||||
notes: Maybe<TextValue>;
|
notes: Maybe<TextValue>;
|
||||||
languageCode: LanguageCode;
|
languageCode: LanguageCode;
|
||||||
currencyCode: CurrencyCode;
|
currencyCode: CurrencyCode;
|
||||||
subtotalAmount: Maybe<ItemAmount>;
|
subtotalAmount: Maybe<InvoiceAmount>;
|
||||||
globalDiscountPercentage: DiscountPercentage;
|
globalDiscountPercentage: DiscountPercentage;
|
||||||
itemsDiscountAmount: Maybe<ItemAmount>;
|
itemsDiscountAmount: Maybe<InvoiceAmount>;
|
||||||
taxableAmount: Maybe<ItemAmount>;
|
taxableAmount: Maybe<InvoiceAmount>;
|
||||||
taxes: ProformaItemTaxesProps;
|
taxes: ProformaItemTaxesProps;
|
||||||
taxesAmount: Maybe<ItemAmount>;
|
taxesAmount: Maybe<InvoiceAmount>;
|
||||||
totalAmount: Maybe<ItemAmount>;
|
totalAmount: Maybe<InvoiceAmount>;
|
||||||
items: ProformaDraftItem[];
|
items: ProformaDraftItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -2,9 +2,12 @@ import type { JsonTaxCatalogProvider } from "@erp/core";
|
|||||||
import { type ITransactionManager, Tax, isEntityNotFoundError } from "@erp/core/api";
|
import { type ITransactionManager, Tax, isEntityNotFoundError } from "@erp/core/api";
|
||||||
import type { ProformaPublicServices } from "@erp/customer-invoices/api";
|
import type { ProformaPublicServices } from "@erp/customer-invoices/api";
|
||||||
import {
|
import {
|
||||||
|
type InvoiceAmount,
|
||||||
InvoicePaymentMethod,
|
InvoicePaymentMethod,
|
||||||
type InvoiceRecipient,
|
type InvoiceRecipient,
|
||||||
InvoiceStatus,
|
InvoiceStatus,
|
||||||
|
type ItemAmount,
|
||||||
|
type Proforma,
|
||||||
} from "@erp/customer-invoices/api/domain";
|
} from "@erp/customer-invoices/api/domain";
|
||||||
import type { CustomerPublicServices } from "@erp/customers/api";
|
import type { CustomerPublicServices } from "@erp/customers/api";
|
||||||
import {
|
import {
|
||||||
@ -13,7 +16,14 @@ import {
|
|||||||
CustomerTaxes,
|
CustomerTaxes,
|
||||||
type ICustomerCreateProps,
|
type ICustomerCreateProps,
|
||||||
} from "@erp/customers/api/domain";
|
} from "@erp/customers/api/domain";
|
||||||
import { type Name, type PhoneNumber, type TextValue, UniqueID } from "@repo/rdx-ddd";
|
import {
|
||||||
|
type Name,
|
||||||
|
type PhoneNumber,
|
||||||
|
type TextValue,
|
||||||
|
UniqueID,
|
||||||
|
ValidationErrorCollection,
|
||||||
|
type ValidationErrorDetail,
|
||||||
|
} from "@repo/rdx-ddd";
|
||||||
import { Maybe, Result } from "@repo/rdx-utils";
|
import { Maybe, Result } from "@repo/rdx-utils";
|
||||||
import type { Transaction } from "sequelize";
|
import type { Transaction } from "sequelize";
|
||||||
|
|
||||||
@ -86,8 +96,8 @@ export class CreateProformaFromFactugesUseCase {
|
|||||||
companyId,
|
companyId,
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
if (customerResult.isFailure) {
|
if (paymentResult.isFailure) {
|
||||||
return Result.fail(customerResult.error);
|
return Result.fail(paymentResult.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const payment = paymentResult.data;
|
const payment = paymentResult.data;
|
||||||
@ -122,6 +132,13 @@ export class CreateProformaFromFactugesUseCase {
|
|||||||
return Result.fail(createResult.error);
|
return Result.fail(createResult.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Valida que los datos de entrada coincidan con el snapshot
|
||||||
|
const proforma = createResult.data;
|
||||||
|
const validationResult = this.validateDraftAgainstProforma(proformaDraft, proforma);
|
||||||
|
if (validationResult.isFailure) {
|
||||||
|
return Result.fail(validationResult.error);
|
||||||
|
}
|
||||||
|
|
||||||
const readResult = await this.proformaServices.getProformaSnapshotById(
|
const readResult = await this.proformaServices.getProformaSnapshotById(
|
||||||
createResult.data.id,
|
createResult.data.id,
|
||||||
{
|
{
|
||||||
@ -136,8 +153,6 @@ export class CreateProformaFromFactugesUseCase {
|
|||||||
|
|
||||||
const snapshot = readResult.data;
|
const snapshot = readResult.data;
|
||||||
|
|
||||||
//const comparisonResults = this.compare()
|
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
customer_id: customer.id.toString(),
|
customer_id: customer.id.toString(),
|
||||||
proforma_id: snapshot.id.toString(),
|
proforma_id: snapshot.id.toString(),
|
||||||
@ -150,6 +165,148 @@ export class CreateProformaFromFactugesUseCase {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valida que las magnitudes importadas del borrador coincidan con la proforma
|
||||||
|
* generada por el dominio.
|
||||||
|
*
|
||||||
|
* Motivo:
|
||||||
|
* - Detecta divergencias entre el payload legacy y los cálculos reales del dominio.
|
||||||
|
* - Actúa como validación de reconciliación, no como sustituto de las invariantes del agregado.
|
||||||
|
*/
|
||||||
|
private validateDraftAgainstProforma(
|
||||||
|
proformaDraft: FactugesProformaPayload["proformaDraft"],
|
||||||
|
proforma: Proforma
|
||||||
|
): Result<void, Error> {
|
||||||
|
const errors: ValidationErrorDetail[] = [];
|
||||||
|
const proformaTotals = proforma.totals();
|
||||||
|
|
||||||
|
console.log(proformaTotals);
|
||||||
|
|
||||||
|
if (proformaDraft.items.length !== proforma.items.size()) {
|
||||||
|
errors.push({
|
||||||
|
path: "items",
|
||||||
|
message: "La cantidad de ítems de la proforma no coincide con los datos de entrada.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.validateOptionalExpectedAmount({
|
||||||
|
expected: proformaDraft.subtotalAmount,
|
||||||
|
actual: proformaTotals.subtotalAmount,
|
||||||
|
path: "subtotalAmount",
|
||||||
|
message: "El subtotal de la proforma no coincide con los datos de entrada.",
|
||||||
|
errors,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.validateOptionalExpectedAmount({
|
||||||
|
expected: proformaDraft.taxableAmount,
|
||||||
|
actual: proformaTotals.taxableAmount,
|
||||||
|
path: "taxableAmount",
|
||||||
|
message: "La base imponible de la proforma no coincide con los datos de entrada.",
|
||||||
|
errors,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.validateOptionalExpectedAmount({
|
||||||
|
expected: proformaDraft.taxesAmount,
|
||||||
|
actual: proformaTotals.taxesAmount,
|
||||||
|
path: "taxesAmount",
|
||||||
|
message: "La suma de impuestos de la proforma no coincide con los datos de entrada.",
|
||||||
|
errors,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.validateOptionalExpectedAmount({
|
||||||
|
expected: proformaDraft.totalAmount,
|
||||||
|
actual: proformaTotals.totalAmount,
|
||||||
|
path: "totalAmount",
|
||||||
|
message: "El total de la proforma no coincide con los datos de entrada.",
|
||||||
|
errors,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
return Result.fail(
|
||||||
|
new ValidationErrorCollection(
|
||||||
|
"La proforma generada no coincide con las magnitudes validadas del borrador importado.",
|
||||||
|
errors
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateOptionalExpectedAmount(params: {
|
||||||
|
expected: Maybe<InvoiceAmount | ItemAmount>;
|
||||||
|
actual: InvoiceAmount | ItemAmount;
|
||||||
|
path: string;
|
||||||
|
message: string;
|
||||||
|
errors: ValidationErrorDetail[];
|
||||||
|
}): void {
|
||||||
|
const { expected, actual, path, message, errors } = params;
|
||||||
|
|
||||||
|
if (expected.isNone()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedAmount = expected.unwrap();
|
||||||
|
|
||||||
|
if (!actual.equalsTo(expectedAmount)) {
|
||||||
|
errors.push({
|
||||||
|
path,
|
||||||
|
message: this.buildAmountMismatchMessage({
|
||||||
|
baseMessage: message,
|
||||||
|
expected: expectedAmount,
|
||||||
|
actual,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildAmountMismatchMessage(params: {
|
||||||
|
baseMessage: string;
|
||||||
|
expected: InvoiceAmount | ItemAmount;
|
||||||
|
actual: InvoiceAmount | ItemAmount;
|
||||||
|
}): string {
|
||||||
|
const { baseMessage, expected, actual } = params;
|
||||||
|
|
||||||
|
return `${baseMessage} Esperado: ${expected.formattedValue}. Actual: ${actual.formattedValue}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valida un importe opcional esperado contra un importe real también opcional.
|
||||||
|
*
|
||||||
|
* Motivo:
|
||||||
|
* - Algunos campos pueden faltar tanto en el payload importado como en
|
||||||
|
* la proyección o snapshot generado.
|
||||||
|
* - Si el esperado existe pero el real no, se considera discrepancia.
|
||||||
|
*/
|
||||||
|
private validateOptionalMaybeAmount(params: {
|
||||||
|
expected: Maybe<InvoiceAmount | ItemAmount>;
|
||||||
|
actual: Maybe<InvoiceAmount | ItemAmount>;
|
||||||
|
path: string;
|
||||||
|
message: string;
|
||||||
|
errors: ValidationErrorDetail[];
|
||||||
|
}): void {
|
||||||
|
const { expected, actual, path, message, errors } = params;
|
||||||
|
|
||||||
|
if (expected.isNone()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actual.isNone()) {
|
||||||
|
errors.push({
|
||||||
|
path,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!actual.unwrap().equals(expected.unwrap())) {
|
||||||
|
errors.push({
|
||||||
|
path,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private buildProformaCreateProps(deps: {
|
private buildProformaCreateProps(deps: {
|
||||||
proformaDraft: FactugesProformaPayload["proformaDraft"];
|
proformaDraft: FactugesProformaPayload["proformaDraft"];
|
||||||
customerId: UniqueID;
|
customerId: UniqueID;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user