Facturas de cliente
This commit is contained in:
parent
061ec30cbd
commit
0fcba918a6
@ -5,7 +5,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup src/index.ts --config tsup.config.ts",
|
"build": "tsup src/index.ts --config tsup.config.ts",
|
||||||
"start": "NODE_ENV=production node --env-file=.env.production dist/index.js",
|
"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",
|
"clean": "rimraf .turbo node_modules dist",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"lint": "biome lint --fix",
|
"lint": "biome lint --fix",
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import { globalErrorHandler } from "@erp/core/api";
|
|
||||||
import cors, { CorsOptions } from "cors";
|
import cors, { CorsOptions } from "cors";
|
||||||
import express, { Application } from "express";
|
import express, { Application } from "express";
|
||||||
import helmet from "helmet";
|
import helmet from "helmet";
|
||||||
@ -90,7 +89,7 @@ export function createApp(): Application {
|
|||||||
// Gestión global de errores.
|
// Gestión global de errores.
|
||||||
// Siempre al final de la cadena de middlewares
|
// Siempre al final de la cadena de middlewares
|
||||||
// y después de las rutas.
|
// y después de las rutas.
|
||||||
app.use(globalErrorHandler);
|
//app.use(globalErrorHandler);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
export * from "./customer-invoice-application.service";
|
|
||||||
export * from "./presenters";
|
export * from "./presenters";
|
||||||
|
export * from "./services";
|
||||||
export * from "./use-cases";
|
export * from "./use-cases";
|
||||||
|
|||||||
@ -47,6 +47,7 @@ export class CustomerInvoiceFullPresenter extends Presenter<
|
|||||||
id: invoice.id.toString(),
|
id: invoice.id.toString(),
|
||||||
company_id: invoice.companyId.toString(),
|
company_id: invoice.companyId.toString(),
|
||||||
|
|
||||||
|
is_proforma: invoice.isProforma ? "true" : "false",
|
||||||
invoice_number: invoice.invoiceNumber.toString(),
|
invoice_number: invoice.invoiceNumber.toString(),
|
||||||
status: invoice.status.toPrimitive(),
|
status: invoice.status.toPrimitive(),
|
||||||
series: toEmptyString(invoice.series, (value) => value.toString()),
|
series: toEmptyString(invoice.series, (value) => value.toString()),
|
||||||
|
|||||||
@ -5,15 +5,16 @@ import { Transaction } from "sequelize";
|
|||||||
import {
|
import {
|
||||||
CustomerInvoiceNumber,
|
CustomerInvoiceNumber,
|
||||||
CustomerInvoiceSerie,
|
CustomerInvoiceSerie,
|
||||||
|
CustomerInvoiceStatus,
|
||||||
ICustomerInvoiceNumberGenerator,
|
ICustomerInvoiceNumberGenerator,
|
||||||
} from "../domain";
|
} from "../../domain";
|
||||||
import {
|
import {
|
||||||
CustomerInvoice,
|
CustomerInvoice,
|
||||||
CustomerInvoicePatchProps,
|
CustomerInvoicePatchProps,
|
||||||
CustomerInvoiceProps,
|
CustomerInvoiceProps,
|
||||||
} from "../domain/aggregates";
|
} from "../../domain/aggregates";
|
||||||
import { ICustomerInvoiceRepository } from "../domain/repositories";
|
import { ICustomerInvoiceRepository } from "../../domain/repositories";
|
||||||
import { CustomerInvoiceListDTO } from "../infrastructure";
|
import { CustomerInvoiceListDTO } from "../../infrastructure";
|
||||||
|
|
||||||
export class CustomerInvoiceApplicationService {
|
export class CustomerInvoiceApplicationService {
|
||||||
constructor(
|
constructor(
|
||||||
@ -67,28 +68,6 @@ export class CustomerInvoiceApplicationService {
|
|||||||
return CustomerInvoice.create({ ...props, companyId }, invoiceId);
|
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.
|
* Guarda una nueva factura y devuelve la factura guardada.
|
||||||
*
|
*
|
||||||
@ -226,4 +205,28 @@ export class CustomerInvoiceApplicationService {
|
|||||||
): Promise<Result<boolean, Error>> {
|
): Promise<Result<boolean, Error>> {
|
||||||
return this.repository.deleteByIdInCompany(companyId, invoiceId, transaction);
|
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 { Maybe, Result } from "@repo/rdx-utils";
|
||||||
import { Transaction } from "sequelize";
|
import { Transaction } from "sequelize";
|
||||||
import { CreateCustomerInvoiceRequestDTO } from "../../../../common/dto";
|
import { CreateCustomerInvoiceRequestDTO } from "../../../../common/dto";
|
||||||
import { CustomerInvoiceApplicationService } from "../../customer-invoice-application.service";
|
|
||||||
import { CustomerInvoiceFullPresenter } from "../../presenters";
|
import { CustomerInvoiceFullPresenter } from "../../presenters";
|
||||||
|
import { CustomerInvoiceApplicationService } from "../../services/customer-invoice-application.service";
|
||||||
import { CreateCustomerInvoicePropsMapper } from "./map-dto-to-create-customer-invoice-props";
|
import { CreateCustomerInvoicePropsMapper } from "./map-dto-to-create-customer-invoice-props";
|
||||||
|
|
||||||
type CreateCustomerInvoiceUseCaseInput = {
|
type CreateCustomerInvoiceUseCaseInput = {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { EntityNotFoundError, ITransactionManager } from "@erp/core/api";
|
import { EntityNotFoundError, ITransactionManager } from "@erp/core/api";
|
||||||
import { UniqueID } from "@repo/rdx-ddd";
|
import { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
import { CustomerInvoiceApplicationService } from "../../application";
|
import { CustomerInvoiceApplicationService } from "../services";
|
||||||
|
|
||||||
type DeleteCustomerInvoiceUseCaseInput = {
|
type DeleteCustomerInvoiceUseCaseInput = {
|
||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
|
import { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
|
||||||
import { UniqueID } from "@repo/rdx-ddd";
|
import { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
import { CustomerInvoiceApplicationService } from "../../application";
|
|
||||||
import { CustomerInvoiceFullPresenter } from "../presenters/domain";
|
import { CustomerInvoiceFullPresenter } from "../presenters/domain";
|
||||||
|
import { CustomerInvoiceApplicationService } from "../services";
|
||||||
|
|
||||||
type GetCustomerInvoiceUseCaseInput = {
|
type GetCustomerInvoiceUseCaseInput = {
|
||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
export * from "./change-status-customer-invoice.use-case";
|
||||||
export * from "./create";
|
export * from "./create";
|
||||||
export * from "./get-customer-invoice.use-case";
|
export * from "./get-customer-invoice.use-case";
|
||||||
export * from "./issue-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 { UniqueID, UtcDate } from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
import { ProformaCannotBeConvertedToInvoiceError } from "../../domain";
|
import {
|
||||||
import { ProformaCanTranstionToIssuedSpecification } from "../../domain/specs";
|
IssueCustomerInvoiceDomainService,
|
||||||
import { CustomerInvoiceApplicationService } from "../customer-invoice-application.service";
|
ProformaCustomerInvoiceDomainService,
|
||||||
|
} from "../../domain";
|
||||||
|
import { CustomerInvoiceFullPresenter } from "../presenters";
|
||||||
|
import { CustomerInvoiceApplicationService } from "../services";
|
||||||
|
|
||||||
type IssueCustomerInvoiceUseCaseInput = {
|
type IssueCustomerInvoiceUseCaseInput = {
|
||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
@ -20,21 +23,28 @@ type IssueCustomerInvoiceUseCaseInput = {
|
|||||||
* - Persiste ambas dentro de la misma transacción
|
* - Persiste ambas dentro de la misma transacción
|
||||||
*/
|
*/
|
||||||
export class IssueCustomerInvoiceUseCase {
|
export class IssueCustomerInvoiceUseCase {
|
||||||
|
private readonly issueDomainService: IssueCustomerInvoiceDomainService;
|
||||||
|
private readonly proformaDomainService: ProformaCustomerInvoiceDomainService;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly service: CustomerInvoiceApplicationService,
|
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) {
|
public execute(params: IssueCustomerInvoiceUseCaseInput) {
|
||||||
const { proforma_id, companyId } = params;
|
const { proforma_id, companyId } = params;
|
||||||
|
|
||||||
const idOrError = UniqueID.create(proforma_id);
|
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 proformaId = idOrError.data;
|
||||||
|
const presenter = this.presenterRegistry.getPresenter({
|
||||||
|
resource: "customer-invoice",
|
||||||
|
projection: "FULL",
|
||||||
|
}) as CustomerInvoiceFullPresenter;
|
||||||
|
|
||||||
return this.transactionManager.complete(async (transaction) => {
|
return this.transactionManager.complete(async (transaction) => {
|
||||||
try {
|
try {
|
||||||
@ -44,60 +54,47 @@ export class IssueCustomerInvoiceUseCase {
|
|||||||
proformaId,
|
proformaId,
|
||||||
transaction
|
transaction
|
||||||
);
|
);
|
||||||
|
if (proformaResult.isFailure) return Result.fail(proformaResult.error);
|
||||||
if (proformaResult.isFailure) {
|
|
||||||
return Result.fail(proformaResult.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const proforma = proformaResult.data;
|
const proforma = proformaResult.data;
|
||||||
|
|
||||||
/** 2. Comprobamos que la proforma origen está aprovada para generar la factura */
|
/** 2. Generar nueva factura */
|
||||||
const isOk = new ProformaCanTranstionToIssuedSpecification();
|
|
||||||
if (!(await isOk.isSatisfiedBy(proforma))) {
|
|
||||||
return Result.fail(new ProformaCannotBeConvertedToInvoiceError(proformaId.toString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 3. Generar nueva factura */
|
|
||||||
const nextNumberResult = await this.service.getNextIssueInvoiceNumber(
|
const nextNumberResult = await this.service.getNextIssueInvoiceNumber(
|
||||||
companyId,
|
companyId,
|
||||||
proforma.series,
|
proforma.series,
|
||||||
transaction
|
transaction
|
||||||
);
|
);
|
||||||
if (nextNumberResult.isFailure) {
|
if (nextNumberResult.isFailure) return Result.fail(nextNumberResult.error);
|
||||||
return Result.fail(nextNumberResult.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newIssueNumber = nextNumberResult.data;
|
/** 4. Crear factura definitiva (dominio) */
|
||||||
|
const issuedInvoiceOrError = await this.issueDomainService.issueFromProforma(proforma, {
|
||||||
// props base obtenidas del agregado proforma
|
issueNumber: nextNumberResult.data,
|
||||||
const issuedInvoiceOrError = this.service.buildIssueInvoiceInCompany(companyId, proforma, {
|
issueDate: UtcDate.today(),
|
||||||
invoiceNumber: newIssueNumber,
|
|
||||||
invoiceDate: UtcDate.today(),
|
|
||||||
});
|
});
|
||||||
|
if (issuedInvoiceOrError.isFailure) return Result.fail(issuedInvoiceOrError.error);
|
||||||
|
|
||||||
if (issuedInvoiceOrError.isFailure) {
|
/** 5. Guardar la nueva factura */
|
||||||
return Result.fail(issuedInvoiceOrError.error);
|
const saveInvoiceResult = await this.service.createInvoiceInCompany(
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
|
||||||
companyId,
|
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
|
transaction
|
||||||
);
|
);
|
||||||
|
|
||||||
/** 5. Resultado */
|
const dto = presenter.toOutput(saveInvoiceResult.data);
|
||||||
return Result.ok(issuedInvoice);
|
return Result.ok(dto);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
return Result.fail(error as Error);
|
return Result.fail(error as Error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,8 +4,8 @@ import { UniqueID } from "@repo/rdx-ddd";
|
|||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
import { Transaction } from "sequelize";
|
import { Transaction } from "sequelize";
|
||||||
import { ListCustomerInvoicesResponseDTO } from "../../../common/dto";
|
import { ListCustomerInvoicesResponseDTO } from "../../../common/dto";
|
||||||
import { CustomerInvoiceApplicationService } from "../../application";
|
|
||||||
import { ListCustomerInvoicesPresenter } from "../presenters";
|
import { ListCustomerInvoicesPresenter } from "../presenters";
|
||||||
|
import { CustomerInvoiceApplicationService } from "../services";
|
||||||
|
|
||||||
type ListCustomerInvoicesUseCaseInput = {
|
type ListCustomerInvoicesUseCaseInput = {
|
||||||
companyId: UniqueID;
|
companyId: UniqueID;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
|
import { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
|
||||||
import { UniqueID } from "@repo/rdx-ddd";
|
import { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
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";
|
import { CustomerInvoiceReportPDFPresenter } from "./reporter";
|
||||||
|
|
||||||
type ReportCustomerInvoiceUseCaseInput = {
|
type ReportCustomerInvoiceUseCaseInput = {
|
||||||
|
|||||||
@ -4,8 +4,8 @@ import { Result } from "@repo/rdx-utils";
|
|||||||
import { Transaction } from "sequelize";
|
import { Transaction } from "sequelize";
|
||||||
import { UpdateCustomerInvoiceByIdRequestDTO } from "../../../../common";
|
import { UpdateCustomerInvoiceByIdRequestDTO } from "../../../../common";
|
||||||
import { CustomerInvoicePatchProps } from "../../../domain";
|
import { CustomerInvoicePatchProps } from "../../../domain";
|
||||||
import { CustomerInvoiceApplicationService } from "../../customer-invoice-application.service";
|
|
||||||
import { CustomerInvoiceFullPresenter } from "../../presenters";
|
import { CustomerInvoiceFullPresenter } from "../../presenters";
|
||||||
|
import { CustomerInvoiceApplicationService } from "../../services/customer-invoice-application.service";
|
||||||
import { mapDTOToUpdateCustomerInvoicePatchProps } from "./map-dto-to-update-customer-invoice-props";
|
import { mapDTOToUpdateCustomerInvoicePatchProps } from "./map-dto-to-update-customer-invoice-props";
|
||||||
|
|
||||||
type UpdateCustomerInvoiceUseCaseInput = {
|
type UpdateCustomerInvoiceUseCaseInput = {
|
||||||
|
|||||||
@ -65,18 +65,8 @@ export interface ICustomerInvoice {
|
|||||||
hasRecipient: boolean;
|
hasRecipient: boolean;
|
||||||
hasPaymentMethod: boolean;
|
hasPaymentMethod: boolean;
|
||||||
|
|
||||||
_getSubtotalAmount(): InvoiceAmount;
|
|
||||||
getHeaderDiscountAmount(): InvoiceAmount;
|
|
||||||
|
|
||||||
getTaxableAmount(): InvoiceAmount;
|
|
||||||
getTaxesAmount(): InvoiceAmount;
|
|
||||||
getTotalAmount(): InvoiceAmount;
|
|
||||||
|
|
||||||
getTaxes(): InvoiceTaxTotal[];
|
getTaxes(): InvoiceTaxTotal[];
|
||||||
|
getProps(): CustomerInvoiceProps;
|
||||||
asIssued(): Result<CustomerInvoice, Error>;
|
|
||||||
|
|
||||||
getIssuedInvoiceProps(): CustomerInvoiceProps;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CustomerInvoice
|
export class CustomerInvoice
|
||||||
@ -94,15 +84,19 @@ export class CustomerInvoice
|
|||||||
currencyCode: props.currencyCode,
|
currencyCode: props.currencyCode,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getHeaderDiscountAmount(): InvoiceAmount {
|
getHeaderDiscountAmount(): InvoiceAmount {
|
||||||
throw new Error("Method not implemented.");
|
throw new Error("Method not implemented.");
|
||||||
}
|
}
|
||||||
|
|
||||||
getTaxableAmount(): InvoiceAmount {
|
getTaxableAmount(): InvoiceAmount {
|
||||||
throw new Error("Method not implemented.");
|
throw new Error("Method not implemented.");
|
||||||
}
|
}
|
||||||
|
|
||||||
getTaxesAmount(): InvoiceAmount {
|
getTaxesAmount(): InvoiceAmount {
|
||||||
throw new Error("Method not implemented.");
|
throw new Error("Method not implemented.");
|
||||||
}
|
}
|
||||||
|
|
||||||
getTotalAmount(): InvoiceAmount {
|
getTotalAmount(): InvoiceAmount {
|
||||||
throw new Error("Method not implemented.");
|
throw new Error("Method not implemented.");
|
||||||
}
|
}
|
||||||
@ -348,21 +342,7 @@ export class CustomerInvoice
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public asIssued(): Result<CustomerInvoice, Error> {
|
public getProps(): CustomerInvoiceProps {
|
||||||
const newProps: CustomerInvoiceProps = {
|
return this.props;
|
||||||
...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(),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import {
|
|||||||
ItemDiscount,
|
ItemDiscount,
|
||||||
ItemQuantity,
|
ItemQuantity,
|
||||||
} from "../../value-objects";
|
} from "../../value-objects";
|
||||||
import { ItemTaxTotal, ItemTaxes } from "../item-taxes";
|
import { ItemTaxes, ItemTaxTotal } from "../item-taxes";
|
||||||
|
|
||||||
export interface CustomerInvoiceItemProps {
|
export interface CustomerInvoiceItemProps {
|
||||||
description: Maybe<CustomerInvoiceItemDescription>;
|
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 "./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";
|
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 { Collection, Result } from "@repo/rdx-utils";
|
||||||
import { CustomerInvoiceListDTO } from "../../infrastructure";
|
import { CustomerInvoiceListDTO } from "../../infrastructure";
|
||||||
import { CustomerInvoice } from "../aggregates";
|
import { CustomerInvoice } from "../aggregates";
|
||||||
|
import { CustomerInvoiceStatus } from "../value-objects";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interfaz del repositorio para el agregado `CustomerInvoice`.
|
* Interfaz del repositorio para el agregado `CustomerInvoice`.
|
||||||
@ -80,4 +81,21 @@ export interface ICustomerInvoiceRepository {
|
|||||||
id: UniqueID,
|
id: UniqueID,
|
||||||
transaction: unknown
|
transaction: unknown
|
||||||
): Promise<Result<boolean, Error>>;
|
): 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 "./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";
|
export * from "./proforma-can-transtion-to-issued.specification";
|
||||||
|
|||||||
@ -4,6 +4,6 @@ import { INVOICE_STATUS } from "../value-objects";
|
|||||||
|
|
||||||
export class ProformaCanTranstionToIssuedSpecification extends CompositeSpecification<CustomerInvoice> {
|
export class ProformaCanTranstionToIssuedSpecification extends CompositeSpecification<CustomerInvoice> {
|
||||||
public async isSatisfiedBy(proforma: CustomerInvoice): Promise<boolean> {
|
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
|
// status === "issued" <- (si is_proforma === false) => Factura y enviará/enviada a Veri*Factu
|
||||||
ISSUED = "issued",
|
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> {
|
export class CustomerInvoiceStatus extends ValueObject<ICustomerInvoiceStatusProps> {
|
||||||
private static readonly ALLOWED_STATUSES = ["draft", "sent", "approved", "rejected", "issued"];
|
private static readonly ALLOWED_STATUSES = ["draft", "sent", "approved", "rejected", "issued"];
|
||||||
private static readonly FIELD = "invoiceStatus";
|
private static readonly FIELD = "invoiceStatus";
|
||||||
private static readonly ERROR_CODE = "INVALID_INVOICE_STATUS";
|
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> {
|
static create(value: string): Result<CustomerInvoiceStatus, Error> {
|
||||||
if (!CustomerInvoiceStatus.ALLOWED_STATUSES.includes(value)) {
|
if (!CustomerInvoiceStatus.ALLOWED_STATUSES.includes(value)) {
|
||||||
const detail = `Estado de la factura no válido: ${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 {
|
canTransitionTo(nextStatus: string): boolean {
|
||||||
return CustomerInvoiceStatus.TRANSITIONS[this.props.value].includes(nextStatus);
|
return INVOICE_TRANSITIONS[this.props.value].includes(nextStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import { IModuleServer, ModuleParams } from "@erp/core/api";
|
import { IModuleServer, ModuleParams } from "@erp/core/api";
|
||||||
import { UniqueID } from "@repo/rdx-ddd";
|
import { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { Transaction } from "sequelize";
|
import { Transaction } from "sequelize";
|
||||||
import { customerInvoicesRouter, models } from "./infrastructure";
|
import { buildCustomerInvoiceDependencies, customerInvoicesRouter, models } from "./infrastructure";
|
||||||
import { buildCustomerInvoiceDependencies } from "./infrastructure/dependencies";
|
|
||||||
|
|
||||||
export const customerInvoicesAPIModule: IModuleServer = {
|
export const customerInvoicesAPIModule: IModuleServer = {
|
||||||
name: "customer-invoices",
|
name: "customer-invoices",
|
||||||
@ -17,7 +16,7 @@ export const customerInvoicesAPIModule: IModuleServer = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async registerDependencies(params) {
|
async registerDependencies(params) {
|
||||||
const { logger, listServices } = params; /* = ModuleParams & {
|
const { logger } = params; /* = ModuleParams & {
|
||||||
getService: (name: string) => any;
|
getService: (name: string) => any;
|
||||||
};*/
|
};*/
|
||||||
|
|
||||||
@ -25,9 +24,6 @@ export const customerInvoicesAPIModule: IModuleServer = {
|
|||||||
label: this.name,
|
label: this.name,
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(listServices());
|
|
||||||
//getService()
|
|
||||||
|
|
||||||
const deps = buildCustomerInvoiceDependencies(params);
|
const deps = buildCustomerInvoiceDependencies(params);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -38,9 +34,9 @@ export const customerInvoicesAPIModule: IModuleServer = {
|
|||||||
invoiceId: UniqueID,
|
invoiceId: UniqueID,
|
||||||
transaction?: Transaction
|
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,
|
SequelizeTransactionManager,
|
||||||
} from "@erp/core/api";
|
} from "@erp/core/api";
|
||||||
import {
|
import {
|
||||||
|
ChangeStatusCustomerInvoiceUseCase,
|
||||||
CreateCustomerInvoiceUseCase,
|
CreateCustomerInvoiceUseCase,
|
||||||
CustomerInvoiceApplicationService,
|
CustomerInvoiceApplicationService,
|
||||||
CustomerInvoiceFullPresenter,
|
CustomerInvoiceFullPresenter,
|
||||||
@ -33,11 +34,11 @@ export type CustomerInvoiceDeps = {
|
|||||||
mapperRegistry: IMapperRegistry;
|
mapperRegistry: IMapperRegistry;
|
||||||
presenterRegistry: IPresenterRegistry;
|
presenterRegistry: IPresenterRegistry;
|
||||||
repo: CustomerInvoiceRepository;
|
repo: CustomerInvoiceRepository;
|
||||||
service: CustomerInvoiceApplicationService;
|
appService: CustomerInvoiceApplicationService;
|
||||||
catalogs: {
|
catalogs: {
|
||||||
taxes: JsonTaxCatalogProvider;
|
taxes: JsonTaxCatalogProvider;
|
||||||
};
|
};
|
||||||
build: {
|
useCases: {
|
||||||
list: () => ListCustomerInvoicesUseCase;
|
list: () => ListCustomerInvoicesUseCase;
|
||||||
get: () => GetCustomerInvoiceUseCase;
|
get: () => GetCustomerInvoiceUseCase;
|
||||||
create: () => CreateCustomerInvoiceUseCase;
|
create: () => CreateCustomerInvoiceUseCase;
|
||||||
@ -45,17 +46,19 @@ export type CustomerInvoiceDeps = {
|
|||||||
//delete: () => DeleteCustomerInvoiceUseCase;
|
//delete: () => DeleteCustomerInvoiceUseCase;
|
||||||
report: () => ReportCustomerInvoiceUseCase;
|
report: () => ReportCustomerInvoiceUseCase;
|
||||||
issue: () => IssueCustomerInvoiceUseCase;
|
issue: () => IssueCustomerInvoiceUseCase;
|
||||||
|
changeStatus: () => ChangeStatusCustomerInvoiceUseCase;
|
||||||
};
|
};
|
||||||
getService: (name: string) => any;
|
|
||||||
listServices: () => string[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function buildCustomerInvoiceDependencies(params: ModuleParams): CustomerInvoiceDeps {
|
export function buildCustomerInvoiceDependencies(params: ModuleParams): CustomerInvoiceDeps {
|
||||||
const { database, listServices, getService } = params;
|
const { database } = params;
|
||||||
const transactionManager = new SequelizeTransactionManager(database);
|
|
||||||
|
/** Dominio */
|
||||||
const catalogs = { taxes: SpainTaxCatalogProvider() };
|
const catalogs = { taxes: SpainTaxCatalogProvider() };
|
||||||
|
|
||||||
// Mapper Registry
|
/** Infraestructura */
|
||||||
|
const transactionManager = new SequelizeTransactionManager(database);
|
||||||
|
|
||||||
const mapperRegistry = new InMemoryMapperRegistry();
|
const mapperRegistry = new InMemoryMapperRegistry();
|
||||||
mapperRegistry
|
mapperRegistry
|
||||||
.registerDomainMapper(
|
.registerDomainMapper(
|
||||||
@ -70,100 +73,74 @@ export function buildCustomerInvoiceDependencies(params: ModuleParams): Customer
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Repository & Services
|
// Repository & Services
|
||||||
const repo = new CustomerInvoiceRepository({ mapperRegistry, database });
|
const repository = new CustomerInvoiceRepository({ mapperRegistry, database });
|
||||||
const numberGenerator = new SequelizeInvoiceNumberGenerator();
|
const numberGenerator = new SequelizeInvoiceNumberGenerator();
|
||||||
const service = new CustomerInvoiceApplicationService(repo, numberGenerator);
|
|
||||||
|
/** Aplicación */
|
||||||
|
const appService = new CustomerInvoiceApplicationService(repository, numberGenerator);
|
||||||
|
|
||||||
// Presenter Registry
|
// Presenter Registry
|
||||||
const presenterRegistry = new InMemoryPresenterRegistry();
|
const presenterRegistry = new InMemoryPresenterRegistry();
|
||||||
presenterRegistry.registerPresenters([
|
presenterRegistry.registerPresenters([
|
||||||
{
|
{
|
||||||
key: {
|
key: { resource: "customer-invoice-items", projection: "FULL" },
|
||||||
resource: "customer-invoice-items",
|
|
||||||
projection: "FULL",
|
|
||||||
},
|
|
||||||
presenter: new CustomerInvoiceItemsFullPresenter(presenterRegistry),
|
presenter: new CustomerInvoiceItemsFullPresenter(presenterRegistry),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: {
|
key: { resource: "recipient-invoice", projection: "FULL" },
|
||||||
resource: "recipient-invoice",
|
|
||||||
projection: "FULL",
|
|
||||||
},
|
|
||||||
presenter: new RecipientInvoiceFullPresenter(presenterRegistry),
|
presenter: new RecipientInvoiceFullPresenter(presenterRegistry),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: {
|
key: { resource: "customer-invoice", projection: "FULL" },
|
||||||
resource: "customer-invoice",
|
|
||||||
projection: "FULL",
|
|
||||||
},
|
|
||||||
presenter: new CustomerInvoiceFullPresenter(presenterRegistry),
|
presenter: new CustomerInvoiceFullPresenter(presenterRegistry),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: {
|
key: { resource: "customer-invoice", projection: "LIST" },
|
||||||
resource: "customer-invoice",
|
|
||||||
projection: "LIST",
|
|
||||||
},
|
|
||||||
presenter: new ListCustomerInvoicesPresenter(presenterRegistry),
|
presenter: new ListCustomerInvoicesPresenter(presenterRegistry),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: {
|
key: { resource: "customer-invoice", projection: "REPORT", format: "JSON" },
|
||||||
resource: "customer-invoice",
|
|
||||||
projection: "REPORT",
|
|
||||||
format: "JSON",
|
|
||||||
},
|
|
||||||
presenter: new CustomerInvoiceReportPresenter(presenterRegistry),
|
presenter: new CustomerInvoiceReportPresenter(presenterRegistry),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: {
|
key: { resource: "customer-invoice-items", projection: "REPORT", format: "JSON" },
|
||||||
resource: "customer-invoice-items",
|
|
||||||
projection: "REPORT",
|
|
||||||
format: "JSON",
|
|
||||||
},
|
|
||||||
presenter: new CustomerInvoiceItemsReportPersenter(presenterRegistry),
|
presenter: new CustomerInvoiceItemsReportPersenter(presenterRegistry),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: {
|
key: { resource: "customer-invoice", projection: "REPORT", format: "HTML" },
|
||||||
resource: "customer-invoice",
|
|
||||||
projection: "REPORT",
|
|
||||||
format: "HTML",
|
|
||||||
},
|
|
||||||
presenter: new CustomerInvoiceReportHTMLPresenter(presenterRegistry),
|
presenter: new CustomerInvoiceReportHTMLPresenter(presenterRegistry),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: {
|
key: { resource: "customer-invoice", projection: "REPORT", format: "PDF" },
|
||||||
resource: "customer-invoice",
|
|
||||||
projection: "REPORT",
|
|
||||||
format: "PDF",
|
|
||||||
},
|
|
||||||
presenter: new CustomerInvoiceReportPDFPresenter(presenterRegistry),
|
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 {
|
return {
|
||||||
transactionManager,
|
transactionManager,
|
||||||
repo,
|
repo: repository,
|
||||||
mapperRegistry,
|
mapperRegistry,
|
||||||
presenterRegistry,
|
presenterRegistry,
|
||||||
service,
|
appService,
|
||||||
catalogs,
|
catalogs,
|
||||||
build: {
|
useCases,
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 "./create-customer-invoice.controller";
|
||||||
//export * from "./delete-customer-invoice.controller";
|
//export * from "./delete-customer-invoice.controller";
|
||||||
export * from "./get-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 "./list-customer-invoices.controller";
|
||||||
export * from "./report-customer-invoice.controller";
|
export * from "./report-customer-invoice.controller";
|
||||||
export * from "./update-customer-invoice.controller";
|
export * from "./update-customer-invoice.controller";
|
||||||
|
|||||||
@ -9,7 +9,9 @@ import {
|
|||||||
} from "@erp/core/api";
|
} from "@erp/core/api";
|
||||||
import {
|
import {
|
||||||
CustomerInvoiceIdAlreadyExistsError,
|
CustomerInvoiceIdAlreadyExistsError,
|
||||||
|
EntityIsNotProformaError,
|
||||||
isCustomerInvoiceIdAlreadyExistsError,
|
isCustomerInvoiceIdAlreadyExistsError,
|
||||||
|
isEntityIsNotProformaError,
|
||||||
isProformaCannotBeConvertedToInvoiceError,
|
isProformaCannotBeConvertedToInvoiceError,
|
||||||
ProformaCannotBeConvertedToInvoiceError,
|
ProformaCannotBeConvertedToInvoiceError,
|
||||||
} from "../../domain";
|
} 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 = {
|
const proformaConversionRule: ErrorToApiRule = {
|
||||||
priority: 120,
|
priority: 120,
|
||||||
matches: (e) => isProformaCannotBeConvertedToInvoiceError(e),
|
matches: (e) => isProformaCannotBeConvertedToInvoiceError(e),
|
||||||
@ -38,4 +49,5 @@ const proformaConversionRule: ErrorToApiRule = {
|
|||||||
// Cómo aplicarla: crea una nueva instancia del mapper con la regla extra
|
// Cómo aplicarla: crea una nueva instancia del mapper con la regla extra
|
||||||
export const customerInvoicesApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default()
|
export const customerInvoicesApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default()
|
||||||
.register(invoiceDuplicateRule)
|
.register(invoiceDuplicateRule)
|
||||||
|
.register(entityIsNotProformaError)
|
||||||
.register(proformaConversionRule);
|
.register(proformaConversionRule);
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import { ILogger } from "@repo/rdx-logger";
|
|||||||
import { Application, NextFunction, Request, Response, Router } from "express";
|
import { Application, NextFunction, Request, Response, Router } from "express";
|
||||||
import { Sequelize } from "sequelize";
|
import { Sequelize } from "sequelize";
|
||||||
import {
|
import {
|
||||||
|
ChangeStatusCustomerInvoiceByIdParamsRequestSchema,
|
||||||
|
ChangeStatusCustomerInvoiceByIdRequestSchema,
|
||||||
CreateCustomerInvoiceRequestSchema,
|
CreateCustomerInvoiceRequestSchema,
|
||||||
CustomerInvoiceListRequestSchema,
|
CustomerInvoiceListRequestSchema,
|
||||||
GetCustomerInvoiceByIdRequestSchema,
|
GetCustomerInvoiceByIdRequestSchema,
|
||||||
@ -13,6 +15,7 @@ import {
|
|||||||
} from "../../../common/dto";
|
} from "../../../common/dto";
|
||||||
import { buildCustomerInvoiceDependencies } from "../dependencies";
|
import { buildCustomerInvoiceDependencies } from "../dependencies";
|
||||||
import {
|
import {
|
||||||
|
ChangeStatusCustomerInvoiceController,
|
||||||
CreateCustomerInvoiceController,
|
CreateCustomerInvoiceController,
|
||||||
GetCustomerInvoiceController,
|
GetCustomerInvoiceController,
|
||||||
ListCustomerInvoicesController,
|
ListCustomerInvoicesController,
|
||||||
@ -55,7 +58,7 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
|
|||||||
//checkTabContext,
|
//checkTabContext,
|
||||||
validateRequest(CustomerInvoiceListRequestSchema, "params"),
|
validateRequest(CustomerInvoiceListRequestSchema, "params"),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
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 */);
|
const controller = new ListCustomerInvoicesController(useCase /*, deps.presenters.list */);
|
||||||
return controller.execute(req, res, next);
|
return controller.execute(req, res, next);
|
||||||
}
|
}
|
||||||
@ -66,7 +69,7 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
|
|||||||
//checkTabContext,
|
//checkTabContext,
|
||||||
validateRequest(GetCustomerInvoiceByIdRequestSchema, "params"),
|
validateRequest(GetCustomerInvoiceByIdRequestSchema, "params"),
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const useCase = deps.build.get();
|
const useCase = deps.useCases.get();
|
||||||
const controller = new GetCustomerInvoiceController(useCase);
|
const controller = new GetCustomerInvoiceController(useCase);
|
||||||
return controller.execute(req, res, next);
|
return controller.execute(req, res, next);
|
||||||
}
|
}
|
||||||
@ -78,7 +81,7 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
|
|||||||
|
|
||||||
validateRequest(CreateCustomerInvoiceRequestSchema, "body"),
|
validateRequest(CreateCustomerInvoiceRequestSchema, "body"),
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const useCase = deps.build.create();
|
const useCase = deps.useCases.create();
|
||||||
const controller = new CreateCustomerInvoiceController(useCase);
|
const controller = new CreateCustomerInvoiceController(useCase);
|
||||||
return controller.execute(req, res, next);
|
return controller.execute(req, res, next);
|
||||||
}
|
}
|
||||||
@ -91,7 +94,7 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
|
|||||||
validateRequest(UpdateCustomerInvoiceByIdParamsRequestSchema, "params"),
|
validateRequest(UpdateCustomerInvoiceByIdParamsRequestSchema, "params"),
|
||||||
validateRequest(UpdateCustomerInvoiceByIdRequestSchema, "body"),
|
validateRequest(UpdateCustomerInvoiceByIdRequestSchema, "body"),
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const useCase = deps.build.update();
|
const useCase = deps.useCases.update();
|
||||||
const controller = new UpdateCustomerInvoiceController(useCase);
|
const controller = new UpdateCustomerInvoiceController(useCase);
|
||||||
return controller.execute(req, res, next);
|
return controller.execute(req, res, next);
|
||||||
}
|
}
|
||||||
@ -103,7 +106,7 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
|
|||||||
|
|
||||||
validateRequest(DeleteCustomerInvoiceByIdRequestSchema, "params"),
|
validateRequest(DeleteCustomerInvoiceByIdRequestSchema, "params"),
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const useCase = deps.build.delete();
|
const useCase = deps.useCases.delete();
|
||||||
const controller = new DeleteCustomerInvoiceController(useCase);
|
const controller = new DeleteCustomerInvoiceController(useCase);
|
||||||
return controller.execute(req, res, next);
|
return controller.execute(req, res, next);
|
||||||
}
|
}
|
||||||
@ -114,12 +117,26 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
|
|||||||
//checkTabContext,
|
//checkTabContext,
|
||||||
validateRequest(ReportCustomerInvoiceByIdRequestSchema, "params"),
|
validateRequest(ReportCustomerInvoiceByIdRequestSchema, "params"),
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const useCase = deps.build.report();
|
const useCase = deps.useCases.report();
|
||||||
const controller = new ReportCustomerInvoiceController(useCase);
|
const controller = new ReportCustomerInvoiceController(useCase);
|
||||||
return controller.execute(req, res, next);
|
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(
|
router.put(
|
||||||
"/:proforma_id/issue",
|
"/:proforma_id/issue",
|
||||||
//checkTabContext,
|
//checkTabContext,
|
||||||
@ -128,7 +145,7 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
|
|||||||
validateRequest(XXX, "body"),*/
|
validateRequest(XXX, "body"),*/
|
||||||
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const useCase = deps.build.issue();
|
const useCase = deps.useCases.issue();
|
||||||
const controller = new IssueCustomerInvoiceController(useCase);
|
const controller = new IssueCustomerInvoiceController(useCase);
|
||||||
return controller.execute(req, res, next);
|
return controller.execute(req, res, next);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
export * from "./dependencies";
|
||||||
export * from "./express";
|
export * from "./express";
|
||||||
export * from "./mappers";
|
export * from "./mappers";
|
||||||
export * from "./sequelize";
|
export * from "./sequelize";
|
||||||
|
|||||||
@ -313,6 +313,7 @@ export class CustomerInvoiceDomainMapper
|
|||||||
parent: source,
|
parent: source,
|
||||||
...params,
|
...params,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (itemsResult.isFailure) {
|
if (itemsResult.isFailure) {
|
||||||
errors.push({
|
errors.push({
|
||||||
path: "items",
|
path: "items",
|
||||||
@ -320,14 +321,13 @@ export class CustomerInvoiceDomainMapper
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = itemsResult.data;
|
|
||||||
|
|
||||||
// 2) Taxes
|
// 2) Taxes
|
||||||
const taxesResult = this._taxesMapper.mapToPersistenceArray(new Collection(source.getTaxes()), {
|
const taxesResult = this._taxesMapper.mapToPersistenceArray(new Collection(source.getTaxes()), {
|
||||||
errors,
|
errors,
|
||||||
parent: source,
|
parent: source,
|
||||||
...params,
|
...params,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (taxesResult.isFailure) {
|
if (taxesResult.isFailure) {
|
||||||
errors.push({
|
errors.push({
|
||||||
path: "taxes",
|
path: "taxes",
|
||||||
@ -335,25 +335,25 @@ export class CustomerInvoiceDomainMapper
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const taxes = taxesResult.data;
|
// 3) Cliente
|
||||||
|
|
||||||
// 3) Calcular totales
|
|
||||||
const allAmounts = source.getAllAmounts();
|
|
||||||
|
|
||||||
// 4) Cliente
|
|
||||||
const recipient = this._recipientMapper.mapToPersistence(source.recipient, {
|
const recipient = this._recipientMapper.mapToPersistence(source.recipient, {
|
||||||
errors,
|
errors,
|
||||||
parent: source,
|
parent: source,
|
||||||
...params,
|
...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) {
|
if (errors.length > 0) {
|
||||||
return Result.fail(
|
return Result.fail(
|
||||||
new ValidationErrorCollection("Customer invoice mapping to persistence failed", errors)
|
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> = {
|
const invoiceValues: Partial<CustomerInvoiceCreationAttributes> = {
|
||||||
// Identificación
|
// Identificación
|
||||||
id: source.id.toPrimitive(),
|
id: source.id.toPrimitive(),
|
||||||
@ -381,8 +381,8 @@ export class CustomerInvoiceDomainMapper
|
|||||||
discount_percentage_value: source.discountPercentage.toPrimitive().value,
|
discount_percentage_value: source.discountPercentage.toPrimitive().value,
|
||||||
discount_percentage_scale: source.discountPercentage.toPrimitive().scale,
|
discount_percentage_scale: source.discountPercentage.toPrimitive().scale,
|
||||||
|
|
||||||
discount_amount_value: allAmounts.discountAmount.value,
|
discount_amount_value: allAmounts.headerDiscountAmount.value,
|
||||||
discount_amount_scale: allAmounts.discountAmount.scale,
|
discount_amount_scale: allAmounts.headerDiscountAmount.scale,
|
||||||
|
|
||||||
taxable_amount_value: allAmounts.taxableAmount.value,
|
taxable_amount_value: allAmounts.taxableAmount.value,
|
||||||
taxable_amount_scale: allAmounts.taxableAmount.scale,
|
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 { UniqueID } from "@repo/rdx-ddd";
|
||||||
import { Collection, Result } from "@repo/rdx-utils";
|
import { Collection, Result } from "@repo/rdx-utils";
|
||||||
import { Transaction } from "sequelize";
|
import { Transaction } from "sequelize";
|
||||||
import { CustomerInvoice, ICustomerInvoiceRepository } from "../../domain";
|
import { CustomerInvoice, CustomerInvoiceStatus, ICustomerInvoiceRepository } from "../../domain";
|
||||||
import {
|
import {
|
||||||
CustomerInvoiceListDTO,
|
CustomerInvoiceListDTO,
|
||||||
ICustomerInvoiceDomainMapper,
|
ICustomerInvoiceDomainMapper,
|
||||||
@ -102,7 +102,13 @@ export class CustomerInvoiceRepository
|
|||||||
});
|
});
|
||||||
const dto = mapper.mapToPersistence(invoice);
|
const dto = mapper.mapToPersistence(invoice);
|
||||||
|
|
||||||
|
if (dto.isFailure) {
|
||||||
|
return Result.fail(dto.error);
|
||||||
|
}
|
||||||
const { id, ...updatePayload } = dto.data;
|
const { id, ...updatePayload } = dto.data;
|
||||||
|
|
||||||
|
console.log(id);
|
||||||
|
|
||||||
const [affected] = await CustomerInvoiceModel.update(updatePayload, {
|
const [affected] = await CustomerInvoiceModel.update(updatePayload, {
|
||||||
where: { id /*, version */ },
|
where: { id /*, version */ },
|
||||||
//fields: Object.keys(updatePayload),
|
//fields: Object.keys(updatePayload),
|
||||||
@ -110,6 +116,8 @@ export class CustomerInvoiceRepository
|
|||||||
individualHooks: true,
|
individualHooks: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(affected);
|
||||||
|
|
||||||
if (affected === 0) {
|
if (affected === 0) {
|
||||||
return Result.fail(
|
return Result.fail(
|
||||||
new InfrastructureRepositoryError(
|
new InfrastructureRepositoryError(
|
||||||
@ -332,4 +340,49 @@ export class CustomerInvoiceRepository
|
|||||||
return Result.fail(translateSequelizeError(err));
|
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 "./create-customer-invoice.request.dto";
|
||||||
export * from "./customer-invoices-list.request.dto";
|
export * from "./customer-invoices-list.request.dto";
|
||||||
export * from "./delete-customer-invoice-by-id.request.dto";
|
export * from "./delete-customer-invoice-by-id.request.dto";
|
||||||
|
|||||||
@ -5,6 +5,7 @@ export const GetCustomerInvoiceByIdResponseSchema = z.object({
|
|||||||
id: z.uuid(),
|
id: z.uuid(),
|
||||||
company_id: z.uuid(),
|
company_id: z.uuid(),
|
||||||
|
|
||||||
|
is_proforma: z.string(),
|
||||||
invoice_number: z.string(),
|
invoice_number: z.string(),
|
||||||
status: z.string(),
|
status: z.string(),
|
||||||
series: z.string(),
|
series: z.string(),
|
||||||
|
|||||||
@ -40,6 +40,7 @@
|
|||||||
"@repo/shadcn-ui": "workspace:*",
|
"@repo/shadcn-ui": "workspace:*",
|
||||||
"@tanstack/react-query": "^5.90.6",
|
"@tanstack/react-query": "^5.90.6",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
|
"express": "^4.18.2",
|
||||||
"i18next": "^25.6.0",
|
"i18next": "^25.6.0",
|
||||||
"lucide-react": "^0.503.0",
|
"lucide-react": "^0.503.0",
|
||||||
"react-data-table-component": "^7.7.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 { ModuleParams, validateRequest } from "@erp/core/api";
|
||||||
import { ILogger } from "@repo/rdx-logger";
|
import { ILogger } from "@repo/rdx-logger";
|
||||||
import { Application, NextFunction, Request, Response, Router } from "express";
|
import { Application, NextFunction, Request, Response, Router } from "express";
|
||||||
|
|||||||
@ -586,6 +586,9 @@ importers:
|
|||||||
'@tanstack/react-table':
|
'@tanstack/react-table':
|
||||||
specifier: ^8.21.3
|
specifier: ^8.21.3
|
||||||
version: 8.21.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
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:
|
i18next:
|
||||||
specifier: ^25.6.0
|
specifier: ^25.6.0
|
||||||
version: 25.6.0(typescript@5.9.3)
|
version: 25.6.0(typescript@5.9.3)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user