Facturas de cliente

This commit is contained in:
David Arranz 2025-06-26 13:32:55 +02:00
parent 4a73456c46
commit dba421e9c5
27 changed files with 477 additions and 474 deletions

View File

@ -32,7 +32,8 @@
"noExplicitAny": "info"
},
"style": {
"useImportType": "off"
"useImportType": "off",
"noNonNullAssertion": "info"
}
}
},

View File

@ -1,4 +1,10 @@
import { ConnectionError, UniqueConstraintError } from "sequelize";
import {
ConnectionError,
DatabaseError,
ForeignKeyConstraintError,
UniqueConstraintError,
ValidationError,
} from "sequelize";
import { ApiError } from "./api-error";
import { ConflictApiError } from "./conflict-api-error";
@ -11,26 +17,50 @@ import { UnavailableApiError } from "./unavailable-api-error";
import { ValidationApiError } from "./validation-api-error";
import { ValidationErrorCollection } from "./validation-error-collection";
/**
* Mapea errores de la aplicación a errores de la API.
*
* Esta función toma un error de la aplicación y lo convierte en un objeto ApiError
* adecuado para enviar como respuesta HTTP. Maneja errores comunes como validación,
* conflictos, no encontrados, autenticación y errores de infraestructura.
*
* @param error - El error de la aplicación a mapear.
* @returns Un objeto ApiError que representa el error mapeado.
* @example
* const error = new Error("Invalid input");
* const apiError = errorMapper.toApiError(error);
* console.log(apiError);
* // Output: ValidationApiError { status: 422, title: 'Validation Failed', detail: 'Invalid input', type: 'https://httpstatuses.com/422' }
* @throws {ApiError} Si el error no puede ser mapeado a un tipo conocido.
* @see ApiError
* @see ValidationApiError
*/
export const errorMapper = {
toDomainError(error: unknown): Error {
if (error instanceof UniqueConstraintError) {
const field = error.errors[0]?.path || "unknown_field";
return new Error(`A record with this ${field} already exists.`);
}
if (error instanceof ForeignKeyConstraintError) {
return new Error("A referenced entity was not found or is invalid.");
}
if (error instanceof ValidationError) {
return new Error(`Invalid data provided: ${error.message}`);
}
if (error instanceof DatabaseError) {
return new Error("Database error occurred.");
}
if (error instanceof Error) {
return error; // Fallback a error estándar
}
return new Error("Unknown persistence error.");
},
/**
* Mapea errores de la aplicación a errores de la API.
*
* Esta función toma un error de la aplicación y lo convierte en un objeto ApiError
* adecuado para enviar como respuesta HTTP. Maneja errores comunes como validación,
* conflictos, no encontrados, autenticación y errores de infraestructura.
*
* @param error - El error de la aplicación a mapear.
* @returns Un objeto ApiError que representa el error mapeado.
* @example
* const error = new Error("Invalid input");
* const apiError = errorMapper.toApiError(error);
* console.log(apiError);
* // Output: ValidationApiError { status: 422, title: 'Validation Failed', detail: 'Invalid input', type: 'https://httpstatuses.com/422' }
* @throws {ApiError} Si el error no puede ser mapeado a un tipo conocido.
* @see ApiError
* @see ValidationApiError
*/
toApiError: (error: Error): ApiError => {
const message = error.message || "An unexpected error occurred";

View File

@ -1,149 +1,41 @@
import { ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { CreateCustomerInvoiceCommandDTO } from "@erp/customer-invoices/common/dto";
import { Result } from "@repo/rdx-utils";
import { Transaction } from "sequelize";
import { ICreateCustomerInvoiceRequestDTO } from "../../common/dto";
import { ICustomerInvoiceProps, ICustomerInvoiceService } from "../domain";
import { ICustomerInvoiceService } from "../../domain";
import { mapDTOToCustomerInvoiceProps } from "../helpers";
import { CreateCustomerInvoicesPresenter } from "./presenter";
export class CreateCustomerInvoiceUseCase {
constructor(
private readonly customerInvoiceService: ICustomerInvoiceService,
private readonly transactionManager: ITransactionManager
private readonly transactionManager: ITransactionManager,
private readonly presenter: CreateCustomerInvoicesPresenter
) {}
public execute(customerInvoiceID: UniqueID, data: ICreateCustomerInvoiceRequestDTO) {
public execute(dto: CreateCustomerInvoiceCommandDTO) {
const invoicePropOrError = mapDTOToCustomerInvoiceProps(dto);
if (invoicePropOrError.isFailure) {
return Result.fail(invoicePropOrError.error);
}
const invoiceOrError = this.customerInvoiceService.build(invoicePropOrError.data);
if (invoiceOrError.isFailure) {
return Result.fail(invoiceOrError.error);
}
const newInvoice = invoiceOrError.data;
return this.transactionManager.complete(async (transaction: Transaction) => {
try {
/*const validOrErrors = this.validateCustomerInvoiceData(dto);
if (validOrErrors.isFailure) {
return Result.fail(validOrErrors.error);
}
const data = validOrErrors.data;*/
const invoiceProps: ICustomerInvoiceProps = {
customerInvoiceNumber: data.customerInvoice_number,
customerInvoiceSeries: data.customerInvoice_series,
issueDate: data.issue_date,
operationDate: data.operation_date,
customerInvoiceCurrency: data.currency,
};
// Update customerInvoice with dto
return await this.customerInvoiceService.createCustomerInvoice(
customerInvoiceID,
invoiceProps,
transaction
);
} catch (error: unknown) {
logger.error(error as Error);
return Result.fail(error as Error);
const result = await this.customerInvoiceService.save(newInvoice, transaction);
if (result.isFailure) {
return Result.fail(result.error);
}
const viewDTO = this.presenter.toDTO(newInvoice);
return Result.ok(viewDTO);
});
}
private validateCustomerInvoiceData(
dto: ICreateCustomerInvoiceRequestDTO
): Result<ICustomerInvoiceProps, Error> {
const errors: Error[] = [];
const customerInvoiceNumerOrError = CustomerInvoiceNumber.create(dto.customerInvoice_number);
const customerInvoiceSeriesOrError = CustomerInvoiceSerie.create(dto.customerInvoice_series);
const issueDateOrError = UtcDate.create(dto.issue_date);
const operationDateOrError = UtcDate.create(dto.operation_date);
const result = Result.combine([
customerInvoiceNumerOrError,
customerInvoiceSeriesOrError,
issueDateOrError,
operationDateOrError,
]);
if (result.isFailure) {
return Result.fail(result.error);
}
const validatedData: ICustomerInvoiceProps = {
status: CustomerInvoiceStatus.createDraft(),
customerInvoiceNumber: customerInvoiceNumerOrError.data,
customerInvoiceSeries: customerInvoiceSeriesOrError.data,
issueDate: issueDateOrError.data,
operationDate: operationDateOrError.data,
customerInvoiceCurrency: dto.currency,
};
/*if (errors.length > 0) {
const message = errors.map((err) => err.message).toString();
return Result.fail(new Error(message));
}*/
return Result.ok(validatedData);
/*let customerInvoice_status = CustomerInvoiceStatus.create(dto.status).object;
if (customerInvoice_status.isEmpty()) {
customerInvoice_status = CustomerInvoiceStatus.createDraft();
}
let customerInvoice_series = CustomerInvoiceSeries.create(dto.customerInvoice_series).object;
if (customerInvoice_series.isEmpty()) {
customerInvoice_series = CustomerInvoiceSeries.create(dto.customerInvoice_series).object;
}
let issue_date = CustomerInvoiceDate.create(dto.issue_date).object;
if (issue_date.isEmpty()) {
issue_date = CustomerInvoiceDate.createCurrentDate().object;
}
let operation_date = CustomerInvoiceDate.create(dto.operation_date).object;
if (operation_date.isEmpty()) {
operation_date = CustomerInvoiceDate.createCurrentDate().object;
}
let customerInvoiceCurrency = Currency.createFromCode(dto.currency).object;
if (customerInvoiceCurrency.isEmpty()) {
customerInvoiceCurrency = Currency.createDefaultCode().object;
}
let customerInvoiceLanguage = Language.createFromCode(dto.language_code).object;
if (customerInvoiceLanguage.isEmpty()) {
customerInvoiceLanguage = Language.createDefaultCode().object;
}
const items = new Collection<CustomerInvoiceItem>(
dto.items?.map(
(item) =>
CustomerInvoiceSimpleItem.create({
description: Description.create(item.description).object,
quantity: Quantity.create(item.quantity).object,
unitPrice: UnitPrice.create({
amount: item.unit_price.amount,
currencyCode: item.unit_price.currency,
precision: item.unit_price.precision,
}).object,
}).object
)
);
if (!customerInvoice_status.isDraft()) {
throw Error("Error al crear una factura que no es borrador");
}
return DraftCustomerInvoice.create(
{
customerInvoiceSeries: customerInvoice_series,
issueDate: issue_date,
operationDate: operation_date,
customerInvoiceCurrency,
language: customerInvoiceLanguage,
customerInvoiceNumber: CustomerInvoiceNumber.create(undefined).object,
//notes: Note.create(customerInvoiceDTO.notes).object,
//senderId: UniqueID.create(null).object,
recipient,
items,
},
customerInvoiceId
);*/
}
}

View File

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

View File

@ -0,0 +1,27 @@
import { CustomerInvoice } from "@erp/customer-invoices/api/domain";
import { CustomerInvoicesCreationResultDTO } from "@erp/customer-invoices/common/dto";
export class CreateCustomerInvoicesPresenter {
public toDTO(invoice: CustomerInvoice): CustomerInvoicesCreationResultDTO {
return {
id: invoice.id.toPrimitive(),
invoice_status: invoice.status.toString(),
invoice_number: invoice.invoiceNumber.toString(),
invoice_series: invoice.invoiceSeries.toString(),
issue_date: invoice.issueDate.toISOString(),
operation_date: invoice.operationDate.toISOString(),
language_code: "ES",
currency: "EUR",
//subtotal_price: invoice.calculateSubtotal().toPrimitive(),
//total_price: invoice.calculateTotal().toPrimitive(),
//recipient: CustomerInvoiceParticipantPresenter(customerInvoice.recipient),
metadata: {
entity: "customer-invoice",
},
};
}
}

View File

@ -0,0 +1 @@
export * from "./create-customer-invoices.presenter";

View File

@ -1,54 +0,0 @@
import { ValidationErrorCollection, ValidationErrorDetail } from "@erp/core/api";
import { UniqueID, UtcDate } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { CreateCustomerInvoiceCommandDTO } from "../../../common/dto";
import { CustomerInvoiceNumber, CustomerInvoiceProps, CustomerInvoiceSerie } from "../../domain";
import { buildInvoiceItemsFromDTO } from "./build-customer-invoice-items-from-dto";
import { extractOrPushError } from "./extract-or-push-error";
export async function buildInvoiceFromDTO(
dto: CreateCustomerInvoiceCommandDTO
): Promise<Result<CustomerInvoiceProps, Error>> {
const errors: ValidationErrorDetail[] = [];
const invoiceNumber = extractOrPushError(
CustomerInvoiceNumber.create(dto.invoice_number),
"invoice_number",
errors
);
const invoiceSeries = extractOrPushError(
CustomerInvoiceSerie.create(dto.invoice_series),
"invoice_series",
errors
);
const issueDate = extractOrPushError(UtcDate.createFromISO(dto.issue_date), "issue_date", errors);
const operationDate = extractOrPushError(
UtcDate.createFromISO(dto.operation_date),
"operation_date",
errors
);
// 🔄 Validar y construir los items de factura con helper especializado
const itemsResult = await buildInvoiceItemsFromDTO(dto.items);
if (itemsResult.isFailure) {
return Result.fail(itemsResult.error);
}
if (errors.length > 0) {
return Result.fail(new ValidationErrorCollection(errors));
}
return Result.ok({
id: UniqueID.create(),
customerId: customerId.data,
invoiceNumber: invoiceNumber.data,
invoiceSeries: invoiceSeries.data,
issueDate: issueDate.data,
operationDate: operationDate.data,
subtotalPrice: subtotalPrice.data,
discount: discount.data,
tax: tax.data,
totalAmount: totalAmount.data,
lines: itemsResult.data,
});
}

View File

@ -24,3 +24,27 @@ export function hasNoUndefinedFields<T extends Record<string, any>>(
): obj is { [K in keyof T]-?: Exclude<T[K], undefined> } {
return Object.values(obj).every((value) => value !== undefined);
}
/**
*
* @description Verifica si un objeto tiene campos con valor undefined.
* Esta función es el complemento de `hasNoUndefinedFields`.
*
* @example
* const obj = { a: 1, b: 'test', c: null };
* console.log(hasUndefinedFields(obj)); // false
*
* const objWithUndefined = { a: 1, b: undefined, c: null };
* console.log(hasUndefinedFields(objWithUndefined)); // true
*
* @template T - El tipo del objeto.
* @param obj - El objeto a evaluar.
* @returns true si el objeto tiene al menos un campo undefined, false en caso contrario.
*
*/
export function hasUndefinedFields<T extends Record<string, any>>(
obj: T
): obj is { [K in keyof T]-?: Exclude<T[K], undefined> } {
return !hasNoUndefinedFields(obj);
}

View File

@ -1 +1 @@
export * from "./build-customer-invoice-from-dto";
export * from "./map-dto-to-customer-invoice-props";

View File

@ -11,7 +11,7 @@ import {
import { extractOrPushError } from "./extract-or-push-error";
import { hasNoUndefinedFields } from "./has-no-undefined-fields";
export function buildInvoiceItemsFromDTO(
export function mapDTOToCustomerInvoiceItemsProps(
dtoItems: Pick<CreateCustomerInvoiceCommandDTO, "items">["items"]
): Result<CustomerInvoiceItem[], ValidationErrorCollection> {
const errors: ValidationErrorDetail[] = [];

View File

@ -0,0 +1,84 @@
import { ValidationErrorCollection, ValidationErrorDetail } from "@erp/core/api";
import { UtcDate } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { CreateCustomerInvoiceCommandDTO } from "../../../common/dto";
import {
CustomerInvoiceNumber,
CustomerInvoiceProps,
CustomerInvoiceSerie,
CustomerInvoiceStatus,
} from "../../domain";
import { extractOrPushError } from "./extract-or-push-error";
import { mapDTOToCustomerInvoiceItemsProps } from "./map-dto-to-customer-invoice-items-props";
/**
* Convierte el DTO a las props validadas (CustomerInvoiceProps).
* 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
*
*/
export function mapDTOToCustomerInvoiceProps(
dto: CreateCustomerInvoiceCommandDTO
): Result<CustomerInvoiceProps, Error> {
const errors: ValidationErrorDetail[] = [];
//const invoiceId = extractOrPushError(UniqueID.create(dto.id), "invoice_id", errors);
const invoiceNumber = extractOrPushError(
CustomerInvoiceNumber.create(dto.invoice_number),
"invoice_number",
errors
);
const invoiceSeries = extractOrPushError(
CustomerInvoiceSerie.create(dto.invoice_series),
"invoice_series",
errors
);
const issueDate = extractOrPushError(UtcDate.createFromISO(dto.issue_date), "issue_date", errors);
const operationDate = extractOrPushError(
UtcDate.createFromISO(dto.operation_date),
"operation_date",
errors
);
//const currency = extractOrPushError(Currency.(dto.currency), "currency", errors);
const currency = dto.currency;
// 🔄 Validar y construir los items de factura con helper especializado
const itemsResult = mapDTOToCustomerInvoiceItemsProps(dto.items);
if (itemsResult.isFailure) {
return Result.fail(itemsResult.error);
}
if (errors.length > 0) {
return Result.fail(new ValidationErrorCollection(errors));
}
const invoiceProps: CustomerInvoiceProps = {
invoiceNumber: invoiceNumber!,
invoiceSeries: invoiceSeries!,
issueDate: issueDate!,
operationDate: operationDate!,
status: CustomerInvoiceStatus.createDraft(),
currency,
};
return Result.ok(invoiceProps);
/*if (hasNoUndefinedFields(invoiceProps)) {
const invoiceOrError = CustomerInvoice.create(invoiceProps, invoiceId);
if (invoiceOrError.isFailure) {
return Result.fail(invoiceOrError.error);
}
return Result.ok(invoiceOrError.data);
}
return Result.fail(
new ValidationErrorCollection([
{ path: "", message: "Error building from DTO: Some fields are undefined" },
])
);*/
}

View File

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

View File

@ -20,8 +20,8 @@ export const listCustomerInvoicesPresenter: ListCustomerInvoicesPresenter = {
id: invoice.id.toPrimitive(),
invoice_status: invoice.status.toString(),
invoice_number: invoice.customerInvoiceNumber.toString(),
invoice_series: invoice.customerInvoiceSeries.toString(),
invoice_number: invoice.invoiceNumber.toString(),
invoice_series: invoice.invoiceSeries.toString(),
issue_date: invoice.issueDate.toISOString(),
operation_date: invoice.operationDate.toISOString(),
language_code: "ES",

View File

@ -3,8 +3,7 @@ import { CreateCustomerInvoiceCommandDTO } from "../../../common/dto";
export class CreateCustomerInvoiceController extends ExpressController {
public constructor(
private readonly createCustomerInvoice: any, // Replace with actual type
private readonly presenter: any // Replace with actual type
private readonly createCustomerInvoice: any // Replace with actual type
) {
super();
}
@ -26,14 +25,10 @@ export class CreateCustomerInvoiceController extends ExpressController {
const result = await this.createCustomerInvoice.execute(dto);
if (result.isFailure) {
/*if (error instanceof AggregatedValidationError) {
return this.invalidInputError(error.message, error.details);
}*/
const apiError = errorMapper.toApiError(result.error);
return this.handleApiError(apiError);
}
return this.created(this.presenter.toDTO(result.data));
return this.created(result.data);
}
}

View File

@ -1 +1,21 @@
export * from "./create-customer-invoice";
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 { CreateCustomerInvoiceController } from "./create-customer-invoice";
export const buildCreateCustomerInvoicesController = (database: Sequelize) => {
const transactionManager = new SequelizeTransactionManager(database);
const customerInvoiceRepository = new CustomerInvoiceRepository(database, customerInvoiceMapper);
const customerInvoiceService = new CustomerInvoiceService(customerInvoiceRepository);
const presenter = new CreateCustomerInvoicesPresenter();
const useCase = new CreateCustomerInvoiceUseCase(
customerInvoiceService,
transactionManager,
presenter
);
return new CreateCustomerInvoiceController(useCase);
};

View File

@ -10,8 +10,8 @@ export const getCustomerInvoicePresenter: IGetCustomerInvoicePresenter = {
id: customerInvoice.id.toPrimitive(),
customerInvoice_status: customerInvoice.status.toString(),
customerInvoice_number: customerInvoice.customerInvoiceNumber.toString(),
customerInvoice_series: customerInvoice.customerInvoiceSeries.toString(),
customerInvoice_number: customerInvoice.invoiceNumber.toString(),
customerInvoice_series: customerInvoice.invoiceSeries.toString(),
issue_date: customerInvoice.issueDate.toDateString(),
operation_date: customerInvoice.operationDate.toDateString(),
language_code: "ES",

View File

@ -24,10 +24,6 @@ export class ListCustomerInvoicesController extends ExpressController {
const result = await this.listCustomerInvoices.execute(criteria);
if (result.isFailure) {
/*if (error instanceof AggregatedValidationError) {
return this.invalidInputError(error.message, error.details);
}*/
const apiError = errorMapper.toApiError(result.error);
return this.handleApiError(apiError);
}

View File

@ -8,8 +8,8 @@ import {
} from "../value-objects";
export interface CustomerInvoiceProps {
customerInvoiceNumber: CustomerInvoiceNumber;
customerInvoiceSeries: CustomerInvoiceSerie;
invoiceNumber: CustomerInvoiceNumber;
invoiceSeries: CustomerInvoiceSerie;
status: CustomerInvoiceStatus;
@ -19,7 +19,7 @@ export interface CustomerInvoiceProps {
//dueDate: UtcDate; // ? --> depende de la forma de pago
//tax: Tax; // ? --> detalles?
customerInvoiceCurrency: string;
currency: string;
//language: Language;
@ -37,8 +37,8 @@ export interface CustomerInvoiceProps {
export interface ICustomerInvoice {
id: UniqueID;
customerInvoiceNumber: CustomerInvoiceNumber;
customerInvoiceSeries: CustomerInvoiceSerie;
invoiceNumber: CustomerInvoiceNumber;
invoiceSeries: CustomerInvoiceSerie;
status: CustomerInvoiceStatus;
@ -53,7 +53,7 @@ export interface ICustomerInvoice {
//tax: Tax;
//language: Language;
customerInvoiceCurrency: string;
currency: string;
//purchareOrderNumber: string;
//notes: Note;
@ -77,7 +77,6 @@ export class CustomerInvoice
protected constructor(props: CustomerInvoiceProps, id?: UniqueID) {
super(props, id);
this._items = props.items || CustomerInvoiceItems.create();
}
@ -95,12 +94,12 @@ export class CustomerInvoice
return Result.ok(customerInvoice);
}
get customerInvoiceNumber() {
return this.props.customerInvoiceNumber;
get invoiceNumber() {
return this.props.invoiceNumber;
}
get customerInvoiceSeries() {
return this.props.customerInvoiceSeries;
get invoiceSeries() {
return this.props.invoiceSeries;
}
get issueDate() {
@ -159,8 +158,8 @@ export class CustomerInvoice
return this.props.shipTo;
}*/
get customerInvoiceCurrency() {
return this.props.customerInvoiceCurrency;
get currency() {
return this.props.currency;
}
/*get notes() {
@ -183,7 +182,7 @@ export class CustomerInvoice
calculateSubtotal(): MoneyValue {
const customerInvoiceSubtotal = MoneyValue.create({
amount: 0,
currency_code: this.props.customerInvoiceCurrency,
currency_code: this.props.currency,
scale: 2,
}).data;
@ -196,7 +195,7 @@ export class CustomerInvoice
calculateTaxTotal(): MoneyValue {
const taxTotal = MoneyValue.create({
amount: 0,
currency_code: this.props.customerInvoiceCurrency,
currency_code: this.props.currency,
scale: 2,
}).data;

View File

@ -4,13 +4,46 @@ import { Collection, Result } from "@repo/rdx-utils";
import { CustomerInvoice } from "../aggregates";
export interface ICustomerInvoiceRepository {
findAll(
criteria: Criteria,
transaction?: any
): Promise<Result<Collection<CustomerInvoice>, Error>>;
getById(id: UniqueID, transaction?: any): Promise<Result<CustomerInvoice, Error>>;
deleteById(id: UniqueID, transaction?: any): Promise<Result<boolean, Error>>;
/**
*
* Persiste una nueva factura o actualiza una existente.
*
* @param invoice - El agregado a guardar.
* @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice, Error>
*/
save(invoice: CustomerInvoice, transaction: any): Promise<Result<CustomerInvoice, Error>>;
create(customerInvoice: CustomerInvoice, transaction?: any): Promise<void>;
update(customerInvoice: CustomerInvoice, transaction?: any): Promise<void>;
}
/**
*
* Busca una factura por su identificador único.
* @param id - UUID de la factura.
* @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice, Error>
*/
findById(id: UniqueID, transaction: any): Promise<Result<CustomerInvoice, Error>>;
/**
*
* Consulta facturas usando un objeto Criteria (filtros, orden, paginación).
* @param criteria - Criterios de búsqueda.
* @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice[], Error>
*
* @see Criteria
*/
findByCriteria(
criteria: Criteria,
transaction: any
): Promise<Result<Collection<CustomerInvoice>, Error>>;
/**
*
* Elimina o marca como eliminada una factura.
* @param id - UUID de la factura a eliminar.
* @param transaction - Transacción activa para la operación.
* @returns Result<void, Error>
*/
deleteById(id: UniqueID, transaction: any): Promise<Result<void, Error>>;

View File

@ -4,29 +4,28 @@ import { Collection, Result } from "@repo/rdx-utils";
import { CustomerInvoice, CustomerInvoiceProps } from "../aggregates";
export interface ICustomerInvoiceService {
findCustomerInvoices(
build(props: CustomerInvoiceProps): Result<CustomerInvoice, Error>;
save(invoice: CustomerInvoice, transaction: any): Promise<Result<CustomerInvoice, Error>>;
findByCriteria(
criteria: Criteria,
transaction?: any
): Promise<Result<Collection<CustomerInvoice>, Error>>;
findCustomerInvoiceById(
customerInvoiceId: UniqueID,
transaction?: any
): Promise<Result<CustomerInvoice>>;
updateCustomerInvoiceById(
customerInvoiceId: UniqueID,
getById(id: UniqueID, transaction?: any): Promise<Result<CustomerInvoice>>;
updateById(
id: UniqueID,
data: Partial<CustomerInvoiceProps>,
transaction?: any
): Promise<Result<CustomerInvoice, Error>>;
createCustomerInvoice(
customerInvoiceId: UniqueID,
id: UniqueID,
data: CustomerInvoiceProps,
transaction?: any
): Promise<Result<CustomerInvoice, Error>>;
deleteCustomerInvoiceById(
customerInvoiceId: UniqueID,
transaction?: any
): Promise<Result<boolean, Error>>;
deleteById(id: UniqueID, transaction?: any): Promise<Result<boolean, Error>>;
}

View File

@ -7,13 +7,42 @@ import { ICustomerInvoiceRepository } from "../repositories";
import { ICustomerInvoiceService } from "./customer-invoice-service.interface";
export class CustomerInvoiceService implements ICustomerInvoiceService {
constructor(private readonly repo: ICustomerInvoiceRepository) {}
constructor(private readonly repository: ICustomerInvoiceRepository) {}
async findCustomerInvoices(
/**
* Construye un nuevo agregado CustomerInvoice a partir de props validadas.
*
* @param props - Las propiedades ya validadas para crear la factura.
* @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);
}
/**
* Guarda una instancia de CustomerInvoice en persistencia.
*
* @param invoice - El agregado a guardar.
* @param transaction - Transacción activa para 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>> {
const saved = await this.repository.save(invoice, transaction);
return saved.isSuccess ? Result.ok(invoice) : Result.fail(saved.error);
}
/**
* Obtiene una colección de facturas que cumplen con los filtros definidos en un objeto Criteria.
*
* @param criteria - Objeto con condiciones de filtro, paginación y orden.
* @param transaction - Transacción activa para la operación.
* @returns Result<Collection<CustomerInvoice>, Error> - Colección de facturas o error.
*/
async findByCriteria(
criteria: Criteria,
transaction?: Transaction
): Promise<Result<Collection<CustomerInvoice>, Error>> {
const customerInvoicesOrError = await this.repo.findAll(criteria, transaction);
const customerInvoicesOrError = await this.repository.findAll(criteria, transaction);
if (customerInvoicesOrError.isFailure) {
return Result.fail(customerInvoicesOrError.error);
}
@ -25,20 +54,32 @@ export class CustomerInvoiceService implements ICustomerInvoiceService {
return customerInvoicesOrError;
}
async findCustomerInvoiceById(
customerInvoiceId: UniqueID,
transaction?: Transaction
): Promise<Result<CustomerInvoice>> {
return await this.repo.getById(customerInvoiceId, transaction);
/**
* Recupera una factura por su identificador único.
*
* @param id - Identificador UUID de la factura.
* @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice, Error> - Factura encontrada o error.
*/
async getById(id: UniqueID, transaction?: Transaction): Promise<Result<CustomerInvoice>> {
return await this.repository.getById(id, transaction);
}
async updateCustomerInvoiceById(
/**
* Actualiza parcialmente una factura existente con nuevos datos.
*
* @param id - Identificador de la factura a actualizar.
* @param changes - Subconjunto de props válidas para aplicar.
* @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice, Error> - Factura actualizada o error.
*/
async updateById(
customerInvoiceId: UniqueID,
data: Partial<CustomerInvoiceProps>,
changes: Partial<CustomerInvoiceProps>,
transaction?: Transaction
): Promise<Result<CustomerInvoice, Error>> {
// Verificar si la factura existe
const customerInvoiceOrError = await this.repo.getById(customerInvoiceId, transaction);
const customerInvoiceOrError = await this.repository.getById(customerInvoiceId, transaction);
if (customerInvoiceOrError.isFailure) {
return Result.fail(new Error("CustomerInvoice not found"));
}
@ -64,7 +105,7 @@ export class CustomerInvoiceService implements ICustomerInvoiceService {
transaction?: Transaction
): Promise<Result<CustomerInvoice, Error>> {
// Verificar si la factura existe
const customerInvoiceOrError = await this.repo.getById(customerInvoiceId, transaction);
const customerInvoiceOrError = await this.repository.getById(customerInvoiceId, transaction);
if (customerInvoiceOrError.isSuccess) {
return Result.fail(new Error("CustomerInvoice exists"));
}
@ -78,14 +119,18 @@ export class CustomerInvoiceService implements ICustomerInvoiceService {
const newCustomerInvoice = newCustomerInvoiceOrError.data;
await this.repo.create(newCustomerInvoice, transaction);
await this.repository.create(newCustomerInvoice, transaction);
return Result.ok(newCustomerInvoice);
}
async deleteCustomerInvoiceById(
customerInvoiceId: UniqueID,
transaction?: Transaction
): Promise<Result<boolean, Error>> {
return this.repo.deleteById(customerInvoiceId, transaction);
/**
* Elimina (o marca como eliminada) una factura según su ID.
*
* @param id - Identificador UUID de la factura.
* @param transaction - Transacción activa para la operación.
* @returns Result<boolean, Error> - Resultado de la operación.
*/
async deleteById(id: UniqueID, transaction?: Transaction): Promise<Result<boolean, Error>> {
return this.repository.deleteById(id, transaction);
}
}

View File

@ -90,8 +90,8 @@ export class CustomerInvoiceMapper
return {
id: source.id.toString(),
invoice_status: source.status.toPrimitive(),
invoice_series: source.customerInvoiceSeries.toPrimitive(),
invoice_number: source.customerInvoiceNumber.toPrimitive(),
invoice_series: source.invoiceSeries.toPrimitive(),
invoice_number: source.invoiceNumber.toPrimitive(),
issue_date: source.issueDate.toPrimitive(),
operation_date: source.operationDate.toPrimitive(),
invoice_language: "es",

View File

@ -1,123 +1,109 @@
import { SequelizeRepository } from "@erp/core/api";
import { Criteria } from "@repo/rdx-criteria/server";
import { SequelizeRepository, errorMapper } from "@erp/core/api";
import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
import { UniqueID } from "@repo/rdx-ddd";
import { Collection, Result } from "@repo/rdx-utils";
import { Sequelize, Transaction } from "sequelize";
import { CustomerInvoice, ICustomerInvoiceRepository } from "../../domain";
import { ICustomerInvoiceMapper } from "../mappers/customer-invoice.mapper";
import { CustomerInvoiceItemModel } from "./customer-invoice-item.model";
import { CustomerInvoiceModel } from "./customer-invoice.model";
export class CustomerInvoiceRepository
extends SequelizeRepository<CustomerInvoice>
implements ICustomerInvoiceRepository
{
private readonly _mapper!: ICustomerInvoiceMapper;
/**
* 🔹 Función personalizada para mapear errores de unicidad en autenticación
*/
private _customErrorMapper(error: Error): string | null {
if (error.name === "SequelizeUniqueConstraintError") {
return "CustomerInvoice with this email already exists";
}
return null;
}
private readonly model: typeof CustomerInvoiceModel;
private readonly mapper!: ICustomerInvoiceMapper;
constructor(database: Sequelize, mapper: ICustomerInvoiceMapper) {
super(database);
this._mapper = mapper;
this.model = database.model("CustomerInvoice") as typeof CustomerInvoiceModel;
this.mapper = mapper;
}
async customerInvoiceExists(
id: UniqueID,
transaction?: Transaction
): Promise<Result<boolean, Error>> {
/**
*
* Persiste una nueva factura o actualiza una existente.
*
* @param invoice - El agregado a guardar.
* @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice, Error>
*/
async save(
invoice: CustomerInvoice,
transaction: Transaction
): Promise<Result<CustomerInvoice, Error>> {
try {
const _customerInvoice = await this._getById(CustomerInvoiceModel, id, {}, transaction);
return Result.ok(Boolean(id.equals(_customerInvoice.id)));
} catch (error: any) {
return this._handleDatabaseError(error, this._customErrorMapper);
const data = this.mapper.mapToPersistence(invoice);
await this.model.upsert(data, { transaction });
return Result.ok(invoice);
} catch (err: unknown) {
return Result.fail(errorMapper.toDomainError(err));
}
}
async findAll(
/**
*
* Busca una factura por su identificador único.
* @param id - UUID de la factura.
* @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice, Error>
*/
async findById(id: UniqueID, transaction: Transaction): Promise<Result<CustomerInvoice, Error>> {
try {
const rawData = await this._findById(this.model, id.toString(), { transaction });
if (!rawData) {
return Result.fail(new Error(`Invoice with id ${id} not found.`));
}
return this.mapper.mapToDomain(rawData);
} catch (err: unknown) {
return Result.fail(errorMapper.toDomainError(err));
}
}
/**
*
* Consulta facturas usando un objeto Criteria (filtros, orden, paginación).
* @param criteria - Criterios de búsqueda.
* @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice[], Error>
*
* @see Criteria
*/
public async findByCriteria(
criteria: Criteria,
transaction?: Transaction
transaction: Transaction
): Promise<Result<Collection<CustomerInvoice>, Error>> {
try {
const rawCustomerInvoices = await CustomerInvoiceModel.findAll({
include: [
{
model: CustomerInvoiceItemModel,
as: "items",
},
],
const converter = new CriteriaToSequelizeConverter();
const query = converter.convert(criteria);
const instances = await this.model.findAll({
...query,
transaction,
...this.convertCriteria(criteria),
});
return this._mapper.mapArrayToDomain(rawCustomerInvoices);
} catch (error: unknown) {
console.error("Error in findAll", error);
return this._handleDatabaseError(error, this._customErrorMapper);
return this.mapper.mapArrayToDomain(instances);
} catch (err: unknown) {
return Result.fail(errorMapper.toDomainError(err));
}
}
async getById(id: UniqueID, transaction?: Transaction): Promise<Result<CustomerInvoice, Error>> {
/**
*
* Elimina o marca como eliminada una factura.
* @param id - UUID de la factura a eliminar.
* @param transaction - Transacción activa para la operación.
* @returns Result<void, Error>
*/
async deleteById(id: UniqueID, transaction: any): Promise<Result<void, Error>> {
try {
const rawCustomerInvoice: any = await this._getById(
CustomerInvoiceModel,
id,
{
include: [
{
model: CustomerInvoiceItemModel,
as: "items",
},
],
},
transaction
);
if (!rawCustomerInvoice === true) {
return Result.fail(new Error(`CustomerInvoice with id ${id.toString()} not exists`));
}
} catch (error: any) {
return this._handleDatabaseError(error, this._customErrorMapper);
await this._deleteById(this.model, id, false, transaction);
return Result.ok<void>();
} catch (err: unknown) {
return Result.fail(errorMapper.toDomainError(err));
}
}
async deleteById(id: UniqueID, transaction?: Transaction): Promise<Result<boolean, Error>> {
try {
this._deleteById(CustomerInvoiceModel, id, false, transaction);
return Result.ok<boolean>(true);
} catch (error: any) {
return this._handleDatabaseError(error, this._customErrorMapper);
}
}
async create(customerInvoice: CustomerInvoice, transaction?: Transaction): Promise<void> {
const customerInvoiceData = this._mapper.mapToPersistence(customerInvoice);
await this._save(
CustomerInvoiceModel,
customerInvoice.id,
customerInvoiceData,
{},
transaction
);
}
async update(customerInvoice: CustomerInvoice, transaction?: Transaction): Promise<void> {
const customerInvoiceData = this._mapper.mapToPersistence(customerInvoice);
await this._save(
CustomerInvoiceModel,
customerInvoice.id,
customerInvoiceData,
{},
transaction
);
}
}

View File

@ -2,6 +2,7 @@ import * as z from "zod/v4";
export const CreateCustomerInvoiceCommandSchema = z.object({
id: z.string().uuid(),
status: z.string(),
invoice_number: z.string().min(1, "Customer invoice number is required"),
invoice_series: z.string().min(1, "Customer invoice series is required"),
issue_date: z.string().datetime({ offset: true, message: "Invalid issue date format" }),

View File

@ -0,0 +1,19 @@
import { MetadataSchema } from "@erp/core";
import * as z from "zod/v4";
export const CustomerInvoicesCreationResultSchema = 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 CustomerInvoicesCreationResultDTO = z.infer<
typeof CustomerInvoicesCreationResultSchema
>;

View File

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

View File

@ -639,7 +639,7 @@ importers:
version: 4.1.10
tsup:
specifier: ^8.4.0
version: 8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.19.4)(typescript@5.8.3)
version: 8.4.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.19.4)(typescript@5.8.3)
tw-animate-css:
specifier: ^1.2.9
version: 1.3.4
@ -2855,11 +2855,6 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
acorn@8.15.0:
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
engines: {node: '>=0.4.0'}
hasBin: true
add@2.0.6:
resolution: {integrity: sha512-j5QzrmsokwWWp6kUcJQySpbG+xfOBqqKnup3OIk1pz+kB/80SLorZ9V8zHFLO92Lcd+hbvq8bT+zOGoPkmBV0Q==}
@ -3269,9 +3264,6 @@ packages:
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
confbox@0.1.8:
resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==}
connect-history-api-fallback@1.6.0:
resolution: {integrity: sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==}
engines: {node: '>=0.8'}
@ -3765,9 +3757,6 @@ packages:
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
engines: {node: '>=8'}
fix-dts-default-cjs-exports@1.0.1:
resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==}
fn.name@1.1.0:
resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
@ -4721,9 +4710,6 @@ packages:
engines: {node: '>=10'}
hasBin: true
mlly@1.7.4:
resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==}
module-alias@2.2.3:
resolution: {integrity: sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==}
@ -5012,9 +4998,6 @@ packages:
pathe@0.2.0:
resolution: {integrity: sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==}
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
pause@0.0.1:
resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==}
@ -5047,9 +5030,6 @@ packages:
resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
engines: {node: '>=8'}
pkg-types@1.3.1:
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
pnpm@10.12.2:
resolution: {integrity: sha512-oyVAGFuWTuMLtOl55AWtxq9ZImtDjuTMGfnodzZnpm0wL1v+5go508rGnjXkuW5winHdACt+k1nEESoXIqwyPw==}
engines: {node: '>=18.12'}
@ -5899,25 +5879,6 @@ packages:
typescript:
optional: true
tsup@8.5.0:
resolution: {integrity: sha512-VmBp77lWNQq6PfuMqCHD3xWl22vEoWsKajkF8t+yMBawlUS8JzEI+vOVMeuNZIuMML8qXRizFKi9oD5glKQVcQ==}
engines: {node: '>=18'}
hasBin: true
peerDependencies:
'@microsoft/api-extractor': ^7.36.0
'@swc/core': ^1
postcss: ^8.4.12
typescript: '>=4.5.0'
peerDependenciesMeta:
'@microsoft/api-extractor':
optional: true
'@swc/core':
optional: true
postcss:
optional: true
typescript:
optional: true
tsutils@3.21.0:
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
engines: {node: '>= 6'}
@ -5992,9 +5953,6 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
ufo@1.6.1:
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
uglify-js@3.19.3:
resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==}
engines: {node: '>=0.8.0'}
@ -8412,8 +8370,6 @@ snapshots:
acorn@8.14.1: {}
acorn@8.15.0: {}
add@2.0.6: {}
ag-charts-types@11.3.2: {}
@ -8864,8 +8820,6 @@ snapshots:
concat-map@0.0.1: {}
confbox@0.1.8: {}
connect-history-api-fallback@1.6.0: {}
consola@2.15.3: {}
@ -9376,12 +9330,6 @@ snapshots:
locate-path: 5.0.0
path-exists: 4.0.0
fix-dts-default-cjs-exports@1.0.1:
dependencies:
magic-string: 0.30.17
mlly: 1.7.4
rollup: 4.41.1
fn.name@1.1.0: {}
follow-redirects@1.15.9: {}
@ -10485,13 +10433,6 @@ snapshots:
mkdirp@3.0.1: {}
mlly@1.7.4:
dependencies:
acorn: 8.15.0
pathe: 2.0.3
pkg-types: 1.3.1
ufo: 1.6.1
module-alias@2.2.3: {}
moment-timezone@0.5.48:
@ -10792,8 +10733,6 @@ snapshots:
pathe@0.2.0: {}
pathe@2.0.3: {}
pause@0.0.1: {}
pg-connection-string@2.9.0: {}
@ -10815,12 +10754,6 @@ snapshots:
dependencies:
find-up: 4.1.0
pkg-types@1.3.1:
dependencies:
confbox: 0.1.8
mlly: 1.7.4
pathe: 2.0.3
pnpm@10.12.2: {}
postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.15.32)(typescript@5.8.3)):
@ -11707,34 +11640,6 @@ snapshots:
- tsx
- yaml
tsup@8.5.0(jiti@2.4.2)(postcss@8.5.6)(tsx@4.19.4)(typescript@5.8.3):
dependencies:
bundle-require: 5.1.0(esbuild@0.25.5)
cac: 6.7.14
chokidar: 4.0.3
consola: 3.4.2
debug: 4.4.1(supports-color@8.1.1)
esbuild: 0.25.5
fix-dts-default-cjs-exports: 1.0.1
joycon: 3.1.1
picocolors: 1.1.1
postcss-load-config: 6.0.1(jiti@2.4.2)(postcss@8.5.6)(tsx@4.19.4)
resolve-from: 5.0.0
rollup: 4.41.1
source-map: 0.8.0-beta.0
sucrase: 3.35.0
tinyexec: 0.3.2
tinyglobby: 0.2.14
tree-kill: 1.2.2
optionalDependencies:
postcss: 8.5.6
typescript: 5.8.3
transitivePeerDependencies:
- jiti
- supports-color
- tsx
- yaml
tsutils@3.21.0(typescript@5.8.3):
dependencies:
tslib: 1.14.1
@ -11812,8 +11717,6 @@ snapshots:
typescript@5.8.3: {}
ufo@1.6.1: {}
uglify-js@3.19.3:
optional: true