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

View File

@ -105,7 +105,8 @@ const server = http
// Manejo de promesas no capturadas
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
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 { ConflictApiError } from "./conflict-api-error";
import { DomainValidationError } from "./domain-validation-error";
import { DuplicateEntityError } from "./duplicate-entity-error";
import { ForbiddenApiError } from "./forbidden-api-error";
import { InternalApiError } from "./internal-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 }]);
}
if (error instanceof DuplicateEntityError) {
return new ConflictApiError(error.message);
}
// 3. 🔍 Errores individuales de validación
if (
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 "./duplicate-entity-error";
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-error-collection";

View File

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

View File

@ -41,7 +41,8 @@ export abstract class SequelizeMapper<
source: TModel[],
params?: MapperParamsType
): Result<Collection<TEntity>, Error> {
return this.mapArrayAndCountToDomain(source, source.length, params);
const items = source ?? [];
return this.mapArrayAndCountToDomain(items, items.length, params);
}
public mapArrayAndCountToDomain(
@ -49,12 +50,14 @@ export abstract class SequelizeMapper<
totalCount: number,
params?: MapperParamsType
): Result<Collection<TEntity>, Error> {
const _source = source ?? [];
try {
if (source.length === 0) {
if (_source.length === 0) {
return Result.ok(new Collection([], totalCount));
}
const items = source.map(
const items = _source.map(
(value, index) => this.mapToDomain(value, { index, ...params }).data
);
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 { Result } from "@repo/rdx-utils";
import { Transaction } from "sequelize";
@ -8,19 +8,21 @@ import { CreateCustomerInvoicesPresenter } from "./presenter";
export class CreateCustomerInvoiceUseCase {
constructor(
private readonly customerInvoiceService: ICustomerInvoiceService,
private readonly service: ICustomerInvoiceService,
private readonly transactionManager: ITransactionManager,
private readonly presenter: CreateCustomerInvoicesPresenter
) {}
public execute(dto: CreateCustomerInvoiceCommandDTO) {
const invoicePropOrError = mapDTOToCustomerInvoiceProps(dto);
const invoicePropsOrError = mapDTOToCustomerInvoiceProps(dto);
if (invoicePropOrError.isFailure) {
return Result.fail(invoicePropOrError.error);
if (invoicePropsOrError.isFailure) {
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) {
return Result.fail(invoiceOrError.error);
@ -29,13 +31,27 @@ export class CreateCustomerInvoiceUseCase {
const newInvoice = invoiceOrError.data;
return this.transactionManager.complete(async (transaction: Transaction) => {
const result = await this.customerInvoiceService.save(newInvoice, transaction);
if (result.isFailure) {
return Result.fail(result.error);
}
try {
const duplicateCheck = await this.service.existsById(id, transaction);
const viewDTO = this.presenter.toDTO(newInvoice);
return Result.ok(viewDTO);
if (duplicateCheck.isFailure) {
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 "@/core/common/infrastructure/database";
import { logger } from "@/lib/logger";
import { ITransactionManager } from "@erp/core/api";
import { GetCustomerInvoiceByIdQueryDTO } from "@erp/customer-invoices/common/dto";
import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { CustomerInvoice, ICustomerInvoiceService } from "../domain";
import { ICustomerInvoiceService } from "../../domain";
import { GetCustomerInvoicePresenter } from "./presenter";
export class GetCustomerInvoiceUseCase {
constructor(
private readonly customerInvoiceService: ICustomerInvoiceService,
private readonly transactionManager: ITransactionManager
private readonly service: ICustomerInvoiceService,
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) => {
try {
return await this.customerInvoiceService.findCustomerInvoiceById(
customerInvoiceID,
transaction
);
const invoiceOrError = await this.service.getById(idOrError.data, transaction);
if (invoiceOrError.isFailure) {
return Result.fail(invoiceOrError.error);
}
const getDTO = this.presenter.toDTO(invoiceOrError.data);
return Result.ok(getDTO);
} catch (error: unknown) {
logger.error(error as Error);
return Result.fail(error as Error);
}
});

View File

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

View File

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

View File

@ -1,5 +1,5 @@
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 { CreateCustomerInvoiceCommandDTO } from "../../../common/dto";
import {
@ -16,16 +16,15 @@ import { mapDTOToCustomerInvoiceItemsProps } from "./map-dto-to-customer-invoice
* No construye directamente el agregado.
*
* @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(
dto: CreateCustomerInvoiceCommandDTO
): Result<CustomerInvoiceProps, Error> {
export function mapDTOToCustomerInvoiceProps(dto: CreateCustomerInvoiceCommandDTO) {
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(
CustomerInvoiceNumber.create(dto.invoice_number),
@ -66,7 +65,7 @@ export function mapDTOToCustomerInvoiceProps(
currency,
};
return Result.ok(invoiceProps);
return Result.ok({ id: invoiceId!, props: invoiceProps });
/*if (hasNoUndefinedFields(invoiceProps)) {
const invoiceOrError = CustomerInvoice.create(invoiceProps, invoiceId);

View File

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

View File

@ -16,10 +16,7 @@ export class ListCustomerInvoicesUseCase {
public execute(criteria: Criteria): Promise<Result<ListCustomerInvoicesResultDTO, Error>> {
return this.transactionManager.complete(async (transaction: Transaction) => {
try {
const result = await this.customerInvoiceService.findCustomerInvoices(
criteria,
transaction
);
const result = await this.customerInvoiceService.findByCriteria(criteria, transaction);
if (result.isFailure) {
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 { Collection } from "@repo/rdx-utils";
import { ListCustomerInvoicesResultDTO } from "../../../../common/dto";
import { CustomerInvoice } from "../../../domain";
export interface ListCustomerInvoicesPresenter {
toDTO: (
customerInvoices: Collection<CustomerInvoice>,
criteria: Criteria
) => ListCustomerInvoicesViewDTO;
) => ListCustomerInvoicesResultDTO;
}
export const listCustomerInvoicesPresenter: ListCustomerInvoicesPresenter = {
toDTO: (
customerInvoices: Collection<CustomerInvoice>,
criteria: Criteria
): ListCustomerInvoicesViewDTO => {
): ListCustomerInvoicesResultDTO => {
const items = customerInvoices.map((invoice) => {
return {
id: invoice.id.toPrimitive(),
@ -25,7 +25,7 @@ export const listCustomerInvoicesPresenter: ListCustomerInvoicesPresenter = {
issue_date: invoice.issueDate.toISOString(),
operation_date: invoice.operationDate.toISOString(),
language_code: "ES",
currency: invoice.customerInvoiceCurrency.toString(),
currency: "EUR",
subtotal_price: invoice.calculateSubtotal().toPrimitive(),
total_price: invoice.calculateTotal().toPrimitive(),

View File

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

View File

@ -1,47 +1,30 @@
import { ExpressController } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { ExpressController, errorMapper } from "@erp/core/api";
import { GetCustomerInvoiceUseCase } from "../../application";
import { IGetCustomerInvoicePresenter } from "./presenter";
export class GetCustomerInvoiceController extends ExpressController {
public constructor(
private readonly getCustomerInvoice: GetCustomerInvoiceUseCase,
private readonly presenter: IGetCustomerInvoicePresenter
) {
public constructor(private readonly getCustomerInvoice: GetCustomerInvoiceUseCase) {
super();
}
protected async executeImpl() {
const { customerInvoiceId } = this.req.params;
const { id } = this.req.params;
// Validar ID
const customerInvoiceIdOrError = UniqueID.create(customerInvoiceId);
if (customerInvoiceIdOrError.isFailure)
return this.invalidInputError("CustomerInvoice ID not valid");
/*
const user = this.req.user; // asumimos middleware authenticateJWT inyecta user
const customerInvoiceOrError = await this.getCustomerInvoice.execute(
customerInvoiceIdOrError.data
);
if (!user || !user.companyId) {
this.unauthorized(res, "Unauthorized: user or company not found");
return;
}
*/
if (customerInvoiceOrError.isFailure) {
return this.handleError(customerInvoiceOrError.error);
const result = await this.getCustomerInvoice.execute({ id });
if (result.isFailure) {
const apiError = errorMapper.toApiError(result.error);
return this.handleApiError(apiError);
}
return this.ok(this.presenter.toDTO(customerInvoiceOrError.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);
return this.ok(result.data);
}
}

View File

@ -1,19 +1,21 @@
import { SequelizeTransactionManager } from "@erp/core/api";
import { Sequelize } from "sequelize";
import { GetCustomerInvoiceUseCase, getCustomerInvoicePresenter } from "../../application";
import { CustomerInvoiceService } from "../../domain";
import { CustomerInvoiceRepository, customerInvoiceMapper } from "../../infrastructure";
import { GetCustomerInvoiceUseCase } from "../../application";
import { GetCustomerInvoiceController } from "./get-invoice.controller";
import { getCustomerInvoicePresenter } from "./presenter";
export const buildGetCustomerInvoiceController = (database: Sequelize) => {
const transactionManager = new SequelizeTransactionManager(database);
const customerInvoiceRepository = new CustomerInvoiceRepository(database, customerInvoiceMapper);
const customerInvoiceService = new CustomerInvoiceService(customerInvoiceRepository);
const useCase = new GetCustomerInvoiceUseCase(customerInvoiceService, transactionManager);
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 "./entities";
export * from "./errors";
export * from "./repositories";
export * from "./services";
export * from "./value-objects";

View File

@ -4,6 +4,8 @@ import { Collection, Result } from "@repo/rdx-utils";
import { CustomerInvoice } from "../aggregates";
export interface ICustomerInvoiceRepository {
existsById(id: UniqueID, transaction?: any): Promise<Result<boolean, Error>>;
/**
*
* 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";
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>>;
existsById(id: UniqueID, transaction?: any): Promise<Result<boolean, Error>>;
findByCriteria(
criteria: Criteria,
transaction?: any

View File

@ -13,10 +13,11 @@ export class CustomerInvoiceService implements ICustomerInvoiceService {
* Construye un nuevo agregado CustomerInvoice a partir de props validadas.
*
* @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.
*/
build(props: CustomerInvoiceProps): Result<CustomerInvoice, Error> {
return CustomerInvoice.create(props);
build(props: CustomerInvoiceProps, id?: UniqueID): Result<CustomerInvoice, Error> {
return CustomerInvoice.create(props, id);
}
/**
@ -31,6 +32,19 @@ export class CustomerInvoiceService implements ICustomerInvoiceService {
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.
*
@ -62,7 +76,7 @@ export class CustomerInvoiceService implements ICustomerInvoiceService {
* @returns Result<CustomerInvoice, Error> - Factura encontrada o error.
*/
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 {
CreateCustomerInvoiceCommandSchema,
GetCustomerInvoiceByIdQuerySchema,
ListCustomerInvoicesQuerySchema,
} from "../../../common/dto";
import {
buildCreateCustomerInvoicesController,
buildGetCustomerInvoiceController,
buildListCustomerInvoicesController,
} from "../../controllers";
@ -24,21 +26,21 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
"/",
//checkTabContext,
//checkUser,
validateRequest(ListCustomerInvoicesQuerySchema, "query"),
validateRequest(ListCustomerInvoicesQuerySchema, "params"),
(req: Request, res: Response, next: NextFunction) => {
buildListCustomerInvoicesController(database).execute(req, res, next);
}
);
/*routes.get(
"/:customerInvoiceId",
routes.get(
"/:id",
//checkTabContext,
//checkUser,
validateRequest(GetCustomerInvoiceByIdQuerySchema, "query"),
validateRequest(GetCustomerInvoiceByIdQuerySchema, "params"),
(req: Request, res: Response, next: NextFunction) => {
buildGetCustomerInvoiceController(database).execute(req, res, next);
}
);*/
);
routes.post(
"/",

View File

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

View File

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

View File

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