Compare commits

..

No commits in common. "db4a79422eb6af7a429b1be8b830f018045b95aa" and "0420b8e090db4c2c3981c9fe1385a56025760d66" have entirely different histories.

26 changed files with 507 additions and 473 deletions

View File

@ -1,13 +1,6 @@
import { import { AggregateRoot, UniqueID, UtcDate } from "@repo/rdx-ddd";
AggregateRoot, import { Collection, Result } from "@repo/rdx-utils";
CurrencyCode, import { CustomerInvoiceCustomer, CustomerInvoiceItem, CustomerInvoiceItems } from "../entities";
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,
@ -15,19 +8,20 @@ import {
} from "../value-objects"; } from "../value-objects";
export interface CustomerInvoiceProps { export interface CustomerInvoiceProps {
companyId: UniqueID;
status: CustomerInvoiceStatus;
series: Maybe<CustomerInvoiceSerie>;
invoiceNumber: CustomerInvoiceNumber; invoiceNumber: CustomerInvoiceNumber;
invoiceSeries: CustomerInvoiceSerie;
status: CustomerInvoiceStatus;
issueDate: UtcDate; issueDate: UtcDate;
operationDate: Maybe<UtcDate>; operationDate: 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;
@ -37,27 +31,53 @@ export interface CustomerInvoiceProps {
//paymentInstructions: Note; //paymentInstructions: Note;
//paymentTerms: string; //paymentTerms: string;
languageCode: LanguageCode; customer?: CustomerInvoiceCustomer;
currencyCode: CurrencyCode;
//customer?: CustomerInvoiceCustomer;
items?: CustomerInvoiceItems; items?: CustomerInvoiceItems;
} }
export type CustomerInvoicePatchProps = Partial<Omit<CustomerInvoiceProps, "companyId">>; export interface ICustomerInvoice {
id: UniqueID;
invoiceNumber: CustomerInvoiceNumber;
invoiceSeries: CustomerInvoiceSerie;
export class CustomerInvoice extends AggregateRoot<CustomerInvoiceProps> { status: CustomerInvoiceStatus;
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 = this._items = props.items || CustomerInvoiceItems.create();
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> {
@ -74,55 +94,50 @@ export class CustomerInvoice extends AggregateRoot<CustomerInvoiceProps> {
return Result.ok(customerInvoice); return Result.ok(customerInvoice);
} }
public update(partialInvoice: CustomerInvoicePatchProps): Result<CustomerInvoice, Error> { get invoiceNumber() {
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;
} }
public get issueDate(): UtcDate { get invoiceSeries() {
return this.props.invoiceSeries;
}
get issueDate() {
return this.props.issueDate; return this.props.issueDate;
} }
public get operationDate(): Maybe<UtcDate> {
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;
} }
@ -143,7 +158,19 @@ export class CustomerInvoice extends AggregateRoot<CustomerInvoiceProps> {
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);
@ -152,7 +179,7 @@ export class CustomerInvoice extends AggregateRoot<CustomerInvoiceProps> {
} }
}*/ }*/
/*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,
@ -162,10 +189,10 @@ export class CustomerInvoice extends AggregateRoot<CustomerInvoiceProps> {
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,
@ -173,10 +200,10 @@ export class CustomerInvoice extends AggregateRoot<CustomerInvoiceProps> {
}).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,102 +0,0 @@
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

@ -1,25 +0,0 @@
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 "./customer-invoice-item"; export * from "./invoice-item";
export * from "./customer-invoice-items"; export * from "./invoice-items";

View File

@ -1,4 +1,4 @@
import { CurrencyCode, LanguageCode, MoneyValue, Percentage, Quantity } from "@repo/rdx-ddd"; import { MoneyValue, Percentage, Quantity } from "@/core/common/domain";
import { CustomerInvoiceItemDescription } from "../../value-objects"; import { CustomerInvoiceItemDescription } from "../../value-objects";
import { CustomerInvoiceItem } from "./customer-invoice-item"; import { CustomerInvoiceItem } from "./customer-invoice-item";
@ -9,8 +9,6 @@ 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,107 @@
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

@ -0,0 +1,8 @@
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,15 +3,12 @@ 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.
@ -20,55 +17,34 @@ 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>
*/ */
existsByIdInCompany( findById(id: UniqueID, transaction: any): Promise<Result<CustomerInvoice, Error>>;
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 dentro de una empresa usando un * Consulta facturas usando un objeto Criteria (filtros, orden, paginación).
* 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
*/ */
findByCriteriaInCompany( findByCriteria(
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 dentro de una empresa. * Elimina o marca como eliminada una factura.
*
* @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>
*/ */
deleteByIdInCompany( deleteById(id: UniqueID, transaction: any): Promise<Result<void, Error>>;
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 (!CustomerInvoiceAddressType.ALLOWED_TYPES.includes(value)) { if (!this.ALLOWED_TYPES.includes(value)) {
return Result.fail( return Result.fail(
new Error( new Error(
`Invalid address type: ${value}. Allowed types are: ${CustomerInvoiceAddressType.ALLOWED_TYPES.join(", ")}` `Invalid address type: ${value}. Allowed types are: ${this.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 CustomerInvoiceItemDescriptionProps { interface ICustomerInvoiceItemDescriptionProps {
value: string; value: string;
} }
export class CustomerInvoiceItemDescription extends ValueObject<CustomerInvoiceItemDescriptionProps> { export class CustomerInvoiceItemDescription extends ValueObject<ICustomerInvoiceItemDescriptionProps> {
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<CustomerInvoiceI
} }
static create(value: string) { static create(value: string) {
const valueIsValid = CustomerInvoiceItemDescription.validate(value); const result = CustomerInvoiceItemDescription.validate(value);
if (!valueIsValid.success) { if (!result.success) {
const detail = valueIsValid.error.message; const detail = result.error.message;
return Result.fail( return Result.fail(
new DomainValidationError( new DomainValidationError(
CustomerInvoiceItemDescription.ERROR_CODE, CustomerInvoiceItemDescription.ERROR_CODE,

View File

@ -11,13 +11,4 @@ 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 valueIsValid = CustomerInvoiceNumber.validate(value); const result = CustomerInvoiceNumber.validate(value);
if (!valueIsValid.success) { if (!result.success) {
const detail = valueIsValid.error.message; const detail = result.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 valueIsValid = CustomerInvoiceSerie.validate(value); const result = CustomerInvoiceSerie.validate(value);
if (!valueIsValid.success) { if (!result.success) {
const detail = valueIsValid.error.message; const detail = result.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: ["customers"], dependencies: [],
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: this.name }); logger.info("🚀 CustomerInvoices module initialized", { label: "customer-invoices" });
}, },
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: this.name, label: "customer-invoices",
}); });
return { return {
models, models,

View File

@ -8,8 +8,6 @@ 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";
@ -24,13 +22,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: {
@ -56,7 +54,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
}; };
} }
@ -72,8 +70,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,17 +1,9 @@
import { import { ISequelizeMapper, MapperParamsType, SequelizeMapper } from "@erp/core/api";
ISequelizeMapper, import { UniqueID, UtcDate } from "@repo/rdx-ddd";
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";
@ -40,75 +32,50 @@ export class CustomerInvoiceMapper
source: CustomerInvoiceModel, source: CustomerInvoiceModel,
params?: MapperParamsType params?: MapperParamsType
): Result<CustomerInvoice, Error> { ): Result<CustomerInvoice, Error> {
const errors: ValidationErrorDetail[] = []; const idOrError = UniqueID.create(source.id);
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 invoiceId = extractOrPushError(UniqueID.create(source.id), "id", errors); const result = Result.combine([
const companyId = extractOrPushError(UniqueID.create(source.company_id), "company_id", errors); idOrError,
statusOrError,
customerInvoiceSeriesOrError,
customerInvoiceNumberOrError,
issueDateOrError,
operationDateOrError,
]);
const status = extractOrPushError( if (result.isFailure) {
CustomerInvoiceStatus.create(source.status), return Result.fail(result.error);
"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 invoiceProps: CustomerInvoiceProps = { const customerInvoiceCurrency = source.invoice_currency || "EUR";
status: status!,
series: series!,
invoiceNumber: invoiceNumber!,
issueDate: issueDate!,
operationDate: operationDate!,
languageCode: languageCode!, return CustomerInvoice.create(
currencyCode: currencyCode!, {
//items: itemsOrErrors.data, status: statusOrError.data,
}; invoiceSeries: customerInvoiceSeriesOrError.data,
invoiceNumber: customerInvoiceNumberOrError.data,
return CustomerInvoice.create(invoiceProps, invoiceId); issueDate: issueDateOrError.data,
operationDate: operationDateOrError.data,
currency: customerInvoiceCurrency,
items: itemsOrErrors.data,
},
idOrError.data
);
} }
public mapToPersistence( public mapToPersistence(

View File

@ -1,4 +1,5 @@
import { import {
CreationOptional,
DataTypes, DataTypes,
InferAttributes, InferAttributes,
InferCreationAttributes, InferCreationAttributes,
@ -20,26 +21,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: string; declare parent_id: CreationOptional<string>;
declare position: number; declare position: number;
declare item_type: string; declare item_type: string;
declare description: string; declare description: CreationOptional<string>;
declare quantity_amount: number; declare quantity_amount: CreationOptional<number>;
declare quantity_scale: number; declare quantity_scale: CreationOptional<number>;
declare unit_price_amount: number; declare unit_price_amount: CreationOptional<number>;
declare unit_price_scale: number; declare unit_price_scale: CreationOptional<number>;
declare subtotal_amount: number; declare subtotal_amount: CreationOptional<number>;
declare subtotal_scale: number; declare subtotal_scale: CreationOptional<number>;
declare discount_amount: number; declare discount_amount: CreationOptional<number>;
declare discount_scale: number; declare discount_scale: CreationOptional<number>;
declare total_amount: number; declare total_amount: CreationOptional<number>;
declare total_scale: number; declare total_scale: CreationOptional<number>;
declare invoice: NonAttribute<CustomerInvoiceModel>; declare invoice: NonAttribute<CustomerInvoiceModel>;
@ -64,14 +65,14 @@ export default (database: Sequelize) => {
}, },
invoice_id: { invoice_id: {
type: new DataTypes.UUID(), type: new DataTypes.UUID(),
allowNull: false, primaryKey: true,
}, },
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().UNSIGNED, type: new DataTypes.MEDIUMINT(),
autoIncrement: false, autoIncrement: false,
allowNull: false, allowNull: false,
}, },
@ -83,7 +84,6 @@ 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,16 +160,8 @@ export default (database: Sequelize) => {
}, },
{ {
sequelize: database, sequelize: database,
tableName: "customer_invoice_items",
underscored: true, underscored: true,
tableName: "customer_invoice_items",
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: {},

View File

@ -1,4 +1,5 @@
import { import {
CreationOptional,
DataTypes, DataTypes,
InferAttributes, InferAttributes,
InferCreationAttributes, InferCreationAttributes,
@ -23,23 +24,22 @@ export class CustomerInvoiceModel extends Model<
InferCreationAttributes<CustomerInvoiceModel, { omit: "items" }> InferCreationAttributes<CustomerInvoiceModel, { omit: "items" }>
> { > {
declare id: string; declare id: string;
declare company_id: string;
declare status: string; declare invoice_status: string;
declare series: string; declare invoice_series: CreationOptional<string>;
declare invoice_number: string; declare invoice_number: CreationOptional<string>;
declare issue_date: string; declare issue_date: CreationOptional<string>;
declare operation_date: string; declare operation_date: CreationOptional<string>;
declare language_code: string; declare invoice_language: string;
declare currency_code: string; declare invoice_currency: string;
// Subtotal // Subtotal
declare subtotal_amount: number; declare subtotal_amount: CreationOptional<number>;
declare subtotal_scale: number; declare subtotal_scale: CreationOptional<number>;
// Total // Total
declare total_amount: number; declare total_amount: CreationOptional<number>;
declare total_scale: number; declare total_scale: CreationOptional<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: { invoice_id: invoice.id }, where: { invoiceId: invoice.id },
individualHooks: true, individualHooks: true,
transaction: options.transaction, transaction: options.transaction,
}); });
});*/ });
} }
} }
@ -78,18 +78,12 @@ export default (database: Sequelize) => {
primaryKey: true, primaryKey: true,
}, },
company_id: { invoice_status: {
type: DataTypes.UUID,
allowNull: false,
},
status: {
type: new DataTypes.STRING(), type: new DataTypes.STRING(),
allowNull: false, allowNull: false,
defaultValue: "draft",
}, },
series: { invoice_series: {
type: new DataTypes.STRING(), type: new DataTypes.STRING(),
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,
@ -113,16 +107,14 @@ export default (database: Sequelize) => {
defaultValue: null, defaultValue: null,
}, },
language_code: { invoice_language: {
type: DataTypes.STRING(2), type: new DataTypes.STRING(),
allowNull: false, allowNull: false,
defaultValue: "es",
}, },
currency_code: { invoice_currency: {
type: new DataTypes.STRING(3), type: new DataTypes.STRING(3), // ISO 4217
allowNull: false, allowNull: false,
defaultValue: "EUR",
}, },
subtotal_amount: { subtotal_amount: {
@ -159,11 +151,7 @@ export default (database: Sequelize) => {
updatedAt: "updated_at", updatedAt: "updated_at",
deletedAt: "deleted_at", deletedAt: "deleted_at",
indexes: [ indexes: [{ unique: true, fields: ["invoice_number"] }],
{ 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 { EntityNotFoundError, SequelizeRepository, translateSequelizeError } from "@erp/core/api"; import { SequelizeRepository } 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,6 +11,7 @@ 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) {
@ -51,6 +52,16 @@ 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.
@ -65,64 +76,29 @@ 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);
const [instance] = await CustomerInvoiceModel.upsert(data, { transaction, returning: true }); await CustomerInvoiceModel.upsert(data, { transaction });
const savedInvoice = this.mapper.mapToDomain(instance); return Result.ok(invoice);
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 getByIdInCompany( async findById(id: UniqueID, transaction: Transaction): Promise<Result<CustomerInvoice, Error>> {
companyId: UniqueID,
id: UniqueID,
transaction: Transaction
): Promise<Result<CustomerInvoice, Error>> {
try { try {
const row = await CustomerInvoiceModel.findOne({ const rawData = await this._findById(CustomerInvoiceModel, id.toString(), { transaction });
where: { id: id.toString(), company_id: companyId.toString() },
transaction,
});
if (!row) { if (!rawData) {
return Result.fail(new EntityNotFoundError("CustomerInvoice", "id", id.toString())); return Result.fail(new Error(`Invoice with id ${id} not found.`));
} }
const customer = this.mapper.mapToDomain(row); return this.mapper.mapToDomain(rawData);
return customer;
} catch (err: unknown) { } catch (err: unknown) {
return Result.fail(translateSequelizeError(err)); return Result.fail(translateSequelizeError(err));
} }
@ -131,16 +107,13 @@ 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 findByCriteriaInCompany( public async findByCriteria(
companyId: UniqueID,
criteria: Criteria, criteria: Criteria,
transaction: Transaction transaction: Transaction
): Promise<Result<Collection<CustomerInvoice>, Error>> { ): Promise<Result<Collection<CustomerInvoice>, Error>> {
@ -148,11 +121,6 @@ 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,
@ -167,23 +135,13 @@ 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 deleteByIdInCompany( async deleteById(id: UniqueID, transaction: any): Promise<Result<void, Error>> {
companyId: UniqueID,
id: UniqueID,
transaction: any
): Promise<Result<void, Error>> {
try { try {
const deleted = await CustomerInvoiceModel.destroy({ await this._deleteById(CustomerInvoiceModel, id, false, transaction);
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,6 +7,7 @@ import {
PhoneNumber, PhoneNumber,
PostalAddress, PostalAddress,
PostalAddressPatchProps, PostalAddressPatchProps,
PostalAddressSnapshot,
TINNumber, TINNumber,
TaxCode, TaxCode,
TextValue, TextValue,
@ -40,6 +41,32 @@ 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,7 +4,8 @@ import { Collection, Result } from "@repo/rdx-utils";
import { Customer } from "../aggregates"; import { Customer } from "../aggregates";
/** /**
* Interfaz del repositorio para el agregado `Customer`. * Contrato del repositorio de Customers.
* 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 {
@ -34,8 +35,7 @@ export interface ICustomerRepository {
): Promise<Result<Customer, Error>>; ): Promise<Result<Customer, Error>>;
/** /**
* Recupera múltiples customers dentro de una empresa * Recupera múltiples customers dentro de una empresa según un criterio dinámico (búsqueda, paginación, etc.).
* 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,11 +47,6 @@ 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( deleteByIdInCompany(companyId: UniqueID, id: UniqueID, transaction: any): Promise<Result<void>>;
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: this.name }); logger.info("🚀 Customers module initialized", { label: "customers" });
}, },
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: this.name, label: "customers",
}); });
return { return {
models, models,

View File

@ -17,7 +17,7 @@ export class UpdateCustomerController extends ExpressController {
const result = await this.useCase.execute({ customer_id, companyId, dto }); const result = await this.useCase.execute({ customer_id, companyId, dto });
return result.match( return result.match(
(data) => this.ok(data), (data) => this.created(data),
(err) => this.handleError(err) (err) => this.handleError(err)
); );
} }

View File

@ -0,0 +1,128 @@
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,7 +171,6 @@ 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,6 +11,7 @@ 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) {
@ -148,6 +149,7 @@ 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));
} }
} }