- CustomerInvoiceNumber es obligatorio

- Proforma ID
- Cálculo de siguiente número de factura
- Paso de proforma a issue
This commit is contained in:
David Arranz 2025-11-05 18:19:33 +01:00
parent 46e6b01a97
commit 78db3318fc
30 changed files with 448 additions and 254 deletions

View File

@ -1,7 +1,12 @@
import { Criteria } from "@repo/rdx-criteria/server"; import { Criteria } from "@repo/rdx-criteria/server";
import { UniqueID } from "@repo/rdx-ddd"; 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 { Transaction } from "sequelize";
import {
CustomerInvoiceNumber,
CustomerInvoiceSerie,
ICustomerInvoiceNumberGenerator,
} from "../domain";
import { import {
CustomerInvoice, CustomerInvoice,
CustomerInvoicePatchProps, CustomerInvoicePatchProps,
@ -11,17 +16,50 @@ import { ICustomerInvoiceRepository } from "../domain/repositories";
import { CustomerInvoiceListDTO } from "../infrastructure"; import { CustomerInvoiceListDTO } from "../infrastructure";
export class CustomerInvoiceApplicationService { 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 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 para facturas (issue)
* *
* @param companyId - Identificador de la empresa a la que pertenece la factura. * @param companyId - Identificador de la empresa a la que pertenece la factura.
* @param props - Las propiedades ya validadas para crear la factura. * @param series - Serie por la que buscar la última factura
* @param invoiceId - Identificador UUID de la factura (opcional). * @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. * @returns Result<CustomerInvoice, Error> - El agregado construido o un error si falla la creación.
*/ */
buildInvoiceInCompany( buildProformaInCompany(
companyId: UniqueID, companyId: UniqueID,
props: Omit<CustomerInvoiceProps, "companyId">, props: Omit<CustomerInvoiceProps, "companyId">,
invoiceId?: UniqueID invoiceId?: UniqueID
@ -29,6 +67,28 @@ export class CustomerInvoiceApplicationService {
return CustomerInvoice.create({ ...props, companyId }, invoiceId); return CustomerInvoice.create({ ...props, companyId }, invoiceId);
} }
/**
* Construye una factura issue a partir de una proforma.
*
* @param companyId - Identificador de la empresa a la que pertenece la factura.
* @param issueInvoiceId - Identificador UUID de la factura (opcional).
* @param proforma - La proforma de la cual se generará la issue
* @param pathcProps - otros props personalizados que se trasladarán a la issue
* @returns Result<CustomerInvoice, Error> - El agregado construido o un error si falla la creación.
*/
buildIssueInvoiceInCompany(
companyId: UniqueID,
proforma: CustomerInvoice,
patchProps: CustomerInvoicePatchProps
): Result<CustomerInvoice, Error> {
const proformaProps = proforma.getIssuedInvoiceProps();
return CustomerInvoice.create({
...proformaProps,
...patchProps,
companyId,
});
}
/** /**
* Guarda una nueva factura y devuelve la factura guardada. * Guarda una nueva factura y devuelve la factura guardada.
* *

View File

@ -47,7 +47,7 @@ export class CustomerInvoiceFullPresenter extends Presenter<
id: invoice.id.toString(), id: invoice.id.toString(),
company_id: invoice.companyId.toString(), company_id: invoice.companyId.toString(),
invoice_number: toEmptyString(invoice.invoiceNumber, (value) => value.toString()), invoice_number: invoice.invoiceNumber.toString(),
status: invoice.status.toPrimitive(), status: invoice.status.toPrimitive(),
series: toEmptyString(invoice.series, (value) => value.toString()), series: toEmptyString(invoice.series, (value) => value.toString()),

View File

@ -15,7 +15,7 @@ export class ListCustomerInvoicesPresenter extends Presenter {
is_proforma: invoice.isProforma, is_proforma: invoice.isProforma,
customer_id: invoice.customerId.toString(), customer_id: invoice.customerId.toString(),
invoice_number: toEmptyString(invoice.invoiceNumber, (value) => value.toString()), invoice_number: invoice.invoiceNumber.toString(),
status: invoice.status.toPrimitive(), status: invoice.status.toPrimitive(),
series: toEmptyString(invoice.series, (value) => value.toString()), series: toEmptyString(invoice.series, (value) => value.toString()),

View File

@ -1 +0,0 @@
export * from "./status-invoice_is_approved.spec";

View File

@ -1,7 +1,7 @@
import { JsonTaxCatalogProvider } from "@erp/core"; import { JsonTaxCatalogProvider } from "@erp/core";
import { DuplicateEntityError, IPresenterRegistry, ITransactionManager } from "@erp/core/api"; import { DuplicateEntityError, IPresenterRegistry, ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import { Transaction } from "sequelize"; import { Transaction } from "sequelize";
import { CreateCustomerInvoiceRequestDTO } from "../../../../common/dto"; import { CreateCustomerInvoiceRequestDTO } from "../../../../common/dto";
import { CustomerInvoiceApplicationService } from "../../customer-invoice-application.service"; import { CustomerInvoiceApplicationService } from "../../customer-invoice-application.service";
@ -21,7 +21,7 @@ export class CreateCustomerInvoiceUseCase {
private readonly taxCatalog: JsonTaxCatalogProvider private readonly taxCatalog: JsonTaxCatalogProvider
) {} ) {}
public execute(params: CreateCustomerInvoiceUseCaseInput) { public async execute(params: CreateCustomerInvoiceUseCaseInput) {
const { dto, companyId } = params; const { dto, companyId } = params;
const presenter = this.presenterRegistry.getPresenter({ const presenter = this.presenterRegistry.getPresenter({
resource: "customer-invoice", resource: "customer-invoice",
@ -37,17 +37,30 @@ export class CreateCustomerInvoiceUseCase {
const { props, id } = dtoResult.data; const { props, id } = dtoResult.data;
// 2) Construir entidad de dominio // 3) Ejecutar bajo transacción: verificar duplicado → persistir → ensamblar vista
const buildResult = this.service.buildInvoiceInCompany(companyId, props, id); 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) { if (buildResult.isFailure) {
return Result.fail(buildResult.error); return Result.fail(buildResult.error);
} }
const newInvoice = buildResult.data; const newInvoice = buildResult.data;
// 3) Ejecutar bajo transacción: verificar duplicado → persistir → ensamblar vista
return this.transactionManager.complete(async (transaction: Transaction) => {
try {
const existsGuard = await this.ensureNotExists(companyId, id, transaction); const existsGuard = await this.ensureNotExists(companyId, id, transaction);
if (existsGuard.isFailure) { if (existsGuard.isFailure) {
return Result.fail(existsGuard.error); return Result.fail(existsGuard.error);

View File

@ -1,15 +1,24 @@
import { EntityNotFoundError, ITransactionManager } from "@erp/core/api"; import { ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID, UtcDate } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import { CustomerInvoiceNumber } from "../../domain"; import { InvalidProformaStatusError } from "../../domain";
import { StatusInvoiceIsApprovedSpecification } from "../../domain/specs";
import { CustomerInvoiceApplicationService } from "../customer-invoice-application.service"; import { CustomerInvoiceApplicationService } from "../customer-invoice-application.service";
import { StatusInvoiceIsApprovedSpecification } from "../specs";
type IssueCustomerInvoiceUseCaseInput = { type IssueCustomerInvoiceUseCaseInput = {
companyId: UniqueID; 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 { export class IssueCustomerInvoiceUseCase {
constructor( constructor(
private readonly service: CustomerInvoiceApplicationService, private readonly service: CustomerInvoiceApplicationService,
@ -17,56 +26,78 @@ export class IssueCustomerInvoiceUseCase {
) {} ) {}
public execute(params: IssueCustomerInvoiceUseCaseInput) { 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) { if (idOrError.isFailure) {
return Result.fail(idOrError.error); return Result.fail(idOrError.error);
} }
const invoiceId = idOrError.data; const proformaId = idOrError.data;
return this.transactionManager.complete(async (transaction) => { return this.transactionManager.complete(async (transaction) => {
try { try {
const invoiceResult = await this.service.getInvoiceByIdInCompany( /** 1. Recuperamos la proforma */
const proformaResult = await this.service.getInvoiceByIdInCompany(
companyId, companyId,
invoiceId, proformaId,
transaction transaction
); );
if (invoiceResult.isFailure) { if (proformaResult.isFailure) {
return Result.fail(invoiceResult.error); return Result.fail(proformaResult.error);
} }
const invoiceProforma = invoiceResult.data; const proforma = proformaResult.data;
const isOk = new StatusInvoiceIsApprovedSpecification().isSatisfiedBy(invoiceProforma); /** 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()));
}
if (!isOk) { /** 3. Generar nueva factura */
return Result.fail( const nextNumberResult = await this.service.getNextIssueInvoiceNumber(
new EntityNotFoundError("Customer invoice", "id", invoiceId.toString()) companyId,
proforma.series,
transaction
); );
if (nextNumberResult.isFailure) {
return Result.fail(nextNumberResult.error);
} }
// La factura se puede emitir. const newIssueNumber = nextNumberResult.data;
// Pedir el número de factura
const newInvoiceNumber = CustomerInvoiceNumber.create("xxx/001").data;
// Asignamos el número de la factura // props base obtenidas del agregado proforma
const issuedInvoiceOrError = this.service.buildIssueInvoiceInCompany(companyId, proforma, {
invoiceNumber: Maybe.some(newIssueNumber),
invoiceDate: UtcDate.today(),
});
const issuedInvoiceResult = invoiceProforma.issueInvoice(newInvoiceNumber); if (issuedInvoiceOrError.isFailure) {
if (issuedInvoiceResult.isFailure) { return Result.fail(issuedInvoiceOrError.error);
return Result.fail( }
new EntityNotFoundError("Customer invoice", "id", issuedInvoiceResult.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
); );
}
const issuedInvoice = issuedInvoiceResult.data; /** 5. Resultado */
return Result.ok(issuedInvoice);
this.service.updateInvoiceInCompany(companyId, issuedInvoice, transaction);
//return await this.service.IssueInvoiceByIdInCompany(companyId, invoiceId, transaction);
} catch (error: unknown) { } catch (error: unknown) {
return Result.fail(error as Error); return Result.fail(error as Error);
} }

View File

@ -175,7 +175,7 @@
<td>{{description}}</td> <td>{{description}}</td>
<td class="text-right">{{#if quantity}}{{quantity}}{{else}}&nbsp;{{/if}}</td> <td class="text-right">{{#if quantity}}{{quantity}}{{else}}&nbsp;{{/if}}</td>
<td class="text-right">{{#if unit_amount}}{{unit_amount}}{{else}}&nbsp;{{/if}}</td> <td class="text-right">{{#if unit_amount}}{{unit_amount}}{{else}}&nbsp;{{/if}}</td>
<td class="text-right">{{#if subtotal_amount}}{{subtotal_amount}}{{else}}&nbsp;{{/if}}</td> <td class="text-right">{{#if total_amount}}{{total_amount}}{{else}}&nbsp;{{/if}}</td>
</td> </td>
</tr> </tr>
{{/each}} {{/each}}

View File

@ -24,8 +24,10 @@ export interface CustomerInvoiceProps {
isProforma: boolean; isProforma: boolean;
status: CustomerInvoiceStatus; status: CustomerInvoiceStatus;
proformaId: Maybe<UniqueID>;
series: Maybe<CustomerInvoiceSerie>; series: Maybe<CustomerInvoiceSerie>;
invoiceNumber: Maybe<CustomerInvoiceNumber>; invoiceNumber: CustomerInvoiceNumber;
invoiceDate: UtcDate; invoiceDate: UtcDate;
operationDate: Maybe<UtcDate>; operationDate: Maybe<UtcDate>;
@ -70,7 +72,9 @@ export interface ICustomerInvoice {
getTaxes(): InvoiceTaxTotal[]; getTaxes(): InvoiceTaxTotal[];
issueInvoice(newInvoiceNumber: CustomerInvoiceNumber): Result<CustomerInvoice, Error>; asIssued(): Result<CustomerInvoice, Error>;
getIssuedInvoiceProps(): CustomerInvoiceProps;
} }
export class CustomerInvoice export class CustomerInvoice
@ -143,6 +147,10 @@ export class CustomerInvoice
return this.props.isProforma; return this.props.isProforma;
} }
public get proformaId(): Maybe<UniqueID> {
return this.props.proformaId;
}
public get status(): CustomerInvoiceStatus { public get status(): CustomerInvoiceStatus {
return this.props.status; return this.props.status;
} }
@ -301,15 +309,21 @@ export class CustomerInvoice
}; };
} }
public issueInvoice(newInvoiceNumber: CustomerInvoiceNumber) { public asIssued(): Result<CustomerInvoice, Error> {
return CustomerInvoice.create( const newProps: CustomerInvoiceProps = {
{
...this.props, ...this.props,
status: CustomerInvoiceStatus.createIssued(), status: CustomerInvoiceStatus.createIssued(),
};
return CustomerInvoice.create(newProps, this.id);
}
public getIssuedInvoiceProps(): CustomerInvoiceProps {
return {
...this.props,
isProforma: false, isProforma: false,
invoiceNumber: Maybe.some(newInvoiceNumber), proformaId: Maybe.some(this.id),
}, status: CustomerInvoiceStatus.createIssued(),
this.id invoiceDate: UtcDate.today(),
); };
} }
} }

View File

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

View File

@ -1 +1,2 @@
export * from "./customer-invoice-id-already-exits-error"; export * from "./customer-invoice-id-already-exits-error";
export * from "./invalid-proforma-status-error";

View File

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

View File

@ -2,4 +2,6 @@ export * from "./aggregates";
export * from "./entities"; export * from "./entities";
export * from "./errors"; export * from "./errors";
export * from "./repositories"; export * from "./repositories";
export * from "./services";
export * from "./specs";
export * from "./value-objects"; export * from "./value-objects";

View File

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

View File

@ -0,0 +1 @@
export * from "./customer-invoice-number-generator.interface";

View File

@ -0,0 +1 @@
export * from "./status-invoice-is-approved.specification";

View File

@ -1,5 +1,5 @@
import { CompositeSpecification } from "@repo/rdx-ddd"; import { CompositeSpecification } from "@repo/rdx-ddd";
import { CustomerInvoice } from "../../domain"; import { CustomerInvoice } from "../aggregates";
export class StatusInvoiceIsApprovedSpecification extends CompositeSpecification<CustomerInvoice> { export class StatusInvoiceIsApprovedSpecification extends CompositeSpecification<CustomerInvoice> {
public async isSatisfiedBy(invoice: CustomerInvoice): Promise<boolean> { public async isSatisfiedBy(invoice: CustomerInvoice): Promise<boolean> {

View File

@ -7,7 +7,7 @@ interface ICustomerInvoiceNumberProps {
} }
export class CustomerInvoiceNumber extends ValueObject<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 FIELD = "invoiceNumber";
private static readonly ERROR_CODE = "INVALID_INVOICE_NUMBER"; private static readonly ERROR_CODE = "INVALID_INVOICE_NUMBER";

View File

@ -7,7 +7,7 @@ interface ICustomerInvoiceSerieProps {
} }
export class CustomerInvoiceSerie extends ValueObject<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 FIELD = "invoiceSeries";
private static readonly ERROR_CODE = "INVALID_INVOICE_SERIE"; private static readonly ERROR_CODE = "INVALID_INVOICE_SERIE";

View File

@ -11,8 +11,9 @@ export enum INVOICE_STATUS {
APPROVED = "approved", // <- Proforma APPROVED = "approved", // <- Proforma
REJECTED = "rejected", // <- Proforma REJECTED = "rejected", // <- Proforma
// issued <- (si is_proforma === true) => Es una proforma (histórica) // status === 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 === false) => Factura y enviará/enviada a Veri*Factu
ISSUED = "issued", ISSUED = "issued",
} }
export class CustomerInvoiceStatus extends ValueObject<ICustomerInvoiceStatusProps> { export class CustomerInvoiceStatus extends ValueObject<ICustomerInvoiceStatusProps> {

View File

@ -1,35 +1,32 @@
// modules/invoice/infrastructure/invoice-dependencies.factory.ts // modules/invoice/infrastructure/invoice-dependencies.factory.ts
import { JsonTaxCatalogProvider, SpainTaxCatalogProvider } from "@erp/core";
import type { IMapperRegistry, IPresenterRegistry, ModuleParams } from "@erp/core/api"; import type { IMapperRegistry, IPresenterRegistry, ModuleParams } from "@erp/core/api";
import { import {
InMemoryMapperRegistry, InMemoryMapperRegistry,
InMemoryPresenterRegistry, InMemoryPresenterRegistry,
SequelizeTransactionManager, SequelizeTransactionManager,
} from "@erp/core/api"; } from "@erp/core/api";
import { import {
CreateCustomerInvoiceUseCase, CreateCustomerInvoiceUseCase,
CustomerInvoiceApplicationService,
CustomerInvoiceFullPresenter, CustomerInvoiceFullPresenter,
CustomerInvoiceItemsFullPresenter, CustomerInvoiceItemsFullPresenter,
CustomerInvoiceItemsReportPersenter,
CustomerInvoiceReportHTMLPresenter, CustomerInvoiceReportHTMLPresenter,
CustomerInvoiceReportPDFPresenter, CustomerInvoiceReportPDFPresenter,
CustomerInvoiceReportPresenter, CustomerInvoiceReportPresenter,
GetCustomerInvoiceUseCase, GetCustomerInvoiceUseCase,
IssueCustomerInvoiceUseCase,
ListCustomerInvoicesPresenter, ListCustomerInvoicesPresenter,
ListCustomerInvoicesUseCase, ListCustomerInvoicesUseCase,
RecipientInvoiceFullPresenter, RecipientInvoiceFullPresenter,
ReportCustomerInvoiceUseCase, ReportCustomerInvoiceUseCase,
UpdateCustomerInvoiceUseCase, UpdateCustomerInvoiceUseCase,
} from "../application"; } from "../application";
import { JsonTaxCatalogProvider, SpainTaxCatalogProvider } from "@erp/core";
import {
CustomerInvoiceApplicationService,
CustomerInvoiceItemsReportPersenter,
} from "../application";
import { CustomerInvoiceDomainMapper, CustomerInvoiceListMapper } from "./mappers"; import { CustomerInvoiceDomainMapper, CustomerInvoiceListMapper } from "./mappers";
import { CustomerInvoiceRepository } from "./sequelize"; import { CustomerInvoiceRepository } from "./sequelize";
import { SequelizeInvoiceNumberGenerator } from "./services";
export type CustomerInvoiceDeps = { export type CustomerInvoiceDeps = {
transactionManager: SequelizeTransactionManager; transactionManager: SequelizeTransactionManager;
@ -47,6 +44,7 @@ export type CustomerInvoiceDeps = {
update: () => UpdateCustomerInvoiceUseCase; update: () => UpdateCustomerInvoiceUseCase;
//delete: () => DeleteCustomerInvoiceUseCase; //delete: () => DeleteCustomerInvoiceUseCase;
report: () => ReportCustomerInvoiceUseCase; report: () => ReportCustomerInvoiceUseCase;
issue: () => IssueCustomerInvoiceUseCase;
}; };
getService: (name: string) => any; getService: (name: string) => any;
listServices: () => string[]; listServices: () => string[];
@ -73,7 +71,8 @@ export function buildCustomerInvoiceDependencies(params: ModuleParams): Customer
// Repository & Services // Repository & Services
const repo = new CustomerInvoiceRepository({ mapperRegistry, database }); const repo = new CustomerInvoiceRepository({ mapperRegistry, database });
const service = new CustomerInvoiceApplicationService(repo); const numberGenerator = new SequelizeInvoiceNumberGenerator();
const service = new CustomerInvoiceApplicationService(repo, numberGenerator);
// Presenter Registry // Presenter Registry
const presenterRegistry = new InMemoryPresenterRegistry(); const presenterRegistry = new InMemoryPresenterRegistry();
@ -162,6 +161,7 @@ export function buildCustomerInvoiceDependencies(params: ModuleParams): Customer
// delete: () => new DeleteCustomerInvoiceUseCase(service, transactionManager), // delete: () => new DeleteCustomerInvoiceUseCase(service, transactionManager),
report: () => report: () =>
new ReportCustomerInvoiceUseCase(service, transactionManager, presenterRegistry), new ReportCustomerInvoiceUseCase(service, transactionManager, presenterRegistry),
issue: () => new IssueCustomerInvoiceUseCase(service, transactionManager),
}, },
listServices, listServices,
getService, getService,

View File

@ -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"; import { IssueCustomerInvoiceUseCase } from "../../../application";
export class IssueCustomerInvoiceController extends ExpressController { export class IssueCustomerInvoiceController extends ExpressController {
public constructor( public constructor(private readonly useCase: IssueCustomerInvoiceUseCase) {
private readonly useCase: IssueCustomerInvoiceUseCase
/* private readonly presenter: any */
) {
super(); super();
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId")); this.registerGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
} }
async executeImpl() { async executeImpl(): Promise<any> {
const tenantId = this.getTenantId()!; // garantizado por tenantGuard const companyId = this.getTenantId(); // garantizado por tenantGuard
const { id } = this.req.params; 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( return result.match(
(data) => this.ok(data), (data) => this.ok(data),

View File

@ -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 { UpdateCustomerInvoiceByIdRequestDTO } from "../../../../common/dto";
import { UpdateCustomerInvoiceUseCase } from "../../../application"; import { UpdateCustomerInvoiceUseCase } from "../../../application";
@ -14,10 +14,15 @@ export class UpdateCustomerInvoiceController extends ExpressController {
if (!companyId) { if (!companyId) {
return this.forbiddenError("Tenant ID not found"); 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 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( return result.match(
(data) => this.ok(data), (data) => this.ok(data),

View File

@ -19,6 +19,7 @@ import {
ReportCustomerInvoiceController, ReportCustomerInvoiceController,
UpdateCustomerInvoiceController, UpdateCustomerInvoiceController,
} from "./controllers"; } from "./controllers";
import { IssueCustomerInvoiceController } from "./controllers/issue-customer-invoice.controller";
export const customerInvoicesRouter = (params: ModuleParams) => { export const customerInvoicesRouter = (params: ModuleParams) => {
const { app, baseRoutePath, logger } = params as { const { app, baseRoutePath, logger } = params as {
@ -84,7 +85,7 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
); );
router.put( router.put(
"/:invoice_id", "/:proforma_id",
//checkTabContext, //checkTabContext,
validateRequest(UpdateCustomerInvoiceByIdParamsRequestSchema, "params"), validateRequest(UpdateCustomerInvoiceByIdParamsRequestSchema, "params"),
@ -117,18 +118,21 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
const controller = new ReportCustomerInvoiceController(useCase); const controller = new ReportCustomerInvoiceController(useCase);
return controller.execute(req, res, next); return controller.execute(req, res, next);
} }
); /*router.put( );
"/:invoice_id/issue",
router.put(
"/:proforma_id/issue",
//checkTabContext, //checkTabContext,
validateRequest(XXX, "params"),
validateRequest(XXX, "body"), /*validateRequest(XXX, "params"),
validateRequest(XXX, "body"),*/
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.build.issue(); const useCase = deps.build.issue();
const controller = new IssueCustomerInvoiceController(useCase); const controller = new IssueCustomerInvoiceController(useCase);
return controller.execute(req, res, next); return controller.execute(req, res, next);
} }
); );
*/
app.use(`${baseRoutePath}/customer-invoices`, router); app.use(`${baseRoutePath}/customer-invoices`, router);
}; };

View File

@ -70,6 +70,12 @@ export class CustomerInvoiceDomainMapper
const isProforma = Boolean(source.is_proforma); const isProforma = Boolean(source.is_proforma);
const proformaId = extractOrPushError(
maybeFromNullableVO(source.proforma_id, (v) => UniqueID.create(v)),
"proforma_id",
errors
);
const status = extractOrPushError( const status = extractOrPushError(
CustomerInvoiceStatus.create(source.status), CustomerInvoiceStatus.create(source.status),
"status", "status",
@ -83,7 +89,7 @@ export class CustomerInvoiceDomainMapper
); );
const invoiceNumber = extractOrPushError( const invoiceNumber = extractOrPushError(
maybeFromNullableVO(source.invoice_number, (v) => CustomerInvoiceNumber.create(v)), CustomerInvoiceNumber.create(source.invoice_number),
"invoice_number", "invoice_number",
errors errors
); );
@ -172,6 +178,7 @@ export class CustomerInvoiceDomainMapper
companyId, companyId,
customerId, customerId,
isProforma, isProforma,
proformaId,
status, status,
series, series,
invoiceNumber, invoiceNumber,
@ -204,13 +211,13 @@ export class CustomerInvoiceDomainMapper
...params, ...params,
}); });
if (recipientResult.isFailure) { /*if (recipientResult.isFailure) {
errors.push({ errors.push({
path: "recipient", path: "recipient",
message: recipientResult.error.message, message: recipientResult.error.message,
}); });
} }*/
// 3) Items (colección) // 3) Items (colección)
const itemsResults = this._itemsMapper.mapToDomainCollection( const itemsResults = this._itemsMapper.mapToDomainCollection(
@ -223,12 +230,12 @@ export class CustomerInvoiceDomainMapper
} }
); );
if (itemsResults.isFailure) { /*if (itemsResults.isFailure) {
errors.push({ errors.push({
path: "items", path: "items",
message: itemsResults.error.message, message: itemsResults.error.message,
}); });
} }*/
// Nota: los impuestos a nivel factura (tabla customer_invoice_taxes) se derivan de los items. // 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. // El agregado expone un getter `taxes` (derivado). No se incluye en las props.
@ -254,6 +261,7 @@ export class CustomerInvoiceDomainMapper
companyId: attributes.companyId!, companyId: attributes.companyId!,
isProforma: attributes.isProforma, isProforma: attributes.isProforma,
proformaId: attributes.proformaId!,
status: attributes.status!, status: attributes.status!,
series: attributes.series!, series: attributes.series!,
invoiceNumber: attributes.invoiceNumber!, invoiceNumber: attributes.invoiceNumber!,
@ -333,7 +341,7 @@ export class CustomerInvoiceDomainMapper
const allAmounts = source.getAllAmounts(); const allAmounts = source.getAllAmounts();
// 4) Cliente // 4) Cliente
const recipient = this._mapRecipientToPersistence(source, { const recipient = this._recipientMapper.mapToPersistence(source.recipient, {
errors, errors,
parent: source, parent: source,
...params, ...params,
@ -346,16 +354,17 @@ export class CustomerInvoiceDomainMapper
); );
} }
const invoiceValues: CustomerInvoiceCreationAttributes = { const invoiceValues: Partial<CustomerInvoiceCreationAttributes> = {
// Identificación // Identificación
id: source.id.toPrimitive(), id: source.id.toPrimitive(),
company_id: source.companyId.toPrimitive(), company_id: source.companyId.toPrimitive(),
// Flags / estado / serie / número // Flags / estado / serie / número
is_proforma: source.isProforma, is_proforma: source.isProforma,
proforma_id: toNullable(source.proformaId, (v) => v.toPrimitive()),
status: source.status.toPrimitive(), status: source.status.toPrimitive(),
series: toNullable(source.series, (v) => v.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(), invoice_date: source.invoiceDate.toPrimitive(),
operation_date: toNullable(source.operationDate, (v) => v.toPrimitive()), operation_date: toNullable(source.operationDate, (v) => v.toPrimitive()),
@ -397,47 +406,8 @@ export class CustomerInvoiceDomainMapper
items, items,
}; };
return Result.ok<CustomerInvoiceCreationAttributes>(invoiceValues); return Result.ok<CustomerInvoiceCreationAttributes>(
} invoiceValues as CustomerInvoiceCreationAttributes
);
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;
} }
} }

View File

@ -1,20 +1,20 @@
import { MapperParamsType } from "@erp/core/api";
import { import {
City, City,
Country, Country,
extractOrPushError,
maybeFromNullableVO,
Name, Name,
PostalCode, PostalCode,
Province, Province,
Street, Street,
TINNumber, TINNumber,
toNullable,
ValidationErrorCollection, ValidationErrorCollection,
ValidationErrorDetail, ValidationErrorDetail,
extractOrPushError,
maybeFromNullableVO,
} from "@repo/rdx-ddd"; } from "@repo/rdx-ddd";
import { MapperParamsType } from "@erp/core/api";
import { Maybe, Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import { CustomerInvoiceProps, InvoiceRecipient } from "../../../domain"; import { CustomerInvoice, CustomerInvoiceProps, InvoiceRecipient } from "../../../domain";
import { CustomerInvoiceModel } from "../../sequelize"; import { CustomerInvoiceModel } from "../../sequelize";
export class InvoiceRecipientDomainMapper { export class InvoiceRecipientDomainMapper {
@ -114,4 +114,64 @@ export class InvoiceRecipientDomainMapper {
return Result.ok(Maybe.some(createResult.data)); 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()),
};
}
} }

View File

@ -27,7 +27,7 @@ export type CustomerInvoiceListDTO = {
companyId: UniqueID; companyId: UniqueID;
isProforma: boolean; isProforma: boolean;
invoiceNumber: Maybe<CustomerInvoiceNumber>; invoiceNumber: CustomerInvoiceNumber;
status: CustomerInvoiceStatus; status: CustomerInvoiceStatus;
series: Maybe<CustomerInvoiceSerie>; series: Maybe<CustomerInvoiceSerie>;
@ -152,7 +152,7 @@ export class CustomerInvoiceListMapper
); );
const invoiceNumber = extractOrPushError( const invoiceNumber = extractOrPushError(
maybeFromNullableVO(raw.invoice_number, (value) => CustomerInvoiceNumber.create(value)), CustomerInvoiceNumber.create(raw.invoice_number),
"invoice_number", "invoice_number",
errors errors
); );

View File

@ -45,7 +45,7 @@ export class CustomerInvoiceModel extends Model<
declare proforma_id: CreationOptional<string | null>; // ID de la proforma origen de la factura declare proforma_id: CreationOptional<string | null>; // ID de la proforma origen de la factura
declare series: CreationOptional<string | null>; declare series: CreationOptional<string | null>;
declare invoice_number: CreationOptional<string | null>; declare invoice_number: CreationOptional<string>;
declare invoice_date: CreationOptional<string>; declare invoice_date: CreationOptional<string>;
declare operation_date: CreationOptional<string | null>; declare operation_date: CreationOptional<string | null>;
declare language_code: CreationOptional<string>; declare language_code: CreationOptional<string>;
@ -183,7 +183,7 @@ export default (database: Sequelize) => {
proforma_id: { proforma_id: {
type: DataTypes.UUID, type: DataTypes.UUID,
allowNull: true, allowNull: true,
defaultValue: true, defaultValue: null,
}, },
status: { status: {
@ -193,13 +193,13 @@ export default (database: Sequelize) => {
}, },
series: { series: {
type: new DataTypes.STRING(), type: new DataTypes.STRING(10),
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,
}, },
invoice_number: { invoice_number: {
type: new DataTypes.STRING(), type: new DataTypes.STRING(12),
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,
}, },
@ -404,10 +404,22 @@ export default (database: Sequelize) => {
name: "idx_invoices_company_date", name: "idx_invoices_company_date",
fields: ["company_id", "deleted_at", { name: "invoice_date", order: "DESC" }], 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_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_invoice_company_id", fields: ["id", "company_id"], unique: true }, // <- para consulta get
{ name: "idx_factuges", fields: ["factuges_id"], unique: false }, // <- para el proceso python
{ 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", name: "ft_customer_invoice",
type: "FULLTEXT", type: "FULLTEXT",

View File

@ -0,0 +1 @@
export * from "./sequelize-invoice-number-generator";

View File

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

View File

@ -4,25 +4,25 @@ import {
Country, Country,
CurrencyCode, CurrencyCode,
EmailAddress, EmailAddress,
extractOrPushError,
LanguageCode, LanguageCode,
maybeFromNullableVO,
Name, Name,
PhoneNumber, PhoneNumber,
PostalAddress, PostalAddress,
PostalCode, PostalCode,
Province, Province,
Street, Street,
TINNumber,
TaxCode, TaxCode,
TextValue, TextValue,
URLAddress, TINNumber,
toNullable,
UniqueID, UniqueID,
URLAddress,
ValidationErrorCollection, ValidationErrorCollection,
ValidationErrorDetail, ValidationErrorDetail,
extractOrPushError,
maybeFromNullableVO,
toNullable,
} from "@repo/rdx-ddd"; } 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 { Customer, CustomerProps, CustomerStatus } from "../../../domain";
import { CustomerCreationAttributes, CustomerModel } from "../../sequelize"; import { CustomerCreationAttributes, CustomerModel } from "../../sequelize";