Facturas de cliente

This commit is contained in:
David Arranz 2025-09-03 12:41:12 +02:00
parent b302870647
commit db4a79422e
25 changed files with 472 additions and 506 deletions

View File

@ -1,6 +1,13 @@
import { AggregateRoot, UniqueID, UtcDate } from "@repo/rdx-ddd"; import {
import { Collection, Result } from "@repo/rdx-utils"; AggregateRoot,
import { CustomerInvoiceCustomer, CustomerInvoiceItem, CustomerInvoiceItems } from "../entities"; CurrencyCode,
LanguageCode,
TextValue,
UniqueID,
UtcDate,
} from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
import { CustomerInvoiceItems } from "../entities";
import { import {
CustomerInvoiceNumber, CustomerInvoiceNumber,
CustomerInvoiceSerie, CustomerInvoiceSerie,
@ -8,20 +15,19 @@ import {
} from "../value-objects"; } from "../value-objects";
export interface CustomerInvoiceProps { export interface CustomerInvoiceProps {
invoiceNumber: CustomerInvoiceNumber; companyId: UniqueID;
invoiceSeries: CustomerInvoiceSerie;
status: CustomerInvoiceStatus; status: CustomerInvoiceStatus;
series: Maybe<CustomerInvoiceSerie>;
invoiceNumber: CustomerInvoiceNumber;
issueDate: UtcDate; issueDate: UtcDate;
operationDate: UtcDate; operationDate: Maybe<UtcDate>;
notes: Maybe<TextValue>;
//dueDate: UtcDate; // ? --> depende de la forma de pago //dueDate: UtcDate; // ? --> depende de la forma de pago
//tax: Tax; // ? --> detalles? //tax: Tax; // ? --> detalles?
currency: string;
//language: Language;
//purchareOrderNumber: string; //purchareOrderNumber: string;
//notes: Note; //notes: Note;
@ -31,53 +37,27 @@ export interface CustomerInvoiceProps {
//paymentInstructions: Note; //paymentInstructions: Note;
//paymentTerms: string; //paymentTerms: string;
customer?: CustomerInvoiceCustomer; languageCode: LanguageCode;
currencyCode: CurrencyCode;
//customer?: CustomerInvoiceCustomer;
items?: CustomerInvoiceItems; items?: CustomerInvoiceItems;
} }
export interface ICustomerInvoice { export type CustomerInvoicePatchProps = Partial<Omit<CustomerInvoiceProps, "companyId">>;
id: UniqueID;
invoiceNumber: CustomerInvoiceNumber;
invoiceSeries: CustomerInvoiceSerie;
status: CustomerInvoiceStatus; export class CustomerInvoice extends AggregateRoot<CustomerInvoiceProps> {
private _items!: CustomerInvoiceItems;
issueDate: UtcDate;
operationDate: UtcDate;
//senderId: UniqueID;
customer?: CustomerInvoiceCustomer;
//dueDate
//tax: Tax;
//language: Language;
currency: string;
//purchareOrderNumber: string;
//notes: Note;
//paymentInstructions: Note;
//paymentTerms: string;
items: CustomerInvoiceItems;
calculateSubtotal: () => MoneyValue;
calculateTaxTotal: () => MoneyValue;
calculateTotal: () => MoneyValue;
}
export class CustomerInvoice
extends AggregateRoot<CustomerInvoiceProps>
implements ICustomerInvoice
{
private _items!: Collection<CustomerInvoiceItem>;
//protected _status: CustomerInvoiceStatus; //protected _status: CustomerInvoiceStatus;
protected constructor(props: CustomerInvoiceProps, id?: UniqueID) { protected constructor(props: CustomerInvoiceProps, id?: UniqueID) {
super(props, id); super(props, id);
this._items = props.items || CustomerInvoiceItems.create(); this._items =
props.items ||
CustomerInvoiceItems.create({
languageCode: props.languageCode,
currencyCode: props.currencyCode,
});
} }
static create(props: CustomerInvoiceProps, id?: UniqueID): Result<CustomerInvoice, Error> { static create(props: CustomerInvoiceProps, id?: UniqueID): Result<CustomerInvoice, Error> {
@ -94,50 +74,55 @@ export class CustomerInvoice
return Result.ok(customerInvoice); return Result.ok(customerInvoice);
} }
get invoiceNumber() { public update(partialInvoice: CustomerInvoicePatchProps): Result<CustomerInvoice, Error> {
throw new Error("Not implemented");
}
public get companyId(): UniqueID {
return this.props.companyId;
}
public get series(): Maybe<CustomerInvoiceSerie> {
return this.props.series;
}
public get invoiceNumber() {
return this.props.invoiceNumber; return this.props.invoiceNumber;
} }
get invoiceSeries() { public get issueDate(): UtcDate {
return this.props.invoiceSeries; return this.props.issueDate;
} }
get issueDate() { public get operationDate(): Maybe<UtcDate> {
return this.props.issueDate; return this.props.operationDate;
}
public get notes(): Maybe<TextValue> {
return this.props.notes;
}
public get languageCode(): LanguageCode {
return this.props.languageCode;
}
public get currencyCode(): CurrencyCode {
return this.props.currencyCode;
}
// Method to get the complete list of line items
get lineItems(): CustomerInvoiceItems {
return this._items;
} }
/*get senderId(): UniqueID { /*get senderId(): UniqueID {
return this.props.senderId; return this.props.senderId;
}*/ }*/
get customer(): CustomerInvoiceCustomer | undefined { /* get customer(): CustomerInvoiceCustomer | undefined {
return this.props.customer; return this.props.customer;
}
get operationDate() {
return this.props.operationDate;
}
/*get language() {
return this.props.language;
}*/ }*/
get dueDate() {
return undefined;
}
get tax() {
return undefined;
}
get status() {
return this.props.status;
}
get items() {
return this._items;
}
/*get purchareOrderNumber() { /*get purchareOrderNumber() {
return this.props.purchareOrderNumber; return this.props.purchareOrderNumber;
} }
@ -158,19 +143,7 @@ export class CustomerInvoice
return this.props.shipTo; return this.props.shipTo;
}*/ }*/
get currency() { /*
return this.props.currency;
}
/*get notes() {
return this.props.notes;
}*/
// Method to get the complete list of line items
/*get lineItems(): CustomerInvoiceLineItem[] {
return this._lineItems;
}
addLineItem(lineItem: CustomerInvoiceLineItem, position?: number): void { addLineItem(lineItem: CustomerInvoiceLineItem, position?: number): void {
if (position === undefined) { if (position === undefined) {
this._lineItems.push(lineItem); this._lineItems.push(lineItem);
@ -179,7 +152,7 @@ export class CustomerInvoice
} }
}*/ }*/
calculateSubtotal(): MoneyValue { /*calculateSubtotal(): MoneyValue {
const customerInvoiceSubtotal = MoneyValue.create({ const customerInvoiceSubtotal = MoneyValue.create({
amount: 0, amount: 0,
currency_code: this.props.currency, currency_code: this.props.currency,
@ -189,10 +162,10 @@ export class CustomerInvoice
return this._items.getAll().reduce((subtotal, item) => { return this._items.getAll().reduce((subtotal, item) => {
return subtotal.add(item.calculateTotal()); return subtotal.add(item.calculateTotal());
}, customerInvoiceSubtotal); }, customerInvoiceSubtotal);
} }*/
// Method to calculate the total tax in the customerInvoice // Method to calculate the total tax in the customerInvoice
calculateTaxTotal(): MoneyValue { /*calculateTaxTotal(): MoneyValue {
const taxTotal = MoneyValue.create({ const taxTotal = MoneyValue.create({
amount: 0, amount: 0,
currency_code: this.props.currency, currency_code: this.props.currency,
@ -200,10 +173,10 @@ export class CustomerInvoice
}).data; }).data;
return taxTotal; return taxTotal;
} }*/
// Method to calculate the total customerInvoice amount, including taxes // Method to calculate the total customerInvoice amount, including taxes
calculateTotal(): MoneyValue { /*calculateTotal(): MoneyValue {
return this.calculateSubtotal().add(this.calculateTaxTotal()); return this.calculateSubtotal().add(this.calculateTaxTotal());
} }*/
} }

View File

@ -1,4 +1,4 @@
import { MoneyValue, Percentage, Quantity } from "@/core/common/domain"; import { CurrencyCode, LanguageCode, MoneyValue, Percentage, Quantity } from "@repo/rdx-ddd";
import { CustomerInvoiceItemDescription } from "../../value-objects"; import { CustomerInvoiceItemDescription } from "../../value-objects";
import { CustomerInvoiceItem } from "./customer-invoice-item"; import { CustomerInvoiceItem } from "./customer-invoice-item";
@ -9,6 +9,8 @@ describe("CustomerInvoiceItem", () => {
quantity: Quantity.create({ amount: 200, scale: 2 }), quantity: Quantity.create({ amount: 200, scale: 2 }),
unitPrice: MoneyValue.create(50), unitPrice: MoneyValue.create(50),
discount: Percentage.create(0), discount: Percentage.create(0),
languageCode: LanguageCode.create("es"),
currencyCode: CurrencyCode.create("EUR"),
}; };
const result = CustomerInvoiceItem.create(props); const result = CustomerInvoiceItem.create(props);

View File

@ -0,0 +1,102 @@
import { CurrencyCode, DomainEntity, LanguageCode, UniqueID } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
import {
CustomerInvoiceItemDescription,
CustomerInvoiceItemDiscount,
CustomerInvoiceItemQuantity,
CustomerInvoiceItemSubtotalPrice,
CustomerInvoiceItemTotalPrice,
CustomerInvoiceItemUnitPrice,
} from "../../value-objects";
export interface CustomerInvoiceItemProps {
description: Maybe<CustomerInvoiceItemDescription>;
quantity: Maybe<CustomerInvoiceItemQuantity>; // Cantidad de unidades
unitPrice: Maybe<CustomerInvoiceItemUnitPrice>; // Precio unitario en la moneda de la factura
discount: Maybe<CustomerInvoiceItemDiscount>; // % descuento
languageCode: LanguageCode;
currencyCode: CurrencyCode;
}
export class CustomerInvoiceItem extends DomainEntity<CustomerInvoiceItemProps> {
private _subtotalPrice!: CustomerInvoiceItemSubtotalPrice;
private _totalPrice!: CustomerInvoiceItemTotalPrice;
public static create(
props: CustomerInvoiceItemProps,
id?: UniqueID
): Result<CustomerInvoiceItem, Error> {
const item = new CustomerInvoiceItem(props, id);
// Reglas de negocio / validaciones
// ...
// ...
// 🔹 Disparar evento de dominio "CustomerInvoiceItemCreatedEvent"
//const { customerInvoice } = props;
//user.addDomainEvent(new CustomerInvoiceAuthenticatedEvent(id, customerInvoice.toString()));
return Result.ok(item);
}
get description(): Maybe<CustomerInvoiceItemDescription> {
return this.props.description;
}
get quantity(): Maybe<CustomerInvoiceItemQuantity> {
return this.props.quantity;
}
get unitPrice(): Maybe<CustomerInvoiceItemUnitPrice> {
return this.props.unitPrice;
}
get subtotalPrice(): CustomerInvoiceItemSubtotalPrice {
if (!this._subtotalPrice) {
this._subtotalPrice = this.calculateSubtotal();
}
return this._subtotalPrice;
}
get discount(): Maybe<CustomerInvoiceItemDiscount> {
return this.props.discount;
}
get totalPrice(): CustomerInvoiceItemTotalPrice {
if (!this._totalPrice) {
this._totalPrice = this.calculateTotal();
}
return this._totalPrice;
}
public get languageCode(): LanguageCode {
return this.props.languageCode;
}
public get currencyCode(): CurrencyCode {
return this.props.currencyCode;
}
getValue(): CustomerInvoiceItemProps {
return this.props;
}
toPrimitive() {
return this.getValue();
}
calculateSubtotal(): CustomerInvoiceItemSubtotalPrice {
throw new Error("Not implemented");
/*const unitPrice = this.unitPrice.isSome()
? this.unitPrice.unwrap()
: CustomerInvoiceItemUnitPrice.zero();
return this.unitPrice.multiply(this.quantity.toNumber()); // Precio unitario * Cantidad*/
}
calculateTotal(): CustomerInvoiceItemTotalPrice {
throw new Error("Not implemented");
//return this.subtotalPrice.subtract(this.subtotalPrice.percentage(this.discount.toNumber()));
}
}

View File

@ -0,0 +1,25 @@
import { CurrencyCode, LanguageCode } from "@repo/rdx-ddd";
import { Collection } from "@repo/rdx-utils";
import { CustomerInvoiceItem } from "./customer-invoice-item";
export interface CustomerInvoiceItemsProps {
items?: CustomerInvoiceItem[];
languageCode: LanguageCode;
currencyCode: CurrencyCode;
}
export class CustomerInvoiceItems extends Collection<CustomerInvoiceItem> {
private _languageCode!: LanguageCode;
private _currencyCode!: CurrencyCode;
constructor(props: CustomerInvoiceItemsProps) {
const { items, languageCode, currencyCode } = props;
super(items);
this._languageCode = languageCode;
this._currencyCode = currencyCode;
}
public static create(props: CustomerInvoiceItemsProps): CustomerInvoiceItems {
return new CustomerInvoiceItems(props);
}
}

View File

@ -1,2 +1,2 @@
export * from "./invoice-item"; export * from "./customer-invoice-item";
export * from "./invoice-items"; export * from "./customer-invoice-items";

View File

@ -1,107 +0,0 @@
import { DomainEntity, UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import {
CustomerInvoiceItemDescription,
CustomerInvoiceItemDiscount,
CustomerInvoiceItemQuantity,
CustomerInvoiceItemSubtotalPrice,
CustomerInvoiceItemTotalPrice,
CustomerInvoiceItemUnitPrice,
} from "../../value-objects";
export interface ICustomerInvoiceItemProps {
description: CustomerInvoiceItemDescription;
quantity: CustomerInvoiceItemQuantity; // Cantidad de unidades
unitPrice: CustomerInvoiceItemUnitPrice; // Precio unitario en la moneda de la factura
//subtotalPrice?: MoneyValue; // Precio unitario * Cantidad
discount: CustomerInvoiceItemDiscount; // % descuento
//totalPrice?: MoneyValue;
}
export interface ICustomerInvoiceItem {
id: UniqueID;
description: CustomerInvoiceItemDescription;
quantity: CustomerInvoiceItemQuantity;
unitPrice: CustomerInvoiceItemUnitPrice;
subtotalPrice: CustomerInvoiceItemSubtotalPrice;
discount: CustomerInvoiceItemDiscount;
totalPrice: CustomerInvoiceItemTotalPrice;
}
export class CustomerInvoiceItem
extends DomainEntity<ICustomerInvoiceItemProps>
implements ICustomerInvoiceItem
{
private _subtotalPrice!: CustomerInvoiceItemSubtotalPrice;
private _totalPrice!: CustomerInvoiceItemTotalPrice;
public static create(
props: ICustomerInvoiceItemProps,
id?: UniqueID
): Result<CustomerInvoiceItem, Error> {
const item = new CustomerInvoiceItem(props, id);
// Reglas de negocio / validaciones
// ...
// ...
// 🔹 Disparar evento de dominio "CustomerInvoiceItemCreatedEvent"
//const { customerInvoice } = props;
//user.addDomainEvent(new CustomerInvoiceAuthenticatedEvent(id, customerInvoice.toString()));
return Result.ok(item);
}
get description(): CustomerInvoiceItemDescription {
return this.props.description;
}
get quantity(): CustomerInvoiceItemQuantity {
return this.props.quantity;
}
get unitPrice(): CustomerInvoiceItemUnitPrice {
return this.props.unitPrice;
}
get subtotalPrice(): CustomerInvoiceItemSubtotalPrice {
if (!this._subtotalPrice) {
this._subtotalPrice = this.calculateSubtotal();
}
return this._subtotalPrice;
}
get discount(): CustomerInvoiceItemDiscount {
return this.props.discount;
}
get totalPrice(): CustomerInvoiceItemTotalPrice {
if (!this._totalPrice) {
this._totalPrice = this.calculateTotal();
}
return this._totalPrice;
}
getValue() {
return this.props;
}
toPrimitive() {
return {
description: this.description.toPrimitive(),
quantity: this.quantity.toPrimitive(),
unit_price: this.unitPrice.toPrimitive(),
subtotal_price: this.subtotalPrice.toPrimitive(),
discount: this.discount.toPrimitive(),
total_price: this.totalPrice.toPrimitive(),
};
}
calculateSubtotal(): CustomerInvoiceItemSubtotalPrice {
return this.unitPrice.multiply(this.quantity.toNumber()); // Precio unitario * Cantidad
}
calculateTotal(): CustomerInvoiceItemTotalPrice {
return this.subtotalPrice.subtract(this.subtotalPrice.percentage(this.discount.toNumber()));
}
}

View File

@ -1,8 +0,0 @@
import { Collection } from "@repo/rdx-utils";
import { CustomerInvoiceItem } from "./customer-invoice-item";
export class CustomerInvoiceItems extends Collection<CustomerInvoiceItem> {
public static create(items?: CustomerInvoiceItem[]): CustomerInvoiceItems {
return new CustomerInvoiceItems(items);
}
}

View File

@ -3,12 +3,15 @@ import { UniqueID } from "@repo/rdx-ddd";
import { Collection, Result } from "@repo/rdx-utils"; import { Collection, Result } from "@repo/rdx-utils";
import { CustomerInvoice } from "../aggregates"; import { CustomerInvoice } from "../aggregates";
/**
* Interfaz del repositorio para el agregado `CustomerInvoice`.
* El escopado multitenant está representado por `companyId`.
*/
export interface ICustomerInvoiceRepository { export interface ICustomerInvoiceRepository {
existsById(id: UniqueID, transaction?: any): Promise<Result<boolean, Error>>;
/** /**
* *
* Persiste una nueva factura o actualiza una existente. * Persiste una nueva factura o actualiza una existente.
* Retorna el objeto actualizado tras la operación.
* *
* @param invoice - El agregado a guardar. * @param invoice - El agregado a guardar.
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
@ -17,34 +20,55 @@ export interface ICustomerInvoiceRepository {
save(invoice: CustomerInvoice, transaction: any): Promise<Result<CustomerInvoice, Error>>; save(invoice: CustomerInvoice, transaction: any): Promise<Result<CustomerInvoice, Error>>;
/** /**
* * Comprueba si existe una factura con un `id` dentro de una `company`.
* Busca una factura por su identificador único.
* @param id - UUID de la factura.
* @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice, Error>
*/ */
findById(id: UniqueID, transaction: any): Promise<Result<CustomerInvoice, Error>>; existsByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction?: any
): Promise<Result<boolean, Error>>;
/**
* Recupera una factura por su ID y companyId.
* Devuelve un `NotFoundError` si no se encuentra.
*/
getByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction?: any
): Promise<Result<CustomerInvoice, Error>>;
/** /**
* *
* Consulta facturas usando un objeto Criteria (filtros, orden, paginación). * Consulta facturas dentro de una empresa usando un
* objeto Criteria (filtros, orden, paginación).
* El resultado está encapsulado en un objeto `Collection<T>`.
*
* @param companyId - ID de la empresa.
* @param criteria - Criterios de búsqueda. * @param criteria - Criterios de búsqueda.
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice[], Error> * @returns Result<CustomerInvoice[], Error>
* *
* @see Criteria * @see Criteria
*/ */
findByCriteria( findByCriteriaInCompany(
companyId: UniqueID,
criteria: Criteria, criteria: Criteria,
transaction: any transaction: any
): Promise<Result<Collection<CustomerInvoice>, Error>>; ): Promise<Result<Collection<CustomerInvoice>, Error>>;
/** /**
* *
* Elimina o marca como eliminada una factura. * Elimina o marca como eliminada una factura dentro de una empresa.
*
* @param companyId - ID de la empresa.
* @param id - UUID de la factura a eliminar. * @param id - UUID de la factura a eliminar.
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<void, Error> * @returns Result<void, Error>
*/ */
deleteById(id: UniqueID, transaction: any): Promise<Result<void, Error>>; deleteByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction: any
): Promise<Result<void, Error>>;
} }

View File

@ -14,10 +14,10 @@ export class CustomerInvoiceAddressType extends ValueObject<ICustomerInvoiceAddr
private static readonly ALLOWED_TYPES = ["shipping", "billing"]; private static readonly ALLOWED_TYPES = ["shipping", "billing"];
static create(value: string): Result<CustomerInvoiceAddressType, Error> { static create(value: string): Result<CustomerInvoiceAddressType, Error> {
if (!this.ALLOWED_TYPES.includes(value)) { if (!CustomerInvoiceAddressType.ALLOWED_TYPES.includes(value)) {
return Result.fail( return Result.fail(
new Error( new Error(
`Invalid address type: ${value}. Allowed types are: ${this.ALLOWED_TYPES.join(", ")}` `Invalid address type: ${value}. Allowed types are: ${CustomerInvoiceAddressType.ALLOWED_TYPES.join(", ")}`
) )
); );
} }

View File

@ -3,11 +3,11 @@ import { ValueObject } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import * as z from "zod/v4"; import * as z from "zod/v4";
interface ICustomerInvoiceItemDescriptionProps { interface CustomerInvoiceItemDescriptionProps {
value: string; value: string;
} }
export class CustomerInvoiceItemDescription extends ValueObject<ICustomerInvoiceItemDescriptionProps> { export class CustomerInvoiceItemDescription extends ValueObject<CustomerInvoiceItemDescriptionProps> {
private static readonly MAX_LENGTH = 255; private static readonly MAX_LENGTH = 255;
private static readonly FIELD = "invoiceItemDescription"; private static readonly FIELD = "invoiceItemDescription";
private static readonly ERROR_CODE = "INVALID_INVOICE_ITEM_DESCRIPTION"; private static readonly ERROR_CODE = "INVALID_INVOICE_ITEM_DESCRIPTION";
@ -23,10 +23,10 @@ export class CustomerInvoiceItemDescription extends ValueObject<ICustomerInvoice
} }
static create(value: string) { static create(value: string) {
const result = CustomerInvoiceItemDescription.validate(value); const valueIsValid = CustomerInvoiceItemDescription.validate(value);
if (!result.success) { if (!valueIsValid.success) {
const detail = result.error.message; const detail = valueIsValid.error.message;
return Result.fail( return Result.fail(
new DomainValidationError( new DomainValidationError(
CustomerInvoiceItemDescription.ERROR_CODE, CustomerInvoiceItemDescription.ERROR_CODE,

View File

@ -11,4 +11,13 @@ export class CustomerInvoiceItemUnitPrice extends MoneyValue {
}; };
return MoneyValue.create(props); return MoneyValue.create(props);
} }
static zero(currency_code: string, scale: number = CustomerInvoiceItemUnitPrice.DEFAULT_SCALE) {
const props: MoneyValueProps = {
amount: 0,
scale,
currency_code,
};
return MoneyValue.create(props);
}
} }

View File

@ -23,10 +23,10 @@ export class CustomerInvoiceNumber extends ValueObject<ICustomerInvoiceNumberPro
} }
static create(value: string) { static create(value: string) {
const result = CustomerInvoiceNumber.validate(value); const valueIsValid = CustomerInvoiceNumber.validate(value);
if (!result.success) { if (!valueIsValid.success) {
const detail = result.error.message; const detail = valueIsValid.error.message;
return Result.fail( return Result.fail(
new DomainValidationError( new DomainValidationError(
CustomerInvoiceNumber.ERROR_CODE, CustomerInvoiceNumber.ERROR_CODE,

View File

@ -23,10 +23,10 @@ export class CustomerInvoiceSerie extends ValueObject<ICustomerInvoiceSerieProps
} }
static create(value: string) { static create(value: string) {
const result = CustomerInvoiceSerie.validate(value); const valueIsValid = CustomerInvoiceSerie.validate(value);
if (!result.success) { if (!valueIsValid.success) {
const detail = result.error.message; const detail = valueIsValid.error.message;
return Result.fail( return Result.fail(
new DomainValidationError( new DomainValidationError(
CustomerInvoiceSerie.ERROR_CODE, CustomerInvoiceSerie.ERROR_CODE,

View File

@ -4,18 +4,18 @@ import { customerInvoicesRouter, models } from "./infrastructure";
export const customerInvoicesAPIModule: IModuleServer = { export const customerInvoicesAPIModule: IModuleServer = {
name: "customer-invoices", name: "customer-invoices",
version: "1.0.0", version: "1.0.0",
dependencies: [], dependencies: ["customers"],
async init(params: ModuleParams) { async init(params: ModuleParams) {
// const contacts = getService<ContactsService>("contacts"); // const contacts = getService<ContactsService>("contacts");
const { logger } = params; const { logger } = params;
customerInvoicesRouter(params); customerInvoicesRouter(params);
logger.info("🚀 CustomerInvoices module initialized", { label: "customer-invoices" }); logger.info("🚀 CustomerInvoices module initialized", { label: this.name });
}, },
async registerDependencies(params) { async registerDependencies(params) {
const { database, logger } = params; const { database, logger } = params;
logger.info("🚀 CustomerInvoices module dependencies registered", { logger.info("🚀 CustomerInvoices module dependencies registered", {
label: "customer-invoices", label: this.name,
}); });
return { return {
models, models,

View File

@ -8,6 +8,8 @@ import {
GetCustomerInvoiceUseCase, GetCustomerInvoiceUseCase,
ListCustomerInvoicesAssembler, ListCustomerInvoicesAssembler,
ListCustomerInvoicesUseCase, ListCustomerInvoicesUseCase,
UpdateCustomerInvoiceAssembler,
UpdateCustomerInvoiceUseCase,
} from "../application"; } from "../application";
import { CustomerInvoiceService, ICustomerInvoiceService } from "../domain"; import { CustomerInvoiceService, ICustomerInvoiceService } from "../domain";
import { CustomerInvoiceMapper } from "./mappers"; import { CustomerInvoiceMapper } from "./mappers";
@ -22,13 +24,13 @@ type InvoiceDeps = {
list: ListCustomerInvoicesAssembler; list: ListCustomerInvoicesAssembler;
get: GetCustomerInvoiceAssembler; get: GetCustomerInvoiceAssembler;
create: CreateCustomerInvoicesAssembler; create: CreateCustomerInvoicesAssembler;
//update: UpdateCustomerInvoiceAssembler; update: UpdateCustomerInvoiceAssembler;
}; };
build: { build: {
list: () => ListCustomerInvoicesUseCase; list: () => ListCustomerInvoicesUseCase;
get: () => GetCustomerInvoiceUseCase; get: () => GetCustomerInvoiceUseCase;
create: () => CreateCustomerInvoiceUseCase; create: () => CreateCustomerInvoiceUseCase;
//update: () => UpdateCustomerInvoiceUseCase; update: () => UpdateCustomerInvoiceUseCase;
delete: () => DeleteCustomerInvoiceUseCase; delete: () => DeleteCustomerInvoiceUseCase;
}; };
presenters: { presenters: {
@ -54,7 +56,7 @@ export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps {
list: new ListCustomerInvoicesAssembler(), // transforma domain → ListDTO list: new ListCustomerInvoicesAssembler(), // transforma domain → ListDTO
get: new GetCustomerInvoiceAssembler(), // transforma domain → DetailDTO get: new GetCustomerInvoiceAssembler(), // transforma domain → DetailDTO
create: new CreateCustomerInvoicesAssembler(), // transforma domain → CreatedDTO create: new CreateCustomerInvoicesAssembler(), // transforma domain → CreatedDTO
//update: new UpdateCustomerInvoiceAssembler(), // transforma domain -> UpdateDTO update: new UpdateCustomerInvoiceAssembler(), // transforma domain -> UpdateDTO
}; };
} }
@ -70,8 +72,8 @@ export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps {
get: () => new GetCustomerInvoiceUseCase(_service!, transactionManager!, _assemblers!.get), get: () => new GetCustomerInvoiceUseCase(_service!, transactionManager!, _assemblers!.get),
create: () => create: () =>
new CreateCustomerInvoiceUseCase(_service!, transactionManager!, _assemblers!.create), new CreateCustomerInvoiceUseCase(_service!, transactionManager!, _assemblers!.create),
/*update: () => update: () =>
new UpdateCustomerInvoiceUseCase(_service!, transactionManager!, _assemblers!.update),*/ new UpdateCustomerInvoiceUseCase(_service!, transactionManager!, _assemblers!.update),
delete: () => new DeleteCustomerInvoiceUseCase(_service!, transactionManager!), delete: () => new DeleteCustomerInvoiceUseCase(_service!, transactionManager!),
}, },
presenters: { presenters: {

View File

@ -1,9 +1,17 @@
import { ISequelizeMapper, MapperParamsType, SequelizeMapper } from "@erp/core/api"; import {
import { UniqueID, UtcDate } from "@repo/rdx-ddd"; ISequelizeMapper,
MapperParamsType,
SequelizeMapper,
ValidationErrorCollection,
ValidationErrorDetail,
extractOrPushError,
} from "@erp/core/api";
import { CurrencyCode, LanguageCode, UniqueID, UtcDate, maybeFromNullableVO } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { import {
CustomerInvoice, CustomerInvoice,
CustomerInvoiceNumber, CustomerInvoiceNumber,
CustomerInvoiceProps,
CustomerInvoiceSerie, CustomerInvoiceSerie,
CustomerInvoiceStatus, CustomerInvoiceStatus,
} from "../../domain"; } from "../../domain";
@ -32,50 +40,75 @@ export class CustomerInvoiceMapper
source: CustomerInvoiceModel, source: CustomerInvoiceModel,
params?: MapperParamsType params?: MapperParamsType
): Result<CustomerInvoice, Error> { ): Result<CustomerInvoice, Error> {
const idOrError = UniqueID.create(source.id); const errors: ValidationErrorDetail[] = [];
const statusOrError = CustomerInvoiceStatus.create(source.invoice_status);
const customerInvoiceSeriesOrError = CustomerInvoiceSerie.create(source.invoice_series);
const customerInvoiceNumberOrError = CustomerInvoiceNumber.create(source.invoice_number);
const issueDateOrError = UtcDate.createFromISO(source.issue_date);
const operationDateOrError = UtcDate.createFromISO(source.operation_date);
const result = Result.combine([ const invoiceId = extractOrPushError(UniqueID.create(source.id), "id", errors);
idOrError, const companyId = extractOrPushError(UniqueID.create(source.company_id), "company_id", errors);
statusOrError,
customerInvoiceSeriesOrError,
customerInvoiceNumberOrError,
issueDateOrError,
operationDateOrError,
]);
if (result.isFailure) { const status = extractOrPushError(
return Result.fail(result.error); CustomerInvoiceStatus.create(source.status),
"status",
errors
);
const series = extractOrPushError(CustomerInvoiceSerie.create(source.series), "series", errors);
const invoiceNumber = extractOrPushError(
CustomerInvoiceNumber.create(source.invoice_number),
"invoice_number",
errors
);
const issueDate = extractOrPushError(
UtcDate.createFromISO(source.issue_date),
"issue_date",
errors
);
const operationDate = extractOrPushError(
maybeFromNullableVO(source.operation_date, (value) => UtcDate.createFromISO(value)),
"operation_date",
errors
);
const languageCode = extractOrPushError(
LanguageCode.create(source.language_code),
"language_code",
errors
);
const currencyCode = extractOrPushError(
CurrencyCode.create(source.currency_code),
"currency_code",
errors
);
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("Customer invoice props mapping failed", errors)
);
} }
// Mapear los items de la factura // Mapear los items de la factura
const itemsOrErrors = this.customerInvoiceItemMapper.mapArrayToDomain(source.items, { /*const itemsOrErrors = this.customerInvoiceItemMapper.mapArrayToDomain(source.items, {
sourceParent: source, sourceParent: source,
...params, ...params,
}); });
if (itemsOrErrors.isFailure) { if (itemsOrErrors.isFailure) {
return Result.fail(itemsOrErrors.error); return Result.fail(itemsOrErrors.error);
} }*/
const customerInvoiceCurrency = source.invoice_currency || "EUR"; const invoiceProps: CustomerInvoiceProps = {
status: status!,
series: series!,
invoiceNumber: invoiceNumber!,
issueDate: issueDate!,
operationDate: operationDate!,
return CustomerInvoice.create( languageCode: languageCode!,
{ currencyCode: currencyCode!,
status: statusOrError.data, //items: itemsOrErrors.data,
invoiceSeries: customerInvoiceSeriesOrError.data, };
invoiceNumber: customerInvoiceNumberOrError.data,
issueDate: issueDateOrError.data, return CustomerInvoice.create(invoiceProps, invoiceId);
operationDate: operationDateOrError.data,
currency: customerInvoiceCurrency,
items: itemsOrErrors.data,
},
idOrError.data
);
} }
public mapToPersistence( public mapToPersistence(

View File

@ -1,5 +1,4 @@
import { import {
CreationOptional,
DataTypes, DataTypes,
InferAttributes, InferAttributes,
InferCreationAttributes, InferCreationAttributes,
@ -21,26 +20,26 @@ export class CustomerInvoiceItemModel extends Model<
declare item_id: string; declare item_id: string;
declare invoice_id: string; declare invoice_id: string;
declare parent_id: CreationOptional<string>; declare parent_id: string;
declare position: number; declare position: number;
declare item_type: string; declare item_type: string;
declare description: CreationOptional<string>; declare description: string;
declare quantity_amount: CreationOptional<number>; declare quantity_amount: number;
declare quantity_scale: CreationOptional<number>; declare quantity_scale: number;
declare unit_price_amount: CreationOptional<number>; declare unit_price_amount: number;
declare unit_price_scale: CreationOptional<number>; declare unit_price_scale: number;
declare subtotal_amount: CreationOptional<number>; declare subtotal_amount: number;
declare subtotal_scale: CreationOptional<number>; declare subtotal_scale: number;
declare discount_amount: CreationOptional<number>; declare discount_amount: number;
declare discount_scale: CreationOptional<number>; declare discount_scale: number;
declare total_amount: CreationOptional<number>; declare total_amount: number;
declare total_scale: CreationOptional<number>; declare total_scale: number;
declare invoice: NonAttribute<CustomerInvoiceModel>; declare invoice: NonAttribute<CustomerInvoiceModel>;
@ -65,14 +64,14 @@ export default (database: Sequelize) => {
}, },
invoice_id: { invoice_id: {
type: new DataTypes.UUID(), type: new DataTypes.UUID(),
primaryKey: true, allowNull: false,
}, },
parent_id: { parent_id: {
type: new DataTypes.UUID(), type: new DataTypes.UUID(),
allowNull: true, // Puede ser nulo para elementos de nivel superior allowNull: true, // Puede ser nulo para elementos de nivel superior
}, },
position: { position: {
type: new DataTypes.MEDIUMINT(), type: new DataTypes.MEDIUMINT().UNSIGNED,
autoIncrement: false, autoIncrement: false,
allowNull: false, allowNull: false,
}, },
@ -84,6 +83,7 @@ export default (database: Sequelize) => {
description: { description: {
type: new DataTypes.TEXT(), type: new DataTypes.TEXT(),
allowNull: true, allowNull: true,
defaultValue: null,
}, },
quantity_amount: { quantity_amount: {
@ -160,9 +160,17 @@ export default (database: Sequelize) => {
}, },
{ {
sequelize: database, sequelize: database,
underscored: true,
tableName: "customer_invoice_items", tableName: "customer_invoice_items",
underscored: true,
indexes: [
{ name: "invoice_idx", fields: ["invoice_id"], unique: false },
{ name: "parent_idx", fields: ["parent_id"], unique: false },
],
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
defaultScope: {}, defaultScope: {},
scopes: {}, scopes: {},

View File

@ -1,5 +1,4 @@
import { import {
CreationOptional,
DataTypes, DataTypes,
InferAttributes, InferAttributes,
InferCreationAttributes, InferCreationAttributes,
@ -24,22 +23,23 @@ export class CustomerInvoiceModel extends Model<
InferCreationAttributes<CustomerInvoiceModel, { omit: "items" }> InferCreationAttributes<CustomerInvoiceModel, { omit: "items" }>
> { > {
declare id: string; declare id: string;
declare company_id: string;
declare invoice_status: string; declare status: string;
declare invoice_series: CreationOptional<string>; declare series: string;
declare invoice_number: CreationOptional<string>; declare invoice_number: string;
declare issue_date: CreationOptional<string>; declare issue_date: string;
declare operation_date: CreationOptional<string>; declare operation_date: string;
declare invoice_language: string; declare language_code: string;
declare invoice_currency: string; declare currency_code: string;
// Subtotal // Subtotal
declare subtotal_amount: CreationOptional<number>; declare subtotal_amount: number;
declare subtotal_scale: CreationOptional<number>; declare subtotal_scale: number;
// Total // Total
declare total_amount: CreationOptional<number>; declare total_amount: number;
declare total_scale: CreationOptional<number>; declare total_scale: number;
// Relaciones // Relaciones
declare items: NonAttribute<CustomerInvoiceItemModel[]>; declare items: NonAttribute<CustomerInvoiceItemModel[]>;
@ -59,14 +59,14 @@ export class CustomerInvoiceModel extends Model<
static hooks(database: Sequelize) { static hooks(database: Sequelize) {
// Soft-cascade manual: al borrar una factura, marcamos items como borrados (paranoid). // Soft-cascade manual: al borrar una factura, marcamos items como borrados (paranoid).
CustomerInvoiceModel.addHook("afterDestroy", async (invoice, options) => { /*CustomerInvoiceModel.addHook("afterDestroy", async (invoice, options) => {
if (!invoice?.id) return; if (!invoice?.id) return;
await CustomerInvoiceItemModel.destroy({ await CustomerInvoiceItemModel.destroy({
where: { invoiceId: invoice.id }, where: { invoice_id: invoice.id },
individualHooks: true, individualHooks: true,
transaction: options.transaction, transaction: options.transaction,
}); });
}); });*/
} }
} }
@ -78,12 +78,18 @@ export default (database: Sequelize) => {
primaryKey: true, primaryKey: true,
}, },
invoice_status: { company_id: {
type: new DataTypes.STRING(), type: DataTypes.UUID,
allowNull: false, allowNull: false,
}, },
invoice_series: { status: {
type: new DataTypes.STRING(),
allowNull: false,
defaultValue: "draft",
},
series: {
type: new DataTypes.STRING(), type: new DataTypes.STRING(),
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,
@ -107,14 +113,16 @@ export default (database: Sequelize) => {
defaultValue: null, defaultValue: null,
}, },
invoice_language: { language_code: {
type: new DataTypes.STRING(), type: DataTypes.STRING(2),
allowNull: false, allowNull: false,
defaultValue: "es",
}, },
invoice_currency: { currency_code: {
type: new DataTypes.STRING(3), // ISO 4217 type: new DataTypes.STRING(3),
allowNull: false, allowNull: false,
defaultValue: "EUR",
}, },
subtotal_amount: { subtotal_amount: {
@ -151,7 +159,11 @@ export default (database: Sequelize) => {
updatedAt: "updated_at", updatedAt: "updated_at",
deletedAt: "deleted_at", deletedAt: "deleted_at",
indexes: [{ unique: true, fields: ["invoice_number"] }], indexes: [
{ name: "company_idx", fields: ["company_id"], unique: false },
{ name: "idx_company_idx", fields: ["id", "company_id"], unique: true },
{ unique: true, fields: ["invoice_number"] },
],
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope whereMergeStrategy: "and", // <- cómo tratar el merge de un scope

View File

@ -1,4 +1,4 @@
import { SequelizeRepository } from "@erp/core/api"; import { EntityNotFoundError, SequelizeRepository, translateSequelizeError } from "@erp/core/api";
import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server"; import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Collection, Result } from "@repo/rdx-utils"; import { Collection, Result } from "@repo/rdx-utils";
@ -11,7 +11,6 @@ export class CustomerInvoiceRepository
extends SequelizeRepository<CustomerInvoice> extends SequelizeRepository<CustomerInvoice>
implements ICustomerInvoiceRepository implements ICustomerInvoiceRepository
{ {
//private readonly model: typeof CustomerInvoiceModel;
private readonly mapper!: ICustomerInvoiceMapper; private readonly mapper!: ICustomerInvoiceMapper;
constructor(mapper: ICustomerInvoiceMapper) { constructor(mapper: ICustomerInvoiceMapper) {
@ -52,16 +51,6 @@ export class CustomerInvoiceRepository
}; };
} */ } */
async existsById(id: UniqueID, transaction?: Transaction): Promise<Result<boolean, Error>> {
try {
const result = await this._exists(CustomerInvoiceModel, "id", id.toString(), transaction);
return Result.ok(Boolean(result));
} catch (err: unknown) {
return Result.fail(translateSequelizeError(err));
}
}
/** /**
* *
* Persiste una nueva factura o actualiza una existente. * Persiste una nueva factura o actualiza una existente.
@ -76,29 +65,64 @@ export class CustomerInvoiceRepository
): Promise<Result<CustomerInvoice, Error>> { ): Promise<Result<CustomerInvoice, Error>> {
try { try {
const data = this.mapper.mapToPersistence(invoice); const data = this.mapper.mapToPersistence(invoice);
await CustomerInvoiceModel.upsert(data, { transaction }); const [instance] = await CustomerInvoiceModel.upsert(data, { transaction, returning: true });
return Result.ok(invoice); const savedInvoice = this.mapper.mapToDomain(instance);
return savedInvoice;
} catch (err: unknown) { } catch (err: unknown) {
return Result.fail(translateSequelizeError(err)); return Result.fail(translateSequelizeError(err));
} }
} }
/**
* Comprueba si existe una factura con un `id` dentro de una `company`.
*
* @param companyId - Identificador UUID de la empresa a la que pertenece la factura.
* @param id - Identificador UUID de la factura.
* @param transaction - Transacción activa para la operación.
* @returns Result<boolean, Error>
*/
async existsByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction?: Transaction
): Promise<Result<boolean, Error>> {
try {
const count = await CustomerInvoiceModel.count({
where: { id: id.toString(), company_id: companyId.toString() },
transaction,
});
return Result.ok(Boolean(count > 0));
} catch (error: any) {
return Result.fail(translateSequelizeError(error));
}
}
/** /**
* *
* Busca una factura por su identificador único. * Busca una factura por su identificador único.
*
* @param companyId - Identificador UUID de la empresa a la que pertenece la factura.
* @param id - UUID de la factura. * @param id - UUID de la factura.
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice, Error> * @returns Result<CustomerInvoice, Error>
*/ */
async findById(id: UniqueID, transaction: Transaction): Promise<Result<CustomerInvoice, Error>> { async getByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction: Transaction
): Promise<Result<CustomerInvoice, Error>> {
try { try {
const rawData = await this._findById(CustomerInvoiceModel, id.toString(), { transaction }); const row = await CustomerInvoiceModel.findOne({
where: { id: id.toString(), company_id: companyId.toString() },
transaction,
});
if (!rawData) { if (!row) {
return Result.fail(new Error(`Invoice with id ${id} not found.`)); return Result.fail(new EntityNotFoundError("CustomerInvoice", "id", id.toString()));
} }
return this.mapper.mapToDomain(rawData); const customer = this.mapper.mapToDomain(row);
return customer;
} catch (err: unknown) { } catch (err: unknown) {
return Result.fail(translateSequelizeError(err)); return Result.fail(translateSequelizeError(err));
} }
@ -107,13 +131,16 @@ export class CustomerInvoiceRepository
/** /**
* *
* Consulta facturas usando un objeto Criteria (filtros, orden, paginación). * Consulta facturas usando un objeto Criteria (filtros, orden, paginación).
*
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
* @param criteria - Criterios de búsqueda. * @param criteria - Criterios de búsqueda.
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice[], Error> * @returns Result<CustomerInvoice[], Error>
* *
* @see Criteria * @see Criteria
*/ */
public async findByCriteria( public async findByCriteriaInCompany(
companyId: UniqueID,
criteria: Criteria, criteria: Criteria,
transaction: Transaction transaction: Transaction
): Promise<Result<Collection<CustomerInvoice>, Error>> { ): Promise<Result<Collection<CustomerInvoice>, Error>> {
@ -121,6 +148,11 @@ export class CustomerInvoiceRepository
const converter = new CriteriaToSequelizeConverter(); const converter = new CriteriaToSequelizeConverter();
const query = converter.convert(criteria); const query = converter.convert(criteria);
query.where = {
...query.where,
company_id: companyId.toString(),
};
const instances = await CustomerInvoiceModel.findAll({ const instances = await CustomerInvoiceModel.findAll({
...query, ...query,
transaction, transaction,
@ -135,13 +167,23 @@ export class CustomerInvoiceRepository
/** /**
* *
* Elimina o marca como eliminada una factura. * Elimina o marca como eliminada una factura.
*
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
* @param id - UUID de la factura a eliminar. * @param id - UUID de la factura a eliminar.
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<void, Error> * @returns Result<void, Error>
*/ */
async deleteById(id: UniqueID, transaction: any): Promise<Result<void, Error>> { async deleteByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction: any
): Promise<Result<void, Error>> {
try { try {
await this._deleteById(CustomerInvoiceModel, id, false, transaction); const deleted = await CustomerInvoiceModel.destroy({
where: { id: id.toString(), company_id: companyId.toString() },
transaction,
});
return Result.ok<void>(); return Result.ok<void>();
} catch (err: unknown) { } catch (err: unknown) {
return Result.fail(translateSequelizeError(err)); return Result.fail(translateSequelizeError(err));

View File

@ -7,7 +7,6 @@ import {
PhoneNumber, PhoneNumber,
PostalAddress, PostalAddress,
PostalAddressPatchProps, PostalAddressPatchProps,
PostalAddressSnapshot,
TINNumber, TINNumber,
TaxCode, TaxCode,
TextValue, TextValue,
@ -41,32 +40,6 @@ export interface CustomerProps {
currencyCode: CurrencyCode; currencyCode: CurrencyCode;
} }
export interface CustomerSnapshot {
id: string;
companyId: string;
status: string;
reference: string | null;
isCompany: boolean;
name: string;
tradeName: string | null;
tin: string | null;
address: PostalAddressSnapshot; // snapshot serializable del VO PostalAddress
email: string | null;
phone: string | null;
fax: string | null;
website: string | null;
legalRecord: string | null;
defaultTaxes: string[];
languageCode: string;
currencyCode: string;
}
export type CustomerPatchProps = Partial<Omit<CustomerProps, "companyId" | "address">> & { export type CustomerPatchProps = Partial<Omit<CustomerProps, "companyId" | "address">> & {
address?: PostalAddressPatchProps; address?: PostalAddressPatchProps;
}; };

View File

@ -4,8 +4,7 @@ import { Collection, Result } from "@repo/rdx-utils";
import { Customer } from "../aggregates"; import { Customer } from "../aggregates";
/** /**
* Contrato del repositorio de Customers. * Interfaz del repositorio para el agregado `Customer`.
* Define la interfaz de persistencia para el agregado `Customer`.
* El escopado multitenant está representado por `companyId`. * El escopado multitenant está representado por `companyId`.
*/ */
export interface ICustomerRepository { export interface ICustomerRepository {
@ -35,7 +34,8 @@ export interface ICustomerRepository {
): Promise<Result<Customer, Error>>; ): Promise<Result<Customer, Error>>;
/** /**
* Recupera múltiples customers dentro de una empresa según un criterio dinámico (búsqueda, paginación, etc.). * Recupera múltiples customers dentro de una empresa
* según un criterio dinámico (búsqueda, paginación, etc.).
* El resultado está encapsulado en un objeto `Collection<T>`. * El resultado está encapsulado en un objeto `Collection<T>`.
*/ */
findByCriteriaInCompany( findByCriteriaInCompany(
@ -47,6 +47,11 @@ export interface ICustomerRepository {
/** /**
* Elimina un Customer por su ID, dentro de una empresa. * Elimina un Customer por su ID, dentro de una empresa.
* Retorna `void` si se elimina correctamente, o `NotFoundError` si no existía. * Retorna `void` si se elimina correctamente, o `NotFoundError` si no existía.
*
*/ */
deleteByIdInCompany(companyId: UniqueID, id: UniqueID, transaction: any): Promise<Result<void>>; deleteByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction: any
): Promise<Result<void, Error>>;
} }

View File

@ -10,12 +10,12 @@ export const customersAPIModule: IModuleServer = {
// const contacts = getService<ContactsService>("contacts"); // const contacts = getService<ContactsService>("contacts");
const { logger } = params; const { logger } = params;
customersRouter(params); customersRouter(params);
logger.info("🚀 Customers module initialized", { label: "customers" }); logger.info("🚀 Customers module initialized", { label: this.name });
}, },
async registerDependencies(params) { async registerDependencies(params) {
const { database, logger } = params; const { database, logger } = params;
logger.info("🚀 Customers module dependencies registered", { logger.info("🚀 Customers module dependencies registered", {
label: "customers", label: this.name,
}); });
return { return {
models, models,

View File

@ -1,128 +0,0 @@
import { ISequelizeMapper, MapperParamsType, SequelizeMapper } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { InferCreationAttributes } from "sequelize";
import {
Customer,
CustomerItem,
CustomerItemDescription,
CustomerItemDiscount,
CustomerItemQuantity,
CustomerItemUnitPrice,
} from "../../domain";
import { CustomerItemCreationAttributes, CustomerItemModel, CustomerModel } from "../sequelize";
export interface ICustomerItemMapper
extends ISequelizeMapper<CustomerItemModel, CustomerItemCreationAttributes, CustomerItem> {}
export class CustomerItemMapper
extends SequelizeMapper<CustomerItemModel, CustomerItemCreationAttributes, CustomerItem>
implements ICustomerItemMapper
{
public mapToDomain(
source: CustomerItemModel,
params?: MapperParamsType
): Result<CustomerItem, Error> {
const { sourceParent } = params as { sourceParent: CustomerModel };
// Validación y creación de ID único
const idOrError = UniqueID.create(source.item_id);
if (idOrError.isFailure) {
return Result.fail(idOrError.error);
}
// Validación y creación de descripción
const descriptionOrError = CustomerItemDescription.create(source.description || "");
if (descriptionOrError.isFailure) {
return Result.fail(descriptionOrError.error);
}
// Validación y creación de cantidad
const quantityOrError = CustomerItemQuantity.create({
amount: source.quantity_amount,
scale: source.quantity_scale,
});
if (quantityOrError.isFailure) {
return Result.fail(quantityOrError.error);
}
// Validación y creación de precio unitario
const unitPriceOrError = CustomerItemUnitPrice.create({
amount: source.unit_price_amount,
scale: source.unit_price_scale,
currency_code: sourceParent.invoice_currency,
});
if (unitPriceOrError.isFailure) {
return Result.fail(unitPriceOrError.error);
}
// Validación y creación de descuento
const discountOrError = CustomerItemDiscount.create({
amount: source.discount_amount || 0,
scale: source.discount_scale || 0,
});
if (discountOrError.isFailure) {
return Result.fail(discountOrError.error);
}
// Combinación de resultados
const result = Result.combine([
idOrError,
descriptionOrError,
quantityOrError,
unitPriceOrError,
discountOrError,
]);
if (result.isFailure) {
return Result.fail(result.error);
}
// Creación del objeto de dominio
return CustomerItem.create(
{
description: descriptionOrError.data,
quantity: quantityOrError.data,
unitPrice: unitPriceOrError.data,
discount: discountOrError.data,
},
idOrError.data
);
}
public mapToPersistence(
source: CustomerItem,
params?: MapperParamsType
): InferCreationAttributes<CustomerItemModel, {}> {
const { index, sourceParent } = params as {
index: number;
sourceParent: Customer;
};
const lineData = {
parent_id: undefined,
invoice_id: sourceParent.id.toPrimitive(),
item_type: "simple",
position: index,
item_id: source.id.toPrimitive(),
description: source.description.toPrimitive(),
quantity_amount: source.quantity.toPrimitive().amount,
quantity_scale: source.quantity.toPrimitive().scale,
unit_price_amount: source.unitPrice.toPrimitive().amount,
unit_price_scale: source.unitPrice.toPrimitive().scale,
subtotal_amount: source.subtotalPrice.toPrimitive().amount,
subtotal_scale: source.subtotalPrice.toPrimitive().scale,
discount_amount: source.discount.toPrimitive().amount,
discount_scale: source.discount.toPrimitive().scale,
total_amount: source.totalPrice.toPrimitive().amount,
total_scale: source.totalPrice.toPrimitive().scale,
};
return lineData;
}
}

View File

@ -171,6 +171,7 @@ export default (database: Sequelize) => {
sequelize: database, sequelize: database,
tableName: "customers", tableName: "customers",
underscored: true,
paranoid: true, // softs deletes paranoid: true, // softs deletes
timestamps: true, timestamps: true,

View File

@ -11,7 +11,6 @@ export class CustomerRepository
extends SequelizeRepository<Customer> extends SequelizeRepository<Customer>
implements ICustomerRepository implements ICustomerRepository
{ {
//private readonly model: typeof CustomerModel;
private readonly mapper!: ICustomerMapper; private readonly mapper!: ICustomerMapper;
constructor(mapper: ICustomerMapper) { constructor(mapper: ICustomerMapper) {
@ -149,7 +148,6 @@ export class CustomerRepository
return Result.ok<void>(); return Result.ok<void>();
} catch (err: unknown) { } catch (err: unknown) {
// , `Error deleting customer ${id} in company ${companyId}`
return Result.fail(translateSequelizeError(err)); return Result.fail(translateSequelizeError(err));
} }
} }