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

576 lines
15 KiB
TypeScript
Raw Normal View History

2026-03-16 17:45:45 +00:00
import type { JsonTaxCatalogProvider } from "@erp/core";
import { DiscountPercentage, Tax } from "@erp/core/api";
import {
type IProformaItemProps,
InvoicePaymentMethod,
InvoiceSerie,
ItemAmount,
ItemDescription,
ItemQuantity,
type ProformaItemTaxesProps,
} from "@erp/customer-invoices/api/domain";
import type { CustomerTaxesProps } from "@erp/customers/api/domain";
import {
City,
Country,
CurrencyCode,
DomainError,
EmailAddress,
LanguageCode,
Name,
Percentage,
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 interface IProformaFromFactuGESProps {
customerLookup: {
tin: TINNumber;
};
customerDraft: {
//reference: Maybe<Name>;
isCompany: boolean;
name: Name;
//tradeName: Maybe<Name>;
tin: TINNumber;
address: PostalAddressProps;
emailPrimary: Maybe<EmailAddress>;
emailSecondary: Maybe<EmailAddress>;
phonePrimary: Maybe<PhoneNumber>;
phoneSecondary: Maybe<PhoneNumber>;
mobilePrimary: Maybe<PhoneNumber>;
mobileSecondary: Maybe<PhoneNumber>;
//fax: Maybe<PhoneNumber>;
website: Maybe<URLAddress>;
//legalRecord: Maybe<TextValue>;
//defaultTaxes: CustomerTaxesProps;
languageCode: LanguageCode;
currencyCode: CurrencyCode;
};
proformaDraft: {
series: Maybe<InvoiceSerie>;
invoiceDate: UtcDate;
operationDate: Maybe<UtcDate>;
reference: Maybe<string>;
description: Maybe<string>;
notes: Maybe<TextValue>;
languageCode: LanguageCode;
currencyCode: CurrencyCode;
paymentMethod: Maybe<InvoicePaymentMethod>;
items: IProformaItemProps[];
globalDiscountPercentage: DiscountPercentage;
};
}
export interface ICreateProformaFromFactugesInputMapper {
map(
dto: CreateProformaFromFactugesRequestDTO,
params: { companyId: UniqueID }
): Result<IProformaFromFactuGESProps>;
}
export class CreateProformaFromFactugesInputMapper
implements ICreateProformaFromFactugesInputMapper
{
private readonly taxCatalog: JsonTaxCatalogProvider;
constructor(params: { taxCatalog: JsonTaxCatalogProvider }) {
this.taxCatalog = params.taxCatalog;
}
public map(
dto: CreateProformaFromFactugesRequestDTO,
params: { companyId: UniqueID }
): Result<IProformaFromFactuGESProps> {
try {
const errors: ValidationErrorDetail[] = [];
const { companyId } = params;
const currencyCode = CurrencyCode.fromEUR();
const proformaProps = this.mapProformaProps(dto, {
companyId,
currencyCode,
errors,
});
const customerProps = this.mapCustomerProps(dto.customer, {
companyId,
currencyCode,
errors,
});
this.throwIfValidationErrors(errors);
return Result.ok({
customerLookup: {
tin: customerProps.tin,
},
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);
}
}
public mapProformaProps(
dto: CreateProformaFromFactugesRequestDTO,
params: {
companyId: UniqueID;
currencyCode: CurrencyCode;
errors: ValidationErrorDetail[];
}
): IProformaFromFactuGESProps["proformaDraft"] {
const errors: ValidationErrorDetail[] = [];
const { companyId } = params;
//const defaultStatus = InvoiceStatus.fromApproved();
//const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
const series = extractOrPushError(
maybeFromNullableResult(dto.series, (value) => InvoiceSerie.create(value)),
"series",
errors
);
/*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.invoice_date),
"invoice_date",
errors
);
const operationDate = extractOrPushError(
maybeFromNullableResult(dto.operation_date, (value) => UtcDate.createFromISO(value)),
"operation_date",
errors
);
const description = extractOrPushError(
maybeFromNullableResult(dto.reference, (value) => Result.ok(String(value))),
"description",
errors
);
const notes = extractOrPushError(
maybeFromNullableResult(dto.notes, (value) => TextValue.create(value)),
"notes",
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),
}),
"discount_percentage",
errors
);
const languageCode = extractOrPushError(
LanguageCode.create(dto.customer.language_code),
"language_code",
errors
);
const currencyCode = CurrencyCode.fromEUR();
const itemsProps = this.mapItemsProps(dto, {
languageCode: languageCode!,
currencyCode: currencyCode,
globalDiscountPercentage: globalDiscountPercentage!,
errors,
});
const props: IProformaFromFactuGESProps["proformaDraft"] = {
//companyId,
//status: defaultStatus,
//invoiceNumber: proformaNumber!,
series: series!,
invoiceDate: invoiceDate!,
operationDate: operationDate!,
//customerId: customerId!,
//recipient,
reference: reference!,
description: description!,
notes: notes!,
languageCode: languageCode!,
currencyCode: currencyCode!,
paymentMethod: paymentMethod!,
globalDiscountPercentage: globalDiscountPercentage!,
items: itemsProps, // ← IProformaItemProps[]
};
return props;
}
private mapCustomerProps(
dto: CreateProformaFromFactugesRequestDTO["customer"],
params: {
companyId: UniqueID;
currencyCode: CurrencyCode;
errors: ValidationErrorDetail[];
}
): IProformaFromFactuGESProps["customerDraft"] {
const { errors, currencyCode } = params;
const isCompany = dto.is_company === "1";
const name = extractOrPushError(Name.create(dto.name), "name", errors);
const tinNumber = extractOrPushError(TINNumber.create(dto.tin), "tin", errors);
const street = extractOrPushError(
maybeFromNullableResult(dto.street, (value) => Street.create(value)),
"street",
errors
);
const city = extractOrPushError(
maybeFromNullableResult(dto.city, (value) => City.create(value)),
"city",
errors
);
const province = extractOrPushError(
maybeFromNullableResult(dto.province, (value) => Province.create(value)),
"province",
errors
);
const postalCode = extractOrPushError(
maybeFromNullableResult(dto.postal_code, (value) => PostalCode.create(value)),
"postal_code",
errors
);
const country = extractOrPushError(
maybeFromNullableResult(dto.country, (value) => Country.create(value)),
"country",
errors
);
const primaryEmailAddress = extractOrPushError(
maybeFromNullableResult(dto.email_primary, (value) => EmailAddress.create(value)),
"email_primary",
errors
);
const secondaryEmailAddress = extractOrPushError(
maybeFromNullableResult(dto.email_secondary, (value) => EmailAddress.create(value)),
"email_secondary",
errors
);
const primaryPhoneNumber = extractOrPushError(
maybeFromNullableResult(dto.phone_primary, (value) => PhoneNumber.create(value)),
"phone_primary",
errors
);
const secondaryPhoneNumber = extractOrPushError(
maybeFromNullableResult(dto.phone_secondary, (value) => PhoneNumber.create(value)),
"phone_secondary",
errors
);
const primaryMobileNumber = extractOrPushError(
maybeFromNullableResult(dto.mobile_primary, (value) => PhoneNumber.create(value)),
"mobile_primary",
errors
);
const secondaryMobileNumber = extractOrPushError(
maybeFromNullableResult(dto.mobile_secondary, (value) => PhoneNumber.create(value)),
"mobile_secondary",
errors
);
const website = extractOrPushError(
maybeFromNullableResult(dto.website, (value) => URLAddress.create(value)),
"website",
errors
);
const languageCode = extractOrPushError(
LanguageCode.create(dto.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 customerTaxes = this.mapCustomerTaxesProps(dto, {
errors,
});*/
const customerProps: IProformaFromFactuGESProps["customerDraft"] = {
//companyId,
//status: status!,
isCompany: isCompany,
name: name!,
tin: tinNumber!,
//tradeName: Maybe.none(),
//reference: Maybe.none(),
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 mapCustomerTaxesProps(
dto: CreateProformaFromFactugesRequestDTO["customer"],
params: {
errors: ValidationErrorDetail[];
}
): CustomerTaxesProps {
const { errors } = params;
const iva = extractOrPushError(
maybeFromNullableResult("iva_21", (value) => Tax.createFromCode(value, this.taxCatalog)),
"iva_21",
errors
);
this.throwIfValidationErrors(errors);
return {
iva: iva!,
rec: Maybe.none(),
retention: Maybe.none(),
};
}
private mapItemsProps(
dto: CreateProformaFromFactugesRequestDTO,
params: {
languageCode: LanguageCode;
currencyCode: CurrencyCode;
globalDiscountPercentage: DiscountPercentage;
errors: ValidationErrorDetail[];
}
): IProformaItemProps[] {
const itemsProps: IProformaItemProps[] = [];
dto.items.forEach((item, index) => {
const description = extractOrPushError(
maybeFromNullableResult(item.description, (v) => ItemDescription.create(v)),
`items[${index}].description`,
params.errors
);
const quantity = extractOrPushError(
maybeFromNullableResult(item.quantity_value, (v) => ItemQuantity.create(v)),
`items[${index}].quantity`,
params.errors
);
const unitAmount = extractOrPushError(
maybeFromNullableResult(item.unit_value, (v) => ItemAmount.create(v)),
`items[${index}].unit_amount`,
params.errors
);
const discountPercentage = extractOrPushError(
maybeFromNullableResult(item.discount_percentage_value, (v) =>
DiscountPercentage.create(v)
),
`items[${index}].discount_percentage`,
params.errors
);
const taxes = this.mapItempTaxesProps(item, {
itemIndex: index,
errors: params.errors,
});
this.throwIfValidationErrors(params.errors);
itemsProps.push({
globalDiscountPercentage: params.globalDiscountPercentage,
languageCode: params.languageCode,
currencyCode: params.currencyCode,
description: description!,
quantity: quantity!,
unitAmount: unitAmount!,
itemDiscountPercentage: discountPercentage!,
taxes,
});
});
return itemsProps;
}
/* Devuelve las propiedades de los impustos de una línea de detalle */
private mapItempTaxesProps(
itemDTO: CreateProformaItemFromFactugesRequestDTO,
params: { itemIndex: number; errors: ValidationErrorDetail[] }
): ProformaItemTaxesProps {
const { itemIndex, errors } = params;
const taxesProps: ProformaItemTaxesProps = {
iva: Maybe.none(),
retention: Maybe.none(),
rec: Maybe.none(),
};
// Normaliza: "" -> []
// IVA por defecto => iva_21
const taxStrCodes = [itemDTO.iva_code === "" ? "iva_21" : itemDTO.iva_code];
if (itemDTO.rec_code) taxStrCodes.push(itemDTO.rec_code);
if (itemDTO.retention_code) taxStrCodes.push(itemDTO.retention_code);
taxStrCodes.forEach((strCode, taxIndex) => {
const taxResult = Tax.createFromCode(strCode, this.taxCatalog);
if (!taxResult.isSuccess) {
errors.push({
path: `items[${itemIndex}].taxes[${taxIndex}]`,
message: taxResult.error.message,
});
return;
}
const tax = taxResult.data;
if (tax.isVATLike()) {
if (taxesProps.iva.isSome()) {
errors.push({
path: `items[${itemIndex}].taxes`,
message: "Multiple taxes for group VAT are not allowed",
});
}
taxesProps.iva = Maybe.some(tax);
}
if (tax.isRetention()) {
if (taxesProps.retention.isSome()) {
errors.push({
path: `items[${itemIndex}].taxes`,
message: "Multiple taxes for group retention are not allowed",
});
}
taxesProps.retention = Maybe.some(tax);
}
if (tax.isRec()) {
if (taxesProps.rec.isSome()) {
errors.push({
path: `items[${itemIndex}].taxes`,
message: "Multiple taxes for group rec are not allowed",
});
}
taxesProps.rec = Maybe.some(tax);
}
});
this.throwIfValidationErrors(errors);
return taxesProps;
}
private throwIfValidationErrors(errors: ValidationErrorDetail[]): void {
if (errors.length > 0) {
throw new ValidationErrorCollection("Customer proforma props mapping failed", errors);
}
}
}