- CustomerInvoiceNumber es obligatorio
- Proforma ID - Cálculo de siguiente número de factura - Paso de proforma a issue
This commit is contained in:
parent
46e6b01a97
commit
78db3318fc
@ -1,7 +1,12 @@
|
||||
import { Criteria } from "@repo/rdx-criteria/server";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Collection, Result } from "@repo/rdx-utils";
|
||||
import { Collection, Maybe, Result } from "@repo/rdx-utils";
|
||||
import { Transaction } from "sequelize";
|
||||
import {
|
||||
CustomerInvoiceNumber,
|
||||
CustomerInvoiceSerie,
|
||||
ICustomerInvoiceNumberGenerator,
|
||||
} from "../domain";
|
||||
import {
|
||||
CustomerInvoice,
|
||||
CustomerInvoicePatchProps,
|
||||
@ -11,17 +16,50 @@ import { ICustomerInvoiceRepository } from "../domain/repositories";
|
||||
import { CustomerInvoiceListDTO } from "../infrastructure";
|
||||
|
||||
export class CustomerInvoiceApplicationService {
|
||||
constructor(private readonly repository: ICustomerInvoiceRepository) {}
|
||||
constructor(
|
||||
private readonly repository: ICustomerInvoiceRepository,
|
||||
private readonly numberGenerator: ICustomerInvoiceNumberGenerator
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Construye un nuevo agregado CustomerInvoice a partir de props validadas.
|
||||
* Devuelve el siguiente nº para proformas
|
||||
*
|
||||
* @param companyId - Identificador de la empresa a la que pertenece la proforma.
|
||||
* @param transaction - Transacción activa para la operación.
|
||||
* @returns Result<CustomerInvoiceNumber, Error> - El agregado construido o un error si falla la creación.
|
||||
*/
|
||||
async getNextProformaNumber(
|
||||
companyId: UniqueID,
|
||||
transaction: Transaction
|
||||
): Promise<Result<CustomerInvoiceNumber, Error>> {
|
||||
return await this.numberGenerator.nextForCompany(companyId, Maybe.none(), transaction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Devuelve el siguiente nº para facturas (issue)
|
||||
*
|
||||
* @param companyId - Identificador de la empresa a la que pertenece la factura.
|
||||
* @param props - Las propiedades ya validadas para crear la factura.
|
||||
* @param invoiceId - Identificador UUID de la factura (opcional).
|
||||
* @param series - Serie por la que buscar la última factura
|
||||
* @param transaction - Transacción activa para la operación.
|
||||
* @returns Result<CustomerInvoiceNumber, Error> - El agregado construido o un error si falla la creación.
|
||||
*/
|
||||
async getNextIssueInvoiceNumber(
|
||||
companyId: UniqueID,
|
||||
series: Maybe<CustomerInvoiceSerie>,
|
||||
transaction: Transaction
|
||||
): Promise<Result<CustomerInvoiceNumber, Error>> {
|
||||
return await this.numberGenerator.nextForCompany(companyId, series, transaction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construye una proforma a partir de props validadas.
|
||||
*
|
||||
* @param companyId - Identificador de la empresa a la que pertenece la proforma.
|
||||
* @param props - Las propiedades ya validadas para crear la proforma.
|
||||
* @param invoiceId - Identificador UUID de la proforma (opcional).
|
||||
* @returns Result<CustomerInvoice, Error> - El agregado construido o un error si falla la creación.
|
||||
*/
|
||||
buildInvoiceInCompany(
|
||||
buildProformaInCompany(
|
||||
companyId: UniqueID,
|
||||
props: Omit<CustomerInvoiceProps, "companyId">,
|
||||
invoiceId?: UniqueID
|
||||
@ -29,6 +67,28 @@ 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.
|
||||
*
|
||||
|
||||
@ -47,7 +47,7 @@ export class CustomerInvoiceFullPresenter extends Presenter<
|
||||
id: invoice.id.toString(),
|
||||
company_id: invoice.companyId.toString(),
|
||||
|
||||
invoice_number: toEmptyString(invoice.invoiceNumber, (value) => value.toString()),
|
||||
invoice_number: invoice.invoiceNumber.toString(),
|
||||
status: invoice.status.toPrimitive(),
|
||||
series: toEmptyString(invoice.series, (value) => value.toString()),
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@ export class ListCustomerInvoicesPresenter extends Presenter {
|
||||
is_proforma: invoice.isProforma,
|
||||
customer_id: invoice.customerId.toString(),
|
||||
|
||||
invoice_number: toEmptyString(invoice.invoiceNumber, (value) => value.toString()),
|
||||
invoice_number: invoice.invoiceNumber.toString(),
|
||||
status: invoice.status.toPrimitive(),
|
||||
series: toEmptyString(invoice.series, (value) => value.toString()),
|
||||
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export * from "./status-invoice_is_approved.spec";
|
||||
@ -1,7 +1,7 @@
|
||||
import { JsonTaxCatalogProvider } from "@erp/core";
|
||||
import { DuplicateEntityError, IPresenterRegistry, ITransactionManager } from "@erp/core/api";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
import { Transaction } from "sequelize";
|
||||
import { CreateCustomerInvoiceRequestDTO } from "../../../../common/dto";
|
||||
import { CustomerInvoiceApplicationService } from "../../customer-invoice-application.service";
|
||||
@ -21,7 +21,7 @@ export class CreateCustomerInvoiceUseCase {
|
||||
private readonly taxCatalog: JsonTaxCatalogProvider
|
||||
) {}
|
||||
|
||||
public execute(params: CreateCustomerInvoiceUseCaseInput) {
|
||||
public async execute(params: CreateCustomerInvoiceUseCaseInput) {
|
||||
const { dto, companyId } = params;
|
||||
const presenter = this.presenterRegistry.getPresenter({
|
||||
resource: "customer-invoice",
|
||||
@ -37,17 +37,30 @@ export class CreateCustomerInvoiceUseCase {
|
||||
|
||||
const { props, id } = dtoResult.data;
|
||||
|
||||
// 2) Construir entidad de dominio
|
||||
const buildResult = this.service.buildInvoiceInCompany(companyId, props, id);
|
||||
if (buildResult.isFailure) {
|
||||
return Result.fail(buildResult.error);
|
||||
}
|
||||
|
||||
const newInvoice = buildResult.data;
|
||||
|
||||
// 3) Ejecutar bajo transacción: verificar duplicado → persistir → ensamblar vista
|
||||
return this.transactionManager.complete(async (transaction: Transaction) => {
|
||||
try {
|
||||
// 2) Generar nuevo nº de proforma
|
||||
const nextNumberResult = await this.service.getNextProformaNumber(companyId, transaction);
|
||||
if (nextNumberResult.isFailure) {
|
||||
return Result.fail(nextNumberResult.error);
|
||||
}
|
||||
|
||||
const newProformaNumber = nextNumberResult.data;
|
||||
|
||||
// 3) Construir entidad de dominio
|
||||
const proformaProps = {
|
||||
...props,
|
||||
invoiceNumber: Maybe.some(newProformaNumber),
|
||||
};
|
||||
|
||||
const buildResult = this.service.buildProformaInCompany(companyId, proformaProps, id);
|
||||
if (buildResult.isFailure) {
|
||||
return Result.fail(buildResult.error);
|
||||
}
|
||||
|
||||
const newInvoice = buildResult.data;
|
||||
|
||||
const existsGuard = await this.ensureNotExists(companyId, id, transaction);
|
||||
if (existsGuard.isFailure) {
|
||||
return Result.fail(existsGuard.error);
|
||||
|
||||
@ -1,15 +1,24 @@
|
||||
import { EntityNotFoundError, ITransactionManager } from "@erp/core/api";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import { CustomerInvoiceNumber } from "../../domain";
|
||||
import { ITransactionManager } from "@erp/core/api";
|
||||
import { UniqueID, UtcDate } from "@repo/rdx-ddd";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
import { InvalidProformaStatusError } from "../../domain";
|
||||
import { StatusInvoiceIsApprovedSpecification } from "../../domain/specs";
|
||||
import { CustomerInvoiceApplicationService } from "../customer-invoice-application.service";
|
||||
import { StatusInvoiceIsApprovedSpecification } from "../specs";
|
||||
|
||||
type IssueCustomerInvoiceUseCaseInput = {
|
||||
companyId: UniqueID;
|
||||
invoice_id: string;
|
||||
proforma_id: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Caso de uso: Conversión de una proforma a factura definitiva.
|
||||
*
|
||||
* - Recupera la proforma
|
||||
* - Valida su estado ("approved")
|
||||
* - Genera la factura definitiva (nueva entidad)
|
||||
* - Marca la proforma como "issued"
|
||||
* - Persiste ambas dentro de la misma transacción
|
||||
*/
|
||||
export class IssueCustomerInvoiceUseCase {
|
||||
constructor(
|
||||
private readonly service: CustomerInvoiceApplicationService,
|
||||
@ -17,56 +26,78 @@ export class IssueCustomerInvoiceUseCase {
|
||||
) {}
|
||||
|
||||
public execute(params: IssueCustomerInvoiceUseCaseInput) {
|
||||
const { invoice_id, companyId } = params;
|
||||
const { proforma_id, companyId } = params;
|
||||
|
||||
const idOrError = UniqueID.create(invoice_id);
|
||||
const idOrError = UniqueID.create(proforma_id);
|
||||
|
||||
if (idOrError.isFailure) {
|
||||
return Result.fail(idOrError.error);
|
||||
}
|
||||
|
||||
const invoiceId = idOrError.data;
|
||||
const proformaId = idOrError.data;
|
||||
|
||||
return this.transactionManager.complete(async (transaction) => {
|
||||
try {
|
||||
const invoiceResult = await this.service.getInvoiceByIdInCompany(
|
||||
/** 1. Recuperamos la proforma */
|
||||
const proformaResult = await this.service.getInvoiceByIdInCompany(
|
||||
companyId,
|
||||
invoiceId,
|
||||
proformaId,
|
||||
transaction
|
||||
);
|
||||
|
||||
if (invoiceResult.isFailure) {
|
||||
return Result.fail(invoiceResult.error);
|
||||
if (proformaResult.isFailure) {
|
||||
return Result.fail(proformaResult.error);
|
||||
}
|
||||
|
||||
const invoiceProforma = invoiceResult.data;
|
||||
const proforma = proformaResult.data;
|
||||
|
||||
const isOk = new StatusInvoiceIsApprovedSpecification().isSatisfiedBy(invoiceProforma);
|
||||
|
||||
if (!isOk) {
|
||||
return Result.fail(
|
||||
new EntityNotFoundError("Customer invoice", "id", invoiceId.toString())
|
||||
);
|
||||
/** 2. Comprobamos que la proforma origen está aprovada para generar la factura */
|
||||
const isApprovedSpec = new StatusInvoiceIsApprovedSpecification();
|
||||
if (!(await isApprovedSpec.isSatisfiedBy(proforma))) {
|
||||
return Result.fail(new InvalidProformaStatusError(proformaId.toString()));
|
||||
}
|
||||
|
||||
// La factura se puede emitir.
|
||||
// Pedir el número de factura
|
||||
const newInvoiceNumber = CustomerInvoiceNumber.create("xxx/001").data;
|
||||
|
||||
// Asignamos el número de la factura
|
||||
|
||||
const issuedInvoiceResult = invoiceProforma.issueInvoice(newInvoiceNumber);
|
||||
if (issuedInvoiceResult.isFailure) {
|
||||
return Result.fail(
|
||||
new EntityNotFoundError("Customer invoice", "id", issuedInvoiceResult.error)
|
||||
);
|
||||
/** 3. Generar nueva factura */
|
||||
const nextNumberResult = await this.service.getNextIssueInvoiceNumber(
|
||||
companyId,
|
||||
proforma.series,
|
||||
transaction
|
||||
);
|
||||
if (nextNumberResult.isFailure) {
|
||||
return Result.fail(nextNumberResult.error);
|
||||
}
|
||||
|
||||
const issuedInvoice = issuedInvoiceResult.data;
|
||||
const newIssueNumber = nextNumberResult.data;
|
||||
|
||||
this.service.updateInvoiceInCompany(companyId, issuedInvoice, transaction);
|
||||
// props base obtenidas del agregado proforma
|
||||
const issuedInvoiceOrError = this.service.buildIssueInvoiceInCompany(companyId, proforma, {
|
||||
invoiceNumber: Maybe.some(newIssueNumber),
|
||||
invoiceDate: UtcDate.today(),
|
||||
});
|
||||
|
||||
//return await this.service.IssueInvoiceByIdInCompany(companyId, invoiceId, transaction);
|
||||
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(
|
||||
companyId,
|
||||
updatedProformaResult.data,
|
||||
transaction
|
||||
);
|
||||
|
||||
/** 5. Resultado */
|
||||
return Result.ok(issuedInvoice);
|
||||
} catch (error: unknown) {
|
||||
return Result.fail(error as Error);
|
||||
}
|
||||
|
||||
@ -175,7 +175,7 @@
|
||||
<td>{{description}}</td>
|
||||
<td class="text-right">{{#if quantity}}{{quantity}}{{else}} {{/if}}</td>
|
||||
<td class="text-right">{{#if unit_amount}}{{unit_amount}}{{else}} {{/if}}</td>
|
||||
<td class="text-right">{{#if subtotal_amount}}{{subtotal_amount}}{{else}} {{/if}}</td>
|
||||
<td class="text-right">{{#if total_amount}}{{total_amount}}{{else}} {{/if}}</td>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
|
||||
@ -24,8 +24,10 @@ export interface CustomerInvoiceProps {
|
||||
isProforma: boolean;
|
||||
status: CustomerInvoiceStatus;
|
||||
|
||||
proformaId: Maybe<UniqueID>;
|
||||
|
||||
series: Maybe<CustomerInvoiceSerie>;
|
||||
invoiceNumber: Maybe<CustomerInvoiceNumber>;
|
||||
invoiceNumber: CustomerInvoiceNumber;
|
||||
|
||||
invoiceDate: UtcDate;
|
||||
operationDate: Maybe<UtcDate>;
|
||||
@ -70,7 +72,9 @@ export interface ICustomerInvoice {
|
||||
|
||||
getTaxes(): InvoiceTaxTotal[];
|
||||
|
||||
issueInvoice(newInvoiceNumber: CustomerInvoiceNumber): Result<CustomerInvoice, Error>;
|
||||
asIssued(): Result<CustomerInvoice, Error>;
|
||||
|
||||
getIssuedInvoiceProps(): CustomerInvoiceProps;
|
||||
}
|
||||
|
||||
export class CustomerInvoice
|
||||
@ -143,6 +147,10 @@ export class CustomerInvoice
|
||||
return this.props.isProforma;
|
||||
}
|
||||
|
||||
public get proformaId(): Maybe<UniqueID> {
|
||||
return this.props.proformaId;
|
||||
}
|
||||
|
||||
public get status(): CustomerInvoiceStatus {
|
||||
return this.props.status;
|
||||
}
|
||||
@ -301,15 +309,21 @@ export class CustomerInvoice
|
||||
};
|
||||
}
|
||||
|
||||
public issueInvoice(newInvoiceNumber: CustomerInvoiceNumber) {
|
||||
return CustomerInvoice.create(
|
||||
{
|
||||
...this.props,
|
||||
status: CustomerInvoiceStatus.createIssued(),
|
||||
isProforma: false,
|
||||
invoiceNumber: Maybe.some(newInvoiceNumber),
|
||||
},
|
||||
this.id
|
||||
);
|
||||
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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,85 +0,0 @@
|
||||
import { CurrencyCode, LanguageCode, MoneyValue, Percentage, Quantity } from "@repo/rdx-ddd";
|
||||
import { CustomerInvoiceItemDescription } from "../../value-objects";
|
||||
import { CustomerInvoiceItem } from "./customer-invoice-item";
|
||||
|
||||
describe("CustomerInvoiceItem", () => {
|
||||
it("debería calcular correctamente el subtotal (unitPrice * quantity)", () => {
|
||||
const props = {
|
||||
description: CustomerInvoiceItemDescription.create("Producto A"),
|
||||
quantity: Quantity.create({ amount: 200, scale: 2 }),
|
||||
unitPrice: MoneyValue.create(50),
|
||||
discount: Percentage.create(0),
|
||||
languageCode: LanguageCode.create("es"),
|
||||
currencyCode: CurrencyCode.create("EUR"),
|
||||
};
|
||||
|
||||
const result = CustomerInvoiceItem.create(props);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const customerInvoiceItem = result.unwrap();
|
||||
expect(customerInvoiceItem.subtotalPrice.value).toBe(100); // 50 * 2
|
||||
});
|
||||
|
||||
it("debería calcular correctamente el total con descuento", () => {
|
||||
const props = {
|
||||
description: new CustomerInvoiceItemDescription("Producto B"),
|
||||
quantity: new Quantity(3),
|
||||
unitPrice: new MoneyValue(30),
|
||||
discount: new Percentage(10), // 10%
|
||||
};
|
||||
|
||||
const result = CustomerInvoiceItem.create(props);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const customerInvoiceItem = result.unwrap();
|
||||
expect(customerInvoiceItem.totalPrice.value).toBe(81); // (30 * 3) - 10% de (30 * 3)
|
||||
});
|
||||
|
||||
it("debería devolver los valores correctos de las propiedades", () => {
|
||||
const props = {
|
||||
description: new CustomerInvoiceItemDescription("Producto C"),
|
||||
quantity: new Quantity(1),
|
||||
unitPrice: new MoneyValue(100),
|
||||
discount: new Percentage(5),
|
||||
};
|
||||
|
||||
const result = CustomerInvoiceItem.create(props);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const customerInvoiceItem = result.unwrap();
|
||||
expect(customerInvoiceItem.description.value).toBe("Producto C");
|
||||
expect(customerInvoiceItem.quantity.value).toBe(1);
|
||||
expect(customerInvoiceItem.unitPrice.value).toBe(100);
|
||||
expect(customerInvoiceItem.discount.value).toBe(5);
|
||||
});
|
||||
|
||||
it("debería manejar correctamente un descuento del 0%", () => {
|
||||
const props = {
|
||||
description: new CustomerInvoiceItemDescription("Producto D"),
|
||||
quantity: new Quantity(4),
|
||||
unitPrice: new MoneyValue(25),
|
||||
discount: new Percentage(0),
|
||||
};
|
||||
|
||||
const result = CustomerInvoiceItem.create(props);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const customerInvoiceItem = result.unwrap();
|
||||
expect(customerInvoiceItem.totalPrice.value).toBe(100); // 25 * 4
|
||||
});
|
||||
|
||||
it("debería manejar correctamente un descuento del 100%", () => {
|
||||
const props = {
|
||||
description: new CustomerInvoiceItemDescription("Producto E"),
|
||||
quantity: new Quantity(2),
|
||||
unitPrice: new MoneyValue(50),
|
||||
discount: new Percentage(100),
|
||||
};
|
||||
|
||||
const result = CustomerInvoiceItem.create(props);
|
||||
|
||||
expect(result.isOk()).toBe(true);
|
||||
const customerInvoiceItem = result.unwrap();
|
||||
expect(customerInvoiceItem.totalPrice.value).toBe(0); // (50 * 2) - 100% de (50 * 2)
|
||||
});
|
||||
});
|
||||
@ -1 +1,2 @@
|
||||
export * from "./customer-invoice-id-already-exits-error";
|
||||
export * from "./invalid-proforma-status-error";
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
import { DomainError } from "@repo/rdx-ddd";
|
||||
|
||||
export class InvalidProformaStatusError extends DomainError {
|
||||
constructor(id: string, options?: ErrorOptions) {
|
||||
super(`Error. Proforma with id '${id}' has invalid status.`, options);
|
||||
this.name = "InvalidProformaStatusError";
|
||||
}
|
||||
}
|
||||
|
||||
export const isInvalidProformaStatusError = (e: unknown): e is InvalidProformaStatusError =>
|
||||
e instanceof InvalidProformaStatusError;
|
||||
@ -2,4 +2,6 @@ export * from "./aggregates";
|
||||
export * from "./entities";
|
||||
export * from "./errors";
|
||||
export * from "./repositories";
|
||||
export * from "./services";
|
||||
export * from "./specs";
|
||||
export * from "./value-objects";
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
import { CustomerInvoiceNumber, CustomerInvoiceSerie } from "../value-objects";
|
||||
|
||||
/**
|
||||
* Servicio de dominio que define cómo se genera el siguiente número de factura.
|
||||
*/
|
||||
export interface ICustomerInvoiceNumberGenerator {
|
||||
/**
|
||||
* Devuelve el siguiente número de factura disponible para una empresa dentro de una "serie" de factura.
|
||||
*
|
||||
* @param companyId - Identificador de la empresa
|
||||
* @param serie - Serie por la que buscar la última factura
|
||||
* @param transaction - Transacción activa
|
||||
*/
|
||||
nextForCompany(
|
||||
companyId: UniqueID,
|
||||
series: Maybe<CustomerInvoiceSerie>,
|
||||
transaction: any
|
||||
): Promise<Result<CustomerInvoiceNumber, Error>>;
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from "./customer-invoice-number-generator.interface";
|
||||
1
modules/customer-invoices/src/api/domain/specs/index.ts
Normal file
1
modules/customer-invoices/src/api/domain/specs/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./status-invoice-is-approved.specification";
|
||||
@ -1,5 +1,5 @@
|
||||
import { CompositeSpecification } from "@repo/rdx-ddd";
|
||||
import { CustomerInvoice } from "../../domain";
|
||||
import { CustomerInvoice } from "../aggregates";
|
||||
|
||||
export class StatusInvoiceIsApprovedSpecification extends CompositeSpecification<CustomerInvoice> {
|
||||
public async isSatisfiedBy(invoice: CustomerInvoice): Promise<boolean> {
|
||||
@ -7,7 +7,7 @@ interface ICustomerInvoiceNumberProps {
|
||||
}
|
||||
|
||||
export class CustomerInvoiceNumber extends ValueObject<ICustomerInvoiceNumberProps> {
|
||||
private static readonly MAX_LENGTH = 255;
|
||||
private static readonly MAX_LENGTH = 12;
|
||||
private static readonly FIELD = "invoiceNumber";
|
||||
private static readonly ERROR_CODE = "INVALID_INVOICE_NUMBER";
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ interface ICustomerInvoiceSerieProps {
|
||||
}
|
||||
|
||||
export class CustomerInvoiceSerie extends ValueObject<ICustomerInvoiceSerieProps> {
|
||||
private static readonly MAX_LENGTH = 255;
|
||||
private static readonly MAX_LENGTH = 10;
|
||||
private static readonly FIELD = "invoiceSeries";
|
||||
private static readonly ERROR_CODE = "INVALID_INVOICE_SERIE";
|
||||
|
||||
|
||||
@ -11,8 +11,9 @@ export enum INVOICE_STATUS {
|
||||
APPROVED = "approved", // <- Proforma
|
||||
REJECTED = "rejected", // <- Proforma
|
||||
|
||||
// issued <- (si is_proforma === true) => Es una proforma (histórica)
|
||||
// issued <- (si is_proforma === false) => Factura y enviada a Veri*Factu
|
||||
// status === issued <- (si is_proforma === true) => Es una proforma (histórica)
|
||||
// status === issued <- (si is_proforma === false) => Factura y enviará/enviada a Veri*Factu
|
||||
|
||||
ISSUED = "issued",
|
||||
}
|
||||
export class CustomerInvoiceStatus extends ValueObject<ICustomerInvoiceStatusProps> {
|
||||
|
||||
@ -1,35 +1,32 @@
|
||||
// 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 { JsonTaxCatalogProvider, SpainTaxCatalogProvider } from "@erp/core";
|
||||
import {
|
||||
CustomerInvoiceApplicationService,
|
||||
CustomerInvoiceItemsReportPersenter,
|
||||
} from "../application";
|
||||
import { CustomerInvoiceDomainMapper, CustomerInvoiceListMapper } from "./mappers";
|
||||
import { CustomerInvoiceRepository } from "./sequelize";
|
||||
import { SequelizeInvoiceNumberGenerator } from "./services";
|
||||
|
||||
export type CustomerInvoiceDeps = {
|
||||
transactionManager: SequelizeTransactionManager;
|
||||
@ -47,6 +44,7 @@ export type CustomerInvoiceDeps = {
|
||||
update: () => UpdateCustomerInvoiceUseCase;
|
||||
//delete: () => DeleteCustomerInvoiceUseCase;
|
||||
report: () => ReportCustomerInvoiceUseCase;
|
||||
issue: () => IssueCustomerInvoiceUseCase;
|
||||
};
|
||||
getService: (name: string) => any;
|
||||
listServices: () => string[];
|
||||
@ -73,7 +71,8 @@ export function buildCustomerInvoiceDependencies(params: ModuleParams): Customer
|
||||
|
||||
// Repository & Services
|
||||
const repo = new CustomerInvoiceRepository({ mapperRegistry, database });
|
||||
const service = new CustomerInvoiceApplicationService(repo);
|
||||
const numberGenerator = new SequelizeInvoiceNumberGenerator();
|
||||
const service = new CustomerInvoiceApplicationService(repo, numberGenerator);
|
||||
|
||||
// Presenter Registry
|
||||
const presenterRegistry = new InMemoryPresenterRegistry();
|
||||
@ -162,6 +161,7 @@ export function buildCustomerInvoiceDependencies(params: ModuleParams): Customer
|
||||
// delete: () => new DeleteCustomerInvoiceUseCase(service, transactionManager),
|
||||
report: () =>
|
||||
new ReportCustomerInvoiceUseCase(service, transactionManager, presenterRegistry),
|
||||
issue: () => new IssueCustomerInvoiceUseCase(service, transactionManager),
|
||||
},
|
||||
listServices,
|
||||
getService,
|
||||
|
||||
@ -1,21 +1,25 @@
|
||||
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||
import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||
import { IssueCustomerInvoiceUseCase } from "../../../application";
|
||||
|
||||
export class IssueCustomerInvoiceController extends ExpressController {
|
||||
public constructor(
|
||||
private readonly useCase: IssueCustomerInvoiceUseCase
|
||||
/* private readonly presenter: any */
|
||||
) {
|
||||
public constructor(private readonly useCase: IssueCustomerInvoiceUseCase) {
|
||||
super();
|
||||
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
|
||||
}
|
||||
|
||||
async executeImpl() {
|
||||
const tenantId = this.getTenantId()!; // garantizado por tenantGuard
|
||||
const { id } = this.req.params;
|
||||
async executeImpl(): Promise<any> {
|
||||
const companyId = this.getTenantId(); // garantizado por tenantGuard
|
||||
if (!companyId) {
|
||||
return this.forbiddenError("Tenant ID not found");
|
||||
}
|
||||
|
||||
const result = await this.useCase.execute({ id, tenantId });
|
||||
const { proforma_id } = this.req.params;
|
||||
if (!proforma_id) {
|
||||
return this.invalidInputError("Proforma ID missing");
|
||||
}
|
||||
|
||||
const result = await this.useCase.execute({ proforma_id, companyId });
|
||||
|
||||
return result.match(
|
||||
(data) => this.ok(data),
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||
import { authGuard, ExpressController, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
|
||||
import { UpdateCustomerInvoiceByIdRequestDTO } from "../../../../common/dto";
|
||||
import { UpdateCustomerInvoiceUseCase } from "../../../application";
|
||||
|
||||
@ -14,10 +14,15 @@ export class UpdateCustomerInvoiceController extends ExpressController {
|
||||
if (!companyId) {
|
||||
return this.forbiddenError("Tenant ID not found");
|
||||
}
|
||||
const { invoice_id } = this.req.params;
|
||||
|
||||
const { proforma_id } = this.req.params;
|
||||
if (!proforma_id) {
|
||||
return this.invalidInputError("Proforma ID missing");
|
||||
}
|
||||
|
||||
const dto = this.req.body as UpdateCustomerInvoiceByIdRequestDTO;
|
||||
|
||||
const result = await this.useCase.execute({ invoice_id, companyId, dto });
|
||||
const result = await this.useCase.execute({ invoice_id: proforma_id, companyId, dto });
|
||||
|
||||
return result.match(
|
||||
(data) => this.ok(data),
|
||||
|
||||
@ -19,6 +19,7 @@ import {
|
||||
ReportCustomerInvoiceController,
|
||||
UpdateCustomerInvoiceController,
|
||||
} from "./controllers";
|
||||
import { IssueCustomerInvoiceController } from "./controllers/issue-customer-invoice.controller";
|
||||
|
||||
export const customerInvoicesRouter = (params: ModuleParams) => {
|
||||
const { app, baseRoutePath, logger } = params as {
|
||||
@ -84,7 +85,7 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
|
||||
);
|
||||
|
||||
router.put(
|
||||
"/:invoice_id",
|
||||
"/:proforma_id",
|
||||
//checkTabContext,
|
||||
|
||||
validateRequest(UpdateCustomerInvoiceByIdParamsRequestSchema, "params"),
|
||||
@ -117,18 +118,21 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
|
||||
const controller = new ReportCustomerInvoiceController(useCase);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
); /*router.put(
|
||||
"/:invoice_id/issue",
|
||||
);
|
||||
|
||||
router.put(
|
||||
"/:proforma_id/issue",
|
||||
//checkTabContext,
|
||||
validateRequest(XXX, "params"),
|
||||
validateRequest(XXX, "body"),
|
||||
|
||||
/*validateRequest(XXX, "params"),
|
||||
validateRequest(XXX, "body"),*/
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const useCase = deps.build.issue();
|
||||
const controller = new IssueCustomerInvoiceController(useCase);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
);
|
||||
*/
|
||||
);
|
||||
|
||||
app.use(`${baseRoutePath}/customer-invoices`, router);
|
||||
};
|
||||
|
||||
@ -70,6 +70,12 @@ export class CustomerInvoiceDomainMapper
|
||||
|
||||
const isProforma = Boolean(source.is_proforma);
|
||||
|
||||
const proformaId = extractOrPushError(
|
||||
maybeFromNullableVO(source.proforma_id, (v) => UniqueID.create(v)),
|
||||
"proforma_id",
|
||||
errors
|
||||
);
|
||||
|
||||
const status = extractOrPushError(
|
||||
CustomerInvoiceStatus.create(source.status),
|
||||
"status",
|
||||
@ -83,7 +89,7 @@ export class CustomerInvoiceDomainMapper
|
||||
);
|
||||
|
||||
const invoiceNumber = extractOrPushError(
|
||||
maybeFromNullableVO(source.invoice_number, (v) => CustomerInvoiceNumber.create(v)),
|
||||
CustomerInvoiceNumber.create(source.invoice_number),
|
||||
"invoice_number",
|
||||
errors
|
||||
);
|
||||
@ -172,6 +178,7 @@ export class CustomerInvoiceDomainMapper
|
||||
companyId,
|
||||
customerId,
|
||||
isProforma,
|
||||
proformaId,
|
||||
status,
|
||||
series,
|
||||
invoiceNumber,
|
||||
@ -204,13 +211,13 @@ export class CustomerInvoiceDomainMapper
|
||||
...params,
|
||||
});
|
||||
|
||||
if (recipientResult.isFailure) {
|
||||
/*if (recipientResult.isFailure) {
|
||||
errors.push({
|
||||
path: "recipient",
|
||||
|
||||
message: recipientResult.error.message,
|
||||
});
|
||||
}
|
||||
}*/
|
||||
|
||||
// 3) Items (colección)
|
||||
const itemsResults = this._itemsMapper.mapToDomainCollection(
|
||||
@ -223,12 +230,12 @@ export class CustomerInvoiceDomainMapper
|
||||
}
|
||||
);
|
||||
|
||||
if (itemsResults.isFailure) {
|
||||
/*if (itemsResults.isFailure) {
|
||||
errors.push({
|
||||
path: "items",
|
||||
message: itemsResults.error.message,
|
||||
});
|
||||
}
|
||||
}*/
|
||||
|
||||
// Nota: los impuestos a nivel factura (tabla customer_invoice_taxes) se derivan de los items.
|
||||
// El agregado expone un getter `taxes` (derivado). No se incluye en las props.
|
||||
@ -254,6 +261,7 @@ export class CustomerInvoiceDomainMapper
|
||||
companyId: attributes.companyId!,
|
||||
|
||||
isProforma: attributes.isProforma,
|
||||
proformaId: attributes.proformaId!,
|
||||
status: attributes.status!,
|
||||
series: attributes.series!,
|
||||
invoiceNumber: attributes.invoiceNumber!,
|
||||
@ -333,7 +341,7 @@ export class CustomerInvoiceDomainMapper
|
||||
const allAmounts = source.getAllAmounts();
|
||||
|
||||
// 4) Cliente
|
||||
const recipient = this._mapRecipientToPersistence(source, {
|
||||
const recipient = this._recipientMapper.mapToPersistence(source.recipient, {
|
||||
errors,
|
||||
parent: source,
|
||||
...params,
|
||||
@ -346,16 +354,17 @@ export class CustomerInvoiceDomainMapper
|
||||
);
|
||||
}
|
||||
|
||||
const invoiceValues: CustomerInvoiceCreationAttributes = {
|
||||
const invoiceValues: Partial<CustomerInvoiceCreationAttributes> = {
|
||||
// Identificación
|
||||
id: source.id.toPrimitive(),
|
||||
company_id: source.companyId.toPrimitive(),
|
||||
|
||||
// Flags / estado / serie / número
|
||||
is_proforma: source.isProforma,
|
||||
proforma_id: toNullable(source.proformaId, (v) => v.toPrimitive()),
|
||||
status: source.status.toPrimitive(),
|
||||
series: toNullable(source.series, (v) => v.toPrimitive()),
|
||||
invoice_number: toNullable(source.invoiceNumber, (v) => v.toPrimitive()),
|
||||
invoice_number: source.invoiceNumber.toPrimitive(),
|
||||
|
||||
invoice_date: source.invoiceDate.toPrimitive(),
|
||||
operation_date: toNullable(source.operationDate, (v) => v.toPrimitive()),
|
||||
@ -397,47 +406,8 @@ export class CustomerInvoiceDomainMapper
|
||||
items,
|
||||
};
|
||||
|
||||
return Result.ok<CustomerInvoiceCreationAttributes>(invoiceValues);
|
||||
}
|
||||
|
||||
protected _mapRecipientToPersistence(source: CustomerInvoice, params?: MapperParamsType) {
|
||||
const { errors } = params as {
|
||||
errors: ValidationErrorDetail[];
|
||||
};
|
||||
|
||||
const hasRecipient = source.hasRecipient;
|
||||
const recipient = source.recipient?.getOrUndefined();
|
||||
|
||||
if (!source.isProforma && !hasRecipient) {
|
||||
errors.push({
|
||||
path: "recipient",
|
||||
message: "[CustomerInvoiceDomainMapper] Issued customer invoice w/o recipient data",
|
||||
});
|
||||
}
|
||||
|
||||
const recipientValues = {
|
||||
customer_tin: !source.isProforma ? recipient?.tin.toPrimitive() : null,
|
||||
customer_name: !source.isProforma ? recipient?.name.toPrimitive() : null,
|
||||
customer_street: !source.isProforma
|
||||
? toNullable(recipient?.street, (v) => v.toPrimitive())
|
||||
: null,
|
||||
customer_street2: !source.isProforma
|
||||
? toNullable(recipient?.street2, (v) => v.toPrimitive())
|
||||
: null,
|
||||
customer_city: !source.isProforma
|
||||
? toNullable(recipient?.city, (v) => v.toPrimitive())
|
||||
: null,
|
||||
customer_province: !source.isProforma
|
||||
? toNullable(recipient?.province, (v) => v.toPrimitive())
|
||||
: null,
|
||||
customer_postal_code: !source.isProforma
|
||||
? toNullable(recipient?.postalCode, (v) => v.toPrimitive())
|
||||
: null,
|
||||
customer_country: !source.isProforma
|
||||
? toNullable(recipient?.country, (v) => v.toPrimitive())
|
||||
: null,
|
||||
};
|
||||
|
||||
return recipientValues;
|
||||
return Result.ok<CustomerInvoiceCreationAttributes>(
|
||||
invoiceValues as CustomerInvoiceCreationAttributes
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
import { MapperParamsType } from "@erp/core/api";
|
||||
import {
|
||||
City,
|
||||
Country,
|
||||
extractOrPushError,
|
||||
maybeFromNullableVO,
|
||||
Name,
|
||||
PostalCode,
|
||||
Province,
|
||||
Street,
|
||||
TINNumber,
|
||||
toNullable,
|
||||
ValidationErrorCollection,
|
||||
ValidationErrorDetail,
|
||||
extractOrPushError,
|
||||
maybeFromNullableVO,
|
||||
} from "@repo/rdx-ddd";
|
||||
|
||||
import { MapperParamsType } from "@erp/core/api";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
import { CustomerInvoiceProps, InvoiceRecipient } from "../../../domain";
|
||||
import { CustomerInvoice, CustomerInvoiceProps, InvoiceRecipient } from "../../../domain";
|
||||
import { CustomerInvoiceModel } from "../../sequelize";
|
||||
|
||||
export class InvoiceRecipientDomainMapper {
|
||||
@ -114,4 +114,64 @@ export class InvoiceRecipientDomainMapper {
|
||||
|
||||
return Result.ok(Maybe.some(createResult.data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapea los datos del destinatario (recipient) de una factura de cliente
|
||||
* al formato esperado por la capa de persistencia.
|
||||
*
|
||||
* Reglas:
|
||||
* - Si la factura es proforma (`isProforma === true`), todos los campos de recipient son `null`.
|
||||
* - Si la factura no es proforma (`isProforma === false`), debe existir `recipient`.
|
||||
* En caso contrario, se agrega un error de validación.
|
||||
*/
|
||||
mapToPersistence(source: Maybe<InvoiceRecipient>, params?: MapperParamsType) {
|
||||
const { errors, parent } = params as {
|
||||
parent: CustomerInvoice;
|
||||
errors: ValidationErrorDetail[];
|
||||
};
|
||||
|
||||
const { isProforma, hasRecipient } = parent;
|
||||
|
||||
// Validación: facturas emitidas deben tener destinatario.
|
||||
if (!isProforma && !hasRecipient) {
|
||||
errors.push({
|
||||
path: "recipient",
|
||||
message: "[CustomerInvoiceDomainMapper] Issued customer invoice without recipient data",
|
||||
});
|
||||
}
|
||||
|
||||
// Si hay errores previos, devolvemos fallo de validación inmediatamente.
|
||||
if (errors.length > 0) {
|
||||
return Result.fail(
|
||||
new ValidationErrorCollection("Customer invoice mapping to persistence failed", errors)
|
||||
);
|
||||
}
|
||||
|
||||
// Si es proforma o no hay destinatario, todos los campos deben ser null.
|
||||
if (isProforma || source.isNone()) {
|
||||
return {
|
||||
customer_tin: null,
|
||||
customer_name: null,
|
||||
customer_street: null,
|
||||
customer_street2: null,
|
||||
customer_city: null,
|
||||
customer_province: null,
|
||||
customer_postal_code: null,
|
||||
customer_country: null,
|
||||
};
|
||||
}
|
||||
|
||||
const recipient = source.unwrap();
|
||||
|
||||
return {
|
||||
customer_tin: recipient.tin.toPrimitive(),
|
||||
customer_name: recipient.name.toPrimitive(),
|
||||
customer_street: toNullable(recipient.street, (v) => v.toPrimitive()),
|
||||
customer_street2: toNullable(recipient.street2, (v) => v.toPrimitive()),
|
||||
customer_city: toNullable(recipient.city, (v) => v.toPrimitive()),
|
||||
customer_province: toNullable(recipient.province, (v) => v.toPrimitive()),
|
||||
customer_postal_code: toNullable(recipient.postalCode, (v) => v.toPrimitive()),
|
||||
customer_country: toNullable(recipient.country, (v) => v.toPrimitive()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@ export type CustomerInvoiceListDTO = {
|
||||
companyId: UniqueID;
|
||||
|
||||
isProforma: boolean;
|
||||
invoiceNumber: Maybe<CustomerInvoiceNumber>;
|
||||
invoiceNumber: CustomerInvoiceNumber;
|
||||
status: CustomerInvoiceStatus;
|
||||
series: Maybe<CustomerInvoiceSerie>;
|
||||
|
||||
@ -152,7 +152,7 @@ export class CustomerInvoiceListMapper
|
||||
);
|
||||
|
||||
const invoiceNumber = extractOrPushError(
|
||||
maybeFromNullableVO(raw.invoice_number, (value) => CustomerInvoiceNumber.create(value)),
|
||||
CustomerInvoiceNumber.create(raw.invoice_number),
|
||||
"invoice_number",
|
||||
errors
|
||||
);
|
||||
|
||||
@ -45,7 +45,7 @@ export class CustomerInvoiceModel extends Model<
|
||||
declare proforma_id: CreationOptional<string | null>; // ID de la proforma origen de la factura
|
||||
|
||||
declare series: CreationOptional<string | null>;
|
||||
declare invoice_number: CreationOptional<string | null>;
|
||||
declare invoice_number: CreationOptional<string>;
|
||||
declare invoice_date: CreationOptional<string>;
|
||||
declare operation_date: CreationOptional<string | null>;
|
||||
declare language_code: CreationOptional<string>;
|
||||
@ -183,7 +183,7 @@ export default (database: Sequelize) => {
|
||||
proforma_id: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: true,
|
||||
defaultValue: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
|
||||
status: {
|
||||
@ -193,13 +193,13 @@ export default (database: Sequelize) => {
|
||||
},
|
||||
|
||||
series: {
|
||||
type: new DataTypes.STRING(),
|
||||
type: new DataTypes.STRING(10),
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
|
||||
invoice_number: {
|
||||
type: new DataTypes.STRING(),
|
||||
type: new DataTypes.STRING(12),
|
||||
allowNull: true,
|
||||
defaultValue: null,
|
||||
},
|
||||
@ -404,10 +404,22 @@ export default (database: Sequelize) => {
|
||||
name: "idx_invoices_company_date",
|
||||
fields: ["company_id", "deleted_at", { name: "invoice_date", order: "DESC" }],
|
||||
},
|
||||
|
||||
{
|
||||
name: "idx_invoice_company_series_number",
|
||||
fields: ["company_id", "series", "invoice_number"],
|
||||
unique: true,
|
||||
}, // <- para consulta get
|
||||
|
||||
{ name: "idx_invoice_date", fields: ["invoice_date"] }, // <- para ordenación
|
||||
{ name: "idx_company_idx", fields: ["id", "company_id"], unique: true }, // <- para consulta get
|
||||
{ name: "idx_proforma_id", fields: ["proforma_id"], unique: false },
|
||||
{ name: "idx_factuges", fields: ["factuges_id"], unique: false }, // <- para el proceso python
|
||||
|
||||
{ name: "idx_invoice_company_id", fields: ["id", "company_id"], unique: true }, // <- para consulta get
|
||||
|
||||
{ name: "idx_invoice_proforma_id", fields: ["proforma_id"], unique: false }, // <- para localizar factura por medio de proforma
|
||||
|
||||
{ name: "idx_invoice_factuges", fields: ["factuges_id"], unique: false }, // <- para el proceso python
|
||||
|
||||
// Para búsquedas simples
|
||||
{
|
||||
name: "ft_customer_invoice",
|
||||
type: "FULLTEXT",
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export * from "./sequelize-invoice-number-generator";
|
||||
@ -0,0 +1,68 @@
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
import { literal, Transaction, WhereOptions } from "sequelize";
|
||||
import {
|
||||
CustomerInvoiceNumber,
|
||||
CustomerInvoiceSerie,
|
||||
ICustomerInvoiceNumberGenerator,
|
||||
} from "../../domain";
|
||||
import { CustomerInvoiceModel } from "../sequelize";
|
||||
|
||||
/**
|
||||
* Generador de números de factura
|
||||
*/
|
||||
export class SequelizeInvoiceNumberGenerator implements ICustomerInvoiceNumberGenerator {
|
||||
public async nextForCompany(
|
||||
companyId: UniqueID,
|
||||
series: Maybe<CustomerInvoiceSerie>,
|
||||
transaction: Transaction
|
||||
): Promise<Result<CustomerInvoiceNumber, Error>> {
|
||||
const where: WhereOptions = {
|
||||
company_id: companyId.toString(),
|
||||
is_proforma: false,
|
||||
};
|
||||
|
||||
series.match(
|
||||
(serieVO) => {
|
||||
where.series = serieVO.toString();
|
||||
},
|
||||
() => {
|
||||
where.series = null;
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
const lastInvoice = await CustomerInvoiceModel.findOne({
|
||||
attributes: ["invoice_number"],
|
||||
where,
|
||||
// Orden numérico real: CAST(... AS UNSIGNED)
|
||||
order: [literal("CAST(invoice_number AS UNSIGNED) DESC")],
|
||||
transaction,
|
||||
raw: true,
|
||||
// Bloqueo opcional para evitar carreras si estás dentro de una TX
|
||||
lock: transaction.LOCK.UPDATE, // requiere InnoDB y TX abierta
|
||||
});
|
||||
|
||||
let nextValue = "00001"; // valor inicial por defecto
|
||||
|
||||
if (lastInvoice) {
|
||||
const current = Number(lastInvoice.invoice_number);
|
||||
const next = Number.isFinite(current) && current > 0 ? current + 1 : 1;
|
||||
nextValue = String(next).padStart(5, "0");
|
||||
}
|
||||
|
||||
const numberResult = CustomerInvoiceNumber.create(nextValue);
|
||||
if (numberResult.isFailure) {
|
||||
return Result.fail(numberResult.error);
|
||||
}
|
||||
|
||||
return Result.ok(numberResult.data);
|
||||
} catch (error) {
|
||||
return Result.fail(
|
||||
new Error(
|
||||
`Error generating invoice number for company ${companyId}: ${(error as Error).message}`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4,25 +4,25 @@ import {
|
||||
Country,
|
||||
CurrencyCode,
|
||||
EmailAddress,
|
||||
extractOrPushError,
|
||||
LanguageCode,
|
||||
maybeFromNullableVO,
|
||||
Name,
|
||||
PhoneNumber,
|
||||
PostalAddress,
|
||||
PostalCode,
|
||||
Province,
|
||||
Street,
|
||||
TINNumber,
|
||||
TaxCode,
|
||||
TextValue,
|
||||
URLAddress,
|
||||
TINNumber,
|
||||
toNullable,
|
||||
UniqueID,
|
||||
URLAddress,
|
||||
ValidationErrorCollection,
|
||||
ValidationErrorDetail,
|
||||
extractOrPushError,
|
||||
maybeFromNullableVO,
|
||||
toNullable,
|
||||
} from "@repo/rdx-ddd";
|
||||
import { Collection, Result, isNullishOrEmpty } from "@repo/rdx-utils";
|
||||
import { Collection, isNullishOrEmpty, Result } from "@repo/rdx-utils";
|
||||
import { Customer, CustomerProps, CustomerStatus } from "../../../domain";
|
||||
import { CustomerCreationAttributes, CustomerModel } from "../../sequelize";
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user