Facturas de cliente
This commit is contained in:
parent
061ec30cbd
commit
0fcba918a6
@ -5,7 +5,7 @@
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --config tsup.config.ts",
|
||||
"start": "NODE_ENV=production node --env-file=.env.production dist/index.js",
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"dev": "node --import=tsx --watch src/index.ts",
|
||||
"clean": "rimraf .turbo node_modules dist",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "biome lint --fix",
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { globalErrorHandler } from "@erp/core/api";
|
||||
import cors, { CorsOptions } from "cors";
|
||||
import express, { Application } from "express";
|
||||
import helmet from "helmet";
|
||||
@ -90,7 +89,7 @@ export function createApp(): Application {
|
||||
// Gestión global de errores.
|
||||
// Siempre al final de la cadena de middlewares
|
||||
// y después de las rutas.
|
||||
app.use(globalErrorHandler);
|
||||
//app.use(globalErrorHandler);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
export * from "./customer-invoice-application.service";
|
||||
export * from "./presenters";
|
||||
export * from "./services";
|
||||
export * from "./use-cases";
|
||||
|
||||
@ -47,6 +47,7 @@ export class CustomerInvoiceFullPresenter extends Presenter<
|
||||
id: invoice.id.toString(),
|
||||
company_id: invoice.companyId.toString(),
|
||||
|
||||
is_proforma: invoice.isProforma ? "true" : "false",
|
||||
invoice_number: invoice.invoiceNumber.toString(),
|
||||
status: invoice.status.toPrimitive(),
|
||||
series: toEmptyString(invoice.series, (value) => value.toString()),
|
||||
|
||||
@ -5,15 +5,16 @@ import { Transaction } from "sequelize";
|
||||
import {
|
||||
CustomerInvoiceNumber,
|
||||
CustomerInvoiceSerie,
|
||||
CustomerInvoiceStatus,
|
||||
ICustomerInvoiceNumberGenerator,
|
||||
} from "../domain";
|
||||
} from "../../domain";
|
||||
import {
|
||||
CustomerInvoice,
|
||||
CustomerInvoicePatchProps,
|
||||
CustomerInvoiceProps,
|
||||
} from "../domain/aggregates";
|
||||
import { ICustomerInvoiceRepository } from "../domain/repositories";
|
||||
import { CustomerInvoiceListDTO } from "../infrastructure";
|
||||
} from "../../domain/aggregates";
|
||||
import { ICustomerInvoiceRepository } from "../../domain/repositories";
|
||||
import { CustomerInvoiceListDTO } from "../../infrastructure";
|
||||
|
||||
export class CustomerInvoiceApplicationService {
|
||||
constructor(
|
||||
@ -67,28 +68,6 @@ export class CustomerInvoiceApplicationService {
|
||||
return CustomerInvoice.create({ ...props, companyId }, invoiceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construye una factura issue a partir de una proforma.
|
||||
*
|
||||
* @param companyId - Identificador de la empresa a la que pertenece la factura.
|
||||
* @param issueInvoiceId - Identificador UUID de la factura (opcional).
|
||||
* @param proforma - La proforma de la cual se generará la issue
|
||||
* @param pathcProps - otros props personalizados que se trasladarán a la issue
|
||||
* @returns Result<CustomerInvoice, Error> - El agregado construido o un error si falla la creación.
|
||||
*/
|
||||
buildIssueInvoiceInCompany(
|
||||
companyId: UniqueID,
|
||||
proforma: CustomerInvoice,
|
||||
patchProps: CustomerInvoicePatchProps
|
||||
): Result<CustomerInvoice, Error> {
|
||||
const proformaProps = proforma.getIssuedInvoiceProps();
|
||||
return CustomerInvoice.create({
|
||||
...proformaProps,
|
||||
...patchProps,
|
||||
companyId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Guarda una nueva factura y devuelve la factura guardada.
|
||||
*
|
||||
@ -226,4 +205,28 @@ export class CustomerInvoiceApplicationService {
|
||||
): Promise<Result<boolean, Error>> {
|
||||
return this.repository.deleteByIdInCompany(companyId, invoiceId, transaction);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Actualiza el "status" de una proforma
|
||||
*
|
||||
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
|
||||
* @param proformaId - UUID de la factura a eliminar.
|
||||
* @param newStatus - nuevo estado
|
||||
* @param transaction - Transacción activa para la operación.
|
||||
* @returns Result<boolean, Error>
|
||||
*/
|
||||
async updateProformaStatusByIdInCompany(
|
||||
companyId: UniqueID,
|
||||
proformaId: UniqueID,
|
||||
newStatus: CustomerInvoiceStatus,
|
||||
transaction?: Transaction
|
||||
): Promise<Result<boolean, Error>> {
|
||||
return this.repository.updateProformaStatusByIdInCompany(
|
||||
companyId,
|
||||
proformaId,
|
||||
newStatus,
|
||||
transaction
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from "./customer-invoice-application.service";
|
||||
@ -0,0 +1,65 @@
|
||||
import { ITransactionManager } from "@erp/core/api";
|
||||
import { ChangeStatusCustomerInvoiceByIdRequestDTO } from "@erp/customer-invoices/common";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import { ProformaCustomerInvoiceDomainService } from "../../domain";
|
||||
import { CustomerInvoiceApplicationService } from "../services";
|
||||
|
||||
type ChangeStatusCustomerInvoiceUseCaseInput = {
|
||||
companyId: UniqueID;
|
||||
proforma_id: string;
|
||||
dto: ChangeStatusCustomerInvoiceByIdRequestDTO;
|
||||
};
|
||||
|
||||
export class ChangeStatusCustomerInvoiceUseCase {
|
||||
private readonly proformaDomainService: ProformaCustomerInvoiceDomainService;
|
||||
|
||||
constructor(
|
||||
private readonly service: CustomerInvoiceApplicationService,
|
||||
private readonly transactionManager: ITransactionManager
|
||||
) {
|
||||
this.proformaDomainService = new ProformaCustomerInvoiceDomainService();
|
||||
}
|
||||
|
||||
public execute(params: ChangeStatusCustomerInvoiceUseCaseInput) {
|
||||
const {
|
||||
proforma_id,
|
||||
companyId,
|
||||
dto: { new_status },
|
||||
} = params;
|
||||
|
||||
const idOrError = UniqueID.create(proforma_id);
|
||||
if (idOrError.isFailure) return Result.fail(idOrError.error);
|
||||
|
||||
const proformaId = idOrError.data;
|
||||
|
||||
return this.transactionManager.complete(async (transaction) => {
|
||||
try {
|
||||
/** 1. Recuperamos la proforma */
|
||||
const proformaResult = await this.service.getInvoiceByIdInCompany(
|
||||
companyId,
|
||||
proformaId,
|
||||
transaction
|
||||
);
|
||||
if (proformaResult.isFailure) return Result.fail(proformaResult.error);
|
||||
const proforma = proformaResult.data;
|
||||
|
||||
/** 2. Hacer el cambio de estado */
|
||||
const transitionResult = await this.proformaDomainService.transition(proforma, new_status!);
|
||||
if (transitionResult.isFailure) return Result.fail(transitionResult.error);
|
||||
|
||||
const updateResult = await this.service.updateProformaStatusByIdInCompany(
|
||||
companyId,
|
||||
proformaId,
|
||||
transitionResult.data.status,
|
||||
transaction
|
||||
);
|
||||
if (updateResult.isFailure) return Result.fail(updateResult.error);
|
||||
|
||||
return Result.ok();
|
||||
} catch (error: unknown) {
|
||||
return Result.fail(error as Error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -4,8 +4,8 @@ import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
import { Transaction } from "sequelize";
|
||||
import { CreateCustomerInvoiceRequestDTO } from "../../../../common/dto";
|
||||
import { CustomerInvoiceApplicationService } from "../../customer-invoice-application.service";
|
||||
import { CustomerInvoiceFullPresenter } from "../../presenters";
|
||||
import { CustomerInvoiceApplicationService } from "../../services/customer-invoice-application.service";
|
||||
import { CreateCustomerInvoicePropsMapper } from "./map-dto-to-create-customer-invoice-props";
|
||||
|
||||
type CreateCustomerInvoiceUseCaseInput = {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { EntityNotFoundError, ITransactionManager } from "@erp/core/api";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import { CustomerInvoiceApplicationService } from "../../application";
|
||||
import { CustomerInvoiceApplicationService } from "../services";
|
||||
|
||||
type DeleteCustomerInvoiceUseCaseInput = {
|
||||
companyId: UniqueID;
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import { CustomerInvoiceApplicationService } from "../../application";
|
||||
import { CustomerInvoiceFullPresenter } from "../presenters/domain";
|
||||
import { CustomerInvoiceApplicationService } from "../services";
|
||||
|
||||
type GetCustomerInvoiceUseCaseInput = {
|
||||
companyId: UniqueID;
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
export * from "./change-status-customer-invoice.use-case";
|
||||
export * from "./create";
|
||||
export * from "./get-customer-invoice.use-case";
|
||||
export * from "./issue-customer-invoice.use-case";
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import { ITransactionManager } from "@erp/core/api";
|
||||
import { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
|
||||
import { UniqueID, UtcDate } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import { ProformaCannotBeConvertedToInvoiceError } from "../../domain";
|
||||
import { ProformaCanTranstionToIssuedSpecification } from "../../domain/specs";
|
||||
import { CustomerInvoiceApplicationService } from "../customer-invoice-application.service";
|
||||
import {
|
||||
IssueCustomerInvoiceDomainService,
|
||||
ProformaCustomerInvoiceDomainService,
|
||||
} from "../../domain";
|
||||
import { CustomerInvoiceFullPresenter } from "../presenters";
|
||||
import { CustomerInvoiceApplicationService } from "../services";
|
||||
|
||||
type IssueCustomerInvoiceUseCaseInput = {
|
||||
companyId: UniqueID;
|
||||
@ -20,21 +23,28 @@ type IssueCustomerInvoiceUseCaseInput = {
|
||||
* - Persiste ambas dentro de la misma transacción
|
||||
*/
|
||||
export class IssueCustomerInvoiceUseCase {
|
||||
private readonly issueDomainService: IssueCustomerInvoiceDomainService;
|
||||
private readonly proformaDomainService: ProformaCustomerInvoiceDomainService;
|
||||
|
||||
constructor(
|
||||
private readonly service: CustomerInvoiceApplicationService,
|
||||
private readonly transactionManager: ITransactionManager
|
||||
) {}
|
||||
private readonly transactionManager: ITransactionManager,
|
||||
private readonly presenterRegistry: IPresenterRegistry
|
||||
) {
|
||||
this.issueDomainService = new IssueCustomerInvoiceDomainService();
|
||||
this.proformaDomainService = new ProformaCustomerInvoiceDomainService();
|
||||
}
|
||||
|
||||
public execute(params: IssueCustomerInvoiceUseCaseInput) {
|
||||
const { proforma_id, companyId } = params;
|
||||
|
||||
const idOrError = UniqueID.create(proforma_id);
|
||||
|
||||
if (idOrError.isFailure) {
|
||||
return Result.fail(idOrError.error);
|
||||
}
|
||||
if (idOrError.isFailure) return Result.fail(idOrError.error);
|
||||
|
||||
const proformaId = idOrError.data;
|
||||
const presenter = this.presenterRegistry.getPresenter({
|
||||
resource: "customer-invoice",
|
||||
projection: "FULL",
|
||||
}) as CustomerInvoiceFullPresenter;
|
||||
|
||||
return this.transactionManager.complete(async (transaction) => {
|
||||
try {
|
||||
@ -44,60 +54,47 @@ export class IssueCustomerInvoiceUseCase {
|
||||
proformaId,
|
||||
transaction
|
||||
);
|
||||
|
||||
if (proformaResult.isFailure) {
|
||||
return Result.fail(proformaResult.error);
|
||||
}
|
||||
|
||||
if (proformaResult.isFailure) return Result.fail(proformaResult.error);
|
||||
const proforma = proformaResult.data;
|
||||
|
||||
/** 2. Comprobamos que la proforma origen está aprovada para generar la factura */
|
||||
const isOk = new ProformaCanTranstionToIssuedSpecification();
|
||||
if (!(await isOk.isSatisfiedBy(proforma))) {
|
||||
return Result.fail(new ProformaCannotBeConvertedToInvoiceError(proformaId.toString()));
|
||||
}
|
||||
|
||||
/** 3. Generar nueva factura */
|
||||
/** 2. Generar nueva factura */
|
||||
const nextNumberResult = await this.service.getNextIssueInvoiceNumber(
|
||||
companyId,
|
||||
proforma.series,
|
||||
transaction
|
||||
);
|
||||
if (nextNumberResult.isFailure) {
|
||||
return Result.fail(nextNumberResult.error);
|
||||
}
|
||||
if (nextNumberResult.isFailure) return Result.fail(nextNumberResult.error);
|
||||
|
||||
const newIssueNumber = nextNumberResult.data;
|
||||
|
||||
// props base obtenidas del agregado proforma
|
||||
const issuedInvoiceOrError = this.service.buildIssueInvoiceInCompany(companyId, proforma, {
|
||||
invoiceNumber: newIssueNumber,
|
||||
invoiceDate: UtcDate.today(),
|
||||
/** 4. Crear factura definitiva (dominio) */
|
||||
const issuedInvoiceOrError = await this.issueDomainService.issueFromProforma(proforma, {
|
||||
issueNumber: nextNumberResult.data,
|
||||
issueDate: UtcDate.today(),
|
||||
});
|
||||
if (issuedInvoiceOrError.isFailure) return Result.fail(issuedInvoiceOrError.error);
|
||||
|
||||
if (issuedInvoiceOrError.isFailure) {
|
||||
return Result.fail(issuedInvoiceOrError.error);
|
||||
}
|
||||
|
||||
const issuedInvoice = issuedInvoiceOrError.data;
|
||||
|
||||
/** 4. Persistencia */
|
||||
await this.service.createInvoiceInCompany(companyId, issuedInvoice, transaction);
|
||||
|
||||
// actualizamos la proforma
|
||||
const updatedProformaResult = proforma.asIssued();
|
||||
if (updatedProformaResult.isFailure) {
|
||||
return Result.fail(updatedProformaResult.error);
|
||||
}
|
||||
|
||||
await this.service.updateInvoiceInCompany(
|
||||
/** 5. Guardar la nueva factura */
|
||||
const saveInvoiceResult = await this.service.createInvoiceInCompany(
|
||||
companyId,
|
||||
updatedProformaResult.data,
|
||||
issuedInvoiceOrError.data,
|
||||
transaction
|
||||
);
|
||||
if (saveInvoiceResult.isFailure) return Result.fail(saveInvoiceResult.error);
|
||||
|
||||
/** 6. Actualizar la proforma */
|
||||
const closedProformaResult = await this.proformaDomainService.markAsIssued(proforma);
|
||||
if (closedProformaResult.isFailure) return Result.fail(closedProformaResult.error);
|
||||
const closedProforma = closedProformaResult.data;
|
||||
|
||||
/** 7. Guardar la proforma */
|
||||
await this.service.updateProformaStatusByIdInCompany(
|
||||
companyId,
|
||||
proformaId,
|
||||
closedProforma.status,
|
||||
transaction
|
||||
);
|
||||
|
||||
/** 5. Resultado */
|
||||
return Result.ok(issuedInvoice);
|
||||
const dto = presenter.toOutput(saveInvoiceResult.data);
|
||||
return Result.ok(dto);
|
||||
} catch (error: unknown) {
|
||||
return Result.fail(error as Error);
|
||||
}
|
||||
|
||||
@ -4,8 +4,8 @@ import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import { Transaction } from "sequelize";
|
||||
import { ListCustomerInvoicesResponseDTO } from "../../../common/dto";
|
||||
import { CustomerInvoiceApplicationService } from "../../application";
|
||||
import { ListCustomerInvoicesPresenter } from "../presenters";
|
||||
import { CustomerInvoiceApplicationService } from "../services";
|
||||
|
||||
type ListCustomerInvoicesUseCaseInput = {
|
||||
companyId: UniqueID;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import { CustomerInvoiceApplicationService } from "../../customer-invoice-application.service";
|
||||
import { CustomerInvoiceApplicationService } from "../../services/customer-invoice-application.service";
|
||||
import { CustomerInvoiceReportPDFPresenter } from "./reporter";
|
||||
|
||||
type ReportCustomerInvoiceUseCaseInput = {
|
||||
|
||||
@ -4,8 +4,8 @@ import { Result } from "@repo/rdx-utils";
|
||||
import { Transaction } from "sequelize";
|
||||
import { UpdateCustomerInvoiceByIdRequestDTO } from "../../../../common";
|
||||
import { CustomerInvoicePatchProps } from "../../../domain";
|
||||
import { CustomerInvoiceApplicationService } from "../../customer-invoice-application.service";
|
||||
import { CustomerInvoiceFullPresenter } from "../../presenters";
|
||||
import { CustomerInvoiceApplicationService } from "../../services/customer-invoice-application.service";
|
||||
import { mapDTOToUpdateCustomerInvoicePatchProps } from "./map-dto-to-update-customer-invoice-props";
|
||||
|
||||
type UpdateCustomerInvoiceUseCaseInput = {
|
||||
|
||||
@ -65,18 +65,8 @@ export interface ICustomerInvoice {
|
||||
hasRecipient: boolean;
|
||||
hasPaymentMethod: boolean;
|
||||
|
||||
_getSubtotalAmount(): InvoiceAmount;
|
||||
getHeaderDiscountAmount(): InvoiceAmount;
|
||||
|
||||
getTaxableAmount(): InvoiceAmount;
|
||||
getTaxesAmount(): InvoiceAmount;
|
||||
getTotalAmount(): InvoiceAmount;
|
||||
|
||||
getTaxes(): InvoiceTaxTotal[];
|
||||
|
||||
asIssued(): Result<CustomerInvoice, Error>;
|
||||
|
||||
getIssuedInvoiceProps(): CustomerInvoiceProps;
|
||||
getProps(): CustomerInvoiceProps;
|
||||
}
|
||||
|
||||
export class CustomerInvoice
|
||||
@ -94,15 +84,19 @@ export class CustomerInvoice
|
||||
currencyCode: props.currencyCode,
|
||||
});
|
||||
}
|
||||
|
||||
getHeaderDiscountAmount(): InvoiceAmount {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
getTaxableAmount(): InvoiceAmount {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
getTaxesAmount(): InvoiceAmount {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
getTotalAmount(): InvoiceAmount {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
@ -348,21 +342,7 @@ export class CustomerInvoice
|
||||
};
|
||||
}
|
||||
|
||||
public asIssued(): Result<CustomerInvoice, Error> {
|
||||
const newProps: CustomerInvoiceProps = {
|
||||
...this.props,
|
||||
status: CustomerInvoiceStatus.createIssued(),
|
||||
};
|
||||
return CustomerInvoice.create(newProps, this.id);
|
||||
}
|
||||
|
||||
public getIssuedInvoiceProps(): CustomerInvoiceProps {
|
||||
return {
|
||||
...this.props,
|
||||
isProforma: false,
|
||||
proformaId: Maybe.some(this.id),
|
||||
status: CustomerInvoiceStatus.createIssued(),
|
||||
invoiceDate: UtcDate.today(),
|
||||
};
|
||||
public getProps(): CustomerInvoiceProps {
|
||||
return this.props;
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ import {
|
||||
ItemDiscount,
|
||||
ItemQuantity,
|
||||
} from "../../value-objects";
|
||||
import { ItemTaxTotal, ItemTaxes } from "../item-taxes";
|
||||
import { ItemTaxes, ItemTaxTotal } from "../item-taxes";
|
||||
|
||||
export interface CustomerInvoiceItemProps {
|
||||
description: Maybe<CustomerInvoiceItemDescription>;
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
import { DomainError } from "@repo/rdx-ddd";
|
||||
|
||||
/**
|
||||
* Error de dominio que indica que el documento no es una Proforma.
|
||||
*
|
||||
* @remarks
|
||||
* - Se lanza cuando una operación requiere explícitamente una Proforma
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class EntityIsNotProformaError extends DomainError {
|
||||
/**
|
||||
* Crea una instancia del error con el identificador de la Proforma.
|
||||
*
|
||||
* @param id - Identificador de la Proforma.
|
||||
* @param options - Opciones nativas de Error (puedes pasar `cause`).
|
||||
*/
|
||||
constructor(id: string, options?: ErrorOptions) {
|
||||
super(`Error. Entity with id '${id}' is not a proforma .`, options);
|
||||
this.name = "EntityIsNotProformaError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* *Type guard* para `EntityIsNotProformaError`.
|
||||
*
|
||||
* @param e - Error desconocido
|
||||
* @returns `true` si `e` es `EntityIsNotProformaError`
|
||||
*/
|
||||
export const isEntityIsNotProformaError = (e: unknown): e is EntityIsNotProformaError =>
|
||||
e instanceof EntityIsNotProformaError;
|
||||
@ -1,2 +1,4 @@
|
||||
export * from "./customer-invoice-id-already-exits-error";
|
||||
export * from "./entity-is-not-proforma-error";
|
||||
export * from "./invalid-proforma-transition-error";
|
||||
export * from "./proforma-cannot-be-converted-to-invoice-error";
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
import { DomainError } from "@repo/rdx-ddd";
|
||||
|
||||
export class InvalidProformaTransitionError extends DomainError {
|
||||
constructor(current: string, next: string, id: string) {
|
||||
super(`Invalid transition for proforma ${id}: ${current} → ${next}`);
|
||||
this.name = "InvalidProformaTransitionError";
|
||||
}
|
||||
}
|
||||
|
||||
export const isInvalidProformaTransitionError = (e: unknown): e is InvalidProformaTransitionError =>
|
||||
e instanceof InvalidProformaTransitionError;
|
||||
@ -3,6 +3,7 @@ import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Collection, Result } from "@repo/rdx-utils";
|
||||
import { CustomerInvoiceListDTO } from "../../infrastructure";
|
||||
import { CustomerInvoice } from "../aggregates";
|
||||
import { CustomerInvoiceStatus } from "../value-objects";
|
||||
|
||||
/**
|
||||
* Interfaz del repositorio para el agregado `CustomerInvoice`.
|
||||
@ -80,4 +81,21 @@ export interface ICustomerInvoiceRepository {
|
||||
id: UniqueID,
|
||||
transaction: unknown
|
||||
): Promise<Result<boolean, Error>>;
|
||||
|
||||
/**
|
||||
*
|
||||
* Actualiza el "status" de una proforma
|
||||
*
|
||||
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
|
||||
* @param id - UUID de la factura a eliminar.
|
||||
* @param newStatus - nuevo estado
|
||||
* @param transaction - Transacción activa para la operación.
|
||||
* @returns Result<boolean, Error>
|
||||
*/
|
||||
updateProformaStatusByIdInCompany(
|
||||
companyId: UniqueID,
|
||||
id: UniqueID,
|
||||
newStatus: CustomerInvoiceStatus,
|
||||
transaction: unknown
|
||||
): Promise<Result<boolean, Error>>;
|
||||
}
|
||||
|
||||
@ -1 +1,3 @@
|
||||
export * from "./customer-invoice-number-generator.interface";
|
||||
export * from "./issue-customer-invoice-domain-service";
|
||||
export * from "./proforma-customer-invoice-domain-service";
|
||||
|
||||
@ -0,0 +1,68 @@
|
||||
import { UtcDate } from "@repo/rdx-ddd";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
import { CustomerInvoice } from "../aggregates";
|
||||
import { EntityIsNotProformaError, ProformaCannotBeConvertedToInvoiceError } from "../errors";
|
||||
import {
|
||||
CustomerInvoiceIsProformaSpecification,
|
||||
ProformaCanTranstionToIssuedSpecification,
|
||||
} from "../specs";
|
||||
import { CustomerInvoiceNumber, CustomerInvoiceStatus } from "../value-objects";
|
||||
|
||||
/**
|
||||
* Servicio de dominio que encapsula la lógica de emisión de factura definitiva desde una proforma.
|
||||
*/
|
||||
export class IssueCustomerInvoiceDomainService {
|
||||
private readonly isProformaSpec = new CustomerInvoiceIsProformaSpecification();
|
||||
private readonly isApprovedSpec = new ProformaCanTranstionToIssuedSpecification();
|
||||
|
||||
public linkWithProforma(
|
||||
invoice: CustomerInvoice,
|
||||
proforma: CustomerInvoice
|
||||
): Result<CustomerInvoice, Error> {}
|
||||
|
||||
/**
|
||||
* Convierte una proforma en factura definitiva.
|
||||
*
|
||||
* @param proforma - Entidad CustomerInvoice en estado proforma aprobada.
|
||||
* @param params.issueNumber - Número de la nueva factura.
|
||||
* @param params.issueDate - Fecha de emisión.
|
||||
* @returns Result<CustomerInvoice, Error> - Nueva factura emitida o error de dominio.
|
||||
*/
|
||||
public async issueFromProforma(
|
||||
proforma: CustomerInvoice,
|
||||
params: {
|
||||
issueNumber: CustomerInvoiceNumber;
|
||||
issueDate: UtcDate;
|
||||
}
|
||||
): Promise<Result<CustomerInvoice, Error>> {
|
||||
const { issueDate, issueNumber } = params;
|
||||
|
||||
/** 1. Validar que la entidad origen es una proforma */
|
||||
if (!(await this.isProformaSpec.isSatisfiedBy(proforma))) {
|
||||
return Result.fail(new EntityIsNotProformaError(proforma.id.toString()));
|
||||
}
|
||||
|
||||
/** 2. Validar que la proforma puede emitirse */
|
||||
if (!(await this.isApprovedSpec.isSatisfiedBy(proforma))) {
|
||||
return Result.fail(new ProformaCannotBeConvertedToInvoiceError(proforma.id.toString()));
|
||||
}
|
||||
|
||||
/** 3. Generar la nueva factura definitiva (inmutable) */
|
||||
const proformaProps = proforma.getProps();
|
||||
const newInvoiceOrError = CustomerInvoice.create({
|
||||
...proformaProps,
|
||||
isProforma: false,
|
||||
proformaId: Maybe.some(proforma.id),
|
||||
status: CustomerInvoiceStatus.createIssued(),
|
||||
invoiceNumber: issueNumber,
|
||||
invoiceDate: issueDate,
|
||||
description: proformaProps.description.isNone() ? Maybe.some(".") : proformaProps.description,
|
||||
});
|
||||
|
||||
if (newInvoiceOrError.isFailure) {
|
||||
return Result.fail(newInvoiceOrError.error);
|
||||
}
|
||||
|
||||
return Result.ok(newInvoiceOrError.data);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import { CustomerInvoice } from "../aggregates";
|
||||
import { EntityIsNotProformaError, InvalidProformaTransitionError } from "../errors";
|
||||
import { CustomerInvoiceIsProformaSpecification } from "../specs";
|
||||
import { CustomerInvoiceStatus, INVOICE_STATUS } from "../value-objects";
|
||||
|
||||
/**
|
||||
* Servicio de dominio que encapsula la lógica de emisión de factura definitiva desde una proforma.
|
||||
*/
|
||||
export class ProformaCustomerInvoiceDomainService {
|
||||
/** Aplica la transición si está permitida según INVOICE_TRANSITIONS. */
|
||||
async transition(
|
||||
proforma: CustomerInvoice,
|
||||
nextStatus: string
|
||||
): Promise<Result<CustomerInvoice, Error>> {
|
||||
// Validar que la entidad es una proforma
|
||||
const isProformaSpec = new CustomerInvoiceIsProformaSpecification();
|
||||
if (!(await isProformaSpec.isSatisfiedBy(proforma))) {
|
||||
return Result.fail(new EntityIsNotProformaError(proforma.id.toString()));
|
||||
}
|
||||
|
||||
const current = proforma.status.toString();
|
||||
const allowed = proforma.canTransitionTo(nextStatus);
|
||||
|
||||
if (!allowed) {
|
||||
return Result.fail(
|
||||
new InvalidProformaTransitionError(current, nextStatus, proforma.id.toString())
|
||||
);
|
||||
}
|
||||
|
||||
// Validaciones adicionales de dominio, si las hubiera
|
||||
// (por ejemplo, no aprobar si no hay líneas)
|
||||
// new ProformaHasLinesSpecification().isSatisfiedBy(proforma)
|
||||
|
||||
return CustomerInvoice.create({
|
||||
...proforma.getProps(),
|
||||
status: CustomerInvoiceStatus.create(nextStatus).data,
|
||||
});
|
||||
}
|
||||
|
||||
/** Envía la proforma (draft → sent) */
|
||||
async send(proforma: CustomerInvoice): Promise<Result<CustomerInvoice, Error>> {
|
||||
return this.transition(proforma, INVOICE_STATUS.SENT);
|
||||
}
|
||||
|
||||
/** Aprueba la proforma (sent → approved) */
|
||||
async approve(proforma: CustomerInvoice): Promise<Result<CustomerInvoice, Error>> {
|
||||
return this.transition(proforma, INVOICE_STATUS.APPROVED);
|
||||
}
|
||||
|
||||
/** Rechaza la proforma (sent → rejected) */
|
||||
async reject(proforma: CustomerInvoice): Promise<Result<CustomerInvoice, Error>> {
|
||||
return this.transition(proforma, INVOICE_STATUS.REJECTED);
|
||||
}
|
||||
|
||||
/** Reabre una proforma rechazada (rejected → draft) */
|
||||
async reopen(proforma: CustomerInvoice): Promise<Result<CustomerInvoice, Error>> {
|
||||
return this.transition(proforma, INVOICE_STATUS.DRAFT);
|
||||
}
|
||||
|
||||
/** Marca la proforma como emitida (approved → issued) */
|
||||
async markAsIssued(proforma: CustomerInvoice): Promise<Result<CustomerInvoice, Error>> {
|
||||
return this.transition(proforma, INVOICE_STATUS.ISSUED);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
import { CompositeSpecification } from "@repo/rdx-ddd";
|
||||
import { CustomerInvoice } from "../aggregates";
|
||||
|
||||
export class CustomerInvoiceIsProformaSpecification extends CompositeSpecification<CustomerInvoice> {
|
||||
public async isSatisfiedBy(proforma: CustomerInvoice): Promise<boolean> {
|
||||
return proforma.isProforma;
|
||||
}
|
||||
}
|
||||
@ -1 +1,2 @@
|
||||
export * from "./customer-invoice-is-proforma.specification";
|
||||
export * from "./proforma-can-transtion-to-issued.specification";
|
||||
|
||||
@ -4,6 +4,6 @@ import { INVOICE_STATUS } from "../value-objects";
|
||||
|
||||
export class ProformaCanTranstionToIssuedSpecification extends CompositeSpecification<CustomerInvoice> {
|
||||
public async isSatisfiedBy(proforma: CustomerInvoice): Promise<boolean> {
|
||||
return proforma.isProforma && proforma.canTransitionTo(INVOICE_STATUS.ISSUED);
|
||||
return proforma.canTransitionTo(INVOICE_STATUS.ISSUED);
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,18 +15,20 @@ export enum INVOICE_STATUS {
|
||||
// status === "issued" <- (si is_proforma === false) => Factura y enviará/enviada a Veri*Factu
|
||||
ISSUED = "issued",
|
||||
}
|
||||
|
||||
const INVOICE_TRANSITIONS: Record<string, string[]> = {
|
||||
draft: [INVOICE_STATUS.SENT],
|
||||
sent: [INVOICE_STATUS.APPROVED, INVOICE_STATUS.REJECTED],
|
||||
approved: [INVOICE_STATUS.ISSUED],
|
||||
rejected: [INVOICE_STATUS.DRAFT],
|
||||
issued: [],
|
||||
};
|
||||
|
||||
export class CustomerInvoiceStatus extends ValueObject<ICustomerInvoiceStatusProps> {
|
||||
private static readonly ALLOWED_STATUSES = ["draft", "sent", "approved", "rejected", "issued"];
|
||||
private static readonly FIELD = "invoiceStatus";
|
||||
private static readonly ERROR_CODE = "INVALID_INVOICE_STATUS";
|
||||
|
||||
private static readonly TRANSITIONS: Record<string, string[]> = {
|
||||
draft: [INVOICE_STATUS.SENT],
|
||||
sent: [INVOICE_STATUS.APPROVED, INVOICE_STATUS.REJECTED],
|
||||
approved: [INVOICE_STATUS.ISSUED],
|
||||
rejected: [INVOICE_STATUS.DRAFT],
|
||||
};
|
||||
|
||||
static create(value: string): Result<CustomerInvoiceStatus, Error> {
|
||||
if (!CustomerInvoiceStatus.ALLOWED_STATUSES.includes(value)) {
|
||||
const detail = `Estado de la factura no válido: ${value}`;
|
||||
@ -89,7 +91,7 @@ export class CustomerInvoiceStatus extends ValueObject<ICustomerInvoiceStatusPro
|
||||
}
|
||||
|
||||
canTransitionTo(nextStatus: string): boolean {
|
||||
return CustomerInvoiceStatus.TRANSITIONS[this.props.value].includes(nextStatus);
|
||||
return INVOICE_TRANSITIONS[this.props.value].includes(nextStatus);
|
||||
}
|
||||
|
||||
toString() {
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { IModuleServer, ModuleParams } from "@erp/core/api";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Transaction } from "sequelize";
|
||||
import { customerInvoicesRouter, models } from "./infrastructure";
|
||||
import { buildCustomerInvoiceDependencies } from "./infrastructure/dependencies";
|
||||
import { buildCustomerInvoiceDependencies, customerInvoicesRouter, models } from "./infrastructure";
|
||||
|
||||
export const customerInvoicesAPIModule: IModuleServer = {
|
||||
name: "customer-invoices",
|
||||
@ -17,7 +16,7 @@ export const customerInvoicesAPIModule: IModuleServer = {
|
||||
},
|
||||
|
||||
async registerDependencies(params) {
|
||||
const { logger, listServices } = params; /* = ModuleParams & {
|
||||
const { logger } = params; /* = ModuleParams & {
|
||||
getService: (name: string) => any;
|
||||
};*/
|
||||
|
||||
@ -25,9 +24,6 @@ export const customerInvoicesAPIModule: IModuleServer = {
|
||||
label: this.name,
|
||||
});
|
||||
|
||||
logger.info(listServices());
|
||||
//getService()
|
||||
|
||||
const deps = buildCustomerInvoiceDependencies(params);
|
||||
|
||||
return {
|
||||
@ -38,9 +34,9 @@ export const customerInvoicesAPIModule: IModuleServer = {
|
||||
invoiceId: UniqueID,
|
||||
transaction?: Transaction
|
||||
) => {
|
||||
const { service } = deps;
|
||||
/*const { service } = deps;
|
||||
|
||||
return service.getInvoiceByIdInCompany(companyId, invoiceId, transaction);
|
||||
return service.getInvoiceByIdInCompany(companyId, invoiceId, transaction);*/
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -0,0 +1,169 @@
|
||||
// modules/invoice/infrastructure/invoice-dependencies.factory.ts
|
||||
|
||||
import { JsonTaxCatalogProvider, SpainTaxCatalogProvider } from "@erp/core";
|
||||
import type { IMapperRegistry, IPresenterRegistry, ModuleParams } from "@erp/core/api";
|
||||
import {
|
||||
InMemoryMapperRegistry,
|
||||
InMemoryPresenterRegistry,
|
||||
SequelizeTransactionManager,
|
||||
} from "@erp/core/api";
|
||||
import {
|
||||
CreateCustomerInvoiceUseCase,
|
||||
CustomerInvoiceApplicationService,
|
||||
CustomerInvoiceFullPresenter,
|
||||
CustomerInvoiceItemsFullPresenter,
|
||||
CustomerInvoiceItemsReportPersenter,
|
||||
CustomerInvoiceReportHTMLPresenter,
|
||||
CustomerInvoiceReportPDFPresenter,
|
||||
CustomerInvoiceReportPresenter,
|
||||
GetCustomerInvoiceUseCase,
|
||||
IssueCustomerInvoiceUseCase,
|
||||
ListCustomerInvoicesPresenter,
|
||||
ListCustomerInvoicesUseCase,
|
||||
RecipientInvoiceFullPresenter,
|
||||
ReportCustomerInvoiceUseCase,
|
||||
UpdateCustomerInvoiceUseCase,
|
||||
} from "../application";
|
||||
import { CustomerInvoiceDomainMapper, CustomerInvoiceListMapper } from "./mappers";
|
||||
import { CustomerInvoiceRepository } from "./sequelize";
|
||||
import { SequelizeInvoiceNumberGenerator } from "./services";
|
||||
|
||||
export type CustomerInvoiceDeps = {
|
||||
transactionManager: SequelizeTransactionManager;
|
||||
mapperRegistry: IMapperRegistry;
|
||||
presenterRegistry: IPresenterRegistry;
|
||||
repo: CustomerInvoiceRepository;
|
||||
service: CustomerInvoiceApplicationService;
|
||||
catalogs: {
|
||||
taxes: JsonTaxCatalogProvider;
|
||||
};
|
||||
build: {
|
||||
list: () => ListCustomerInvoicesUseCase;
|
||||
get: () => GetCustomerInvoiceUseCase;
|
||||
create: () => CreateCustomerInvoiceUseCase;
|
||||
update: () => UpdateCustomerInvoiceUseCase;
|
||||
//delete: () => DeleteCustomerInvoiceUseCase;
|
||||
report: () => ReportCustomerInvoiceUseCase;
|
||||
issue: () => IssueCustomerInvoiceUseCase;
|
||||
};
|
||||
getService: (name: string) => any;
|
||||
listServices: () => string[];
|
||||
};
|
||||
|
||||
export function buildCustomerInvoiceDependencies(params: ModuleParams): CustomerInvoiceDeps {
|
||||
const { database, listServices, getService } = params;
|
||||
const transactionManager = new SequelizeTransactionManager(database);
|
||||
const catalogs = { taxes: SpainTaxCatalogProvider() };
|
||||
|
||||
// Mapper Registry
|
||||
const mapperRegistry = new InMemoryMapperRegistry();
|
||||
mapperRegistry
|
||||
.registerDomainMapper(
|
||||
{ resource: "customer-invoice" },
|
||||
new CustomerInvoiceDomainMapper({ taxCatalog: catalogs.taxes })
|
||||
)
|
||||
.registerQueryMappers([
|
||||
{
|
||||
key: { resource: "customer-invoice", query: "LIST" },
|
||||
mapper: new CustomerInvoiceListMapper(),
|
||||
},
|
||||
]);
|
||||
|
||||
// Repository & Services
|
||||
const repo = new CustomerInvoiceRepository({ mapperRegistry, database });
|
||||
const numberGenerator = new SequelizeInvoiceNumberGenerator();
|
||||
const service = new CustomerInvoiceApplicationService(repo, numberGenerator);
|
||||
|
||||
// Presenter Registry
|
||||
const presenterRegistry = new InMemoryPresenterRegistry();
|
||||
presenterRegistry.registerPresenters([
|
||||
{
|
||||
key: {
|
||||
resource: "customer-invoice-items",
|
||||
projection: "FULL",
|
||||
},
|
||||
presenter: new CustomerInvoiceItemsFullPresenter(presenterRegistry),
|
||||
},
|
||||
{
|
||||
key: {
|
||||
resource: "recipient-invoice",
|
||||
projection: "FULL",
|
||||
},
|
||||
presenter: new RecipientInvoiceFullPresenter(presenterRegistry),
|
||||
},
|
||||
{
|
||||
key: {
|
||||
resource: "customer-invoice",
|
||||
projection: "FULL",
|
||||
},
|
||||
presenter: new CustomerInvoiceFullPresenter(presenterRegistry),
|
||||
},
|
||||
{
|
||||
key: {
|
||||
resource: "customer-invoice",
|
||||
projection: "LIST",
|
||||
},
|
||||
presenter: new ListCustomerInvoicesPresenter(presenterRegistry),
|
||||
},
|
||||
{
|
||||
key: {
|
||||
resource: "customer-invoice",
|
||||
projection: "REPORT",
|
||||
format: "JSON",
|
||||
},
|
||||
presenter: new CustomerInvoiceReportPresenter(presenterRegistry),
|
||||
},
|
||||
{
|
||||
key: {
|
||||
resource: "customer-invoice-items",
|
||||
projection: "REPORT",
|
||||
format: "JSON",
|
||||
},
|
||||
presenter: new CustomerInvoiceItemsReportPersenter(presenterRegistry),
|
||||
},
|
||||
{
|
||||
key: {
|
||||
resource: "customer-invoice",
|
||||
projection: "REPORT",
|
||||
format: "HTML",
|
||||
},
|
||||
presenter: new CustomerInvoiceReportHTMLPresenter(presenterRegistry),
|
||||
},
|
||||
{
|
||||
key: {
|
||||
resource: "customer-invoice",
|
||||
projection: "REPORT",
|
||||
format: "PDF",
|
||||
},
|
||||
presenter: new CustomerInvoiceReportPDFPresenter(presenterRegistry),
|
||||
},
|
||||
]);
|
||||
|
||||
return {
|
||||
transactionManager,
|
||||
repo,
|
||||
mapperRegistry,
|
||||
presenterRegistry,
|
||||
service,
|
||||
catalogs,
|
||||
build: {
|
||||
list: () => new ListCustomerInvoicesUseCase(service, transactionManager, presenterRegistry),
|
||||
get: () => new GetCustomerInvoiceUseCase(service, transactionManager, presenterRegistry),
|
||||
create: () =>
|
||||
new CreateCustomerInvoiceUseCase(
|
||||
service,
|
||||
transactionManager,
|
||||
presenterRegistry,
|
||||
catalogs.taxes
|
||||
),
|
||||
update: () =>
|
||||
new UpdateCustomerInvoiceUseCase(service, transactionManager, presenterRegistry),
|
||||
// delete: () => new DeleteCustomerInvoiceUseCase(service, transactionManager),
|
||||
report: () =>
|
||||
new ReportCustomerInvoiceUseCase(service, transactionManager, presenterRegistry),
|
||||
issue: () => new IssueCustomerInvoiceUseCase(service, transactionManager),
|
||||
},
|
||||
listServices,
|
||||
getService,
|
||||
};
|
||||
}
|
||||
@ -8,6 +8,7 @@ import {
|
||||
SequelizeTransactionManager,
|
||||
} from "@erp/core/api";
|
||||
import {
|
||||
ChangeStatusCustomerInvoiceUseCase,
|
||||
CreateCustomerInvoiceUseCase,
|
||||
CustomerInvoiceApplicationService,
|
||||
CustomerInvoiceFullPresenter,
|
||||
@ -33,11 +34,11 @@ export type CustomerInvoiceDeps = {
|
||||
mapperRegistry: IMapperRegistry;
|
||||
presenterRegistry: IPresenterRegistry;
|
||||
repo: CustomerInvoiceRepository;
|
||||
service: CustomerInvoiceApplicationService;
|
||||
appService: CustomerInvoiceApplicationService;
|
||||
catalogs: {
|
||||
taxes: JsonTaxCatalogProvider;
|
||||
};
|
||||
build: {
|
||||
useCases: {
|
||||
list: () => ListCustomerInvoicesUseCase;
|
||||
get: () => GetCustomerInvoiceUseCase;
|
||||
create: () => CreateCustomerInvoiceUseCase;
|
||||
@ -45,17 +46,19 @@ export type CustomerInvoiceDeps = {
|
||||
//delete: () => DeleteCustomerInvoiceUseCase;
|
||||
report: () => ReportCustomerInvoiceUseCase;
|
||||
issue: () => IssueCustomerInvoiceUseCase;
|
||||
changeStatus: () => ChangeStatusCustomerInvoiceUseCase;
|
||||
};
|
||||
getService: (name: string) => any;
|
||||
listServices: () => string[];
|
||||
};
|
||||
|
||||
export function buildCustomerInvoiceDependencies(params: ModuleParams): CustomerInvoiceDeps {
|
||||
const { database, listServices, getService } = params;
|
||||
const transactionManager = new SequelizeTransactionManager(database);
|
||||
const { database } = params;
|
||||
|
||||
/** Dominio */
|
||||
const catalogs = { taxes: SpainTaxCatalogProvider() };
|
||||
|
||||
// Mapper Registry
|
||||
/** Infraestructura */
|
||||
const transactionManager = new SequelizeTransactionManager(database);
|
||||
|
||||
const mapperRegistry = new InMemoryMapperRegistry();
|
||||
mapperRegistry
|
||||
.registerDomainMapper(
|
||||
@ -70,100 +73,74 @@ export function buildCustomerInvoiceDependencies(params: ModuleParams): Customer
|
||||
]);
|
||||
|
||||
// Repository & Services
|
||||
const repo = new CustomerInvoiceRepository({ mapperRegistry, database });
|
||||
const repository = new CustomerInvoiceRepository({ mapperRegistry, database });
|
||||
const numberGenerator = new SequelizeInvoiceNumberGenerator();
|
||||
const service = new CustomerInvoiceApplicationService(repo, numberGenerator);
|
||||
|
||||
/** Aplicación */
|
||||
const appService = new CustomerInvoiceApplicationService(repository, numberGenerator);
|
||||
|
||||
// Presenter Registry
|
||||
const presenterRegistry = new InMemoryPresenterRegistry();
|
||||
presenterRegistry.registerPresenters([
|
||||
{
|
||||
key: {
|
||||
resource: "customer-invoice-items",
|
||||
projection: "FULL",
|
||||
},
|
||||
key: { resource: "customer-invoice-items", projection: "FULL" },
|
||||
presenter: new CustomerInvoiceItemsFullPresenter(presenterRegistry),
|
||||
},
|
||||
{
|
||||
key: {
|
||||
resource: "recipient-invoice",
|
||||
projection: "FULL",
|
||||
},
|
||||
key: { resource: "recipient-invoice", projection: "FULL" },
|
||||
presenter: new RecipientInvoiceFullPresenter(presenterRegistry),
|
||||
},
|
||||
{
|
||||
key: {
|
||||
resource: "customer-invoice",
|
||||
projection: "FULL",
|
||||
},
|
||||
key: { resource: "customer-invoice", projection: "FULL" },
|
||||
presenter: new CustomerInvoiceFullPresenter(presenterRegistry),
|
||||
},
|
||||
{
|
||||
key: {
|
||||
resource: "customer-invoice",
|
||||
projection: "LIST",
|
||||
},
|
||||
key: { resource: "customer-invoice", projection: "LIST" },
|
||||
presenter: new ListCustomerInvoicesPresenter(presenterRegistry),
|
||||
},
|
||||
{
|
||||
key: {
|
||||
resource: "customer-invoice",
|
||||
projection: "REPORT",
|
||||
format: "JSON",
|
||||
},
|
||||
key: { resource: "customer-invoice", projection: "REPORT", format: "JSON" },
|
||||
presenter: new CustomerInvoiceReportPresenter(presenterRegistry),
|
||||
},
|
||||
{
|
||||
key: {
|
||||
resource: "customer-invoice-items",
|
||||
projection: "REPORT",
|
||||
format: "JSON",
|
||||
},
|
||||
key: { resource: "customer-invoice-items", projection: "REPORT", format: "JSON" },
|
||||
presenter: new CustomerInvoiceItemsReportPersenter(presenterRegistry),
|
||||
},
|
||||
{
|
||||
key: {
|
||||
resource: "customer-invoice",
|
||||
projection: "REPORT",
|
||||
format: "HTML",
|
||||
},
|
||||
key: { resource: "customer-invoice", projection: "REPORT", format: "HTML" },
|
||||
presenter: new CustomerInvoiceReportHTMLPresenter(presenterRegistry),
|
||||
},
|
||||
{
|
||||
key: {
|
||||
resource: "customer-invoice",
|
||||
projection: "REPORT",
|
||||
format: "PDF",
|
||||
},
|
||||
key: { resource: "customer-invoice", projection: "REPORT", format: "PDF" },
|
||||
presenter: new CustomerInvoiceReportPDFPresenter(presenterRegistry),
|
||||
},
|
||||
]);
|
||||
|
||||
const useCases = {
|
||||
list: () => new ListCustomerInvoicesUseCase(appService, transactionManager, presenterRegistry),
|
||||
get: () => new GetCustomerInvoiceUseCase(appService, transactionManager, presenterRegistry),
|
||||
create: () =>
|
||||
new CreateCustomerInvoiceUseCase(
|
||||
appService,
|
||||
transactionManager,
|
||||
presenterRegistry,
|
||||
catalogs.taxes
|
||||
),
|
||||
update: () =>
|
||||
new UpdateCustomerInvoiceUseCase(appService, transactionManager, presenterRegistry),
|
||||
report: () =>
|
||||
new ReportCustomerInvoiceUseCase(appService, transactionManager, presenterRegistry),
|
||||
issue: () => new IssueCustomerInvoiceUseCase(appService, transactionManager, presenterRegistry),
|
||||
changeStatus: () => new ChangeStatusCustomerInvoiceUseCase(appService, transactionManager),
|
||||
};
|
||||
|
||||
return {
|
||||
transactionManager,
|
||||
repo,
|
||||
repo: repository,
|
||||
mapperRegistry,
|
||||
presenterRegistry,
|
||||
service,
|
||||
appService,
|
||||
catalogs,
|
||||
build: {
|
||||
list: () => new ListCustomerInvoicesUseCase(service, transactionManager, presenterRegistry),
|
||||
get: () => new GetCustomerInvoiceUseCase(service, transactionManager, presenterRegistry),
|
||||
create: () =>
|
||||
new CreateCustomerInvoiceUseCase(
|
||||
service,
|
||||
transactionManager,
|
||||
presenterRegistry,
|
||||
catalogs.taxes
|
||||
),
|
||||
update: () =>
|
||||
new UpdateCustomerInvoiceUseCase(service, transactionManager, presenterRegistry),
|
||||
// delete: () => new DeleteCustomerInvoiceUseCase(service, transactionManager),
|
||||
report: () =>
|
||||
new ReportCustomerInvoiceUseCase(service, transactionManager, presenterRegistry),
|
||||
issue: () => new IssueCustomerInvoiceUseCase(service, transactionManager),
|
||||
},
|
||||
listServices,
|
||||
getService,
|
||||
useCases,
|
||||
};
|
||||
}
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||
import { ChangeStatusCustomerInvoiceByIdRequestDTO } from "@erp/customer-invoices/common";
|
||||
import { ChangeStatusCustomerInvoiceUseCase } from "../../../application";
|
||||
import { customerInvoicesApiErrorMapper } from "../customer-invoices-api-error-mapper";
|
||||
|
||||
export class ChangeStatusCustomerInvoiceController extends ExpressController {
|
||||
public constructor(private readonly useCase: ChangeStatusCustomerInvoiceUseCase) {
|
||||
super();
|
||||
this.errorMapper = customerInvoicesApiErrorMapper;
|
||||
|
||||
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
||||
}
|
||||
|
||||
async executeImpl(): Promise<any> {
|
||||
const companyId = this.getTenantId(); // garantizado por tenantGuard
|
||||
if (!companyId) {
|
||||
return this.forbiddenError("Tenant ID not found");
|
||||
}
|
||||
|
||||
const { proforma_id } = this.req.params;
|
||||
if (!proforma_id) {
|
||||
return this.invalidInputError("Proforma ID missing");
|
||||
}
|
||||
|
||||
const dto = this.req.body as ChangeStatusCustomerInvoiceByIdRequestDTO;
|
||||
const result = await this.useCase.execute({ proforma_id, dto, companyId });
|
||||
|
||||
return result.match(
|
||||
(data) => this.ok(data),
|
||||
(err) => this.handleError(err)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,8 @@
|
||||
export * from "./change-status-customer-invoice.controller";
|
||||
export * from "./create-customer-invoice.controller";
|
||||
//export * from "./delete-customer-invoice.controller";
|
||||
export * from "./get-customer-invoice.controller";
|
||||
//export * from "./issue-customer-invoice.controller";
|
||||
export * from "./issue-customer-invoice.controller";
|
||||
export * from "./list-customer-invoices.controller";
|
||||
export * from "./report-customer-invoice.controller";
|
||||
export * from "./update-customer-invoice.controller";
|
||||
|
||||
@ -9,7 +9,9 @@ import {
|
||||
} from "@erp/core/api";
|
||||
import {
|
||||
CustomerInvoiceIdAlreadyExistsError,
|
||||
EntityIsNotProformaError,
|
||||
isCustomerInvoiceIdAlreadyExistsError,
|
||||
isEntityIsNotProformaError,
|
||||
isProformaCannotBeConvertedToInvoiceError,
|
||||
ProformaCannotBeConvertedToInvoiceError,
|
||||
} from "../../domain";
|
||||
@ -25,6 +27,15 @@ const invoiceDuplicateRule: ErrorToApiRule = {
|
||||
),
|
||||
};
|
||||
|
||||
const entityIsNotProformaError: ErrorToApiRule = {
|
||||
priority: 120,
|
||||
matches: (e) => isEntityIsNotProformaError(e),
|
||||
build: (e) =>
|
||||
new ValidationApiError(
|
||||
(e as EntityIsNotProformaError).message || "Entity with the provided id is not proforma"
|
||||
),
|
||||
};
|
||||
|
||||
const proformaConversionRule: ErrorToApiRule = {
|
||||
priority: 120,
|
||||
matches: (e) => isProformaCannotBeConvertedToInvoiceError(e),
|
||||
@ -38,4 +49,5 @@ const proformaConversionRule: ErrorToApiRule = {
|
||||
// Cómo aplicarla: crea una nueva instancia del mapper con la regla extra
|
||||
export const customerInvoicesApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default()
|
||||
.register(invoiceDuplicateRule)
|
||||
.register(entityIsNotProformaError)
|
||||
.register(proformaConversionRule);
|
||||
|
||||
@ -4,6 +4,8 @@ import { ILogger } from "@repo/rdx-logger";
|
||||
import { Application, NextFunction, Request, Response, Router } from "express";
|
||||
import { Sequelize } from "sequelize";
|
||||
import {
|
||||
ChangeStatusCustomerInvoiceByIdParamsRequestSchema,
|
||||
ChangeStatusCustomerInvoiceByIdRequestSchema,
|
||||
CreateCustomerInvoiceRequestSchema,
|
||||
CustomerInvoiceListRequestSchema,
|
||||
GetCustomerInvoiceByIdRequestSchema,
|
||||
@ -13,6 +15,7 @@ import {
|
||||
} from "../../../common/dto";
|
||||
import { buildCustomerInvoiceDependencies } from "../dependencies";
|
||||
import {
|
||||
ChangeStatusCustomerInvoiceController,
|
||||
CreateCustomerInvoiceController,
|
||||
GetCustomerInvoiceController,
|
||||
ListCustomerInvoicesController,
|
||||
@ -55,7 +58,7 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
|
||||
//checkTabContext,
|
||||
validateRequest(CustomerInvoiceListRequestSchema, "params"),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const useCase = deps.build.list();
|
||||
const useCase = deps.useCases.list();
|
||||
const controller = new ListCustomerInvoicesController(useCase /*, deps.presenters.list */);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
@ -66,7 +69,7 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
|
||||
//checkTabContext,
|
||||
validateRequest(GetCustomerInvoiceByIdRequestSchema, "params"),
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const useCase = deps.build.get();
|
||||
const useCase = deps.useCases.get();
|
||||
const controller = new GetCustomerInvoiceController(useCase);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
@ -78,7 +81,7 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
|
||||
|
||||
validateRequest(CreateCustomerInvoiceRequestSchema, "body"),
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const useCase = deps.build.create();
|
||||
const useCase = deps.useCases.create();
|
||||
const controller = new CreateCustomerInvoiceController(useCase);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
@ -91,7 +94,7 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
|
||||
validateRequest(UpdateCustomerInvoiceByIdParamsRequestSchema, "params"),
|
||||
validateRequest(UpdateCustomerInvoiceByIdRequestSchema, "body"),
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const useCase = deps.build.update();
|
||||
const useCase = deps.useCases.update();
|
||||
const controller = new UpdateCustomerInvoiceController(useCase);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
@ -103,7 +106,7 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
|
||||
|
||||
validateRequest(DeleteCustomerInvoiceByIdRequestSchema, "params"),
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const useCase = deps.build.delete();
|
||||
const useCase = deps.useCases.delete();
|
||||
const controller = new DeleteCustomerInvoiceController(useCase);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
@ -114,12 +117,26 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
|
||||
//checkTabContext,
|
||||
validateRequest(ReportCustomerInvoiceByIdRequestSchema, "params"),
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const useCase = deps.build.report();
|
||||
const useCase = deps.useCases.report();
|
||||
const controller = new ReportCustomerInvoiceController(useCase);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/:proforma_id/status",
|
||||
//checkTabContext,
|
||||
|
||||
validateRequest(ChangeStatusCustomerInvoiceByIdParamsRequestSchema, "params"),
|
||||
validateRequest(ChangeStatusCustomerInvoiceByIdRequestSchema, "body"),
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const useCase = deps.useCases.changeStatus();
|
||||
const controller = new ChangeStatusCustomerInvoiceController(useCase);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
);
|
||||
|
||||
router.put(
|
||||
"/:proforma_id/issue",
|
||||
//checkTabContext,
|
||||
@ -128,7 +145,7 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
|
||||
validateRequest(XXX, "body"),*/
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const useCase = deps.build.issue();
|
||||
const useCase = deps.useCases.issue();
|
||||
const controller = new IssueCustomerInvoiceController(useCase);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
export * from "./dependencies";
|
||||
export * from "./express";
|
||||
export * from "./mappers";
|
||||
export * from "./sequelize";
|
||||
|
||||
@ -313,6 +313,7 @@ export class CustomerInvoiceDomainMapper
|
||||
parent: source,
|
||||
...params,
|
||||
});
|
||||
|
||||
if (itemsResult.isFailure) {
|
||||
errors.push({
|
||||
path: "items",
|
||||
@ -320,14 +321,13 @@ export class CustomerInvoiceDomainMapper
|
||||
});
|
||||
}
|
||||
|
||||
const items = itemsResult.data;
|
||||
|
||||
// 2) Taxes
|
||||
const taxesResult = this._taxesMapper.mapToPersistenceArray(new Collection(source.getTaxes()), {
|
||||
errors,
|
||||
parent: source,
|
||||
...params,
|
||||
});
|
||||
|
||||
if (taxesResult.isFailure) {
|
||||
errors.push({
|
||||
path: "taxes",
|
||||
@ -335,25 +335,25 @@ export class CustomerInvoiceDomainMapper
|
||||
});
|
||||
}
|
||||
|
||||
const taxes = taxesResult.data;
|
||||
|
||||
// 3) Calcular totales
|
||||
const allAmounts = source.getAllAmounts();
|
||||
|
||||
// 4) Cliente
|
||||
// 3) Cliente
|
||||
const recipient = this._recipientMapper.mapToPersistence(source.recipient, {
|
||||
errors,
|
||||
parent: source,
|
||||
...params,
|
||||
});
|
||||
|
||||
// 7) Si hubo errores de mapeo, devolvemos colección de validación
|
||||
// 4) Si hubo errores de mapeo, devolvemos colección de validación
|
||||
if (errors.length > 0) {
|
||||
return Result.fail(
|
||||
new ValidationErrorCollection("Customer invoice mapping to persistence failed", errors)
|
||||
);
|
||||
}
|
||||
|
||||
const items = itemsResult.data;
|
||||
const taxes = taxesResult.data;
|
||||
|
||||
const allAmounts = source.getAllAmounts(); // Da los totales ya calculados
|
||||
|
||||
const invoiceValues: Partial<CustomerInvoiceCreationAttributes> = {
|
||||
// Identificación
|
||||
id: source.id.toPrimitive(),
|
||||
@ -381,8 +381,8 @@ export class CustomerInvoiceDomainMapper
|
||||
discount_percentage_value: source.discountPercentage.toPrimitive().value,
|
||||
discount_percentage_scale: source.discountPercentage.toPrimitive().scale,
|
||||
|
||||
discount_amount_value: allAmounts.discountAmount.value,
|
||||
discount_amount_scale: allAmounts.discountAmount.scale,
|
||||
discount_amount_value: allAmounts.headerDiscountAmount.value,
|
||||
discount_amount_scale: allAmounts.headerDiscountAmount.scale,
|
||||
|
||||
taxable_amount_value: allAmounts.taxableAmount.value,
|
||||
taxable_amount_scale: allAmounts.taxableAmount.scale,
|
||||
|
||||
@ -8,7 +8,7 @@ import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/serve
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Collection, Result } from "@repo/rdx-utils";
|
||||
import { Transaction } from "sequelize";
|
||||
import { CustomerInvoice, ICustomerInvoiceRepository } from "../../domain";
|
||||
import { CustomerInvoice, CustomerInvoiceStatus, ICustomerInvoiceRepository } from "../../domain";
|
||||
import {
|
||||
CustomerInvoiceListDTO,
|
||||
ICustomerInvoiceDomainMapper,
|
||||
@ -102,7 +102,13 @@ export class CustomerInvoiceRepository
|
||||
});
|
||||
const dto = mapper.mapToPersistence(invoice);
|
||||
|
||||
if (dto.isFailure) {
|
||||
return Result.fail(dto.error);
|
||||
}
|
||||
const { id, ...updatePayload } = dto.data;
|
||||
|
||||
console.log(id);
|
||||
|
||||
const [affected] = await CustomerInvoiceModel.update(updatePayload, {
|
||||
where: { id /*, version */ },
|
||||
//fields: Object.keys(updatePayload),
|
||||
@ -110,6 +116,8 @@ export class CustomerInvoiceRepository
|
||||
individualHooks: true,
|
||||
});
|
||||
|
||||
console.log(affected);
|
||||
|
||||
if (affected === 0) {
|
||||
return Result.fail(
|
||||
new InfrastructureRepositoryError(
|
||||
@ -332,4 +340,49 @@ export class CustomerInvoiceRepository
|
||||
return Result.fail(translateSequelizeError(err));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Actualiza el "status" de una proforma
|
||||
*
|
||||
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
|
||||
* @param id - UUID de la factura a eliminar.
|
||||
* @param newStatus - nuevo estado
|
||||
* @param transaction - Transacción activa para la operación.
|
||||
* @returns Result<boolean, Error>
|
||||
*/
|
||||
async updateProformaStatusByIdInCompany(
|
||||
companyId: UniqueID,
|
||||
id: UniqueID,
|
||||
newStatus: CustomerInvoiceStatus,
|
||||
transaction: Transaction
|
||||
): Promise<Result<boolean, Error>> {
|
||||
try {
|
||||
const [affected] = await CustomerInvoiceModel.update(
|
||||
{
|
||||
status: newStatus.toPrimitive(),
|
||||
},
|
||||
{
|
||||
where: { id: id.toPrimitive(), company_id: companyId.toPrimitive() },
|
||||
fields: ["status"],
|
||||
transaction,
|
||||
individualHooks: true,
|
||||
}
|
||||
);
|
||||
|
||||
console.log(affected);
|
||||
|
||||
if (affected === 0) {
|
||||
return Result.fail(
|
||||
new InfrastructureRepositoryError(
|
||||
"Concurrency conflict or not found update customer invoice"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return Result.ok(true);
|
||||
} catch (err: unknown) {
|
||||
return Result.fail(translateSequelizeError(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const ChangeStatusCustomerInvoiceByIdParamsRequestSchema = z.object({
|
||||
proforma_id: z.string(),
|
||||
});
|
||||
|
||||
export const ChangeStatusCustomerInvoiceByIdRequestSchema = z.object({
|
||||
new_status: z.string(),
|
||||
});
|
||||
|
||||
export type ChangeStatusCustomerInvoiceByIdRequestDTO = Partial<
|
||||
z.infer<typeof ChangeStatusCustomerInvoiceByIdRequestSchema>
|
||||
>;
|
||||
@ -1,3 +1,4 @@
|
||||
export * from "./change-status-customer-invoice-by-id.request.dto";
|
||||
export * from "./create-customer-invoice.request.dto";
|
||||
export * from "./customer-invoices-list.request.dto";
|
||||
export * from "./delete-customer-invoice-by-id.request.dto";
|
||||
|
||||
@ -5,6 +5,7 @@ export const GetCustomerInvoiceByIdResponseSchema = z.object({
|
||||
id: z.uuid(),
|
||||
company_id: z.uuid(),
|
||||
|
||||
is_proforma: z.string(),
|
||||
invoice_number: z.string(),
|
||||
status: z.string(),
|
||||
series: z.string(),
|
||||
|
||||
@ -40,6 +40,7 @@
|
||||
"@repo/shadcn-ui": "workspace:*",
|
||||
"@tanstack/react-query": "^5.90.6",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"express": "^4.18.2",
|
||||
"i18next": "^25.6.0",
|
||||
"lucide-react": "^0.503.0",
|
||||
"react-data-table-component": "^7.7.0",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { RequestWithAuth, enforceTenant, enforceUser, mockUser } from "@erp/auth/api";
|
||||
import { enforceTenant, enforceUser, mockUser, RequestWithAuth } from "@erp/auth/api";
|
||||
import { ModuleParams, validateRequest } from "@erp/core/api";
|
||||
import { ILogger } from "@repo/rdx-logger";
|
||||
import { Application, NextFunction, Request, Response, Router } from "express";
|
||||
|
||||
@ -586,6 +586,9 @@ importers:
|
||||
'@tanstack/react-table':
|
||||
specifier: ^8.21.3
|
||||
version: 8.21.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
express:
|
||||
specifier: ^4.18.2
|
||||
version: 4.21.2
|
||||
i18next:
|
||||
specifier: ^25.6.0
|
||||
version: 25.6.0(typescript@5.9.3)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user