Facturas de cliente

This commit is contained in:
David Arranz 2025-09-09 20:13:54 +02:00
parent 67c76c3185
commit 5879220fe9
21 changed files with 551 additions and 349 deletions

View File

@ -11,10 +11,10 @@ const DEFAULT_MIN_SCALE = 0;
const DEFAULT_MAX_SCALE = 4;
export interface TaxProps {
value: number;
scale: number;
name: string;
code: string;
code: string; // iva_21
name: string; // 21% IVA
value: number; // 2100
scale: number; // 2
}
export class Tax extends ValueObject<TaxProps> {

View File

@ -1,3 +1,4 @@
import { JsonTaxCatalogProvider } from "@erp/core";
import { ITransactionManager } from "@erp/core/api";
import { Criteria } from "@repo/rdx-criteria/server";
import { UniqueID } from "@repo/rdx-ddd";
@ -16,7 +17,8 @@ export class ListCustomerInvoicesUseCase {
constructor(
private readonly service: CustomerInvoiceService,
private readonly transactionManager: ITransactionManager,
private readonly assembler: ListCustomerInvoicesAssembler
private readonly assembler: ListCustomerInvoicesAssembler,
private readonly taxCatalog: JsonTaxCatalogProvider
) {}
public execute(

View File

@ -1,4 +1,4 @@
import { DomainValidationError, Taxes } from "@erp/core/api";
import { DomainValidationError } from "@erp/core/api";
import {
AggregateRoot,
CurrencyCode,
@ -11,6 +11,7 @@ import {
} from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
import { CustomerInvoiceItems, InvoiceRecipient } from "../entities";
import { InvoiceTaxes } from "../entities/invoice-taxes";
import {
CustomerInvoiceNumber,
CustomerInvoiceSerie,
@ -53,7 +54,7 @@ export interface CustomerInvoiceProps {
discountPercentage: Percentage;
//discountAmount: MoneyValue;
taxes: Taxes;
taxes: InvoiceTaxes;
//totalAmount: MoneyValue;

View File

@ -1,4 +1,3 @@
export * from "./customer-invoice-items";
export * from "./invoice-customer";
export * from "./invoice-recipient";
export * from "./invoice-taxes";

View File

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

View File

@ -1,81 +0,0 @@
import { EmailAddress, Name, PostalAddress, ValueObject } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { PhoneNumber } from "libphonenumber-js";
import { CustomerInvoiceAddressType } from "../../value-objects";
export interface ICustomerInvoiceAddressProps {
type: CustomerInvoiceAddressType;
title: Name;
address: PostalAddress;
email: EmailAddress;
phone: PhoneNumber;
}
export interface ICustomerInvoiceAddress {
type: CustomerInvoiceAddressType;
title: Name;
address: PostalAddress;
email: EmailAddress;
phone: PhoneNumber;
}
export class CustomerInvoiceAddress
extends ValueObject<ICustomerInvoiceAddressProps>
implements ICustomerInvoiceAddress
{
public static create(props: ICustomerInvoiceAddressProps) {
return Result.ok(new CustomerInvoiceAddress(props));
}
public static createShippingAddress(props: ICustomerInvoiceAddressProps) {
return Result.ok(
new CustomerInvoiceAddress({
...props,
type: CustomerInvoiceAddressType.create("shipping").data,
})
);
}
public static createBillingAddress(props: ICustomerInvoiceAddressProps) {
return Result.ok(
new CustomerInvoiceAddress({
...props,
type: CustomerInvoiceAddressType.create("billing").data,
})
);
}
get title(): Name {
return this.props.title;
}
get address(): PostalAddress {
return this.props.address;
}
get email(): EmailAddress {
return this.props.email;
}
get phone(): PhoneNumber {
return this.props.phone;
}
get type(): CustomerInvoiceAddressType {
return this.props.type;
}
getProps(): ICustomerInvoiceAddressProps {
return this.props;
}
toPrimitive() {
return {
type: this.type.toString(),
title: this.title.toString(),
address: this.address.toString(),
email: this.email.toString(),
phone: this.phone.toString(),
};
}
}

View File

@ -1,61 +0,0 @@
import { DomainEntity, Name, TINNumber, UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { CustomerInvoiceAddress } from "./customer-invoice-address";
export interface ICustomerInvoiceCustomerProps {
tin: TINNumber;
companyName: Name;
firstName: Name;
lastName: Name;
billingAddress?: CustomerInvoiceAddress;
shippingAddress?: CustomerInvoiceAddress;
}
export interface ICustomerInvoiceCustomer {
id: UniqueID;
tin: TINNumber;
companyName: Name;
firstName: Name;
lastName: Name;
billingAddress?: CustomerInvoiceAddress;
shippingAddress?: CustomerInvoiceAddress;
}
export class CustomerInvoiceCustomer
extends DomainEntity<ICustomerInvoiceCustomerProps>
implements ICustomerInvoiceCustomer
{
public static create(
props: ICustomerInvoiceCustomerProps,
id?: UniqueID
): Result<CustomerInvoiceCustomer, Error> {
const participant = new CustomerInvoiceCustomer(props, id);
return Result.ok<CustomerInvoiceCustomer>(participant);
}
get tin(): TINNumber {
return this.props.tin;
}
get companyName(): Name {
return this.props.companyName;
}
get firstName(): Name {
return this.props.firstName;
}
get lastName(): Name {
return this.props.lastName;
}
get billingAddress() {
return this.props.billingAddress;
}
get shippingAddress() {
return this.props.shippingAddress;
}
}

View File

@ -0,0 +1,2 @@
export * from "./invoice-tax";
export * from "./invoice-taxes";

View File

@ -0,0 +1,55 @@
import { Tax } from "@erp/core/api";
import { ValueObject } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { InvoiceAmount } from "../../value-objects/invoice-amount";
export interface InvoiceTaxProps {
tax: Tax;
taxableAmount: InvoiceAmount;
taxesAmount: InvoiceAmount;
}
export class InvoiceTax extends ValueObject<InvoiceTaxProps> {
protected static validate(values: InvoiceTaxProps) {
return Result.ok(values);
}
static create(values: InvoiceTaxProps): Result<InvoiceTax, Error> {
const valueIsValid = InvoiceTax.validate(values);
if (valueIsValid.isFailure) {
return Result.fail(valueIsValid.error);
}
return Result.ok(new InvoiceTax(values));
}
public update(partial: Partial<InvoiceTaxProps>): Result<InvoiceTax, Error> {
const updatedProps = {
...this.props,
...partial,
} as InvoiceTaxProps;
return InvoiceTax.create(updatedProps);
}
public get tax(): Tax {
return this.props.tax;
}
public get taxableAmount(): InvoiceAmount {
return this.props.taxableAmount;
}
public get taxesAmount(): InvoiceAmount {
return this.props.taxesAmount;
}
getProps(): InvoiceTaxProps {
return this.props;
}
toPrimitive() {
return this.getProps();
}
}

View File

@ -0,0 +1,17 @@
import { Collection } from "@repo/rdx-utils";
import { InvoiceTax } from "./invoice-tax";
export interface InvoiceTaxesProps {
items?: InvoiceTax[];
}
export class InvoiceTaxes extends Collection<InvoiceTax> {
constructor(props: InvoiceTaxesProps) {
const { items = [] } = props;
super(items);
}
public static create(props: InvoiceTaxesProps): InvoiceTaxes {
return new InvoiceTaxes(props);
}
}

View File

@ -3,6 +3,7 @@ export * from "./customer-invoice-item-description";
export * from "./customer-invoice-number";
export * from "./customer-invoice-serie";
export * from "./customer-invoice-status";
export * from "./invoice-amount";
export * from "./item-amount";
export * from "./item-discount";
export * from "./item-quantity";

View File

@ -0,0 +1,24 @@
import { MoneyValue, MoneyValueProps } from "@repo/rdx-ddd";
type InvoiceAmountProps = Pick<MoneyValueProps, "value" | "currency_code">;
export class InvoiceAmount extends MoneyValue {
public static DEFAULT_SCALE = 2;
static create({ value, currency_code }: InvoiceAmountProps) {
const props = {
value: Number(value),
scale: InvoiceAmount.DEFAULT_SCALE,
currency_code,
};
return MoneyValue.create(props);
}
static zero(currency_code: string) {
const props = {
value: 0,
currency_code,
};
return InvoiceAmount.create(props);
}
}

View File

@ -52,10 +52,13 @@ export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps {
const { database } = params;
const transactionManager = new SequelizeTransactionManager(database);
if (!_mapper) _mapper = new CustomerInvoiceMapper();
if (!_catalogs) _catalogs = { taxes: spainTaxCatalogProvider };
if (!_mapper)
_mapper = new CustomerInvoiceMapper({
taxCatalog: _catalogs!.taxes,
});
if (!_repo) _repo = new CustomerInvoiceRepository({ mapper: _mapper, database });
if (!_service) _service = new CustomerInvoiceService(_repo);
if (!_catalogs) _catalogs = { taxes: spainTaxCatalogProvider };
if (!_assemblers) {
_assemblers = {
@ -75,8 +78,19 @@ export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps {
catalogs: _catalogs,
build: {
list: () =>
new ListCustomerInvoicesUseCase(_service!, transactionManager!, _assemblers!.list),
get: () => new GetCustomerInvoiceUseCase(_service!, transactionManager!, _assemblers!.get),
new ListCustomerInvoicesUseCase(
_service!,
transactionManager!,
_assemblers!.list,
_catalogs!.taxes
),
get: () =>
new GetCustomerInvoiceUseCase(
_service!,
transactionManager!,
_assemblers!.get,
_catalogs!.taxes
),
create: () =>
new CreateCustomerInvoiceUseCase(
_service!,
@ -85,7 +99,12 @@ export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps {
_catalogs!.taxes
),
update: () =>
new UpdateCustomerInvoiceUseCase(_service!, transactionManager!, _assemblers!.update),
new UpdateCustomerInvoiceUseCase(
_service!,
transactionManager!,
_assemblers!.update,
_catalogs!.taxes
),
delete: () => new DeleteCustomerInvoiceUseCase(_service!, transactionManager!),
},
presenters: {

View File

@ -2,31 +2,23 @@ import {
ISequelizeMapper,
MapperParamsType,
SequelizeMapper,
ValidationErrorCollection,
ValidationErrorDetail,
extractOrPushError,
} from "@erp/core/api";
import {
CurrencyCode,
LanguageCode,
UniqueID,
maybeFromNullableVO,
toNullable,
} from "@repo/rdx-ddd";
import { UniqueID, maybeFromNullableVO, toNullable } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { InferCreationAttributes } from "sequelize";
import {
CustomerInvoice,
CustomerInvoiceItem,
CustomerInvoiceItemDescription,
CustomerInvoiceProps,
ItemAmount,
ItemDiscount,
ItemQuantity,
} from "../../domain";
import {
CustomerInvoiceItemCreationAttributes,
CustomerInvoiceItemModel,
CustomerInvoiceModel,
} from "../sequelize";
import { CustomerInvoiceItemCreationAttributes, CustomerInvoiceItemModel } from "../sequelize";
export interface ICustomerInvoiceItemMapper
extends ISequelizeMapper<
@ -43,26 +35,16 @@ export class CustomerInvoiceItemMapper
>
implements ICustomerInvoiceItemMapper
{
public mapToDomain(
source: CustomerInvoiceItemModel,
params?: MapperParamsType
): Result<CustomerInvoiceItem, Error> {
const { sourceParent, errors } = params as {
sourceParent: CustomerInvoiceModel;
private mapAttributesToDomain(source: CustomerInvoiceItemModel, params?: MapperParamsType) {
const { errors, index, attributes } = params as {
index: number;
errors: ValidationErrorDetail[];
attributes: Partial<CustomerInvoiceProps>;
};
const itemId = extractOrPushError(UniqueID.create(source.item_id), "item_id", errors);
const languageCode = extractOrPushError(
LanguageCode.create(sourceParent.language_code),
"language_code",
errors
);
const currencyCode = extractOrPushError(
CurrencyCode.create(sourceParent.currency_code),
"currency_code",
const itemId = extractOrPushError(
UniqueID.create(source.item_id),
`items[${index}].item_id`,
errors
);
@ -70,51 +52,87 @@ export class CustomerInvoiceItemMapper
maybeFromNullableVO(source.description, (value) =>
CustomerInvoiceItemDescription.create(value)
),
"description",
`items[${index}].description`,
errors
);
const quantity = extractOrPushError(
maybeFromNullableVO(source.quantity_value, (value) => ItemQuantity.create({ value })),
"discount_percentage",
`items[${index}].discount_percentage`,
errors
);
const unitAmount = extractOrPushError(
maybeFromNullableVO(source.unit_amount_value, (value) =>
ItemAmount.create({ value, currency_code: currencyCode!.code })
ItemAmount.create({ value, currency_code: attributes.currencyCode!.code })
),
"unit_amount",
`items[${index}].unit_amount`,
errors
);
return {
itemId,
languageCode: attributes.languageCode,
currencyCode: attributes.currencyCode,
description,
quantity,
unitAmount,
};
}
public mapToDomain(
source: CustomerInvoiceItemModel,
params?: MapperParamsType
): Result<CustomerInvoiceItem, Error> {
const { errors, index, requireIncludes } = params as {
index: number;
requireIncludes: boolean;
errors: ValidationErrorDetail[];
attributes: Partial<CustomerInvoiceProps>;
};
if (requireIncludes) {
if (!source.taxes) {
errors.push({
path: `items[${index}].taxes`,
message: "Taxes not included in query (requireIncludes=true)",
});
}
}
const attributes = this.mapAttributesToDomain(source, params);
const discountPercentage = extractOrPushError(
maybeFromNullableVO(source.discount_percentage_value, (value) =>
ItemDiscount.create({ value })
),
"discount_percentage",
`items[${index}].discount_percentage`,
errors
);
// Creación del objeto de dominio
const itemOrError = CustomerInvoiceItem.create(
const createResult = CustomerInvoiceItem.create(
{
languageCode: languageCode!,
currencyCode: currencyCode!,
description: description!,
quantity: quantity!,
unitAmount: unitAmount!,
languageCode: attributes.languageCode!,
currencyCode: attributes.currencyCode!,
description: attributes.description!,
quantity: attributes.quantity!,
unitAmount: attributes.unitAmount!,
discountPercentage: discountPercentage!,
taxes: "",
},
itemId
attributes.itemId
);
if (itemOrError.isFailure) {
errors.push({ path: "item", message: itemOrError.error.message });
if (createResult.isFailure) {
return Result.fail(
new ValidationErrorCollection("Invoice item entity creation failed", [
{ path: `items[${index}]`, message: createResult.error.message },
])
);
}
return itemOrError;
return createResult;
}
public mapToPersistence(

View File

@ -1,3 +1,4 @@
import { JsonTaxCatalogProvider } from "@erp/core";
import {
ISequelizeMapper,
MapperParamsType,
@ -25,6 +26,7 @@ import {
CustomerInvoiceSerie,
CustomerInvoiceStatus,
} from "../../domain";
import { InvoiceTaxes } from "../../domain/entities/invoice-taxes";
import { CustomerInvoiceCreationAttributes, CustomerInvoiceModel } from "../sequelize";
import { CustomerInvoiceItemMapper } from "./customer-invoice-item.mapper";
import { InvoiceRecipientMapper } from "./invoice-recipient.mapper";
@ -45,11 +47,103 @@ export class CustomerInvoiceMapper
private _recipientMapper: InvoiceRecipientMapper;
private _taxesMapper: TaxesMapper;
constructor() {
constructor(params: {
taxCatalog: JsonTaxCatalogProvider;
}) {
super();
this._itemsMapper = new CustomerInvoiceItemMapper(); // Instanciar el mapper de items
this._recipientMapper = new InvoiceRecipientMapper();
this._taxesMapper = new TaxesMapper();
this._taxesMapper = new TaxesMapper(params);
}
private mapAttributesToDomain(source: CustomerInvoiceModel, params?: MapperParamsType) {
const { errors } = params as {
errors: ValidationErrorDetail[];
};
const invoiceId = extractOrPushError(UniqueID.create(source.id), "id", errors);
const companyId = extractOrPushError(UniqueID.create(source.company_id), "company_id", errors);
const customerId = extractOrPushError(
UniqueID.create(source.customer_id),
"customer_id",
errors
);
const isProforma = Boolean(source.is_proforma);
const status = extractOrPushError(
CustomerInvoiceStatus.create(source.status),
"status",
errors
);
const series = extractOrPushError(
maybeFromNullableVO(source.series, (value) => CustomerInvoiceSerie.create(value)),
"serie",
errors
);
const invoiceNumber = extractOrPushError(
CustomerInvoiceNumber.create(source.invoice_number),
"invoice_number",
errors
);
const invoiceDate = extractOrPushError(
UtcDate.createFromISO(source.invoice_date),
"invoice_date",
errors
);
const operationDate = extractOrPushError(
maybeFromNullableVO(source.operation_date, (value) => UtcDate.createFromISO(value)),
"operation_date",
errors
);
const notes = extractOrPushError(
maybeFromNullableVO(source.notes, (value) => TextValue.create(value)),
"notes",
errors
);
const languageCode = extractOrPushError(
LanguageCode.create(source.language_code),
"language_code",
errors
);
const currencyCode = extractOrPushError(
CurrencyCode.create(source.currency_code),
"currency_code",
errors
);
const discountPercentage = extractOrPushError(
Percentage.create({
value: source.discount_percentage_value,
scale: source.discount_percentage_scale,
}),
"discount_percentage",
errors
);
return {
invoiceId,
companyId,
customerId,
isProforma,
status,
series,
invoiceNumber,
invoiceDate,
operationDate,
notes,
languageCode,
currencyCode,
discountPercentage,
};
}
public mapToDomain(
@ -59,135 +153,130 @@ export class CustomerInvoiceMapper
try {
const errors: ValidationErrorDetail[] = [];
const invoiceId = extractOrPushError(UniqueID.create(source.id), "id", errors);
const companyId = extractOrPushError(
UniqueID.create(source.company_id),
"company_id",
errors
);
const attributes = this.mapAttributesToDomain(source, { errors, ...params });
const isProforma = Boolean(source.is_proforma);
const requireIncludes = Boolean(params?.requireIncludes);
if (requireIncludes) {
if (!source.items) {
errors.push({
path: "items",
message: "Items not included in query (requireIncludes=true)",
});
}
if (!source.taxes) {
errors.push({
path: "taxes",
message: "Taxes not included in query (requireIncludes=true)",
});
}
const status = extractOrPushError(
CustomerInvoiceStatus.create(source.status),
"status",
errors
);
if (attributes.isProforma && !source.current_customer) {
errors.push({
path: "current_customer",
message: "Current customer not included in query (requireIncludes=true)",
});
}
}
const series = extractOrPushError(
maybeFromNullableVO(source.series, (value) => CustomerInvoiceSerie.create(value)),
"serie",
errors
);
const invoiceNumber = extractOrPushError(
CustomerInvoiceNumber.create(source.invoice_number),
"invoice_number",
errors
);
const invoiceDate = extractOrPushError(
UtcDate.createFromISO(source.invoice_date),
"invoice_date",
errors
);
const operationDate = extractOrPushError(
maybeFromNullableVO(source.operation_date, (value) => UtcDate.createFromISO(value)),
"operation_date",
errors
);
const notes = extractOrPushError(
maybeFromNullableVO(source.notes, (value) => TextValue.create(value)),
"notes",
errors
);
const languageCode = extractOrPushError(
LanguageCode.create(source.language_code),
"language_code",
errors
);
const currencyCode = extractOrPushError(
CurrencyCode.create(source.currency_code),
"currency_code",
errors
);
const discountPercentage = extractOrPushError(
Percentage.create({
value: source.discount_percentage_value,
scale: source.discount_percentage_scale,
}),
"discount_percentage",
errors
);
// Customer
const customerId = extractOrPushError(
UniqueID.create(source.customer_id),
"customer_id",
errors
);
// Recipient (customer data) (snapshot)
const recipient = this._recipientMapper.mapToDomain(source, {
// 3) Recipient (snapshot en la factura o include)
const recipientResult = this._recipientMapper.mapToDomain(source, {
errors,
attributes,
...params,
});
// Mapear los items de la factura
const itemsOrResult = this._itemsMapper.mapArrayToDomain(source.items, {
parent: source,
if (recipientResult.isFailure) {
errors.push({
path: "recipient",
message: recipientResult.error.message,
});
}
// 4) Items (colección)
const itemsResults = this._itemsMapper.mapArrayToDomain(source.items, {
requireIncludes,
errors,
attributes,
...params,
});
// Mapear los impuestos
const taxesOrResult = this._taxesMapper.mapArrayToDomain(source.taxes, {
parent: source,
if (itemsResults.isFailure) {
errors.push({
path: "items",
message: recipientResult.error.message,
});
}
// 5) Taxes (colección a nivel factura)
const taxesResults = this._taxesMapper.mapArrayToDomain(source.taxes, {
errors,
attributes,
...params,
});
if (taxesResults.isFailure) {
errors.push({
path: "taxes",
message: recipientResult.error.message,
});
}
// 6) Si hubo errores de mapeo, devolvemos colección de validación
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("Customer invoice item props mapping failed", errors)
new ValidationErrorCollection("Customer invoice mapping failed", errors)
);
}
// 7) Construcción del agregado (Dominio)
const recipient = recipientResult.data;
const taxes = InvoiceTaxes.create({
items: taxesResults.data.getAll(),
});
const items = CustomerInvoiceItems.create({
languageCode: attributes.languageCode!,
currencyCode: attributes.currencyCode!,
items: itemsResults.data.getAll(),
});
const invoiceProps: CustomerInvoiceProps = {
companyId: companyId!,
companyId: attributes.companyId!,
isProforma: isProforma,
status: status!,
series: series!,
invoiceNumber: invoiceNumber!,
invoiceDate: invoiceDate!,
operationDate: operationDate!,
isProforma: attributes.isProforma,
status: attributes.status!,
series: attributes.series!,
invoiceNumber: attributes.invoiceNumber!,
invoiceDate: attributes.invoiceDate!,
operationDate: attributes.operationDate!,
customerId: customerId!,
customerId: attributes.customerId!,
recipient: recipient,
notes: notes!,
notes: attributes.notes!,
languageCode: languageCode!,
currencyCode: currencyCode!,
languageCode: attributes.languageCode!,
currencyCode: attributes.currencyCode!,
discountPercentage: discountPercentage!,
discountPercentage: attributes.discountPercentage!,
taxes: taxesOrResult,
items: CustomerInvoiceItems.create({
languageCode: languageCode!,
currencyCode: currencyCode!,
items: itemsOrResult.isSuccess ? itemsOrResult.data.getAll() : [],
}),
taxes,
items,
};
return CustomerInvoice.create(invoiceProps, invoiceId);
const createResult = CustomerInvoice.create(invoiceProps, attributes.invoiceId);
if (createResult.isFailure) {
return Result.fail(
new ValidationErrorCollection("Customer invoice entity creation failed", [
{ path: "invoice", message: createResult.error.message },
])
);
}
return Result.ok(createResult.data);
} catch (err: unknown) {
return Result.fail(err as Error);
}
@ -262,9 +351,10 @@ export class CustomerInvoiceMapper
discount_amount_scale: source.discountAmount.scale,
taxable_amount_value: source.taxableAmount.value,
taxable_amount_scale: source.taxableAmount.value,
tax_amount_value: source.taxAmount.value,
tax_amount_scale: source.taxAmount.value,
taxable_amount_scale: source.taxableAmount.scale,
taxes_amount_value: source.taxAmount.value,
taxes_amount_scale: source.taxAmount.scale,
total_amount_value: 0, //total.amount,
total_amount_scale: 2, //total.scale,

View File

@ -9,69 +9,82 @@ import {
maybeFromNullableVO,
} from "@repo/rdx-ddd";
import { MapperParamsType, ValidationErrorDetail, extractOrPushError } from "@erp/core/api";
import { Maybe, isNullishOrEmpty } from "@repo/rdx-utils";
import {
MapperParamsType,
ValidationErrorCollection,
ValidationErrorDetail,
extractOrPushError,
} from "@erp/core/api";
import { Maybe, Result } from "@repo/rdx-utils";
import { InferCreationAttributes } from "sequelize";
import { CustomerInvoice, InvoiceRecipient } from "../../domain";
import { CustomerInvoice, CustomerInvoiceProps, InvoiceRecipient } from "../../domain";
import { CustomerInvoiceModel } from "../sequelize";
export class InvoiceRecipientMapper {
public mapToDomain(source: CustomerInvoiceModel, params?: MapperParamsType) {
const { errors } = params as {
public mapToDomain(
source: CustomerInvoiceModel,
params?: MapperParamsType
): Result<Maybe<InvoiceRecipient>, Error> {
const { errors, attributes } = params as {
errors: ValidationErrorDetail[];
attributes: Partial<CustomerInvoiceProps>;
};
const { isProforma } = attributes;
const _name = isProforma ? source.current_customer.name : source.customer_name;
const _tin = isProforma ? source.current_customer.tin : source.customer_tin;
const _street = isProforma ? source.current_customer.street : source.customer_street;
const _street2 = isProforma ? source.current_customer.street2 : source.customer_street2;
const _city = isProforma ? source.current_customer.city : source.customer_city;
const _postal_code = isProforma
? source.current_customer.postal_code
: source.customer_postal_code;
const _province = isProforma ? source.current_customer.province : source.customer_province;
const _country = isProforma ? source.current_customer.country : source.customer_country;
// Customer (snapshot)
const customerName = extractOrPushError(Name.create(_name), "customer_name", errors);
const customerName = extractOrPushError(
Name.create(source.customer_name),
"customer_name",
errors
);
const customerTin = extractOrPushError(
TINNumber.create(source.customer_tin),
"customer_tin",
errors
);
const customerTin = extractOrPushError(TINNumber.create(_tin), "customer_tin", errors);
const customerStreet = extractOrPushError(
maybeFromNullableVO(source.customer_street, (value) => Street.create(value)),
maybeFromNullableVO(_street, (value) => Street.create(value)),
"customer_street",
errors
);
const customerStreet2 = extractOrPushError(
maybeFromNullableVO(source.customer_street2, (value) => Street.create(value)),
maybeFromNullableVO(_street2, (value) => Street.create(value)),
"customer_street2",
errors
);
const customerCity = extractOrPushError(
maybeFromNullableVO(source.customer_city, (value) => City.create(value)),
maybeFromNullableVO(_city, (value) => City.create(value)),
"customer_city",
errors
);
const customerProvince = extractOrPushError(
maybeFromNullableVO(source.customer_province, (value) => Province.create(value)),
maybeFromNullableVO(_province, (value) => Province.create(value)),
"customer_province",
errors
);
const customerPostalCode = extractOrPushError(
maybeFromNullableVO(source.customer_postal_code, (value) => PostalCode.create(value)),
maybeFromNullableVO(_postal_code, (value) => PostalCode.create(value)),
"customer_postal_code",
errors
);
const customerCountry = extractOrPushError(
maybeFromNullableVO(source.customer_country, (value) => Country.create(value)),
maybeFromNullableVO(_country, (value) => Country.create(value)),
"customer_country",
errors
);
const recipientOrError = InvoiceRecipient.create({
const createResult = InvoiceRecipient.create({
name: customerName!,
tin: customerTin!,
street: customerStreet!,
@ -82,16 +95,21 @@ export class InvoiceRecipientMapper {
country: customerCountry!,
});
return isNullishOrEmpty(recipientOrError)
? Maybe.none<InvoiceRecipient>()
: Maybe.some(recipientOrError.data);
if (createResult.isFailure) {
return Result.fail(
new ValidationErrorCollection("Invoice recipient entity creation failed", [
{ path: "recipient", message: createResult.error.message },
])
);
}
return Result.ok(Maybe.some(createResult.data));
}
public mapToPersistence(
source: InvoiceRecipient,
params?: MapperParamsType
): Partial<InferCreationAttributes<CustomerInvoiceModel, {}>> {
1;
const { index, sourceParent } = params as {
index: number;
sourceParent: CustomerInvoice;

View File

@ -1,9 +1,104 @@
import { MapperParamsType, Taxes } from "@erp/core/api";
import { JsonTaxCatalogProvider } from "@erp/core";
import {
MapperParamsType,
SequelizeMapper,
Tax,
Taxes,
ValidationErrorCollection,
ValidationErrorDetail,
extractOrPushError,
} from "@erp/core/api";
import { Result } from "@repo/rdx-utils";
import { InferCreationAttributes } from "sequelize";
import { CustomerInvoiceItemTaxModel, CustomerInvoiceTaxModel } from "../sequelize";
import { CustomerInvoiceProps, InvoiceAmount } from "../../domain";
import { InvoiceTax } from "../../domain/entities/invoice-taxes";
import {
CustomerInvoiceItemTaxModel,
CustomerInvoiceTaxCreationAttributes,
CustomerInvoiceTaxModel,
} from "../sequelize";
export class TaxesMapper {
public mapArrayToDomain(taxes: CustomerInvoiceTaxModel[], params?: MapperParamsType) {}
export class TaxesMapper extends SequelizeMapper<
CustomerInvoiceTaxModel,
CustomerInvoiceTaxCreationAttributes,
InvoiceTax
> {
private _taxCatalog: JsonTaxCatalogProvider;
constructor(params: {
taxCatalog: JsonTaxCatalogProvider;
}) {
super();
const { taxCatalog } = params;
this._taxCatalog = taxCatalog;
}
public mapToDomain(
source: CustomerInvoiceTaxModel,
params?: MapperParamsType
): Result<InvoiceTax, Error> {
const { errors, index, attributes } = params as {
index: number;
requireIncludes: boolean;
errors: ValidationErrorDetail[];
attributes: Partial<CustomerInvoiceProps>;
};
const tax = extractOrPushError(
Tax.createFromCode(source.tax_code, this._taxCatalog),
`taxes[${index}].tax_code`,
errors
);
const taxableAmount = extractOrPushError(
InvoiceAmount.create({
value: source.taxable_amount_value,
currency_code: attributes.currencyCode?.code,
}),
`taxes[${index}].taxable_amount_value`,
errors
);
if (source.taxable_amount_scale !== InvoiceAmount.DEFAULT_SCALE) {
errors.push({
path: `taxes[${index}].taxable_amount_scale`,
message: "Invalid taxable amount scale",
});
}
const taxesAmount = extractOrPushError(
InvoiceAmount.create({
value: source.taxes_amount_value,
currency_code: attributes.currencyCode?.code,
}),
`taxes[${index}].taxes_amount_value`,
errors
);
if (source.taxes_amount_scale !== InvoiceAmount.DEFAULT_SCALE) {
errors.push({
path: `taxes[${index}].taxes_amount_scale`,
message: "Invalid taxes amount scale",
});
}
// Creación del objeto de dominio
const createResult = InvoiceTax.create({
tax: tax!,
taxableAmount: taxableAmount!,
taxesAmount: taxesAmount!,
});
if (createResult.isFailure) {
return Result.fail(
new ValidationErrorCollection("Invoice taxes creation failed", [
{ path: `taxes[${index}]`, message: createResult.error.message },
])
);
}
return createResult;
}
public mapToPersistence(
source: Taxes,

View File

@ -27,8 +27,8 @@ export class CustomerInvoiceItemTaxModel extends Model<
declare taxable_amount_scale: number;
// Total tax amount / taxes total // 21,00 €
declare tax_amount_value: number;
declare tax_amount_scale: number;
declare taxes_amount_value: number;
declare taxes_amount_scale: number;
// Relaciones
declare item: NonAttribute<CustomerInvoiceItem>;
@ -71,18 +71,20 @@ export default (database: Sequelize) => {
allowNull: true,
defaultValue: null,
},
taxable_amount_scale: {
type: new DataTypes.SMALLINT(),
allowNull: false,
defaultValue: 2,
},
tax_amount_value: {
taxes_amount_value: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true,
defaultValue: null,
},
tax_amount_scale: {
taxes_amount_scale: {
type: new DataTypes.SMALLINT(),
allowNull: false,
defaultValue: 2,

View File

@ -27,8 +27,8 @@ export class CustomerInvoiceTaxModel extends Model<
declare taxable_amount_scale: number;
// Total tax amount / taxes total // 21,00 €
declare tax_amount_value: number;
declare tax_amount_scale: number;
declare taxes_amount_value: number;
declare taxes_amount_scale: number;
// Relaciones
declare invoice: NonAttribute<CustomerInvoice>;
@ -70,18 +70,20 @@ export default (database: Sequelize) => {
allowNull: true,
defaultValue: null,
},
taxable_amount_scale: {
type: new DataTypes.SMALLINT(),
allowNull: false,
defaultValue: 2,
},
tax_amount_value: {
taxes_amount_value: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true,
defaultValue: null,
},
tax_amount_scale: {
taxes_amount_scale: {
type: new DataTypes.SMALLINT(),
allowNull: false,
defaultValue: 2,

View File

@ -19,7 +19,7 @@ import {
export type CustomerInvoiceCreationAttributes = InferCreationAttributes<
CustomerInvoiceModel,
{ omit: "items" | "taxes" | "currentCustomer" }
{ omit: "items" | "taxes" | "current_customer" }
> & {
items?: CustomerInvoiceItemCreationAttributes[];
taxes?: CustomerInvoiceTaxCreationAttributes[];
@ -81,7 +81,7 @@ export class CustomerInvoiceModel extends Model<
// Relaciones
declare items: NonAttribute<CustomerInvoiceItemModel[]>;
declare taxes: NonAttribute<CustomerInvoiceTaxModel[]>;
declare currentCustomer: NonAttribute<CustomerModel>;
declare current_customer: NonAttribute<CustomerModel>;
static associate(database: Sequelize) {
const {
@ -92,7 +92,7 @@ export class CustomerInvoiceModel extends Model<
} = database.models;
CustomerInvoiceModel.belongsTo(CustomerModel, {
as: "currentCustomer",
as: "current_customer",
foreignKey: "customer_id",
constraints: false,
});

View File

@ -159,7 +159,7 @@ export class CustomerInvoiceRepository
query.include = [
{
model: CustomerModel,
as: "currentCustomer",
as: "current_customer",
required: false, // false => LEFT JOIN
},
];