Facturas de cliente

This commit is contained in:
David Arranz 2025-11-07 18:51:18 +01:00
parent 061ec30cbd
commit 0fcba918a6
44 changed files with 753 additions and 219 deletions

View File

@ -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",

View File

@ -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;
}

View File

@ -1,3 +1,3 @@
export * from "./customer-invoice-application.service";
export * from "./presenters";
export * from "./services";
export * from "./use-cases";

View File

@ -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()),

View File

@ -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
);
}
}

View File

@ -0,0 +1 @@
export * from "./customer-invoice-application.service";

View File

@ -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);
}
});
}
}

View File

@ -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 = {

View File

@ -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;

View File

@ -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;

View File

@ -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";

View File

@ -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);
}

View File

@ -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;

View File

@ -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 = {

View File

@ -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 = {

View File

@ -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;
}
}

View File

@ -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>;

View File

@ -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;

View File

@ -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";

View File

@ -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;

View File

@ -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>>;
}

View File

@ -1 +1,3 @@
export * from "./customer-invoice-number-generator.interface";
export * from "./issue-customer-invoice-domain-service";
export * from "./proforma-customer-invoice-domain-service";

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -1 +1,2 @@
export * from "./customer-invoice-is-proforma.specification";
export * from "./proforma-can-transtion-to-issued.specification";

View File

@ -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);
}
}

View File

@ -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() {

View File

@ -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);*/
},
},
};

View File

@ -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,
};
}

View File

@ -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,
};
}

View File

@ -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)
);
}
}

View File

@ -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";

View File

@ -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);

View File

@ -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);
}

View File

@ -1,3 +1,4 @@
export * from "./dependencies";
export * from "./express";
export * from "./mappers";
export * from "./sequelize";

View File

@ -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,

View File

@ -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));
}
}
}

View File

@ -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>
>;

View File

@ -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";

View File

@ -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(),

View File

@ -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",

View File

@ -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";

View File

@ -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)