Facturas de cliente

This commit is contained in:
David Arranz 2025-06-26 20:05:33 +02:00
parent 6c472c21aa
commit b87082754b
35 changed files with 227 additions and 130 deletions

View File

@ -27,7 +27,7 @@
"@types/glob": "^8.1.0", "@types/glob": "^8.1.0",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/jsonwebtoken": "^9.0.8", "@types/jsonwebtoken": "^9.0.8",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.6.2",
"@types/node": "^22.15.12", "@types/node": "^22.15.12",
"@types/passport": "^1.0.16", "@types/passport": "^1.0.16",
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",
@ -54,7 +54,7 @@
"helmet": "^8.0.0", "helmet": "^8.0.0",
"http": "0.0.1-security", "http": "0.0.1-security",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"luxon": "^3.5.0", "luxon": "^3.6.1",
"module-alias": "^2.2.3", "module-alias": "^2.2.3",
"mysql2": "^3.12.0", "mysql2": "^3.12.0",
"passport": "^0.7.0", "passport": "^0.7.0",
@ -75,14 +75,9 @@
"node": ">=22" "node": ">=22"
}, },
"tsup": { "tsup": {
"entry": [ "entry": ["src/index.ts"],
"src/index.ts"
],
"outDir": "dist", "outDir": "dist",
"format": [ "format": ["esm", "cjs"],
"esm",
"cjs"
],
"target": "es2020", "target": "es2020",
"sourcemap": true, "sourcemap": true,
"clean": true, "clean": true,

View File

@ -105,7 +105,8 @@ const server = http
// Manejo de promesas no capturadas // Manejo de promesas no capturadas
process.on("unhandledRejection", (reason: any, promise: Promise<any>) => { process.on("unhandledRejection", (reason: any, promise: Promise<any>) => {
logger.error(`❌ Unhandled rejection at:", ${promise}, "reason:", ${reason}`); const error = `❌ Unhandled rejection at:", ${promise}, "reason:", ${reason}`;
logger.error(error);
// Dependiendo de la aplicación, podrías desear una salida total o un cierre controlado // Dependiendo de la aplicación, podrías desear una salida total o un cierre controlado
process.exit(1); process.exit(1);
}); });

View File

@ -0,0 +1,6 @@
export class DuplicateEntityError extends Error {
constructor(entity: string, id: string) {
super(`Entity '${entity}' with ID '${id}' already exists.`);
this.name = "DuplicateEntityError";
}
}

View File

@ -9,6 +9,7 @@ import {
import { ApiError } from "./api-error"; import { ApiError } from "./api-error";
import { ConflictApiError } from "./conflict-api-error"; import { ConflictApiError } from "./conflict-api-error";
import { DomainValidationError } from "./domain-validation-error"; import { DomainValidationError } from "./domain-validation-error";
import { DuplicateEntityError } from "./duplicate-entity-error";
import { ForbiddenApiError } from "./forbidden-api-error"; import { ForbiddenApiError } from "./forbidden-api-error";
import { InternalApiError } from "./internal-api-error"; import { InternalApiError } from "./internal-api-error";
import { NotFoundApiError } from "./not-found-api-error"; import { NotFoundApiError } from "./not-found-api-error";
@ -74,6 +75,10 @@ export const errorMapper = {
return new ValidationApiError(error.detail, [{ path: error.field, message: error.detail }]); return new ValidationApiError(error.detail, [{ path: error.field, message: error.detail }]);
} }
if (error instanceof DuplicateEntityError) {
return new ConflictApiError(error.message);
}
// 3. 🔍 Errores individuales de validación // 3. 🔍 Errores individuales de validación
if ( if (
message.includes("invalid") || message.includes("invalid") ||

View File

@ -1,4 +1,12 @@
export * from "./api-error";
export * from "./conflict-api-error";
export * from "./domain-validation-error"; export * from "./domain-validation-error";
export * from "./duplicate-entity-error";
export * from "./error-mapper"; export * from "./error-mapper";
export * from "./forbidden-api-error";
export * from "./internal-api-error";
export * from "./not-found-api-error";
export * from "./unauthorized-api-error";
export * from "./unavailable-api-error";
export * from "./validation-api-error"; export * from "./validation-api-error";
export * from "./validation-error-collection"; export * from "./validation-error-collection";

View File

@ -41,6 +41,7 @@ export const validateRequest = <T extends "body" | "query" | "params">(
): RequestHandler => { ): RequestHandler => {
return async (req, res, next) => { return async (req, res, next) => {
console.debug(`Validating request ${source} with schema.`); console.debug(`Validating request ${source} with schema.`);
console.debug(req[source]);
const result = schema.safeParse(req[source]); const result = schema.safeParse(req[source]);
if (!result.success) { if (!result.success) {

View File

@ -41,7 +41,8 @@ export abstract class SequelizeMapper<
source: TModel[], source: TModel[],
params?: MapperParamsType params?: MapperParamsType
): Result<Collection<TEntity>, Error> { ): Result<Collection<TEntity>, Error> {
return this.mapArrayAndCountToDomain(source, source.length, params); const items = source ?? [];
return this.mapArrayAndCountToDomain(items, items.length, params);
} }
public mapArrayAndCountToDomain( public mapArrayAndCountToDomain(
@ -49,12 +50,14 @@ export abstract class SequelizeMapper<
totalCount: number, totalCount: number,
params?: MapperParamsType params?: MapperParamsType
): Result<Collection<TEntity>, Error> { ): Result<Collection<TEntity>, Error> {
const _source = source ?? [];
try { try {
if (source.length === 0) { if (_source.length === 0) {
return Result.ok(new Collection([], totalCount)); return Result.ok(new Collection([], totalCount));
} }
const items = source.map( const items = _source.map(
(value, index) => this.mapToDomain(value, { index, ...params }).data (value, index) => this.mapToDomain(value, { index, ...params }).data
); );
return Result.ok(new Collection(items, totalCount)); return Result.ok(new Collection(items, totalCount));

View File

@ -1,4 +1,4 @@
import { ITransactionManager } from "@erp/core/api"; import { DuplicateEntityError, ITransactionManager } from "@erp/core/api";
import { CreateCustomerInvoiceCommandDTO } from "@erp/customer-invoices/common/dto"; import { CreateCustomerInvoiceCommandDTO } from "@erp/customer-invoices/common/dto";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { Transaction } from "sequelize"; import { Transaction } from "sequelize";
@ -8,19 +8,21 @@ import { CreateCustomerInvoicesPresenter } from "./presenter";
export class CreateCustomerInvoiceUseCase { export class CreateCustomerInvoiceUseCase {
constructor( constructor(
private readonly customerInvoiceService: ICustomerInvoiceService, private readonly service: ICustomerInvoiceService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
private readonly presenter: CreateCustomerInvoicesPresenter private readonly presenter: CreateCustomerInvoicesPresenter
) {} ) {}
public execute(dto: CreateCustomerInvoiceCommandDTO) { public execute(dto: CreateCustomerInvoiceCommandDTO) {
const invoicePropOrError = mapDTOToCustomerInvoiceProps(dto); const invoicePropsOrError = mapDTOToCustomerInvoiceProps(dto);
if (invoicePropOrError.isFailure) { if (invoicePropsOrError.isFailure) {
return Result.fail(invoicePropOrError.error); return Result.fail(invoicePropsOrError.error);
} }
const invoiceOrError = this.customerInvoiceService.build(invoicePropOrError.data); const { props, id } = invoicePropsOrError.data;
const invoiceOrError = this.service.build(props, id);
if (invoiceOrError.isFailure) { if (invoiceOrError.isFailure) {
return Result.fail(invoiceOrError.error); return Result.fail(invoiceOrError.error);
@ -29,13 +31,27 @@ export class CreateCustomerInvoiceUseCase {
const newInvoice = invoiceOrError.data; const newInvoice = invoiceOrError.data;
return this.transactionManager.complete(async (transaction: Transaction) => { return this.transactionManager.complete(async (transaction: Transaction) => {
const result = await this.customerInvoiceService.save(newInvoice, transaction); try {
if (result.isFailure) { const duplicateCheck = await this.service.existsById(id, transaction);
return Result.fail(result.error);
}
const viewDTO = this.presenter.toDTO(newInvoice); if (duplicateCheck.isFailure) {
return Result.ok(viewDTO); return Result.fail(duplicateCheck.error);
}
if (duplicateCheck.data) {
return Result.fail(new DuplicateEntityError("CustomerInvoice", id.toString()));
}
const result = await this.service.save(newInvoice, transaction);
if (result.isFailure) {
return Result.fail(result.error);
}
const viewDTO = this.presenter.toDTO(newInvoice);
return Result.ok(viewDTO);
} catch (error: unknown) {
return Result.fail(error as Error);
}
}); });
} }
} }

View File

@ -1,24 +1,34 @@
import { UniqueID } from "@/core/common/domain"; import { ITransactionManager } from "@erp/core/api";
import { ITransactionManager } from "@/core/common/infrastructure/database"; import { GetCustomerInvoiceByIdQueryDTO } from "@erp/customer-invoices/common/dto";
import { logger } from "@/lib/logger"; import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { CustomerInvoice, ICustomerInvoiceService } from "../domain"; import { ICustomerInvoiceService } from "../../domain";
import { GetCustomerInvoicePresenter } from "./presenter";
export class GetCustomerInvoiceUseCase { export class GetCustomerInvoiceUseCase {
constructor( constructor(
private readonly customerInvoiceService: ICustomerInvoiceService, private readonly service: ICustomerInvoiceService,
private readonly transactionManager: ITransactionManager private readonly transactionManager: ITransactionManager,
private readonly presenter: GetCustomerInvoicePresenter
) {} ) {}
public execute(customerInvoiceID: UniqueID): Promise<Result<CustomerInvoice, Error>> { public execute(dto: GetCustomerInvoiceByIdQueryDTO) {
const idOrError = UniqueID.create(dto.id);
if (idOrError.isFailure) {
return Result.fail(idOrError.error);
}
return this.transactionManager.complete(async (transaction) => { return this.transactionManager.complete(async (transaction) => {
try { try {
return await this.customerInvoiceService.findCustomerInvoiceById( const invoiceOrError = await this.service.getById(idOrError.data, transaction);
customerInvoiceID, if (invoiceOrError.isFailure) {
transaction return Result.fail(invoiceOrError.error);
); }
const getDTO = this.presenter.toDTO(invoiceOrError.data);
return Result.ok(getDTO);
} catch (error: unknown) { } catch (error: unknown) {
logger.error(error as Error);
return Result.fail(error as Error); return Result.fail(error as Error);
} }
}); });

View File

@ -1 +1,2 @@
export * from "./get-customer-invoice.use-case"; export * from "./get-customer-invoice.use-case";
export * from "./presenter";

View File

@ -1,25 +1,31 @@
import { IGetCustomerInvoiceResponseDTO } from "../../../../common/dto"; import { GetCustomerInvoiceResultDTO } from "../../../../common/dto";
import { CustomerInvoice, CustomerInvoiceItem } from "../../../domain"; import { CustomerInvoice } from "../../../domain";
export interface IGetCustomerInvoicePresenter { export interface GetCustomerInvoicePresenter {
toDTO: (customerInvoice: CustomerInvoice) => IGetCustomerInvoiceResponseDTO; toDTO: (customerInvoice: CustomerInvoice) => GetCustomerInvoiceResultDTO;
} }
export const getCustomerInvoicePresenter: IGetCustomerInvoicePresenter = { export const getCustomerInvoicePresenter: GetCustomerInvoicePresenter = {
toDTO: (customerInvoice: CustomerInvoice): IGetCustomerInvoiceResponseDTO => ({ toDTO: (customerInvoice: CustomerInvoice): GetCustomerInvoiceResultDTO => ({
id: customerInvoice.id.toPrimitive(), id: customerInvoice.id.toPrimitive(),
customerInvoice_status: customerInvoice.status.toString(), invoice_status: customerInvoice.status.toString(),
customerInvoice_number: customerInvoice.invoiceNumber.toString(), invoice_number: customerInvoice.invoiceNumber.toString(),
customerInvoice_series: customerInvoice.invoiceSeries.toString(), invoice_series: customerInvoice.invoiceSeries.toString(),
issue_date: customerInvoice.issueDate.toDateString(), issue_date: customerInvoice.issueDate.toDateString(),
operation_date: customerInvoice.operationDate.toDateString(), operation_date: customerInvoice.operationDate.toDateString(),
language_code: "ES", language_code: "ES",
currency: customerInvoice.customerInvoiceCurrency.toString(), currency: customerInvoice.currency,
subtotal: customerInvoice.calculateSubtotal().toPrimitive(),
total: customerInvoice.calculateTotal().toPrimitive(),
items: metadata: {
entity: "customer-invoices",
},
//subtotal: customerInvoice.calculateSubtotal().toPrimitive(),
//total: customerInvoice.calculateTotal().toPrimitive(),
/*items:
customerInvoice.items.size() > 0 customerInvoice.items.size() > 0
? customerInvoice.items.map((item: CustomerInvoiceItem) => ({ ? customerInvoice.items.map((item: CustomerInvoiceItem) => ({
description: item.description.toString(), description: item.description.toString(),
@ -30,7 +36,7 @@ export const getCustomerInvoicePresenter: IGetCustomerInvoicePresenter = {
//tax_amount: item.calculateTaxAmount().toPrimitive(), //tax_amount: item.calculateTaxAmount().toPrimitive(),
total: item.calculateTotal().toPrimitive(), total: item.calculateTotal().toPrimitive(),
})) }))
: [], : [],*/
//sender: {}, //await CustomerInvoiceParticipantPresenter(customerInvoice.senderId, context), //sender: {}, //await CustomerInvoiceParticipantPresenter(customerInvoice.senderId, context),

View File

@ -1,5 +1,5 @@
import { ValidationErrorCollection, ValidationErrorDetail } from "@erp/core/api"; import { ValidationErrorCollection, ValidationErrorDetail } from "@erp/core/api";
import { UtcDate } from "@repo/rdx-ddd"; import { UniqueID, UtcDate } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { CreateCustomerInvoiceCommandDTO } from "../../../common/dto"; import { CreateCustomerInvoiceCommandDTO } from "../../../common/dto";
import { import {
@ -16,16 +16,15 @@ import { mapDTOToCustomerInvoiceItemsProps } from "./map-dto-to-customer-invoice
* No construye directamente el agregado. * No construye directamente el agregado.
* *
* @param dto - DTO con los datos de la factura de cliente * @param dto - DTO con los datos de la factura de cliente
* @returns CustomerInvoiceProps - Las propiedades para crear una factura de cliente o error * @returns
* *
*/ */
export function mapDTOToCustomerInvoiceProps( export function mapDTOToCustomerInvoiceProps(dto: CreateCustomerInvoiceCommandDTO) {
dto: CreateCustomerInvoiceCommandDTO
): Result<CustomerInvoiceProps, Error> {
const errors: ValidationErrorDetail[] = []; const errors: ValidationErrorDetail[] = [];
//const invoiceId = extractOrPushError(UniqueID.create(dto.id), "invoice_id", errors); const invoiceId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
const invoiceNumber = extractOrPushError( const invoiceNumber = extractOrPushError(
CustomerInvoiceNumber.create(dto.invoice_number), CustomerInvoiceNumber.create(dto.invoice_number),
@ -66,7 +65,7 @@ export function mapDTOToCustomerInvoiceProps(
currency, currency,
}; };
return Result.ok(invoiceProps); return Result.ok({ id: invoiceId!, props: invoiceProps });
/*if (hasNoUndefinedFields(invoiceProps)) { /*if (hasNoUndefinedFields(invoiceProps)) {
const invoiceOrError = CustomerInvoice.create(invoiceProps, invoiceId); const invoiceOrError = CustomerInvoice.create(invoiceProps, invoiceId);

View File

@ -1,5 +1,5 @@
export * from "./create-customer-invoice"; export * from "./create-customer-invoice";
//export * from "./delete-customer-invoice"; //export * from "./delete-customer-invoice";
//export * from "./get-customer-invoice"; export * from "./get-customer-invoice";
export * from "./list-customer-invoices"; export * from "./list-customer-invoices";
//export * from "./update-customer-invoice"; //export * from "./update-customer-invoice";

View File

@ -16,10 +16,7 @@ export class ListCustomerInvoicesUseCase {
public execute(criteria: Criteria): Promise<Result<ListCustomerInvoicesResultDTO, Error>> { public execute(criteria: Criteria): Promise<Result<ListCustomerInvoicesResultDTO, Error>> {
return this.transactionManager.complete(async (transaction: Transaction) => { return this.transactionManager.complete(async (transaction: Transaction) => {
try { try {
const result = await this.customerInvoiceService.findCustomerInvoices( const result = await this.customerInvoiceService.findByCriteria(criteria, transaction);
criteria,
transaction
);
if (result.isFailure) { if (result.isFailure) {
return Result.fail(result.error); return Result.fail(result.error);

View File

@ -1,20 +1,20 @@
import { ListCustomerInvoicesViewDTO } from "@erp/customer-invoices/common/dto";
import { Criteria } from "@repo/rdx-criteria/server"; import { Criteria } from "@repo/rdx-criteria/server";
import { Collection } from "@repo/rdx-utils"; import { Collection } from "@repo/rdx-utils";
import { ListCustomerInvoicesResultDTO } from "../../../../common/dto";
import { CustomerInvoice } from "../../../domain"; import { CustomerInvoice } from "../../../domain";
export interface ListCustomerInvoicesPresenter { export interface ListCustomerInvoicesPresenter {
toDTO: ( toDTO: (
customerInvoices: Collection<CustomerInvoice>, customerInvoices: Collection<CustomerInvoice>,
criteria: Criteria criteria: Criteria
) => ListCustomerInvoicesViewDTO; ) => ListCustomerInvoicesResultDTO;
} }
export const listCustomerInvoicesPresenter: ListCustomerInvoicesPresenter = { export const listCustomerInvoicesPresenter: ListCustomerInvoicesPresenter = {
toDTO: ( toDTO: (
customerInvoices: Collection<CustomerInvoice>, customerInvoices: Collection<CustomerInvoice>,
criteria: Criteria criteria: Criteria
): ListCustomerInvoicesViewDTO => { ): ListCustomerInvoicesResultDTO => {
const items = customerInvoices.map((invoice) => { const items = customerInvoices.map((invoice) => {
return { return {
id: invoice.id.toPrimitive(), id: invoice.id.toPrimitive(),
@ -25,7 +25,7 @@ export const listCustomerInvoicesPresenter: ListCustomerInvoicesPresenter = {
issue_date: invoice.issueDate.toISOString(), issue_date: invoice.issueDate.toISOString(),
operation_date: invoice.operationDate.toISOString(), operation_date: invoice.operationDate.toISOString(),
language_code: "ES", language_code: "ES",
currency: invoice.customerInvoiceCurrency.toString(), currency: "EUR",
subtotal_price: invoice.calculateSubtotal().toPrimitive(), subtotal_price: invoice.calculateSubtotal().toPrimitive(),
total_price: invoice.calculateTotal().toPrimitive(), total_price: invoice.calculateTotal().toPrimitive(),

View File

@ -2,12 +2,15 @@ import { SequelizeTransactionManager } from "@erp/core/api";
import { Sequelize } from "sequelize"; import { Sequelize } from "sequelize";
import { CreateCustomerInvoiceUseCase, CreateCustomerInvoicesPresenter } from "../../application/"; import { CreateCustomerInvoiceUseCase, CreateCustomerInvoicesPresenter } from "../../application/";
import { CustomerInvoiceService } from "../../domain"; import { CustomerInvoiceService } from "../../domain";
import { CustomerInvoiceRepository, customerInvoiceMapper } from "../../infrastructure"; import { CustomerInvoiceMapper, CustomerInvoiceRepository } from "../../infrastructure";
import { CreateCustomerInvoiceController } from "./create-customer-invoice"; import { CreateCustomerInvoiceController } from "./create-customer-invoice";
export const buildCreateCustomerInvoicesController = (database: Sequelize) => { export const buildCreateCustomerInvoicesController = (database: Sequelize) => {
const transactionManager = new SequelizeTransactionManager(database); const transactionManager = new SequelizeTransactionManager(database);
const customerInvoiceRepository = new CustomerInvoiceRepository(database, customerInvoiceMapper); const customerInvoiceRepository = new CustomerInvoiceRepository(
database,
new CustomerInvoiceMapper()
);
const customerInvoiceService = new CustomerInvoiceService(customerInvoiceRepository); const customerInvoiceService = new CustomerInvoiceService(customerInvoiceRepository);
const presenter = new CreateCustomerInvoicesPresenter(); const presenter = new CreateCustomerInvoicesPresenter();

View File

@ -1,47 +1,30 @@
import { ExpressController } from "@erp/core/api"; import { ExpressController, errorMapper } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { GetCustomerInvoiceUseCase } from "../../application"; import { GetCustomerInvoiceUseCase } from "../../application";
import { IGetCustomerInvoicePresenter } from "./presenter";
export class GetCustomerInvoiceController extends ExpressController { export class GetCustomerInvoiceController extends ExpressController {
public constructor( public constructor(private readonly getCustomerInvoice: GetCustomerInvoiceUseCase) {
private readonly getCustomerInvoice: GetCustomerInvoiceUseCase,
private readonly presenter: IGetCustomerInvoicePresenter
) {
super(); super();
} }
protected async executeImpl() { protected async executeImpl() {
const { customerInvoiceId } = this.req.params; const { id } = this.req.params;
// Validar ID /*
const customerInvoiceIdOrError = UniqueID.create(customerInvoiceId); const user = this.req.user; // asumimos middleware authenticateJWT inyecta user
if (customerInvoiceIdOrError.isFailure)
return this.invalidInputError("CustomerInvoice ID not valid");
const customerInvoiceOrError = await this.getCustomerInvoice.execute( if (!user || !user.companyId) {
customerInvoiceIdOrError.data this.unauthorized(res, "Unauthorized: user or company not found");
); return;
}
*/
if (customerInvoiceOrError.isFailure) { const result = await this.getCustomerInvoice.execute({ id });
return this.handleError(customerInvoiceOrError.error);
if (result.isFailure) {
const apiError = errorMapper.toApiError(result.error);
return this.handleApiError(apiError);
} }
return this.ok(this.presenter.toDTO(customerInvoiceOrError.data)); return this.ok(result.data);
}
private handleError(error: Error) {
const message = error.message;
if (
message.includes("Database connection lost") ||
message.includes("Database request timed out")
) {
return this.unavailableError(
"Database service is currently unavailable. Please try again later."
);
}
return this.conflictError(message);
} }
} }

View File

@ -1,19 +1,21 @@
import { SequelizeTransactionManager } from "@erp/core/api"; import { SequelizeTransactionManager } from "@erp/core/api";
import { Sequelize } from "sequelize"; import { Sequelize } from "sequelize";
import { GetCustomerInvoiceUseCase, getCustomerInvoicePresenter } from "../../application";
import { CustomerInvoiceService } from "../../domain"; import { CustomerInvoiceService } from "../../domain";
import { CustomerInvoiceRepository, customerInvoiceMapper } from "../../infrastructure"; import { CustomerInvoiceRepository, customerInvoiceMapper } from "../../infrastructure";
import { GetCustomerInvoiceUseCase } from "../../application";
import { GetCustomerInvoiceController } from "./get-invoice.controller"; import { GetCustomerInvoiceController } from "./get-invoice.controller";
import { getCustomerInvoicePresenter } from "./presenter";
export const buildGetCustomerInvoiceController = (database: Sequelize) => { export const buildGetCustomerInvoiceController = (database: Sequelize) => {
const transactionManager = new SequelizeTransactionManager(database); const transactionManager = new SequelizeTransactionManager(database);
const customerInvoiceRepository = new CustomerInvoiceRepository(database, customerInvoiceMapper); const customerInvoiceRepository = new CustomerInvoiceRepository(database, customerInvoiceMapper);
const customerInvoiceService = new CustomerInvoiceService(customerInvoiceRepository); const customerInvoiceService = new CustomerInvoiceService(customerInvoiceRepository);
const useCase = new GetCustomerInvoiceUseCase(customerInvoiceService, transactionManager);
const presenter = getCustomerInvoicePresenter; const presenter = getCustomerInvoicePresenter;
return new GetCustomerInvoiceController(useCase, presenter); const useCase = new GetCustomerInvoiceUseCase(
customerInvoiceService,
transactionManager,
presenter
);
return new GetCustomerInvoiceController(useCase);
}; };

View File

@ -1,6 +1,5 @@
export * from "./aggregates"; export * from "./aggregates";
export * from "./entities"; export * from "./entities";
export * from "./errors";
export * from "./repositories"; export * from "./repositories";
export * from "./services"; export * from "./services";
export * from "./value-objects"; export * from "./value-objects";

View File

@ -4,6 +4,8 @@ import { Collection, Result } from "@repo/rdx-utils";
import { CustomerInvoice } from "../aggregates"; import { CustomerInvoice } from "../aggregates";
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.

View File

@ -4,10 +4,12 @@ import { Collection, Result } from "@repo/rdx-utils";
import { CustomerInvoice, CustomerInvoiceProps } from "../aggregates"; import { CustomerInvoice, CustomerInvoiceProps } from "../aggregates";
export interface ICustomerInvoiceService { export interface ICustomerInvoiceService {
build(props: CustomerInvoiceProps): Result<CustomerInvoice, Error>; build(props: CustomerInvoiceProps, id?: UniqueID): Result<CustomerInvoice, Error>;
save(invoice: CustomerInvoice, transaction: any): Promise<Result<CustomerInvoice, Error>>; save(invoice: CustomerInvoice, transaction: any): Promise<Result<CustomerInvoice, Error>>;
existsById(id: UniqueID, transaction?: any): Promise<Result<boolean, Error>>;
findByCriteria( findByCriteria(
criteria: Criteria, criteria: Criteria,
transaction?: any transaction?: any

View File

@ -13,10 +13,11 @@ export class CustomerInvoiceService implements ICustomerInvoiceService {
* Construye un nuevo agregado CustomerInvoice a partir de props validadas. * Construye un nuevo agregado CustomerInvoice a partir de props validadas.
* *
* @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).
* @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): Result<CustomerInvoice, Error> { build(props: CustomerInvoiceProps, id?: UniqueID): Result<CustomerInvoice, Error> {
return CustomerInvoice.create(props); return CustomerInvoice.create(props, id);
} }
/** /**
@ -31,6 +32,19 @@ export class CustomerInvoiceService implements ICustomerInvoiceService {
return saved.isSuccess ? Result.ok(invoice) : Result.fail(saved.error); return saved.isSuccess ? Result.ok(invoice) : Result.fail(saved.error);
} }
/**
*
* Comprueba si existe o no en persistencia una factura con el ID proporcionado
*
* @param id - Identificador UUID de la factura.
* @param transaction - Transacción activa para la operación.
* @returns Result<Boolean, Error> - Existe la factura o no.
*/
async existsById(id: UniqueID, transaction?: any): Promise<Result<boolean, Error>> {
return this.repository.existsById(id, 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.
* *
@ -62,7 +76,7 @@ export class CustomerInvoiceService implements ICustomerInvoiceService {
* @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 getById(id: UniqueID, transaction?: Transaction): Promise<Result<CustomerInvoice>> {
return await this.repository.getById(id, transaction); return await this.repository.findById(id, transaction);
} }
/** /**

View File

@ -3,10 +3,12 @@ import { Application, NextFunction, Request, Response, Router } from "express";
import { Sequelize } from "sequelize"; import { Sequelize } from "sequelize";
import { import {
CreateCustomerInvoiceCommandSchema, CreateCustomerInvoiceCommandSchema,
GetCustomerInvoiceByIdQuerySchema,
ListCustomerInvoicesQuerySchema, ListCustomerInvoicesQuerySchema,
} from "../../../common/dto"; } from "../../../common/dto";
import { import {
buildCreateCustomerInvoicesController, buildCreateCustomerInvoicesController,
buildGetCustomerInvoiceController,
buildListCustomerInvoicesController, buildListCustomerInvoicesController,
} from "../../controllers"; } from "../../controllers";
@ -24,21 +26,21 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
"/", "/",
//checkTabContext, //checkTabContext,
//checkUser, //checkUser,
validateRequest(ListCustomerInvoicesQuerySchema, "query"), validateRequest(ListCustomerInvoicesQuerySchema, "params"),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
buildListCustomerInvoicesController(database).execute(req, res, next); buildListCustomerInvoicesController(database).execute(req, res, next);
} }
); );
/*routes.get( routes.get(
"/:customerInvoiceId", "/:id",
//checkTabContext, //checkTabContext,
//checkUser, //checkUser,
validateRequest(GetCustomerInvoiceByIdQuerySchema, "query"), validateRequest(GetCustomerInvoiceByIdQuerySchema, "params"),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
buildGetCustomerInvoiceController(database).execute(req, res, next); buildGetCustomerInvoiceController(database).execute(req, res, next);
} }
);*/ );
routes.post( routes.post(
"/", "/",

View File

@ -67,11 +67,11 @@ export class CustomerInvoiceMapper
return CustomerInvoice.create( return CustomerInvoice.create(
{ {
status: statusOrError.data, status: statusOrError.data,
customerInvoiceSeries: customerInvoiceSeriesOrError.data, invoiceSeries: customerInvoiceSeriesOrError.data,
customerInvoiceNumber: customerInvoiceNumberOrError.data, invoiceNumber: customerInvoiceNumberOrError.data,
issueDate: issueDateOrError.data, issueDate: issueDateOrError.data,
operationDate: operationDateOrError.data, operationDate: operationDateOrError.data,
customerInvoiceCurrency, currency: customerInvoiceCurrency,
items: itemsOrErrors.data, items: itemsOrErrors.data,
}, },
idOrError.data idOrError.data
@ -95,7 +95,7 @@ export class CustomerInvoiceMapper
issue_date: source.issueDate.toPrimitive(), issue_date: source.issueDate.toPrimitive(),
operation_date: source.operationDate.toPrimitive(), operation_date: source.operationDate.toPrimitive(),
invoice_language: "es", invoice_language: "es",
invoice_currency: source.customerInvoiceCurrency || "EUR", invoice_currency: source.currency || "EUR",
subtotal_amount: subtotal.amount, subtotal_amount: subtotal.amount,
subtotal_scale: subtotal.scale, subtotal_scale: subtotal.scale,

View File

@ -11,16 +11,26 @@ export class CustomerInvoiceRepository
extends SequelizeRepository<CustomerInvoice> extends SequelizeRepository<CustomerInvoice>
implements ICustomerInvoiceRepository implements ICustomerInvoiceRepository
{ {
private readonly model: typeof CustomerInvoiceModel; //private readonly model: typeof CustomerInvoiceModel;
private readonly mapper!: ICustomerInvoiceMapper; private readonly mapper!: ICustomerInvoiceMapper;
constructor(database: Sequelize, mapper: ICustomerInvoiceMapper) { constructor(database: Sequelize, mapper: ICustomerInvoiceMapper) {
super(database); super(database);
this.model = database.model("CustomerInvoice") as typeof CustomerInvoiceModel; //CustomerInvoice = database.model("CustomerInvoice") as typeof CustomerInvoiceModel;
this.mapper = mapper; this.mapper = mapper;
} }
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(errorMapper.toDomainError(err));
}
}
/** /**
* *
* Persiste una nueva factura o actualiza una existente. * Persiste una nueva factura o actualiza una existente.
@ -35,7 +45,7 @@ export class CustomerInvoiceRepository
): Promise<Result<CustomerInvoice, Error>> { ): Promise<Result<CustomerInvoice, Error>> {
try { try {
const data = this.mapper.mapToPersistence(invoice); const data = this.mapper.mapToPersistence(invoice);
await this.model.upsert(data, { transaction }); await CustomerInvoiceModel.upsert(data, { transaction });
return Result.ok(invoice); return Result.ok(invoice);
} catch (err: unknown) { } catch (err: unknown) {
return Result.fail(errorMapper.toDomainError(err)); return Result.fail(errorMapper.toDomainError(err));
@ -51,7 +61,7 @@ export class CustomerInvoiceRepository
*/ */
async findById(id: UniqueID, transaction: Transaction): Promise<Result<CustomerInvoice, Error>> { async findById(id: UniqueID, transaction: Transaction): Promise<Result<CustomerInvoice, Error>> {
try { try {
const rawData = await this._findById(this.model, id.toString(), { transaction }); const rawData = await this._findById(CustomerInvoiceModel, id.toString(), { transaction });
if (!rawData) { if (!rawData) {
return Result.fail(new Error(`Invoice with id ${id} not found.`)); return Result.fail(new Error(`Invoice with id ${id} not found.`));
@ -80,7 +90,7 @@ export class CustomerInvoiceRepository
const converter = new CriteriaToSequelizeConverter(); const converter = new CriteriaToSequelizeConverter();
const query = converter.convert(criteria); const query = converter.convert(criteria);
const instances = await this.model.findAll({ const instances = await CustomerInvoiceModel.findAll({
...query, ...query,
transaction, transaction,
}); });
@ -100,7 +110,7 @@ export class CustomerInvoiceRepository
*/ */
async deleteById(id: UniqueID, transaction: any): Promise<Result<void, Error>> { async deleteById(id: UniqueID, transaction: any): Promise<Result<void, Error>> {
try { try {
await this._deleteById(this.model, id, false, transaction); await this._deleteById(CustomerInvoiceModel, id, false, transaction);
return Result.ok<void>(); return Result.ok<void>();
} catch (err: unknown) { } catch (err: unknown) {
return Result.fail(errorMapper.toDomainError(err)); return Result.fail(errorMapper.toDomainError(err));

View File

@ -0,0 +1,13 @@
import * as z from "zod/v4";
/**
* Este DTO es utilizado por el endpoint:
* `GET /customer-invoices/:id` (consultar una factura por ID).
*
*/
export const GetCustomerInvoiceByIdQuerySchema = z.object({
id: z.string(),
});
export type GetCustomerInvoiceByIdQueryDTO = z.infer<typeof GetCustomerInvoiceByIdQuerySchema>;

View File

@ -1,2 +1,3 @@
export * from "./create-customer-invoice.command.dto"; export * from "./create-customer-invoice.command.dto";
export * from "./get-customer-invoice.query.dto";
export * from "./list-customer-invoices.query.dto"; export * from "./list-customer-invoices.query.dto";

View File

@ -0,0 +1,17 @@
import { MetadataSchema } from "@erp/core";
import * as z from "zod/v4";
export const GetCustomerInvoiceResultSchema = z.object({
id: z.uuid(),
invoice_status: z.string(),
invoice_number: z.string(),
invoice_series: z.string(),
issue_date: z.iso.datetime({ offset: true }),
operation_date: z.iso.datetime({ offset: true }),
language_code: z.string(),
currency: z.string(),
metadata: MetadataSchema.optional(),
});
export type GetCustomerInvoiceResultDTO = z.infer<typeof GetCustomerInvoiceResultSchema>;

View File

@ -1,2 +1,3 @@
export * from "./customer-invoice-creation.result.dto"; export * from "./customer-invoice-creation.result.dto";
export * from "./get-customer-invoice.result.dto";
export * from "./list-customer-invoices.result.dto"; export * from "./list-customer-invoices.result.dto";

View File

@ -75,7 +75,7 @@ importers:
specifier: ^9.0.2 specifier: ^9.0.2
version: 9.0.2 version: 9.0.2
luxon: luxon:
specifier: ^3.5.0 specifier: ^3.6.1
version: 3.6.1 version: 3.6.1
module-alias: module-alias:
specifier: ^2.2.3 specifier: ^2.2.3
@ -151,7 +151,7 @@ importers:
specifier: ^9.0.8 specifier: ^9.0.8
version: 9.0.10 version: 9.0.10
'@types/luxon': '@types/luxon':
specifier: ^3.4.2 specifier: ^3.6.2
version: 3.6.2 version: 3.6.2
'@types/node': '@types/node':
specifier: ^22.15.12 specifier: ^22.15.12