This commit is contained in:
David Arranz 2026-03-28 22:10:05 +01:00
parent 3a61d726f8
commit 5d59598106
105 changed files with 1134 additions and 1263 deletions

View File

@ -5,6 +5,7 @@
"cweijan.vscode-mysql-client2", "cweijan.vscode-mysql-client2",
"ms-vscode.vscode-json", "ms-vscode.vscode-json",
"formulahendry.auto-rename-tag", "formulahendry.auto-rename-tag",
"cweijan.dbclient-jdbc" "cweijan.dbclient-jdbc",
"pkief.material-icon-theme"
] ]
} }

33
.vscode/settings.json vendored
View File

@ -1,20 +1,19 @@
{ {
"files.associations": { // Font Family
"tsconfig.json": "jsonc", "editor.fontFamily": "'Fira Code', 'Cascadia Code', 'Consolas'",
"typescript-config/*.json": "jsonc",
"*.css": "tailwindcss" // Enable Font Ligatures
}, "editor.fontLigatures": true,
// Javascript and TypeScript settings // Javascript and TypeScript settings
"javascript.suggest.enabled": true, "js/ts.suggest.enabled": true,
"javascript.suggest.autoImports": true, "js/ts.suggest.autoImports": true,
"javascript.preferences.importModuleSpecifier": "shortest", "js/ts.preferences.importModuleSpecifier": "shortest",
"typescript.suggest.enabled": true, "js/ts.suggest.completeFunctionCalls": true,
"typescript.suggest.completeFunctionCalls": true, "js/ts.suggest.includeAutomaticOptionalChainCompletions": true,
"typescript.suggest.includeAutomaticOptionalChainCompletions": true, "js/ts.suggestionActions.enabled": true,
"typescript.suggestionActions.enabled": true, "js/ts.autoClosingTags.enabled": true,
"typescript.autoClosingTags": true,
"editor.quickSuggestions": { "editor.quickSuggestions": {
"strings": "on" "strings": "on"
@ -54,11 +53,5 @@
// other vscode settings // other vscode settings
"[sql]": { "[sql]": {
"editor.defaultFormatter": "cweijan.vscode-mysql-client2" "editor.defaultFormatter": "cweijan.vscode-mysql-client2"
}, // <- your root font size here } // <- your root font size here
"invisibleAiChartDetector.watermark.includeSpaceFamily": true,
"invisibleAiChartDetector.watermark.includeUnicodeCf": true,
"invisibleAiChartDetector.doubleBlankThreshold": 2,
"invisibleAiChartDetector.replace.format": "unicode",
"invisibleAiChartDetector.clean.replaceSpaceLikesToAscii": true
} }

View File

@ -154,7 +154,12 @@ async function setupModule(name: string, params: ModuleParams, stack: string[])
function makeGetService(moduleName: string, pkg: IModuleServer) { function makeGetService(moduleName: string, pkg: IModuleServer) {
return <T>(serviceName: string): T => { return <T>(serviceName: string): T => {
const [serviceModule] = serviceName.split(":"); const [serviceModule] = serviceName.split(":");
// No registrar dependencias para modulos
// que usan sus propios servicios.
if (serviceModule !== "self") {
trackDependencyUse(moduleName, serviceModule); trackDependencyUse(moduleName, serviceModule);
}
// IMPORTANTE: devolver el valor // IMPORTANTE: devolver el valor
return getServiceScoped<T>(moduleName, pkg.dependencies ?? [], serviceName); return getServiceScoped<T>(moduleName, pkg.dependencies ?? [], serviceName);
@ -213,6 +218,8 @@ function validateModuleDependencies() {
const declared = new Set(pkg.dependencies ?? []); const declared = new Set(pkg.dependencies ?? []);
const used = usedDependenciesByModule.get(moduleName) ?? new Set<string>(); const used = usedDependenciesByModule.get(moduleName) ?? new Set<string>();
console.log(declared, used);
// ❌ usadas pero no declaradas // ❌ usadas pero no declaradas
const undeclaredUsed = [...used].filter((d) => !declared.has(d)); const undeclaredUsed = [...used].filter((d) => !declared.has(d));

View File

@ -12,15 +12,25 @@ export function registerService(name: string, api: any) {
/** /**
* Recupera un servicio registrado bajo un "scope". * Recupera un servicio registrado bajo un "scope".
*
* getService("customers:repository") * getService("customers:repository")
* Debe declarar: dependencies: ["customers"] * Debe declarar: dependencies: ["customers"]
*
* El "scope" puede ser "self" para recuperar
* los servicios propios registrados.
*
* getService("self:repository")
*/ */
export function getServiceScoped<T = any>( export function getServiceScoped<T = any>(
requesterModule: string, requesterModule: string,
allowedDeps: readonly string[], allowedDeps: readonly string[],
name: string name: string
): T { ): T {
const [serviceModule] = name.split(":"); const [serviceModule, ...key] = name.split(":");
if (serviceModule === "self") {
return getService<T>(`${requesterModule}:${key.join(":")}`);
}
if (!allowedDeps.includes(serviceModule)) { if (!allowedDeps.includes(serviceModule)) {
throw new Error( throw new Error(

View File

@ -1,50 +0,0 @@
/**
*
* @param obj - El objeto a evaluar.
* @template T - El tipo del objeto.
* @description Verifica si un objeto no tiene campos con valor undefined.
*
* Esta función recorre los valores del objeto y devuelve true si todos los valores son diferentes de undefined.
* Si al menos un valor es undefined, devuelve false.
*
* @example
* const obj = { a: 1, b: 'test', c: null };
* console.log(hasNoUndefinedFields(obj)); // true
*
* const objWithUndefined = { a: 1, b: undefined, c: null };
* console.log(hasNoUndefinedFields(objWithUndefined)); // false
*
* @template T - El tipo del objeto.
* @param obj - El objeto a evaluar.
* @returns true si el objeto no tiene campos undefined, false en caso contrario.
*/
export function hasNoUndefinedFields<T extends Record<string, unknown>>(
obj: T
): obj is { [K in keyof T]-?: Exclude<T[K], undefined> } {
return Object.values(obj).every((value) => value !== undefined);
}
/**
*
* @description Verifica si un objeto tiene campos con valor undefined.
* Esta función es el complemento de `hasNoUndefinedFields`.
*
* @example
* const obj = { a: 1, b: 'test', c: null };
* console.log(hasUndefinedFields(obj)); // false
*
* const objWithUndefined = { a: 1, b: undefined, c: null };
* console.log(hasUndefinedFields(objWithUndefined)); // true
*
* @template T - El tipo del objeto.
* @param obj - El objeto a evaluar.
* @returns true si el objeto tiene al menos un campo undefined, false en caso contrario.
*
*/
export function hasUndefinedFields<T extends Record<string, unknown>>(
obj: T
): obj is { [K in keyof T]-?: Exclude<T[K], undefined> } {
return !hasNoUndefinedFields(obj);
}

View File

@ -1,85 +0,0 @@
import {
ValidationErrorCollection,
type ValidationErrorDetail,
extractOrPushError,
maybeFromNullableResult,
} from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { CreateCustomerInvoiceRequestDTO } from "../../../common";
import {
IssuedInvoiceItem,
type IssuedInvoiceItemProps,
ItemAmount,
ItemDescription,
ItemDiscountPercentage,
ItemQuantity,
} from "../../domain";
import { hasNoUndefinedFields } from "./has-no-undefined-fields";
export function mapDTOToCustomerInvoiceItemsProps(
dtoItems: Pick<CreateCustomerInvoiceRequestDTO, "items">["items"]
): Result<IssuedInvoiceItem[], ValidationErrorCollection> {
const errors: ValidationErrorDetail[] = [];
const items: IssuedInvoiceItem[] = [];
dtoItems.forEach((item, index) => {
const path = (field: string) => `items[${index}].${field}`;
const description = extractOrPushError(
maybeFromNullableResult(item.description, (value) => ItemDescription.create(value)),
path("description"),
errors
);
const quantity = extractOrPushError(
maybeFromNullableResult(item.quantity, (value) => ItemQuantity.create({ value })),
path("quantity"),
errors
);
const unitAmount = extractOrPushError(
maybeFromNullableResult(item.unit_amount, (value) => ItemAmount.create({ value })),
path("unit_amount"),
errors
);
const discountPercentage = extractOrPushError(
maybeFromNullableResult(item.discount_percentage, (value) =>
ItemDiscountPercentage.create({ value })
),
path("discount_percentage"),
errors
);
if (errors.length === 0) {
const itemProps: IssuedInvoiceItemProps = {
description: description,
quantity: quantity,
unitAmount: unitAmount,
itemDiscountPercentage: discountPercentage,
//currencyCode,
//languageCode,
//taxes:
};
if (hasNoUndefinedFields(itemProps)) {
// Validar y crear el item de factura
const itemOrError = IssuedInvoiceItem.create(itemProps);
if (itemOrError.isSuccess) {
items.push(itemOrError.data);
} else {
errors.push({ path: `items[${index}]`, message: itemOrError.error.message });
}
}
}
if (errors.length > 0) {
return Result.fail(new ValidationErrorCollection("Invoice items dto mapping failed", errors));
}
});
return Result.ok(items);
}

View File

@ -1,98 +0,0 @@
import {
CurrencyCode,
UniqueID,
UtcDate,
ValidationErrorCollection,
type ValidationErrorDetail,
extractOrPushError,
maybeFromNullableResult,
} from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { CreateCustomerInvoiceRequestDTO } from "../../../common";
import {
type IProformaCreateProps,
InvoiceNumber,
InvoiceSerie,
InvoiceStatus,
} from "../../domain";
import { mapDTOToCustomerInvoiceItemsProps } from "./map-dto-to-customer-invoice-items-props";
/**
* Convierte el DTO a las props validadas (CustomerInvoiceProps).
* No construye directamente el agregado.
*
* @param dto - DTO con los datos de la factura de cliente
* @returns
*
*/
export function mapDTOToCustomerInvoiceProps(dto: CreateCustomerInvoiceRequestDTO) {
const errors: ValidationErrorDetail[] = [];
const invoiceId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
const invoiceNumber = extractOrPushError(
maybeFromNullableResult(dto.invoice_number, (value) => InvoiceNumber.create(value)),
"invoice_number",
errors
);
const invoiceSeries = extractOrPushError(
maybeFromNullableResult(dto.series, (value) => InvoiceSerie.create(value)),
"invoice_series",
errors
);
const invoiceDate = extractOrPushError(
UtcDate.createFromISO(dto.invoice_date),
"invoice_date",
errors
);
const operationDate = extractOrPushError(
maybeFromNullableResult(dto.operation_date, (value) => UtcDate.createFromISO(value)),
"operation_date",
errors
);
const currencyCode = extractOrPushError(
CurrencyCode.create(dto.currency_code),
"currency",
errors
);
// 🔄 Validar y construir los items de factura con helper especializado
const itemsResult = mapDTOToCustomerInvoiceItemsProps(dto.items);
if (itemsResult.isFailure) {
return Result.fail(itemsResult.error);
}
if (errors.length > 0) {
return Result.fail(new ValidationErrorCollection("Invoice dto mapping failed", errors));
}
const invoiceProps: IProformaCreateProps = {
invoiceNumber: invoiceNumber!,
series: invoiceSeries!,
invoiceDate: invoiceDate!,
operationDate: operationDate!,
status: InvoiceStatus.fromDraft(),
currencyCode: currencyCode!,
};
return Result.ok({ id: invoiceId!, props: invoiceProps });
/*if (hasNoUndefinedFields(invoiceProps)) {
const invoiceOrError = CustomerInvoice.create(invoiceProps, invoiceId);
if (invoiceOrError.isFailure) {
return Result.fail(invoiceOrError.error);
}
return Result.ok(invoiceOrError.data);
}
return Result.fail(
new ValidationErrorCollection([
{ path: "", message: "Error building from DTO: Some fields are undefined" },
])
);*/
}

View File

@ -1,3 +1,5 @@
export * from "./issued-invoice-creator.di";
export * from "./issued-invoice-finder.di"; export * from "./issued-invoice-finder.di";
export * from "./issued-invoice-snapshot-builders.di"; export * from "./issued-invoice-snapshot-builders.di";
export * from "./issued-invoice-use-cases.di"; export * from "./issued-invoice-use-cases.di";
export * from "./proforma-to-issued-invoice-props-converter.di";

View File

@ -0,0 +1,18 @@
import type { IIssuedInvoiceRepository } from "../repositories";
import {
type IIssuedInvoiceCreator,
type IIssuedInvoiceNumberGenerator,
IssuedInvoiceCreator,
} from "../services";
export function buildIssuedInvoiceCreator(params: {
numberService: IIssuedInvoiceNumberGenerator;
repository: IIssuedInvoiceRepository;
}): IIssuedInvoiceCreator {
const { numberService, repository } = params;
return new IssuedInvoiceCreator({
repository,
numberService,
});
}

View File

@ -0,0 +1,8 @@
import {
type IProformaToIssuedInvoiceConverter,
ProformaToIssuedInvoiceConverter,
} from "../services";
export function buildProformaToIssuedInvoicePropsConverter(): IProformaToIssuedInvoiceConverter {
return new ProformaToIssuedInvoiceConverter();
}

View File

@ -1,5 +1,8 @@
export * from "./issued-invoice-creator";
export * from "./issued-invoice-document-generator.interface"; export * from "./issued-invoice-document-generator.interface";
export * from "./issued-invoice-document-metadata-factory"; export * from "./issued-invoice-document-metadata-factory";
export * from "./issued-invoice-document-properties-factory"; export * from "./issued-invoice-document-properties-factory";
export * from "./issued-invoice-finder"; export * from "./issued-invoice-finder";
export * from "./proforma-to-issued-invoice-materializer"; export * from "./issued-invoice-number-generator.interface";
export * from "./issued-invoice-public-services.interface";
export * from "./proforma-to-issued-invoice-props-converter";

View File

@ -0,0 +1,64 @@
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { type IIssuedInvoiceCreateProps, IssuedInvoice } from "../../../domain";
import type { IIssuedInvoiceRepository } from "../repositories";
import type { IIssuedInvoiceNumberGenerator } from "./issued-invoice-number-generator.interface";
export interface IIssuedInvoiceCreatorParams {
companyId: UniqueID;
id: UniqueID;
props: Omit<IIssuedInvoiceCreateProps, "invoiceNumber">;
transaction: unknown;
}
export type IIssuedInvoiceCreator = {
create(params: IIssuedInvoiceCreatorParams): Promise<Result<IssuedInvoice, Error>>;
};
type IssuedInvoiceCreatorDeps = {
numberService: IIssuedInvoiceNumberGenerator;
repository: IIssuedInvoiceRepository;
};
export class IssuedInvoiceCreator implements IIssuedInvoiceCreator {
private readonly numberService: IIssuedInvoiceNumberGenerator;
private readonly repository: IIssuedInvoiceRepository;
constructor(deps: IssuedInvoiceCreatorDeps) {
this.numberService = deps.numberService;
this.repository = deps.repository;
}
async create(params: IIssuedInvoiceCreatorParams): Promise<Result<IssuedInvoice, Error>> {
const { companyId, id, props, transaction } = params;
// 1. Obtener siguiente número
const { series } = props;
const numberResult = await this.numberService.getNextForCompany(companyId, series, transaction);
if (numberResult.isFailure) {
return Result.fail(numberResult.error);
}
const invoiceNumber = numberResult.data;
const invoiceResult = IssuedInvoice.create({ ...props, invoiceNumber, companyId }, id);
if (invoiceResult.isFailure) {
return Result.fail(invoiceResult.error);
}
const invoice = invoiceResult.data;
// 3. Persistir
const saveResult = await this.repository.create(invoice, transaction);
if (saveResult.isFailure) {
return Result.fail(saveResult.error);
}
return Result.ok(invoice);
}
}

View File

@ -1,6 +1,6 @@
import type { DocumentGenerationService } from "@erp/core/api"; import type { DocumentGenerationService } from "@erp/core/api";
import type { IssuedInvoiceReportSnapshot } from "../application-models"; import type { IIssuedInvoiceReportSnapshot } from "../snapshot-builders/report";
export interface IssuedInvoiceDocumentGeneratorService export interface IssuedInvoiceDocumentGeneratorService
extends DocumentGenerationService<IssuedInvoiceReportSnapshot> {} extends DocumentGenerationService<IIssuedInvoiceReportSnapshot> {}

View File

@ -1,6 +1,6 @@
import type { IDocumentMetadata, IDocumentMetadataFactory } from "@erp/core/api"; import type { IDocumentMetadata, IDocumentMetadataFactory } from "@erp/core/api";
import type { IssuedInvoiceReportSnapshot } from "../application-models"; import type { IIssuedInvoiceReportSnapshot } from "../snapshot-builders";
/** /**
* Construye los metadatos del documento PDF de una factura emitida. * Construye los metadatos del documento PDF de una factura emitida.
@ -10,9 +10,9 @@ import type { IssuedInvoiceReportSnapshot } from "../application-models";
* - Sin IO * - Sin IO
*/ */
export class IssuedInvoiceDocumentMetadataFactory export class IssuedInvoiceDocumentMetadataFactory
implements IDocumentMetadataFactory<IssuedInvoiceReportSnapshot> implements IDocumentMetadataFactory<IIssuedInvoiceReportSnapshot>
{ {
build(snapshot: IssuedInvoiceReportSnapshot): IDocumentMetadata { build(snapshot: IIssuedInvoiceReportSnapshot): IDocumentMetadata {
if (!snapshot.id) { if (!snapshot.id) {
throw new Error("IssuedInvoiceReportSnapshot.id is required"); throw new Error("IssuedInvoiceReportSnapshot.id is required");
} }
@ -33,12 +33,12 @@ export class IssuedInvoiceDocumentMetadataFactory
}; };
} }
private buildFilename(snapshot: IssuedInvoiceReportSnapshot): string { private buildFilename(snapshot: IIssuedInvoiceReportSnapshot): string {
// Ejemplo: factura-F2024-000123-FULANITO.pdf // Ejemplo: factura-F2024-000123-FULANITO.pdf
return `factura-${snapshot.series}${snapshot.invoice_number}-${snapshot.recipient.name}.pdf`; return `factura-${snapshot.series}${snapshot.invoice_number}-${snapshot.recipient.name}.pdf`;
} }
private buildCacheKey(snapshot: IssuedInvoiceReportSnapshot): string { private buildCacheKey(snapshot: IIssuedInvoiceReportSnapshot): string {
// Versionado explícito para invalidaciones futuras // Versionado explícito para invalidaciones futuras
return [ return [
"issued-invoice", "issued-invoice",

View File

@ -1,7 +1,6 @@
import type { Criteria } from "@repo/rdx-criteria/server"; import type { Criteria } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import type { Collection, Result } from "@repo/rdx-utils"; import type { Collection, Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type { IssuedInvoice } from "../../../domain"; import type { IssuedInvoice } from "../../../domain";
import type { IssuedInvoiceSummary } from "../models"; import type { IssuedInvoiceSummary } from "../models";
@ -11,19 +10,19 @@ export interface IIssuedInvoiceFinder {
findIssuedInvoiceById( findIssuedInvoiceById(
companyId: UniqueID, companyId: UniqueID,
invoiceId: UniqueID, invoiceId: UniqueID,
transaction?: Transaction transaction?: unknown
): Promise<Result<IssuedInvoice, Error>>; ): Promise<Result<IssuedInvoice, Error>>;
issuedInvoiceExists( issuedInvoiceExists(
companyId: UniqueID, companyId: UniqueID,
invoiceId: UniqueID, invoiceId: UniqueID,
transaction?: Transaction transaction?: unknown
): Promise<Result<boolean, Error>>; ): Promise<Result<boolean, Error>>;
findIssuedInvoicesByCriteria( findIssuedInvoicesByCriteria(
companyId: UniqueID, companyId: UniqueID,
criteria: Criteria, criteria: Criteria,
transaction?: Transaction transaction?: unknown
): Promise<Result<Collection<IssuedInvoiceSummary>, Error>>; ): Promise<Result<Collection<IssuedInvoiceSummary>, Error>>;
} }
@ -33,7 +32,7 @@ export class IssuedInvoiceFinder implements IIssuedInvoiceFinder {
async findIssuedInvoiceById( async findIssuedInvoiceById(
companyId: UniqueID, companyId: UniqueID,
invoiceId: UniqueID, invoiceId: UniqueID,
transaction?: Transaction transaction?: unknown
): Promise<Result<IssuedInvoice, Error>> { ): Promise<Result<IssuedInvoice, Error>> {
return this.repository.getByIdInCompany(companyId, invoiceId, transaction); return this.repository.getByIdInCompany(companyId, invoiceId, transaction);
} }
@ -41,7 +40,7 @@ export class IssuedInvoiceFinder implements IIssuedInvoiceFinder {
async issuedInvoiceExists( async issuedInvoiceExists(
companyId: UniqueID, companyId: UniqueID,
invoiceId: UniqueID, invoiceId: UniqueID,
transaction?: Transaction transaction?: unknown
): Promise<Result<boolean, Error>> { ): Promise<Result<boolean, Error>> {
return this.repository.existsByIdInCompany(companyId, invoiceId, transaction); return this.repository.existsByIdInCompany(companyId, invoiceId, transaction);
} }
@ -49,7 +48,7 @@ export class IssuedInvoiceFinder implements IIssuedInvoiceFinder {
async findIssuedInvoicesByCriteria( async findIssuedInvoicesByCriteria(
companyId: UniqueID, companyId: UniqueID,
criteria: Criteria, criteria: Criteria,
transaction?: Transaction transaction?: unknown
): Promise<Result<Collection<IssuedInvoiceSummary>, Error>> { ): Promise<Result<Collection<IssuedInvoiceSummary>, Error>> {
return this.repository.findByCriteriaInCompany(companyId, criteria, transaction); return this.repository.findByCriteriaInCompany(companyId, criteria, transaction);
} }

View File

@ -0,0 +1,19 @@
import type { UniqueID } from "@repo/rdx-ddd";
import type { Maybe, Result } from "@repo/rdx-utils";
import type { InvoiceNumber, InvoiceSerie } from "../../../domain";
export interface IIssuedInvoiceNumberGenerator {
/**
* 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
*/
getNextForCompany(
companyId: UniqueID,
series: Maybe<InvoiceSerie>,
transaction: unknown
): Promise<Result<InvoiceNumber, Error>>;
}

View File

@ -1,27 +0,0 @@
import type { UniqueID } from "@repo/rdx-ddd";
import type { Maybe, Result } from "@repo/rdx-utils";
import type { ICustomerInvoiceNumberGenerator, InvoiceNumber, InvoiceSerie } from "../../../domain";
export interface IIssuedInvoiceNumberService {
/**
* Devuelve el siguiente número disponible para una factura emitida.
*/
nextIssuedInvoiceNumber(
companyId: UniqueID,
series: Maybe<InvoiceSerie>,
transaction: unknown
): Promise<Result<InvoiceNumber, Error>>;
}
export class IssuedInvoiceNumberService implements IIssuedInvoiceNumberService {
constructor(private readonly numberGenerator: ICustomerInvoiceNumberGenerator) {}
async nextIssuedInvoiceNumber(
companyId: UniqueID,
series: Maybe<InvoiceSerie>,
transaction: unknown
): Promise<Result<InvoiceNumber, Error>> {
return this.numberGenerator.nextForCompany(companyId, series, transaction);
}
}

View File

@ -0,0 +1,19 @@
import type { UniqueID } from "@repo/rdx-ddd";
import type { Result } from "@repo/rdx-utils";
import type { IIssuedInvoiceCreatorParams } from ".";
import type { IssuedInvoice } from "../../../domain";
export interface IIssuedInvoiceServicesContext {
transaction: unknown;
companyId: UniqueID;
}
export interface IIssuedInvoicePublicServices {
createIssuedInvoice: (
id: UniqueID,
props: IIssuedInvoiceCreatorParams["props"],
context: IIssuedInvoiceServicesContext
) => Promise<Result<IssuedInvoice, Error>>;
}

View File

@ -1,32 +0,0 @@
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type { CustomerInvoice } from "../../../domain/aggregates";
import type { ICustomerInvoiceRepository } from "../../../domain/repositories";
export type IIssuedInvoiceWriteService = {};
export class IssuedInvoiceWriteService implements IIssuedInvoiceWriteService {
constructor(private readonly repository: ICustomerInvoiceRepository) {}
/**
* Emite (crea) una factura definitiva a partir de una factura ya preparada.
*
* Asume que:
* - el número ya ha sido asignado
* - el estado es correcto
*/
async createIssuedInvoice(
companyId: UniqueID,
invoice: CustomerInvoice,
transaction: Transaction
): Promise<Result<CustomerInvoice, Error>> {
const result = await this.repository.create(invoice, transaction);
if (result.isFailure) {
return Result.fail(result.error);
}
return result;
}
}

View File

@ -1,56 +0,0 @@
import type { UniqueID } from "@repo/rdx-ddd";
import { Collection, Result } from "@repo/rdx-utils";
import type { IIssuedInvoiceProps, Proforma } from "../../../domain";
export interface IProformaToIssuedInvoiceMaterializer {
materialize(proforma: Proforma, issuedInvoiceId: UniqueID): Result<IIssuedInvoiceProps, Error>;
}
export class ProformaToIssuedInvoiceMaterializer implements IProformaToIssuedInvoiceMaterializer {
public materialize(
proforma: Proforma,
issuedInvoiceId: UniqueID
): Result<IIssuedInvoiceProps, Error> {
const amounts = proforma.calculateAllAmounts();
const taxGroups = proforma.getTaxes();
const issuedItems = proforma.items.map((item) => ({
description: item.description,
quantity: item.quantity,
unitPrice: item.unitAmount,
taxableAmount: item.getTaxableAmount(),
taxesAmount: item.getTaxesAmount(),
totalAmount: item.getTotalAmount(),
}));
const issuedTaxes = taxGroups.map((group) => ({
ivaCode: group.iva.code,
ivaPercentage: group.iva.percentage,
ivaAmount: group.calculateAmounts().ivaAmount,
recCode: group.rec?.code,
recPercentage: group.rec?.percentage,
recAmount: group.calculateAmounts().recAmount,
retentionCode: group.retention?.code,
retentionPercentage: group.retention?.percentage,
retentionAmount: group.calculateAmounts().retentionAmount,
}));
return Result.ok({
companyId: proforma.companyId,
invoiceNumber: proforma.invoiceNumber,
invoiceDate: proforma.invoiceDate,
customerId: proforma.customerId,
languageCode: proforma.languageCode,
currencyCode: proforma.currencyCode,
paymentMethod: proforma.paymentMethod,
discountPercentage: proforma.globalDiscountPercentage,
items: new Collection(issuedItems),
taxes: new Collection(issuedTaxes),
subtotalAmount: amounts.subtotalAmount,
taxableAmount: amounts.taxableAmount,
totalAmount: amounts.totalAmount,
});
}
}

View File

@ -0,0 +1,149 @@
import { Maybe, Result } from "@repo/rdx-utils";
import {
type IIssuedInvoiceCreateProps,
InvoiceStatus,
IssuedInvoiceItem,
IssuedInvoiceTax,
IssuedInvoiceTaxes,
type Proforma,
} from "../../../domain";
export interface IProformaToIssuedInvoiceConverter {
toCreateProps(proforma: Proforma): Result<IIssuedInvoiceCreateProps, Error>;
}
export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoiceConverter {
public toCreateProps(proforma: Proforma): Result<IIssuedInvoiceCreateProps, Error> {
const proformaTotals = proforma.totals();
const taxTotals = proforma.taxes();
const issuedItems: IssuedInvoiceItem[] = [];
for (const item of proforma.items.getAll()) {
const itemTotals = item.totals();
const itemOrResult = IssuedInvoiceItem.create({
description: item.description,
quantity: item.quantity,
unitAmount: item.unitAmount,
currencyCode: proforma.currencyCode,
languageCode: proforma.languageCode,
itemDiscountPercentage: item.itemDiscountPercentage,
itemDiscountAmount: itemTotals.itemDiscountAmount,
globalDiscountPercentage: item.globalDiscountPercentage,
globalDiscountAmount: itemTotals.globalDiscountAmount,
totalDiscountAmount: itemTotals.totalDiscountAmount,
ivaCode: item.ivaCode(),
ivaPercentage: item.ivaPercentage(),
ivaAmount: itemTotals.ivaAmount,
recCode: item.recCode(),
recPercentage: item.recPercentage(),
recAmount: itemTotals.recAmount,
retentionCode: item.retentionCode(),
retentionPercentage: item.retentionPercentage(),
retentionAmount: itemTotals.recAmount,
subtotalAmount: itemTotals.subtotalAmount,
taxableAmount: itemTotals.taxableAmount,
taxesAmount: itemTotals.taxesAmount,
totalAmount: itemTotals.totalAmount,
});
if (itemOrResult.isFailure) {
return Result.fail(itemOrResult.error);
}
issuedItems.push(itemOrResult.data);
}
const issuedTaxes: IssuedInvoiceTax[] = [];
for (const tax of taxTotals.getAll()) {
if (tax.ivaCode.isNone()) {
return Result.fail(new Error("IVA code is required"));
}
const taxOrResult = IssuedInvoiceTax.create({
taxableAmount: tax.taxableAmount,
ivaCode: tax.ivaCode.unwrap(),
ivaPercentage: tax.ivaPercentage.unwrap(),
ivaAmount: tax.ivaAmount,
recCode: tax.recCode,
recPercentage: tax.recPercentage,
recAmount: tax.recAmount,
retentionCode: tax.retentionCode,
retentionAmount: tax.retentionAmount,
retentionPercentage: tax.retentionPercentage,
taxesAmount: tax.taxesAmount,
});
if (taxOrResult.isFailure) {
return Result.fail(taxOrResult.error);
}
issuedTaxes.push(taxOrResult.data);
}
const issuedInvoiceProps: IIssuedInvoiceCreateProps = {
companyId: proforma.companyId,
status: InvoiceStatus.issued(),
series: proforma.series,
proformaId: proforma.id,
invoiceNumber: proforma.invoiceNumber,
invoiceDate: proforma.invoiceDate,
operationDate: proforma.operationDate,
description: proforma.description,
languageCode: proforma.languageCode,
currencyCode: proforma.currencyCode,
notes: proforma.notes,
reference: proforma.reference,
paymentMethod: proforma.paymentMethod,
customerId: proforma.customerId,
recipient: proforma.recipient,
items: issuedItems,
taxes: IssuedInvoiceTaxes.create({
currencyCode: proforma.currencyCode,
languageCode: proforma.languageCode,
taxes: issuedTaxes,
}),
subtotalAmount: proformaTotals.subtotalAmount,
itemsDiscountAmount: proformaTotals.itemsDiscountAmount,
globalDiscountPercentage: proforma.globalDiscountPercentage,
globalDiscountAmount: proformaTotals.globalDiscountAmount,
totalDiscountAmount: proformaTotals.totalDiscountAmount,
taxableAmount: proformaTotals.taxableAmount,
ivaAmount: proformaTotals.ivaAmount,
recAmount: proformaTotals.recAmount,
retentionAmount: proformaTotals.retentionAmount,
taxesAmount: proformaTotals.taxesAmount,
totalAmount: proformaTotals.totalAmount,
verifactu: Maybe.none(),
};
return Result.ok(issuedInvoiceProps);
}
}

View File

@ -1,2 +1,3 @@
export * from "./full"; export * from "./full";
export * from "./report";
export * from "./summary"; export * from "./summary";

View File

@ -27,10 +27,10 @@ export class IssuedInvoiceReportItemSnapshotBuilder
quantity: QuantityDTOHelper.format(item.quantity, locale, { minimumFractionDigits: 0 }), quantity: QuantityDTOHelper.format(item.quantity, locale, { minimumFractionDigits: 0 }),
unit_amount: MoneyDTOHelper.format(item.unit_amount, locale, moneyOptions), unit_amount: MoneyDTOHelper.format(item.unit_amount, locale, moneyOptions),
subtotal_amount: MoneyDTOHelper.format(item.subtotal_amount, locale, moneyOptions), subtotal_amount: MoneyDTOHelper.format(item.subtotal_amount, locale, moneyOptions),
discount_percentage: PercentageDTOHelper.format(item.discount_percentage, locale, { discount_percentage: PercentageDTOHelper.format(item.item_discount_percentage, locale, {
minimumFractionDigits: 0, minimumFractionDigits: 0,
}), }),
discount_amount: MoneyDTOHelper.format(item.discount_amount, locale, moneyOptions), discount_amount: MoneyDTOHelper.format(item.item_discount_amount, locale, moneyOptions),
taxable_amount: MoneyDTOHelper.format(item.taxable_amount, locale, moneyOptions), taxable_amount: MoneyDTOHelper.format(item.taxable_amount, locale, moneyOptions),
taxes_amount: MoneyDTOHelper.format(item.taxes_amount, locale, moneyOptions), taxes_amount: MoneyDTOHelper.format(item.taxes_amount, locale, moneyOptions),
total_amount: MoneyDTOHelper.format(item.total_amount, locale, moneyOptions), total_amount: MoneyDTOHelper.format(item.total_amount, locale, moneyOptions),

View File

@ -2,7 +2,6 @@ import type { ITransactionManager } from "@erp/core/api";
import type { Criteria } from "@repo/rdx-criteria/server"; import type { Criteria } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type { IIssuedInvoiceFinder } from "../services"; import type { IIssuedInvoiceFinder } from "../services";
import type { IIssuedInvoiceSummarySnapshotBuilder } from "../snapshot-builders"; import type { IIssuedInvoiceSummarySnapshotBuilder } from "../snapshot-builders";
@ -22,7 +21,7 @@ export class ListIssuedInvoicesUseCase {
public execute(params: ListIssuedInvoicesUseCaseInput) { public execute(params: ListIssuedInvoicesUseCaseInput) {
const { criteria, companyId } = params; const { criteria, companyId } = params;
return this.transactionManager.complete(async (transaction: Transaction) => { return this.transactionManager.complete(async (transaction: unknown) => {
try { try {
const result = await this.finder.findIssuedInvoicesByCriteria( const result = await this.finder.findIssuedInvoicesByCriteria(
companyId, companyId,

View File

@ -1,5 +1,6 @@
export * from "./proforma-creator.di"; export * from "./proforma-creator.di";
export * from "./proforma-finder.di"; export * from "./proforma-finder.di";
export * from "./proforma-input-mappers.di"; export * from "./proforma-input-mappers.di";
export * from "./proforma-issuer.di";
export * from "./proforma-snapshot-builders.di"; export * from "./proforma-snapshot-builders.di";
export * from "./proforma-use-cases.di"; export * from "./proforma-use-cases.di";

View File

@ -0,0 +1,15 @@
import type { IProformaToIssuedInvoiceConverter } from "../../issued-invoices";
import type { IProformaRepository } from "../repositories";
import { type IProformaIssuer, ProformaIssuer } from "../services";
export const buildProformaIssuer = (params: {
proformaConverter: IProformaToIssuedInvoiceConverter;
repository: IProformaRepository;
}): IProformaIssuer => {
const { proformaConverter, repository } = params;
return new ProformaIssuer({
proformaConverter,
repository,
});
};

View File

@ -1,9 +1,11 @@
import type { ITransactionManager } from "@erp/core/api"; import type { ITransactionManager } from "@erp/core/api";
import type { IIssuedInvoicePublicServices } from "../../issued-invoices";
import type { ICreateProformaInputMapper } from "../mappers"; import type { ICreateProformaInputMapper } from "../mappers";
import type { import type {
IProformaCreator, IProformaCreator,
IProformaFinder, IProformaFinder,
IProformaIssuer,
ProformaDocumentGeneratorService, ProformaDocumentGeneratorService,
} from "../services"; } from "../services";
import type { import type {
@ -11,9 +13,13 @@ import type {
IProformaSummarySnapshotBuilder, IProformaSummarySnapshotBuilder,
} from "../snapshot-builders"; } from "../snapshot-builders";
import type { IProformaFullSnapshotBuilder } from "../snapshot-builders/full"; import type { IProformaFullSnapshotBuilder } from "../snapshot-builders/full";
import { GetProformaByIdUseCase, ListProformasUseCase, ReportProformaUseCase } from "../use-cases"; import {
import { CreateProformaUseCase } from "../use-cases/create-proforma"; CreateProformaUseCase,
import { IssueProformaUseCase } from "../use-cases/issue-proforma.use-case"; GetProformaByIdUseCase,
IssueProformaUseCase,
ListProformasUseCase,
ReportProformaUseCase,
} from "../use-cases";
export function buildGetProformaByIdUseCase(deps: { export function buildGetProformaByIdUseCase(deps: {
finder: IProformaFinder; finder: IProformaFinder;
@ -65,8 +71,26 @@ export function buildCreateProformaUseCase(deps: {
}); });
} }
export function buildIssueProformaUseCase(deps: { finder: IProformaFinder }) { export function buildIssueProformaUseCase(deps: {
return new IssueProformaUseCase(deps.finder); publicServices: {
issuedInvoiceServices: IIssuedInvoicePublicServices;
};
finder: IProformaFinder;
issuer: IProformaIssuer;
transactionManager: ITransactionManager;
}) {
const {
finder,
issuer,
transactionManager,
publicServices: { issuedInvoiceServices },
} = deps;
return new IssueProformaUseCase({
issuedInvoiceServices,
finder,
issuer,
transactionManager,
});
} }
/*export function buildUpdateProformaUseCase(deps: { /*export function buildUpdateProformaUseCase(deps: {

View File

@ -69,7 +69,7 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
const { companyId } = params; const { companyId } = params;
try { try {
const defaultStatus = InvoiceStatus.fromDraft(); const defaultStatus = InvoiceStatus.draft();
const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", errors); const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", errors);

View File

@ -5,3 +5,4 @@ export * from "./proforma-document-properties-factory";
export * from "./proforma-finder"; export * from "./proforma-finder";
export * from "./proforma-issuer"; export * from "./proforma-issuer";
export * from "./proforma-number-generator.interface"; export * from "./proforma-number-generator.interface";
export * from "./proforma-public-services.interface";

View File

@ -1,6 +1,5 @@
import type { UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import { type IProformaCreateProps, Proforma } from "../../../domain"; import { type IProformaCreateProps, Proforma } from "../../../domain";
import type { IProformaRepository } from "../repositories"; import type { IProformaRepository } from "../repositories";
@ -11,7 +10,7 @@ export interface IProformaCreatorParams {
companyId: UniqueID; companyId: UniqueID;
id: UniqueID; id: UniqueID;
props: Omit<IProformaCreateProps, "invoiceNumber">; props: Omit<IProformaCreateProps, "invoiceNumber">;
transaction: Transaction; transaction: unknown;
} }
export interface IProformaCreator { export interface IProformaCreator {
@ -32,12 +31,7 @@ export class ProformaCreator implements IProformaCreator {
this.repository = deps.repository; this.repository = deps.repository;
} }
async create(params: { async create(params: IProformaCreatorParams): Promise<Result<Proforma, Error>> {
companyId: UniqueID;
id: UniqueID;
props: IProformaCreateProps;
transaction: Transaction;
}): Promise<Result<Proforma, Error>> {
const { companyId, id, props, transaction } = params; const { companyId, id, props, transaction } = params;
// 1. Obtener siguiente número // 1. Obtener siguiente número

View File

@ -1,7 +1,6 @@
import type { Criteria } from "@repo/rdx-criteria/server"; import type { Criteria } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import type { Collection, Result } from "@repo/rdx-utils"; import type { Collection, Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type { Proforma } from "../../../domain"; import type { Proforma } from "../../../domain";
import type { ProformaSummary } from "../models"; import type { ProformaSummary } from "../models";
@ -11,19 +10,19 @@ export interface IProformaFinder {
findProformaById( findProformaById(
companyId: UniqueID, companyId: UniqueID,
invoiceId: UniqueID, invoiceId: UniqueID,
transaction?: Transaction transaction?: unknown
): Promise<Result<Proforma, Error>>; ): Promise<Result<Proforma, Error>>;
proformaExists( proformaExists(
companyId: UniqueID, companyId: UniqueID,
invoiceId: UniqueID, invoiceId: UniqueID,
transaction?: Transaction transaction?: unknown
): Promise<Result<boolean, Error>>; ): Promise<Result<boolean, Error>>;
findProformasByCriteria( findProformasByCriteria(
companyId: UniqueID, companyId: UniqueID,
criteria: Criteria, criteria: Criteria,
transaction?: Transaction transaction?: unknown
): Promise<Result<Collection<ProformaSummary>, Error>>; ): Promise<Result<Collection<ProformaSummary>, Error>>;
} }
@ -33,7 +32,7 @@ export class ProformaFinder implements IProformaFinder {
async findProformaById( async findProformaById(
companyId: UniqueID, companyId: UniqueID,
proformaId: UniqueID, proformaId: UniqueID,
transaction?: Transaction transaction?: unknown
): Promise<Result<Proforma, Error>> { ): Promise<Result<Proforma, Error>> {
return this.repository.getByIdInCompany(companyId, proformaId, transaction); return this.repository.getByIdInCompany(companyId, proformaId, transaction);
} }
@ -41,7 +40,7 @@ export class ProformaFinder implements IProformaFinder {
async proformaExists( async proformaExists(
companyId: UniqueID, companyId: UniqueID,
proformaId: UniqueID, proformaId: UniqueID,
transaction?: Transaction transaction?: unknown
): Promise<Result<boolean, Error>> { ): Promise<Result<boolean, Error>> {
return this.repository.existsByIdInCompany(companyId, proformaId, transaction); return this.repository.existsByIdInCompany(companyId, proformaId, transaction);
} }
@ -49,7 +48,7 @@ export class ProformaFinder implements IProformaFinder {
async findProformasByCriteria( async findProformasByCriteria(
companyId: UniqueID, companyId: UniqueID,
criteria: Criteria, criteria: Criteria,
transaction?: Transaction transaction?: unknown
): Promise<Result<Collection<ProformaSummary>, Error>> { ): Promise<Result<Collection<ProformaSummary>, Error>> {
return this.repository.findByCriteriaInCompany(companyId, criteria, transaction); return this.repository.findByCriteriaInCompany(companyId, criteria, transaction);
} }

View File

@ -1,44 +1,65 @@
import type { UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type { IProformaToIssuedInvoiceMaterializer } from "../../issued-invoices"; import type { IIssuedInvoiceCreateProps, Proforma } from "../../../domain";
import type { IProformaToIssuedInvoiceConverter } from "../../issued-invoices";
import type { IProformaRepository } from "../repositories";
export interface IProformaIssuerParams {
companyId: UniqueID;
proforma: Proforma;
issuedInvoiceId: UniqueID;
transaction: unknown;
}
export interface IProformaIssuer {
issueProforma(params: IProformaIssuerParams): Promise<Result<IIssuedInvoiceCreateProps, Error>>;
}
type ProformaIssuerDeps = {
proformaConverter: IProformaToIssuedInvoiceConverter;
repository: IProformaRepository;
};
export class ProformaIssuer implements IProformaIssuer { export class ProformaIssuer implements IProformaIssuer {
private readonly proformaRepository: IProformaRepository; private readonly proformaConverter: IProformaToIssuedInvoiceConverter;
private readonly issuedInvoiceFactory: IIssuedInvoiceFactory; private readonly repository: IProformaRepository;
private readonly issuedInvoiceRepository: IIssuedInvoiceRepository;
private readonly materializer: IProformaToIssuedInvoiceMaterializer;
constructor(deps: ProformaIssuerDeps) { constructor(deps: ProformaIssuerDeps) {
this.proformaRepository = deps.proformaRepository; this.proformaConverter = deps.proformaConverter;
this.issuedInvoiceFactory = deps.issuedInvoiceFactory; this.repository = deps.repository;
this.issuedInvoiceRepository = deps.issuedInvoiceRepository;
this.materializer = deps.materializer;
} }
public async issue( public async issueProforma(
proforma: Proforma, params: IProformaIssuerParams
issuedInvoiceId: UniqueID, ): Promise<Result<IIssuedInvoiceCreateProps, Error>> {
transaction: Transaction const { proforma, companyId, transaction } = params;
): Promise<Result<Proforma, Error>> {
// Cambiamos el estado de la proforma a 'issued'
const issueResult = proforma.issue(); const issueResult = proforma.issue();
if (issueResult.isFailure) return Result.fail(issueResult.error); if (issueResult.isFailure) {
return Result.fail(issueResult.error);
const propsResult = this.materializer.materialize(proforma, issuedInvoiceId);
if (propsResult.isFailure) return Result.fail(propsResult.error);
const invoiceResult = this.issuedInvoiceFactory.create(propsResult.data, issuedInvoiceId);
if (invoiceResult.isFailure) {
return Result.fail(invoiceResult.error);
} }
await this.issuedInvoiceRepository.save(proforma.companyId, invoiceResult.data, transaction); // Persistir
const updateStatusResult = await this.repository.updateStatusByIdInCompany(
companyId,
proforma.id,
proforma.status,
transaction
);
await this.proformaRepository.save(proforma.companyId, proforma, transaction); if (updateStatusResult.isFailure) {
return Result.fail(updateStatusResult.error);
}
return Result.ok(proforma); // Generamos las propiedades de la factura a partir de la proforma
const propsResult = this.proformaConverter.toCreateProps(proforma);
if (propsResult.isFailure) {
return Result.fail(propsResult.error);
}
return Result.ok(propsResult.data);
} }
} }

View File

@ -7,10 +7,10 @@ import type { Maybe, Result } from "@repo/rdx-utils";
*/ */
export interface IProformaNumberGenerator { export interface IProformaNumberGenerator {
/** /**
* Devuelve el siguiente número de factura disponible para una empresa dentro de una "serie" de factura. * Devuelve el siguiente número de proforma disponible para una empresa dentro de una "serie" de proforma.
* *
* @param companyId - Identificador de la empresa * @param companyId - Identificador de la empresa
* @param serie - Serie por la que buscar la última factura * @param serie - Serie por la que buscar la última proforma
* @param transaction - Transacción activa * @param transaction - Transacción activa
*/ */
getNextForCompany( getNextForCompany(

View File

@ -0,0 +1,34 @@
import type { UniqueID } from "@repo/rdx-ddd";
import type { Result } from "@repo/rdx-utils";
import type { Proforma } from "../../../domain/proformas";
import type { IProformaFullSnapshot } from "../snapshot-builders";
import type { IProformaCreatorParams } from "./proforma-creator";
export interface IProformaServicesContext {
transaction: unknown;
companyId: UniqueID;
}
export interface IProformaPublicServices {
createProforma: (
id: UniqueID,
props: IProformaCreatorParams["props"],
context: IProformaServicesContext
) => Promise<Result<Proforma, Error>>;
listProformas: (filters: unknown, context: unknown) => null;
getProformaById: (
id: UniqueID,
context: IProformaServicesContext
) => Promise<Result<Proforma, Error>>;
getProformaSnapshotById: (
id: UniqueID,
context: IProformaServicesContext
) => Promise<Result<IProformaFullSnapshot, Error>>;
generateProformaReport: (id: unknown, options: unknown, context: unknown) => null;
}

View File

@ -1,134 +0,0 @@
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import {
CustomerInvoiceIsProformaSpecification,
type InvoiceStatus,
ProformaCannotBeDeletedError,
StatusInvoiceIsDraftSpecification,
} from "../../../domain";
import type {
CustomerInvoice,
CustomerInvoicePatchProps,
CustomerInvoiceProps,
} from "../../../domain/aggregates";
import type { ICustomerInvoiceRepository } from "../../../domain/repositories";
import type { IProformaFactory } from "../../services/proforma-factory";
export type IIssuedInvoiceWriteService = {};
export class IssuedInvoiceWriteService implements IIssuedInvoiceWriteService {
constructor(
private readonly repository: ICustomerInvoiceRepository,
private readonly proformaFactory: IProformaFactory
) {}
/**
* Crea y persiste una nueva proforma para una empresa.
*/
async createProforma(
companyId: UniqueID,
props: Omit<CustomerInvoiceProps, "companyId">,
transaction: Transaction
): Promise<Result<CustomerInvoice, Error>> {
const invoiceResult = this.proformaFactory.createProforma(companyId, props);
if (invoiceResult.isFailure) {
return Result.fail(invoiceResult.error);
}
return this.repository.create(invoiceResult.data, transaction);
}
/**
* Actualiza una proforma existente.
*/
async updateProforma(
companyId: UniqueID,
proforma: CustomerInvoice,
transaction: Transaction
): Promise<Result<CustomerInvoice, Error>> {
return this.repository.update(proforma, transaction);
}
/**
* Aplica cambios parciales a una proforma existente.
* No persiste automáticamente.
*/
async patchProforma(
companyId: UniqueID,
proformaId: UniqueID,
changes: CustomerInvoicePatchProps,
transaction: Transaction
): Promise<Result<CustomerInvoice, Error>> {
const proformaResult = await this.repository.getProformaByIdInCompany(
companyId,
proformaId,
transaction,
{}
);
if (proformaResult.isFailure) {
return Result.fail(proformaResult.error);
}
const updated = proformaResult.data.update(changes);
if (updated.isFailure) {
return Result.fail(updated.error);
}
return Result.ok(updated.data);
}
/**
* Elimina (baja lógica) una proforma.
*/
async deleteProforma(
companyId: UniqueID,
proformaId: UniqueID,
transaction: Transaction
): Promise<Result<boolean, Error>> {
const proformaResult = await this.repository.getProformaByIdInCompany(
companyId,
proformaId,
transaction,
{}
);
if (proformaResult.isFailure) {
return Result.fail(proformaResult.error);
}
const proforma = proformaResult.data;
const isProforma = new CustomerInvoiceIsProformaSpecification();
if (!(await isProforma.isSatisfiedBy(proforma))) {
return Result.fail(new ProformaCannotBeDeletedError(proformaId.toString(), "not a proforma"));
}
const isDraft = new StatusInvoiceIsDraftSpecification();
if (!(await isDraft.isSatisfiedBy(proforma))) {
return Result.fail(
new ProformaCannotBeDeletedError(proformaId.toString(), "status is not 'draft'")
);
}
return this.repository.deleteProformaByIdInCompany(companyId, proformaId, transaction);
}
/**
* Actualiza el estado de una proforma.
*/
async updateProformaStatus(
companyId: UniqueID,
proformaId: UniqueID,
newStatus: InvoiceStatus,
transaction?: Transaction
): Promise<Result<boolean, Error>> {
return this.repository.updateProformaStatusByIdInCompany(
companyId,
proformaId,
newStatus,
transaction
);
}
}

View File

@ -9,8 +9,8 @@ export interface IProformaItemFullSnapshot {
subtotal_amount: { value: string; scale: string; currency_code: string }; subtotal_amount: { value: string; scale: string; currency_code: string };
discount_percentage: { value: string; scale: string }; item_discount_percentage: { value: string; scale: string };
discount_amount: { value: string; scale: string; currency_code: string }; item_discount_amount: { value: string; scale: string; currency_code: string };
global_discount_percentage: { value: string; scale: string }; global_discount_percentage: { value: string; scale: string };
global_discount_amount: { value: string; scale: string; currency_code: string }; global_discount_amount: { value: string; scale: string; currency_code: string };

View File

@ -32,8 +32,10 @@ export class ProformaItemsFullSnapshotBuilder implements IProformaItemsFullSnaps
? allAmounts.subtotalAmount.toObjectString() ? allAmounts.subtotalAmount.toObjectString()
: ItemAmount.EMPTY_MONEY_OBJECT, : ItemAmount.EMPTY_MONEY_OBJECT,
discount_percentage: maybeToEmptyPercentageObjectString(proformaItem.itemDiscountPercentage), item_discount_percentage: maybeToEmptyPercentageObjectString(
discount_amount: isValued proformaItem.itemDiscountPercentage
),
item_discount_amount: isValued
? allAmounts.itemDiscountAmount.toObjectString() ? allAmounts.itemDiscountAmount.toObjectString()
: ItemAmount.EMPTY_MONEY_OBJECT, : ItemAmount.EMPTY_MONEY_OBJECT,

View File

@ -1,14 +1,16 @@
import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/core"; import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/core";
import type { ISnapshotBuilder, ISnapshotBuilderParams } from "@erp/core/api"; import type { ISnapshotBuilder, ISnapshotBuilderParams } from "@erp/core/api";
import type { ProformaFullSnapshot, ProformaReportItemSnapshot } from "../../application-models"; import type { IProformaFullSnapshot } from "../full";
import type { ProformaReportItemSnapshot } from "./proforma-report-item-snapshot.interface";
export interface IProformaItemReportSnapshotBuilder export interface IProformaItemReportSnapshotBuilder
extends ISnapshotBuilder<ProformaFullSnapshot["items"], ProformaReportItemSnapshot[]> {} extends ISnapshotBuilder<IProformaFullSnapshot["items"], ProformaReportItemSnapshot[]> {}
export class ProformaItemReportSnapshotBuilder implements IProformaItemReportSnapshotBuilder { export class ProformaItemReportSnapshotBuilder implements IProformaItemReportSnapshotBuilder {
toOutput( toOutput(
items: ProformaFullSnapshot["items"], items: IProformaFullSnapshot["items"],
params?: ISnapshotBuilderParams params?: ISnapshotBuilderParams
): ProformaReportItemSnapshot[] { ): ProformaReportItemSnapshot[] {
const locale = params?.locale as string; const locale = params?.locale as string;

View File

@ -1,30 +1,29 @@
import { DateHelper, MoneyDTOHelper, PercentageDTOHelper } from "@erp/core"; import { DateHelper, MoneyDTOHelper, PercentageDTOHelper } from "@erp/core";
import type { ISnapshotBuilder, ISnapshotBuilderParams } from "@erp/core/api"; import type { ISnapshotBuilder, ISnapshotBuilderParams } from "@erp/core/api";
import type { import type { IProformaFullSnapshot } from "../full";
ProformaFullSnapshot,
ProformaReportItemSnapshot, import type { ProformaReportItemSnapshot } from "./proforma-report-item-snapshot.interface";
ProformaReportSnapshot, import type { ProformaReportSnapshot } from "./proforma-report-snapshot.interface";
ProformaReportTaxSnapshot, import type { ProformaReportTaxSnapshot } from "./proforma-report-tax-snapshot.interface";
} from "../../application-models";
export interface IProformaReportSnapshotBuilder export interface IProformaReportSnapshotBuilder
extends ISnapshotBuilder<ProformaFullSnapshot, ProformaReportSnapshot> {} extends ISnapshotBuilder<IProformaFullSnapshot, ProformaReportSnapshot> {}
export class ProformaReportSnapshotBuilder implements IProformaReportSnapshotBuilder { export class ProformaReportSnapshotBuilder implements IProformaReportSnapshotBuilder {
constructor( constructor(
private readonly itemsBuilder: ISnapshotBuilder< private readonly itemsBuilder: ISnapshotBuilder<
ProformaFullSnapshot["items"], IProformaFullSnapshot["items"],
ProformaReportItemSnapshot[] ProformaReportItemSnapshot[]
>, >,
private readonly taxesBuilder: ISnapshotBuilder< private readonly taxesBuilder: ISnapshotBuilder<
ProformaFullSnapshot["taxes"], IProformaFullSnapshot["taxes"],
ProformaReportTaxSnapshot[] ProformaReportTaxSnapshot[]
> >
) {} ) {}
toOutput( toOutput(
snapshot: ProformaFullSnapshot, snapshot: IProformaFullSnapshot,
params?: ISnapshotBuilderParams params?: ISnapshotBuilderParams
): ProformaReportSnapshot { ): ProformaReportSnapshot {
const locale = params?.locale as string; const locale = params?.locale as string;
@ -61,17 +60,37 @@ export class ProformaReportSnapshotBuilder implements IProformaReportSnapshotBui
taxes: this.taxesBuilder.toOutput(snapshot.taxes, { locale }), taxes: this.taxesBuilder.toOutput(snapshot.taxes, { locale }),
subtotal_amount: MoneyDTOHelper.format(snapshot.subtotal_amount, locale, moneyOptions), subtotal_amount: MoneyDTOHelper.format(snapshot.subtotal_amount, locale, moneyOptions),
discount_percentage: PercentageDTOHelper.format(snapshot.discount_percentage, locale, {
items_discount_amount: MoneyDTOHelper.format(
snapshot.items_discount_amount,
locale,
moneyOptions
),
global_discount_percentage: PercentageDTOHelper.format(
snapshot.global_discount_percentage,
locale,
{
hideZeros: true, hideZeros: true,
}), }
discount_amount: MoneyDTOHelper.format(snapshot.discount_amount, locale, moneyOptions), ),
global_discount_amount: MoneyDTOHelper.format(
snapshot.global_discount_amount,
locale,
moneyOptions
),
total_discount_amount: MoneyDTOHelper.format(
snapshot.total_discount_amount,
locale,
moneyOptions
),
taxable_amount: MoneyDTOHelper.format(snapshot.taxable_amount, locale, moneyOptions), taxable_amount: MoneyDTOHelper.format(snapshot.taxable_amount, locale, moneyOptions),
taxes_amount: MoneyDTOHelper.format(snapshot.taxes_amount, locale, moneyOptions), taxes_amount: MoneyDTOHelper.format(snapshot.taxes_amount, locale, moneyOptions),
total_amount: MoneyDTOHelper.format(snapshot.total_amount, locale, moneyOptions), total_amount: MoneyDTOHelper.format(snapshot.total_amount, locale, moneyOptions),
}; };
} }
private formatAddress(recipient: ProformaFullSnapshot["recipient"]): string { private formatAddress(recipient: IProformaFullSnapshot["recipient"]): string {
const lines: string[] = []; const lines: string[] = [];
if (recipient.street) lines.push(recipient.street); if (recipient.street) lines.push(recipient.street);

View File

@ -27,8 +27,14 @@ export interface ProformaReportSnapshot {
taxes: ProformaReportTaxSnapshot[]; taxes: ProformaReportTaxSnapshot[];
subtotal_amount: string; subtotal_amount: string;
discount_percentage: string;
discount_amount: string; items_discount_amount: string;
global_discount_percentage: string;
global_discount_amount: string;
total_discount_amount: string;
taxable_amount: string; taxable_amount: string;
taxes_amount: string; taxes_amount: string;
total_amount: string; total_amount: string;

View File

@ -1,7 +1,6 @@
import type { ITransactionManager } from "@erp/core/api"; import type { ITransactionManager } from "@erp/core/api";
import type { UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type { CreateProformaRequestDTO } from "../../../../../common"; import type { CreateProformaRequestDTO } from "../../../../../common";
import type { ICreateProformaInputMapper } from "../../mappers"; import type { ICreateProformaInputMapper } from "../../mappers";
@ -44,7 +43,7 @@ export class CreateProformaUseCase {
const { props, id } = mappedPropsResult.data; const { props, id } = mappedPropsResult.data;
return this.transactionManager.complete(async (transaction: Transaction) => { return this.transactionManager.complete(async (transaction: unknown) => {
try { try {
const createResult = await this.creator.create({ companyId, id, props, transaction }); const createResult = await this.creator.create({ companyId, id, props, transaction });

View File

@ -1,20 +1,23 @@
import type { IPresenterRegistry, ITransactionManager } from "@erp/core/api"; import type { ITransactionManager } from "@erp/core/api";
import { UniqueID, UtcDate } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { import type { IIssuedInvoicePublicServices } from "../../issued-invoices";
IssueCustomerInvoiceDomainService, import type { IProformaFinder, IProformaIssuer } from "../services";
ProformaCustomerInvoiceDomainService,
} from "../../../domain";
import type { CustomerInvoiceApplicationService } from "../../services";
type IssueProformaUseCaseInput = { type IssueProformaUseCaseInput = {
companyId: UniqueID; companyId: UniqueID;
proforma_id: string; proforma_id: string;
}; };
type IssueProformaUseCaseDeps = {
issuedInvoiceServices: IIssuedInvoicePublicServices;
finder: IProformaFinder;
issuer: IProformaIssuer;
transactionManager: ITransactionManager;
};
/** /**
* Caso de uso: Conversión de una proforma a factura definitiva. * Caso de uso: Conversión de una issuedinvoice a factura definitiva.
* *
* - Recupera la proforma * - Recupera la proforma
* - Valida su estado ("approved") * - Valida su estado ("approved")
@ -23,29 +26,30 @@ type IssueProformaUseCaseInput = {
* - Persiste ambas dentro de la misma transacción * - Persiste ambas dentro de la misma transacción
*/ */
export class IssueProformaUseCase { export class IssueProformaUseCase {
private readonly issueDomainService: IssueCustomerInvoiceDomainService; private readonly issuedInvoiceServices: IIssuedInvoicePublicServices;
private readonly proformaDomainService: ProformaCustomerInvoiceDomainService; private readonly finder: IProformaFinder;
private readonly issuer: IProformaIssuer;
private readonly transactionManager: ITransactionManager;
constructor( constructor(deps: IssueProformaUseCaseDeps) {
private readonly service: CustomerInvoiceApplicationService, this.issuedInvoiceServices = deps.issuedInvoiceServices;
private readonly transactionManager: ITransactionManager, this.finder = deps.finder;
private readonly presenterRegistry: IPresenterRegistry this.issuer = deps.issuer;
) { this.transactionManager = deps.transactionManager;
this.issueDomainService = new IssueCustomerInvoiceDomainService();
this.proformaDomainService = new ProformaCustomerInvoiceDomainService();
} }
public execute(params: IssueProformaUseCaseInput) { public execute(params: IssueProformaUseCaseInput) {
const { proforma_id, companyId } = params; const { proforma_id, companyId } = params;
const idOrError = UniqueID.create(proforma_id);
if (idOrError.isFailure) return Result.fail(idOrError.error);
const proformaId = idOrError.data; const proformaIdOrError = UniqueID.create(proforma_id);
if (proformaIdOrError.isFailure) return Result.fail(proformaIdOrError.error);
const proformaId = proformaIdOrError.data;
return this.transactionManager.complete(async (transaction) => { return this.transactionManager.complete(async (transaction) => {
try { try {
/** 1. Recuperamos la proforma */ // 1. Recuperamos la issuedinvoice
const proformaResult = await this.service.getProformaByIdInCompany( const proformaResult = await this.finder.findProformaById(
companyId, companyId,
proformaId, proformaId,
transaction transaction
@ -54,49 +58,39 @@ export class IssueProformaUseCase {
if (proformaResult.isFailure) return Result.fail(proformaResult.error); if (proformaResult.isFailure) return Result.fail(proformaResult.error);
const proforma = proformaResult.data; const proforma = proformaResult.data;
/** 2. Generar nueva factura */ // 2. Generamos la factura definitiva y la guardamos
const nextNumberResult = await this.service.getNextIssuedInvoiceNumber( const issuedInvoiceId = UniqueID.generateNewID();
const createPropsOrError = await this.issuer.issueProforma({
companyId, companyId,
proforma.series, issuedInvoiceId,
transaction proforma,
); transaction,
if (nextNumberResult.isFailure) return Result.fail(nextNumberResult.error);
/** 4. Crear factura definitiva (dominio) */
const issuedInvoiceOrError = await this.issueDomainService.issueFromProforma(proforma, {
issueNumber: nextNumberResult.data,
issueDate: UtcDate.today(),
}); });
if (issuedInvoiceOrError.isFailure) return Result.fail(issuedInvoiceOrError.error); if (createPropsOrError.isFailure) {
return Result.fail(createPropsOrError.error);
}
/** 5. Guardar la nueva factura */ const createProps = createPropsOrError.data;
const saveInvoiceResult = await this.service.createIssuedInvoiceInCompany(
// Creamos y guardamos en persistencia la factura definitiva
const invoiceResult = await this.issuedInvoiceServices.createIssuedInvoice(
issuedInvoiceId,
createProps,
{
companyId, companyId,
issuedInvoiceOrError.data, transaction,
transaction }
);
if (saveInvoiceResult.isFailure) return Result.fail(saveInvoiceResult.error);
/** 6. Actualizar la proforma */
const closedProformaResult = await this.proformaDomainService.markAsIssued(proforma);
if (closedProformaResult.isFailure) return Result.fail(closedProformaResult.error);
const closedProforma = closedProformaResult.data;
/** 7. Guardar la proforma */
await this.service.updateProformaStatusByIdInCompany(
companyId,
proformaId,
closedProforma.status,
transaction
); );
const invoice = saveInvoiceResult.data; if (invoiceResult.isFailure) {
return Result.fail(invoiceResult.error);
}
const dto = { const dto = {
proforma_id: proforma.id.toString(), issuedinvoice_id: issuedInvoiceId.toString(),
invoice_id: invoice.id.toString(), proforma_id: proformaId.toString(),
customer_id: invoice.customerId.toString(), customer_id: proforma.customerId.toString(),
}; };
return Result.ok(dto); return Result.ok(dto);
} catch (error: unknown) { } catch (error: unknown) {

View File

@ -2,7 +2,6 @@ import type { ITransactionManager } from "@erp/core/api";
import type { Criteria } from "@repo/rdx-criteria/server"; import type { Criteria } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type { IProformaFinder } from "../services"; import type { IProformaFinder } from "../services";
import type { IProformaSummarySnapshotBuilder } from "../snapshot-builders"; import type { IProformaSummarySnapshotBuilder } from "../snapshot-builders";
@ -22,7 +21,7 @@ export class ListProformasUseCase {
public execute(params: ListProformasUseCaseInput) { public execute(params: ListProformasUseCaseInput) {
const { criteria, companyId } = params; const { criteria, companyId } = params;
return this.transactionManager.complete(async (transaction: Transaction) => { return this.transactionManager.complete(async (transaction: unknown) => {
try { try {
const result = await this.finder.findProformasByCriteria(companyId, criteria, transaction); const result = await this.finder.findProformasByCriteria(companyId, criteria, transaction);

View File

@ -1,7 +1,6 @@
import type { IPresenterRegistry, ITransactionManager } from "@erp/core/api"; import type { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type { UpdateProformaByIdRequestDTO } from "../../../../../common"; import type { UpdateProformaByIdRequestDTO } from "../../../../../common";
import type { ProformaPatchProps } from "../../../../domain"; import type { ProformaPatchProps } from "../../../../domain";
@ -43,7 +42,7 @@ export class UpdateProformaUseCase {
const patchProps: ProformaPatchProps = patchPropsResult.data; const patchProps: ProformaPatchProps = patchPropsResult.data;
return this.transactionManager.complete(async (transaction: Transaction) => { return this.transactionManager.complete(async (transaction: unknown) => {
try { try {
const updatedInvoice = await this.service.patchProformaByIdInCompany( const updatedInvoice = await this.service.patchProformaByIdInCompany(
companyId, companyId,

View File

@ -1,7 +1,6 @@
import type { Criteria } from "@repo/rdx-criteria/server"; import type { Criteria } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { type Collection, Maybe, Result } from "@repo/rdx-utils"; import { type Collection, Maybe, Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import { import {
CustomerInvoiceIsProformaSpecification, CustomerInvoiceIsProformaSpecification,
@ -35,7 +34,7 @@ export class CustomerInvoiceApplicationService {
*/ */
async getNextProformaNumber( async getNextProformaNumber(
companyId: UniqueID, companyId: UniqueID,
transaction: Transaction transaction: unknown
): Promise<Result<InvoiceNumber, Error>> { ): Promise<Result<InvoiceNumber, Error>> {
return await this.numberGenerator.nextForCompany(companyId, Maybe.none(), transaction); return await this.numberGenerator.nextForCompany(companyId, Maybe.none(), transaction);
} }
@ -51,7 +50,7 @@ export class CustomerInvoiceApplicationService {
async getNextIssuedInvoiceNumber( async getNextIssuedInvoiceNumber(
companyId: UniqueID, companyId: UniqueID,
series: Maybe<InvoiceSerie>, series: Maybe<InvoiceSerie>,
transaction: Transaction transaction: unknown
): Promise<Result<InvoiceNumber, Error>> { ): Promise<Result<InvoiceNumber, Error>> {
return await this.numberGenerator.nextForCompany(companyId, series, transaction); return await this.numberGenerator.nextForCompany(companyId, series, transaction);
} }
@ -83,7 +82,7 @@ export class CustomerInvoiceApplicationService {
async createIssuedInvoiceInCompany( async createIssuedInvoiceInCompany(
companyId: UniqueID, companyId: UniqueID,
invoice: CustomerInvoice, invoice: CustomerInvoice,
transaction: Transaction transaction: unknown
): Promise<Result<CustomerInvoice, Error>> { ): Promise<Result<CustomerInvoice, Error>> {
const result = await this.repository.create(invoice, transaction); const result = await this.repository.create(invoice, transaction);
if (result.isFailure) { if (result.isFailure) {
@ -104,7 +103,7 @@ export class CustomerInvoiceApplicationService {
async createProformaInCompany( async createProformaInCompany(
companyId: UniqueID, companyId: UniqueID,
proforma: CustomerInvoice, proforma: CustomerInvoice,
transaction: Transaction transaction: unknown
): Promise<Result<CustomerInvoice, Error>> { ): Promise<Result<CustomerInvoice, Error>> {
const result = await this.repository.create(proforma, transaction); const result = await this.repository.create(proforma, transaction);
if (result.isFailure) { if (result.isFailure) {
@ -125,7 +124,7 @@ export class CustomerInvoiceApplicationService {
async updateProformaInCompany( async updateProformaInCompany(
companyId: UniqueID, companyId: UniqueID,
proforma: CustomerInvoice, proforma: CustomerInvoice,
transaction: Transaction transaction: unknown
): Promise<Result<CustomerInvoice, Error>> { ): Promise<Result<CustomerInvoice, Error>> {
const result = await this.repository.update(proforma, transaction); const result = await this.repository.update(proforma, transaction);
if (result.isFailure) { if (result.isFailure) {
@ -148,7 +147,7 @@ export class CustomerInvoiceApplicationService {
async existsProformaByIdInCompany( async existsProformaByIdInCompany(
companyId: UniqueID, companyId: UniqueID,
proformaId: UniqueID, proformaId: UniqueID,
transaction?: Transaction transaction?: unknown
): Promise<Result<boolean, Error>> { ): Promise<Result<boolean, Error>> {
return this.repository.existsByIdInCompany(companyId, proformaId, transaction, { return this.repository.existsByIdInCompany(companyId, proformaId, transaction, {
is_proforma: true, is_proforma: true,
@ -168,7 +167,7 @@ export class CustomerInvoiceApplicationService {
async existsIssuedInvoiceByIdInCompany( async existsIssuedInvoiceByIdInCompany(
companyId: UniqueID, companyId: UniqueID,
invoiceId: UniqueID, invoiceId: UniqueID,
transaction?: Transaction transaction?: unknown
): Promise<Result<boolean, Error>> { ): Promise<Result<boolean, Error>> {
return this.repository.existsByIdInCompany(companyId, invoiceId, transaction, { return this.repository.existsByIdInCompany(companyId, invoiceId, transaction, {
is_proforma: false, is_proforma: false,
@ -186,7 +185,7 @@ export class CustomerInvoiceApplicationService {
async findProformasByCriteriaInCompany( async findProformasByCriteriaInCompany(
companyId: UniqueID, companyId: UniqueID,
criteria: Criteria, criteria: Criteria,
transaction?: Transaction transaction?: unknown
): Promise<Result<Collection<CustomerInvoiceListDTO>, Error>> { ): Promise<Result<Collection<CustomerInvoiceListDTO>, Error>> {
return this.repository.findProformasByCriteriaInCompany(companyId, criteria, transaction, {}); return this.repository.findProformasByCriteriaInCompany(companyId, criteria, transaction, {});
} }
@ -202,7 +201,7 @@ export class CustomerInvoiceApplicationService {
async findIssuedInvoiceByCriteriaInCompany( async findIssuedInvoiceByCriteriaInCompany(
companyId: UniqueID, companyId: UniqueID,
criteria: Criteria, criteria: Criteria,
transaction?: Transaction transaction?: unknown
): Promise<Result<Collection<CustomerInvoiceListDTO>, Error>> { ): Promise<Result<Collection<CustomerInvoiceListDTO>, Error>> {
return this.repository.findIssuedInvoicesByCriteriaInCompany( return this.repository.findIssuedInvoicesByCriteriaInCompany(
companyId, companyId,
@ -222,7 +221,7 @@ export class CustomerInvoiceApplicationService {
async getIssuedInvoiceByIdInCompany( async getIssuedInvoiceByIdInCompany(
companyId: UniqueID, companyId: UniqueID,
invoiceId: UniqueID, invoiceId: UniqueID,
transaction?: Transaction transaction?: unknown
): Promise<Result<CustomerInvoice>> { ): Promise<Result<CustomerInvoice>> {
return await this.repository.getIssuedInvoiceByIdInCompany( return await this.repository.getIssuedInvoiceByIdInCompany(
companyId, companyId,
@ -242,7 +241,7 @@ export class CustomerInvoiceApplicationService {
async getProformaByIdInCompany( async getProformaByIdInCompany(
companyId: UniqueID, companyId: UniqueID,
proformaId: UniqueID, proformaId: UniqueID,
transaction?: Transaction transaction?: unknown
): Promise<Result<CustomerInvoice>> { ): Promise<Result<CustomerInvoice>> {
return await this.repository.getProformaByIdInCompany(companyId, proformaId, transaction, {}); return await this.repository.getProformaByIdInCompany(companyId, proformaId, transaction, {});
} }
@ -261,7 +260,7 @@ export class CustomerInvoiceApplicationService {
companyId: UniqueID, companyId: UniqueID,
proformaId: UniqueID, proformaId: UniqueID,
changes: CustomerInvoicePatchProps, changes: CustomerInvoicePatchProps,
transaction?: Transaction transaction?: unknown
): Promise<Result<CustomerInvoice, Error>> { ): Promise<Result<CustomerInvoice, Error>> {
const proformaResult = await this.getProformaByIdInCompany(companyId, proformaId, transaction); const proformaResult = await this.getProformaByIdInCompany(companyId, proformaId, transaction);
@ -289,7 +288,7 @@ export class CustomerInvoiceApplicationService {
async deleteProformaByIdInCompany( async deleteProformaByIdInCompany(
companyId: UniqueID, companyId: UniqueID,
proformaId: UniqueID, proformaId: UniqueID,
transaction?: Transaction transaction?: unknown
): Promise<Result<boolean, Error>> { ): Promise<Result<boolean, Error>> {
// 1) Buscar la proforma // 1) Buscar la proforma
const proformaResult = await this.getProformaByIdInCompany(companyId, proformaId, transaction); const proformaResult = await this.getProformaByIdInCompany(companyId, proformaId, transaction);
@ -329,7 +328,7 @@ export class CustomerInvoiceApplicationService {
companyId: UniqueID, companyId: UniqueID,
proformaId: UniqueID, proformaId: UniqueID,
newStatus: InvoiceStatus, newStatus: InvoiceStatus,
transaction?: Transaction transaction?: unknown
): Promise<Result<boolean, Error>> { ): Promise<Result<boolean, Error>> {
return this.repository.updateProformaStatusByIdInCompany( return this.repository.updateProformaStatusByIdInCompany(
companyId, companyId,

View File

@ -32,9 +32,11 @@ export class InvoiceAmount extends MoneyValue {
} }
// Ensure fluent operations keep the subclass type // Ensure fluent operations keep the subclass type
convertScale(newScale: number) { roundUsingScale(intermediateScale: number) {
const mv = super.convertScale(newScale); const scaled = super.convertScale(intermediateScale);
const p = mv.toPrimitive(); const normalized = scaled.convertScale(InvoiceAmount.DEFAULT_SCALE);
const p = normalized.toPrimitive();
return new InvoiceAmount({ return new InvoiceAmount({
value: p.value, value: p.value,
currency_code: p.currency_code, currency_code: p.currency_code,

View File

@ -19,7 +19,7 @@ export enum INVOICE_STATUS {
const INVOICE_TRANSITIONS: Record<string, string[]> = { const INVOICE_TRANSITIONS: Record<string, string[]> = {
draft: [INVOICE_STATUS.SENT], draft: [INVOICE_STATUS.SENT],
sent: [INVOICE_STATUS.APPROVED, INVOICE_STATUS.REJECTED], sent: [INVOICE_STATUS.APPROVED, INVOICE_STATUS.REJECTED],
approved: [INVOICE_STATUS.ISSUED, INVOICE_STATUS.DRAFT], approved: [INVOICE_STATUS.ISSUED, INVOICE_STATUS.REJECTED, INVOICE_STATUS.DRAFT],
rejected: [INVOICE_STATUS.DRAFT], rejected: [INVOICE_STATUS.DRAFT],
issued: [], issued: [],
}; };
@ -39,34 +39,34 @@ export class InvoiceStatus extends ValueObject<IInvoiceStatusProps> {
return Result.ok( return Result.ok(
value === "rejected" value === "rejected"
? InvoiceStatus.fromRejected() ? InvoiceStatus.rejected()
: value === "sent" : value === "sent"
? InvoiceStatus.fromSent() ? InvoiceStatus.sent()
: value === "issued" : value === "issued"
? InvoiceStatus.fromIssued() ? InvoiceStatus.issued()
: value === "approved" : value === "approved"
? InvoiceStatus.fromApproved() ? InvoiceStatus.approved()
: InvoiceStatus.fromDraft() : InvoiceStatus.draft()
); );
} }
public static fromDraft(): InvoiceStatus { public static draft(): InvoiceStatus {
return new InvoiceStatus({ value: INVOICE_STATUS.DRAFT }); return new InvoiceStatus({ value: INVOICE_STATUS.DRAFT });
} }
public static fromIssued(): InvoiceStatus { public static issued(): InvoiceStatus {
return new InvoiceStatus({ value: INVOICE_STATUS.ISSUED }); return new InvoiceStatus({ value: INVOICE_STATUS.ISSUED });
} }
public static fromSent(): InvoiceStatus { public static sent(): InvoiceStatus {
return new InvoiceStatus({ value: INVOICE_STATUS.SENT }); return new InvoiceStatus({ value: INVOICE_STATUS.SENT });
} }
public static fromApproved(): InvoiceStatus { public static approved(): InvoiceStatus {
return new InvoiceStatus({ value: INVOICE_STATUS.APPROVED }); return new InvoiceStatus({ value: INVOICE_STATUS.APPROVED });
} }
public static fromRejected(): InvoiceStatus { public static rejected(): InvoiceStatus {
return new InvoiceStatus({ value: INVOICE_STATUS.REJECTED }); return new InvoiceStatus({ value: INVOICE_STATUS.REJECTED });
} }

View File

@ -32,9 +32,11 @@ export class ItemAmount extends MoneyValue {
} }
// Ensure fluent operations keep the subclass type // Ensure fluent operations keep the subclass type
convertScale(newScale: number) { roundUsingScale(intermediateScale: number) {
const mv = super.convertScale(newScale); const scaled = super.convertScale(intermediateScale);
const p = mv.toPrimitive(); const normalized = scaled.convertScale(ItemAmount.DEFAULT_SCALE);
const p = normalized.toPrimitive();
return new ItemAmount({ return new ItemAmount({
value: p.value, value: p.value,
currency_code: p.currency_code, currency_code: p.currency_code,

View File

@ -1,19 +0,0 @@
import { Percentage, type PercentageProps } from "@repo/rdx-ddd";
import type { Result } from "@repo/rdx-utils";
type ItemDiscountPercentageProps = Pick<PercentageProps, "value">;
export class ItemDiscountPercentage extends Percentage {
static DEFAULT_SCALE = 2;
static create({ value }: ItemDiscountPercentageProps): Result<Percentage> {
return Percentage.create({
value,
scale: ItemDiscountPercentage.DEFAULT_SCALE,
});
}
static zero() {
return ItemDiscountPercentage.create({ value: 0 }).data;
}
}

View File

@ -19,13 +19,20 @@ import type {
InvoiceSerie, InvoiceSerie,
InvoiceStatus, InvoiceStatus,
} from "../../common"; } from "../../common";
import { IssuedInvoiceItems, type IssuedInvoiceTaxes, type VerifactuRecord } from "../entities"; import {
type IIssuedInvoiceItemCreateProps,
IssuedInvoiceItem,
IssuedInvoiceItems,
type IssuedInvoiceTaxes,
type VerifactuRecord,
} from "../entities";
import { IssuedInvoiceItemMismatch } from "../errors";
export interface IIssuedInvoiceProps { export interface IIssuedInvoiceCreateProps {
companyId: UniqueID; companyId: UniqueID;
status: InvoiceStatus; status: InvoiceStatus;
proformaId: Maybe<UniqueID>; // <- proforma padre en caso de issue proformaId: UniqueID; // <- id de la proforma padre en caso de issue
series: Maybe<InvoiceSerie>; series: Maybe<InvoiceSerie>;
invoiceNumber: InvoiceNumber; invoiceNumber: InvoiceNumber;
@ -45,7 +52,7 @@ export interface IIssuedInvoiceProps {
paymentMethod: Maybe<InvoicePaymentMethod>; paymentMethod: Maybe<InvoicePaymentMethod>;
items: IssuedInvoiceItems; items: IIssuedInvoiceItemCreateProps[];
taxes: IssuedInvoiceTaxes; taxes: IssuedInvoiceTaxes;
subtotalAmount: InvoiceAmount; subtotalAmount: InvoiceAmount;
@ -67,21 +74,28 @@ export interface IIssuedInvoiceProps {
verifactu: Maybe<VerifactuRecord>; verifactu: Maybe<VerifactuRecord>;
} }
export class IssuedInvoice extends AggregateRoot<IIssuedInvoiceProps> { export interface IIssuedInvoice {
private _items!: IssuedInvoiceItems; companyId: UniqueID;
protected constructor(props: IIssuedInvoiceProps, id?: UniqueID) {
super(props, id);
this._items =
props.items ||
IssuedInvoiceItems.create({
languageCode: props.languageCode,
currencyCode: props.currencyCode,
globalDiscountPercentage: props.globalDiscountPercentage,
});
} }
static create(props: IIssuedInvoiceProps, id?: UniqueID): Result<IssuedInvoice, Error> { export type InternalIssuedInvoiceProps = Omit<IIssuedInvoiceCreateProps, "items">;
export class IssuedInvoice
extends AggregateRoot<InternalIssuedInvoiceProps>
implements IIssuedInvoice
{
private readonly _items!: IssuedInvoiceItems;
protected constructor(
props: InternalIssuedInvoiceProps,
items: IssuedInvoiceItems,
id?: UniqueID
) {
super(props, id);
this._items = items;
}
static create(props: IIssuedInvoiceCreateProps, id?: UniqueID): Result<IssuedInvoice, Error> {
if (!props.recipient) { if (!props.recipient) {
return Result.fail( return Result.fail(
new DomainValidationError( new DomainValidationError(
@ -92,7 +106,21 @@ export class IssuedInvoice extends AggregateRoot<IIssuedInvoiceProps> {
); );
} }
const issuedInvoice = new IssuedInvoice(props, id); const internalItems = IssuedInvoiceItems.create({
items: [],
languageCode: props.languageCode,
currencyCode: props.currencyCode,
globalDiscountPercentage: props.globalDiscountPercentage,
});
const { items, ...internalProps } = props;
const issuedInvoice = new IssuedInvoice(internalProps, internalItems, id);
const initializeResult = issuedInvoice.initializeItems(items);
if (initializeResult.isFailure) {
return Result.fail(initializeResult.error);
}
// Reglas de negocio / validaciones // Reglas de negocio / validaciones
// ... // ...
@ -104,6 +132,32 @@ export class IssuedInvoice extends AggregateRoot<IIssuedInvoiceProps> {
return Result.ok(issuedInvoice); return Result.ok(issuedInvoice);
} }
// Rehidratación desde persistencia
static rehydrate(
props: InternalIssuedInvoiceProps,
items: IssuedInvoiceItems,
id: UniqueID
): IssuedInvoice {
return new IssuedInvoice(props, items, id);
}
private initializeItems(itemsProps: IIssuedInvoiceItemCreateProps[]): Result<void, Error> {
for (const [index, itemProps] of itemsProps.entries()) {
const itemResult = IssuedInvoiceItem.create(itemProps);
if (itemResult.isFailure) {
return Result.fail(itemResult.error);
}
const added = this._items.add(itemResult.data);
if (!added) {
return Result.fail(new IssuedInvoiceItemMismatch(index));
}
}
return Result.ok();
}
// Getters // Getters
public get companyId(): UniqueID { public get companyId(): UniqueID {
@ -114,7 +168,7 @@ export class IssuedInvoice extends AggregateRoot<IIssuedInvoiceProps> {
return this.props.customerId; return this.props.customerId;
} }
public get proformaId(): Maybe<UniqueID> { public get proformaId(): UniqueID {
return this.props.proformaId; return this.props.proformaId;
} }
@ -230,8 +284,4 @@ export class IssuedInvoice extends AggregateRoot<IIssuedInvoiceProps> {
public get hasPaymentMethod() { public get hasPaymentMethod() {
return this.paymentMethod.isSome(); return this.paymentMethod.isSome();
} }
public getProps(): IIssuedInvoiceProps {
return this.props;
}
} }

View File

@ -14,7 +14,7 @@ import type { ItemAmount, ItemDescription, ItemQuantity } from "../../../common"
* Todos los importes están previamente calculados y congelados. * Todos los importes están previamente calculados y congelados.
*/ */
export type IssuedInvoiceItemProps = { export interface IIssuedInvoiceItemCreateProps {
description: Maybe<ItemDescription>; description: Maybe<ItemDescription>;
quantity: Maybe<ItemQuantity>; quantity: Maybe<ItemQuantity>;
@ -49,18 +49,55 @@ export type IssuedInvoiceItemProps = {
languageCode: LanguageCode; languageCode: LanguageCode;
currencyCode: CurrencyCode; currencyCode: CurrencyCode;
};
export interface IIssuedInvoiceItem extends IssuedInvoiceItemProps {
isValued(): boolean; // Indica si el item tiene cantidad o precio (o ambos) para ser considerado "valorizado"
} }
export interface IIssuedInvoiceItem {
isValued(): boolean; // Indica si el item tiene cantidad o precio (o ambos) para ser considerado "valorizado"
description: Maybe<ItemDescription>;
quantity: Maybe<ItemQuantity>;
unitAmount: Maybe<ItemAmount>;
subtotalAmount: ItemAmount;
itemDiscountPercentage: Maybe<DiscountPercentage>;
itemDiscountAmount: ItemAmount;
globalDiscountPercentage: DiscountPercentage;
globalDiscountAmount: ItemAmount;
totalDiscountAmount: ItemAmount;
taxableAmount: ItemAmount;
ivaCode: Maybe<string>;
ivaPercentage: Maybe<DiscountPercentage>;
ivaAmount: ItemAmount;
recCode: Maybe<string>;
recPercentage: Maybe<DiscountPercentage>;
recAmount: ItemAmount;
retentionCode: Maybe<string>;
retentionPercentage: Maybe<DiscountPercentage>;
retentionAmount: ItemAmount;
taxesAmount: ItemAmount;
totalAmount: ItemAmount;
languageCode: LanguageCode;
currencyCode: CurrencyCode;
}
type InternalIssuedInvoiceItemProps = IIssuedInvoiceItemCreateProps;
export class IssuedInvoiceItem export class IssuedInvoiceItem
extends DomainEntity<IssuedInvoiceItemProps> extends DomainEntity<InternalIssuedInvoiceItemProps>
implements IIssuedInvoiceItem implements IIssuedInvoiceItem
{ {
public static create( public static create(
props: IssuedInvoiceItemProps, props: IIssuedInvoiceItemCreateProps,
id?: UniqueID id?: UniqueID
): Result<IssuedInvoiceItem, Error> { ): Result<IssuedInvoiceItem, Error> {
const item = new IssuedInvoiceItem(props, id); const item = new IssuedInvoiceItem(props, id);
@ -72,7 +109,11 @@ export class IssuedInvoiceItem
return Result.ok(item); return Result.ok(item);
} }
protected constructor(props: IssuedInvoiceItemProps, id?: UniqueID) { static rehydrate(props: InternalIssuedInvoiceItemProps, id: UniqueID): IssuedInvoiceItem {
return new IssuedInvoiceItem(props, id);
}
protected constructor(props: InternalIssuedInvoiceItemProps, id?: UniqueID) {
super(props, id); super(props, id);
} }
@ -163,7 +204,7 @@ export class IssuedInvoiceItem
return this.props.totalAmount; return this.props.totalAmount;
} }
getProps(): IssuedInvoiceItemProps { getProps(): IIssuedInvoiceItemCreateProps {
return this.props; return this.props;
} }

View File

@ -0,0 +1 @@
export * from "./issued-invoice-item-not-valid-error";

View File

@ -0,0 +1,32 @@
import { DomainError } from "@repo/rdx-ddd";
/**
* Error de dominio que indica que al añadir un nuevo item a la lista
* de detalles, este no tiene el mismo "currencyCode" y "languageCode"
* que la colección de items.
*
*/
export class IssuedInvoiceItemMismatch extends DomainError {
/**
* Crea una instancia del error con el identificador del item.
*
* @param position - Posición del item
* @param options - Opciones nativas de Error (puedes pasar `cause`).
*/
constructor(position: number, options?: ErrorOptions) {
super(
`Error. IssuedInvoice item with position '${position}' rejected due to currency/language mismatch.`,
options
);
this.name = "IssuedInvoiceItemMismatch";
}
}
/**
* *Type guard* para `IssuedInvoiceItemNotValid`.
*
* @param e - Error desconocido
* @returns `true` si `e` es `IssuedInvoiceItemNotValid`
*/
export const isIssuedInvoiceItemMismatch = (e: unknown): e is IssuedInvoiceItemMismatch =>
e instanceof IssuedInvoiceItemMismatch;

View File

@ -1,3 +1,4 @@
export * from "./aggregates"; export * from "./aggregates";
export * from "./entities"; export * from "./entities";
export * from "./errors";
export * from "./value-objects"; export * from "./value-objects";

View File

@ -17,7 +17,7 @@ import {
type InvoiceNumber, type InvoiceNumber,
type InvoiceRecipient, type InvoiceRecipient,
type InvoiceSerie, type InvoiceSerie,
type InvoiceStatus, InvoiceStatus,
type ItemAmount, type ItemAmount,
} from "../../common/value-objects"; } from "../../common/value-objects";
import { import {
@ -27,7 +27,7 @@ import {
ProformaItems, ProformaItems,
} from "../entities"; } from "../entities";
import { ProformaItemMismatch } from "../errors"; import { ProformaItemMismatch } from "../errors";
import { type IProformaTaxTotals, ProformaTaxesCalculator } from "../services"; import type { IProformaTaxTotals } from "../services";
import { ProformaItemTaxes } from "../value-objects"; import { ProformaItemTaxes } from "../value-objects";
export interface IProformaCreateProps { export interface IProformaCreateProps {
@ -249,7 +249,7 @@ export class Proforma extends AggregateRoot<InternalProformaProps> implements IP
} }
public issue(): Result<void, Error> { public issue(): Result<void, Error> {
if (!this.props.status.canTransitionTo("ISSUED")) { if (!this.props.status.canTransitionTo("issued")) {
return Result.fail( return Result.fail(
new DomainValidationError( new DomainValidationError(
"INVALID_STATE", "INVALID_STATE",
@ -259,8 +259,7 @@ export class Proforma extends AggregateRoot<InternalProformaProps> implements IP
); );
} }
// Falta this.props.status = InvoiceStatus.issued();
//this.props.status = this.props.status.canTransitionTo("ISSUED");
return Result.ok(); return Result.ok();
} }
@ -292,7 +291,7 @@ export class Proforma extends AggregateRoot<InternalProformaProps> implements IP
} }
public taxes(): Collection<IProformaTaxTotals> { public taxes(): Collection<IProformaTaxTotals> {
return new ProformaTaxesCalculator(this.items).calculate(); return this.items.taxes();
} }
public addItem(props: IProformaItemCreateProps): Result<void, Error> { public addItem(props: IProformaItemCreateProps): Result<void, Error> {

View File

@ -3,6 +3,7 @@ import type { CurrencyCode, LanguageCode } from "@repo/rdx-ddd";
import { Collection, Result } from "@repo/rdx-utils"; import { Collection, Result } from "@repo/rdx-utils";
import { ProformaItemMismatch } from "../../errors"; import { ProformaItemMismatch } from "../../errors";
import { type IProformaTaxTotals, ProformaTaxesCalculator } from "../../services";
import { ProformaItemsTotalsCalculator } from "../../services/proforma-items-totals-calculator"; import { ProformaItemsTotalsCalculator } from "../../services/proforma-items-totals-calculator";
import type { IProformaItem, IProformaItemTotals, ProformaItem } from "./proforma-item.entity"; import type { IProformaItem, IProformaItemTotals, ProformaItem } from "./proforma-item.entity";
@ -37,6 +38,7 @@ export interface IProformaItems {
valued(): IProformaItem[]; // Devuelve solo las líneas valoradas. valued(): IProformaItem[]; // Devuelve solo las líneas valoradas.
totals(): IProformaItemTotals; totals(): IProformaItemTotals;
taxes(): Collection<IProformaTaxTotals>;
readonly globalDiscountPercentage: DiscountPercentage; // % descuento de la cabecera readonly globalDiscountPercentage: DiscountPercentage; // % descuento de la cabecera
readonly languageCode: LanguageCode; // Para formateos específicos de idioma readonly languageCode: LanguageCode; // Para formateos específicos de idioma
@ -57,6 +59,7 @@ export class ProformaItems extends Collection<ProformaItem> implements IProforma
if (this.items.length > 0) { if (this.items.length > 0) {
this.ensureSameContext(this.items); this.ensureSameContext(this.items);
} }
4;
} }
static create(props: IProformaItemsProps): ProformaItems { static create(props: IProformaItemsProps): ProformaItems {
@ -113,6 +116,10 @@ export class ProformaItems extends Collection<ProformaItem> implements IProforma
return new ProformaItemsTotalsCalculator(this).calculate(); return new ProformaItemsTotalsCalculator(this).calculate();
} }
public taxes(): Collection<IProformaTaxTotals> {
return new ProformaTaxesCalculator(this).calculate();
}
private ensureSameContext(items: IProformaItem[]): void { private ensureSameContext(items: IProformaItem[]): void {
for (const item of items) { for (const item of items) {
const same = const same =

View File

@ -1,5 +1,5 @@
import { ItemAmount } from "../../common"; import { ItemAmount } from "../../common";
import type { IProformaItems } from "../entities"; import type { ProformaItems } from "../entities";
export interface IProformaItemsTotals { export interface IProformaItemsTotals {
subtotalAmount: ItemAmount; subtotalAmount: ItemAmount;
@ -23,7 +23,7 @@ export interface IProformaItemsTotals {
* La lógica fiscal está en ProformaItem; aquí solo se agregan resultados. * La lógica fiscal está en ProformaItem; aquí solo se agregan resultados.
*/ */
export class ProformaItemsTotalsCalculator { export class ProformaItemsTotalsCalculator {
constructor(private readonly items: IProformaItems) {} constructor(private readonly items: ProformaItems) {}
public calculate(): IProformaItemsTotals { public calculate(): IProformaItemsTotals {
const zero = ItemAmount.zero(this.items.currencyCode.code); const zero = ItemAmount.zero(this.items.currencyCode.code);
@ -47,26 +47,28 @@ export class ProformaItemsTotalsCalculator {
const amounts = item.totals(); const amounts = item.totals();
// Subtotales // Subtotales
subtotalAmount = subtotalAmount.add(amounts.subtotalAmount); subtotalAmount = subtotalAmount.add(amounts.subtotalAmount.roundUsingScale(2));
// Descuentos // Descuentos
itemDiscountAmount = itemDiscountAmount.add(amounts.itemDiscountAmount); itemDiscountAmount = itemDiscountAmount.add(amounts.itemDiscountAmount.roundUsingScale(2));
globalDiscountAmount = globalDiscountAmount.add(amounts.globalDiscountAmount); globalDiscountAmount = globalDiscountAmount.add(
totalDiscountAmount = totalDiscountAmount.add(amounts.totalDiscountAmount); amounts.globalDiscountAmount.roundUsingScale(2)
);
totalDiscountAmount = totalDiscountAmount.add(amounts.totalDiscountAmount.roundUsingScale(2));
// Base imponible // Base imponible
taxableAmount = taxableAmount.add(amounts.taxableAmount); taxableAmount = taxableAmount.add(amounts.taxableAmount.roundUsingScale(2));
// Impuestos individuales // Impuestos individuales
ivaAmount = ivaAmount.add(amounts.ivaAmount); ivaAmount = ivaAmount.add(amounts.ivaAmount.roundUsingScale(2));
recAmount = recAmount.add(amounts.recAmount); recAmount = recAmount.add(amounts.recAmount.roundUsingScale(2));
retentionAmount = retentionAmount.add(amounts.retentionAmount); retentionAmount = retentionAmount.add(amounts.retentionAmount.roundUsingScale(2));
// Total impuestos del ítem // Total impuestos del ítem
taxesAmount = taxesAmount.add(amounts.taxesAmount); taxesAmount = taxesAmount.add(amounts.taxesAmount.roundUsingScale(2));
// Total final del ítem // Total final del ítem
totalAmount = totalAmount.add(amounts.totalAmount); totalAmount = totalAmount.add(amounts.totalAmount.roundUsingScale(2));
} }
return { return {

View File

@ -29,8 +29,8 @@ export class ProformaTaxesCalculator {
constructor(private readonly items: IProformaItems) {} constructor(private readonly items: IProformaItems) {}
public calculate(): Collection<IProformaTaxTotals> { public calculate(): Collection<IProformaTaxTotals> {
const groups = proformaComputeTaxGroups(this.items); const groups = proformaComputeTaxGroups(this.items); // <- devuelve en escala 4
const currencyCode = this.items.currencyCode; //const currencyCode = this.items.currencyCode;
const rows = Array.from(groups.values()).map((g) => { const rows = Array.from(groups.values()).map((g) => {
const taxableAmount = this.toInvoiceAmount(g.taxableAmount); const taxableAmount = this.toInvoiceAmount(g.taxableAmount);

View File

@ -67,7 +67,7 @@ export class IssueCustomerInvoiceDomainService {
...proformaProps, ...proformaProps,
isProforma: false, isProforma: false,
proformaId: Maybe.some(proforma.id), proformaId: Maybe.some(proforma.id),
status: InvoiceStatus.fromIssued(), status: InvoiceStatus.issued(),
invoiceNumber: issueNumber, invoiceNumber: issueNumber,
invoiceDate: issueDate, invoiceDate: issueDate,
description: proformaProps.description.isNone() ? Maybe.some(".") : proformaProps.description, description: proformaProps.description.isNone() ? Maybe.some(".") : proformaProps.description,

View File

@ -1,21 +1,15 @@
import type { IModuleServer } from "@erp/core/api"; import type { IModuleServer } from "@erp/core/api";
import { import {
type IssuedInvoicePublicServices, buildIssuedInvoicePublicServices,
type IssuedInvoicesInternalDeps,
type ProformaPublicServices,
type ProformasInternalDeps,
buildIssuedInvoiceServices,
buildIssuedInvoicesDependencies, buildIssuedInvoicesDependencies,
buildProformaServices, buildProformaPublicServices,
buildProformasDependencies, buildProformasDependencies,
issuedInvoicesRouter, issuedInvoicesRouter,
models, models,
proformasRouter, proformasRouter,
} from "./infrastructure"; } from "./infrastructure";
export type { IssuedInvoicePublicServices, ProformaPublicServices };
export const customerInvoicesAPIModule: IModuleServer = { export const customerInvoicesAPIModule: IModuleServer = {
name: "customer-invoices", name: "customer-invoices",
version: "1.0.0", version: "1.0.0",
@ -36,14 +30,8 @@ export const customerInvoicesAPIModule: IModuleServer = {
const proformasInternal = buildProformasDependencies(params); const proformasInternal = buildProformasDependencies(params);
// 2) Servicios públicos (Application Services) // 2) Servicios públicos (Application Services)
const issuedInvoicesServices: IssuedInvoicePublicServices = buildIssuedInvoiceServices( const issuedInvoicesServices = buildIssuedInvoicePublicServices(params, issuedInvoicesInternal);
params, const proformasServices = buildProformaPublicServices(params, proformasInternal);
issuedInvoicesInternal
);
const proformasServices: ProformaPublicServices = buildProformaServices(
params,
proformasInternal
);
logger.info("🚀 CustomerInvoices module dependencies registered", { label: this.name }); logger.info("🚀 CustomerInvoices module dependencies registered", { label: this.name });
@ -73,22 +61,11 @@ export const customerInvoicesAPIModule: IModuleServer = {
* - NO construye dominio * - NO construye dominio
*/ */
async start(params) { async start(params) {
const { app, baseRoutePath, logger, getInternal } = params; const { app, baseRoutePath, logger, getInternal, getService } = params;
// Recuperamos el dominio interno del módulo
const issuedInvoicesInternalDeps = getInternal<IssuedInvoicesInternalDeps>(
"customer-invoices",
"issuedInvoices"
);
const proformasInternalDeps = getInternal<ProformasInternalDeps>(
"customer-invoices",
"proformas"
);
// Registro de rutas HTTP // Registro de rutas HTTP
issuedInvoicesRouter(params, issuedInvoicesInternalDeps); issuedInvoicesRouter(params);
proformasRouter(params, proformasInternalDeps); proformasRouter(params);
logger.info("🚀 CustomerInvoices module started", { logger.info("🚀 CustomerInvoices module started", {
label: this.name, label: this.name,

View File

@ -1,5 +1,6 @@
import type { JsonTaxCatalogProvider } from "@erp/core"; import type { JsonTaxCatalogProvider } from "@erp/core";
import { import {
DiscountPercentage,
type ISequelizeDomainMapper, type ISequelizeDomainMapper,
type MapperParamsType, type MapperParamsType,
SequelizeDomainMapper, SequelizeDomainMapper,
@ -18,18 +19,11 @@ import { Result } from "@repo/rdx-utils";
import { import {
type IProformaCreateProps, type IProformaCreateProps,
IssuedInvoiceItem, IssuedInvoiceItem,
type IssuedInvoiceItemProps,
ItemAmount, ItemAmount,
ItemDescription, ItemDescription,
ItemDiscountPercentage,
ItemQuantity, ItemQuantity,
ItemTaxGroup,
type Proforma, type Proforma,
} from "../../../../../../domain"; } from "../../../../../../domain";
import type {
CustomerInvoiceItemCreationAttributes,
CustomerInvoiceItemModel,
} from "../../../../sequelize";
export interface ICustomerInvoiceItemDomainMapper export interface ICustomerInvoiceItemDomainMapper
extends ISequelizeDomainMapper< extends ISequelizeDomainMapper<
@ -99,7 +93,7 @@ export class CustomerInvoiceItemDomainMapper
const discountPercentage = extractOrPushError( const discountPercentage = extractOrPushError(
maybeFromNullableResult(source.discount_percentage_value, (v) => maybeFromNullableResult(source.discount_percentage_value, (v) =>
ItemDiscountPercentage.create({ value: v }) DiscountPercentage.create({ value: v })
), ),
`items[${index}].discount_percentage`, `items[${index}].discount_percentage`,
errors errors
@ -107,7 +101,7 @@ export class CustomerInvoiceItemDomainMapper
const globalDiscountPercentage = extractOrPushError( const globalDiscountPercentage = extractOrPushError(
maybeFromNullableResult(source.global_discount_percentage_value, (v) => maybeFromNullableResult(source.global_discount_percentage_value, (v) =>
ItemDiscountPercentage.create({ value: v }) DiscountPercentage.create({ value: v })
), ),
`items[${index}].discount_percentage`, `items[${index}].discount_percentage`,
errors errors
@ -242,7 +236,7 @@ export class CustomerInvoiceItemDomainMapper
), ),
discount_percentage_scale: discount_percentage_scale:
maybeToNullable(source.itemDiscountPercentage, (v) => v.toPrimitive().scale) ?? maybeToNullable(source.itemDiscountPercentage, (v) => v.toPrimitive().scale) ??
ItemDiscountPercentage.DEFAULT_SCALE, DiscountPercentage.DEFAULT_SCALE,
discount_amount_value: allAmounts.itemDiscountAmount.value, discount_amount_value: allAmounts.itemDiscountAmount.value,
discount_amount_scale: allAmounts.itemDiscountAmount.scale, discount_amount_scale: allAmounts.itemDiscountAmount.scale,
@ -255,7 +249,7 @@ export class CustomerInvoiceItemDomainMapper
global_discount_percentage_scale: global_discount_percentage_scale:
maybeToNullable(source.globalDiscountPercentage, (v) => v.toPrimitive().scale) ?? maybeToNullable(source.globalDiscountPercentage, (v) => v.toPrimitive().scale) ??
ItemDiscountPercentage.DEFAULT_SCALE, DiscountPercentage.DEFAULT_SCALE,
global_discount_amount_value: allAmounts.globalDiscountAmount.value, global_discount_amount_value: allAmounts.globalDiscountAmount.value,
global_discount_amount_scale: allAmounts.globalDiscountAmount.scale, global_discount_amount_scale: allAmounts.globalDiscountAmount.scale,

View File

@ -1,2 +1,3 @@
export * from "./issued-invoice-number-generator.di";
export * from "./issued-invoice-public-services"; export * from "./issued-invoice-public-services";
export * from "./issued-invoices.di"; export * from "./issued-invoices.di";

View File

@ -0,0 +1,5 @@
import type { IIssuedInvoiceNumberGenerator } from "../../../application";
import { SequelizeIssuedInvoiceNumberGenerator } from "../persistence";
export const buildIssuedInvoiceNumberGenerator = (): IIssuedInvoiceNumberGenerator =>
new SequelizeIssuedInvoiceNumberGenerator();

View File

@ -1,26 +1,27 @@
import type { SetupParams } from "@erp/core/api"; import type { SetupParams } from "@erp/core/api";
import { buildCatalogs, buildTransactionManager } from "@erp/core/api"; import { buildCatalogs, buildTransactionManager } from "@erp/core/api";
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { import {
type IIssuedInvoiceCreatorParams,
type IIssuedInvoicePublicServices,
type IIssuedInvoiceServicesContext,
buildIssuedInvoiceCreator,
buildIssuedInvoiceFinder, buildIssuedInvoiceFinder,
buildIssuedInvoiceSnapshotBuilders, buildIssuedInvoiceSnapshotBuilders,
} from "../../../application/issued-invoices"; } from "../../../application/issued-invoices";
import { buildIssuedInvoiceDocumentService } from "./issued-invoice-documents.di"; import { buildIssuedInvoiceDocumentService } from "./issued-invoice-documents.di";
import { buildIssuedInvoiceNumberGenerator } from "./issued-invoice-number-generator.di";
import { buildIssuedInvoicePersistenceMappers } from "./issued-invoice-persistence-mappers.di"; import { buildIssuedInvoicePersistenceMappers } from "./issued-invoice-persistence-mappers.di";
import { buildIssuedInvoiceRepository } from "./issued-invoice-repositories.di"; import { buildIssuedInvoiceRepository } from "./issued-invoice-repositories.di";
import type { IssuedInvoicesInternalDeps } from "./issued-invoices.di"; import type { IssuedInvoicesInternalDeps } from "./issued-invoices.di";
export type IssuedInvoicePublicServices = { export function buildIssuedInvoicePublicServices(
listIssuedInvoices: (filters: unknown, context: unknown) => null;
getIssuedInvoiceById: (id: unknown, context: unknown) => null;
generateIssuedInvoiceReport: (id: unknown, options: unknown, context: unknown) => null;
};
export function buildIssuedInvoiceServices(
params: SetupParams, params: SetupParams,
deps: IssuedInvoicesInternalDeps deps: IssuedInvoicesInternalDeps
): IssuedInvoicePublicServices { ): IIssuedInvoicePublicServices {
const { database } = params; const { database } = params;
// Infrastructure // Infrastructure
@ -29,20 +30,29 @@ export function buildIssuedInvoiceServices(
const persistenceMappers = buildIssuedInvoicePersistenceMappers(catalogs); const persistenceMappers = buildIssuedInvoicePersistenceMappers(catalogs);
const repository = buildIssuedInvoiceRepository({ database, mappers: persistenceMappers }); const repository = buildIssuedInvoiceRepository({ database, mappers: persistenceMappers });
const numberService = buildIssuedInvoiceNumberGenerator();
// Application helpers // Application helpers
const creator = buildIssuedInvoiceCreator({ numberService, repository });
const finder = buildIssuedInvoiceFinder(repository); const finder = buildIssuedInvoiceFinder(repository);
const snapshotBuilders = buildIssuedInvoiceSnapshotBuilders(); const snapshotBuilders = buildIssuedInvoiceSnapshotBuilders();
const documentGeneratorPipeline = buildIssuedInvoiceDocumentService(params); const documentGeneratorPipeline = buildIssuedInvoiceDocumentService(params);
return { return {
listIssuedInvoices: (filters, context) => null, createIssuedInvoice: async (
//internal.useCases.listIssuedInvoices().execute(filters, context), id: UniqueID,
props: IIssuedInvoiceCreatorParams["props"],
context: IIssuedInvoiceServicesContext
) => {
const { transaction, companyId } = context;
getIssuedInvoiceById: (id, context) => null, const createResult = await creator.create({ companyId, id, props, transaction });
//internal.useCases.getIssuedInvoiceById().execute(id, context),
generateIssuedInvoiceReport: (id, options, context) => null, if (createResult.isFailure) {
//internal.useCases.reportIssuedInvoice().execute(id, options, context), return Result.fail(createResult.error);
}
return Result.ok(createResult.data);
},
}; };
} }

View File

@ -10,14 +10,9 @@ import {
import { import {
type CustomerInvoiceIdAlreadyExistsError, type CustomerInvoiceIdAlreadyExistsError,
type EntityIsNotProformaError, type IssuedInvoiceItemMismatch,
type InvalidProformaTransitionError,
type ProformaCannotBeConvertedToInvoiceError,
isCustomerInvoiceIdAlreadyExistsError, isCustomerInvoiceIdAlreadyExistsError,
isEntityIsNotProformaError, isIssuedInvoiceItemMismatch,
isInvalidProformaTransitionError,
isProformaCannotBeConvertedToInvoiceError,
isProformaCannotBeDeletedError,
} from "../../../domain"; } from "../../../domain";
// Crea una regla específica (prioridad alta para sobreescribir mensajes) // Crea una regla específica (prioridad alta para sobreescribir mensajes)
@ -31,47 +26,17 @@ const invoiceDuplicateRule: ErrorToApiRule = {
), ),
}; };
const entityIsNotProformaError: ErrorToApiRule = { const issuedinvoiceItemMismatchError: ErrorToApiRule = {
priority: 120, priority: 120,
matches: (e) => isEntityIsNotProformaError(e), matches: (e) => isIssuedInvoiceItemMismatch(e),
build: (e) => build: (e) =>
new ValidationApiError( new ValidationApiError(
(e as EntityIsNotProformaError).message || "Entity with the provided id is not proforma" (e as IssuedInvoiceItemMismatch).message ||
), "IssuedInvoice item rejected due to currency/language mismatch"
};
const proformaTransitionRule: ErrorToApiRule = {
priority: 120,
matches: (e) => isInvalidProformaTransitionError(e),
build: (e) =>
new ValidationApiError(
(e as InvalidProformaTransitionError).message || "Invalid transition for proforma."
),
};
const proformaConversionRule: ErrorToApiRule = {
priority: 120,
matches: (e) => isProformaCannotBeConvertedToInvoiceError(e),
build: (e) =>
new ValidationApiError(
(e as ProformaCannotBeConvertedToInvoiceError).message ||
"Proforma cannot be converted to an Invoice."
),
};
const proformaCannotBeDeletedRule: ErrorToApiRule = {
priority: 120,
matches: (e) => isProformaCannotBeDeletedError(e),
build: (e) =>
new ValidationApiError(
(e as ProformaCannotBeConvertedToInvoiceError).message || "Proforma cannot be deleted."
), ),
}; };
// Cómo aplicarla: crea una nueva instancia del mapper con la regla extra // Cómo aplicarla: crea una nueva instancia del mapper con la regla extra
export const customerInvoicesApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default() export const customerInvoicesApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default()
.register(invoiceDuplicateRule) .register(invoiceDuplicateRule)
.register(entityIsNotProformaError) .register(issuedinvoiceItemMismatchError);
.register(proformaConversionRule)
.register(proformaCannotBeDeletedRule)
.register(proformaTransitionRule);

View File

@ -1,5 +1,5 @@
import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth/api"; import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth/api";
import { type ModuleParams, type RequestWithAuth, validateRequest } from "@erp/core/api"; import { type RequestWithAuth, type StartParams, validateRequest } from "@erp/core/api";
import { type NextFunction, type Request, type Response, Router } from "express"; import { type NextFunction, type Request, type Response, Router } from "express";
import { import {
@ -16,11 +16,15 @@ import {
ReportIssuedInvoiceController, ReportIssuedInvoiceController,
} from "./controllers"; } from "./controllers";
export const issuedInvoicesRouter = (params: ModuleParams, deps: IssuedInvoicesInternalDeps) => { export const issuedInvoicesRouter = (params: StartParams) => {
const { app, config } = params; const { app, config, getService, getInternal } = params;
const deps = getInternal<IssuedInvoicesInternalDeps>("customer-invoices", "issuedInvoices");
const router: Router = Router({ mergeParams: true }); const router: Router = Router({ mergeParams: true });
// ----------------------------------------------
if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "production") { if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "production") {
// 🔐 Autenticación + Tenancy para TODO el router // 🔐 Autenticación + Tenancy para TODO el router
router.use( router.use(

View File

@ -1,2 +1,3 @@
export * from "./mappers"; export * from "./mappers";
export * from "./repositories"; export * from "./repositories";
export * from "./services";

View File

@ -14,7 +14,7 @@ import {
import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils"; import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils";
import { import {
type IIssuedInvoiceProps, type InternalIssuedInvoiceProps,
InvoiceAmount, InvoiceAmount,
InvoiceNumber, InvoiceNumber,
InvoicePaymentMethod, InvoicePaymentMethod,
@ -63,8 +63,9 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
const customerId = extractOrPushError(UniqueID.create(raw.customer_id), "customer_id", errors); const customerId = extractOrPushError(UniqueID.create(raw.customer_id), "customer_id", errors);
// Para issued invoices, proforma_id debe estar relleno
const proformaId = extractOrPushError( const proformaId = extractOrPushError(
maybeFromNullableResult(raw.proforma_id, (v) => UniqueID.create(v)), UniqueID.create(String(raw.proforma_id)),
"proforma_id", "proforma_id",
errors errors
); );
@ -346,7 +347,7 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
currencyCode: attributes.currencyCode!, currencyCode: attributes.currencyCode!,
}); });
const invoiceProps: IIssuedInvoiceProps = { const invoiceProps: InternalIssuedInvoiceProps = {
companyId: attributes.companyId!, companyId: attributes.companyId!,
proformaId: attributes.proformaId!, proformaId: attributes.proformaId!,
@ -383,22 +384,14 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
paymentMethod: attributes.paymentMethod!, paymentMethod: attributes.paymentMethod!,
items,
taxes, taxes,
verifactu, verifactu,
}; };
const createResult = IssuedInvoice.create(invoiceProps, attributes.invoiceId); const invoiceId = attributes.invoiceId!;
const invoice = IssuedInvoice.rehydrate(invoiceProps, items, invoiceId);
if (createResult.isFailure) { return Result.ok(invoice);
return Result.fail(
new ValidationErrorCollection("Customer invoice entity creation failed", [
{ path: "invoice", message: createResult.error.message },
])
);
}
return Result.ok(createResult.data);
} catch (err: unknown) { } catch (err: unknown) {
return Result.fail(err as Error); return Result.fail(err as Error);
} }
@ -471,7 +464,7 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
// Flags / estado / serie / número // Flags / estado / serie / número
is_proforma: false, is_proforma: false,
status: source.status.toPrimitive(), status: source.status.toPrimitive(),
proforma_id: maybeToNullable(source.proformaId, (v) => v.toPrimitive()), proforma_id: source.proformaId.toPrimitive(),
series: maybeToNullable(source.series, (v) => v.toPrimitive()), series: maybeToNullable(source.series, (v) => v.toPrimitive()),
invoice_number: source.invoiceNumber.toPrimitive(), invoice_number: source.invoiceNumber.toPrimitive(),

View File

@ -1,5 +1,10 @@
import type { JsonTaxCatalogProvider } from "@erp/core"; import type { JsonTaxCatalogProvider } from "@erp/core";
import { type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api"; import {
DiscountPercentage,
type MapperParamsType,
SequelizeDomainMapper,
TaxPercentage,
} from "@erp/core/api";
import { import {
UniqueID, UniqueID,
ValidationErrorCollection, ValidationErrorCollection,
@ -13,16 +18,13 @@ import {
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { import {
DiscountPercentage, type IIssuedInvoiceCreateProps,
type IIssuedInvoiceProps, type IIssuedInvoiceItemCreateProps,
type IssuedInvoice, type IssuedInvoice,
IssuedInvoiceItem, IssuedInvoiceItem,
type IssuedInvoiceItemProps,
ItemAmount, ItemAmount,
ItemDescription, ItemDescription,
ItemDiscountPercentage,
ItemQuantity, ItemQuantity,
ItemTaxPercentage,
} from "../../../../../../domain"; } from "../../../../../../domain";
import type { import type {
CustomerInvoiceItemCreationAttributes, CustomerInvoiceItemCreationAttributes,
@ -34,7 +36,7 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
CustomerInvoiceItemCreationAttributes, CustomerInvoiceItemCreationAttributes,
IssuedInvoiceItem IssuedInvoiceItem
> { > {
private taxCatalog!: JsonTaxCatalogProvider; private readonly taxCatalog!: JsonTaxCatalogProvider;
constructor(params: MapperParamsType) { constructor(params: MapperParamsType) {
super(); super();
@ -52,11 +54,11 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
private mapAttributesToDomain( private mapAttributesToDomain(
raw: CustomerInvoiceItemModel, raw: CustomerInvoiceItemModel,
params?: MapperParamsType params?: MapperParamsType
): Partial<IssuedInvoiceItemProps> & { itemId?: UniqueID } { ): Partial<IIssuedInvoiceItemCreateProps> & { itemId?: UniqueID } {
const { errors, index, attributes } = params as { const { errors, index, attributes } = params as {
index: number; index: number;
errors: ValidationErrorDetail[]; errors: ValidationErrorDetail[];
attributes: Partial<IIssuedInvoiceProps>; attributes: Partial<IIssuedInvoiceCreateProps>;
}; };
const itemId = extractOrPushError( const itemId = extractOrPushError(
@ -96,7 +98,7 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
const itemDiscountPercentage = extractOrPushError( const itemDiscountPercentage = extractOrPushError(
maybeFromNullableResult(raw.item_discount_percentage_value, (v) => maybeFromNullableResult(raw.item_discount_percentage_value, (v) =>
ItemDiscountPercentage.create({ value: v }) DiscountPercentage.create({ value: v })
), ),
`items[${index}].item_discount_percentage_value`, `items[${index}].item_discount_percentage_value`,
errors errors
@ -112,9 +114,7 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
); );
const globalDiscountPercentage = extractOrPushError( const globalDiscountPercentage = extractOrPushError(
maybeFromNullableResult(raw.global_discount_percentage_value, (v) => DiscountPercentage.create({ value: raw.global_discount_percentage_value }),
DiscountPercentage.create({ value: v })
),
`items[${index}].global_discount_percentage_value`, `items[${index}].global_discount_percentage_value`,
errors errors
); );
@ -149,9 +149,7 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
const ivaCode = maybeFromNullableOrEmptyString(raw.iva_code); const ivaCode = maybeFromNullableOrEmptyString(raw.iva_code);
const ivaPercentage = extractOrPushError( const ivaPercentage = extractOrPushError(
maybeFromNullableResult(raw.iva_percentage_value, (value) => maybeFromNullableResult(raw.iva_percentage_value, (value) => TaxPercentage.create({ value })),
ItemTaxPercentage.create({ value })
),
`items[${index}].iva_percentage_value`, `items[${index}].iva_percentage_value`,
errors errors
); );
@ -168,9 +166,7 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
const recCode = maybeFromNullableOrEmptyString(raw.rec_code); const recCode = maybeFromNullableOrEmptyString(raw.rec_code);
const recPercentage = extractOrPushError( const recPercentage = extractOrPushError(
maybeFromNullableResult(raw.rec_percentage_value, (value) => maybeFromNullableResult(raw.rec_percentage_value, (value) => TaxPercentage.create({ value })),
ItemTaxPercentage.create({ value })
),
`items[${index}].rec_percentage_value`, `items[${index}].rec_percentage_value`,
errors errors
); );
@ -188,7 +184,7 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
const retentionPercentage = extractOrPushError( const retentionPercentage = extractOrPushError(
maybeFromNullableResult(raw.retention_percentage_value, (value) => maybeFromNullableResult(raw.retention_percentage_value, (value) =>
ItemTaxPercentage.create({ value }) TaxPercentage.create({ value })
), ),
`items[${index}].retention_percentage_value`, `items[${index}].retention_percentage_value`,
errors errors
@ -263,7 +259,7 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
const { errors, index } = params as { const { errors, index } = params as {
index: number; index: number;
errors: ValidationErrorDetail[]; errors: ValidationErrorDetail[];
attributes: Partial<IIssuedInvoiceProps>; attributes: Partial<IIssuedInvoiceCreateProps>;
}; };
// 1) Valores escalares (atributos generales) // 1) Valores escalares (atributos generales)
@ -277,7 +273,8 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
} }
// 2) Construcción del elemento de dominio // 2) Construcción del elemento de dominio
const createResult = IssuedInvoiceItem.create( const itemId = attributes.itemId!;
const newItem = IssuedInvoiceItem.rehydrate(
{ {
description: attributes.description!, description: attributes.description!,
@ -314,18 +311,10 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
languageCode: attributes.languageCode!, languageCode: attributes.languageCode!,
currencyCode: attributes.currencyCode!, currencyCode: attributes.currencyCode!,
}, },
attributes.itemId itemId
); );
if (createResult.isFailure) { return Result.ok(newItem);
return Result.fail(
new ValidationErrorCollection("Invoice item entity creation failed", [
{ path: `items[${index}]`, message: createResult.error.message },
])
);
}
return createResult;
} }
public mapToPersistence( public mapToPersistence(
@ -364,18 +353,14 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
), ),
item_discount_percentage_scale: item_discount_percentage_scale:
maybeToNullable(source.itemDiscountPercentage, (v) => v.toPrimitive().scale) ?? maybeToNullable(source.itemDiscountPercentage, (v) => v.toPrimitive().scale) ??
ItemDiscountPercentage.DEFAULT_SCALE, DiscountPercentage.DEFAULT_SCALE,
item_discount_amount_value: source.itemDiscountAmount.toPrimitive().value, item_discount_amount_value: source.itemDiscountAmount.toPrimitive().value,
item_discount_amount_scale: source.itemDiscountAmount.toPrimitive().scale, item_discount_amount_scale: source.itemDiscountAmount.toPrimitive().scale,
global_discount_percentage_value: maybeToNullable( global_discount_percentage_value: source.globalDiscountPercentage.toPrimitive().value,
source.globalDiscountPercentage,
(v) => v.toPrimitive().value
),
global_discount_percentage_scale: global_discount_percentage_scale:
maybeToNullable(source.globalDiscountPercentage, (v) => v.toPrimitive().scale) ?? source.globalDiscountPercentage.toPrimitive().scale ?? DiscountPercentage.DEFAULT_SCALE,
ItemDiscountPercentage.DEFAULT_SCALE,
global_discount_amount_value: source.globalDiscountAmount.value, global_discount_amount_value: source.globalDiscountAmount.value,
global_discount_amount_scale: source.globalDiscountAmount.scale, global_discount_amount_scale: source.globalDiscountAmount.scale,

View File

@ -16,7 +16,7 @@ import {
import { Maybe, Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import { import {
type IIssuedInvoiceProps, type IIssuedInvoiceCreateProps,
InvoiceRecipient, InvoiceRecipient,
type IssuedInvoice, type IssuedInvoice,
} from "../../../../../../domain"; } from "../../../../../../domain";
@ -33,7 +33,7 @@ export class SequelizeIssuedInvoiceRecipientDomainMapper {
const { errors, attributes } = params as { const { errors, attributes } = params as {
errors: ValidationErrorDetail[]; errors: ValidationErrorDetail[];
attributes: Partial<IIssuedInvoiceProps>; attributes: Partial<IIssuedInvoiceCreateProps>;
}; };
const _name = source.customer_name!; const _name = source.customer_name!;

View File

@ -1,5 +1,10 @@
import type { JsonTaxCatalogProvider } from "@erp/core"; import type { JsonTaxCatalogProvider } from "@erp/core";
import { type MapperParamsType, SequelizeDomainMapper, TaxPercentage } from "@erp/core/api"; import {
DiscountPercentage,
type MapperParamsType,
SequelizeDomainMapper,
TaxPercentage,
} from "@erp/core/api";
import { import {
Percentage, Percentage,
UniqueID, UniqueID,
@ -14,7 +19,7 @@ import {
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { import {
type IIssuedInvoiceProps, type IIssuedInvoiceCreateProps,
InvoiceAmount, InvoiceAmount,
type IssuedInvoice, type IssuedInvoice,
IssuedInvoiceTax, IssuedInvoiceTax,
@ -64,7 +69,7 @@ export class SequelizeIssuedInvoiceTaxesDomainMapper extends SequelizeDomainMapp
const { errors, index, attributes } = params as { const { errors, index, attributes } = params as {
index: number; index: number;
errors: ValidationErrorDetail[]; errors: ValidationErrorDetail[];
attributes: Partial<IIssuedInvoiceProps>; attributes: Partial<IIssuedInvoiceCreateProps>;
}; };
const taxableAmount = extractOrPushError( const taxableAmount = extractOrPushError(
@ -78,9 +83,10 @@ export class SequelizeIssuedInvoiceTaxesDomainMapper extends SequelizeDomainMapp
const ivaCode = raw.iva_code; const ivaCode = raw.iva_code;
// Una issued invoice debe traer IVA
const ivaPercentage = extractOrPushError( const ivaPercentage = extractOrPushError(
TaxPercentage.create({ TaxPercentage.create({
value: raw.iva_percentage_value, value: Number(raw.iva_percentage_value),
}), }),
`taxes[${index}].iva_percentage_value`, `taxes[${index}].iva_percentage_value`,
errors errors
@ -210,7 +216,7 @@ export class SequelizeIssuedInvoiceTaxesDomainMapper extends SequelizeDomainMapp
rec_percentage_value: maybeToNullable(source.recPercentage, (v) => v.toPrimitive().value), rec_percentage_value: maybeToNullable(source.recPercentage, (v) => v.toPrimitive().value),
rec_percentage_scale: rec_percentage_scale:
maybeToNullable(source.recPercentage, (v) => v.toPrimitive().scale) ?? maybeToNullable(source.recPercentage, (v) => v.toPrimitive().scale) ??
ItemDiscountPercentage.DEFAULT_SCALE, DiscountPercentage.DEFAULT_SCALE,
rec_amount_value: source.recAmount.toPrimitive().value, rec_amount_value: source.recAmount.toPrimitive().value,
rec_amount_scale: source.recAmount.toPrimitive().scale ?? ItemAmount.DEFAULT_SCALE, rec_amount_scale: source.recAmount.toPrimitive().scale ?? ItemAmount.DEFAULT_SCALE,

View File

@ -12,7 +12,7 @@ import {
import { Maybe, Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import { import {
type IIssuedInvoiceProps, type IIssuedInvoiceCreateProps,
type IssuedInvoice, type IssuedInvoice,
VerifactuRecord, VerifactuRecord,
VerifactuRecordEstado, VerifactuRecordEstado,
@ -33,7 +33,7 @@ export class SequelizeIssuedInvoiceVerifactuDomainMapper extends SequelizeDomain
): Result<Maybe<VerifactuRecord>, Error> { ): Result<Maybe<VerifactuRecord>, Error> {
const { errors, attributes } = params as { const { errors, attributes } = params as {
errors: ValidationErrorDetail[]; errors: ValidationErrorDetail[];
attributes: Partial<IIssuedInvoiceProps>; attributes: Partial<IIssuedInvoiceCreateProps>;
}; };
if (!source) { if (!source) {

View File

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

View File

@ -0,0 +1,66 @@
import type { UniqueID } from "@repo/rdx-ddd";
import { type Maybe, Result } from "@repo/rdx-utils";
import { type Transaction, type WhereOptions, literal } from "sequelize";
import type { IIssuedInvoiceNumberGenerator } from "../../../../../application/issued-invoices";
import { InvoiceNumber, type InvoiceSerie } from "../../../../../domain";
import { CustomerInvoiceModel } from "../../../../common/persistence";
/**
* Generador de números de factura
*/
export class SequelizeIssuedInvoiceNumberGenerator implements IIssuedInvoiceNumberGenerator {
public async getNextForCompany(
companyId: UniqueID,
series: Maybe<InvoiceSerie>,
transaction: Transaction
): Promise<Result<InvoiceNumber, 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 = "0001"; // 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(4, "0");
}
const numberResult = InvoiceNumber.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

@ -23,13 +23,6 @@ type ProformaServicesContext = {
}; };
export type ProformaPublicServices = { export type ProformaPublicServices = {
createProforma: (
id: UniqueID,
props: IProformaCreatorParams["props"],
context: ProformaServicesContext
) => Promise<Result<Proforma, Error>>;
listProformas: (filters: unknown, context: unknown) => null;
getProformaById: ( getProformaById: (
id: UniqueID, id: UniqueID,
context: ProformaServicesContext context: ProformaServicesContext
@ -40,10 +33,14 @@ export type ProformaPublicServices = {
context: ProformaServicesContext context: ProformaServicesContext
) => Promise<Result<IProformaFullSnapshot, Error>>; ) => Promise<Result<IProformaFullSnapshot, Error>>;
generateProformaReport: (id: unknown, options: unknown, context: unknown) => null; createProforma: (
id: UniqueID,
props: IProformaCreatorParams["props"],
context: ProformaServicesContext
) => Promise<Result<Proforma, Error>>;
}; };
export function buildProformaServices( export function buildProformaPublicServices(
params: SetupParams, params: SetupParams,
deps: ProformasInternalDeps deps: ProformasInternalDeps
): ProformaPublicServices { ): ProformaPublicServices {
@ -78,7 +75,6 @@ export function buildProformaServices(
return Result.ok(createResult.data); return Result.ok(createResult.data);
}, },
listProformas: (filters, context) => null,
//internal.useCases.listProformas().execute(filters, context), //internal.useCases.listProformas().execute(filters, context),
getProformaById: async (id: UniqueID, context: ProformaServicesContext) => { getProformaById: async (id: UniqueID, context: ProformaServicesContext) => {
@ -105,7 +101,7 @@ export function buildProformaServices(
return Result.ok(fullSnapshot); return Result.ok(fullSnapshot);
}, },
generateProformaReport: (id, options, context) => null, //generateProformaReport: (id, options, context) => null,
//internal.useCases.reportProforma().execute(id, options, context), //internal.useCases.reportProforma().execute(id, options, context),
}; };
} }

View File

@ -3,15 +3,20 @@ import { type ModuleParams, buildCatalogs, buildTransactionManager } from "@erp/
import { import {
type CreateProformaUseCase, type CreateProformaUseCase,
type GetProformaByIdUseCase, type GetProformaByIdUseCase,
type IIssuedInvoicePublicServices,
type IssueProformaUseCase,
type ListProformasUseCase, type ListProformasUseCase,
type ReportProformaUseCase, type ReportProformaUseCase,
buildCreateProformaUseCase, buildCreateProformaUseCase,
buildGetProformaByIdUseCase, buildGetProformaByIdUseCase,
buildIssueProformaUseCase,
buildListProformasUseCase, buildListProformasUseCase,
buildProformaCreator, buildProformaCreator,
buildProformaFinder, buildProformaFinder,
buildProformaInputMappers, buildProformaInputMappers,
buildProformaIssuer,
buildProformaSnapshotBuilders, buildProformaSnapshotBuilders,
buildProformaToIssuedInvoicePropsConverter,
buildReportProformaUseCase, buildReportProformaUseCase,
} from "../../../application"; } from "../../../application";
@ -26,6 +31,9 @@ export type ProformasInternalDeps = {
getProformaById: () => GetProformaByIdUseCase; getProformaById: () => GetProformaByIdUseCase;
reportProforma: () => ReportProformaUseCase; reportProforma: () => ReportProformaUseCase;
createProforma: () => CreateProformaUseCase; createProforma: () => CreateProformaUseCase;
issueProforma: (publicServices: {
issuedInvoiceServices: IIssuedInvoicePublicServices;
}) => IssueProformaUseCase;
/* /*
updateProforma: () => UpdateProformaUseCase; updateProforma: () => UpdateProformaUseCase;
@ -44,12 +52,18 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
const persistenceMappers = buildProformaPersistenceMappers(catalogs); const persistenceMappers = buildProformaPersistenceMappers(catalogs);
const repository = buildProformaRepository({ database, mappers: persistenceMappers }); const repository = buildProformaRepository({ database, mappers: persistenceMappers });
const numberService = buildProformaNumberGenerator(); const proformaNumberService = buildProformaNumberGenerator();
// Application helpers // Application helpers
const inputMappers = buildProformaInputMappers(catalogs); const inputMappers = buildProformaInputMappers(catalogs);
const finder = buildProformaFinder(repository); const finder = buildProformaFinder(repository);
const creator = buildProformaCreator({ numberService, repository }); const creator = buildProformaCreator({ numberService: proformaNumberService, repository });
const proformaToIssuedInvoiceConverter = buildProformaToIssuedInvoicePropsConverter();
const issuer = buildProformaIssuer({
proformaConverter: proformaToIssuedInvoiceConverter,
repository,
});
const snapshotBuilders = buildProformaSnapshotBuilders(); const snapshotBuilders = buildProformaSnapshotBuilders();
const documentGeneratorPipeline = buildProformaDocumentService(params); const documentGeneratorPipeline = buildProformaDocumentService(params);
@ -87,6 +101,14 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
fullSnapshotBuilder: snapshotBuilders.full, fullSnapshotBuilder: snapshotBuilders.full,
transactionManager, transactionManager,
}), }),
issueProforma: (publicServices: { issuedInvoiceServices: IIssuedInvoicePublicServices }) =>
buildIssueProformaUseCase({
publicServices,
finder,
issuer,
transactionManager,
}),
}, },
}; };
} }

View File

@ -2,7 +2,7 @@
//export * from "./create-proforma.controller"; //export * from "./create-proforma.controller";
//export * from "./delete-proforma.controller"; //export * from "./delete-proforma.controller";
export * from "./get-proforma.controller"; export * from "./get-proforma.controller";
//export * from "./issue-proforma.controller"; export * from "./issue-proforma.controller";
export * from "./list-proformas.controller"; export * from "./list-proformas.controller";
export * from "./report-proforma.controller"; export * from "./report-proforma.controller";
//export * from "./update-proforma.controller"; //export * from "./update-proforma.controller";

View File

@ -5,7 +5,7 @@ import {
requireCompanyContextGuard, requireCompanyContextGuard,
} from "@erp/core/api"; } from "@erp/core/api";
import type { IssueProformaUseCase } from "../../../../application/index.ts"; import type { IssueProformaUseCase } from "../../../../application/proformas";
import { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts"; import { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts";
export class IssueProformaController extends ExpressController { export class IssueProformaController extends ExpressController {

View File

@ -1,4 +1,5 @@
import type { JsonTaxCatalogProvider } from "@erp/core"; import type { JsonTaxCatalogProvider } from "@erp/core";
import { DiscountPercentage } from "@erp/core/api";
import { import {
CurrencyCode, CurrencyCode,
DomainError, DomainError,
@ -57,7 +58,7 @@ export class CreateProformaRequestMapper {
try { try {
this.errors = []; this.errors = [];
const defaultStatus = InvoiceStatus.fromDraft(); const defaultStatus = InvoiceStatus.draft();
const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", this.errors); const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", this.errors);
@ -209,7 +210,7 @@ export class CreateProformaRequestMapper {
const discountPercentage = extractOrPushError( const discountPercentage = extractOrPushError(
maybeFromNullableResult(item.item_discount_percentage, (value) => maybeFromNullableResult(item.item_discount_percentage, (value) =>
ItemDiscountPercentage.create(value) DiscountPercentage.create(value)
), ),
"discount_percentage", "discount_percentage",
this.errors this.errors

View File

@ -1,9 +1,10 @@
import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth/api"; import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth/api";
import { type ModuleParams, type RequestWithAuth, validateRequest } from "@erp/core/api"; import { type RequestWithAuth, type StartParams, validateRequest } from "@erp/core/api";
import { type NextFunction, type Request, type Response, Router } from "express"; import { type NextFunction, type Request, type Response, Router } from "express";
import { import {
GetProformaController, GetProformaController,
IssueProformaController,
ListProformasController, ListProformasController,
type ProformasInternalDeps, type ProformasInternalDeps,
ReportProformaController, ReportProformaController,
@ -12,18 +13,30 @@ import {
import { import {
CreateProformaRequestSchema, CreateProformaRequestSchema,
GetProformaByIdRequestSchema, GetProformaByIdRequestSchema,
IssueProformaByIdParamsRequestSchema,
ListProformasRequestSchema, ListProformasRequestSchema,
ReportProformaByIdParamsRequestSchema, ReportProformaByIdParamsRequestSchema,
ReportProformaByIdQueryRequestSchema, ReportProformaByIdQueryRequestSchema,
} from "../../../../common"; } from "../../../../common";
import type { IIssuedInvoicePublicServices } from "../../../application";
import { CreateProformaController } from "./controllers/create-proforma.controller"; import { CreateProformaController } from "./controllers/create-proforma.controller";
export const proformasRouter = (params: ModuleParams, deps: ProformasInternalDeps) => { export const proformasRouter = (params: StartParams) => {
const { app, config } = params; const { app, config, getService, getInternal } = params;
const deps = getInternal<ProformasInternalDeps>("customer-invoices", "proformas");
const issuedInvoicesServices = getService<IIssuedInvoicePublicServices>("self:issuedInvoices");
const publicServices = {
issuedInvoiceServices: issuedInvoicesServices,
};
const router: Router = Router({ mergeParams: true }); const router: Router = Router({ mergeParams: true });
// ----------------------------------------------
if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "production") { if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "production") {
// 🔐 Autenticación + Tenancy para TODO el router // 🔐 Autenticación + Tenancy para TODO el router
router.use( router.use(
@ -131,18 +144,18 @@ export const proformasRouter = (params: ModuleParams, deps: ProformasInternalDep
} }
);*/ );*/
/*router.put( router.put(
"/:proforma_id/issue", "/:proforma_id/issue",
//checkTabContext, //checkTabContext,
validateRequest(IssueProformaByIdParamsRequestSchema, "params"), validateRequest(IssueProformaByIdParamsRequestSchema, "params"),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.issue_proforma(); const useCase = deps.useCases.issueProforma(publicServices);
const controller = new IssuedProformaController(useCase); const controller = new IssueProformaController(useCase);
return controller.execute(req, res, next); return controller.execute(req, res, next);
} }
);*/ );
app.use(`${config.server.apiBasePath}/proformas`, router); app.use(`${config.server.apiBasePath}/proformas`, router);
}; };

View File

@ -58,12 +58,6 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
const customerId = extractOrPushError(UniqueID.create(raw.customer_id), "customer_id", errors); const customerId = extractOrPushError(UniqueID.create(raw.customer_id), "customer_id", errors);
const proformaId = extractOrPushError(
maybeFromNullableResult(raw.proforma_id, (v) => UniqueID.create(v)),
"proforma_id",
errors
);
const status = extractOrPushError(InvoiceStatus.create(raw.status), "status", errors); const status = extractOrPushError(InvoiceStatus.create(raw.status), "status", errors);
const series = extractOrPushError( const series = extractOrPushError(
@ -160,7 +154,6 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
invoiceId, invoiceId,
companyId, companyId,
customerId, customerId,
proformaId,
status, status,
series, series,
invoiceNumber, invoiceNumber,
@ -244,8 +237,8 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
paymentMethod: attributes.paymentMethod!, paymentMethod: attributes.paymentMethod!,
}; };
const invoiceId = attributes.invoiceId!; const proformaId = attributes.invoiceId!;
const proforma = Proforma.rehydrate(invoiceProps, items, invoiceId); const proforma = Proforma.rehydrate(invoiceProps, items, proformaId);
return Result.ok(proforma); return Result.ok(proforma);
} catch (err: unknown) { } catch (err: unknown) {

View File

@ -75,7 +75,7 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
const quantity = extractOrPushError( const quantity = extractOrPushError(
maybeFromNullableResult(raw.quantity_value, (v) => ItemQuantity.create({ value: v })), maybeFromNullableResult(raw.quantity_value, (v) => ItemQuantity.create({ value: v })),
`items[${index}].quantity`, `items[${index}].quantity_value`,
errors errors
); );
@ -83,7 +83,7 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
maybeFromNullableResult(raw.unit_amount_value, (value) => maybeFromNullableResult(raw.unit_amount_value, (value) =>
ItemAmount.create({ value, currency_code: parent.currencyCode?.code }) ItemAmount.create({ value, currency_code: parent.currencyCode?.code })
), ),
`items[${index}].unit_amount`, `items[${index}].unit_amount_value`,
errors errors
); );
@ -163,26 +163,20 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
const itemId = attributes.itemId!; const itemId = attributes.itemId!;
const newItem = ProformaItem.rehydrate( const newItem = ProformaItem.rehydrate(
{ {
languageCode: attributes.languageCode!,
currencyCode: attributes.currencyCode!,
description: attributes.description!, description: attributes.description!,
quantity: attributes.quantity!, quantity: attributes.quantity!,
unitAmount: attributes.unitAmount!, unitAmount: attributes.unitAmount!,
itemDiscountPercentage: attributes.itemDiscountPercentage!, itemDiscountPercentage: attributes.itemDiscountPercentage!,
globalDiscountPercentage: attributes.globalDiscountPercentage!, globalDiscountPercentage: attributes.globalDiscountPercentage!,
taxes: taxesResult.data, taxes: taxesResult.data,
languageCode: attributes.languageCode!,
currencyCode: attributes.currencyCode!,
}, },
itemId itemId
); );
/*if (createResult.isFailure) {
return Result.fail(
new ValidationErrorCollection("Invoice item entity creation failed", [
{ path: `items[${index}]`, message: createResult.error.message },
])
);
}*/
return Result.ok(newItem); return Result.ok(newItem);
} }

View File

@ -31,13 +31,6 @@ export class SequelizeProformaRecipientDomainMapper {
parent: Partial<IProformaCreateProps>; parent: Partial<IProformaCreateProps>;
}; };
/* if (!source.current_customer) {
errors.push({
path: "current_customer",
message: "Current customer not included in query (SequelizeProformaRecipientDomainMapper)",
});
}
*/
const _name = source.current_customer.name; const _name = source.current_customer.name;
const _tin = source.current_customer.tin; const _tin = source.current_customer.tin;
const _street = source.current_customer.street; const _street = source.current_customer.street;

View File

@ -89,8 +89,8 @@ export const GetIssuedInvoiceByIdResponseSchema = z.object({
subtotal_amount: MoneySchema, subtotal_amount: MoneySchema,
discount_percentage: PercentageSchema, item_discount_percentage: PercentageSchema,
discount_amount: MoneySchema, item_discount_amount: MoneySchema,
global_discount_percentage: PercentageSchema, global_discount_percentage: PercentageSchema,
global_discount_amount: MoneySchema, global_discount_amount: MoneySchema,

View File

@ -83,8 +83,8 @@ export const GetProformaByIdResponseSchema = z.object({
subtotal_amount: MoneySchema, subtotal_amount: MoneySchema,
discount_percentage: PercentageSchema, item_discount_percentage: PercentageSchema,
discount_amount: MoneySchema, item_discount_amount: MoneySchema,
global_discount_percentage: PercentageSchema, global_discount_percentage: PercentageSchema,
global_discount_amount: MoneySchema, global_discount_amount: MoneySchema,

View File

@ -19,24 +19,24 @@ import * as React from "react";
import { useTranslation } from "../../../../../i18n"; import { useTranslation } from "../../../../../i18n";
import { import {
PROFORMA_STATUS_TRANSITIONS, PROFORMA_STATUS_TRANSITIONS,
type ProformaListRow,
type ProformaStatus, type ProformaStatus,
type ProformaSummaryData, } from "../../../../shared";
} from "../../../../types";
import { ProformaStatusBadge } from "../../components"; import { ProformaStatusBadge } from "../../components";
type GridActionHandlers = { type GridActionHandlers = {
onEditClick?: (proforma: ProformaSummaryData) => void; onEditClick?: (proforma: ProformaListRow) => void;
onIssueClick?: (proforma: ProformaSummaryData) => void; onIssueClick?: (proforma: ProformaListRow) => void;
onChangeStatusClick?: (proforma: ProformaSummaryData, nextStatus: string) => void; onChangeStatusClick?: (proforma: ProformaListRow, nextStatus: string) => void;
onDeleteClick?: (proforma: ProformaSummaryData) => void; onDeleteClick?: (proforma: ProformaListRow) => void;
}; };
export function useProformasGridColumns( export function useProformasGridColumns(
actionHandlers: GridActionHandlers = {} actionHandlers: GridActionHandlers = {}
): ColumnDef<ProformaSummaryData, unknown>[] { ): ColumnDef<ProformaListRow, unknown>[] {
const { t } = useTranslation(); const { t } = useTranslation();
return React.useMemo<ColumnDef<ProformaSummaryData, unknown>[]>( return React.useMemo<ColumnDef<ProformaListRow, unknown>[]>(
() => [ () => [
/*{ /*{
id: "select", id: "select",

View File

@ -3,11 +3,11 @@ import { cn } from "@repo/shadcn-ui/lib/utils";
import { useTranslation } from "../../../../i18n"; import { useTranslation } from "../../../../i18n";
import { import {
type ProformaStatus,
getProformaStatusButtonVariant, getProformaStatusButtonVariant,
getProformaStatusColor, getProformaStatusColor,
getProformaStatusIcon, getProformaStatusIcon,
} from "../../../types"; } from "../../../change-status/helpers";
import type { ProformaStatus } from "../../../shared";
export type ProformaStatusBadgeProps = { export type ProformaStatusBadgeProps = {
status: string | ProformaStatus; // permitir cualquier valor status: string | ProformaStatus; // permitir cualquier valor

View File

@ -1,165 +0,0 @@
// application/customer-application-service.ts
import type { Criteria } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd";
import { type Collection, Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import {
Customer,
type CustomerPatchProps,
type ICustomerCreateProps,
type ICustomerRepository,
} from "../domain";
import type { CustomerListDTO } from "../infrastructure";
export class CustomerApplicationService {
constructor(private readonly repository: ICustomerRepository) {}
/**
* Construye un nuevo agregado CustomerInvoice a partir de props validadas.
*
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
* @param props - Las propiedades ya validadas para crear el cliente.
* @param customerId - Identificador UUID del cliente (opcional).
* @returns Result<Customer, Error> - El agregado construido o un error si falla la creación.
*/
buildCustomerInCompany(
companyId: UniqueID,
props: Omit<ICustomerCreateProps, "companyId">,
customerId?: UniqueID
): Result<Customer, Error> {
return Customer.create({ ...props, companyId }, customerId);
}
/**
* Guarda un nuevo cliente y devuelve el cliente guardado.
*
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
* @param customer - El cliente a guardar.
* @param transaction - Transacción activa para la operación.
* @returns Result<Customer, Error> - El cliente guardado o un error si falla la operación.
*/
async createCustomerInCompany(
companyId: UniqueID,
customer: Customer,
transaction?: Transaction
): Promise<Result<Customer, Error>> {
const result = await this.repository.create(customer, transaction);
if (result.isFailure) return Result.fail(result.error);
return this.getCustomerByIdInCompany(companyId, customer.id, transaction);
}
/**
* Actualiza un cliente existente y devuelve el cliente actualizado.
*
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
* @param customer - El cliente a guardar.
* @param transaction - Transacción activa para la operación.
* @returns Result<Customer, Error> - El cliente guardado o un error si falla la operación.
*/
async updateCustomerInCompany(
companyId: UniqueID,
customer: Customer,
transaction?: Transaction
): Promise<Result<Customer, Error>> {
const result = await this.repository.update(customer, transaction);
if (result.isFailure) return Result.fail(result.error);
return this.getCustomerByIdInCompany(companyId, customer.id, transaction);
}
/**
* Actualiza parcialmente un cliente existente con nuevos datos.
* Solo en memoria. No lo guarda en el repositorio.
*
* @param companyId - Identificador de la empresa a la que pertenece el cliente.
* @param customerId - Identificador del cliente a actualizar.
* @param changes - Subconjunto de props válidas para aplicar.
* @param transaction - Transacción activa para la operación.
* @returns Result<Customer, Error> - Cliente actualizado o error.
*/
async patchCustomerByIdInCompany(
companyId: UniqueID,
customerId: UniqueID,
changes: CustomerPatchProps,
transaction?: Transaction
): Promise<Result<Customer, Error>> {
const customerResult = await this.getCustomerByIdInCompany(companyId, customerId, transaction);
if (customerResult.isFailure) {
return Result.fail(customerResult.error);
}
const updated = customerResult.data.update(changes);
if (updated.isFailure) {
return Result.fail(updated.error);
}
return Result.ok(updated.data);
}
/**
* Elimina (o marca como eliminado) un cliente según su ID.
*
* @param companyId - Identificador de la empresa a la que pertenece el cliente.
* @param customerId - Identificador UUID del cliente.
* @param transaction - Transacción activa para la operación.
* @returns Result<boolean, Error> - Resultado de la operación.
*/
async deleteCustomerByIdInCompany(
companyId: UniqueID,
customerId: UniqueID,
transaction?: Transaction
): Promise<Result<boolean, Error>> {
return this.repository.deleteByIdInCompany(companyId, customerId, transaction);
}
/**
*
* Comprueba si existe o no en persistencia un cliente con el ID proporcionado
*
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
* @param customerId - Identificador UUID del cliente
* @param transaction - Transacción activa para la operación.
* @returns Result<Boolean, Error> - Existe el cliente o no.
*/
async existsByIdInCompany(
companyId: UniqueID,
customerId: UniqueID,
transaction?: Transaction
): Promise<Result<boolean, Error>> {
return this.repository.existsByIdInCompany(companyId, customerId, transaction);
}
/**
* Recupera un cliente por su identificador único.
*
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
* @param customerId - Identificador UUID del cliente.
* @param transaction - Transacción activa para la operación.
* @returns Result<Customer, Error> - Cliente encontrado o error.
*/
async getCustomerByIdInCompany(
companyId: UniqueID,
customerId: UniqueID,
transaction?: Transaction
): Promise<Result<Customer, Error>> {
return this.repository.getByIdInCompany(companyId, customerId, transaction);
}
/**
* Obtiene una colección de clientes que cumplen con los filtros definidos en un objeto Criteria.
*
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
* @param criteria - Objeto con condiciones de filtro, paginación y orden.
* @param transaction - Transacción activa para la operación.
* @returns Result<Collection<Customer>, Error> - Colección de clientes o error.
*/
async findCustomerByCriteriaInCompany(
companyId: UniqueID,
criteria: Criteria,
transaction?: Transaction
): Promise<Result<Collection<CustomerListDTO>, Error>> {
return this.repository.findByCriteriaInCompany(companyId, criteria, transaction);
}
}

View File

@ -1,7 +1,6 @@
import { DuplicateEntityError } from "@erp/core/api"; import { DuplicateEntityError } from "@erp/core/api";
import type { UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import { Customer, type ICustomerCreateProps } from "../../domain"; import { Customer, type ICustomerCreateProps } from "../../domain";
import type { ICustomerRepository } from "../repositories"; import type { ICustomerRepository } from "../repositories";
@ -12,7 +11,7 @@ export interface ICustomerCreator {
companyId: UniqueID; companyId: UniqueID;
id: UniqueID; id: UniqueID;
props: ICustomerCreateProps; props: ICustomerCreateProps;
transaction: Transaction; unknown: unknown;
}): Promise<Result<Customer, Error>>; }): Promise<Result<Customer, Error>>;
} }
@ -31,16 +30,12 @@ export class CustomerCreator implements ICustomerCreator {
companyId: UniqueID; companyId: UniqueID;
id: UniqueID; id: UniqueID;
props: ICustomerCreateProps; props: ICustomerCreateProps;
transaction: Transaction; unknown: unknown;
}): Promise<Result<Customer, Error>> { }): Promise<Result<Customer, Error>> {
const { companyId, id, props, transaction } = params; const { companyId, id, props, unknown } = params;
// 1. Verificar unicidad // 1. Verificar unicidad
const spec = new CustomerNotExistsInCompanySpecification( const spec = new CustomerNotExistsInCompanySpecification(this.repository, companyId, unknown);
this.repository,
companyId,
transaction
);
const isNew = await spec.isSatisfiedBy(id); const isNew = await spec.isSatisfiedBy(id);
@ -64,7 +59,7 @@ export class CustomerCreator implements ICustomerCreator {
const newCustomer = createResult.data; const newCustomer = createResult.data;
// 3. Persistir agregado // 3. Persistir agregado
const saveResult = await this.repository.create(newCustomer, transaction); const saveResult = await this.repository.create(newCustomer, unknown);
if (saveResult.isFailure) { if (saveResult.isFailure) {
return Result.fail(saveResult.error); return Result.fail(saveResult.error);

View File

@ -1,7 +1,6 @@
import type { Criteria } from "@repo/rdx-criteria/server"; import type { Criteria } from "@repo/rdx-criteria/server";
import type { TINNumber, UniqueID } from "@repo/rdx-ddd"; import type { TINNumber, UniqueID } from "@repo/rdx-ddd";
import type { Collection, Result } from "@repo/rdx-utils"; import type { Collection, Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type { Customer } from "../../domain"; import type { Customer } from "../../domain";
import type { CustomerSummary } from "../models"; import type { CustomerSummary } from "../models";
@ -11,25 +10,25 @@ export interface ICustomerFinder {
findCustomerById( findCustomerById(
companyId: UniqueID, companyId: UniqueID,
customerId: UniqueID, customerId: UniqueID,
transaction?: Transaction unknown?: unknown
): Promise<Result<Customer, Error>>; ): Promise<Result<Customer, Error>>;
findCustomerByTIN( findCustomerByTIN(
companyId: UniqueID, companyId: UniqueID,
tin: TINNumber, tin: TINNumber,
transaction?: Transaction unknown?: unknown
): Promise<Result<Customer, Error>>; ): Promise<Result<Customer, Error>>;
customerExists( customerExists(
companyId: UniqueID, companyId: UniqueID,
invoiceId: UniqueID, invoiceId: UniqueID,
transaction?: Transaction unknown?: unknown
): Promise<Result<boolean, Error>>; ): Promise<Result<boolean, Error>>;
findCustomersByCriteria( findCustomersByCriteria(
companyId: UniqueID, companyId: UniqueID,
criteria: Criteria, criteria: Criteria,
transaction?: Transaction unknown?: unknown
): Promise<Result<Collection<CustomerSummary>, Error>>; ): Promise<Result<Collection<CustomerSummary>, Error>>;
} }
@ -39,32 +38,32 @@ export class CustomerFinder implements ICustomerFinder {
async findCustomerById( async findCustomerById(
companyId: UniqueID, companyId: UniqueID,
customerId: UniqueID, customerId: UniqueID,
transaction?: Transaction unknown?: unknown
): Promise<Result<Customer, Error>> { ): Promise<Result<Customer, Error>> {
return this.repository.getByIdInCompany(companyId, customerId, transaction); return this.repository.getByIdInCompany(companyId, customerId, unknown);
} }
findCustomerByTIN( findCustomerByTIN(
companyId: UniqueID, companyId: UniqueID,
tin: TINNumber, tin: TINNumber,
transaction?: Transaction unknown?: unknown
): Promise<Result<Customer, Error>> { ): Promise<Result<Customer, Error>> {
return this.repository.getByTINInCompany(companyId, tin, transaction); return this.repository.getByTINInCompany(companyId, tin, unknown);
} }
async customerExists( async customerExists(
companyId: UniqueID, companyId: UniqueID,
customerId: UniqueID, customerId: UniqueID,
transaction?: Transaction unknown?: unknown
): Promise<Result<boolean, Error>> { ): Promise<Result<boolean, Error>> {
return this.repository.existsByIdInCompany(companyId, customerId, transaction); return this.repository.existsByIdInCompany(companyId, customerId, unknown);
} }
async findCustomersByCriteria( async findCustomersByCriteria(
companyId: UniqueID, companyId: UniqueID,
criteria: Criteria, criteria: Criteria,
transaction?: Transaction unknown?: unknown
): Promise<Result<Collection<CustomerSummary>, Error>> { ): Promise<Result<Collection<CustomerSummary>, Error>> {
return this.repository.findByCriteriaInCompany(companyId, criteria, transaction); return this.repository.findByCriteriaInCompany(companyId, criteria, unknown);
} }
} }

View File

@ -0,0 +1,22 @@
import type { TINNumber, UniqueID } from "@repo/rdx-ddd";
import type { Result } from "@repo/rdx-utils";
import type { Customer, ICustomerCreateProps } from "../../domain";
export interface ICustomerServicesContext {
transaction: unknown;
companyId: UniqueID;
}
export interface ICustomerPublicServices {
findCustomerByTIN: (
tin: TINNumber,
context: ICustomerServicesContext
) => Promise<Result<Customer, Error>>;
createCustomer: (
id: UniqueID,
props: ICustomerCreateProps,
context: ICustomerServicesContext
) => Promise<Result<Customer, Error>>;
}

View File

@ -1,6 +1,5 @@
import type { UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type { Customer, CustomerPatchProps } from "../../domain"; import type { Customer, CustomerPatchProps } from "../../domain";
import type { ICustomerRepository } from "../repositories"; import type { ICustomerRepository } from "../repositories";
@ -10,7 +9,7 @@ export interface ICustomerUpdater {
companyId: UniqueID; companyId: UniqueID;
id: UniqueID; id: UniqueID;
props: CustomerPatchProps; props: CustomerPatchProps;
transaction: Transaction; transaction: unknown;
}): Promise<Result<Customer, Error>>; }): Promise<Result<Customer, Error>>;
} }
@ -29,7 +28,7 @@ export class CustomerUpdater implements ICustomerUpdater {
companyId: UniqueID; companyId: UniqueID;
id: UniqueID; id: UniqueID;
props: CustomerPatchProps; props: CustomerPatchProps;
transaction: Transaction; transaction: unknown;
}): Promise<Result<Customer, Error>> { }): Promise<Result<Customer, Error>> {
const { companyId, id, props, transaction } = params; const { companyId, id, props, transaction } = params;

View File

@ -1,3 +1,4 @@
export * from "./customer-creator"; export * from "./customer-creator";
export * from "./customer-finder"; export * from "./customer-finder";
export * from "./customer-public-services.interface";
export * from "./customer-updater"; export * from "./customer-updater";

View File

@ -1,5 +1,4 @@
import { CompositeSpecification, type UniqueID } from "@repo/rdx-ddd"; import { CompositeSpecification, type UniqueID } from "@repo/rdx-ddd";
import type { Transaction } from "sequelize";
import type { ICustomerRepository } from "../../application"; import type { ICustomerRepository } from "../../application";
import { logger } from "../../helpers"; import { logger } from "../../helpers";
@ -8,7 +7,7 @@ export class CustomerNotExistsInCompanySpecification extends CompositeSpecificat
constructor( constructor(
private readonly repository: ICustomerRepository, private readonly repository: ICustomerRepository,
private readonly companyId: UniqueID, private readonly companyId: UniqueID,
private readonly transaction?: Transaction private readonly transaction?: unknown
) { ) {
super(); super();
} }

View File

@ -1,7 +1,6 @@
import type { ITransactionManager } from "@erp/core/api"; import type { ITransactionManager } from "@erp/core/api";
import type { UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type { CreateCustomerRequestDTO } from "../../../common"; import type { CreateCustomerRequestDTO } from "../../../common";
import type { ICreateCustomerInputMapper } from "../mappers"; import type { ICreateCustomerInputMapper } from "../mappers";
@ -43,7 +42,7 @@ export class CreateCustomerUseCase {
const { props, id } = mappedPropsResult.data; const { props, id } = mappedPropsResult.data;
return this.transactionManager.complete(async (transaction: Transaction) => { return this.transactionManager.complete(async (transaction: unknown) => {
try { try {
const createResult = await this.creator.create({ companyId, id, props, transaction }); const createResult = await this.creator.create({ companyId, id, props, transaction });

View File

@ -2,7 +2,6 @@ import type { ITransactionManager } from "@erp/core/api";
import type { Criteria } from "@repo/rdx-criteria/server"; import type { Criteria } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type { ICustomerFinder } from "../services"; import type { ICustomerFinder } from "../services";
import type { ICustomerSummarySnapshotBuilder } from "../snapshot-builders/summary"; import type { ICustomerSummarySnapshotBuilder } from "../snapshot-builders/summary";
@ -22,7 +21,7 @@ export class ListCustomersUseCase {
public execute(params: ListCustomersUseCaseInput) { public execute(params: ListCustomersUseCaseInput) {
const { criteria, companyId } = params; const { criteria, companyId } = params;
return this.transactionManager.complete(async (transaction: Transaction) => { return this.transactionManager.complete(async (transaction: unknown) => {
try { try {
const result = await this.finder.findCustomersByCriteria(companyId, criteria, transaction); const result = await this.finder.findCustomersByCriteria(companyId, criteria, transaction);

View File

@ -1,7 +1,6 @@
import type { ITransactionManager } from "@erp/core/api"; import type { ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type { UpdateCustomerByIdRequestDTO } from "../../../../common/dto"; import type { UpdateCustomerByIdRequestDTO } from "../../../../common/dto";
import type { CustomerPatchProps } from "../../../domain"; import type { CustomerPatchProps } from "../../../domain";
@ -52,7 +51,7 @@ export class UpdateCustomerUseCase {
const patchProps: CustomerPatchProps = patchPropsResult.data; const patchProps: CustomerPatchProps = patchPropsResult.data;
return this.transactionManager.complete(async (transaction: Transaction) => { return this.transactionManager.complete(async (transaction: unknown) => {
try { try {
const updateResult = await this.updater.update({ const updateResult = await this.updater.update({
companyId, companyId,

View File

@ -3,7 +3,7 @@ import type { IModuleServer } from "@erp/core/api";
import { type CustomerPublicServices, customersRouter, models } from "./infrastructure"; import { type CustomerPublicServices, customersRouter, models } from "./infrastructure";
import { import {
type CustomersInternalDeps, type CustomersInternalDeps,
buildCustomerServices, buildCustomerPublicServices,
buildCustomersDependencies, buildCustomersDependencies,
} from "./infrastructure/di"; } from "./infrastructure/di";
@ -30,7 +30,7 @@ export const customersAPIModule: IModuleServer = {
const internal = buildCustomersDependencies(params); const internal = buildCustomersDependencies(params);
// 2) Servicios públicos (Application Services) // 2) Servicios públicos (Application Services)
const customersServices: CustomerPublicServices = buildCustomerServices(params, internal); const customersServices: CustomerPublicServices = buildCustomerPublicServices(params, internal);
logger.info("🚀 Customers module dependencies registered", { logger.info("🚀 Customers module dependencies registered", {
label: this.name, label: this.name,

View File

@ -1,38 +1,23 @@
import { type SetupParams, buildCatalogs } from "@erp/core/api"; import { type SetupParams, buildCatalogs } from "@erp/core/api";
import type { TINNumber, UniqueID } from "@repo/rdx-ddd"; import type { TINNumber, UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import { buildCustomerCreator, buildCustomerFinder } from "../../application"; import {
import type { Customer, ICustomerCreateProps } from "../../domain"; type ICustomerPublicServices,
type ICustomerServicesContext,
buildCustomerCreator,
buildCustomerFinder,
} from "../../application";
import type { ICustomerCreateProps } from "../../domain";
import { buildCustomerPersistenceMappers } from "./customer-persistence-mappers.di"; import { buildCustomerPersistenceMappers } from "./customer-persistence-mappers.di";
import { buildCustomerRepository } from "./customer-repositories.di"; import { buildCustomerRepository } from "./customer-repositories.di";
import type { CustomersInternalDeps } from "./customers.di"; import type { CustomersInternalDeps } from "./customers.di";
type CustomerServicesContext = { export function buildCustomerPublicServices(
transaction: Transaction;
companyId: UniqueID;
};
export type CustomerPublicServices = {
//listCustomers: (filters: unknown, context: unknown) => null;
findCustomerByTIN: (
tin: TINNumber,
context: CustomerServicesContext
) => Promise<Result<Customer, Error>>;
createCustomer: (
id: UniqueID,
props: ICustomerCreateProps,
context: CustomerServicesContext
) => Promise<Result<Customer, Error>>;
//generateCustomerReport: (id: unknown, options: unknown, context: unknown) => null;
};
export function buildCustomerServices(
params: SetupParams, params: SetupParams,
deps: CustomersInternalDeps deps: CustomersInternalDeps
): CustomerPublicServices { ): ICustomerPublicServices {
const { database } = params; const { database } = params;
const catalogs = buildCatalogs(); const catalogs = buildCatalogs();
@ -44,7 +29,7 @@ export function buildCustomerServices(
const creator = buildCustomerCreator({ repository }); const creator = buildCustomerCreator({ repository });
return { return {
findCustomerByTIN: async (tin: TINNumber, context: CustomerServicesContext) => { findCustomerByTIN: async (tin: TINNumber, context: ICustomerServicesContext) => {
const { companyId, transaction } = context; const { companyId, transaction } = context;
const customerResult = await finder.findCustomerByTIN(companyId, tin, transaction); const customerResult = await finder.findCustomerByTIN(companyId, tin, transaction);
@ -59,7 +44,7 @@ export function buildCustomerServices(
createCustomer: async ( createCustomer: async (
id: UniqueID, id: UniqueID,
props: ICustomerCreateProps, props: ICustomerCreateProps,
context: CustomerServicesContext context: ICustomerServicesContext
) => { ) => {
const { companyId, transaction } = context; const { companyId, transaction } = context;

Some files were not shown because too many files have changed in this diff Show More