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; const DEFAULT_MAX_SCALE = 4;
export interface TaxProps { export interface TaxProps {
value: number; code: string; // iva_21
scale: number; name: string; // 21% IVA
name: string; value: number; // 2100
code: string; scale: number; // 2
} }
export class Tax extends ValueObject<TaxProps> { export class Tax extends ValueObject<TaxProps> {

View File

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

View File

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

View File

@ -1,4 +1,3 @@
export * from "./customer-invoice-items"; export * from "./customer-invoice-items";
export * from "./invoice-customer";
export * from "./invoice-recipient"; 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-number";
export * from "./customer-invoice-serie"; export * from "./customer-invoice-serie";
export * from "./customer-invoice-status"; export * from "./customer-invoice-status";
export * from "./invoice-amount";
export * from "./item-amount"; export * from "./item-amount";
export * from "./item-discount"; export * from "./item-discount";
export * from "./item-quantity"; 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 { database } = params;
const transactionManager = new SequelizeTransactionManager(database); 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 (!_repo) _repo = new CustomerInvoiceRepository({ mapper: _mapper, database });
if (!_service) _service = new CustomerInvoiceService(_repo); if (!_service) _service = new CustomerInvoiceService(_repo);
if (!_catalogs) _catalogs = { taxes: spainTaxCatalogProvider };
if (!_assemblers) { if (!_assemblers) {
_assemblers = { _assemblers = {
@ -75,8 +78,19 @@ export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps {
catalogs: _catalogs, catalogs: _catalogs,
build: { build: {
list: () => list: () =>
new ListCustomerInvoicesUseCase(_service!, transactionManager!, _assemblers!.list), new ListCustomerInvoicesUseCase(
get: () => new GetCustomerInvoiceUseCase(_service!, transactionManager!, _assemblers!.get), _service!,
transactionManager!,
_assemblers!.list,
_catalogs!.taxes
),
get: () =>
new GetCustomerInvoiceUseCase(
_service!,
transactionManager!,
_assemblers!.get,
_catalogs!.taxes
),
create: () => create: () =>
new CreateCustomerInvoiceUseCase( new CreateCustomerInvoiceUseCase(
_service!, _service!,
@ -85,7 +99,12 @@ export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps {
_catalogs!.taxes _catalogs!.taxes
), ),
update: () => update: () =>
new UpdateCustomerInvoiceUseCase(_service!, transactionManager!, _assemblers!.update), new UpdateCustomerInvoiceUseCase(
_service!,
transactionManager!,
_assemblers!.update,
_catalogs!.taxes
),
delete: () => new DeleteCustomerInvoiceUseCase(_service!, transactionManager!), delete: () => new DeleteCustomerInvoiceUseCase(_service!, transactionManager!),
}, },
presenters: { presenters: {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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