feat(series-de-facturas): implementar la funcionalidad de gestión de series de facturas
- Se han añadido las dependencias de las series de facturas y los servicios internos. - Se han creado un buscador de series de facturas y generadores de instantáneas para mostrar una lista de series de facturas. - Se ha implementado un caso de uso para mostrar una lista de series de facturas activas por empresa. - Se ha desarrollado la gestión de errores de la API para los errores relacionados con las series de facturas. - Se han introducido nuevas clases de error para gestionar la validación de las series de facturas y los problemas de concurrencia. - Se han añadido rutas y controladores de Express para la gestión de series de facturas. - Se han definido DTO de respuesta para mostrar una lista de series de facturas. - Se han actualizado los DTO comunes para incluir esquemas relacionados con las series de facturas.
This commit is contained in:
parent
db4fa55cc0
commit
05f4b1337b
@ -1 +1,4 @@
|
||||
export * from "./invoice-series-finder.di";
|
||||
export * from "./invoice-series-number-assigner.di";
|
||||
export * from "./invoice-series-snapshot-builders.di";
|
||||
export * from "./invoice-series-use-cases.di";
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
import type { IInvoiceSeriesRepository } from "../repositories";
|
||||
import { type IInvoiceSeriesFinder, InvoiceSeriesFinder } from "../services";
|
||||
|
||||
export function buildInvoiceSeriesFinder(
|
||||
repository: IInvoiceSeriesRepository
|
||||
): IInvoiceSeriesFinder {
|
||||
return new InvoiceSeriesFinder(repository);
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
import { InvoiceSeriesSummarySnapshotBuilder } from "../snapshot-builders";
|
||||
|
||||
export function buildInvoiceSeriesSnapshotBuilders() {
|
||||
return {
|
||||
summary: new InvoiceSeriesSummarySnapshotBuilder(),
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
import type { ITransactionManager } from "@erp/core/api";
|
||||
|
||||
import type { IInvoiceSeriesFinder } from "../services";
|
||||
import type { IInvoiceSeriesSummarySnapshotBuilder } from "../snapshot-builders";
|
||||
import { ListInvoiceSeriesUseCase } from "../use-cases";
|
||||
|
||||
export function buildListInvoiceSeriesUseCase(deps: {
|
||||
finder: IInvoiceSeriesFinder;
|
||||
summarySnapshotBuilder: IInvoiceSeriesSummarySnapshotBuilder;
|
||||
transactionManager: ITransactionManager;
|
||||
}) {
|
||||
return new ListInvoiceSeriesUseCase(
|
||||
deps.finder,
|
||||
deps.summarySnapshotBuilder,
|
||||
deps.transactionManager
|
||||
);
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
export class InactiveInvoiceSeriesError extends Error {
|
||||
public constructor(code: string) {
|
||||
super(`Invoice series "${code}" is inactive and cannot assign numbers`);
|
||||
this.name = "InactiveInvoiceSeriesError";
|
||||
}
|
||||
}
|
||||
@ -1,2 +1,5 @@
|
||||
export * from "./inactive-invoice-series.error";
|
||||
export * from "./invoice-series-concurrency.error";
|
||||
export * from "./invoice-series-inactive.error";
|
||||
export * from "./invoice-series-lock-required.error";
|
||||
export * from "./invoice-series-not-found.error";
|
||||
export * from "./invoice-series-transaction-required.error";
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
export class InvoiceSeriesConcurrencyError extends Error {
|
||||
public constructor(options?: ErrorOptions) {
|
||||
super("Invoice series could not be updated consistently during number assignment.", options);
|
||||
this.name = "InvoiceSeriesConcurrencyError";
|
||||
}
|
||||
}
|
||||
|
||||
export const isInvoiceSeriesConcurrencyError = (
|
||||
error: unknown
|
||||
): error is InvoiceSeriesConcurrencyError => error instanceof InvoiceSeriesConcurrencyError;
|
||||
@ -0,0 +1,10 @@
|
||||
export class InvoiceSeriesInactiveError extends Error {
|
||||
public constructor(code: string, options?: ErrorOptions) {
|
||||
super(`Invoice series "${code}" is inactive and cannot assign invoice numbers.`, options);
|
||||
this.name = "InvoiceSeriesInactiveError";
|
||||
}
|
||||
}
|
||||
|
||||
export const isInvoiceSeriesInactiveError = (
|
||||
error: unknown
|
||||
): error is InvoiceSeriesInactiveError => error instanceof InvoiceSeriesInactiveError;
|
||||
@ -0,0 +1,10 @@
|
||||
export class InvoiceSeriesLockRequiredError extends Error {
|
||||
public constructor(options?: ErrorOptions) {
|
||||
super("Invoice series row lock requires an active transaction.", options);
|
||||
this.name = "InvoiceSeriesLockRequiredError";
|
||||
}
|
||||
}
|
||||
|
||||
export const isInvoiceSeriesLockRequiredError = (
|
||||
error: unknown
|
||||
): error is InvoiceSeriesLockRequiredError => error instanceof InvoiceSeriesLockRequiredError;
|
||||
@ -0,0 +1,10 @@
|
||||
export class InvoiceSeriesNotFoundError extends Error {
|
||||
public constructor(options?: ErrorOptions) {
|
||||
super("Invoice series was not found for the provided company and code.", options);
|
||||
this.name = "InvoiceSeriesNotFoundError";
|
||||
}
|
||||
}
|
||||
|
||||
export const isInvoiceSeriesNotFoundError = (
|
||||
error: unknown
|
||||
): error is InvoiceSeriesNotFoundError => error instanceof InvoiceSeriesNotFoundError;
|
||||
@ -1,6 +1,11 @@
|
||||
export class InvoiceSeriesTransactionRequiredError extends Error {
|
||||
public constructor() {
|
||||
super("InvoiceSeries number assignment requires an active transaction");
|
||||
public constructor(options?: ErrorOptions) {
|
||||
super("Invoice series number assignment requires an active transaction.", options);
|
||||
this.name = "InvoiceSeriesTransactionRequiredError";
|
||||
}
|
||||
}
|
||||
|
||||
export const isInvoiceSeriesTransactionRequiredError = (
|
||||
error: unknown
|
||||
): error is InvoiceSeriesTransactionRequiredError =>
|
||||
error instanceof InvoiceSeriesTransactionRequiredError;
|
||||
|
||||
@ -2,3 +2,5 @@ export * from "./di";
|
||||
export * from "./errors";
|
||||
export * from "./repositories";
|
||||
export * from "./services";
|
||||
export * from "./snapshot-builders";
|
||||
export * from "./use-cases";
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import type { Maybe, Result } from "@repo/rdx-utils";
|
||||
import type { Collection, Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { InvoiceSeries, InvoiceSeriesCode } from "../../../domain";
|
||||
|
||||
export interface IInvoiceSeriesRepository {
|
||||
findActiveByCompany(params: {
|
||||
companyId: UniqueID;
|
||||
transaction?: unknown;
|
||||
}): Promise<Result<Collection<InvoiceSeries>, Error>>;
|
||||
|
||||
findByCodeInCompany(params: {
|
||||
companyId: UniqueID;
|
||||
invoiceSeriesCode: InvoiceSeriesCode;
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export * from "./invoice-series-finder";
|
||||
export * from "./invoice-series-number-assigner";
|
||||
|
||||
@ -0,0 +1,23 @@
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import type { Collection, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { InvoiceSeries } from "../../../domain";
|
||||
import type { IInvoiceSeriesRepository } from "../repositories";
|
||||
|
||||
export interface IInvoiceSeriesFinder {
|
||||
findActiveByCompany(
|
||||
companyId: UniqueID,
|
||||
transaction?: unknown
|
||||
): Promise<Result<Collection<InvoiceSeries>, Error>>;
|
||||
}
|
||||
|
||||
export class InvoiceSeriesFinder implements IInvoiceSeriesFinder {
|
||||
public constructor(private readonly repository: IInvoiceSeriesRepository) {}
|
||||
|
||||
public findActiveByCompany(companyId: UniqueID, transaction?: unknown) {
|
||||
return this.repository.findActiveByCompany({
|
||||
companyId,
|
||||
transaction,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,10 @@
|
||||
import { EntityNotFoundError } from "@erp/core/api";
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import { type InvoiceNumber, type InvoiceSeriesCode } from "../../../domain";
|
||||
import {
|
||||
InactiveInvoiceSeriesError,
|
||||
InvoiceSeriesInactiveError,
|
||||
InvoiceSeriesNotFoundError,
|
||||
InvoiceSeriesTransactionRequiredError,
|
||||
} from "../errors";
|
||||
import type { IInvoiceSeriesRepository } from "../repositories";
|
||||
@ -45,15 +45,13 @@ export class InvoiceSeriesNumberAssigner implements IInvoiceSeriesNumberAssigner
|
||||
}
|
||||
|
||||
if (invoiceSeriesResult.data.isNone()) {
|
||||
return Result.fail(
|
||||
new EntityNotFoundError("InvoiceSeries", "code", params.invoiceSeriesCode.toPrimitive())
|
||||
);
|
||||
return Result.fail(new InvoiceSeriesNotFoundError());
|
||||
}
|
||||
|
||||
const invoiceSeries = invoiceSeriesResult.data.unwrap();
|
||||
|
||||
if (!invoiceSeries.isActive) {
|
||||
return Result.fail(new InactiveInvoiceSeriesError(invoiceSeries.code.toPrimitive()));
|
||||
return Result.fail(new InvoiceSeriesInactiveError(invoiceSeries.code.toPrimitive()));
|
||||
}
|
||||
|
||||
const invoiceNumberResult = invoiceSeries.assignNextInvoiceNumber();
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export * from "./summary";
|
||||
@ -0,0 +1 @@
|
||||
export * from "./invoice-series-summary.snapshot-builder";
|
||||
@ -0,0 +1,21 @@
|
||||
import type { ISnapshotBuilder } from "@erp/core/api";
|
||||
|
||||
import type { InvoiceSeriesSummaryDTO } from "../../../../../common";
|
||||
import type { InvoiceSeries } from "../../../../domain";
|
||||
|
||||
export interface IInvoiceSeriesSummarySnapshotBuilder
|
||||
extends ISnapshotBuilder<InvoiceSeries, InvoiceSeriesSummaryDTO> {}
|
||||
|
||||
export class InvoiceSeriesSummarySnapshotBuilder
|
||||
implements IInvoiceSeriesSummarySnapshotBuilder
|
||||
{
|
||||
public toOutput(invoiceSeries: InvoiceSeries): InvoiceSeriesSummaryDTO {
|
||||
return {
|
||||
id: invoiceSeries.id.toString(),
|
||||
code: invoiceSeries.code.toPrimitive(),
|
||||
next_number: invoiceSeries.nextNumber.toPrimitive(),
|
||||
padding_length: invoiceSeries.paddingLength.toPrimitive(),
|
||||
is_default: invoiceSeries.isDefault,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from "./list-invoice-series.use-case";
|
||||
@ -0,0 +1,40 @@
|
||||
import type { ITransactionManager } from "@erp/core/api";
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { IInvoiceSeriesFinder } from "../services";
|
||||
import type { IInvoiceSeriesSummarySnapshotBuilder } from "../snapshot-builders";
|
||||
|
||||
type ListInvoiceSeriesUseCaseInput = {
|
||||
companyId: UniqueID;
|
||||
};
|
||||
|
||||
export class ListInvoiceSeriesUseCase {
|
||||
public constructor(
|
||||
private readonly finder: IInvoiceSeriesFinder,
|
||||
private readonly summarySnapshotBuilder: IInvoiceSeriesSummarySnapshotBuilder,
|
||||
private readonly transactionManager: ITransactionManager
|
||||
) {}
|
||||
|
||||
public execute(params: ListInvoiceSeriesUseCaseInput) {
|
||||
const { companyId } = params;
|
||||
|
||||
return this.transactionManager.complete(async (transaction: unknown) => {
|
||||
try {
|
||||
const result = await this.finder.findActiveByCompany(companyId, transaction);
|
||||
|
||||
if (result.isFailure) {
|
||||
return Result.fail(result.error);
|
||||
}
|
||||
|
||||
const items = result.data.map((invoiceSeries) =>
|
||||
this.summarySnapshotBuilder.toOutput(invoiceSeries)
|
||||
);
|
||||
|
||||
return Result.ok({ items });
|
||||
} catch (error: unknown) {
|
||||
return Result.fail(error as Error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from "./issued-invoice-number-already-exists.error";
|
||||
@ -0,0 +1,11 @@
|
||||
export class IssuedInvoiceNumberAlreadyExistsError extends Error {
|
||||
public constructor(options?: ErrorOptions) {
|
||||
super("Invoice number already exists for this company and series.", options);
|
||||
this.name = "IssuedInvoiceNumberAlreadyExistsError";
|
||||
}
|
||||
}
|
||||
|
||||
export const isIssuedInvoiceNumberAlreadyExistsError = (
|
||||
error: unknown
|
||||
): error is IssuedInvoiceNumberAlreadyExistsError =>
|
||||
error instanceof IssuedInvoiceNumberAlreadyExistsError;
|
||||
@ -1,4 +1,5 @@
|
||||
export * from "./di";
|
||||
export * from "./errors";
|
||||
export * from "./models";
|
||||
export * from "./repositories";
|
||||
export * from "./services";
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import { type IIssuedInvoiceCreateProps, IssuedInvoice } from "../../../domain";
|
||||
import {
|
||||
type IIssuedInvoiceCreateProps,
|
||||
InvoiceSeriesCode,
|
||||
IssuedInvoice,
|
||||
} from "../../../domain";
|
||||
import type { IInvoiceSeriesNumberAssigner } from "../../invoice-series";
|
||||
import type { IIssuedInvoiceRepository } from "../repositories";
|
||||
|
||||
@ -33,9 +37,15 @@ export class IssuedInvoiceCreator implements IIssuedInvoiceCreator {
|
||||
async create(params: IIssuedInvoiceCreatorParams): Promise<Result<IssuedInvoice, Error>> {
|
||||
const { companyId, id, props, transaction } = params;
|
||||
|
||||
const invoiceSeriesCodeResult = InvoiceSeriesCode.create(props.series.toPrimitive());
|
||||
|
||||
if (invoiceSeriesCodeResult.isFailure) {
|
||||
return Result.fail(invoiceSeriesCodeResult.error);
|
||||
}
|
||||
|
||||
const numberResult = await this.invoiceSeriesNumberAssigner.assignNextNumber({
|
||||
companyId,
|
||||
invoiceSeriesCode: props.series,
|
||||
invoiceSeriesCode: invoiceSeriesCodeResult.data,
|
||||
transaction,
|
||||
});
|
||||
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
export * from "./invalid-invoice-series-code.error";
|
||||
export * from "./invalid-invoice-series-next-number.error";
|
||||
export * from "./invalid-invoice-series-padding-length.error";
|
||||
@ -0,0 +1,16 @@
|
||||
import { DomainError } from "@repo/rdx-ddd";
|
||||
|
||||
export class InvalidInvoiceSeriesCodeError extends DomainError {
|
||||
public constructor(message = "Invoice series code is invalid.", options?: ErrorOptions) {
|
||||
super(message, options);
|
||||
this.name = "InvalidInvoiceSeriesCodeError";
|
||||
}
|
||||
|
||||
public static required(options?: ErrorOptions) {
|
||||
return new InvalidInvoiceSeriesCodeError("Invoice series code is required.", options);
|
||||
}
|
||||
}
|
||||
|
||||
export const isInvalidInvoiceSeriesCodeError = (
|
||||
error: unknown
|
||||
): error is InvalidInvoiceSeriesCodeError => error instanceof InvalidInvoiceSeriesCodeError;
|
||||
@ -0,0 +1,13 @@
|
||||
import { DomainError } from "@repo/rdx-ddd";
|
||||
|
||||
export class InvalidInvoiceSeriesNextNumberError extends DomainError {
|
||||
public constructor(message = "Invoice series next number is invalid.", options?: ErrorOptions) {
|
||||
super(message, options);
|
||||
this.name = "InvalidInvoiceSeriesNextNumberError";
|
||||
}
|
||||
}
|
||||
|
||||
export const isInvalidInvoiceSeriesNextNumberError = (
|
||||
error: unknown
|
||||
): error is InvalidInvoiceSeriesNextNumberError =>
|
||||
error instanceof InvalidInvoiceSeriesNextNumberError;
|
||||
@ -0,0 +1,16 @@
|
||||
import { DomainError } from "@repo/rdx-ddd";
|
||||
|
||||
export class InvalidInvoiceSeriesPaddingLengthError extends DomainError {
|
||||
public constructor(
|
||||
message = "Invoice series padding length is invalid.",
|
||||
options?: ErrorOptions
|
||||
) {
|
||||
super(message, options);
|
||||
this.name = "InvalidInvoiceSeriesPaddingLengthError";
|
||||
}
|
||||
}
|
||||
|
||||
export const isInvalidInvoiceSeriesPaddingLengthError = (
|
||||
error: unknown
|
||||
): error is InvalidInvoiceSeriesPaddingLengthError =>
|
||||
error instanceof InvalidInvoiceSeriesPaddingLengthError;
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./aggregates";
|
||||
export * from "./errors";
|
||||
export * from "./value-objects";
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import { DomainValidationError, ValueObject } from "@repo/rdx-ddd";
|
||||
import { ValueObject } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { InvalidInvoiceSeriesCodeError } from "../errors";
|
||||
|
||||
type InvoiceSeriesCodeProps = {
|
||||
value: string;
|
||||
};
|
||||
|
||||
export class InvoiceSeriesCode extends ValueObject<InvoiceSeriesCodeProps> {
|
||||
private static readonly MAX_LENGTH = 10;
|
||||
private static readonly FIELD = "invoiceSeriesCode";
|
||||
private static readonly ERROR_CODE = "INVALID_INVOICE_SERIES_CODE";
|
||||
|
||||
private static validate(value: string) {
|
||||
return z
|
||||
@ -26,13 +26,7 @@ export class InvoiceSeriesCode extends ValueObject<InvoiceSeriesCodeProps> {
|
||||
const validationResult = InvoiceSeriesCode.validate(value);
|
||||
|
||||
if (!validationResult.success) {
|
||||
return Result.fail(
|
||||
new DomainValidationError(
|
||||
InvoiceSeriesCode.ERROR_CODE,
|
||||
InvoiceSeriesCode.FIELD,
|
||||
validationResult.error.message
|
||||
)
|
||||
);
|
||||
return Result.fail(new InvalidInvoiceSeriesCodeError(validationResult.error.message));
|
||||
}
|
||||
|
||||
return Result.ok(new InvoiceSeriesCode({ value: validationResult.data }));
|
||||
|
||||
@ -1,15 +1,14 @@
|
||||
import { DomainValidationError, ValueObject } from "@repo/rdx-ddd";
|
||||
import { ValueObject } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { InvalidInvoiceSeriesNextNumberError } from "../errors";
|
||||
|
||||
type InvoiceSeriesNextNumberProps = {
|
||||
value: number;
|
||||
};
|
||||
|
||||
export class InvoiceSeriesNextNumber extends ValueObject<InvoiceSeriesNextNumberProps> {
|
||||
private static readonly FIELD = "nextNumber";
|
||||
private static readonly ERROR_CODE = "INVALID_INVOICE_SERIES_NEXT_NUMBER";
|
||||
|
||||
private static validate(value: number) {
|
||||
return z
|
||||
.number()
|
||||
@ -22,13 +21,7 @@ export class InvoiceSeriesNextNumber extends ValueObject<InvoiceSeriesNextNumber
|
||||
const validationResult = InvoiceSeriesNextNumber.validate(value);
|
||||
|
||||
if (!validationResult.success) {
|
||||
return Result.fail(
|
||||
new DomainValidationError(
|
||||
InvoiceSeriesNextNumber.ERROR_CODE,
|
||||
InvoiceSeriesNextNumber.FIELD,
|
||||
validationResult.error.message
|
||||
)
|
||||
);
|
||||
return Result.fail(new InvalidInvoiceSeriesNextNumberError(validationResult.error.message));
|
||||
}
|
||||
|
||||
return Result.ok(new InvoiceSeriesNextNumber({ value: validationResult.data }));
|
||||
|
||||
@ -1,15 +1,14 @@
|
||||
import { DomainValidationError, ValueObject } from "@repo/rdx-ddd";
|
||||
import { ValueObject } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { InvalidInvoiceSeriesPaddingLengthError } from "../errors";
|
||||
|
||||
type InvoiceSeriesPaddingLengthProps = {
|
||||
value: number;
|
||||
};
|
||||
|
||||
export class InvoiceSeriesPaddingLength extends ValueObject<InvoiceSeriesPaddingLengthProps> {
|
||||
private static readonly FIELD = "paddingLength";
|
||||
private static readonly ERROR_CODE = "INVALID_INVOICE_SERIES_PADDING_LENGTH";
|
||||
|
||||
private static validate(value: number) {
|
||||
return z
|
||||
.number()
|
||||
@ -23,11 +22,7 @@ export class InvoiceSeriesPaddingLength extends ValueObject<InvoiceSeriesPadding
|
||||
|
||||
if (!validationResult.success) {
|
||||
return Result.fail(
|
||||
new DomainValidationError(
|
||||
InvoiceSeriesPaddingLength.ERROR_CODE,
|
||||
InvoiceSeriesPaddingLength.FIELD,
|
||||
validationResult.error.message
|
||||
)
|
||||
new InvalidInvoiceSeriesPaddingLengthError(validationResult.error.message)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ import {
|
||||
InvoiceStatus,
|
||||
type ItemAmount,
|
||||
} from "../../common/value-objects";
|
||||
import { InvalidInvoiceSeriesCodeError } from "../../invoice-series";
|
||||
import {
|
||||
type IProformaItemCreateProps,
|
||||
type IProformaItems,
|
||||
@ -363,13 +364,7 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
|
||||
|
||||
private validateCanBeIssued(): Result<void, Error> {
|
||||
if (this.series.isNone()) {
|
||||
return Result.fail(
|
||||
new DomainValidationError(
|
||||
"MISSING_SERIES",
|
||||
"series",
|
||||
"Series is required to issue the proforma"
|
||||
)
|
||||
);
|
||||
return Result.fail(InvalidInvoiceSeriesCodeError.required());
|
||||
}
|
||||
|
||||
if (this.description.isNone()) {
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import type { IModuleServer } from "@erp/core/api";
|
||||
|
||||
import {
|
||||
buildInvoiceSeriesDependencies,
|
||||
buildIssuedInvoicePublicServices,
|
||||
buildIssuedInvoicesDependencies,
|
||||
buildProformaPublicServices,
|
||||
buildProformasDependencies,
|
||||
invoiceSeriesRouter,
|
||||
issuedInvoicesRouter,
|
||||
models,
|
||||
proformasRouter,
|
||||
@ -28,6 +30,7 @@ export const customerInvoicesAPIModule: IModuleServer = {
|
||||
const { env: ENV, app, database, baseRoutePath: API_BASE_PATH, logger } = params;
|
||||
|
||||
// 1) Dominio interno
|
||||
const invoiceSeriesInternal = buildInvoiceSeriesDependencies(params);
|
||||
const issuedInvoicesInternal = buildIssuedInvoicesDependencies(params);
|
||||
const proformasInternal = buildProformasDependencies(params);
|
||||
|
||||
@ -49,6 +52,7 @@ export const customerInvoicesAPIModule: IModuleServer = {
|
||||
|
||||
// Implementación privada del módulo
|
||||
internal: {
|
||||
invoiceSeries: invoiceSeriesInternal,
|
||||
issuedInvoices: issuedInvoicesInternal,
|
||||
proformas: proformasInternal,
|
||||
},
|
||||
@ -66,6 +70,7 @@ export const customerInvoicesAPIModule: IModuleServer = {
|
||||
const { app, baseRoutePath, logger, getInternal, getService } = params;
|
||||
|
||||
// Registro de rutas HTTP
|
||||
invoiceSeriesRouter(params);
|
||||
issuedInvoicesRouter(params);
|
||||
proformasRouter(params);
|
||||
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export * from "./invoice-series.di";
|
||||
export * from "./invoice-series-repository.di";
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
import { type ModuleParams, buildTransactionManager } from "@erp/core/api";
|
||||
|
||||
import {
|
||||
type ListInvoiceSeriesUseCase,
|
||||
buildInvoiceSeriesFinder,
|
||||
buildInvoiceSeriesSnapshotBuilders,
|
||||
buildListInvoiceSeriesUseCase,
|
||||
} from "../../../application";
|
||||
|
||||
import { buildInvoiceSeriesRepository } from "./invoice-series-repository.di";
|
||||
|
||||
export type InvoiceSeriesInternalDeps = {
|
||||
useCases: {
|
||||
listInvoiceSeries: () => ListInvoiceSeriesUseCase;
|
||||
};
|
||||
};
|
||||
|
||||
export function buildInvoiceSeriesDependencies(params: ModuleParams): InvoiceSeriesInternalDeps {
|
||||
const { database } = params;
|
||||
|
||||
const transactionManager = buildTransactionManager(database);
|
||||
const repository = buildInvoiceSeriesRepository(database);
|
||||
const finder = buildInvoiceSeriesFinder(repository);
|
||||
const snapshotBuilders = buildInvoiceSeriesSnapshotBuilders();
|
||||
|
||||
return {
|
||||
useCases: {
|
||||
listInvoiceSeries: () =>
|
||||
buildListInvoiceSeriesUseCase({
|
||||
finder,
|
||||
summarySnapshotBuilder: snapshotBuilders.summary,
|
||||
transactionManager,
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from "./list-invoice-series.controller";
|
||||
@ -0,0 +1,41 @@
|
||||
import {
|
||||
ExpressController,
|
||||
forbidQueryFieldGuard,
|
||||
requireAuthenticatedGuard,
|
||||
requireCompanyContextGuard,
|
||||
} from "@erp/core/api";
|
||||
|
||||
import { ListInvoiceSeriesResponseSchema } from "../../../../../common";
|
||||
import type { ListInvoiceSeriesUseCase } from "../../../../application";
|
||||
import { invoiceSeriesApiErrorMapper } from "../invoice-series-api-error-mapper";
|
||||
|
||||
export class ListInvoiceSeriesController extends ExpressController {
|
||||
public constructor(private readonly useCase: ListInvoiceSeriesUseCase) {
|
||||
super();
|
||||
this.errorMapper = invoiceSeriesApiErrorMapper;
|
||||
|
||||
this.registerGuards(
|
||||
requireAuthenticatedGuard(),
|
||||
requireCompanyContextGuard(),
|
||||
forbidQueryFieldGuard("companyId"),
|
||||
forbidQueryFieldGuard("company_id")
|
||||
);
|
||||
}
|
||||
|
||||
protected async executeImpl() {
|
||||
const companyId = this.getTenantId();
|
||||
if (!companyId) {
|
||||
return this.forbiddenError("Tenant ID not found");
|
||||
}
|
||||
|
||||
const result = await this.useCase.execute({ companyId });
|
||||
|
||||
return result.match(
|
||||
(data) => {
|
||||
const dto = ListInvoiceSeriesResponseSchema.parse(data);
|
||||
return this.ok(dto);
|
||||
},
|
||||
(error) => this.handleError(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
export * from "./controllers";
|
||||
export * from "./invoice-series-api-error-mapper";
|
||||
export * from "./invoice-series.routes";
|
||||
@ -0,0 +1,87 @@
|
||||
import {
|
||||
ApiErrorMapper,
|
||||
type ErrorToApiRule,
|
||||
InternalApiError,
|
||||
ValidationApiError,
|
||||
} from "@erp/core/api";
|
||||
|
||||
import {
|
||||
InvoiceSeriesConcurrencyError,
|
||||
InvoiceSeriesLockRequiredError,
|
||||
InvoiceSeriesTransactionRequiredError,
|
||||
} from "../../../application";
|
||||
import {
|
||||
type InvalidInvoiceSeriesCodeError,
|
||||
type InvalidInvoiceSeriesNextNumberError,
|
||||
type InvalidInvoiceSeriesPaddingLengthError,
|
||||
isInvalidInvoiceSeriesCodeError,
|
||||
isInvalidInvoiceSeriesNextNumberError,
|
||||
isInvalidInvoiceSeriesPaddingLengthError,
|
||||
} from "../../../domain";
|
||||
|
||||
const invalidInvoiceSeriesCodeRule: ErrorToApiRule = {
|
||||
priority: 130,
|
||||
matches: (error) => isInvalidInvoiceSeriesCodeError(error),
|
||||
build: (error) =>
|
||||
new ValidationApiError(
|
||||
(error as InvalidInvoiceSeriesCodeError).message || "Invoice series code is invalid."
|
||||
),
|
||||
};
|
||||
|
||||
const invalidInvoiceSeriesNextNumberRule: ErrorToApiRule = {
|
||||
priority: 130,
|
||||
matches: (error) => isInvalidInvoiceSeriesNextNumberError(error),
|
||||
build: (error) =>
|
||||
new ValidationApiError(
|
||||
(error as InvalidInvoiceSeriesNextNumberError).message ||
|
||||
"Invoice series next number is invalid."
|
||||
),
|
||||
};
|
||||
|
||||
const invalidInvoiceSeriesPaddingLengthRule: ErrorToApiRule = {
|
||||
priority: 130,
|
||||
matches: (error) => isInvalidInvoiceSeriesPaddingLengthError(error),
|
||||
build: (error) =>
|
||||
new ValidationApiError(
|
||||
(error as InvalidInvoiceSeriesPaddingLengthError).message ||
|
||||
"Invoice series padding length is invalid."
|
||||
),
|
||||
};
|
||||
|
||||
const invoiceSeriesTransactionRequiredRule: ErrorToApiRule = {
|
||||
priority: 130,
|
||||
matches: (error) => error instanceof InvoiceSeriesTransactionRequiredError,
|
||||
build: (error) =>
|
||||
new InternalApiError(
|
||||
(error as InvoiceSeriesTransactionRequiredError).message ||
|
||||
"Invoice series operations require an active transaction."
|
||||
),
|
||||
};
|
||||
|
||||
const invoiceSeriesLockRequiredRule: ErrorToApiRule = {
|
||||
priority: 130,
|
||||
matches: (error) => error instanceof InvoiceSeriesLockRequiredError,
|
||||
build: (error) =>
|
||||
new InternalApiError(
|
||||
(error as InvoiceSeriesLockRequiredError).message ||
|
||||
"Invoice series row lock requires an active transaction."
|
||||
),
|
||||
};
|
||||
|
||||
const invoiceSeriesConcurrencyRule: ErrorToApiRule = {
|
||||
priority: 130,
|
||||
matches: (error) => error instanceof InvoiceSeriesConcurrencyError,
|
||||
build: (error) =>
|
||||
new InternalApiError(
|
||||
(error as InvoiceSeriesConcurrencyError).message ||
|
||||
"Invoice series persistence ended in an unexpected concurrency state."
|
||||
),
|
||||
};
|
||||
|
||||
export const invoiceSeriesApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default()
|
||||
.register(invoiceSeriesConcurrencyRule)
|
||||
.register(invoiceSeriesLockRequiredRule)
|
||||
.register(invoiceSeriesTransactionRequiredRule)
|
||||
.register(invalidInvoiceSeriesPaddingLengthRule)
|
||||
.register(invalidInvoiceSeriesNextNumberRule)
|
||||
.register(invalidInvoiceSeriesCodeRule);
|
||||
@ -0,0 +1,24 @@
|
||||
import { type StartParams } from "@erp/core/api";
|
||||
import { requireIdentityTenant } from "@erp/identity/api";
|
||||
import { type NextFunction, type Request, type Response, Router } from "express";
|
||||
|
||||
import type { InvoiceSeriesInternalDeps } from "../di/invoice-series.di";
|
||||
import { ListInvoiceSeriesController } from "./controllers";
|
||||
|
||||
export const invoiceSeriesRouter = (params: StartParams) => {
|
||||
const { app, config, getInternal } = params;
|
||||
|
||||
const deps = getInternal<InvoiceSeriesInternalDeps>("customer-invoices", "invoiceSeries");
|
||||
|
||||
const router: Router = Router({ mergeParams: true });
|
||||
|
||||
router.use(...requireIdentityTenant(params));
|
||||
|
||||
router.get("/", (req: Request, res: Response, next: NextFunction) => {
|
||||
const useCase = deps.useCases.listInvoiceSeries();
|
||||
const controller = new ListInvoiceSeriesController(useCase);
|
||||
return controller.execute(req, res, next);
|
||||
});
|
||||
|
||||
app.use(`${config.server.apiBasePath}/invoice-series`, router);
|
||||
};
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./di";
|
||||
export * from "./express";
|
||||
export * from "./persistence";
|
||||
|
||||
@ -7,6 +7,9 @@ import {
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import {
|
||||
type InvalidInvoiceSeriesCodeError,
|
||||
type InvalidInvoiceSeriesNextNumberError,
|
||||
type InvalidInvoiceSeriesPaddingLengthError,
|
||||
InvoiceSeries,
|
||||
InvoiceSeriesCode,
|
||||
type InvoiceSeriesInternalProps,
|
||||
@ -21,17 +24,21 @@ export class InvoiceSeriesDomainMapper {
|
||||
|
||||
const id = extractOrPushError(UniqueID.create(raw.id), "id", errors);
|
||||
const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors);
|
||||
const code = extractOrPushError(InvoiceSeriesCode.create(raw.code), "code", errors);
|
||||
const nextNumber = extractOrPushError(
|
||||
InvoiceSeriesNextNumber.create(raw.next_number),
|
||||
"next_number",
|
||||
errors
|
||||
);
|
||||
const paddingLength = extractOrPushError(
|
||||
InvoiceSeriesPaddingLength.create(raw.padding_length),
|
||||
"padding_length",
|
||||
errors
|
||||
);
|
||||
|
||||
const codeResult = InvoiceSeriesCode.create(raw.code);
|
||||
if (codeResult.isFailure) {
|
||||
return Result.fail(codeResult.error as InvalidInvoiceSeriesCodeError);
|
||||
}
|
||||
|
||||
const nextNumberResult = InvoiceSeriesNextNumber.create(raw.next_number);
|
||||
if (nextNumberResult.isFailure) {
|
||||
return Result.fail(nextNumberResult.error as InvalidInvoiceSeriesNextNumberError);
|
||||
}
|
||||
|
||||
const paddingLengthResult = InvoiceSeriesPaddingLength.create(raw.padding_length);
|
||||
if (paddingLengthResult.isFailure) {
|
||||
return Result.fail(paddingLengthResult.error as InvalidInvoiceSeriesPaddingLengthError);
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return Result.fail(
|
||||
@ -41,9 +48,9 @@ export class InvoiceSeriesDomainMapper {
|
||||
|
||||
const props: InvoiceSeriesInternalProps = {
|
||||
companyId: companyId!,
|
||||
code: code!,
|
||||
nextNumber: nextNumber!,
|
||||
paddingLength: paddingLength!,
|
||||
code: codeResult.data,
|
||||
nextNumber: nextNumberResult.data,
|
||||
paddingLength: paddingLengthResult.data,
|
||||
isDefault: raw.is_default,
|
||||
isActive: raw.is_active,
|
||||
};
|
||||
|
||||
@ -1,13 +1,17 @@
|
||||
import {
|
||||
InfrastructureRepositoryError,
|
||||
SequelizeRepository,
|
||||
translateSequelizeError,
|
||||
} from "@erp/core/api";
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import { Collection, Result } from "@repo/rdx-utils";
|
||||
import type { Sequelize, Transaction } from "sequelize";
|
||||
|
||||
import type { IInvoiceSeriesRepository } from "../../../../../application";
|
||||
import {
|
||||
InvoiceSeriesConcurrencyError,
|
||||
InvoiceSeriesLockRequiredError,
|
||||
InvoiceSeriesTransactionRequiredError,
|
||||
} from "../../../../../application/invoice-series";
|
||||
import type { InvoiceSeries, InvoiceSeriesCode } from "../../../../../domain";
|
||||
import { CustomerInvoiceSeriesModel } from "../../../../common";
|
||||
import type { InvoiceSeriesDomainMapper } from "../mappers";
|
||||
@ -23,6 +27,41 @@ export class SequelizeInvoiceSeriesRepository
|
||||
super({ database });
|
||||
}
|
||||
|
||||
public async findActiveByCompany(params: {
|
||||
companyId: UniqueID;
|
||||
transaction?: unknown;
|
||||
}) {
|
||||
try {
|
||||
const rows = await CustomerInvoiceSeriesModel.findAll({
|
||||
where: {
|
||||
company_id: params.companyId.toString(),
|
||||
is_active: true,
|
||||
},
|
||||
order: [
|
||||
["is_default", "DESC"],
|
||||
["code", "ASC"],
|
||||
],
|
||||
transaction: params.transaction as Transaction | undefined,
|
||||
});
|
||||
|
||||
const invoiceSeriesRows: InvoiceSeries[] = [];
|
||||
|
||||
for (const row of rows) {
|
||||
const invoiceSeriesResult = this.domainMapper.mapToDomain(row);
|
||||
|
||||
if (invoiceSeriesResult.isFailure) {
|
||||
return Result.fail(invoiceSeriesResult.error);
|
||||
}
|
||||
|
||||
invoiceSeriesRows.push(invoiceSeriesResult.data);
|
||||
}
|
||||
|
||||
return Result.ok(new Collection(invoiceSeriesRows));
|
||||
} catch (error: unknown) {
|
||||
return Result.fail(translateSequelizeError(error));
|
||||
}
|
||||
}
|
||||
|
||||
public async findByCodeInCompany(params: {
|
||||
companyId: UniqueID;
|
||||
invoiceSeriesCode: InvoiceSeriesCode;
|
||||
@ -52,11 +91,7 @@ export class SequelizeInvoiceSeriesRepository
|
||||
const transaction = params.transaction as Transaction;
|
||||
|
||||
if (!transaction) {
|
||||
return Result.fail(
|
||||
new InfrastructureRepositoryError(
|
||||
"InvoiceSeries row lock requires an active transaction"
|
||||
)
|
||||
);
|
||||
return Result.fail(new InvoiceSeriesLockRequiredError());
|
||||
}
|
||||
|
||||
const row = await CustomerInvoiceSeriesModel.findOne({
|
||||
@ -79,6 +114,10 @@ export class SequelizeInvoiceSeriesRepository
|
||||
transaction?: unknown
|
||||
): Promise<Result<void, Error>> {
|
||||
try {
|
||||
if (!transaction) {
|
||||
return Result.fail(new InvoiceSeriesTransactionRequiredError());
|
||||
}
|
||||
|
||||
const dtoResult = this.domainMapper.mapToPersistence(invoiceSeries);
|
||||
|
||||
if (dtoResult.isFailure) {
|
||||
@ -92,11 +131,7 @@ export class SequelizeInvoiceSeriesRepository
|
||||
});
|
||||
|
||||
if (affectedRows !== 1) {
|
||||
return Result.fail(
|
||||
new InfrastructureRepositoryError(
|
||||
`InvoiceSeries ${id} could not be updated or no longer exists`
|
||||
)
|
||||
);
|
||||
return Result.fail(new InvoiceSeriesConcurrencyError());
|
||||
}
|
||||
|
||||
return Result.ok();
|
||||
|
||||
@ -5,9 +5,19 @@ import {
|
||||
ApiErrorMapper,
|
||||
ConflictApiError,
|
||||
type ErrorToApiRule,
|
||||
InternalApiError,
|
||||
ValidationApiError,
|
||||
} from "@erp/core/api";
|
||||
|
||||
import {
|
||||
InvoiceSeriesConcurrencyError,
|
||||
InvoiceSeriesInactiveError,
|
||||
InvoiceSeriesLockRequiredError,
|
||||
InvoiceSeriesNotFoundError,
|
||||
InvoiceSeriesTransactionRequiredError,
|
||||
IssuedInvoiceNumberAlreadyExistsError,
|
||||
isIssuedInvoiceNumberAlreadyExistsError,
|
||||
} from "../../../application";
|
||||
import {
|
||||
type CustomerInvoiceIdAlreadyExistsError,
|
||||
type IssuedInvoiceItemMismatch,
|
||||
@ -36,7 +46,73 @@ const issuedinvoiceItemMismatchError: ErrorToApiRule = {
|
||||
),
|
||||
};
|
||||
|
||||
const invoiceSeriesNotFoundRule: ErrorToApiRule = {
|
||||
priority: 130,
|
||||
matches: (e) => e instanceof InvoiceSeriesNotFoundError,
|
||||
build: (e) =>
|
||||
new ConflictApiError(
|
||||
(e as InvoiceSeriesNotFoundError).message ||
|
||||
"Invoice series was not found for the provided company and code."
|
||||
),
|
||||
};
|
||||
|
||||
const invoiceSeriesInactiveRule: ErrorToApiRule = {
|
||||
priority: 130,
|
||||
matches: (e) => e instanceof InvoiceSeriesInactiveError,
|
||||
build: (e) =>
|
||||
new ConflictApiError(
|
||||
(e as InvoiceSeriesInactiveError).message ||
|
||||
"Invoice series is inactive and cannot assign invoice numbers."
|
||||
),
|
||||
};
|
||||
|
||||
const invoiceSeriesTransactionRequiredRule: ErrorToApiRule = {
|
||||
priority: 130,
|
||||
matches: (e) => e instanceof InvoiceSeriesTransactionRequiredError,
|
||||
build: (e) =>
|
||||
new InternalApiError(
|
||||
(e as InvoiceSeriesTransactionRequiredError).message ||
|
||||
"Invoice series number assignment requires an active transaction."
|
||||
),
|
||||
};
|
||||
|
||||
const invoiceSeriesLockRequiredRule: ErrorToApiRule = {
|
||||
priority: 130,
|
||||
matches: (e) => e instanceof InvoiceSeriesLockRequiredError,
|
||||
build: (e) =>
|
||||
new InternalApiError(
|
||||
(e as InvoiceSeriesLockRequiredError).message ||
|
||||
"Invoice series row lock requires an active transaction."
|
||||
),
|
||||
};
|
||||
|
||||
const invoiceSeriesConcurrencyRule: ErrorToApiRule = {
|
||||
priority: 130,
|
||||
matches: (e) => e instanceof InvoiceSeriesConcurrencyError,
|
||||
build: (e) =>
|
||||
new ConflictApiError(
|
||||
(e as InvoiceSeriesConcurrencyError).message ||
|
||||
"Invoice series could not be updated consistently during number assignment."
|
||||
),
|
||||
};
|
||||
|
||||
const issuedInvoiceNumberAlreadyExistsRule: ErrorToApiRule = {
|
||||
priority: 130,
|
||||
matches: (e) => isIssuedInvoiceNumberAlreadyExistsError(e),
|
||||
build: (e) =>
|
||||
new ConflictApiError(
|
||||
(e as IssuedInvoiceNumberAlreadyExistsError).message ||
|
||||
"Invoice number already exists for this company and series."
|
||||
),
|
||||
};
|
||||
|
||||
// Cómo aplicarla: crea una nueva instancia del mapper con la regla extra
|
||||
export const issuedInvoicesApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default()
|
||||
.register(issuedInvoiceNumberAlreadyExistsRule)
|
||||
.register(invoiceSeriesConcurrencyRule)
|
||||
.register(invoiceSeriesLockRequiredRule)
|
||||
.register(invoiceSeriesTransactionRequiredRule)
|
||||
.register(invoiceSeriesInactiveRule)
|
||||
.register(invoiceSeriesNotFoundRule)
|
||||
.register(invoiceDuplicateRule)
|
||||
.register(issuedinvoiceItemMismatchError);
|
||||
|
||||
@ -17,10 +17,15 @@ import {
|
||||
type OrderItem,
|
||||
type Sequelize,
|
||||
type Transaction,
|
||||
UniqueConstraintError,
|
||||
type WhereOptions,
|
||||
} from "sequelize";
|
||||
|
||||
import type { IIssuedInvoiceRepository, IssuedInvoiceSummary } from "../../../../../application";
|
||||
import {
|
||||
IssuedInvoiceNumberAlreadyExistsError,
|
||||
type IIssuedInvoiceRepository,
|
||||
type IssuedInvoiceSummary,
|
||||
} from "../../../../../application";
|
||||
import type { IssuedInvoice } from "../../../../../domain";
|
||||
import {
|
||||
CustomerInvoiceItemModel,
|
||||
@ -37,6 +42,8 @@ export class IssuedInvoiceRepository
|
||||
extends SequelizeRepository<IssuedInvoice>
|
||||
implements IIssuedInvoiceRepository
|
||||
{
|
||||
private static readonly INVOICE_NUMBER_UNIQUE_INDEX = "idx_invoice_company_series_number";
|
||||
|
||||
constructor(
|
||||
private readonly domainMapper: SequelizeIssuedInvoiceDomainMapper,
|
||||
private readonly summaryMapper: SequelizeIssuedInvoiceSummaryMapper,
|
||||
@ -92,10 +99,33 @@ export class IssuedInvoiceRepository
|
||||
|
||||
return Result.ok();
|
||||
} catch (err: unknown) {
|
||||
if (this.isIssuedInvoiceNumberAlreadyExistsError(err)) {
|
||||
return Result.fail(new IssuedInvoiceNumberAlreadyExistsError({ cause: err as Error }));
|
||||
}
|
||||
|
||||
return Result.fail(translateSequelizeError(err));
|
||||
}
|
||||
}
|
||||
|
||||
private isIssuedInvoiceNumberAlreadyExistsError(error: unknown): boolean {
|
||||
if (!(error instanceof UniqueConstraintError)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const fields = Object.keys(error.fields ?? {});
|
||||
const hasExpectedFields =
|
||||
fields.includes("company_id") &&
|
||||
fields.includes("series") &&
|
||||
fields.includes("invoice_number") &&
|
||||
fields.includes("is_proforma");
|
||||
|
||||
const byIndexName = error.errors.some(
|
||||
(detail) => detail.validatorKey === "not_unique" && detail.path === "idx_invoice_company_series_number"
|
||||
);
|
||||
|
||||
return hasExpectedFields || byIndexName || error.message.includes(IssuedInvoiceRepository.INVOICE_NUMBER_UNIQUE_INDEX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Comprueba si existe una factura con un `id` dentro de una `company`.
|
||||
*
|
||||
|
||||
@ -2,18 +2,35 @@ import {
|
||||
ApiErrorMapper,
|
||||
ConflictApiError,
|
||||
type ErrorToApiRule,
|
||||
InternalApiError,
|
||||
ValidationApiError,
|
||||
} from "@erp/core/api";
|
||||
|
||||
import { CompanyReportProfileNotFoundError, isCompanyReportProfileNotFoundError } from "../../../application";
|
||||
import {
|
||||
CompanyReportProfileNotFoundError,
|
||||
InvoiceSeriesConcurrencyError,
|
||||
InvoiceSeriesInactiveError,
|
||||
InvoiceSeriesLockRequiredError,
|
||||
InvoiceSeriesNotFoundError,
|
||||
InvoiceSeriesTransactionRequiredError,
|
||||
IssuedInvoiceNumberAlreadyExistsError,
|
||||
isCompanyReportProfileNotFoundError,
|
||||
isIssuedInvoiceNumberAlreadyExistsError,
|
||||
} from "../../../application";
|
||||
import {
|
||||
type CustomerInvoiceIdAlreadyExistsError,
|
||||
type EntityIsNotProformaError,
|
||||
type InvalidInvoiceSeriesCodeError,
|
||||
type InvalidInvoiceSeriesNextNumberError,
|
||||
type InvalidInvoiceSeriesPaddingLengthError,
|
||||
type InvalidProformaTransitionError,
|
||||
type ProformaCannotBeConvertedToInvoiceError,
|
||||
type ProformaItemMismatch,
|
||||
isCustomerInvoiceIdAlreadyExistsError,
|
||||
isEntityIsNotProformaError,
|
||||
isInvalidInvoiceSeriesCodeError,
|
||||
isInvalidInvoiceSeriesNextNumberError,
|
||||
isInvalidInvoiceSeriesPaddingLengthError,
|
||||
isInvalidProformaTransitionError,
|
||||
isProformaCannotBeConvertedToInvoiceError,
|
||||
isProformaCannotBeDeletedError,
|
||||
@ -88,8 +105,106 @@ const companyReportProfileNotFoundRule: ErrorToApiRule = {
|
||||
),
|
||||
};
|
||||
|
||||
const invalidInvoiceSeriesCodeRule: ErrorToApiRule = {
|
||||
priority: 130,
|
||||
matches: (e) => isInvalidInvoiceSeriesCodeError(e),
|
||||
build: (e) =>
|
||||
new ValidationApiError(
|
||||
(e as InvalidInvoiceSeriesCodeError).message || "Invoice series code is invalid."
|
||||
),
|
||||
};
|
||||
|
||||
const invalidInvoiceSeriesNextNumberRule: ErrorToApiRule = {
|
||||
priority: 130,
|
||||
matches: (e) => isInvalidInvoiceSeriesNextNumberError(e),
|
||||
build: (e) =>
|
||||
new ValidationApiError(
|
||||
(e as InvalidInvoiceSeriesNextNumberError).message ||
|
||||
"Invoice series next number is invalid."
|
||||
),
|
||||
};
|
||||
|
||||
const invalidInvoiceSeriesPaddingLengthRule: ErrorToApiRule = {
|
||||
priority: 130,
|
||||
matches: (e) => isInvalidInvoiceSeriesPaddingLengthError(e),
|
||||
build: (e) =>
|
||||
new ValidationApiError(
|
||||
(e as InvalidInvoiceSeriesPaddingLengthError).message ||
|
||||
"Invoice series padding length is invalid."
|
||||
),
|
||||
};
|
||||
|
||||
const invoiceSeriesNotFoundRule: ErrorToApiRule = {
|
||||
priority: 130,
|
||||
matches: (e) => e instanceof InvoiceSeriesNotFoundError,
|
||||
build: (e) =>
|
||||
new ConflictApiError(
|
||||
(e as InvoiceSeriesNotFoundError).message ||
|
||||
"Invoice series was not found for the provided company and code."
|
||||
),
|
||||
};
|
||||
|
||||
const invoiceSeriesInactiveRule: ErrorToApiRule = {
|
||||
priority: 130,
|
||||
matches: (e) => e instanceof InvoiceSeriesInactiveError,
|
||||
build: (e) =>
|
||||
new ConflictApiError(
|
||||
(e as InvoiceSeriesInactiveError).message ||
|
||||
"Invoice series is inactive and cannot assign invoice numbers."
|
||||
),
|
||||
};
|
||||
|
||||
const invoiceSeriesTransactionRequiredRule: ErrorToApiRule = {
|
||||
priority: 130,
|
||||
matches: (e) => e instanceof InvoiceSeriesTransactionRequiredError,
|
||||
build: (e) =>
|
||||
new InternalApiError(
|
||||
(e as InvoiceSeriesTransactionRequiredError).message ||
|
||||
"Invoice series number assignment requires an active transaction."
|
||||
),
|
||||
};
|
||||
|
||||
const invoiceSeriesLockRequiredRule: ErrorToApiRule = {
|
||||
priority: 130,
|
||||
matches: (e) => e instanceof InvoiceSeriesLockRequiredError,
|
||||
build: (e) =>
|
||||
new InternalApiError(
|
||||
(e as InvoiceSeriesLockRequiredError).message ||
|
||||
"Invoice series row lock requires an active transaction."
|
||||
),
|
||||
};
|
||||
|
||||
const invoiceSeriesConcurrencyRule: ErrorToApiRule = {
|
||||
priority: 130,
|
||||
matches: (e) => e instanceof InvoiceSeriesConcurrencyError,
|
||||
build: (e) =>
|
||||
new ConflictApiError(
|
||||
(e as InvoiceSeriesConcurrencyError).message ||
|
||||
"Invoice series could not be updated consistently during number assignment."
|
||||
),
|
||||
};
|
||||
|
||||
const issuedInvoiceNumberAlreadyExistsRule: ErrorToApiRule = {
|
||||
priority: 130,
|
||||
matches: (e) => isIssuedInvoiceNumberAlreadyExistsError(e),
|
||||
build: (e) =>
|
||||
new ConflictApiError(
|
||||
(e as IssuedInvoiceNumberAlreadyExistsError).message ||
|
||||
"Invoice number already exists for this company and series."
|
||||
),
|
||||
};
|
||||
|
||||
// Cómo aplicarla: crea una nueva instancia del mapper con la regla extra
|
||||
export const proformasApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default()
|
||||
.register(issuedInvoiceNumberAlreadyExistsRule)
|
||||
.register(invoiceSeriesConcurrencyRule)
|
||||
.register(invoiceSeriesLockRequiredRule)
|
||||
.register(invoiceSeriesTransactionRequiredRule)
|
||||
.register(invoiceSeriesInactiveRule)
|
||||
.register(invoiceSeriesNotFoundRule)
|
||||
.register(invalidInvoiceSeriesPaddingLengthRule)
|
||||
.register(invalidInvoiceSeriesNextNumberRule)
|
||||
.register(invalidInvoiceSeriesCodeRule)
|
||||
.register(companyReportProfileNotFoundRule)
|
||||
.register(proformaDuplicateRule)
|
||||
.register(proformaItemMismatchError)
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./invoice-series";
|
||||
export * from "./issued-invoices";
|
||||
export * from "./proformas";
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export * from "./list-invoice-series.response.dto";
|
||||
@ -0,0 +1,9 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { InvoiceSeriesSummarySchema } from "../../shared/invoice-series";
|
||||
|
||||
export const ListInvoiceSeriesResponseSchema = z.object({
|
||||
items: z.array(InvoiceSeriesSummarySchema),
|
||||
});
|
||||
|
||||
export type ListInvoiceSeriesResponseDTO = z.infer<typeof ListInvoiceSeriesResponseSchema>;
|
||||
@ -1,4 +1,5 @@
|
||||
export * from "./issued-invoices";
|
||||
export * from "./invoice-series";
|
||||
export * from "./item-position.dto";
|
||||
export * from "./payment-method-ref.dto";
|
||||
export * from "./payment-term-ref.dto";
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export * from "./invoice-series-summary.dto";
|
||||
@ -0,0 +1,11 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const InvoiceSeriesSummarySchema = z.object({
|
||||
id: z.uuid(),
|
||||
code: z.string(),
|
||||
next_number: z.number().int().nonnegative(),
|
||||
padding_length: z.number().int().nonnegative(),
|
||||
is_default: z.boolean(),
|
||||
});
|
||||
|
||||
export type InvoiceSeriesSummaryDTO = z.infer<typeof InvoiceSeriesSummarySchema>;
|
||||
Loading…
Reference in New Issue
Block a user