Facturas de cliente

This commit is contained in:
David Arranz 2025-09-03 20:04:09 +02:00
parent db4a79422e
commit 5064494b12
35 changed files with 444 additions and 219 deletions

View File

@ -0,0 +1,23 @@
import * as z from "zod/v4";
import { AmountBaseSchema } from "./base.schemas";
/**
Esquema del DTO para valores monetarios con/sin código de moneda.
No aplica defaults ni correciones: solo valida.
- Con moneda -> "Money"
- Sin moneda -> "Amount"
*/
// 🔹 Con moneda
export const MoneySchema = AmountBaseSchema.extend({
currency_code: z.string(),
});
// 🔹 Sin moneda
export const AmountSchema = AmountBaseSchema;
// 🔹 Tipos DTO
export type MoneyDTO = z.infer<typeof MoneySchema>;
export type AmountDTO = z.infer<typeof AmountSchema>;

View File

@ -0,0 +1,19 @@
import * as z from "zod/v4";
/**
* Cadena con valor numérico:
*
* - Acepta: "" o "123456" (solo dígitos).
* - Rechaza: "1 23", "abc123", "12v34", "+123", "12.3"
*
* */
export const NumericStringSchema = z
.string()
.regex(/^\d$/, { message: "Must be empty or contain only digits (0-9)." });
// Cantidad de dinero (base): solo para la cantidad y la escala, sin moneda
export const AmountBaseSchema = z.object({
amount: NumericStringSchema,
scale: NumericStringSchema,
});

View File

@ -1,8 +1,8 @@
import * as z from "zod/v4"; import * as z from "zod/v4";
/** /**
Esquema del objeto normalizado esperado por Criteria.fromPrimitives(...) Esquema del DTO para Criteria.fromPrimitives(...)
No aplica defaults ni correciones: solo valida. No aplica defaults ni correciones: solo valida.
*/ */
export const FilterPrimitiveSchema = z.object({ export const FilterPrimitiveSchema = z.object({
// Campos mínimos ya normalizados por el conversor // Campos mínimos ya normalizados por el conversor

View File

@ -1,8 +1,9 @@
export * from "./amount-money.dto";
export * from "./base.schemas";
export * from "./critera.dto"; export * from "./critera.dto";
export * from "./error.dto"; export * from "./error.dto";
export * from "./list-view.response.dto"; export * from "./list-view.response.dto";
export * from "./metadata.dto"; export * from "./metadata.dto";
export * from "./money.dto";
export * from "./percentage.dto"; export * from "./percentage.dto";
export * from "./quantity.dto"; export * from "./quantity.dto";
export * from "./tax-type.dto"; export * from "./tax-type.dto";

View File

@ -1,5 +0,0 @@
export type MoneyDTO = {
amount: number | null;
scale: number;
currency_code: string;
};

View File

@ -1,4 +1,14 @@
export type IPercentageDTO = { import * as z from "zod/v4";
amount: number | null; import { NumericStringSchema } from "./base.schemas";
scale: number;
}; /**
Esquema del DTO para valores de porcentajes.
No aplica defaults ni correciones: solo valida.
*/
export const PercentageSchema = z.object({
amount: NumericStringSchema,
scale: NumericStringSchema,
});
export type PercentageDTO = z.infer<typeof PercentageSchema>;

View File

@ -1,4 +1,14 @@
export type IQuantityDTO = { import * as z from "zod/v4";
amount: number | null; import { NumericStringSchema } from "./base.schemas";
scale: number;
}; /**
Esquema del DTO para valores de cantidades.
No aplica defaults ni correciones: solo valida.
*/
export const QuantitySchema = z.object({
amount: NumericStringSchema,
scale: NumericStringSchema,
});
export type QuantityDTO = z.infer<typeof QuantitySchema>;

View File

@ -1,11 +1,11 @@
import { IPercentageDTO } from "./percentage.dto"; import { PercentageDTO } from "./percentage.dto";
export interface ITaxTypeDTO { export interface ITaxTypeDTO {
id: string; id: string;
typecode: string; typecode: string;
taxslug: string; taxslug: string;
taxrate: IPercentageDTO; taxrate: PercentageDTO;
equivalencesurcharge: IPercentageDTO; equivalencesurcharge: PercentageDTO;
} }
export interface ITaxTypeResponseDTO extends ITaxTypeDTO {} export interface ITaxTypeResponseDTO extends ITaxTypeDTO {}

View File

@ -0,0 +1,48 @@
import { GetCustomerInvoiceByIdResponseDTO } from "@erp/customer-invoices/common/dto";
import { toEmptyString } from "@repo/rdx-ddd";
import { CustomerInvoice } from "../../../domain";
type GetCustomerInvoiceItemsByInvoiceIdResponseDTO = GetCustomerInvoiceByIdResponseDTO["items"];
export class GetCustomerInvoiceItemsAssembler {
toDTO(invoice: CustomerInvoice): GetCustomerInvoiceItemsByInvoiceIdResponseDTO {
const { items } = invoice;
return items.map((item, index) => ({
//id: item.
position: index,
description: toEmptyString(item.description, (value) => value.toPrimitive()),
quantity: item.quantity.match(
(quantity) => {
const { amount, scale } = quantity.toPrimitive();
return { amount: amount.toString(), scale: scale.toString() };
},
() => ({ amount: "", scale: "" })
),
unit_price_amount: item.unitAmount.match(
(unitPrice) => {
const { amount, scale } = unitPrice.toPrimitive();
return { amount: amount.toString(), scale: scale.toString() };
},
() => ({ amount: "", scale: "" })
),
discount: item.discount.match(
(discount) => {
const { amount, scale } = discount.toPrimitive();
return { amount: amount.toString(), scale: scale.toString() };
},
() => ({ amount: "", scale: "" })
),
total_amount: item.totalPrice.match(
(discount) => {
const { amount, scale } = discount.toPrimitive();
return { amount: amount.toString(), scale: scale.toString() };
},
() => ({ amount: "", scale: "" })
),
}));
}
}

View File

@ -1,18 +1,29 @@
import { GetCustomerInvoiceByIdResponseDTO } from "@erp/customer-invoices/common/dto"; import { GetCustomerInvoiceByIdResponseDTO } from "@erp/customer-invoices/common/dto";
import { toEmptyString } from "@repo/rdx-ddd";
import { CustomerInvoice } from "../../../domain"; import { CustomerInvoice } from "../../../domain";
export class GetCustomerInvoiceAssembler { export class GetCustomerInvoiceAssembler {
toDTO(customerInvoice: CustomerInvoice): GetCustomerInvoiceByIdResponseDTO { private _itemsAssembler!: GetCu;
return { constructor() {}
id: customerInvoice.id.toPrimitive(),
invoice_status: customerInvoice.status.toString(), public toDTO(invoice: CustomerInvoice): GetCustomerInvoiceByIdResponseDTO {
invoice_number: customerInvoice.invoiceNumber.toString(), //const items = invoice.items.
invoice_series: customerInvoice.invoiceSeries.toString(),
issue_date: customerInvoice.issueDate.toDateString(), return {
operation_date: customerInvoice.operationDate.toDateString(), id: invoice.id.toPrimitive(),
language_code: "ES", company_id: invoice.companyId.toPrimitive(),
currency: customerInvoice.currency,
invoice_number: invoice.invoiceNumber.toString(),
status: invoice.status.toPrimitive(),
series: invoice.series.toString(),
issue_date: invoice.issueDate.toDateString(),
operation_date: toEmptyString(invoice.operationDate, (value) => value.toDateString()),
notes: toEmptyString(invoice.notes, (value) => value.toPrimitive()),
language_code: invoice.languageCode.toPrimitive(),
currency_code: invoice.currencyCode.toPrimitive(),
metadata: { metadata: {
entity: "customer-invoices", entity: "customer-invoices",

View File

@ -1,32 +1,39 @@
import { ITransactionManager } from "@erp/core/api"; import { ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { ICustomerInvoiceService } from "../../domain"; import { CustomerInvoiceService } from "../../domain";
import { GetCustomerInvoiceAssembler } from "./assembler"; import { GetCustomerInvoiceItemsAssembler } from "./assembler";
type GetCustomerInvoiceUseCaseInput = { type GetCustomerInvoiceUseCaseInput = {
tenantId: string; companyId: UniqueID;
id: string; invoice_id: string;
}; };
export class GetCustomerInvoiceUseCase { export class GetCustomerInvoiceUseCase {
constructor( constructor(
private readonly service: ICustomerInvoiceService, private readonly service: CustomerInvoiceService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
private readonly assembler: GetCustomerInvoiceAssembler private readonly assembler: GetCustomerInvoiceItemsAssembler
) {} ) {}
public execute(params: GetCustomerInvoiceUseCaseInput) { public execute(params: GetCustomerInvoiceUseCaseInput) {
const { id, tenantId } = params; const { invoice_id, companyId } = params;
const idOrError = UniqueID.create(id);
const idOrError = UniqueID.create(invoice_id);
if (idOrError.isFailure) { if (idOrError.isFailure) {
return Result.fail(idOrError.error); return Result.fail(idOrError.error);
} }
const invoiceId = idOrError.data;
return this.transactionManager.complete(async (transaction) => { return this.transactionManager.complete(async (transaction) => {
try { try {
const invoiceOrError = await this.service.getById(idOrError.data, transaction); const invoiceOrError = await this.service.getInvoiceByIdInCompany(
companyId,
invoiceId,
transaction
);
if (invoiceOrError.isFailure) { if (invoiceOrError.isFailure) {
return Result.fail(invoiceOrError.error); return Result.fail(invoiceOrError.error);
} }

View File

@ -6,7 +6,7 @@ import {
CustomerInvoiceItemDescription, CustomerInvoiceItemDescription,
CustomerInvoiceItemDiscount, CustomerInvoiceItemDiscount,
CustomerInvoiceItemQuantity, CustomerInvoiceItemQuantity,
CustomerInvoiceItemUnitPrice, CustomerInvoiceItemUnitAmount,
} from "../../domain"; } from "../../domain";
import { extractOrPushError } from "./extract-or-push-error"; import { extractOrPushError } from "./extract-or-push-error";
import { hasNoUndefinedFields } from "./has-no-undefined-fields"; import { hasNoUndefinedFields } from "./has-no-undefined-fields";
@ -36,7 +36,7 @@ export function mapDTOToCustomerInvoiceItemsProps(
); );
const unitPrice = extractOrPushError( const unitPrice = extractOrPushError(
CustomerInvoiceItemUnitPrice.create({ CustomerInvoiceItemUnitAmount.create({
amount: item.unitPrice.amount, amount: item.unitPrice.amount,
scale: item.unitPrice.scale, scale: item.unitPrice.scale,
currency_code: item.unitPrice.currency, currency_code: item.unitPrice.currency,

View File

@ -2,6 +2,8 @@ import {
AggregateRoot, AggregateRoot,
CurrencyCode, CurrencyCode,
LanguageCode, LanguageCode,
MoneyValue,
Percentage,
TextValue, TextValue,
UniqueID, UniqueID,
UtcDate, UtcDate,
@ -16,9 +18,10 @@ import {
export interface CustomerInvoiceProps { export interface CustomerInvoiceProps {
companyId: UniqueID; companyId: UniqueID;
invoiceNumber: CustomerInvoiceNumber;
status: CustomerInvoiceStatus; status: CustomerInvoiceStatus;
series: Maybe<CustomerInvoiceSerie>; series: Maybe<CustomerInvoiceSerie>;
invoiceNumber: CustomerInvoiceNumber;
issueDate: UtcDate; issueDate: UtcDate;
operationDate: Maybe<UtcDate>; operationDate: Maybe<UtcDate>;
@ -40,6 +43,16 @@ export interface CustomerInvoiceProps {
languageCode: LanguageCode; languageCode: LanguageCode;
currencyCode: CurrencyCode; currencyCode: CurrencyCode;
subtotalAmount: MoneyValue;
discountPercentage: Percentage;
//discountAmount: MoneyValue;
//taxableAmount: MoneyValue;
taxAmount: MoneyValue;
totalAmount: MoneyValue;
//customer?: CustomerInvoiceCustomer; //customer?: CustomerInvoiceCustomer;
items?: CustomerInvoiceItems; items?: CustomerInvoiceItems;
} }
@ -82,6 +95,10 @@ export class CustomerInvoice extends AggregateRoot<CustomerInvoiceProps> {
return this.props.companyId; return this.props.companyId;
} }
public get status(): CustomerInvoiceStatus {
return this.props.status;
}
public get series(): Maybe<CustomerInvoiceSerie> { public get series(): Maybe<CustomerInvoiceSerie> {
return this.props.series; return this.props.series;
} }
@ -110,8 +127,32 @@ export class CustomerInvoice extends AggregateRoot<CustomerInvoiceProps> {
return this.props.currencyCode; return this.props.currencyCode;
} }
public get subtotalAmount(): MoneyValue {
return this.props.subtotalAmount;
}
public get discountPercentage(): Percentage {
return this.props.discountPercentage;
}
public get discountAmount(): MoneyValue {
throw new Error("discountAmount not implemented");
}
public get taxableAmount(): MoneyValue {
throw new Error("taxableAmount not implemented");
}
public get taxAmount(): MoneyValue {
return this.props.taxAmount;
}
public get totalAmount(): MoneyValue {
throw new Error("totalAmount not implemented");
}
// Method to get the complete list of line items // Method to get the complete list of line items
get lineItems(): CustomerInvoiceItems { get items(): CustomerInvoiceItems {
return this._items; return this._items;
} }

View File

@ -4,15 +4,15 @@ import {
CustomerInvoiceItemDescription, CustomerInvoiceItemDescription,
CustomerInvoiceItemDiscount, CustomerInvoiceItemDiscount,
CustomerInvoiceItemQuantity, CustomerInvoiceItemQuantity,
CustomerInvoiceItemSubtotalPrice, CustomerInvoiceItemSubtotalAmount,
CustomerInvoiceItemTotalPrice, CustomerInvoiceItemTotalAmount,
CustomerInvoiceItemUnitPrice, CustomerInvoiceItemUnitAmount,
} from "../../value-objects"; } from "../../value-objects";
export interface CustomerInvoiceItemProps { export interface CustomerInvoiceItemProps {
description: Maybe<CustomerInvoiceItemDescription>; description: Maybe<CustomerInvoiceItemDescription>;
quantity: Maybe<CustomerInvoiceItemQuantity>; // Cantidad de unidades quantity: Maybe<CustomerInvoiceItemQuantity>; // Cantidad de unidades
unitPrice: Maybe<CustomerInvoiceItemUnitPrice>; // Precio unitario en la moneda de la factura unitPrice: Maybe<CustomerInvoiceItemUnitAmount>; // Precio unitario en la moneda de la factura
discount: Maybe<CustomerInvoiceItemDiscount>; // % descuento discount: Maybe<CustomerInvoiceItemDiscount>; // % descuento
languageCode: LanguageCode; languageCode: LanguageCode;
@ -20,8 +20,8 @@ export interface CustomerInvoiceItemProps {
} }
export class CustomerInvoiceItem extends DomainEntity<CustomerInvoiceItemProps> { export class CustomerInvoiceItem extends DomainEntity<CustomerInvoiceItemProps> {
private _subtotalPrice!: CustomerInvoiceItemSubtotalPrice; private _subtotalAmount!: CustomerInvoiceItemSubtotalAmount;
private _totalPrice!: CustomerInvoiceItemTotalPrice; private _totalAmount!: CustomerInvoiceItemTotalAmount;
public static create( public static create(
props: CustomerInvoiceItemProps, props: CustomerInvoiceItemProps,
@ -48,26 +48,26 @@ export class CustomerInvoiceItem extends DomainEntity<CustomerInvoiceItemProps>
return this.props.quantity; return this.props.quantity;
} }
get unitPrice(): Maybe<CustomerInvoiceItemUnitPrice> { get unitAmount(): Maybe<CustomerInvoiceItemUnitAmount> {
return this.props.unitPrice; return this.props.unitPrice;
} }
get subtotalPrice(): CustomerInvoiceItemSubtotalPrice { get subtotalAmount(): CustomerInvoiceItemSubtotalAmount {
if (!this._subtotalPrice) { if (!this._subtotalAmount) {
this._subtotalPrice = this.calculateSubtotal(); this._subtotalAmount = this.calculateSubtotal();
} }
return this._subtotalPrice; return this._subtotalAmount;
} }
get discount(): Maybe<CustomerInvoiceItemDiscount> { get discount(): Maybe<CustomerInvoiceItemDiscount> {
return this.props.discount; return this.props.discount;
} }
get totalPrice(): CustomerInvoiceItemTotalPrice { get totalPrice(): CustomerInvoiceItemTotalAmount {
if (!this._totalPrice) { if (!this._totalAmount) {
this._totalPrice = this.calculateTotal(); this._totalAmount = this.calculateTotal();
} }
return this._totalPrice; return this._totalAmount;
} }
public get languageCode(): LanguageCode { public get languageCode(): LanguageCode {
@ -86,7 +86,7 @@ export class CustomerInvoiceItem extends DomainEntity<CustomerInvoiceItemProps>
return this.getValue(); return this.getValue();
} }
calculateSubtotal(): CustomerInvoiceItemSubtotalPrice { calculateSubtotal(): CustomerInvoiceItemSubtotalAmount {
throw new Error("Not implemented"); throw new Error("Not implemented");
/*const unitPrice = this.unitPrice.isSome() /*const unitPrice = this.unitPrice.isSome()
@ -95,7 +95,7 @@ export class CustomerInvoiceItem extends DomainEntity<CustomerInvoiceItemProps>
return this.unitPrice.multiply(this.quantity.toNumber()); // Precio unitario * Cantidad*/ return this.unitPrice.multiply(this.quantity.toNumber()); // Precio unitario * Cantidad*/
} }
calculateTotal(): CustomerInvoiceItemTotalPrice { calculateTotal(): CustomerInvoiceItemTotalAmount {
throw new Error("Not implemented"); throw new Error("Not implemented");
//return this.subtotalPrice.subtract(this.subtotalPrice.percentage(this.discount.toNumber())); //return this.subtotalPrice.subtract(this.subtotalPrice.percentage(this.discount.toNumber()));
} }

View File

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

View File

@ -1,33 +0,0 @@
import { Criteria } from "@repo/rdx-criteria/server";
import { UniqueID } from "@repo/rdx-ddd";
import { Collection, Result } from "@repo/rdx-utils";
import { CustomerInvoice, CustomerInvoiceProps } from "../aggregates";
export interface ICustomerInvoiceService {
build(props: CustomerInvoiceProps, id?: UniqueID): Result<CustomerInvoice, Error>;
save(invoice: CustomerInvoice, transaction: any): Promise<Result<CustomerInvoice, Error>>;
existsById(id: UniqueID, transaction?: any): Promise<Result<boolean, Error>>;
findByCriteria(
criteria: Criteria,
transaction?: any
): Promise<Result<Collection<CustomerInvoice>, Error>>;
getById(id: UniqueID, transaction?: any): Promise<Result<CustomerInvoice>>;
updateById(
id: UniqueID,
data: Partial<CustomerInvoiceProps>,
transaction?: any
): Promise<Result<CustomerInvoice, Error>>;
createCustomerInvoice(
id: UniqueID,
data: CustomerInvoiceProps,
transaction?: any
): Promise<Result<CustomerInvoice, Error>>;
deleteById(id: UniqueID, transaction?: any): Promise<Result<void, Error>>;
}

View File

@ -2,22 +2,26 @@ import { Criteria } 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";
import { Transaction } from "sequelize"; import { Transaction } from "sequelize";
import { CustomerInvoice, CustomerInvoiceProps } from "../aggregates"; import { CustomerInvoice, CustomerInvoicePatchProps, CustomerInvoiceProps } from "../aggregates";
import { ICustomerInvoiceRepository } from "../repositories"; import { ICustomerInvoiceRepository } from "../repositories";
import { ICustomerInvoiceService } from "./customer-invoice-service.interface";
export class CustomerInvoiceService implements ICustomerInvoiceService { export class CustomerInvoiceService {
constructor(private readonly repository: ICustomerInvoiceRepository) {} constructor(private readonly repository: ICustomerInvoiceRepository) {}
/** /**
* Construye un nuevo agregado CustomerInvoice a partir de props validadas. * Construye un nuevo agregado CustomerInvoice a partir de props validadas.
* *
* @param companyId - Identificador de la empresa a la que pertenece el cliente.
* @param props - Las propiedades ya validadas para crear la factura. * @param props - Las propiedades ya validadas para crear la factura.
* @param id - Identificador UUID de la factura (opcional). * @param invoiceId - Identificador UUID de la factura (opcional).
* @returns Result<CustomerInvoice, Error> - El agregado construido o un error si falla la creación. * @returns Result<CustomerInvoice, Error> - El agregado construido o un error si falla la creación.
*/ */
build(props: CustomerInvoiceProps, id?: UniqueID): Result<CustomerInvoice, Error> { buildInvoiceInCompany(
return CustomerInvoice.create(props, id); companyId: UniqueID,
props: Omit<CustomerInvoiceProps, "companyId">,
invoiceId?: UniqueID
): Result<CustomerInvoice, Error> {
return CustomerInvoice.create({ ...props, companyId }, invoiceId);
} }
/** /**
@ -27,124 +31,108 @@ export class CustomerInvoiceService implements ICustomerInvoiceService {
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice, Error> - El agregado guardado o un error si falla la operación. * @returns Result<CustomerInvoice, Error> - El agregado guardado o un error si falla la operación.
*/ */
async save(invoice: CustomerInvoice, transaction: any): Promise<Result<CustomerInvoice, Error>> { async saveInvoice(
const saved = await this.repository.save(invoice, transaction); invoice: CustomerInvoice,
return saved.isSuccess ? Result.ok(invoice) : Result.fail(saved.error); transaction: any
): Promise<Result<CustomerInvoice, Error>> {
return this.repository.save(invoice, transaction);
} }
/** /**
* *
* Comprueba si existe o no en persistencia una factura con el ID proporcionado * Comprueba si existe o no en persistencia una factura con el ID proporcionado
* *
* @param id - Identificador UUID de la factura. * @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
* @param invoiceId - Identificador UUID de la factura.
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<Boolean, Error> - Existe la factura o no. * @returns Result<Boolean, Error> - Existe la factura o no.
*/ */
async existsById(id: UniqueID, transaction?: any): Promise<Result<boolean, Error>> { async existsByIdInCompany(
return this.repository.existsById(id, transaction); companyId: UniqueID,
invoiceId: UniqueID,
transaction?: any
): Promise<Result<boolean, Error>> {
return this.repository.existsByIdInCompany(companyId, invoiceId, transaction);
} }
/** /**
* Obtiene una colección de facturas que cumplen con los filtros definidos en un objeto Criteria. * Obtiene una colección de facturas que cumplen con los filtros definidos en un objeto Criteria.
* *
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
* @param criteria - Objeto con condiciones de filtro, paginación y orden. * @param criteria - Objeto con condiciones de filtro, paginación y orden.
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<Collection<CustomerInvoice>, Error> - Colección de facturas o error. * @returns Result<Collection<CustomerInvoice>, Error> - Colección de facturas o error.
*/ */
async findByCriteria( async findInvoiceByCriteriaInCompany(
companyId: UniqueID,
criteria: Criteria, criteria: Criteria,
transaction?: Transaction transaction?: Transaction
): Promise<Result<Collection<CustomerInvoice>, Error>> { ): Promise<Result<Collection<CustomerInvoice>, Error>> {
const customerInvoicesOrError = await this.repository.findByCriteria(criteria, transaction); return this.repository.findByCriteriaInCompany(companyId, criteria, transaction);
if (customerInvoicesOrError.isFailure) {
return Result.fail(customerInvoicesOrError.error);
}
// Solo devolver usuarios activos
//const allCustomerInvoices = customerInvoicesOrError.data.filter((customerInvoice) => customerInvoice.isActive);
//return Result.ok(new Collection(allCustomerInvoices));
return customerInvoicesOrError;
} }
/** /**
* Recupera una factura por su identificador único. * Recupera una factura por su identificador único.
* *
* @param id - Identificador UUID de la factura. * @param invoiceId - Identificador 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> - Factura encontrada o error. * @returns Result<CustomerInvoice, Error> - Factura encontrada o error.
*/ */
async getById(id: UniqueID, transaction?: Transaction): Promise<Result<CustomerInvoice>> { async getInvoiceByIdInCompany(
return await this.repository.findById(id, transaction); companyId: UniqueID,
invoiceId: UniqueID,
transaction?: Transaction
): Promise<Result<CustomerInvoice>> {
return await this.repository.getByIdInCompany(companyId, invoiceId, transaction);
} }
/** /**
* Actualiza parcialmente una factura existente con nuevos datos. * Actualiza parcialmente una factura existente con nuevos datos.
* No lo guarda en el repositorio.
* *
* @param id - Identificador de la factura a actualizar. * @param companyId - Identificador de la empresa a la que pertenece el cliente.
* @param invoiceId - Identificador de la factura a actualizar.
* @param changes - Subconjunto de props válidas para aplicar. * @param changes - Subconjunto de props válidas para aplicar.
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice, Error> - Factura actualizada o error. * @returns Result<CustomerInvoice, Error> - Factura actualizada o error.
*/ */
async updateById( async updateInvoiceByIdInCompany(
customerInvoiceId: UniqueID, companyId: UniqueID,
changes: Partial<CustomerInvoiceProps>, invoiceId: UniqueID,
changes: CustomerInvoicePatchProps,
transaction?: Transaction transaction?: Transaction
): Promise<Result<CustomerInvoice, Error>> { ): Promise<Result<CustomerInvoice, Error>> {
// Verificar si la factura existe // Verificar si la factura existe
const customerInvoiceOrError = await this.repository.getById(customerInvoiceId, transaction); const invoiceResult = await this.getInvoiceByIdInCompany(companyId, invoiceId, transaction);
if (customerInvoiceOrError.isFailure) {
return Result.fail(new Error("CustomerInvoice not found")); if (invoiceResult.isFailure) {
return Result.fail(invoiceResult.error);
} }
return Result.fail(new Error("No implementado")); const invoice = invoiceResult.data;
const updatedInvoice = invoice.update(changes);
/*const updatedCustomerInvoiceOrError = CustomerInvoice.update(customerInvoiceOrError.data, data); if (updatedInvoice.isFailure) {
if (updatedCustomerInvoiceOrError.isFailure) { return Result.fail(updatedInvoice.error);
return Result.fail(
new Error(`Error updating customerInvoice: ${updatedCustomerInvoiceOrError.error.message}`)
);
} }
const updateCustomerInvoice = updatedCustomerInvoiceOrError.data; return Result.ok(updatedInvoice.data);
await this.repo.update(updateCustomerInvoice, transaction);
return Result.ok(updateCustomerInvoice);*/
}
async createCustomerInvoice(
customerInvoiceId: UniqueID,
data: CustomerInvoiceProps,
transaction?: Transaction
): Promise<Result<CustomerInvoice, Error>> {
// Verificar si la factura existe
const customerInvoiceOrError = await this.repository.getById(customerInvoiceId, transaction);
if (customerInvoiceOrError.isSuccess) {
return Result.fail(new Error("CustomerInvoice exists"));
}
const newCustomerInvoiceOrError = CustomerInvoice.create(data, customerInvoiceId);
if (newCustomerInvoiceOrError.isFailure) {
return Result.fail(
new Error(`Error creating customerInvoice: ${newCustomerInvoiceOrError.error.message}`)
);
}
const newCustomerInvoice = newCustomerInvoiceOrError.data;
await this.repository.create(newCustomerInvoice, transaction);
return Result.ok(newCustomerInvoice);
} }
/** /**
* Elimina (o marca como eliminada) una factura según su ID. * Elimina (o marca como eliminada) una factura según su ID.
* *
* @param id - Identificador UUID de la factura. * @param companyId - Identificador de la empresa a la que pertenece el cliente.
* @param invoiceId - Identificador UUID de la factura.
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<boolean, Error> - Resultado de la operación. * @returns Result<boolean, Error> - Resultado de la operación.
*/ */
async deleteById(id: UniqueID, transaction?: Transaction): Promise<Result<void, Error>> { async deleteById(
return this.repository.deleteById(id, transaction); companyId: UniqueID,
invoiceId: UniqueID,
transaction?: Transaction
): Promise<Result<void, Error>> {
return this.repository.deleteByIdInCompany(companyId, invoiceId, transaction);
} }
} }

View File

@ -1,2 +1 @@
export * from "./customer-invoice-service.interface";
export * from "./customer-invoice.service"; export * from "./customer-invoice.service";

View File

@ -1,6 +1,6 @@
import { MoneyValue, MoneyValueProps } from "@repo/rdx-ddd"; import { MoneyValue, MoneyValueProps } from "@repo/rdx-ddd";
export class CustomerInvoiceItemSubtotalPrice extends MoneyValue { export class CustomerInvoiceItemSubtotalAmount extends MoneyValue {
public static DEFAULT_SCALE = 4; public static DEFAULT_SCALE = 4;
static create({ amount, currency_code, scale }: MoneyValueProps) { static create({ amount, currency_code, scale }: MoneyValueProps) {

View File

@ -1,6 +1,6 @@
import { MoneyValue, MoneyValueProps } from "@repo/rdx-ddd"; import { MoneyValue, MoneyValueProps } from "@repo/rdx-ddd";
export class CustomerInvoiceItemTotalPrice extends MoneyValue { export class CustomerInvoiceItemTotalAmount extends MoneyValue {
public static DEFAULT_SCALE = 4; public static DEFAULT_SCALE = 4;
static create({ amount, currency_code, scale }: MoneyValueProps) { static create({ amount, currency_code, scale }: MoneyValueProps) {

View File

@ -1,6 +1,6 @@
import { MoneyValue, MoneyValueProps } from "@repo/rdx-ddd"; import { MoneyValue, MoneyValueProps } from "@repo/rdx-ddd";
export class CustomerInvoiceItemUnitPrice extends MoneyValue { export class CustomerInvoiceItemUnitAmount extends MoneyValue {
public static DEFAULT_SCALE = 4; public static DEFAULT_SCALE = 4;
static create({ amount, currency_code, scale }: MoneyValueProps) { static create({ amount, currency_code, scale }: MoneyValueProps) {
@ -12,7 +12,7 @@ export class CustomerInvoiceItemUnitPrice extends MoneyValue {
return MoneyValue.create(props); return MoneyValue.create(props);
} }
static zero(currency_code: string, scale: number = CustomerInvoiceItemUnitPrice.DEFAULT_SCALE) { static zero(currency_code: string, scale: number = CustomerInvoiceItemUnitAmount.DEFAULT_SCALE) {
const props: MoneyValueProps = { const props: MoneyValueProps = {
amount: 0, amount: 0,
scale, scale,

View File

@ -2,9 +2,9 @@ export * from "./customer-invoice-address-type";
export * from "./customer-invoice-item-description"; export * from "./customer-invoice-item-description";
export * from "./customer-invoice-item-discount"; export * from "./customer-invoice-item-discount";
export * from "./customer-invoice-item-quantity"; export * from "./customer-invoice-item-quantity";
export * from "./customer-invoice-item-subtotal-price"; export * from "./customer-invoice-item-subtotal-amount";
export * from "./customer-invoice-item-total-price"; export * from "./customer-invoice-item-total-amount";
export * from "./customer-invoice-item-unit-price"; export * from "./customer-invoice-item-unit-amount";
export * from "./customer-invoice-number"; export * from "./customer-invoice-number";
export * from "./customer-invoice-serie"; export * from "./customer-invoice-serie";
export * from "./customer-invoice-status"; export * from "./customer-invoice-status";

View File

@ -4,7 +4,7 @@ import {
CreateCustomerInvoiceUseCase, CreateCustomerInvoiceUseCase,
CreateCustomerInvoicesAssembler, CreateCustomerInvoicesAssembler,
DeleteCustomerInvoiceUseCase, DeleteCustomerInvoiceUseCase,
GetCustomerInvoiceAssembler, GetCustomerInvoiceItemsAssembler,
GetCustomerInvoiceUseCase, GetCustomerInvoiceUseCase,
ListCustomerInvoicesAssembler, ListCustomerInvoicesAssembler,
ListCustomerInvoicesUseCase, ListCustomerInvoicesUseCase,
@ -22,7 +22,7 @@ type InvoiceDeps = {
service: ICustomerInvoiceService; service: ICustomerInvoiceService;
assemblers: { assemblers: {
list: ListCustomerInvoicesAssembler; list: ListCustomerInvoicesAssembler;
get: GetCustomerInvoiceAssembler; get: GetCustomerInvoiceItemsAssembler;
create: CreateCustomerInvoicesAssembler; create: CreateCustomerInvoicesAssembler;
update: UpdateCustomerInvoiceAssembler; update: UpdateCustomerInvoiceAssembler;
}; };
@ -54,7 +54,7 @@ export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps {
if (!_assemblers) { if (!_assemblers) {
_assemblers = { _assemblers = {
list: new ListCustomerInvoicesAssembler(), // transforma domain → ListDTO list: new ListCustomerInvoicesAssembler(), // transforma domain → ListDTO
get: new GetCustomerInvoiceAssembler(), // transforma domain → DetailDTO get: new GetCustomerInvoiceItemsAssembler(), // 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
}; };

View File

@ -2,20 +2,17 @@ import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from
import { GetCustomerInvoiceUseCase } from "../../../application"; import { GetCustomerInvoiceUseCase } from "../../../application";
export class GetCustomerInvoiceController extends ExpressController { export class GetCustomerInvoiceController extends ExpressController {
public constructor( public constructor(private readonly useCase: GetCustomerInvoiceUseCase) {
private readonly useCase: GetCustomerInvoiceUseCase
/* private readonly presenter: any */
) {
super(); super();
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
} }
protected async executeImpl() { protected async executeImpl() {
const tenantId = this.getTenantId()!; // garantizado por tenantGuard const companyId = this.getTenantId()!; // garantizado por tenantGuard
const { id } = this.req.params; const { invoice_id } = this.req.params;
const result = await this.useCase.execute({ id, tenantId }); const result = await this.useCase.execute({ invoice_id, companyId });
return result.match( return result.match(
(data) => this.ok(data), (data) => this.ok(data),

View File

@ -1,4 +1,4 @@
import { RequestWithAuth, enforceTenant } from "@erp/auth/api"; import { RequestWithAuth, enforceTenant, enforceUser, mockUser } from "@erp/auth/api";
import { ILogger, ModuleParams, validateRequest } from "@erp/core/api"; import { ILogger, ModuleParams, validateRequest } from "@erp/core/api";
import { Application, NextFunction, Request, Response, Router } from "express"; import { Application, NextFunction, Request, Response, Router } from "express";
import { Sequelize } from "sequelize"; import { Sequelize } from "sequelize";
@ -24,17 +24,33 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
logger: ILogger; logger: ILogger;
}; };
const router: Router = Router({ mergeParams: true });
const deps = getInvoiceDependencies(params); const deps = getInvoiceDependencies(params);
const router: Router = Router({ mergeParams: true });
// 🔐 Autenticación + Tenancy para TODO el router // 🔐 Autenticación + Tenancy para TODO el router
router.use(/* authenticateJWT(), */ enforceTenant() /*checkTabContext*/); if (process.env.NODE_ENV === "development") {
router.use(
(req: Request, res: Response, next: NextFunction) =>
mockUser(req as RequestWithAuth, res, next) // Debe ir antes de las rutas protegidas
);
}
router.use([
(req: Request, res: Response, next: NextFunction) =>
enforceUser()(req as RequestWithAuth, res, next), // Debe ir antes de las rutas protegidas
(req: Request, res: Response, next: NextFunction) =>
enforceTenant()(req as RequestWithAuth, res, next), // Debe ir antes de las rutas protegidas
]);
// ----------------------------------------------
router.get( router.get(
"/", "/",
//checkTabContext, //checkTabContext,
validateRequest(CustomerInvoiceListRequestSchema, "params"), validateRequest(CustomerInvoiceListRequestSchema, "params"),
async (req: RequestWithAuth, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.build.list(); const useCase = deps.build.list();
const controller = new ListCustomerInvoicesController(useCase /*, deps.presenters.list */); const controller = new ListCustomerInvoicesController(useCase /*, deps.presenters.list */);
return controller.execute(req, res, next); return controller.execute(req, res, next);
@ -64,13 +80,15 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
} }
); );
/*routes.put( /*router.put(
"/:customerInvoiceId", "/:customer_id",
validateAndParseBody(IUpdateCustomerInvoiceRequestSchema), //checkTabContext,
checkTabContext, validateRequest(UpdateCustomerInvoiceByIdParamsRequestSchema, "params"),
validateRequest(UpdateCustomerInvoiceByIdRequestSchema, "body"),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
buildUpdateCustomerInvoiceController().execute(req, res, next); const useCase = deps.build.update();
const controller = new UpdateCustomerInvoiceController(useCase);
return controller.execute(req, res, next);
} }
);*/ );*/

View File

@ -8,7 +8,7 @@ import {
CustomerInvoiceItemDescription, CustomerInvoiceItemDescription,
CustomerInvoiceItemDiscount, CustomerInvoiceItemDiscount,
CustomerInvoiceItemQuantity, CustomerInvoiceItemQuantity,
CustomerInvoiceItemUnitPrice, CustomerInvoiceItemUnitAmount,
} from "../../domain"; } from "../../domain";
import { import {
CustomerInvoiceItemCreationAttributes, CustomerInvoiceItemCreationAttributes,
@ -59,7 +59,7 @@ export class CustomerInvoiceItemMapper
} }
// Validación y creación de precio unitario // Validación y creación de precio unitario
const unitPriceOrError = CustomerInvoiceItemUnitPrice.create({ const unitPriceOrError = CustomerInvoiceItemUnitAmount.create({
amount: source.unit_price_amount, amount: source.unit_price_amount,
scale: source.unit_price_scale, scale: source.unit_price_scale,
currency_code: sourceParent.invoice_currency, currency_code: sourceParent.invoice_currency,
@ -123,11 +123,11 @@ export class CustomerInvoiceItemMapper
quantity_amount: source.quantity.toPrimitive().amount, quantity_amount: source.quantity.toPrimitive().amount,
quantity_scale: source.quantity.toPrimitive().scale, quantity_scale: source.quantity.toPrimitive().scale,
unit_price_amount: source.unitPrice.toPrimitive().amount, unit_price_amount: source.unitAmount.toPrimitive().amount,
unit_price_scale: source.unitPrice.toPrimitive().scale, unit_price_scale: source.unitAmount.toPrimitive().scale,
subtotal_amount: source.subtotalPrice.toPrimitive().amount, subtotal_amount: source.subtotalAmount.toPrimitive().amount,
subtotal_scale: source.subtotalPrice.toPrimitive().scale, subtotal_scale: source.subtotalAmount.toPrimitive().scale,
discount_amount: source.discount.toPrimitive().amount, discount_amount: source.discount.toPrimitive().amount,
discount_scale: source.discount.toPrimitive().scale, discount_scale: source.discount.toPrimitive().scale,

View File

@ -34,12 +34,28 @@ export class CustomerInvoiceModel extends Model<
declare currency_code: string; declare currency_code: string;
// Subtotal // Subtotal
declare subtotal_amount: number; declare subtotal_amount_value: number;
declare subtotal_scale: number; declare subtotal_amount_scale: number;
// Discount percentage
declare discount_percentage_value: number;
declare discount_percentage_scale: number;
// Discount amount
declare discount_amount_value: number;
declare discount_amount_scale: number;
// Taxable amount (base imponible)
declare taxable_amount_value: number;
declare taxable_amount_scale: number;
// Total tax amount / taxes total
declare tax_amount_value: number;
declare tax_amount_scale: number;
// Total // Total
declare total_amount: number; declare total_amount_value: number;
declare total_scale: number; declare total_amount_scale: number;
// Relaciones // Relaciones
declare items: NonAttribute<CustomerInvoiceItemModel[]>; declare items: NonAttribute<CustomerInvoiceItemModel[]>;
@ -125,23 +141,70 @@ export default (database: Sequelize) => {
defaultValue: "EUR", defaultValue: "EUR",
}, },
subtotal_amount: { subtotal_amount_value: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,
}, },
subtotal_scale: { subtotal_amount_scale: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,
}, },
total_amount: { discount_percentage_value: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
},
discount_percentage_scale: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
},
discount_amount_value: {
type: new DataTypes.BIGINT(),
allowNull: true,
defaultValue: null,
},
discount_amount_scale: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
},
taxable_amount_value: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,
}, },
total_scale: { taxable_amount_scale: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
},
tax_amount_value: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true,
defaultValue: null,
},
tax_amount_scale: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
},
total_amount_value: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true,
defaultValue: null,
},
total_amount_scale: {
type: new DataTypes.SMALLINT(), type: new DataTypes.SMALLINT(),
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,

View File

@ -1,7 +1,7 @@
import * as z from "zod/v4"; import * as z from "zod/v4";
export const GetCustomerInvoiceByIdRequestSchema = z.object({ export const GetCustomerInvoiceByIdRequestSchema = z.object({
id: z.string(), invoice_id: z.string(),
}); });
export type GetCustomerInvoiceByIdRequestDTO = z.infer<typeof GetCustomerInvoiceByIdRequestSchema>; export type GetCustomerInvoiceByIdRequestDTO = z.infer<typeof GetCustomerInvoiceByIdRequestSchema>;

View File

@ -1,15 +1,32 @@
import { MetadataSchema } from "@erp/core"; import { AmountSchema, MetadataSchema, PercentageSchema, QuantitySchema } from "@erp/core";
import * as z from "zod/v4"; import * as z from "zod/v4";
export const GetCustomerInvoiceByIdResponseSchema = z.object({ export const GetCustomerInvoiceByIdResponseSchema = z.object({
id: z.uuid(), id: z.uuid(),
invoice_status: z.string(), company_id: z.uuid(),
invoice_number: z.string(), invoice_number: z.string(),
invoice_series: z.string(), status: z.string(),
issue_date: z.iso.datetime({ offset: true }), series: z.string(),
operation_date: z.iso.datetime({ offset: true }),
issue_date: z.string(),
operation_date: z.string(),
notes: z.string(),
language_code: z.string(), language_code: z.string(),
currency: z.string(), currency_code: z.string(),
items: z.array(
z.object({
position: z.string(),
description: z.string(),
quantity: QuantitySchema,
unit_price_amount: AmountSchema,
discount: PercentageSchema,
total_amount: AmountSchema,
})
),
metadata: MetadataSchema.optional(), metadata: MetadataSchema.optional(),
}); });

View File

@ -90,14 +90,14 @@ export class CustomerService {
* *
* @param companyId - Identificador de la empresa a la que pertenece el cliente. * @param companyId - Identificador de la empresa a la que pertenece el cliente.
* @param customerId - Identificador del cliente a actualizar. * @param customerId - Identificador del cliente a actualizar.
* @param partial - Subconjunto de props válidas para aplicar. * @param changes - Subconjunto de props válidas para aplicar.
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<Customer, Error> - Cliente actualizado o error. * @returns Result<Customer, Error> - Cliente actualizado o error.
*/ */
async updateCustomerByIdInCompany( async updateCustomerByIdInCompany(
companyId: UniqueID, companyId: UniqueID,
customerId: UniqueID, customerId: UniqueID,
partial: CustomerPatchProps, changes: CustomerPatchProps,
transaction?: any transaction?: any
): Promise<Result<Customer, Error>> { ): Promise<Result<Customer, Error>> {
const customerResult = await this.getCustomerByIdInCompany(companyId, customerId, transaction); const customerResult = await this.getCustomerByIdInCompany(companyId, customerId, transaction);
@ -107,7 +107,7 @@ export class CustomerService {
} }
const customer = customerResult.data; const customer = customerResult.data;
const updatedCustomer = customer.update(partial); const updatedCustomer = customer.update(changes);
if (updatedCustomer.isFailure) { if (updatedCustomer.isFailure) {
return Result.fail(updatedCustomer.error); return Result.fail(updatedCustomer.error);

View File

@ -1,4 +1,4 @@
import { enforceTenant, enforceUser, mockUser } from "@erp/auth/api"; import { RequestWithAuth, enforceTenant, enforceUser, mockUser } from "@erp/auth/api";
import { ILogger, ModuleParams, validateRequest } from "@erp/core/api"; import { ILogger, ModuleParams, validateRequest } from "@erp/core/api";
import { Application, NextFunction, Request, Response, Router } from "express"; import { Application, NextFunction, Request, Response, Router } from "express";
import { Sequelize } from "sequelize"; import { Sequelize } from "sequelize";
@ -33,11 +33,22 @@ export const customersRouter = (params: ModuleParams) => {
// 🔐 Autenticación + Tenancy para TODO el router // 🔐 Autenticación + Tenancy para TODO el router
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
router.use(mockUser); // Debe ir antes de las rutas protegidas router.use(
(req: Request, res: Response, next: NextFunction) =>
mockUser(req as RequestWithAuth, res, next) // Debe ir antes de las rutas protegidas
);
} }
//router.use(/*authenticateJWT(),*/ enforceTenant() /*checkTabContext*/); //router.use(/*authenticateJWT(),*/ enforceTenant() /*checkTabContext*/);
router.use([enforceUser(), enforceTenant()]); router.use([
(req: Request, res: Response, next: NextFunction) =>
enforceUser()(req as RequestWithAuth, res, next), // Debe ir antes de las rutas protegidas
(req: Request, res: Response, next: NextFunction) =>
enforceTenant()(req as RequestWithAuth, res, next), // Debe ir antes de las rutas protegidas
]);
// ----------------------------------------------
router.get( router.get(
"/", "/",

View File

@ -5,7 +5,7 @@ export function isNullishOrEmpty(input: unknown): boolean {
} }
// Función genérica para asegurar valores básicos // Función genérica para asegurar valores básicos
function ensure<T>(value: T | undefined | null, defaultValue: T): T { export function ensure<T>(value: T | undefined | null, defaultValue: T): T {
return value ?? defaultValue; return value ?? defaultValue;
} }