Uecko_ERP/modules/factuges/src/api/application/mappers/create-proforma-from-factuges-input.mapper.ts

703 lines
19 KiB
TypeScript

import {
DiscountPercentage,
Tax,
type TaxCalculationBehavior,
type TaxGroup,
TaxPercentage,
} from "@erp/core/api";
import {
InvoiceAmount,
type InvoiceSerie,
ItemAmount,
ItemDescription,
ItemQuantity,
type ProformaItemTaxesProps,
} from "@erp/customer-invoices/api/domain";
import {
City,
Country,
CurrencyCode,
DomainError,
EmailAddress,
LanguageCode,
Name,
PhoneNumber,
type PostalAddressProps,
PostalCode,
Province,
Street,
TINNumber,
TextValue,
URLAddress,
type UniqueID,
UtcDate,
ValidationErrorCollection,
type ValidationErrorDetail,
extractOrPushError,
isValidationErrorCollection,
maybeFromNullableResult,
} from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
import type {
CreateProformaFromFactugesRequestDTO,
CreateProformaItemFromFactugesRequestDTO,
} from "../../../common";
export type ProformaCustomerLookup = {
tin: TINNumber;
};
export type ProformaPaymentLookup = {
factuges_id: string;
};
export type ProformaCustomerDraft = {
isCompany: boolean;
name: Name;
tin: TINNumber;
address: PostalAddressProps;
emailPrimary: Maybe<EmailAddress>;
emailSecondary: Maybe<EmailAddress>;
phonePrimary: Maybe<PhoneNumber>;
phoneSecondary: Maybe<PhoneNumber>;
mobilePrimary: Maybe<PhoneNumber>;
mobileSecondary: Maybe<PhoneNumber>;
website: Maybe<URLAddress>;
languageCode: LanguageCode;
currencyCode: CurrencyCode;
};
export type ProformaDraftItem = {
position: string;
description: Maybe<ItemDescription>;
quantity: Maybe<ItemQuantity>;
unitAmount: Maybe<ItemAmount>;
subtotalAmount: Maybe<ItemAmount>;
itemDiscountPercentage: Maybe<DiscountPercentage>;
itemDiscountAmount: Maybe<ItemAmount>;
globalDiscountPercentage: DiscountPercentage;
globalDiscountAmount: Maybe<ItemAmount>;
totalDiscountAmount: Maybe<ItemAmount>;
taxableAmount: Maybe<ItemAmount>;
taxes: ProformaItemTaxesProps;
taxesAmount: Maybe<ItemAmount>;
totalAmount: Maybe<ItemAmount>;
languageCode: LanguageCode;
currencyCode: CurrencyCode;
};
export type ProformaDraft = {
factugesID: string;
proformaSeriesCode: Maybe<InvoiceSerie>;
targetInvoiceSeriesCode: Maybe<InvoiceSerie>;
invoiceDate: UtcDate;
operationDate: Maybe<UtcDate>;
reference: Maybe<string>;
description: Maybe<string>;
notes: Maybe<TextValue>;
languageCode: LanguageCode;
currencyCode: CurrencyCode;
subtotalAmount: Maybe<InvoiceAmount>;
globalDiscountPercentage: DiscountPercentage;
itemsDiscountAmount: Maybe<InvoiceAmount>;
taxableAmount: Maybe<InvoiceAmount>;
taxConfig: FactugesDefaultProformaTaxConfig;
taxes: ProformaItemTaxesProps;
taxesAmount: Maybe<InvoiceAmount>;
totalAmount: Maybe<InvoiceAmount>;
items: ProformaDraftItem[];
};
export type ProformaPaymentDraft = {
payment_id: string;
factuges_id: string;
description: string;
};
export type FactugesProformaPayload = {
customerLookup: ProformaCustomerLookup;
paymentLookup: ProformaPaymentLookup;
customerDraft: ProformaCustomerDraft;
proformaDraft: ProformaDraft;
};
type FactugesDefaultProformaTaxConfig = {
taxMode: "single";
defaultIvaCode: Maybe<string>;
usesEquivalenceSurcharge: false;
defaultRecCode: Maybe<string>;
usesRetention: false;
defaultRetentionCode: Maybe<string>;
};
const FACTUGES_DEFAULT_PROFORMA_TAX_CONFIG: FactugesDefaultProformaTaxConfig = {
taxMode: "single",
defaultIvaCode: Maybe.some("iva_21"),
usesEquivalenceSurcharge: false,
defaultRecCode: Maybe.none(),
usesRetention: false,
defaultRetentionCode: Maybe.none(),
};
export interface ICreateProformaFromFactugesInputMapper {
map(
dto: CreateProformaFromFactugesRequestDTO,
params: { companyId: UniqueID }
): Result<FactugesProformaPayload>;
}
export class CreateProformaFromFactugesInputMapper
implements ICreateProformaFromFactugesInputMapper
{
public map(
dto: CreateProformaFromFactugesRequestDTO,
params: { companyId: UniqueID }
): Result<FactugesProformaPayload> {
try {
const errors: ValidationErrorDetail[] = [];
const { companyId } = params;
const currencyCode = CurrencyCode.fromEUR();
const proformaProps = this.mapProformaProps(dto, {
companyId,
currencyCode,
errors,
});
const customerProps = this.mapCustomerProps(dto, {
companyId,
currencyCode,
errors,
});
this.throwIfValidationErrors(errors);
return Result.ok({
customerLookup: {
tin: customerProps.tin,
},
paymentLookup: {
factuges_id: dto.payment_method_id,
},
customerDraft: customerProps,
proformaDraft: proformaProps,
});
} catch (err: unknown) {
const error = isValidationErrorCollection(err)
? (err as ValidationErrorCollection)
: new DomainError("Customer props mapping failed", { cause: (err as Error).message });
return Result.fail(error);
}
}
private mapProformaProps(
dto: CreateProformaFromFactugesRequestDTO,
params: {
companyId: UniqueID;
currencyCode: CurrencyCode;
errors: ValidationErrorDetail[];
}
): ProformaDraft {
const errors: ValidationErrorDetail[] = [];
//const defaultStatus = InvoiceStatus.fromApproved();
//const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
/**
* Complementa los datos legacy con los defaults actuales de proformas.
*
* La serie legacy `series` no se usa porque la numeracion documental
* se resuelve mediante `document-series`.
*/
/*const proformaNumber = extractOrPushError(
InvoiceNumber.create(dto.),
"invoice_number",
errors
);*/
const factugesID = String(dto.factuges_id);
const reference = extractOrPushError(
maybeFromNullableResult(dto.reference, (value) => Result.ok(String(value))),
"reference",
errors
);
const invoiceDate = extractOrPushError(
UtcDate.createFromISO(dto.proforma_date),
"invoice_date",
errors
);
const operationDate = extractOrPushError(
maybeFromNullableResult(dto.operation_date, (value) => UtcDate.createFromISO(value)),
"operation_date",
errors
);
const description = extractOrPushError(
maybeFromNullableResult(dto.description, (value) => Result.ok(String(value))),
"description",
errors
);
const notes = extractOrPushError(
maybeFromNullableResult(dto.notes, (value) => TextValue.create(value)),
"notes",
errors
);
const languageCode = extractOrPushError(
LanguageCode.create(dto.customer.language_code),
"language_code",
errors
);
const currencyCode = CurrencyCode.fromEUR();
const subtotalAmount = extractOrPushError(
maybeFromNullableResult(dto.subtotal_amount_value, (value) =>
InvoiceAmount.create({ value: Number(value) })
),
"subtotal_amount_value",
params.errors
);
const globalDiscountPercentage = extractOrPushError(
DiscountPercentage.create({ value: Number(dto.global_discount_percentage_value) }),
"global_discount_percentage_value",
params.errors
);
const itemsDiscountAmount = extractOrPushError(
maybeFromNullableResult(dto.discount_amount_value, (value) =>
InvoiceAmount.create({ value: Number(value) })
),
"discount_amount_value",
params.errors
);
const taxableAmount = extractOrPushError(
maybeFromNullableResult(dto.taxable_amount_value, (value) =>
InvoiceAmount.create({ value: Number(value) })
),
"taxable_amount_value",
params.errors
);
// TODO: Determinar cómo calcular los impuestos de la cabecera de la proforma
const taxes: ProformaItemTaxesProps = {
iva: Maybe.none(),
retention: Maybe.none(),
rec: Maybe.none(),
};
const taxesAmount = extractOrPushError(
maybeFromNullableResult(dto.taxes_amount_value, (value) =>
InvoiceAmount.create({ value: Number(value) })
),
"taxes_amount_value",
params.errors
);
const totalAmount = extractOrPushError(
maybeFromNullableResult(dto.total_amount_value, (value) =>
InvoiceAmount.create({ value: Number(value) })
),
"total_amount_value",
params.errors
);
const itemsProps = this.mapItemsProps(dto, {
languageCode: languageCode!,
currencyCode: currencyCode,
globalDiscountPercentage: globalDiscountPercentage!,
errors,
});
const props: ProformaDraft = {
//companyId,
//status: defaultStatus,
//invoiceNumber: proformaNumber!,
factugesID: factugesID,
proformaSeriesCode: Maybe.none(),
targetInvoiceSeriesCode: Maybe.none(),
invoiceDate: invoiceDate!,
operationDate: operationDate!,
//customerId: customerId!,
//recipient,
reference: reference!,
description: description!,
notes: notes!,
languageCode: languageCode!,
currencyCode: currencyCode!,
subtotalAmount: subtotalAmount!,
globalDiscountPercentage: globalDiscountPercentage!,
itemsDiscountAmount: itemsDiscountAmount!,
taxableAmount: taxableAmount!,
taxConfig: FACTUGES_DEFAULT_PROFORMA_TAX_CONFIG,
taxes: taxes,
taxesAmount: taxesAmount!,
totalAmount: totalAmount!,
items: itemsProps,
};
return props;
}
private mapCustomerProps(
dto: CreateProformaFromFactugesRequestDTO,
params: {
companyId: UniqueID;
currencyCode: CurrencyCode;
errors: ValidationErrorDetail[];
}
): ProformaCustomerDraft {
const { customer } = dto;
const { errors, currencyCode } = params;
const isCompany = customer.is_company === "1";
const name = extractOrPushError(Name.create(customer.name), "name", errors);
const tinNumber = extractOrPushError(TINNumber.create(customer.tin), "tin", errors);
const street = extractOrPushError(
maybeFromNullableResult(customer.street, (value) => Street.create(value)),
"street",
errors
);
const city = extractOrPushError(
maybeFromNullableResult(customer.city, (value) => City.create(value)),
"city",
errors
);
const province = extractOrPushError(
maybeFromNullableResult(customer.province, (value) => Province.create(value)),
"province",
errors
);
const postalCode = extractOrPushError(
maybeFromNullableResult(customer.postal_code, (value) => PostalCode.create(value)),
"postal_code",
errors
);
const country = extractOrPushError(
maybeFromNullableResult(customer.country, (value) => Country.create(value)),
"country",
errors
);
const primaryEmailAddress = extractOrPushError(
maybeFromNullableResult(customer.email_primary, (value) => EmailAddress.create(value)),
"email_primary",
errors
);
const secondaryEmailAddress = extractOrPushError(
maybeFromNullableResult(customer.email_secondary, (value) => EmailAddress.create(value)),
"email_secondary",
errors
);
const primaryPhoneNumber = extractOrPushError(
maybeFromNullableResult(customer.phone_primary, (value) => PhoneNumber.create(value)),
"phone_primary",
errors
);
const secondaryPhoneNumber = extractOrPushError(
maybeFromNullableResult(customer.phone_secondary, (value) => PhoneNumber.create(value)),
"phone_secondary",
errors
);
const primaryMobileNumber = extractOrPushError(
maybeFromNullableResult(customer.mobile_primary, (value) => PhoneNumber.create(value)),
"mobile_primary",
errors
);
const secondaryMobileNumber = extractOrPushError(
maybeFromNullableResult(customer.mobile_secondary, (value) => PhoneNumber.create(value)),
"mobile_secondary",
errors
);
const website = extractOrPushError(
maybeFromNullableResult(customer.website, (value) => URLAddress.create(value)),
"website",
errors
);
const languageCode = extractOrPushError(
LanguageCode.create(customer.language_code),
"language_code",
errors
);
this.throwIfValidationErrors(errors);
const postalAddressProps: PostalAddressProps = {
street: street!,
street2: Maybe.none(),
city: city!,
postalCode: postalCode!,
province: province!,
country: country!,
};
const customerProps: ProformaCustomerDraft = {
isCompany: isCompany,
name: name!,
tin: tinNumber!,
address: postalAddressProps!,
emailPrimary: primaryEmailAddress!,
emailSecondary: secondaryEmailAddress!,
phonePrimary: primaryPhoneNumber!,
phoneSecondary: secondaryPhoneNumber!,
mobilePrimary: primaryMobileNumber!,
mobileSecondary: secondaryMobileNumber!,
//fax: Maybe.none(),
website: website!,
//legalRecord: Maybe.none(),
//defaultTaxes: customerTaxes!,
languageCode: languageCode!,
currencyCode: currencyCode!,
};
return customerProps;
}
private mapItemsProps(
dto: CreateProformaFromFactugesRequestDTO,
params: {
languageCode: LanguageCode;
currencyCode: CurrencyCode;
globalDiscountPercentage: DiscountPercentage;
errors: ValidationErrorDetail[];
}
): ProformaDraftItem[] {
const itemsProps: ProformaDraftItem[] = [];
dto.items.forEach((item, index) => {
const position = String(item.position);
const description = extractOrPushError(
maybeFromNullableResult(item.description, (value) => ItemDescription.create(value)),
`items[${index}].description`,
params.errors
);
const quantity = extractOrPushError(
maybeFromNullableResult(item.quantity_value, (value) =>
ItemQuantity.create({ value: Number(value) })
),
`items[${index}].quantity_value`,
params.errors
);
const unitAmount = extractOrPushError(
maybeFromNullableResult(item.unit_amount_value, (value) =>
ItemAmount.create({ value: Number(value) })
),
`items[${index}].unit_amount_value`,
params.errors
);
const subtotalAmount = extractOrPushError(
maybeFromNullableResult(item.subtotal_amount_value, (value) =>
ItemAmount.create({ value: Number(value) })
),
`items[${index}].subtotal_amount_value`,
params.errors
);
const itemDiscountPercentage = extractOrPushError(
maybeFromNullableResult(item.item_discount_percentage_value, (value) =>
DiscountPercentage.create({ value: Number(value) })
),
`items[${index}].item_discount_percentage_value`,
params.errors
);
const itemDiscountAmount = extractOrPushError(
maybeFromNullableResult(item.item_discount_amount_value, (value) =>
ItemAmount.create({ value: Number(value) })
),
`items[${index}].item_discount_amount_value`,
params.errors
);
const globalDiscountAmount = extractOrPushError(
maybeFromNullableResult(item.global_discount_amount_value, (value) =>
ItemAmount.create({ value: Number(value) })
),
`items[${index}].global_discount_amount_value`,
params.errors
);
const totalDiscountAmount = extractOrPushError(
maybeFromNullableResult(item.total_discount_amount_value, (value) =>
ItemAmount.create({ value: Number(value) })
),
`items[${index}].total_discount_amount_value`,
params.errors
);
const taxableAmount = extractOrPushError(
maybeFromNullableResult(item.taxable_amount_value, (value) =>
ItemAmount.create({ value: Number(value) })
),
`items[${index}].taxable_amount_value`,
params.errors
);
const taxesAmount = extractOrPushError(
maybeFromNullableResult(item.taxes_amount_value, (value) =>
ItemAmount.create({ value: Number(value) })
),
`items[${index}].taxes_amount_value`,
params.errors
);
const totalAmount = extractOrPushError(
maybeFromNullableResult(item.total_amount_value, (value) =>
ItemAmount.create({ value: Number(value) })
),
`items[${index}].total_amount_value`,
params.errors
);
const taxes = this.mapItemTaxesProps(item, {
itemIndex: index,
errors: params.errors,
});
const _item: ProformaDraftItem = {
position,
description: description!,
quantity: quantity!,
unitAmount: unitAmount!,
subtotalAmount: subtotalAmount!,
itemDiscountPercentage: itemDiscountPercentage!,
itemDiscountAmount: itemDiscountAmount!,
globalDiscountPercentage: params.globalDiscountPercentage,
globalDiscountAmount: globalDiscountAmount!,
totalDiscountAmount: totalDiscountAmount!,
taxableAmount: taxableAmount!,
taxes,
taxesAmount: taxesAmount!,
totalAmount: totalAmount!,
languageCode: params.languageCode,
currencyCode: params.currencyCode,
};
itemsProps.push(_item);
});
this.throwIfValidationErrors(params.errors);
return itemsProps;
}
/* Devuelve las propiedades de los impustos de una línea de detalle */
private mapItemTaxesProps(
_itemDTO: CreateProformaItemFromFactugesRequestDTO,
params: { itemIndex: number; errors: ValidationErrorDetail[] }
): ProformaItemTaxesProps {
const iva = extractOrPushError(
this.mapTaxToDomain({
code: "iva_21",
percentageValue: 21,
group: "iva",
calculationBehavior: "additive",
fieldPath: `items[${params.itemIndex}].iva`,
}),
`items[${params.itemIndex}].iva`,
params.errors
);
this.throwIfValidationErrors(params.errors);
return {
iva: iva ?? Maybe.none(),
retention: Maybe.none(),
rec: Maybe.none(),
};
}
private mapTaxToDomain(params: {
code: string | null;
percentageValue: number | null;
percentageScale?: number;
group: TaxGroup;
calculationBehavior: TaxCalculationBehavior;
fieldPath: string;
}): Result<Maybe<Tax>, Error> {
if (params.code === null || params.code.trim() === "") {
return Result.ok(Maybe.none());
}
if (params.percentageValue === null) {
return Result.fail(
new Error(`${params.fieldPath}.percentage_value is required when tax code is present`)
);
}
const percentageResult = TaxPercentage.create({
value: params.percentageValue,
});
if (percentageResult.isFailure) {
return Result.fail(percentageResult.error);
}
const taxResult = Tax.create({
code: params.code,
name: params.code,
rate: percentageResult.data,
group: params.group,
calculationBehavior: params.calculationBehavior,
});
if (taxResult.isFailure) {
return Result.fail(taxResult.error);
}
return Result.ok(Maybe.some(taxResult.data));
}
private throwIfValidationErrors(errors: ValidationErrorDetail[]): void {
if (errors.length > 0) {
throw new ValidationErrorCollection("Customer proforma props mapping failed", errors);
}
}
}