Facturas de cliente

This commit is contained in:
David Arranz 2025-09-10 20:14:19 +02:00
parent df1aa258d9
commit 877d98f188
38 changed files with 443 additions and 190 deletions

View File

@ -0,0 +1,71 @@
import { Criteria } from "@repo/rdx-criteria/server";
import { toEmptyString } from "@repo/rdx-ddd";
import { Collection } from "@repo/rdx-utils";
import { CustomerInvoiceListResponseDTO } from "../../../../common/dto";
import { CustomerInvoice } from "../../../domain";
export class ListCustomerInvoicesAssembler {
toDTO(
customerInvoices: Collection<CustomerInvoice>,
criteria: Criteria
): CustomerInvoiceListResponseDTO {
const items = customerInvoices.map((invoice) => {
const recipient = invoice.recipient.match(
(recipient) => recipient.toString(),
() => ({
tin: "",
name: "",
street: "",
street2: "",
city: "",
postal_code: "",
province: "",
country: "",
})
);
return {
id: invoice.id.toString(),
company_id: invoice.companyId.toString(),
customer_id: invoice.customerId.toString(),
invoice_number: invoice.invoiceNumber.toString(),
status: invoice.status.toPrimitive(),
series: toEmptyString(invoice.series, (value) => value.toString()),
invoice_date: invoice.invoiceDate.toDateString(),
operation_date: toEmptyString(invoice.operationDate, (value) => value.toDateString()),
recipient: {
customer_id: invoice.customerId.toString(),
...recipient,
},
items,
metadata: {
entity: "customer-invoice",
},
};
});
const totalItems = customerInvoices.total();
return {
page: criteria.pageNumber,
per_page: criteria.pageSize,
total_pages: Math.ceil(totalItems / criteria.pageSize),
total_items: totalItems,
items: items,
metadata: {
entity: "customer-invoices",
criteria: criteria.toJSON(),
//links: {
// self: `/api/customer-invoices?page=${criteria.pageNumber}&per_page=${criteria.pageSize}`,
// first: `/api/customer-invoices?page=1&per_page=${criteria.pageSize}`,
// last: `/api/customer-invoices?page=${Math.ceil(totalItems / criteria.pageSize)}&per_page=${criteria.pageSize}`,
//},
},
};
}
}

View File

@ -1,5 +1,6 @@
import { Criteria } from "@repo/rdx-criteria/server"; import { Criteria } from "@repo/rdx-criteria/server";
import { Collection } from "@repo/rdx-utils"; import { toEmptyString } from "@repo/rdx-ddd";
import { ArrayElement, Collection } from "@repo/rdx-utils";
import { CustomerInvoiceListResponseDTO } from "../../../../common/dto"; import { CustomerInvoiceListResponseDTO } from "../../../../common/dto";
import { CustomerInvoice } from "../../../domain"; import { CustomerInvoice } from "../../../domain";
@ -8,27 +9,57 @@ export class ListCustomerInvoicesAssembler {
customerInvoices: Collection<CustomerInvoice>, customerInvoices: Collection<CustomerInvoice>,
criteria: Criteria criteria: Criteria
): CustomerInvoiceListResponseDTO { ): CustomerInvoiceListResponseDTO {
const items = customerInvoices.map((invoice) => { const invoices = customerInvoices.map((invoice) => {
return { const recipientDTO = invoice.recipient.match(
id: invoice.id.toPrimitive(), (recipient) => recipient.toString(),
() => ({
tin: "",
name: "",
street: "",
street2: "",
city: "",
postal_code: "",
province: "",
country: "",
})
);
const allAmounts = invoice.getAllAmounts();
const invoiceDTO: ArrayElement<CustomerInvoiceListResponseDTO["items"]> = {
id: invoice.id.toString(),
company_id: invoice.companyId.toString(),
customer_id: invoice.customerId.toString(),
invoice_status: invoice.status.toString(),
invoice_number: invoice.invoiceNumber.toString(), invoice_number: invoice.invoiceNumber.toString(),
invoice_series: invoice.invoiceSeries.toString(), status: invoice.status.toPrimitive(),
invoice_date: invoice.invoiceDate.toISOString(), series: toEmptyString(invoice.series, (value) => value.toString()),
operation_date: invoice.operationDate.toISOString(),
language_code: "ES",
currency: "EUR",
subtotal_price: invoice.calculateSubtotal().toPrimitive(), invoice_date: invoice.invoiceDate.toDateString(),
total_price: invoice.calculateTotal().toPrimitive(), operation_date: toEmptyString(invoice.operationDate, (value) => value.toDateString()),
//recipient: CustomerInvoiceParticipantAssembler(customerInvoice.recipient), recipient: {
customer_id: invoice.customerId.toString(),
...recipientDTO,
},
taxes: invoice.taxes
.getAll()
.map((taxItem) => taxItem.tax.code)
.join(","),
subtotal_amount: allAmounts.subtotalAmount.toObjectString(),
discount_amount: allAmounts.discountAmount.toObjectString(),
taxable_amount: allAmounts.taxableAmount.toObjectString(),
taxes_amount: allAmounts.taxesAmount.toObjectString(),
total_amount: allAmounts.totalAmount.toObjectString(),
metadata: { metadata: {
entity: "customer-invoice", entity: "customer-invoice",
}, },
}; };
return invoiceDTO;
}); });
const totalItems = customerInvoices.total(); const totalItems = customerInvoices.total();
@ -38,7 +69,7 @@ export class ListCustomerInvoicesAssembler {
per_page: criteria.pageSize, per_page: criteria.pageSize,
total_pages: Math.ceil(totalItems / criteria.pageSize), total_pages: Math.ceil(totalItems / criteria.pageSize),
total_items: totalItems, total_items: totalItems,
items: items, items: invoices,
metadata: { metadata: {
entity: "customer-invoices", entity: "customer-invoices",
criteria: criteria.toJSON(), criteria: criteria.toJSON(),

View File

@ -3,7 +3,6 @@ import {
AggregateRoot, AggregateRoot,
CurrencyCode, CurrencyCode,
LanguageCode, LanguageCode,
MoneyValue,
Percentage, Percentage,
TextValue, TextValue,
UniqueID, UniqueID,
@ -16,6 +15,7 @@ import {
CustomerInvoiceNumber, CustomerInvoiceNumber,
CustomerInvoiceSerie, CustomerInvoiceSerie,
CustomerInvoiceStatus, CustomerInvoiceStatus,
InvoiceAmount,
InvoiceRecipient, InvoiceRecipient,
} from "../value-objects"; } from "../value-objects";
@ -38,22 +38,29 @@ export interface CustomerInvoiceProps {
languageCode: LanguageCode; languageCode: LanguageCode;
currencyCode: CurrencyCode; currencyCode: CurrencyCode;
//subtotalAmount: MoneyValue; taxes: Maybe<InvoiceTaxes>;
items: CustomerInvoiceItems;
discountPercentage: Percentage; discountPercentage: Percentage;
//discountAmount: MoneyValue; }
taxes: InvoiceTaxes; export interface ICustomerInvoice {
getSubtotalAmount(): InvoiceAmount;
getDiscountAmount(): InvoiceAmount;
//totalAmount: MoneyValue; getTaxableAmount(): InvoiceAmount;
getTaxesAmount(): InvoiceAmount;
items: CustomerInvoiceItems; getTotalAmount(): InvoiceAmount;
} }
export type CustomerInvoicePatchProps = Partial<Omit<CustomerInvoiceProps, "companyId">>; export type CustomerInvoicePatchProps = Partial<Omit<CustomerInvoiceProps, "companyId">>;
export class CustomerInvoice extends AggregateRoot<CustomerInvoiceProps> { export class CustomerInvoice
extends AggregateRoot<CustomerInvoiceProps>
implements ICustomerInvoice
{
private _items!: CustomerInvoiceItems; private _items!: CustomerInvoiceItems;
private _taxes!: InvoiceTaxes;
protected constructor(props: CustomerInvoiceProps, id?: UniqueID) { protected constructor(props: CustomerInvoiceProps, id?: UniqueID) {
super(props, id); super(props, id);
@ -63,6 +70,11 @@ export class CustomerInvoice extends AggregateRoot<CustomerInvoiceProps> {
languageCode: props.languageCode, languageCode: props.languageCode,
currencyCode: props.currencyCode, currencyCode: props.currencyCode,
}); });
this._taxes = props.taxes.match(
(taxes) => taxes,
() => InvoiceTaxes.create({})
);
} }
static create(props: CustomerInvoiceProps, id?: UniqueID): Result<CustomerInvoice, Error> { static create(props: CustomerInvoiceProps, id?: UniqueID): Result<CustomerInvoice, Error> {
@ -139,101 +151,60 @@ export class CustomerInvoice extends AggregateRoot<CustomerInvoiceProps> {
return this.props.currencyCode; return this.props.currencyCode;
} }
public get subtotalAmount(): MoneyValue {
throw new Error("discountAmount not implemented");
}
public get discountPercentage(): Percentage { public get discountPercentage(): Percentage {
return this.props.discountPercentage; return this.props.discountPercentage;
} }
public get discountAmount(): MoneyValue {
throw new Error("discountAmount not implemented");
}
public get taxableAmount(): MoneyValue {
throw new Error("taxableAmount not implemented");
}
public get taxAmount(): MoneyValue {
throw new Error("discountAmount not implemented");
}
public get totalAmount(): MoneyValue {
throw new Error("totalAmount not implemented");
}
// Method to get the complete list of line items // Method to get the complete list of line items
get items(): CustomerInvoiceItems { public get items(): CustomerInvoiceItems {
return this._items; return this._items;
} }
get hasRecipient() { public get taxes(): InvoiceTaxes {
return this._taxes;
}
public get hasRecipient() {
return this.recipient.isSome(); return this.recipient.isSome();
} }
/*get senderId(): UniqueID { public getSubtotalAmount(): InvoiceAmount {
return this.props.senderId; const itemsSubtotal = this.items.getTotalAmount().convertScale(2);
}*/
/* get customer(): CustomerInvoiceCustomer | undefined { return InvoiceAmount.create({
return this.props.customer; value: itemsSubtotal.value,
}*/ currency_code: this.currencyCode.code,
}).data as InvoiceAmount;
/*get purchareOrderNumber() {
return this.props.purchareOrderNumber;
} }
get paymentInstructions() { public getDiscountAmount(): InvoiceAmount {
return this.props.paymentInstructions; return this.getSubtotalAmount().percentage(this.discountPercentage) as InvoiceAmount;
} }
get paymentTerms() { public getTaxableAmount(): InvoiceAmount {
return this.props.paymentTerms; return this.getSubtotalAmount().subtract(this.getDiscountAmount()) as InvoiceAmount;
} }
get billTo() { public getTaxesAmount(): InvoiceAmount {
return this.props.billTo; return this._getTaxesAmount(this.getTaxableAmount());
} }
get shipTo() { public getTotalAmount(): InvoiceAmount {
return this.props.shipTo; const taxableAmount = this.getTaxableAmount();
}*/ return taxableAmount.add(this._getTaxesAmount(taxableAmount)) as InvoiceAmount;
}
/* public getAllAmounts() {
addLineItem(lineItem: CustomerInvoiceLineItem, position?: number): void { return {
if (position === undefined) { subtotalAmount: this.getSubtotalAmount(),
this._lineItems.push(lineItem); discountAmount: this.getDiscountAmount(),
} else { taxableAmount: this.getTaxableAmount(),
this._lineItems.splice(position, 0, lineItem); taxesAmount: this.getTaxesAmount(),
} totalAmount: this.getTotalAmount(),
}*/ };
}
/*calculateSubtotal(): MoneyValue { private _getTaxesAmount(_taxableAmount: InvoiceAmount): InvoiceAmount {
const customerInvoiceSubtotal = MoneyValue.create({ return this._taxes.getTaxesAmount(_taxableAmount);
amount: 0, }
currency_code: this.props.currency,
scale: 2,
}).data;
return this._items.getAll().reduce((subtotal, item) => {
return subtotal.add(item.calculateTotal());
}, customerInvoiceSubtotal);
}*/
// Method to calculate the total tax in the customerInvoice
/*calculateTaxTotal(): MoneyValue {
const taxTotal = MoneyValue.create({
amount: 0,
currency_code: this.props.currency,
scale: 2,
}).data;
return taxTotal;
}*/
// Method to calculate the total customerInvoice amount, including taxes
/*calculateTotal(): MoneyValue {
return this.calculateSubtotal().add(this.calculateTaxTotal());
}*/
} }

View File

@ -1,5 +1,6 @@
import { CurrencyCode, LanguageCode } from "@repo/rdx-ddd"; import { CurrencyCode, LanguageCode } from "@repo/rdx-ddd";
import { Collection } from "@repo/rdx-utils"; import { Collection } from "@repo/rdx-utils";
import { ItemAmount } from "../../value-objects";
import { CustomerInvoiceItem } from "./customer-invoice-item"; import { CustomerInvoiceItem } from "./customer-invoice-item";
export interface CustomerInvoiceItemsProps { export interface CustomerInvoiceItemsProps {
@ -26,11 +27,19 @@ export class CustomerInvoiceItems extends Collection<CustomerInvoiceItem> {
add(item: CustomerInvoiceItem): boolean { add(item: CustomerInvoiceItem): boolean {
// Antes de añadir un nuevo item, debo comprobar que el item a añadir // Antes de añadir un nuevo item, debo comprobar que el item a añadir
// tiene el mismo "currencyCode" y "languageCode" que la colección de items. // tiene el mismo "currencyCode" y "languageCode" que la colección de items.
if (!this._languageCode.equals(item.languageCode) || !this._currencyCode.equals(item.currencyCode)) { if (
!this._languageCode.equals(item.languageCode) ||
!this._currencyCode.equals(item.currencyCode)
) {
return false; return false;
} }
return super.add(item) return super.add(item);
} }
public getTotalAmount(): ItemAmount {
return this.getAll().reduce(
(total, tax) => total.add(tax.getTotalAmount()),
ItemAmount.zero(this._currencyCode.code)
);
}
} }

View File

@ -1,4 +1,5 @@
import { Collection } from "@repo/rdx-utils"; import { Collection } from "@repo/rdx-utils";
import { InvoiceAmount } from "../../value-objects";
import { InvoiceTax } from "./invoice-tax"; import { InvoiceTax } from "./invoice-tax";
export interface InvoiceTaxesProps { export interface InvoiceTaxesProps {
@ -14,4 +15,11 @@ export class InvoiceTaxes extends Collection<InvoiceTax> {
public static create(props: InvoiceTaxesProps): InvoiceTaxes { public static create(props: InvoiceTaxesProps): InvoiceTaxes {
return new InvoiceTaxes(props); return new InvoiceTaxes(props);
} }
public getTaxesAmount(taxableAmount: InvoiceAmount): InvoiceAmount {
return this.getAll().reduce(
(total, tax) => total.add(tax.getTaxAmount(taxableAmount)),
InvoiceAmount.zero(taxableAmount.currencyCode)
) as InvoiceAmount;
}
} }

View File

@ -28,8 +28,8 @@ export class CustomerInvoiceAddressType extends ValueObject<ICustomerInvoiceAddr
return this.props.value; return this.props.value;
} }
toString(): string { toString() {
return this.getProps(); return String(this.props.value);
} }
toPrimitive(): string { toPrimitive(): string {

View File

@ -50,8 +50,8 @@ export class CustomerInvoiceItemDescription extends ValueObject<CustomerInvoiceI
return this.props.value; return this.props.value;
} }
toString(): string { toString() {
return this.getProps(); return String(this.props.value);
} }
toPrimitive() { toPrimitive() {

View File

@ -42,8 +42,8 @@ export class CustomerInvoiceNumber extends ValueObject<ICustomerInvoiceNumberPro
return this.props.value; return this.props.value;
} }
toString(): string { toString() {
return this.getProps(); return String(this.props.value);
} }
toPrimitive() { toPrimitive() {

View File

@ -50,8 +50,8 @@ export class CustomerInvoiceSerie extends ValueObject<ICustomerInvoiceSerieProps
return this.props.value; return this.props.value;
} }
toString(): string { toString() {
return this.getProps(); return String(this.props.value);
} }
toPrimitive() { toPrimitive() {

View File

@ -95,7 +95,7 @@ export class CustomerInvoiceStatus extends ValueObject<ICustomerInvoiceStatusPro
return CustomerInvoiceStatus.create(nextStatus); return CustomerInvoiceStatus.create(nextStatus);
} }
toString(): string { toString() {
return this.getProps(); return String(this.props.value);
} }
} }

View File

@ -1,4 +1,5 @@
import { MoneyValue, MoneyValueProps } from "@repo/rdx-ddd"; import { MoneyValue, MoneyValueProps, Percentage, Quantity } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
type InvoiceAmountProps = Pick<MoneyValueProps, "value" | "currency_code">; type InvoiceAmountProps = Pick<MoneyValueProps, "value" | "currency_code">;
@ -11,7 +12,7 @@ export class InvoiceAmount extends MoneyValue {
scale: InvoiceAmount.DEFAULT_SCALE, scale: InvoiceAmount.DEFAULT_SCALE,
currency_code, currency_code,
}; };
return MoneyValue.create(props); return Result.ok(new InvoiceAmount(props));
} }
static zero(currency_code: string) { static zero(currency_code: string) {
@ -19,6 +20,50 @@ export class InvoiceAmount extends MoneyValue {
value: 0, value: 0,
currency_code, currency_code,
}; };
return InvoiceAmount.create(props); return InvoiceAmount.create(props).data;
}
toObjectString() {
return {
value: String(this.value),
scale: String(this.scale),
};
}
// Ensure fluent operations keep the subclass type
convertScale(newScale: number) {
const mv = super.convertScale(newScale);
const p = mv.toPrimitive();
return new InvoiceAmount({ value: p.value, currency_code: p.currency_code });
}
add(addend: MoneyValue) {
const mv = super.add(addend);
const p = mv.toPrimitive();
return new InvoiceAmount({ value: p.value, currency_code: p.currency_code });
}
subtract(subtrahend: MoneyValue) {
const mv = super.subtract(subtrahend);
const p = mv.toPrimitive();
return new InvoiceAmount({ value: p.value, currency_code: p.currency_code });
}
multiply(multiplier: number | Quantity) {
const mv = super.multiply(multiplier);
const p = mv.toPrimitive();
return new InvoiceAmount({ value: p.value, currency_code: p.currency_code });
}
divide(divisor: number | Quantity) {
const mv = super.divide(divisor);
const p = mv.toPrimitive();
return new InvoiceAmount({ value: p.value, currency_code: p.currency_code });
}
percentage(percentage: number | Percentage) {
const mv = super.percentage(percentage);
const p = mv.toPrimitive();
return new InvoiceAmount({ value: p.value, currency_code: p.currency_code });
} }
} }

View File

@ -7,6 +7,7 @@ import {
Street, Street,
TINNumber, TINNumber,
ValueObject, ValueObject,
toEmptyString,
} from "@repo/rdx-ddd"; } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
@ -84,4 +85,17 @@ export class InvoiceRecipient extends ValueObject<InvoiceRecipientProps> {
toPrimitive() { toPrimitive() {
return this.getProps(); return this.getProps();
} }
toString() {
return {
tin: this.tin.toString(),
name: this.name.toString(),
street: toEmptyString(this.street, (value) => value.toString()),
street2: toEmptyString(this.street2, (value) => value.toString()),
city: toEmptyString(this.city, (value) => value.toString()),
postal_code: toEmptyString(this.postalCode, (value) => value.toString()),
province: toEmptyString(this.province, (value) => value.toString()),
country: toEmptyString(this.country, (value) => value.toString()),
};
}
} }

View File

@ -1,4 +1,5 @@
import { MoneyValue, MoneyValueProps } from "@repo/rdx-ddd"; import { MoneyValue, MoneyValueProps, Percentage, Quantity } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
type ItemAmountProps = Pick<MoneyValueProps, "value" | "currency_code">; type ItemAmountProps = Pick<MoneyValueProps, "value" | "currency_code">;
@ -11,7 +12,7 @@ export class ItemAmount extends MoneyValue {
scale: ItemAmount.DEFAULT_SCALE, scale: ItemAmount.DEFAULT_SCALE,
currency_code, currency_code,
}; };
return MoneyValue.create(props); return Result.ok(new ItemAmount(props));
} }
static zero(currency_code: string) { static zero(currency_code: string) {
@ -21,4 +22,41 @@ export class ItemAmount extends MoneyValue {
}; };
return ItemAmount.create(props).data; return ItemAmount.create(props).data;
} }
// Ensure fluent operations keep the subclass type
convertScale(newScale: number) {
const mv = super.convertScale(newScale);
const p = mv.toPrimitive();
return new ItemAmount({ value: p.value, currency_code: p.currency_code });
}
add(addend: MoneyValue) {
const mv = super.add(addend);
const p = mv.toPrimitive();
return new ItemAmount({ value: p.value, currency_code: p.currency_code });
}
subtract(subtrahend: MoneyValue) {
const mv = super.subtract(subtrahend);
const p = mv.toPrimitive();
return new ItemAmount({ value: p.value, currency_code: p.currency_code });
}
multiply(multiplier: number | Quantity) {
const mv = super.multiply(multiplier);
const p = mv.toPrimitive();
return new ItemAmount({ value: p.value, currency_code: p.currency_code });
}
divide(divisor: number | Quantity) {
const mv = super.divide(divisor);
const p = mv.toPrimitive();
return new ItemAmount({ value: p.value, currency_code: p.currency_code });
}
percentage(percentage: number | Percentage) {
const mv = super.percentage(percentage);
const p = mv.toPrimitive();
return new ItemAmount({ value: p.value, currency_code: p.currency_code });
}
} }

View File

@ -15,7 +15,7 @@ import {
UtcDate, UtcDate,
maybeFromNullableVO, maybeFromNullableVO,
} from "@repo/rdx-ddd"; } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import { import {
CustomerInvoice, CustomerInvoice,
CustomerInvoiceItems, CustomerInvoiceItems,
@ -118,13 +118,11 @@ export class CustomerInvoiceMapper
); );
const discountPercentage = extractOrPushError( const discountPercentage = extractOrPushError(
maybeFromNullableVO(source.discount_percentage_value, (value) => Percentage.create({
Percentage.create({ value: source.discount_amount_scale,
value: value, scale: source.discount_percentage_scale,
scale: source.discount_percentage_scale, }),
}) "discount_percentage_value",
),
"discount_percentage",
errors errors
); );
@ -155,31 +153,7 @@ export class CustomerInvoiceMapper
// 1) Valores escalares (atributos generales) // 1) Valores escalares (atributos generales)
const attributes = this.mapAttributesToDomain(source, { errors, ...params }); const attributes = this.mapAttributesToDomain(source, { errors, ...params });
// 2) Comprobar relaciones // 2) Recipient (snapshot en la factura o include)
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)",
});
}
if (attributes.isProforma && !source.current_customer) {
errors.push({
path: "current_customer",
message: "Current customer not included in query (requireIncludes=true)",
});
}
}
// 3) Recipient (snapshot en la factura o include)
const recipientResult = this._recipientMapper.mapToDomain(source, { const recipientResult = this._recipientMapper.mapToDomain(source, {
errors, errors,
attributes, attributes,
@ -194,7 +168,7 @@ export class CustomerInvoiceMapper
}); });
} }
// 4) Items (colección) // 3) Items (colección)
const itemsResults = this._itemsMapper.mapArrayToDomain(source.items, { const itemsResults = this._itemsMapper.mapArrayToDomain(source.items, {
errors, errors,
attributes, attributes,
@ -208,7 +182,7 @@ export class CustomerInvoiceMapper
}); });
} }
// 5) Taxes (colección a nivel factura) // 4) Taxes (colección a nivel factura)
const taxesResults = this._taxesMapper.mapArrayToDomain(source.taxes, { const taxesResults = this._taxesMapper.mapArrayToDomain(source.taxes, {
errors, errors,
attributes, attributes,
@ -222,14 +196,14 @@ export class CustomerInvoiceMapper
}); });
} }
// 6) Si hubo errores de mapeo, devolvemos colección de validación // 5) 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 mapping failed", errors) new ValidationErrorCollection("Customer invoice mapping failed", errors)
); );
} }
// 7) Construcción del agregado (Dominio) // 6) Construcción del agregado (Dominio)
const recipient = recipientResult.data; const recipient = recipientResult.data;
@ -261,9 +235,9 @@ export class CustomerInvoiceMapper
languageCode: attributes.languageCode!, languageCode: attributes.languageCode!,
currencyCode: attributes.currencyCode!, currencyCode: attributes.currencyCode!,
//discountPercentage: attributes.discountPercentage!, discountPercentage: attributes.discountPercentage!,
taxes, taxes: Maybe.some(taxes),
items, items,
}; };

View File

@ -24,6 +24,11 @@ export class InvoiceRecipientMapper {
source: CustomerInvoiceModel, source: CustomerInvoiceModel,
params?: MapperParamsType params?: MapperParamsType
): Result<Maybe<InvoiceRecipient>, Error> { ): Result<Maybe<InvoiceRecipient>, Error> {
/**
* - Factura === proforma -> datos de "current_customer"
* - Factura !== proforma -> snapshot de los datos (campos customer_*)
*/
const { errors, attributes } = params as { const { errors, attributes } = params as {
errors: ValidationErrorDetail[]; errors: ValidationErrorDetail[];
attributes: Partial<CustomerInvoiceProps>; attributes: Partial<CustomerInvoiceProps>;
@ -31,6 +36,13 @@ export class InvoiceRecipientMapper {
const { isProforma } = attributes; const { isProforma } = attributes;
if (isProforma && !source.current_customer) {
errors.push({
path: "current_customer",
message: "Current customer not included in query (InvoiceRecipientMapper)",
});
}
const _name = isProforma ? source.current_customer.name : source.customer_name; const _name = isProforma ? source.current_customer.name : source.customer_name;
const _tin = isProforma ? source.current_customer.tin : source.customer_tin; const _tin = isProforma ? source.current_customer.tin : source.customer_tin;
const _street = isProforma ? source.current_customer.street : source.customer_street; const _street = isProforma ? source.current_customer.street : source.customer_street;

View File

@ -191,6 +191,12 @@ export class CustomerInvoiceRepository
as: "current_customer", as: "current_customer",
required: false, // false => LEFT JOIN required: false, // false => LEFT JOIN
}, },
{
model: CustomerInvoiceTaxModel,
as: "taxes",
required: false,
},
]; ];
const instances = await CustomerInvoiceModel.findAll({ const instances = await CustomerInvoiceModel.findAll({

View File

@ -1,28 +1,37 @@
import { MetadataSchema, createListViewResponseSchema } from "@erp/core"; import { AmountSchema, MetadataSchema, createListViewResponseSchema } from "@erp/core";
import * as z from "zod/v4"; import * as z from "zod/v4";
export const CustomerInvoiceListResponseSchema = createListViewResponseSchema( export const CustomerInvoiceListResponseSchema = createListViewResponseSchema(
z.object({ z.object({
id: z.uuid(), id: z.uuid(),
invoice_status: z.string(), company_id: z.uuid(),
customer_id: z.string(),
invoice_number: z.string(), invoice_number: z.string(),
invoice_series: z.string(), status: z.string(),
invoice_date: z.iso.datetime({ offset: true }), series: z.string(),
operation_date: z.iso.datetime({ offset: true }),
language_code: z.string(),
currency: z.string(),
subtotal_price: z.object({ invoice_date: z.string(),
amount: z.number(), operation_date: z.string(),
scale: z.number(),
currency_code: z.string(),
}),
total_price: z.object({ recipient: {
amount: z.number(), tin: z.string(),
scale: z.number(), name: z.string(),
currency_code: z.string(), street: z.string(),
}), street2: z.string(),
city: z.string(),
postal_code: z.string(),
province: z.string(),
country: z.string(),
},
taxes: z.string(),
subtotal_amount: AmountSchema,
discount_amount: AmountSchema,
taxable_amount: AmountSchema,
taxes_amount: AmountSchema,
total_amount: AmountSchema,
metadata: MetadataSchema.optional(), metadata: MetadataSchema.optional(),
}) })

View File

@ -28,8 +28,8 @@ export class CustomerAddressType extends ValueObject<ICustomerAddressTypeProps>
return this.props.value; return this.props.value;
} }
toString(): string { toString() {
return this.getProps(); return String(this.props.value);
} }
toPrimitive(): string { toPrimitive(): string {

View File

@ -38,8 +38,8 @@ export class CustomerNumber extends ValueObject<ICustomerNumberProps> {
return this.props.value; return this.props.value;
} }
toString(): string { toString() {
return this.getProps(); return String(this.props.value);
} }
toPrimitive() { toPrimitive() {

View File

@ -46,8 +46,8 @@ export class CustomerSerie extends ValueObject<ICustomerSerieProps> {
return this.props.value; return this.props.value;
} }
toString(): string { toString() {
return this.getProps(); return String(this.props.value);
} }
toPrimitive() { toPrimitive() {

View File

@ -53,6 +53,10 @@ export class CustomerStatus extends ValueObject<ICustomerStatusProps> {
return this.getProps(); return this.getProps();
} }
toString() {
return String(this.props.value);
}
canTransitionTo(nextStatus: string): boolean { canTransitionTo(nextStatus: string): boolean {
return CustomerStatus.TRANSITIONS[this.props.value].includes(nextStatus); return CustomerStatus.TRANSITIONS[this.props.value].includes(nextStatus);
} }
@ -65,8 +69,4 @@ export class CustomerStatus extends ValueObject<ICustomerStatusProps> {
} }
return CustomerStatus.create(nextStatus); return CustomerStatus.create(nextStatus);
} }
toString(): string {
return this.getProps();
}
} }

View File

@ -39,6 +39,8 @@ export class CustomerModel extends Model<
declare language_code: string; declare language_code: string;
declare currency_code: string; declare currency_code: string;
declare factuges_id: string;
static associate(database: Sequelize) {} static associate(database: Sequelize) {}
static hooks(database: Sequelize) {} static hooks(database: Sequelize) {}
@ -166,6 +168,12 @@ export default (database: Sequelize) => {
allowNull: false, allowNull: false,
defaultValue: "active", defaultValue: "active",
}, },
factuges_id: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
},
}, },
{ {
sequelize: database, sequelize: database,

View File

@ -35,4 +35,8 @@ export class City extends ValueObject<CityProps> {
toPrimitive() { toPrimitive() {
return this.getProps(); return this.getProps();
} }
toString() {
return String(this.props.value);
}
} }

View File

@ -35,4 +35,8 @@ export class Country extends ValueObject<CountryProps> {
toPrimitive() { toPrimitive() {
return this.getProps(); return this.getProps();
} }
toString() {
return String(this.props.value);
}
} }

View File

@ -45,4 +45,8 @@ export class EmailAddress extends ValueObject<EmailAddressProps> {
toPrimitive() { toPrimitive() {
return this.getProps(); return this.getProps();
} }
toString() {
return String(this.props.value);
}
} }

View File

@ -51,4 +51,8 @@ export class Name extends ValueObject<NameProps> {
toPrimitive() { toPrimitive() {
return this.getProps(); return this.getProps();
} }
toString() {
return String(this.props.value);
}
} }

View File

@ -59,6 +59,10 @@ export class PhoneNumber extends ValueObject<PhoneNumberProps> {
return this.getProps(); return this.getProps();
} }
toString() {
return String(this.props.value);
}
getCountryCode(): string | undefined { getCountryCode(): string | undefined {
return parsePhoneNumberWithError(this.props.value).country; return parsePhoneNumberWithError(this.props.value).country;
} }

View File

@ -1,4 +1,5 @@
import { Maybe, Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import { toEmptyString } from "../helpers";
import { City } from "./city"; import { City } from "./city";
import { Country } from "./country"; import { Country } from "./country";
import { PostalCode } from "./postal-code"; import { PostalCode } from "./postal-code";
@ -15,15 +16,6 @@ export interface PostalAddressProps {
country: Maybe<Country>; country: Maybe<Country>;
} }
export interface PostalAddressSnapshot {
street: string | null;
street2: string | null;
city: string | null;
postalCode: string | null;
province: string | null;
country: string | null;
}
export type PostalAddressPatchProps = Partial<PostalAddressProps>; export type PostalAddressPatchProps = Partial<PostalAddressProps>;
export class PostalAddress extends ValueObject<PostalAddressProps> { export class PostalAddress extends ValueObject<PostalAddressProps> {
@ -81,6 +73,17 @@ export class PostalAddress extends ValueObject<PostalAddressProps> {
return this.getProps(); return this.getProps();
} }
toString() {
return {
street: toEmptyString(this.street, (value) => value.toString()),
street2: toEmptyString(this.street2, (value) => value.toString()),
city: toEmptyString(this.city, (value) => value.toString()),
postal_code: toEmptyString(this.postalCode, (value) => value.toString()),
province: toEmptyString(this.province, (value) => value.toString()),
country: toEmptyString(this.country, (value) => value.toString()),
};
}
toFormat(): string { toFormat(): string {
return `${this.props.street}, ${this.props.street2}, ${this.props.city}, ${this.props.postalCode}, ${this.props.province}, ${this.props.country}`; return `${this.props.street}, ${this.props.street2}, ${this.props.city}, ${this.props.postalCode}, ${this.props.province}, ${this.props.country}`;
} }

View File

@ -43,4 +43,8 @@ export class PostalCode extends ValueObject<PostalCodeProps> {
toPrimitive(): string { toPrimitive(): string {
return this.props.value; return this.props.value;
} }
toString() {
return String(this.props.value);
}
} }

View File

@ -35,4 +35,8 @@ export class Province extends ValueObject<ProvinceProps> {
toPrimitive() { toPrimitive() {
return this.getProps(); return this.getProps();
} }
toString() {
return String(this.props.value);
}
} }

View File

@ -39,4 +39,8 @@ export class Slug extends ValueObject<SlugProps> {
toPrimitive(): string { toPrimitive(): string {
return this.getProps(); return this.getProps();
} }
toString() {
return String(this.props.value);
}
} }

View File

@ -35,4 +35,8 @@ export class Street extends ValueObject<StreetProps> {
toPrimitive() { toPrimitive() {
return this.getProps(); return this.getProps();
} }
toString() {
return String(this.props.value);
}
} }

View File

@ -43,4 +43,8 @@ export class TaxCode extends ValueObject<TaxCodeProps> {
toPrimitive(): string { toPrimitive(): string {
return this.getProps(); return this.getProps();
} }
toString() {
return String(this.props.value);
}
} }

View File

@ -40,4 +40,8 @@ export class TINNumber extends ValueObject<TINNumberProps> {
toPrimitive(): string { toPrimitive(): string {
return this.props.value; return this.props.value;
} }
toString() {
return String(this.props.value);
}
} }

View File

@ -29,4 +29,8 @@ export class URLAddress extends ValueObject<URLAddressProps> {
toPrimitive() { toPrimitive() {
return this.getProps(); return this.getProps();
} }
toString() {
return String(this.props.value);
}
} }

View File

@ -58,6 +58,10 @@ export class UtcDate extends ValueObject<UtcDateProps> {
return this.date.toISOString().split("T")[0]; return this.date.toISOString().split("T")[0];
} }
toString() {
return this.toDateString();
}
/** /**
* Devuelve la fecha en formato UTC con hora (ISO 8601). Ejemplo: 2025-12-31T23:59:59Z. * Devuelve la fecha en formato UTC con hora (ISO 8601). Ejemplo: 2025-12-31T23:59:59Z.
*/ */

View File

@ -5,4 +5,5 @@ export * from "./patch-field";
export * from "./result"; export * from "./result";
export * from "./result-collection"; export * from "./result-collection";
export * from "./rule-validator"; export * from "./rule-validator";
export * from "./types";
export * from "./utils"; export * from "./utils";

View File

@ -0,0 +1 @@
export type ArrayElement<T> = T extends readonly (infer U)[] ? U : never;