.
This commit is contained in:
parent
3a61d726f8
commit
5d59598106
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@ -5,6 +5,7 @@
|
||||
"cweijan.vscode-mysql-client2",
|
||||
"ms-vscode.vscode-json",
|
||||
"formulahendry.auto-rename-tag",
|
||||
"cweijan.dbclient-jdbc"
|
||||
"cweijan.dbclient-jdbc",
|
||||
"pkief.material-icon-theme"
|
||||
]
|
||||
}
|
||||
|
||||
33
.vscode/settings.json
vendored
33
.vscode/settings.json
vendored
@ -1,20 +1,19 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"tsconfig.json": "jsonc",
|
||||
"typescript-config/*.json": "jsonc",
|
||||
"*.css": "tailwindcss"
|
||||
},
|
||||
// Font Family
|
||||
"editor.fontFamily": "'Fira Code', 'Cascadia Code', 'Consolas'",
|
||||
|
||||
// Enable Font Ligatures
|
||||
"editor.fontLigatures": true,
|
||||
|
||||
// Javascript and TypeScript settings
|
||||
"javascript.suggest.enabled": true,
|
||||
"javascript.suggest.autoImports": true,
|
||||
"javascript.preferences.importModuleSpecifier": "shortest",
|
||||
"js/ts.suggest.enabled": true,
|
||||
"js/ts.suggest.autoImports": true,
|
||||
"js/ts.preferences.importModuleSpecifier": "shortest",
|
||||
|
||||
"typescript.suggest.enabled": true,
|
||||
"typescript.suggest.completeFunctionCalls": true,
|
||||
"typescript.suggest.includeAutomaticOptionalChainCompletions": true,
|
||||
"typescript.suggestionActions.enabled": true,
|
||||
"typescript.autoClosingTags": true,
|
||||
"js/ts.suggest.completeFunctionCalls": true,
|
||||
"js/ts.suggest.includeAutomaticOptionalChainCompletions": true,
|
||||
"js/ts.suggestionActions.enabled": true,
|
||||
"js/ts.autoClosingTags.enabled": true,
|
||||
|
||||
"editor.quickSuggestions": {
|
||||
"strings": "on"
|
||||
@ -54,11 +53,5 @@
|
||||
// other vscode settings
|
||||
"[sql]": {
|
||||
"editor.defaultFormatter": "cweijan.vscode-mysql-client2"
|
||||
}, // <- your root font size here
|
||||
|
||||
"invisibleAiChartDetector.watermark.includeSpaceFamily": true,
|
||||
"invisibleAiChartDetector.watermark.includeUnicodeCf": true,
|
||||
"invisibleAiChartDetector.doubleBlankThreshold": 2,
|
||||
"invisibleAiChartDetector.replace.format": "unicode",
|
||||
"invisibleAiChartDetector.clean.replaceSpaceLikesToAscii": true
|
||||
} // <- your root font size here
|
||||
}
|
||||
|
||||
@ -154,7 +154,12 @@ async function setupModule(name: string, params: ModuleParams, stack: string[])
|
||||
function makeGetService(moduleName: string, pkg: IModuleServer) {
|
||||
return <T>(serviceName: string): T => {
|
||||
const [serviceModule] = serviceName.split(":");
|
||||
trackDependencyUse(moduleName, serviceModule);
|
||||
|
||||
// No registrar dependencias para modulos
|
||||
// que usan sus propios servicios.
|
||||
if (serviceModule !== "self") {
|
||||
trackDependencyUse(moduleName, serviceModule);
|
||||
}
|
||||
|
||||
// IMPORTANTE: devolver el valor
|
||||
return getServiceScoped<T>(moduleName, pkg.dependencies ?? [], serviceName);
|
||||
@ -213,6 +218,8 @@ function validateModuleDependencies() {
|
||||
const declared = new Set(pkg.dependencies ?? []);
|
||||
const used = usedDependenciesByModule.get(moduleName) ?? new Set<string>();
|
||||
|
||||
console.log(declared, used);
|
||||
|
||||
// ❌ usadas pero no declaradas
|
||||
const undeclaredUsed = [...used].filter((d) => !declared.has(d));
|
||||
|
||||
|
||||
@ -12,15 +12,25 @@ export function registerService(name: string, api: any) {
|
||||
|
||||
/**
|
||||
* Recupera un servicio registrado bajo un "scope".
|
||||
*
|
||||
* getService("customers:repository")
|
||||
* Debe declarar: dependencies: ["customers"]
|
||||
*
|
||||
* El "scope" puede ser "self" para recuperar
|
||||
* los servicios propios registrados.
|
||||
*
|
||||
* getService("self:repository")
|
||||
*/
|
||||
export function getServiceScoped<T = any>(
|
||||
requesterModule: string,
|
||||
allowedDeps: readonly string[],
|
||||
name: string
|
||||
): T {
|
||||
const [serviceModule] = name.split(":");
|
||||
const [serviceModule, ...key] = name.split(":");
|
||||
|
||||
if (serviceModule === "self") {
|
||||
return getService<T>(`${requesterModule}:${key.join(":")}`);
|
||||
}
|
||||
|
||||
if (!allowedDeps.includes(serviceModule)) {
|
||||
throw new Error(
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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" },
|
||||
])
|
||||
);*/
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
export * from "./issued-invoice-creator.di";
|
||||
export * from "./issued-invoice-finder.di";
|
||||
export * from "./issued-invoice-snapshot-builders.di";
|
||||
export * from "./issued-invoice-use-cases.di";
|
||||
export * from "./proforma-to-issued-invoice-props-converter.di";
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
import {
|
||||
type IProformaToIssuedInvoiceConverter,
|
||||
ProformaToIssuedInvoiceConverter,
|
||||
} from "../services";
|
||||
|
||||
export function buildProformaToIssuedInvoicePropsConverter(): IProformaToIssuedInvoiceConverter {
|
||||
return new ProformaToIssuedInvoiceConverter();
|
||||
}
|
||||
@ -1,5 +1,8 @@
|
||||
export * from "./issued-invoice-creator";
|
||||
export * from "./issued-invoice-document-generator.interface";
|
||||
export * from "./issued-invoice-document-metadata-factory";
|
||||
export * from "./issued-invoice-document-properties-factory";
|
||||
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";
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import type { DocumentGenerationService } from "@erp/core/api";
|
||||
|
||||
import type { IssuedInvoiceReportSnapshot } from "../application-models";
|
||||
import type { IIssuedInvoiceReportSnapshot } from "../snapshot-builders/report";
|
||||
|
||||
export interface IssuedInvoiceDocumentGeneratorService
|
||||
extends DocumentGenerationService<IssuedInvoiceReportSnapshot> {}
|
||||
extends DocumentGenerationService<IIssuedInvoiceReportSnapshot> {}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
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.
|
||||
@ -10,9 +10,9 @@ import type { IssuedInvoiceReportSnapshot } from "../application-models";
|
||||
* - Sin IO
|
||||
*/
|
||||
export class IssuedInvoiceDocumentMetadataFactory
|
||||
implements IDocumentMetadataFactory<IssuedInvoiceReportSnapshot>
|
||||
implements IDocumentMetadataFactory<IIssuedInvoiceReportSnapshot>
|
||||
{
|
||||
build(snapshot: IssuedInvoiceReportSnapshot): IDocumentMetadata {
|
||||
build(snapshot: IIssuedInvoiceReportSnapshot): IDocumentMetadata {
|
||||
if (!snapshot.id) {
|
||||
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
|
||||
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
|
||||
return [
|
||||
"issued-invoice",
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
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 type { IssuedInvoice } from "../../../domain";
|
||||
import type { IssuedInvoiceSummary } from "../models";
|
||||
@ -11,19 +10,19 @@ export interface IIssuedInvoiceFinder {
|
||||
findIssuedInvoiceById(
|
||||
companyId: UniqueID,
|
||||
invoiceId: UniqueID,
|
||||
transaction?: Transaction
|
||||
transaction?: unknown
|
||||
): Promise<Result<IssuedInvoice, Error>>;
|
||||
|
||||
issuedInvoiceExists(
|
||||
companyId: UniqueID,
|
||||
invoiceId: UniqueID,
|
||||
transaction?: Transaction
|
||||
transaction?: unknown
|
||||
): Promise<Result<boolean, Error>>;
|
||||
|
||||
findIssuedInvoicesByCriteria(
|
||||
companyId: UniqueID,
|
||||
criteria: Criteria,
|
||||
transaction?: Transaction
|
||||
transaction?: unknown
|
||||
): Promise<Result<Collection<IssuedInvoiceSummary>, Error>>;
|
||||
}
|
||||
|
||||
@ -33,7 +32,7 @@ export class IssuedInvoiceFinder implements IIssuedInvoiceFinder {
|
||||
async findIssuedInvoiceById(
|
||||
companyId: UniqueID,
|
||||
invoiceId: UniqueID,
|
||||
transaction?: Transaction
|
||||
transaction?: unknown
|
||||
): Promise<Result<IssuedInvoice, Error>> {
|
||||
return this.repository.getByIdInCompany(companyId, invoiceId, transaction);
|
||||
}
|
||||
@ -41,7 +40,7 @@ export class IssuedInvoiceFinder implements IIssuedInvoiceFinder {
|
||||
async issuedInvoiceExists(
|
||||
companyId: UniqueID,
|
||||
invoiceId: UniqueID,
|
||||
transaction?: Transaction
|
||||
transaction?: unknown
|
||||
): Promise<Result<boolean, Error>> {
|
||||
return this.repository.existsByIdInCompany(companyId, invoiceId, transaction);
|
||||
}
|
||||
@ -49,7 +48,7 @@ export class IssuedInvoiceFinder implements IIssuedInvoiceFinder {
|
||||
async findIssuedInvoicesByCriteria(
|
||||
companyId: UniqueID,
|
||||
criteria: Criteria,
|
||||
transaction?: Transaction
|
||||
transaction?: unknown
|
||||
): Promise<Result<Collection<IssuedInvoiceSummary>, Error>> {
|
||||
return this.repository.findByCriteriaInCompany(companyId, criteria, transaction);
|
||||
}
|
||||
|
||||
@ -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>>;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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>>;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./full";
|
||||
export * from "./report";
|
||||
export * from "./summary";
|
||||
|
||||
@ -27,10 +27,10 @@ export class IssuedInvoiceReportItemSnapshotBuilder
|
||||
quantity: QuantityDTOHelper.format(item.quantity, locale, { minimumFractionDigits: 0 }),
|
||||
unit_amount: MoneyDTOHelper.format(item.unit_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,
|
||||
}),
|
||||
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),
|
||||
taxes_amount: MoneyDTOHelper.format(item.taxes_amount, locale, moneyOptions),
|
||||
total_amount: MoneyDTOHelper.format(item.total_amount, locale, moneyOptions),
|
||||
|
||||
@ -2,7 +2,6 @@ import type { ITransactionManager } from "@erp/core/api";
|
||||
import type { Criteria } from "@repo/rdx-criteria/server";
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import type { Transaction } from "sequelize";
|
||||
|
||||
import type { IIssuedInvoiceFinder } from "../services";
|
||||
import type { IIssuedInvoiceSummarySnapshotBuilder } from "../snapshot-builders";
|
||||
@ -22,7 +21,7 @@ export class ListIssuedInvoicesUseCase {
|
||||
public execute(params: ListIssuedInvoicesUseCaseInput) {
|
||||
const { criteria, companyId } = params;
|
||||
|
||||
return this.transactionManager.complete(async (transaction: Transaction) => {
|
||||
return this.transactionManager.complete(async (transaction: unknown) => {
|
||||
try {
|
||||
const result = await this.finder.findIssuedInvoicesByCriteria(
|
||||
companyId,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
export * from "./proforma-creator.di";
|
||||
export * from "./proforma-finder.di";
|
||||
export * from "./proforma-input-mappers.di";
|
||||
export * from "./proforma-issuer.di";
|
||||
export * from "./proforma-snapshot-builders.di";
|
||||
export * from "./proforma-use-cases.di";
|
||||
|
||||
@ -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,
|
||||
});
|
||||
};
|
||||
@ -1,9 +1,11 @@
|
||||
import type { ITransactionManager } from "@erp/core/api";
|
||||
|
||||
import type { IIssuedInvoicePublicServices } from "../../issued-invoices";
|
||||
import type { ICreateProformaInputMapper } from "../mappers";
|
||||
import type {
|
||||
IProformaCreator,
|
||||
IProformaFinder,
|
||||
IProformaIssuer,
|
||||
ProformaDocumentGeneratorService,
|
||||
} from "../services";
|
||||
import type {
|
||||
@ -11,9 +13,13 @@ import type {
|
||||
IProformaSummarySnapshotBuilder,
|
||||
} from "../snapshot-builders";
|
||||
import type { IProformaFullSnapshotBuilder } from "../snapshot-builders/full";
|
||||
import { GetProformaByIdUseCase, ListProformasUseCase, ReportProformaUseCase } from "../use-cases";
|
||||
import { CreateProformaUseCase } from "../use-cases/create-proforma";
|
||||
import { IssueProformaUseCase } from "../use-cases/issue-proforma.use-case";
|
||||
import {
|
||||
CreateProformaUseCase,
|
||||
GetProformaByIdUseCase,
|
||||
IssueProformaUseCase,
|
||||
ListProformasUseCase,
|
||||
ReportProformaUseCase,
|
||||
} from "../use-cases";
|
||||
|
||||
export function buildGetProformaByIdUseCase(deps: {
|
||||
finder: IProformaFinder;
|
||||
@ -65,8 +71,26 @@ export function buildCreateProformaUseCase(deps: {
|
||||
});
|
||||
}
|
||||
|
||||
export function buildIssueProformaUseCase(deps: { finder: IProformaFinder }) {
|
||||
return new IssueProformaUseCase(deps.finder);
|
||||
export function buildIssueProformaUseCase(deps: {
|
||||
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: {
|
||||
|
||||
@ -69,7 +69,7 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
|
||||
const { companyId } = params;
|
||||
|
||||
try {
|
||||
const defaultStatus = InvoiceStatus.fromDraft();
|
||||
const defaultStatus = InvoiceStatus.draft();
|
||||
|
||||
const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
|
||||
|
||||
|
||||
@ -5,3 +5,4 @@ export * from "./proforma-document-properties-factory";
|
||||
export * from "./proforma-finder";
|
||||
export * from "./proforma-issuer";
|
||||
export * from "./proforma-number-generator.interface";
|
||||
export * from "./proforma-public-services.interface";
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import type { Transaction } from "sequelize";
|
||||
|
||||
import { type IProformaCreateProps, Proforma } from "../../../domain";
|
||||
import type { IProformaRepository } from "../repositories";
|
||||
@ -11,7 +10,7 @@ export interface IProformaCreatorParams {
|
||||
companyId: UniqueID;
|
||||
id: UniqueID;
|
||||
props: Omit<IProformaCreateProps, "invoiceNumber">;
|
||||
transaction: Transaction;
|
||||
transaction: unknown;
|
||||
}
|
||||
|
||||
export interface IProformaCreator {
|
||||
@ -32,12 +31,7 @@ export class ProformaCreator implements IProformaCreator {
|
||||
this.repository = deps.repository;
|
||||
}
|
||||
|
||||
async create(params: {
|
||||
companyId: UniqueID;
|
||||
id: UniqueID;
|
||||
props: IProformaCreateProps;
|
||||
transaction: Transaction;
|
||||
}): Promise<Result<Proforma, Error>> {
|
||||
async create(params: IProformaCreatorParams): Promise<Result<Proforma, Error>> {
|
||||
const { companyId, id, props, transaction } = params;
|
||||
|
||||
// 1. Obtener siguiente número
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
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 type { Proforma } from "../../../domain";
|
||||
import type { ProformaSummary } from "../models";
|
||||
@ -11,19 +10,19 @@ export interface IProformaFinder {
|
||||
findProformaById(
|
||||
companyId: UniqueID,
|
||||
invoiceId: UniqueID,
|
||||
transaction?: Transaction
|
||||
transaction?: unknown
|
||||
): Promise<Result<Proforma, Error>>;
|
||||
|
||||
proformaExists(
|
||||
companyId: UniqueID,
|
||||
invoiceId: UniqueID,
|
||||
transaction?: Transaction
|
||||
transaction?: unknown
|
||||
): Promise<Result<boolean, Error>>;
|
||||
|
||||
findProformasByCriteria(
|
||||
companyId: UniqueID,
|
||||
criteria: Criteria,
|
||||
transaction?: Transaction
|
||||
transaction?: unknown
|
||||
): Promise<Result<Collection<ProformaSummary>, Error>>;
|
||||
}
|
||||
|
||||
@ -33,7 +32,7 @@ export class ProformaFinder implements IProformaFinder {
|
||||
async findProformaById(
|
||||
companyId: UniqueID,
|
||||
proformaId: UniqueID,
|
||||
transaction?: Transaction
|
||||
transaction?: unknown
|
||||
): Promise<Result<Proforma, Error>> {
|
||||
return this.repository.getByIdInCompany(companyId, proformaId, transaction);
|
||||
}
|
||||
@ -41,7 +40,7 @@ export class ProformaFinder implements IProformaFinder {
|
||||
async proformaExists(
|
||||
companyId: UniqueID,
|
||||
proformaId: UniqueID,
|
||||
transaction?: Transaction
|
||||
transaction?: unknown
|
||||
): Promise<Result<boolean, Error>> {
|
||||
return this.repository.existsByIdInCompany(companyId, proformaId, transaction);
|
||||
}
|
||||
@ -49,7 +48,7 @@ export class ProformaFinder implements IProformaFinder {
|
||||
async findProformasByCriteria(
|
||||
companyId: UniqueID,
|
||||
criteria: Criteria,
|
||||
transaction?: Transaction
|
||||
transaction?: unknown
|
||||
): Promise<Result<Collection<ProformaSummary>, Error>> {
|
||||
return this.repository.findByCriteriaInCompany(companyId, criteria, transaction);
|
||||
}
|
||||
|
||||
@ -1,44 +1,65 @@
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
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 {
|
||||
private readonly proformaRepository: IProformaRepository;
|
||||
private readonly issuedInvoiceFactory: IIssuedInvoiceFactory;
|
||||
private readonly issuedInvoiceRepository: IIssuedInvoiceRepository;
|
||||
private readonly materializer: IProformaToIssuedInvoiceMaterializer;
|
||||
private readonly proformaConverter: IProformaToIssuedInvoiceConverter;
|
||||
private readonly repository: IProformaRepository;
|
||||
|
||||
constructor(deps: ProformaIssuerDeps) {
|
||||
this.proformaRepository = deps.proformaRepository;
|
||||
this.issuedInvoiceFactory = deps.issuedInvoiceFactory;
|
||||
this.issuedInvoiceRepository = deps.issuedInvoiceRepository;
|
||||
this.materializer = deps.materializer;
|
||||
this.proformaConverter = deps.proformaConverter;
|
||||
this.repository = deps.repository;
|
||||
}
|
||||
|
||||
public async issue(
|
||||
proforma: Proforma,
|
||||
issuedInvoiceId: UniqueID,
|
||||
transaction: Transaction
|
||||
): Promise<Result<Proforma, Error>> {
|
||||
public async issueProforma(
|
||||
params: IProformaIssuerParams
|
||||
): Promise<Result<IIssuedInvoiceCreateProps, Error>> {
|
||||
const { proforma, companyId, transaction } = params;
|
||||
|
||||
// Cambiamos el estado de la proforma a 'issued'
|
||||
const issueResult = proforma.issue();
|
||||
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);
|
||||
if (issueResult.isFailure) {
|
||||
return Result.fail(issueResult.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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,10 +7,10 @@ import type { Maybe, Result } from "@repo/rdx-utils";
|
||||
*/
|
||||
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 serie - Serie por la que buscar la última factura
|
||||
* @param serie - Serie por la que buscar la última proforma
|
||||
* @param transaction - Transacción activa
|
||||
*/
|
||||
getNextForCompany(
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -9,8 +9,8 @@ export interface IProformaItemFullSnapshot {
|
||||
|
||||
subtotal_amount: { value: string; scale: string; currency_code: string };
|
||||
|
||||
discount_percentage: { value: string; scale: string };
|
||||
discount_amount: { value: string; scale: string; currency_code: string };
|
||||
item_discount_percentage: { value: string; scale: string };
|
||||
item_discount_amount: { value: string; scale: string; currency_code: string };
|
||||
|
||||
global_discount_percentage: { value: string; scale: string };
|
||||
global_discount_amount: { value: string; scale: string; currency_code: string };
|
||||
|
||||
@ -32,8 +32,10 @@ export class ProformaItemsFullSnapshotBuilder implements IProformaItemsFullSnaps
|
||||
? allAmounts.subtotalAmount.toObjectString()
|
||||
: ItemAmount.EMPTY_MONEY_OBJECT,
|
||||
|
||||
discount_percentage: maybeToEmptyPercentageObjectString(proformaItem.itemDiscountPercentage),
|
||||
discount_amount: isValued
|
||||
item_discount_percentage: maybeToEmptyPercentageObjectString(
|
||||
proformaItem.itemDiscountPercentage
|
||||
),
|
||||
item_discount_amount: isValued
|
||||
? allAmounts.itemDiscountAmount.toObjectString()
|
||||
: ItemAmount.EMPTY_MONEY_OBJECT,
|
||||
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/core";
|
||||
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
|
||||
extends ISnapshotBuilder<ProformaFullSnapshot["items"], ProformaReportItemSnapshot[]> {}
|
||||
extends ISnapshotBuilder<IProformaFullSnapshot["items"], ProformaReportItemSnapshot[]> {}
|
||||
|
||||
export class ProformaItemReportSnapshotBuilder implements IProformaItemReportSnapshotBuilder {
|
||||
toOutput(
|
||||
items: ProformaFullSnapshot["items"],
|
||||
items: IProformaFullSnapshot["items"],
|
||||
params?: ISnapshotBuilderParams
|
||||
): ProformaReportItemSnapshot[] {
|
||||
const locale = params?.locale as string;
|
||||
|
||||
@ -1,30 +1,29 @@
|
||||
import { DateHelper, MoneyDTOHelper, PercentageDTOHelper } from "@erp/core";
|
||||
import type { ISnapshotBuilder, ISnapshotBuilderParams } from "@erp/core/api";
|
||||
|
||||
import type {
|
||||
ProformaFullSnapshot,
|
||||
ProformaReportItemSnapshot,
|
||||
ProformaReportSnapshot,
|
||||
ProformaReportTaxSnapshot,
|
||||
} from "../../application-models";
|
||||
import type { IProformaFullSnapshot } from "../full";
|
||||
|
||||
import type { ProformaReportItemSnapshot } from "./proforma-report-item-snapshot.interface";
|
||||
import type { ProformaReportSnapshot } from "./proforma-report-snapshot.interface";
|
||||
import type { ProformaReportTaxSnapshot } from "./proforma-report-tax-snapshot.interface";
|
||||
|
||||
export interface IProformaReportSnapshotBuilder
|
||||
extends ISnapshotBuilder<ProformaFullSnapshot, ProformaReportSnapshot> {}
|
||||
extends ISnapshotBuilder<IProformaFullSnapshot, ProformaReportSnapshot> {}
|
||||
|
||||
export class ProformaReportSnapshotBuilder implements IProformaReportSnapshotBuilder {
|
||||
constructor(
|
||||
private readonly itemsBuilder: ISnapshotBuilder<
|
||||
ProformaFullSnapshot["items"],
|
||||
IProformaFullSnapshot["items"],
|
||||
ProformaReportItemSnapshot[]
|
||||
>,
|
||||
private readonly taxesBuilder: ISnapshotBuilder<
|
||||
ProformaFullSnapshot["taxes"],
|
||||
IProformaFullSnapshot["taxes"],
|
||||
ProformaReportTaxSnapshot[]
|
||||
>
|
||||
) {}
|
||||
|
||||
toOutput(
|
||||
snapshot: ProformaFullSnapshot,
|
||||
snapshot: IProformaFullSnapshot,
|
||||
params?: ISnapshotBuilderParams
|
||||
): ProformaReportSnapshot {
|
||||
const locale = params?.locale as string;
|
||||
@ -61,17 +60,37 @@ export class ProformaReportSnapshotBuilder implements IProformaReportSnapshotBui
|
||||
taxes: this.taxesBuilder.toOutput(snapshot.taxes, { locale }),
|
||||
|
||||
subtotal_amount: MoneyDTOHelper.format(snapshot.subtotal_amount, locale, moneyOptions),
|
||||
discount_percentage: PercentageDTOHelper.format(snapshot.discount_percentage, locale, {
|
||||
hideZeros: true,
|
||||
}),
|
||||
discount_amount: MoneyDTOHelper.format(snapshot.discount_amount, locale, moneyOptions),
|
||||
|
||||
items_discount_amount: MoneyDTOHelper.format(
|
||||
snapshot.items_discount_amount,
|
||||
locale,
|
||||
moneyOptions
|
||||
),
|
||||
global_discount_percentage: PercentageDTOHelper.format(
|
||||
snapshot.global_discount_percentage,
|
||||
locale,
|
||||
{
|
||||
hideZeros: true,
|
||||
}
|
||||
),
|
||||
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),
|
||||
taxes_amount: MoneyDTOHelper.format(snapshot.taxes_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[] = [];
|
||||
|
||||
if (recipient.street) lines.push(recipient.street);
|
||||
|
||||
@ -27,8 +27,14 @@ export interface ProformaReportSnapshot {
|
||||
taxes: ProformaReportTaxSnapshot[];
|
||||
|
||||
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;
|
||||
taxes_amount: string;
|
||||
total_amount: string;
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import type { ITransactionManager } from "@erp/core/api";
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import type { Transaction } from "sequelize";
|
||||
|
||||
import type { CreateProformaRequestDTO } from "../../../../../common";
|
||||
import type { ICreateProformaInputMapper } from "../../mappers";
|
||||
@ -44,7 +43,7 @@ export class CreateProformaUseCase {
|
||||
|
||||
const { props, id } = mappedPropsResult.data;
|
||||
|
||||
return this.transactionManager.complete(async (transaction: Transaction) => {
|
||||
return this.transactionManager.complete(async (transaction: unknown) => {
|
||||
try {
|
||||
const createResult = await this.creator.create({ companyId, id, props, transaction });
|
||||
|
||||
|
||||
@ -1,20 +1,23 @@
|
||||
import type { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
|
||||
import { UniqueID, UtcDate } from "@repo/rdx-ddd";
|
||||
import type { ITransactionManager } from "@erp/core/api";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import {
|
||||
IssueCustomerInvoiceDomainService,
|
||||
ProformaCustomerInvoiceDomainService,
|
||||
} from "../../../domain";
|
||||
import type { CustomerInvoiceApplicationService } from "../../services";
|
||||
import type { IIssuedInvoicePublicServices } from "../../issued-invoices";
|
||||
import type { IProformaFinder, IProformaIssuer } from "../services";
|
||||
|
||||
type IssueProformaUseCaseInput = {
|
||||
companyId: UniqueID;
|
||||
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
|
||||
* - Valida su estado ("approved")
|
||||
@ -23,29 +26,30 @@ type IssueProformaUseCaseInput = {
|
||||
* - Persiste ambas dentro de la misma transacción
|
||||
*/
|
||||
export class IssueProformaUseCase {
|
||||
private readonly issueDomainService: IssueCustomerInvoiceDomainService;
|
||||
private readonly proformaDomainService: ProformaCustomerInvoiceDomainService;
|
||||
private readonly issuedInvoiceServices: IIssuedInvoicePublicServices;
|
||||
private readonly finder: IProformaFinder;
|
||||
private readonly issuer: IProformaIssuer;
|
||||
private readonly transactionManager: ITransactionManager;
|
||||
|
||||
constructor(
|
||||
private readonly service: CustomerInvoiceApplicationService,
|
||||
private readonly transactionManager: ITransactionManager,
|
||||
private readonly presenterRegistry: IPresenterRegistry
|
||||
) {
|
||||
this.issueDomainService = new IssueCustomerInvoiceDomainService();
|
||||
this.proformaDomainService = new ProformaCustomerInvoiceDomainService();
|
||||
constructor(deps: IssueProformaUseCaseDeps) {
|
||||
this.issuedInvoiceServices = deps.issuedInvoiceServices;
|
||||
this.finder = deps.finder;
|
||||
this.issuer = deps.issuer;
|
||||
this.transactionManager = deps.transactionManager;
|
||||
}
|
||||
|
||||
public execute(params: IssueProformaUseCaseInput) {
|
||||
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) => {
|
||||
try {
|
||||
/** 1. Recuperamos la proforma */
|
||||
const proformaResult = await this.service.getProformaByIdInCompany(
|
||||
// 1. Recuperamos la issuedinvoice
|
||||
const proformaResult = await this.finder.findProformaById(
|
||||
companyId,
|
||||
proformaId,
|
||||
transaction
|
||||
@ -54,49 +58,39 @@ export class IssueProformaUseCase {
|
||||
if (proformaResult.isFailure) return Result.fail(proformaResult.error);
|
||||
const proforma = proformaResult.data;
|
||||
|
||||
/** 2. Generar nueva factura */
|
||||
const nextNumberResult = await this.service.getNextIssuedInvoiceNumber(
|
||||
// 2. Generamos la factura definitiva y la guardamos
|
||||
const issuedInvoiceId = UniqueID.generateNewID();
|
||||
const createPropsOrError = await this.issuer.issueProforma({
|
||||
companyId,
|
||||
proforma.series,
|
||||
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(),
|
||||
issuedInvoiceId,
|
||||
proforma,
|
||||
transaction,
|
||||
});
|
||||
|
||||
if (issuedInvoiceOrError.isFailure) return Result.fail(issuedInvoiceOrError.error);
|
||||
if (createPropsOrError.isFailure) {
|
||||
return Result.fail(createPropsOrError.error);
|
||||
}
|
||||
|
||||
/** 5. Guardar la nueva factura */
|
||||
const saveInvoiceResult = await this.service.createIssuedInvoiceInCompany(
|
||||
companyId,
|
||||
issuedInvoiceOrError.data,
|
||||
transaction
|
||||
);
|
||||
if (saveInvoiceResult.isFailure) return Result.fail(saveInvoiceResult.error);
|
||||
const createProps = createPropsOrError.data;
|
||||
|
||||
/** 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
|
||||
// Creamos y guardamos en persistencia la factura definitiva
|
||||
const invoiceResult = await this.issuedInvoiceServices.createIssuedInvoice(
|
||||
issuedInvoiceId,
|
||||
createProps,
|
||||
{
|
||||
companyId,
|
||||
transaction,
|
||||
}
|
||||
);
|
||||
|
||||
const invoice = saveInvoiceResult.data;
|
||||
if (invoiceResult.isFailure) {
|
||||
return Result.fail(invoiceResult.error);
|
||||
}
|
||||
|
||||
const dto = {
|
||||
proforma_id: proforma.id.toString(),
|
||||
invoice_id: invoice.id.toString(),
|
||||
customer_id: invoice.customerId.toString(),
|
||||
issuedinvoice_id: issuedInvoiceId.toString(),
|
||||
proforma_id: proformaId.toString(),
|
||||
customer_id: proforma.customerId.toString(),
|
||||
};
|
||||
return Result.ok(dto);
|
||||
} catch (error: unknown) {
|
||||
|
||||
@ -2,7 +2,6 @@ import type { ITransactionManager } from "@erp/core/api";
|
||||
import type { Criteria } from "@repo/rdx-criteria/server";
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import type { Transaction } from "sequelize";
|
||||
|
||||
import type { IProformaFinder } from "../services";
|
||||
import type { IProformaSummarySnapshotBuilder } from "../snapshot-builders";
|
||||
@ -22,7 +21,7 @@ export class ListProformasUseCase {
|
||||
public execute(params: ListProformasUseCaseInput) {
|
||||
const { criteria, companyId } = params;
|
||||
|
||||
return this.transactionManager.complete(async (transaction: Transaction) => {
|
||||
return this.transactionManager.complete(async (transaction: unknown) => {
|
||||
try {
|
||||
const result = await this.finder.findProformasByCriteria(companyId, criteria, transaction);
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import type { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import type { Transaction } from "sequelize";
|
||||
|
||||
import type { UpdateProformaByIdRequestDTO } from "../../../../../common";
|
||||
import type { ProformaPatchProps } from "../../../../domain";
|
||||
@ -43,7 +42,7 @@ export class UpdateProformaUseCase {
|
||||
|
||||
const patchProps: ProformaPatchProps = patchPropsResult.data;
|
||||
|
||||
return this.transactionManager.complete(async (transaction: Transaction) => {
|
||||
return this.transactionManager.complete(async (transaction: unknown) => {
|
||||
try {
|
||||
const updatedInvoice = await this.service.patchProformaByIdInCompany(
|
||||
companyId,
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import type { Criteria } from "@repo/rdx-criteria/server";
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { type Collection, Maybe, Result } from "@repo/rdx-utils";
|
||||
import type { Transaction } from "sequelize";
|
||||
|
||||
import {
|
||||
CustomerInvoiceIsProformaSpecification,
|
||||
@ -35,7 +34,7 @@ export class CustomerInvoiceApplicationService {
|
||||
*/
|
||||
async getNextProformaNumber(
|
||||
companyId: UniqueID,
|
||||
transaction: Transaction
|
||||
transaction: unknown
|
||||
): Promise<Result<InvoiceNumber, Error>> {
|
||||
return await this.numberGenerator.nextForCompany(companyId, Maybe.none(), transaction);
|
||||
}
|
||||
@ -51,7 +50,7 @@ export class CustomerInvoiceApplicationService {
|
||||
async getNextIssuedInvoiceNumber(
|
||||
companyId: UniqueID,
|
||||
series: Maybe<InvoiceSerie>,
|
||||
transaction: Transaction
|
||||
transaction: unknown
|
||||
): Promise<Result<InvoiceNumber, Error>> {
|
||||
return await this.numberGenerator.nextForCompany(companyId, series, transaction);
|
||||
}
|
||||
@ -83,7 +82,7 @@ export class CustomerInvoiceApplicationService {
|
||||
async createIssuedInvoiceInCompany(
|
||||
companyId: UniqueID,
|
||||
invoice: CustomerInvoice,
|
||||
transaction: Transaction
|
||||
transaction: unknown
|
||||
): Promise<Result<CustomerInvoice, Error>> {
|
||||
const result = await this.repository.create(invoice, transaction);
|
||||
if (result.isFailure) {
|
||||
@ -104,7 +103,7 @@ export class CustomerInvoiceApplicationService {
|
||||
async createProformaInCompany(
|
||||
companyId: UniqueID,
|
||||
proforma: CustomerInvoice,
|
||||
transaction: Transaction
|
||||
transaction: unknown
|
||||
): Promise<Result<CustomerInvoice, Error>> {
|
||||
const result = await this.repository.create(proforma, transaction);
|
||||
if (result.isFailure) {
|
||||
@ -125,7 +124,7 @@ export class CustomerInvoiceApplicationService {
|
||||
async updateProformaInCompany(
|
||||
companyId: UniqueID,
|
||||
proforma: CustomerInvoice,
|
||||
transaction: Transaction
|
||||
transaction: unknown
|
||||
): Promise<Result<CustomerInvoice, Error>> {
|
||||
const result = await this.repository.update(proforma, transaction);
|
||||
if (result.isFailure) {
|
||||
@ -148,7 +147,7 @@ export class CustomerInvoiceApplicationService {
|
||||
async existsProformaByIdInCompany(
|
||||
companyId: UniqueID,
|
||||
proformaId: UniqueID,
|
||||
transaction?: Transaction
|
||||
transaction?: unknown
|
||||
): Promise<Result<boolean, Error>> {
|
||||
return this.repository.existsByIdInCompany(companyId, proformaId, transaction, {
|
||||
is_proforma: true,
|
||||
@ -168,7 +167,7 @@ export class CustomerInvoiceApplicationService {
|
||||
async existsIssuedInvoiceByIdInCompany(
|
||||
companyId: UniqueID,
|
||||
invoiceId: UniqueID,
|
||||
transaction?: Transaction
|
||||
transaction?: unknown
|
||||
): Promise<Result<boolean, Error>> {
|
||||
return this.repository.existsByIdInCompany(companyId, invoiceId, transaction, {
|
||||
is_proforma: false,
|
||||
@ -186,7 +185,7 @@ export class CustomerInvoiceApplicationService {
|
||||
async findProformasByCriteriaInCompany(
|
||||
companyId: UniqueID,
|
||||
criteria: Criteria,
|
||||
transaction?: Transaction
|
||||
transaction?: unknown
|
||||
): Promise<Result<Collection<CustomerInvoiceListDTO>, Error>> {
|
||||
return this.repository.findProformasByCriteriaInCompany(companyId, criteria, transaction, {});
|
||||
}
|
||||
@ -202,7 +201,7 @@ export class CustomerInvoiceApplicationService {
|
||||
async findIssuedInvoiceByCriteriaInCompany(
|
||||
companyId: UniqueID,
|
||||
criteria: Criteria,
|
||||
transaction?: Transaction
|
||||
transaction?: unknown
|
||||
): Promise<Result<Collection<CustomerInvoiceListDTO>, Error>> {
|
||||
return this.repository.findIssuedInvoicesByCriteriaInCompany(
|
||||
companyId,
|
||||
@ -222,7 +221,7 @@ export class CustomerInvoiceApplicationService {
|
||||
async getIssuedInvoiceByIdInCompany(
|
||||
companyId: UniqueID,
|
||||
invoiceId: UniqueID,
|
||||
transaction?: Transaction
|
||||
transaction?: unknown
|
||||
): Promise<Result<CustomerInvoice>> {
|
||||
return await this.repository.getIssuedInvoiceByIdInCompany(
|
||||
companyId,
|
||||
@ -242,7 +241,7 @@ export class CustomerInvoiceApplicationService {
|
||||
async getProformaByIdInCompany(
|
||||
companyId: UniqueID,
|
||||
proformaId: UniqueID,
|
||||
transaction?: Transaction
|
||||
transaction?: unknown
|
||||
): Promise<Result<CustomerInvoice>> {
|
||||
return await this.repository.getProformaByIdInCompany(companyId, proformaId, transaction, {});
|
||||
}
|
||||
@ -261,7 +260,7 @@ export class CustomerInvoiceApplicationService {
|
||||
companyId: UniqueID,
|
||||
proformaId: UniqueID,
|
||||
changes: CustomerInvoicePatchProps,
|
||||
transaction?: Transaction
|
||||
transaction?: unknown
|
||||
): Promise<Result<CustomerInvoice, Error>> {
|
||||
const proformaResult = await this.getProformaByIdInCompany(companyId, proformaId, transaction);
|
||||
|
||||
@ -289,7 +288,7 @@ export class CustomerInvoiceApplicationService {
|
||||
async deleteProformaByIdInCompany(
|
||||
companyId: UniqueID,
|
||||
proformaId: UniqueID,
|
||||
transaction?: Transaction
|
||||
transaction?: unknown
|
||||
): Promise<Result<boolean, Error>> {
|
||||
// 1) Buscar la proforma
|
||||
const proformaResult = await this.getProformaByIdInCompany(companyId, proformaId, transaction);
|
||||
@ -329,7 +328,7 @@ export class CustomerInvoiceApplicationService {
|
||||
companyId: UniqueID,
|
||||
proformaId: UniqueID,
|
||||
newStatus: InvoiceStatus,
|
||||
transaction?: Transaction
|
||||
transaction?: unknown
|
||||
): Promise<Result<boolean, Error>> {
|
||||
return this.repository.updateProformaStatusByIdInCompany(
|
||||
companyId,
|
||||
|
||||
@ -32,9 +32,11 @@ export class InvoiceAmount extends MoneyValue {
|
||||
}
|
||||
|
||||
// Ensure fluent operations keep the subclass type
|
||||
convertScale(newScale: number) {
|
||||
const mv = super.convertScale(newScale);
|
||||
const p = mv.toPrimitive();
|
||||
roundUsingScale(intermediateScale: number) {
|
||||
const scaled = super.convertScale(intermediateScale);
|
||||
const normalized = scaled.convertScale(InvoiceAmount.DEFAULT_SCALE);
|
||||
const p = normalized.toPrimitive();
|
||||
|
||||
return new InvoiceAmount({
|
||||
value: p.value,
|
||||
currency_code: p.currency_code,
|
||||
|
||||
@ -19,7 +19,7 @@ export enum INVOICE_STATUS {
|
||||
const INVOICE_TRANSITIONS: Record<string, string[]> = {
|
||||
draft: [INVOICE_STATUS.SENT],
|
||||
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],
|
||||
issued: [],
|
||||
};
|
||||
@ -39,34 +39,34 @@ export class InvoiceStatus extends ValueObject<IInvoiceStatusProps> {
|
||||
|
||||
return Result.ok(
|
||||
value === "rejected"
|
||||
? InvoiceStatus.fromRejected()
|
||||
? InvoiceStatus.rejected()
|
||||
: value === "sent"
|
||||
? InvoiceStatus.fromSent()
|
||||
? InvoiceStatus.sent()
|
||||
: value === "issued"
|
||||
? InvoiceStatus.fromIssued()
|
||||
? InvoiceStatus.issued()
|
||||
: value === "approved"
|
||||
? InvoiceStatus.fromApproved()
|
||||
: InvoiceStatus.fromDraft()
|
||||
? InvoiceStatus.approved()
|
||||
: InvoiceStatus.draft()
|
||||
);
|
||||
}
|
||||
|
||||
public static fromDraft(): InvoiceStatus {
|
||||
public static draft(): InvoiceStatus {
|
||||
return new InvoiceStatus({ value: INVOICE_STATUS.DRAFT });
|
||||
}
|
||||
|
||||
public static fromIssued(): InvoiceStatus {
|
||||
public static issued(): InvoiceStatus {
|
||||
return new InvoiceStatus({ value: INVOICE_STATUS.ISSUED });
|
||||
}
|
||||
|
||||
public static fromSent(): InvoiceStatus {
|
||||
public static sent(): InvoiceStatus {
|
||||
return new InvoiceStatus({ value: INVOICE_STATUS.SENT });
|
||||
}
|
||||
|
||||
public static fromApproved(): InvoiceStatus {
|
||||
public static approved(): InvoiceStatus {
|
||||
return new InvoiceStatus({ value: INVOICE_STATUS.APPROVED });
|
||||
}
|
||||
|
||||
public static fromRejected(): InvoiceStatus {
|
||||
public static rejected(): InvoiceStatus {
|
||||
return new InvoiceStatus({ value: INVOICE_STATUS.REJECTED });
|
||||
}
|
||||
|
||||
|
||||
@ -32,9 +32,11 @@ export class ItemAmount extends MoneyValue {
|
||||
}
|
||||
|
||||
// Ensure fluent operations keep the subclass type
|
||||
convertScale(newScale: number) {
|
||||
const mv = super.convertScale(newScale);
|
||||
const p = mv.toPrimitive();
|
||||
roundUsingScale(intermediateScale: number) {
|
||||
const scaled = super.convertScale(intermediateScale);
|
||||
const normalized = scaled.convertScale(ItemAmount.DEFAULT_SCALE);
|
||||
const p = normalized.toPrimitive();
|
||||
|
||||
return new ItemAmount({
|
||||
value: p.value,
|
||||
currency_code: p.currency_code,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -19,13 +19,20 @@ import type {
|
||||
InvoiceSerie,
|
||||
InvoiceStatus,
|
||||
} 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;
|
||||
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>;
|
||||
invoiceNumber: InvoiceNumber;
|
||||
@ -45,7 +52,7 @@ export interface IIssuedInvoiceProps {
|
||||
|
||||
paymentMethod: Maybe<InvoicePaymentMethod>;
|
||||
|
||||
items: IssuedInvoiceItems;
|
||||
items: IIssuedInvoiceItemCreateProps[];
|
||||
taxes: IssuedInvoiceTaxes;
|
||||
|
||||
subtotalAmount: InvoiceAmount;
|
||||
@ -67,21 +74,28 @@ export interface IIssuedInvoiceProps {
|
||||
verifactu: Maybe<VerifactuRecord>;
|
||||
}
|
||||
|
||||
export class IssuedInvoice extends AggregateRoot<IIssuedInvoiceProps> {
|
||||
private _items!: IssuedInvoiceItems;
|
||||
export interface IIssuedInvoice {
|
||||
companyId: UniqueID;
|
||||
}
|
||||
|
||||
protected constructor(props: IIssuedInvoiceProps, id?: UniqueID) {
|
||||
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 =
|
||||
props.items ||
|
||||
IssuedInvoiceItems.create({
|
||||
languageCode: props.languageCode,
|
||||
currencyCode: props.currencyCode,
|
||||
globalDiscountPercentage: props.globalDiscountPercentage,
|
||||
});
|
||||
this._items = items;
|
||||
}
|
||||
|
||||
static create(props: IIssuedInvoiceProps, id?: UniqueID): Result<IssuedInvoice, Error> {
|
||||
static create(props: IIssuedInvoiceCreateProps, id?: UniqueID): Result<IssuedInvoice, Error> {
|
||||
if (!props.recipient) {
|
||||
return Result.fail(
|
||||
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
|
||||
// ...
|
||||
@ -104,6 +132,32 @@ export class IssuedInvoice extends AggregateRoot<IIssuedInvoiceProps> {
|
||||
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
|
||||
|
||||
public get companyId(): UniqueID {
|
||||
@ -114,7 +168,7 @@ export class IssuedInvoice extends AggregateRoot<IIssuedInvoiceProps> {
|
||||
return this.props.customerId;
|
||||
}
|
||||
|
||||
public get proformaId(): Maybe<UniqueID> {
|
||||
public get proformaId(): UniqueID {
|
||||
return this.props.proformaId;
|
||||
}
|
||||
|
||||
@ -230,8 +284,4 @@ export class IssuedInvoice extends AggregateRoot<IIssuedInvoiceProps> {
|
||||
public get hasPaymentMethod() {
|
||||
return this.paymentMethod.isSome();
|
||||
}
|
||||
|
||||
public getProps(): IIssuedInvoiceProps {
|
||||
return this.props;
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ import type { ItemAmount, ItemDescription, ItemQuantity } from "../../../common"
|
||||
* Todos los importes están previamente calculados y congelados.
|
||||
*/
|
||||
|
||||
export type IssuedInvoiceItemProps = {
|
||||
export interface IIssuedInvoiceItemCreateProps {
|
||||
description: Maybe<ItemDescription>;
|
||||
|
||||
quantity: Maybe<ItemQuantity>;
|
||||
@ -49,18 +49,55 @@ export type IssuedInvoiceItemProps = {
|
||||
|
||||
languageCode: LanguageCode;
|
||||
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
|
||||
extends DomainEntity<IssuedInvoiceItemProps>
|
||||
extends DomainEntity<InternalIssuedInvoiceItemProps>
|
||||
implements IIssuedInvoiceItem
|
||||
{
|
||||
public static create(
|
||||
props: IssuedInvoiceItemProps,
|
||||
props: IIssuedInvoiceItemCreateProps,
|
||||
id?: UniqueID
|
||||
): Result<IssuedInvoiceItem, Error> {
|
||||
const item = new IssuedInvoiceItem(props, id);
|
||||
@ -72,7 +109,11 @@ export class IssuedInvoiceItem
|
||||
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);
|
||||
}
|
||||
|
||||
@ -163,7 +204,7 @@ export class IssuedInvoiceItem
|
||||
return this.props.totalAmount;
|
||||
}
|
||||
|
||||
getProps(): IssuedInvoiceItemProps {
|
||||
getProps(): IIssuedInvoiceItemCreateProps {
|
||||
return this.props;
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export * from "./issued-invoice-item-not-valid-error";
|
||||
@ -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;
|
||||
@ -1,3 +1,4 @@
|
||||
export * from "./aggregates";
|
||||
export * from "./entities";
|
||||
export * from "./errors";
|
||||
export * from "./value-objects";
|
||||
|
||||
@ -17,7 +17,7 @@ import {
|
||||
type InvoiceNumber,
|
||||
type InvoiceRecipient,
|
||||
type InvoiceSerie,
|
||||
type InvoiceStatus,
|
||||
InvoiceStatus,
|
||||
type ItemAmount,
|
||||
} from "../../common/value-objects";
|
||||
import {
|
||||
@ -27,7 +27,7 @@ import {
|
||||
ProformaItems,
|
||||
} from "../entities";
|
||||
import { ProformaItemMismatch } from "../errors";
|
||||
import { type IProformaTaxTotals, ProformaTaxesCalculator } from "../services";
|
||||
import type { IProformaTaxTotals } from "../services";
|
||||
import { ProformaItemTaxes } from "../value-objects";
|
||||
|
||||
export interface IProformaCreateProps {
|
||||
@ -249,7 +249,7 @@ export class Proforma extends AggregateRoot<InternalProformaProps> implements IP
|
||||
}
|
||||
|
||||
public issue(): Result<void, Error> {
|
||||
if (!this.props.status.canTransitionTo("ISSUED")) {
|
||||
if (!this.props.status.canTransitionTo("issued")) {
|
||||
return Result.fail(
|
||||
new DomainValidationError(
|
||||
"INVALID_STATE",
|
||||
@ -259,8 +259,7 @@ export class Proforma extends AggregateRoot<InternalProformaProps> implements IP
|
||||
);
|
||||
}
|
||||
|
||||
// Falta
|
||||
//this.props.status = this.props.status.canTransitionTo("ISSUED");
|
||||
this.props.status = InvoiceStatus.issued();
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@ -292,7 +291,7 @@ export class Proforma extends AggregateRoot<InternalProformaProps> implements IP
|
||||
}
|
||||
|
||||
public taxes(): Collection<IProformaTaxTotals> {
|
||||
return new ProformaTaxesCalculator(this.items).calculate();
|
||||
return this.items.taxes();
|
||||
}
|
||||
|
||||
public addItem(props: IProformaItemCreateProps): Result<void, Error> {
|
||||
|
||||
@ -3,6 +3,7 @@ import type { CurrencyCode, LanguageCode } from "@repo/rdx-ddd";
|
||||
import { Collection, Result } from "@repo/rdx-utils";
|
||||
|
||||
import { ProformaItemMismatch } from "../../errors";
|
||||
import { type IProformaTaxTotals, ProformaTaxesCalculator } from "../../services";
|
||||
import { ProformaItemsTotalsCalculator } from "../../services/proforma-items-totals-calculator";
|
||||
|
||||
import type { IProformaItem, IProformaItemTotals, ProformaItem } from "./proforma-item.entity";
|
||||
@ -37,6 +38,7 @@ export interface IProformaItems {
|
||||
|
||||
valued(): IProformaItem[]; // Devuelve solo las líneas valoradas.
|
||||
totals(): IProformaItemTotals;
|
||||
taxes(): Collection<IProformaTaxTotals>;
|
||||
|
||||
readonly globalDiscountPercentage: DiscountPercentage; // % descuento de la cabecera
|
||||
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) {
|
||||
this.ensureSameContext(this.items);
|
||||
}
|
||||
4;
|
||||
}
|
||||
|
||||
static create(props: IProformaItemsProps): ProformaItems {
|
||||
@ -113,6 +116,10 @@ export class ProformaItems extends Collection<ProformaItem> implements IProforma
|
||||
return new ProformaItemsTotalsCalculator(this).calculate();
|
||||
}
|
||||
|
||||
public taxes(): Collection<IProformaTaxTotals> {
|
||||
return new ProformaTaxesCalculator(this).calculate();
|
||||
}
|
||||
|
||||
private ensureSameContext(items: IProformaItem[]): void {
|
||||
for (const item of items) {
|
||||
const same =
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { ItemAmount } from "../../common";
|
||||
import type { IProformaItems } from "../entities";
|
||||
import type { ProformaItems } from "../entities";
|
||||
|
||||
export interface IProformaItemsTotals {
|
||||
subtotalAmount: ItemAmount;
|
||||
@ -23,7 +23,7 @@ export interface IProformaItemsTotals {
|
||||
* La lógica fiscal está en ProformaItem; aquí solo se agregan resultados.
|
||||
*/
|
||||
export class ProformaItemsTotalsCalculator {
|
||||
constructor(private readonly items: IProformaItems) {}
|
||||
constructor(private readonly items: ProformaItems) {}
|
||||
|
||||
public calculate(): IProformaItemsTotals {
|
||||
const zero = ItemAmount.zero(this.items.currencyCode.code);
|
||||
@ -47,26 +47,28 @@ export class ProformaItemsTotalsCalculator {
|
||||
const amounts = item.totals();
|
||||
|
||||
// Subtotales
|
||||
subtotalAmount = subtotalAmount.add(amounts.subtotalAmount);
|
||||
subtotalAmount = subtotalAmount.add(amounts.subtotalAmount.roundUsingScale(2));
|
||||
|
||||
// Descuentos
|
||||
itemDiscountAmount = itemDiscountAmount.add(amounts.itemDiscountAmount);
|
||||
globalDiscountAmount = globalDiscountAmount.add(amounts.globalDiscountAmount);
|
||||
totalDiscountAmount = totalDiscountAmount.add(amounts.totalDiscountAmount);
|
||||
itemDiscountAmount = itemDiscountAmount.add(amounts.itemDiscountAmount.roundUsingScale(2));
|
||||
globalDiscountAmount = globalDiscountAmount.add(
|
||||
amounts.globalDiscountAmount.roundUsingScale(2)
|
||||
);
|
||||
totalDiscountAmount = totalDiscountAmount.add(amounts.totalDiscountAmount.roundUsingScale(2));
|
||||
|
||||
// Base imponible
|
||||
taxableAmount = taxableAmount.add(amounts.taxableAmount);
|
||||
taxableAmount = taxableAmount.add(amounts.taxableAmount.roundUsingScale(2));
|
||||
|
||||
// Impuestos individuales
|
||||
ivaAmount = ivaAmount.add(amounts.ivaAmount);
|
||||
recAmount = recAmount.add(amounts.recAmount);
|
||||
retentionAmount = retentionAmount.add(amounts.retentionAmount);
|
||||
ivaAmount = ivaAmount.add(amounts.ivaAmount.roundUsingScale(2));
|
||||
recAmount = recAmount.add(amounts.recAmount.roundUsingScale(2));
|
||||
retentionAmount = retentionAmount.add(amounts.retentionAmount.roundUsingScale(2));
|
||||
|
||||
// Total impuestos del ítem
|
||||
taxesAmount = taxesAmount.add(amounts.taxesAmount);
|
||||
taxesAmount = taxesAmount.add(amounts.taxesAmount.roundUsingScale(2));
|
||||
|
||||
// Total final del ítem
|
||||
totalAmount = totalAmount.add(amounts.totalAmount);
|
||||
totalAmount = totalAmount.add(amounts.totalAmount.roundUsingScale(2));
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@ -29,8 +29,8 @@ export class ProformaTaxesCalculator {
|
||||
constructor(private readonly items: IProformaItems) {}
|
||||
|
||||
public calculate(): Collection<IProformaTaxTotals> {
|
||||
const groups = proformaComputeTaxGroups(this.items);
|
||||
const currencyCode = this.items.currencyCode;
|
||||
const groups = proformaComputeTaxGroups(this.items); // <- devuelve en escala 4
|
||||
//const currencyCode = this.items.currencyCode;
|
||||
|
||||
const rows = Array.from(groups.values()).map((g) => {
|
||||
const taxableAmount = this.toInvoiceAmount(g.taxableAmount);
|
||||
|
||||
@ -67,7 +67,7 @@ export class IssueCustomerInvoiceDomainService {
|
||||
...proformaProps,
|
||||
isProforma: false,
|
||||
proformaId: Maybe.some(proforma.id),
|
||||
status: InvoiceStatus.fromIssued(),
|
||||
status: InvoiceStatus.issued(),
|
||||
invoiceNumber: issueNumber,
|
||||
invoiceDate: issueDate,
|
||||
description: proformaProps.description.isNone() ? Maybe.some(".") : proformaProps.description,
|
||||
|
||||
@ -1,21 +1,15 @@
|
||||
import type { IModuleServer } from "@erp/core/api";
|
||||
|
||||
import {
|
||||
type IssuedInvoicePublicServices,
|
||||
type IssuedInvoicesInternalDeps,
|
||||
type ProformaPublicServices,
|
||||
type ProformasInternalDeps,
|
||||
buildIssuedInvoiceServices,
|
||||
buildIssuedInvoicePublicServices,
|
||||
buildIssuedInvoicesDependencies,
|
||||
buildProformaServices,
|
||||
buildProformaPublicServices,
|
||||
buildProformasDependencies,
|
||||
issuedInvoicesRouter,
|
||||
models,
|
||||
proformasRouter,
|
||||
} from "./infrastructure";
|
||||
|
||||
export type { IssuedInvoicePublicServices, ProformaPublicServices };
|
||||
|
||||
export const customerInvoicesAPIModule: IModuleServer = {
|
||||
name: "customer-invoices",
|
||||
version: "1.0.0",
|
||||
@ -36,14 +30,8 @@ export const customerInvoicesAPIModule: IModuleServer = {
|
||||
const proformasInternal = buildProformasDependencies(params);
|
||||
|
||||
// 2) Servicios públicos (Application Services)
|
||||
const issuedInvoicesServices: IssuedInvoicePublicServices = buildIssuedInvoiceServices(
|
||||
params,
|
||||
issuedInvoicesInternal
|
||||
);
|
||||
const proformasServices: ProformaPublicServices = buildProformaServices(
|
||||
params,
|
||||
proformasInternal
|
||||
);
|
||||
const issuedInvoicesServices = buildIssuedInvoicePublicServices(params, issuedInvoicesInternal);
|
||||
const proformasServices = buildProformaPublicServices(params, proformasInternal);
|
||||
|
||||
logger.info("🚀 CustomerInvoices module dependencies registered", { label: this.name });
|
||||
|
||||
@ -73,22 +61,11 @@ export const customerInvoicesAPIModule: IModuleServer = {
|
||||
* - NO construye dominio
|
||||
*/
|
||||
async start(params) {
|
||||
const { app, baseRoutePath, logger, getInternal } = params;
|
||||
|
||||
// Recuperamos el dominio interno del módulo
|
||||
const issuedInvoicesInternalDeps = getInternal<IssuedInvoicesInternalDeps>(
|
||||
"customer-invoices",
|
||||
"issuedInvoices"
|
||||
);
|
||||
|
||||
const proformasInternalDeps = getInternal<ProformasInternalDeps>(
|
||||
"customer-invoices",
|
||||
"proformas"
|
||||
);
|
||||
const { app, baseRoutePath, logger, getInternal, getService } = params;
|
||||
|
||||
// Registro de rutas HTTP
|
||||
issuedInvoicesRouter(params, issuedInvoicesInternalDeps);
|
||||
proformasRouter(params, proformasInternalDeps);
|
||||
issuedInvoicesRouter(params);
|
||||
proformasRouter(params);
|
||||
|
||||
logger.info("🚀 CustomerInvoices module started", {
|
||||
label: this.name,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { JsonTaxCatalogProvider } from "@erp/core";
|
||||
import {
|
||||
DiscountPercentage,
|
||||
type ISequelizeDomainMapper,
|
||||
type MapperParamsType,
|
||||
SequelizeDomainMapper,
|
||||
@ -18,18 +19,11 @@ import { Result } from "@repo/rdx-utils";
|
||||
import {
|
||||
type IProformaCreateProps,
|
||||
IssuedInvoiceItem,
|
||||
type IssuedInvoiceItemProps,
|
||||
ItemAmount,
|
||||
ItemDescription,
|
||||
ItemDiscountPercentage,
|
||||
ItemQuantity,
|
||||
ItemTaxGroup,
|
||||
type Proforma,
|
||||
} from "../../../../../../domain";
|
||||
import type {
|
||||
CustomerInvoiceItemCreationAttributes,
|
||||
CustomerInvoiceItemModel,
|
||||
} from "../../../../sequelize";
|
||||
|
||||
export interface ICustomerInvoiceItemDomainMapper
|
||||
extends ISequelizeDomainMapper<
|
||||
@ -99,7 +93,7 @@ export class CustomerInvoiceItemDomainMapper
|
||||
|
||||
const discountPercentage = extractOrPushError(
|
||||
maybeFromNullableResult(source.discount_percentage_value, (v) =>
|
||||
ItemDiscountPercentage.create({ value: v })
|
||||
DiscountPercentage.create({ value: v })
|
||||
),
|
||||
`items[${index}].discount_percentage`,
|
||||
errors
|
||||
@ -107,7 +101,7 @@ export class CustomerInvoiceItemDomainMapper
|
||||
|
||||
const globalDiscountPercentage = extractOrPushError(
|
||||
maybeFromNullableResult(source.global_discount_percentage_value, (v) =>
|
||||
ItemDiscountPercentage.create({ value: v })
|
||||
DiscountPercentage.create({ value: v })
|
||||
),
|
||||
`items[${index}].discount_percentage`,
|
||||
errors
|
||||
@ -242,7 +236,7 @@ export class CustomerInvoiceItemDomainMapper
|
||||
),
|
||||
discount_percentage_scale:
|
||||
maybeToNullable(source.itemDiscountPercentage, (v) => v.toPrimitive().scale) ??
|
||||
ItemDiscountPercentage.DEFAULT_SCALE,
|
||||
DiscountPercentage.DEFAULT_SCALE,
|
||||
|
||||
discount_amount_value: allAmounts.itemDiscountAmount.value,
|
||||
discount_amount_scale: allAmounts.itemDiscountAmount.scale,
|
||||
@ -255,7 +249,7 @@ export class CustomerInvoiceItemDomainMapper
|
||||
|
||||
global_discount_percentage_scale:
|
||||
maybeToNullable(source.globalDiscountPercentage, (v) => v.toPrimitive().scale) ??
|
||||
ItemDiscountPercentage.DEFAULT_SCALE,
|
||||
DiscountPercentage.DEFAULT_SCALE,
|
||||
|
||||
global_discount_amount_value: allAmounts.globalDiscountAmount.value,
|
||||
global_discount_amount_scale: allAmounts.globalDiscountAmount.scale,
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./issued-invoice-number-generator.di";
|
||||
export * from "./issued-invoice-public-services";
|
||||
export * from "./issued-invoices.di";
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
import type { IIssuedInvoiceNumberGenerator } from "../../../application";
|
||||
import { SequelizeIssuedInvoiceNumberGenerator } from "../persistence";
|
||||
|
||||
export const buildIssuedInvoiceNumberGenerator = (): IIssuedInvoiceNumberGenerator =>
|
||||
new SequelizeIssuedInvoiceNumberGenerator();
|
||||
@ -1,26 +1,27 @@
|
||||
import type { SetupParams } 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 {
|
||||
type IIssuedInvoiceCreatorParams,
|
||||
type IIssuedInvoicePublicServices,
|
||||
type IIssuedInvoiceServicesContext,
|
||||
buildIssuedInvoiceCreator,
|
||||
buildIssuedInvoiceFinder,
|
||||
buildIssuedInvoiceSnapshotBuilders,
|
||||
} from "../../../application/issued-invoices";
|
||||
|
||||
import { buildIssuedInvoiceDocumentService } from "./issued-invoice-documents.di";
|
||||
import { buildIssuedInvoiceNumberGenerator } from "./issued-invoice-number-generator.di";
|
||||
import { buildIssuedInvoicePersistenceMappers } from "./issued-invoice-persistence-mappers.di";
|
||||
import { buildIssuedInvoiceRepository } from "./issued-invoice-repositories.di";
|
||||
import type { IssuedInvoicesInternalDeps } from "./issued-invoices.di";
|
||||
|
||||
export type IssuedInvoicePublicServices = {
|
||||
listIssuedInvoices: (filters: unknown, context: unknown) => null;
|
||||
getIssuedInvoiceById: (id: unknown, context: unknown) => null;
|
||||
generateIssuedInvoiceReport: (id: unknown, options: unknown, context: unknown) => null;
|
||||
};
|
||||
|
||||
export function buildIssuedInvoiceServices(
|
||||
export function buildIssuedInvoicePublicServices(
|
||||
params: SetupParams,
|
||||
deps: IssuedInvoicesInternalDeps
|
||||
): IssuedInvoicePublicServices {
|
||||
): IIssuedInvoicePublicServices {
|
||||
const { database } = params;
|
||||
|
||||
// Infrastructure
|
||||
@ -29,20 +30,29 @@ export function buildIssuedInvoiceServices(
|
||||
const persistenceMappers = buildIssuedInvoicePersistenceMappers(catalogs);
|
||||
|
||||
const repository = buildIssuedInvoiceRepository({ database, mappers: persistenceMappers });
|
||||
const numberService = buildIssuedInvoiceNumberGenerator();
|
||||
|
||||
// Application helpers
|
||||
const creator = buildIssuedInvoiceCreator({ numberService, repository });
|
||||
const finder = buildIssuedInvoiceFinder(repository);
|
||||
const snapshotBuilders = buildIssuedInvoiceSnapshotBuilders();
|
||||
const documentGeneratorPipeline = buildIssuedInvoiceDocumentService(params);
|
||||
|
||||
return {
|
||||
listIssuedInvoices: (filters, context) => null,
|
||||
//internal.useCases.listIssuedInvoices().execute(filters, context),
|
||||
createIssuedInvoice: async (
|
||||
id: UniqueID,
|
||||
props: IIssuedInvoiceCreatorParams["props"],
|
||||
context: IIssuedInvoiceServicesContext
|
||||
) => {
|
||||
const { transaction, companyId } = context;
|
||||
|
||||
getIssuedInvoiceById: (id, context) => null,
|
||||
//internal.useCases.getIssuedInvoiceById().execute(id, context),
|
||||
const createResult = await creator.create({ companyId, id, props, transaction });
|
||||
|
||||
generateIssuedInvoiceReport: (id, options, context) => null,
|
||||
//internal.useCases.reportIssuedInvoice().execute(id, options, context),
|
||||
if (createResult.isFailure) {
|
||||
return Result.fail(createResult.error);
|
||||
}
|
||||
|
||||
return Result.ok(createResult.data);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -10,14 +10,9 @@ import {
|
||||
|
||||
import {
|
||||
type CustomerInvoiceIdAlreadyExistsError,
|
||||
type EntityIsNotProformaError,
|
||||
type InvalidProformaTransitionError,
|
||||
type ProformaCannotBeConvertedToInvoiceError,
|
||||
type IssuedInvoiceItemMismatch,
|
||||
isCustomerInvoiceIdAlreadyExistsError,
|
||||
isEntityIsNotProformaError,
|
||||
isInvalidProformaTransitionError,
|
||||
isProformaCannotBeConvertedToInvoiceError,
|
||||
isProformaCannotBeDeletedError,
|
||||
isIssuedInvoiceItemMismatch,
|
||||
} from "../../../domain";
|
||||
|
||||
// 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,
|
||||
matches: (e) => isEntityIsNotProformaError(e),
|
||||
matches: (e) => isIssuedInvoiceItemMismatch(e),
|
||||
build: (e) =>
|
||||
new ValidationApiError(
|
||||
(e as EntityIsNotProformaError).message || "Entity with the provided id is not proforma"
|
||||
),
|
||||
};
|
||||
|
||||
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."
|
||||
(e as IssuedInvoiceItemMismatch).message ||
|
||||
"IssuedInvoice item rejected due to currency/language mismatch"
|
||||
),
|
||||
};
|
||||
|
||||
// Cómo aplicarla: crea una nueva instancia del mapper con la regla extra
|
||||
export const customerInvoicesApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default()
|
||||
.register(invoiceDuplicateRule)
|
||||
.register(entityIsNotProformaError)
|
||||
.register(proformaConversionRule)
|
||||
.register(proformaCannotBeDeletedRule)
|
||||
.register(proformaTransitionRule);
|
||||
.register(issuedinvoiceItemMismatchError);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 {
|
||||
@ -16,11 +16,15 @@ import {
|
||||
ReportIssuedInvoiceController,
|
||||
} from "./controllers";
|
||||
|
||||
export const issuedInvoicesRouter = (params: ModuleParams, deps: IssuedInvoicesInternalDeps) => {
|
||||
const { app, config } = params;
|
||||
export const issuedInvoicesRouter = (params: StartParams) => {
|
||||
const { app, config, getService, getInternal } = params;
|
||||
|
||||
const deps = getInternal<IssuedInvoicesInternalDeps>("customer-invoices", "issuedInvoices");
|
||||
|
||||
const router: Router = Router({ mergeParams: true });
|
||||
|
||||
// ----------------------------------------------
|
||||
|
||||
if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "production") {
|
||||
// 🔐 Autenticación + Tenancy para TODO el router
|
||||
router.use(
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./mappers";
|
||||
export * from "./repositories";
|
||||
export * from "./services";
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils";
|
||||
|
||||
import {
|
||||
type IIssuedInvoiceProps,
|
||||
type InternalIssuedInvoiceProps,
|
||||
InvoiceAmount,
|
||||
InvoiceNumber,
|
||||
InvoicePaymentMethod,
|
||||
@ -63,8 +63,9 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
|
||||
|
||||
const customerId = extractOrPushError(UniqueID.create(raw.customer_id), "customer_id", errors);
|
||||
|
||||
// Para issued invoices, proforma_id debe estar relleno
|
||||
const proformaId = extractOrPushError(
|
||||
maybeFromNullableResult(raw.proforma_id, (v) => UniqueID.create(v)),
|
||||
UniqueID.create(String(raw.proforma_id)),
|
||||
"proforma_id",
|
||||
errors
|
||||
);
|
||||
@ -346,7 +347,7 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
|
||||
currencyCode: attributes.currencyCode!,
|
||||
});
|
||||
|
||||
const invoiceProps: IIssuedInvoiceProps = {
|
||||
const invoiceProps: InternalIssuedInvoiceProps = {
|
||||
companyId: attributes.companyId!,
|
||||
|
||||
proformaId: attributes.proformaId!,
|
||||
@ -383,22 +384,14 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
|
||||
|
||||
paymentMethod: attributes.paymentMethod!,
|
||||
|
||||
items,
|
||||
taxes,
|
||||
verifactu,
|
||||
};
|
||||
|
||||
const createResult = IssuedInvoice.create(invoiceProps, attributes.invoiceId);
|
||||
const invoiceId = attributes.invoiceId!;
|
||||
const invoice = IssuedInvoice.rehydrate(invoiceProps, items, invoiceId);
|
||||
|
||||
if (createResult.isFailure) {
|
||||
return Result.fail(
|
||||
new ValidationErrorCollection("Customer invoice entity creation failed", [
|
||||
{ path: "invoice", message: createResult.error.message },
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
return Result.ok(createResult.data);
|
||||
return Result.ok(invoice);
|
||||
} catch (err: unknown) {
|
||||
return Result.fail(err as Error);
|
||||
}
|
||||
@ -471,7 +464,7 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
|
||||
// Flags / estado / serie / número
|
||||
is_proforma: false,
|
||||
status: source.status.toPrimitive(),
|
||||
proforma_id: maybeToNullable(source.proformaId, (v) => v.toPrimitive()),
|
||||
proforma_id: source.proformaId.toPrimitive(),
|
||||
|
||||
series: maybeToNullable(source.series, (v) => v.toPrimitive()),
|
||||
invoice_number: source.invoiceNumber.toPrimitive(),
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
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 {
|
||||
UniqueID,
|
||||
ValidationErrorCollection,
|
||||
@ -13,16 +18,13 @@ import {
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import {
|
||||
DiscountPercentage,
|
||||
type IIssuedInvoiceProps,
|
||||
type IIssuedInvoiceCreateProps,
|
||||
type IIssuedInvoiceItemCreateProps,
|
||||
type IssuedInvoice,
|
||||
IssuedInvoiceItem,
|
||||
type IssuedInvoiceItemProps,
|
||||
ItemAmount,
|
||||
ItemDescription,
|
||||
ItemDiscountPercentage,
|
||||
ItemQuantity,
|
||||
ItemTaxPercentage,
|
||||
} from "../../../../../../domain";
|
||||
import type {
|
||||
CustomerInvoiceItemCreationAttributes,
|
||||
@ -34,7 +36,7 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
|
||||
CustomerInvoiceItemCreationAttributes,
|
||||
IssuedInvoiceItem
|
||||
> {
|
||||
private taxCatalog!: JsonTaxCatalogProvider;
|
||||
private readonly taxCatalog!: JsonTaxCatalogProvider;
|
||||
|
||||
constructor(params: MapperParamsType) {
|
||||
super();
|
||||
@ -52,11 +54,11 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
|
||||
private mapAttributesToDomain(
|
||||
raw: CustomerInvoiceItemModel,
|
||||
params?: MapperParamsType
|
||||
): Partial<IssuedInvoiceItemProps> & { itemId?: UniqueID } {
|
||||
): Partial<IIssuedInvoiceItemCreateProps> & { itemId?: UniqueID } {
|
||||
const { errors, index, attributes } = params as {
|
||||
index: number;
|
||||
errors: ValidationErrorDetail[];
|
||||
attributes: Partial<IIssuedInvoiceProps>;
|
||||
attributes: Partial<IIssuedInvoiceCreateProps>;
|
||||
};
|
||||
|
||||
const itemId = extractOrPushError(
|
||||
@ -96,7 +98,7 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
|
||||
|
||||
const itemDiscountPercentage = extractOrPushError(
|
||||
maybeFromNullableResult(raw.item_discount_percentage_value, (v) =>
|
||||
ItemDiscountPercentage.create({ value: v })
|
||||
DiscountPercentage.create({ value: v })
|
||||
),
|
||||
`items[${index}].item_discount_percentage_value`,
|
||||
errors
|
||||
@ -112,9 +114,7 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
|
||||
);
|
||||
|
||||
const globalDiscountPercentage = extractOrPushError(
|
||||
maybeFromNullableResult(raw.global_discount_percentage_value, (v) =>
|
||||
DiscountPercentage.create({ value: v })
|
||||
),
|
||||
DiscountPercentage.create({ value: raw.global_discount_percentage_value }),
|
||||
`items[${index}].global_discount_percentage_value`,
|
||||
errors
|
||||
);
|
||||
@ -149,9 +149,7 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
|
||||
const ivaCode = maybeFromNullableOrEmptyString(raw.iva_code);
|
||||
|
||||
const ivaPercentage = extractOrPushError(
|
||||
maybeFromNullableResult(raw.iva_percentage_value, (value) =>
|
||||
ItemTaxPercentage.create({ value })
|
||||
),
|
||||
maybeFromNullableResult(raw.iva_percentage_value, (value) => TaxPercentage.create({ value })),
|
||||
`items[${index}].iva_percentage_value`,
|
||||
errors
|
||||
);
|
||||
@ -168,9 +166,7 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
|
||||
const recCode = maybeFromNullableOrEmptyString(raw.rec_code);
|
||||
|
||||
const recPercentage = extractOrPushError(
|
||||
maybeFromNullableResult(raw.rec_percentage_value, (value) =>
|
||||
ItemTaxPercentage.create({ value })
|
||||
),
|
||||
maybeFromNullableResult(raw.rec_percentage_value, (value) => TaxPercentage.create({ value })),
|
||||
`items[${index}].rec_percentage_value`,
|
||||
errors
|
||||
);
|
||||
@ -188,7 +184,7 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
|
||||
|
||||
const retentionPercentage = extractOrPushError(
|
||||
maybeFromNullableResult(raw.retention_percentage_value, (value) =>
|
||||
ItemTaxPercentage.create({ value })
|
||||
TaxPercentage.create({ value })
|
||||
),
|
||||
`items[${index}].retention_percentage_value`,
|
||||
errors
|
||||
@ -263,7 +259,7 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
|
||||
const { errors, index } = params as {
|
||||
index: number;
|
||||
errors: ValidationErrorDetail[];
|
||||
attributes: Partial<IIssuedInvoiceProps>;
|
||||
attributes: Partial<IIssuedInvoiceCreateProps>;
|
||||
};
|
||||
|
||||
// 1) Valores escalares (atributos generales)
|
||||
@ -277,7 +273,8 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
|
||||
}
|
||||
|
||||
// 2) Construcción del elemento de dominio
|
||||
const createResult = IssuedInvoiceItem.create(
|
||||
const itemId = attributes.itemId!;
|
||||
const newItem = IssuedInvoiceItem.rehydrate(
|
||||
{
|
||||
description: attributes.description!,
|
||||
|
||||
@ -314,18 +311,10 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
|
||||
languageCode: attributes.languageCode!,
|
||||
currencyCode: attributes.currencyCode!,
|
||||
},
|
||||
attributes.itemId
|
||||
itemId
|
||||
);
|
||||
|
||||
if (createResult.isFailure) {
|
||||
return Result.fail(
|
||||
new ValidationErrorCollection("Invoice item entity creation failed", [
|
||||
{ path: `items[${index}]`, message: createResult.error.message },
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
return createResult;
|
||||
return Result.ok(newItem);
|
||||
}
|
||||
|
||||
public mapToPersistence(
|
||||
@ -364,18 +353,14 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
|
||||
),
|
||||
item_discount_percentage_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_scale: source.itemDiscountAmount.toPrimitive().scale,
|
||||
|
||||
global_discount_percentage_value: maybeToNullable(
|
||||
source.globalDiscountPercentage,
|
||||
(v) => v.toPrimitive().value
|
||||
),
|
||||
global_discount_percentage_value: source.globalDiscountPercentage.toPrimitive().value,
|
||||
global_discount_percentage_scale:
|
||||
maybeToNullable(source.globalDiscountPercentage, (v) => v.toPrimitive().scale) ??
|
||||
ItemDiscountPercentage.DEFAULT_SCALE,
|
||||
source.globalDiscountPercentage.toPrimitive().scale ?? DiscountPercentage.DEFAULT_SCALE,
|
||||
|
||||
global_discount_amount_value: source.globalDiscountAmount.value,
|
||||
global_discount_amount_scale: source.globalDiscountAmount.scale,
|
||||
|
||||
@ -16,7 +16,7 @@ import {
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import {
|
||||
type IIssuedInvoiceProps,
|
||||
type IIssuedInvoiceCreateProps,
|
||||
InvoiceRecipient,
|
||||
type IssuedInvoice,
|
||||
} from "../../../../../../domain";
|
||||
@ -33,7 +33,7 @@ export class SequelizeIssuedInvoiceRecipientDomainMapper {
|
||||
|
||||
const { errors, attributes } = params as {
|
||||
errors: ValidationErrorDetail[];
|
||||
attributes: Partial<IIssuedInvoiceProps>;
|
||||
attributes: Partial<IIssuedInvoiceCreateProps>;
|
||||
};
|
||||
|
||||
const _name = source.customer_name!;
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
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 {
|
||||
Percentage,
|
||||
UniqueID,
|
||||
@ -14,7 +19,7 @@ import {
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import {
|
||||
type IIssuedInvoiceProps,
|
||||
type IIssuedInvoiceCreateProps,
|
||||
InvoiceAmount,
|
||||
type IssuedInvoice,
|
||||
IssuedInvoiceTax,
|
||||
@ -64,7 +69,7 @@ export class SequelizeIssuedInvoiceTaxesDomainMapper extends SequelizeDomainMapp
|
||||
const { errors, index, attributes } = params as {
|
||||
index: number;
|
||||
errors: ValidationErrorDetail[];
|
||||
attributes: Partial<IIssuedInvoiceProps>;
|
||||
attributes: Partial<IIssuedInvoiceCreateProps>;
|
||||
};
|
||||
|
||||
const taxableAmount = extractOrPushError(
|
||||
@ -78,9 +83,10 @@ export class SequelizeIssuedInvoiceTaxesDomainMapper extends SequelizeDomainMapp
|
||||
|
||||
const ivaCode = raw.iva_code;
|
||||
|
||||
// Una issued invoice debe traer IVA
|
||||
const ivaPercentage = extractOrPushError(
|
||||
TaxPercentage.create({
|
||||
value: raw.iva_percentage_value,
|
||||
value: Number(raw.iva_percentage_value),
|
||||
}),
|
||||
`taxes[${index}].iva_percentage_value`,
|
||||
errors
|
||||
@ -210,7 +216,7 @@ export class SequelizeIssuedInvoiceTaxesDomainMapper extends SequelizeDomainMapp
|
||||
rec_percentage_value: maybeToNullable(source.recPercentage, (v) => v.toPrimitive().value),
|
||||
rec_percentage_scale:
|
||||
maybeToNullable(source.recPercentage, (v) => v.toPrimitive().scale) ??
|
||||
ItemDiscountPercentage.DEFAULT_SCALE,
|
||||
DiscountPercentage.DEFAULT_SCALE,
|
||||
|
||||
rec_amount_value: source.recAmount.toPrimitive().value,
|
||||
rec_amount_scale: source.recAmount.toPrimitive().scale ?? ItemAmount.DEFAULT_SCALE,
|
||||
|
||||
@ -12,7 +12,7 @@ import {
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import {
|
||||
type IIssuedInvoiceProps,
|
||||
type IIssuedInvoiceCreateProps,
|
||||
type IssuedInvoice,
|
||||
VerifactuRecord,
|
||||
VerifactuRecordEstado,
|
||||
@ -33,7 +33,7 @@ export class SequelizeIssuedInvoiceVerifactuDomainMapper extends SequelizeDomain
|
||||
): Result<Maybe<VerifactuRecord>, Error> {
|
||||
const { errors, attributes } = params as {
|
||||
errors: ValidationErrorDetail[];
|
||||
attributes: Partial<IIssuedInvoiceProps>;
|
||||
attributes: Partial<IIssuedInvoiceCreateProps>;
|
||||
};
|
||||
|
||||
if (!source) {
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export * from "./sequelize-issued-invoice-number-generator.service";
|
||||
@ -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}`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -23,13 +23,6 @@ type ProformaServicesContext = {
|
||||
};
|
||||
|
||||
export type ProformaPublicServices = {
|
||||
createProforma: (
|
||||
id: UniqueID,
|
||||
props: IProformaCreatorParams["props"],
|
||||
context: ProformaServicesContext
|
||||
) => Promise<Result<Proforma, Error>>;
|
||||
|
||||
listProformas: (filters: unknown, context: unknown) => null;
|
||||
getProformaById: (
|
||||
id: UniqueID,
|
||||
context: ProformaServicesContext
|
||||
@ -40,10 +33,14 @@ export type ProformaPublicServices = {
|
||||
context: ProformaServicesContext
|
||||
) => 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,
|
||||
deps: ProformasInternalDeps
|
||||
): ProformaPublicServices {
|
||||
@ -78,7 +75,6 @@ export function buildProformaServices(
|
||||
return Result.ok(createResult.data);
|
||||
},
|
||||
|
||||
listProformas: (filters, context) => null,
|
||||
//internal.useCases.listProformas().execute(filters, context),
|
||||
|
||||
getProformaById: async (id: UniqueID, context: ProformaServicesContext) => {
|
||||
@ -105,7 +101,7 @@ export function buildProformaServices(
|
||||
return Result.ok(fullSnapshot);
|
||||
},
|
||||
|
||||
generateProformaReport: (id, options, context) => null,
|
||||
//generateProformaReport: (id, options, context) => null,
|
||||
//internal.useCases.reportProforma().execute(id, options, context),
|
||||
};
|
||||
}
|
||||
|
||||
@ -3,15 +3,20 @@ import { type ModuleParams, buildCatalogs, buildTransactionManager } from "@erp/
|
||||
import {
|
||||
type CreateProformaUseCase,
|
||||
type GetProformaByIdUseCase,
|
||||
type IIssuedInvoicePublicServices,
|
||||
type IssueProformaUseCase,
|
||||
type ListProformasUseCase,
|
||||
type ReportProformaUseCase,
|
||||
buildCreateProformaUseCase,
|
||||
buildGetProformaByIdUseCase,
|
||||
buildIssueProformaUseCase,
|
||||
buildListProformasUseCase,
|
||||
buildProformaCreator,
|
||||
buildProformaFinder,
|
||||
buildProformaInputMappers,
|
||||
buildProformaIssuer,
|
||||
buildProformaSnapshotBuilders,
|
||||
buildProformaToIssuedInvoicePropsConverter,
|
||||
buildReportProformaUseCase,
|
||||
} from "../../../application";
|
||||
|
||||
@ -26,6 +31,9 @@ export type ProformasInternalDeps = {
|
||||
getProformaById: () => GetProformaByIdUseCase;
|
||||
reportProforma: () => ReportProformaUseCase;
|
||||
createProforma: () => CreateProformaUseCase;
|
||||
issueProforma: (publicServices: {
|
||||
issuedInvoiceServices: IIssuedInvoicePublicServices;
|
||||
}) => IssueProformaUseCase;
|
||||
|
||||
/*
|
||||
updateProforma: () => UpdateProformaUseCase;
|
||||
@ -44,12 +52,18 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
|
||||
const persistenceMappers = buildProformaPersistenceMappers(catalogs);
|
||||
|
||||
const repository = buildProformaRepository({ database, mappers: persistenceMappers });
|
||||
const numberService = buildProformaNumberGenerator();
|
||||
const proformaNumberService = buildProformaNumberGenerator();
|
||||
|
||||
// Application helpers
|
||||
const inputMappers = buildProformaInputMappers(catalogs);
|
||||
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 documentGeneratorPipeline = buildProformaDocumentService(params);
|
||||
@ -87,6 +101,14 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
|
||||
fullSnapshotBuilder: snapshotBuilders.full,
|
||||
transactionManager,
|
||||
}),
|
||||
|
||||
issueProforma: (publicServices: { issuedInvoiceServices: IIssuedInvoicePublicServices }) =>
|
||||
buildIssueProformaUseCase({
|
||||
publicServices,
|
||||
finder,
|
||||
issuer,
|
||||
transactionManager,
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
//export * from "./create-proforma.controller";
|
||||
//export * from "./delete-proforma.controller";
|
||||
export * from "./get-proforma.controller";
|
||||
//export * from "./issue-proforma.controller";
|
||||
export * from "./issue-proforma.controller";
|
||||
export * from "./list-proformas.controller";
|
||||
export * from "./report-proforma.controller";
|
||||
//export * from "./update-proforma.controller";
|
||||
|
||||
@ -5,7 +5,7 @@ import {
|
||||
requireCompanyContextGuard,
|
||||
} 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";
|
||||
|
||||
export class IssueProformaController extends ExpressController {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { JsonTaxCatalogProvider } from "@erp/core";
|
||||
import { DiscountPercentage } from "@erp/core/api";
|
||||
import {
|
||||
CurrencyCode,
|
||||
DomainError,
|
||||
@ -57,7 +58,7 @@ export class CreateProformaRequestMapper {
|
||||
try {
|
||||
this.errors = [];
|
||||
|
||||
const defaultStatus = InvoiceStatus.fromDraft();
|
||||
const defaultStatus = InvoiceStatus.draft();
|
||||
|
||||
const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", this.errors);
|
||||
|
||||
@ -209,7 +210,7 @@ export class CreateProformaRequestMapper {
|
||||
|
||||
const discountPercentage = extractOrPushError(
|
||||
maybeFromNullableResult(item.item_discount_percentage, (value) =>
|
||||
ItemDiscountPercentage.create(value)
|
||||
DiscountPercentage.create(value)
|
||||
),
|
||||
"discount_percentage",
|
||||
this.errors
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
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 {
|
||||
GetProformaController,
|
||||
IssueProformaController,
|
||||
ListProformasController,
|
||||
type ProformasInternalDeps,
|
||||
ReportProformaController,
|
||||
@ -12,18 +13,30 @@ import {
|
||||
import {
|
||||
CreateProformaRequestSchema,
|
||||
GetProformaByIdRequestSchema,
|
||||
IssueProformaByIdParamsRequestSchema,
|
||||
ListProformasRequestSchema,
|
||||
ReportProformaByIdParamsRequestSchema,
|
||||
ReportProformaByIdQueryRequestSchema,
|
||||
} from "../../../../common";
|
||||
import type { IIssuedInvoicePublicServices } from "../../../application";
|
||||
|
||||
import { CreateProformaController } from "./controllers/create-proforma.controller";
|
||||
|
||||
export const proformasRouter = (params: ModuleParams, deps: ProformasInternalDeps) => {
|
||||
const { app, config } = params;
|
||||
export const proformasRouter = (params: StartParams) => {
|
||||
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 });
|
||||
|
||||
// ----------------------------------------------
|
||||
|
||||
if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "production") {
|
||||
// 🔐 Autenticación + Tenancy para TODO el router
|
||||
router.use(
|
||||
@ -131,18 +144,18 @@ export const proformasRouter = (params: ModuleParams, deps: ProformasInternalDep
|
||||
}
|
||||
);*/
|
||||
|
||||
/*router.put(
|
||||
router.put(
|
||||
"/:proforma_id/issue",
|
||||
//checkTabContext,
|
||||
|
||||
validateRequest(IssueProformaByIdParamsRequestSchema, "params"),
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const useCase = deps.useCases.issue_proforma();
|
||||
const controller = new IssuedProformaController(useCase);
|
||||
const useCase = deps.useCases.issueProforma(publicServices);
|
||||
const controller = new IssueProformaController(useCase);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
);*/
|
||||
);
|
||||
|
||||
app.use(`${config.server.apiBasePath}/proformas`, router);
|
||||
};
|
||||
|
||||
@ -58,12 +58,6 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
|
||||
|
||||
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 series = extractOrPushError(
|
||||
@ -160,7 +154,6 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
|
||||
invoiceId,
|
||||
companyId,
|
||||
customerId,
|
||||
proformaId,
|
||||
status,
|
||||
series,
|
||||
invoiceNumber,
|
||||
@ -244,8 +237,8 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
|
||||
paymentMethod: attributes.paymentMethod!,
|
||||
};
|
||||
|
||||
const invoiceId = attributes.invoiceId!;
|
||||
const proforma = Proforma.rehydrate(invoiceProps, items, invoiceId);
|
||||
const proformaId = attributes.invoiceId!;
|
||||
const proforma = Proforma.rehydrate(invoiceProps, items, proformaId);
|
||||
|
||||
return Result.ok(proforma);
|
||||
} catch (err: unknown) {
|
||||
|
||||
@ -75,7 +75,7 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
|
||||
|
||||
const quantity = extractOrPushError(
|
||||
maybeFromNullableResult(raw.quantity_value, (v) => ItemQuantity.create({ value: v })),
|
||||
`items[${index}].quantity`,
|
||||
`items[${index}].quantity_value`,
|
||||
errors
|
||||
);
|
||||
|
||||
@ -83,7 +83,7 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
|
||||
maybeFromNullableResult(raw.unit_amount_value, (value) =>
|
||||
ItemAmount.create({ value, currency_code: parent.currencyCode?.code })
|
||||
),
|
||||
`items[${index}].unit_amount`,
|
||||
`items[${index}].unit_amount_value`,
|
||||
errors
|
||||
);
|
||||
|
||||
@ -163,26 +163,20 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
|
||||
const itemId = attributes.itemId!;
|
||||
const newItem = ProformaItem.rehydrate(
|
||||
{
|
||||
languageCode: attributes.languageCode!,
|
||||
currencyCode: attributes.currencyCode!,
|
||||
description: attributes.description!,
|
||||
quantity: attributes.quantity!,
|
||||
unitAmount: attributes.unitAmount!,
|
||||
|
||||
itemDiscountPercentage: attributes.itemDiscountPercentage!,
|
||||
globalDiscountPercentage: attributes.globalDiscountPercentage!,
|
||||
taxes: taxesResult.data,
|
||||
|
||||
languageCode: attributes.languageCode!,
|
||||
currencyCode: attributes.currencyCode!,
|
||||
},
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@ -31,13 +31,6 @@ export class SequelizeProformaRecipientDomainMapper {
|
||||
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 _tin = source.current_customer.tin;
|
||||
const _street = source.current_customer.street;
|
||||
|
||||
@ -89,8 +89,8 @@ export const GetIssuedInvoiceByIdResponseSchema = z.object({
|
||||
|
||||
subtotal_amount: MoneySchema,
|
||||
|
||||
discount_percentage: PercentageSchema,
|
||||
discount_amount: MoneySchema,
|
||||
item_discount_percentage: PercentageSchema,
|
||||
item_discount_amount: MoneySchema,
|
||||
|
||||
global_discount_percentage: PercentageSchema,
|
||||
global_discount_amount: MoneySchema,
|
||||
|
||||
@ -83,8 +83,8 @@ export const GetProformaByIdResponseSchema = z.object({
|
||||
|
||||
subtotal_amount: MoneySchema,
|
||||
|
||||
discount_percentage: PercentageSchema,
|
||||
discount_amount: MoneySchema,
|
||||
item_discount_percentage: PercentageSchema,
|
||||
item_discount_amount: MoneySchema,
|
||||
|
||||
global_discount_percentage: PercentageSchema,
|
||||
global_discount_amount: MoneySchema,
|
||||
|
||||
@ -19,24 +19,24 @@ import * as React from "react";
|
||||
import { useTranslation } from "../../../../../i18n";
|
||||
import {
|
||||
PROFORMA_STATUS_TRANSITIONS,
|
||||
type ProformaListRow,
|
||||
type ProformaStatus,
|
||||
type ProformaSummaryData,
|
||||
} from "../../../../types";
|
||||
} from "../../../../shared";
|
||||
import { ProformaStatusBadge } from "../../components";
|
||||
|
||||
type GridActionHandlers = {
|
||||
onEditClick?: (proforma: ProformaSummaryData) => void;
|
||||
onIssueClick?: (proforma: ProformaSummaryData) => void;
|
||||
onChangeStatusClick?: (proforma: ProformaSummaryData, nextStatus: string) => void;
|
||||
onDeleteClick?: (proforma: ProformaSummaryData) => void;
|
||||
onEditClick?: (proforma: ProformaListRow) => void;
|
||||
onIssueClick?: (proforma: ProformaListRow) => void;
|
||||
onChangeStatusClick?: (proforma: ProformaListRow, nextStatus: string) => void;
|
||||
onDeleteClick?: (proforma: ProformaListRow) => void;
|
||||
};
|
||||
|
||||
export function useProformasGridColumns(
|
||||
actionHandlers: GridActionHandlers = {}
|
||||
): ColumnDef<ProformaSummaryData, unknown>[] {
|
||||
): ColumnDef<ProformaListRow, unknown>[] {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return React.useMemo<ColumnDef<ProformaSummaryData, unknown>[]>(
|
||||
return React.useMemo<ColumnDef<ProformaListRow, unknown>[]>(
|
||||
() => [
|
||||
/*{
|
||||
id: "select",
|
||||
|
||||
@ -3,11 +3,11 @@ import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import {
|
||||
type ProformaStatus,
|
||||
getProformaStatusButtonVariant,
|
||||
getProformaStatusColor,
|
||||
getProformaStatusIcon,
|
||||
} from "../../../types";
|
||||
} from "../../../change-status/helpers";
|
||||
import type { ProformaStatus } from "../../../shared";
|
||||
|
||||
export type ProformaStatusBadgeProps = {
|
||||
status: string | ProformaStatus; // permitir cualquier valor
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
import { DuplicateEntityError } from "@erp/core/api";
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import type { Transaction } from "sequelize";
|
||||
|
||||
import { Customer, type ICustomerCreateProps } from "../../domain";
|
||||
import type { ICustomerRepository } from "../repositories";
|
||||
@ -12,7 +11,7 @@ export interface ICustomerCreator {
|
||||
companyId: UniqueID;
|
||||
id: UniqueID;
|
||||
props: ICustomerCreateProps;
|
||||
transaction: Transaction;
|
||||
unknown: unknown;
|
||||
}): Promise<Result<Customer, Error>>;
|
||||
}
|
||||
|
||||
@ -31,16 +30,12 @@ export class CustomerCreator implements ICustomerCreator {
|
||||
companyId: UniqueID;
|
||||
id: UniqueID;
|
||||
props: ICustomerCreateProps;
|
||||
transaction: Transaction;
|
||||
unknown: unknown;
|
||||
}): Promise<Result<Customer, Error>> {
|
||||
const { companyId, id, props, transaction } = params;
|
||||
const { companyId, id, props, unknown } = params;
|
||||
|
||||
// 1. Verificar unicidad
|
||||
const spec = new CustomerNotExistsInCompanySpecification(
|
||||
this.repository,
|
||||
companyId,
|
||||
transaction
|
||||
);
|
||||
const spec = new CustomerNotExistsInCompanySpecification(this.repository, companyId, unknown);
|
||||
|
||||
const isNew = await spec.isSatisfiedBy(id);
|
||||
|
||||
@ -64,7 +59,7 @@ export class CustomerCreator implements ICustomerCreator {
|
||||
const newCustomer = createResult.data;
|
||||
|
||||
// 3. Persistir agregado
|
||||
const saveResult = await this.repository.create(newCustomer, transaction);
|
||||
const saveResult = await this.repository.create(newCustomer, unknown);
|
||||
|
||||
if (saveResult.isFailure) {
|
||||
return Result.fail(saveResult.error);
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import type { Criteria } from "@repo/rdx-criteria/server";
|
||||
import type { TINNumber, UniqueID } from "@repo/rdx-ddd";
|
||||
import type { Collection, Result } from "@repo/rdx-utils";
|
||||
import type { Transaction } from "sequelize";
|
||||
|
||||
import type { Customer } from "../../domain";
|
||||
import type { CustomerSummary } from "../models";
|
||||
@ -11,25 +10,25 @@ export interface ICustomerFinder {
|
||||
findCustomerById(
|
||||
companyId: UniqueID,
|
||||
customerId: UniqueID,
|
||||
transaction?: Transaction
|
||||
unknown?: unknown
|
||||
): Promise<Result<Customer, Error>>;
|
||||
|
||||
findCustomerByTIN(
|
||||
companyId: UniqueID,
|
||||
tin: TINNumber,
|
||||
transaction?: Transaction
|
||||
unknown?: unknown
|
||||
): Promise<Result<Customer, Error>>;
|
||||
|
||||
customerExists(
|
||||
companyId: UniqueID,
|
||||
invoiceId: UniqueID,
|
||||
transaction?: Transaction
|
||||
unknown?: unknown
|
||||
): Promise<Result<boolean, Error>>;
|
||||
|
||||
findCustomersByCriteria(
|
||||
companyId: UniqueID,
|
||||
criteria: Criteria,
|
||||
transaction?: Transaction
|
||||
unknown?: unknown
|
||||
): Promise<Result<Collection<CustomerSummary>, Error>>;
|
||||
}
|
||||
|
||||
@ -39,32 +38,32 @@ export class CustomerFinder implements ICustomerFinder {
|
||||
async findCustomerById(
|
||||
companyId: UniqueID,
|
||||
customerId: UniqueID,
|
||||
transaction?: Transaction
|
||||
unknown?: unknown
|
||||
): Promise<Result<Customer, Error>> {
|
||||
return this.repository.getByIdInCompany(companyId, customerId, transaction);
|
||||
return this.repository.getByIdInCompany(companyId, customerId, unknown);
|
||||
}
|
||||
|
||||
findCustomerByTIN(
|
||||
companyId: UniqueID,
|
||||
tin: TINNumber,
|
||||
transaction?: Transaction
|
||||
unknown?: unknown
|
||||
): Promise<Result<Customer, Error>> {
|
||||
return this.repository.getByTINInCompany(companyId, tin, transaction);
|
||||
return this.repository.getByTINInCompany(companyId, tin, unknown);
|
||||
}
|
||||
|
||||
async customerExists(
|
||||
companyId: UniqueID,
|
||||
customerId: UniqueID,
|
||||
transaction?: Transaction
|
||||
unknown?: unknown
|
||||
): Promise<Result<boolean, Error>> {
|
||||
return this.repository.existsByIdInCompany(companyId, customerId, transaction);
|
||||
return this.repository.existsByIdInCompany(companyId, customerId, unknown);
|
||||
}
|
||||
|
||||
async findCustomersByCriteria(
|
||||
companyId: UniqueID,
|
||||
criteria: Criteria,
|
||||
transaction?: Transaction
|
||||
unknown?: unknown
|
||||
): Promise<Result<Collection<CustomerSummary>, Error>> {
|
||||
return this.repository.findByCriteriaInCompany(companyId, criteria, transaction);
|
||||
return this.repository.findByCriteriaInCompany(companyId, criteria, unknown);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>>;
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import type { Transaction } from "sequelize";
|
||||
|
||||
import type { Customer, CustomerPatchProps } from "../../domain";
|
||||
import type { ICustomerRepository } from "../repositories";
|
||||
@ -10,7 +9,7 @@ export interface ICustomerUpdater {
|
||||
companyId: UniqueID;
|
||||
id: UniqueID;
|
||||
props: CustomerPatchProps;
|
||||
transaction: Transaction;
|
||||
transaction: unknown;
|
||||
}): Promise<Result<Customer, Error>>;
|
||||
}
|
||||
|
||||
@ -29,7 +28,7 @@ export class CustomerUpdater implements ICustomerUpdater {
|
||||
companyId: UniqueID;
|
||||
id: UniqueID;
|
||||
props: CustomerPatchProps;
|
||||
transaction: Transaction;
|
||||
transaction: unknown;
|
||||
}): Promise<Result<Customer, Error>> {
|
||||
const { companyId, id, props, transaction } = params;
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
export * from "./customer-creator";
|
||||
export * from "./customer-finder";
|
||||
export * from "./customer-public-services.interface";
|
||||
export * from "./customer-updater";
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { CompositeSpecification, type UniqueID } from "@repo/rdx-ddd";
|
||||
import type { Transaction } from "sequelize";
|
||||
|
||||
import type { ICustomerRepository } from "../../application";
|
||||
import { logger } from "../../helpers";
|
||||
@ -8,7 +7,7 @@ export class CustomerNotExistsInCompanySpecification extends CompositeSpecificat
|
||||
constructor(
|
||||
private readonly repository: ICustomerRepository,
|
||||
private readonly companyId: UniqueID,
|
||||
private readonly transaction?: Transaction
|
||||
private readonly transaction?: unknown
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import type { ITransactionManager } from "@erp/core/api";
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import type { Transaction } from "sequelize";
|
||||
|
||||
import type { CreateCustomerRequestDTO } from "../../../common";
|
||||
import type { ICreateCustomerInputMapper } from "../mappers";
|
||||
@ -43,7 +42,7 @@ export class CreateCustomerUseCase {
|
||||
|
||||
const { props, id } = mappedPropsResult.data;
|
||||
|
||||
return this.transactionManager.complete(async (transaction: Transaction) => {
|
||||
return this.transactionManager.complete(async (transaction: unknown) => {
|
||||
try {
|
||||
const createResult = await this.creator.create({ companyId, id, props, transaction });
|
||||
|
||||
|
||||
@ -2,7 +2,6 @@ import type { ITransactionManager } from "@erp/core/api";
|
||||
import type { Criteria } from "@repo/rdx-criteria/server";
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import type { Transaction } from "sequelize";
|
||||
|
||||
import type { ICustomerFinder } from "../services";
|
||||
import type { ICustomerSummarySnapshotBuilder } from "../snapshot-builders/summary";
|
||||
@ -22,7 +21,7 @@ export class ListCustomersUseCase {
|
||||
public execute(params: ListCustomersUseCaseInput) {
|
||||
const { criteria, companyId } = params;
|
||||
|
||||
return this.transactionManager.complete(async (transaction: Transaction) => {
|
||||
return this.transactionManager.complete(async (transaction: unknown) => {
|
||||
try {
|
||||
const result = await this.finder.findCustomersByCriteria(companyId, criteria, transaction);
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import type { ITransactionManager } from "@erp/core/api";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import type { Transaction } from "sequelize";
|
||||
|
||||
import type { UpdateCustomerByIdRequestDTO } from "../../../../common/dto";
|
||||
import type { CustomerPatchProps } from "../../../domain";
|
||||
@ -52,7 +51,7 @@ export class UpdateCustomerUseCase {
|
||||
|
||||
const patchProps: CustomerPatchProps = patchPropsResult.data;
|
||||
|
||||
return this.transactionManager.complete(async (transaction: Transaction) => {
|
||||
return this.transactionManager.complete(async (transaction: unknown) => {
|
||||
try {
|
||||
const updateResult = await this.updater.update({
|
||||
companyId,
|
||||
|
||||
@ -3,7 +3,7 @@ import type { IModuleServer } from "@erp/core/api";
|
||||
import { type CustomerPublicServices, customersRouter, models } from "./infrastructure";
|
||||
import {
|
||||
type CustomersInternalDeps,
|
||||
buildCustomerServices,
|
||||
buildCustomerPublicServices,
|
||||
buildCustomersDependencies,
|
||||
} from "./infrastructure/di";
|
||||
|
||||
@ -30,7 +30,7 @@ export const customersAPIModule: IModuleServer = {
|
||||
const internal = buildCustomersDependencies(params);
|
||||
|
||||
// 2) Servicios públicos (Application Services)
|
||||
const customersServices: CustomerPublicServices = buildCustomerServices(params, internal);
|
||||
const customersServices: CustomerPublicServices = buildCustomerPublicServices(params, internal);
|
||||
|
||||
logger.info("🚀 Customers module dependencies registered", {
|
||||
label: this.name,
|
||||
|
||||
@ -1,38 +1,23 @@
|
||||
import { type SetupParams, buildCatalogs } from "@erp/core/api";
|
||||
import type { TINNumber, UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import type { Transaction } from "sequelize";
|
||||
|
||||
import { buildCustomerCreator, buildCustomerFinder } from "../../application";
|
||||
import type { Customer, ICustomerCreateProps } from "../../domain";
|
||||
import {
|
||||
type ICustomerPublicServices,
|
||||
type ICustomerServicesContext,
|
||||
buildCustomerCreator,
|
||||
buildCustomerFinder,
|
||||
} from "../../application";
|
||||
import type { ICustomerCreateProps } from "../../domain";
|
||||
|
||||
import { buildCustomerPersistenceMappers } from "./customer-persistence-mappers.di";
|
||||
import { buildCustomerRepository } from "./customer-repositories.di";
|
||||
import type { CustomersInternalDeps } from "./customers.di";
|
||||
|
||||
type CustomerServicesContext = {
|
||||
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(
|
||||
export function buildCustomerPublicServices(
|
||||
params: SetupParams,
|
||||
deps: CustomersInternalDeps
|
||||
): CustomerPublicServices {
|
||||
): ICustomerPublicServices {
|
||||
const { database } = params;
|
||||
const catalogs = buildCatalogs();
|
||||
|
||||
@ -44,7 +29,7 @@ export function buildCustomerServices(
|
||||
const creator = buildCustomerCreator({ repository });
|
||||
|
||||
return {
|
||||
findCustomerByTIN: async (tin: TINNumber, context: CustomerServicesContext) => {
|
||||
findCustomerByTIN: async (tin: TINNumber, context: ICustomerServicesContext) => {
|
||||
const { companyId, transaction } = context;
|
||||
|
||||
const customerResult = await finder.findCustomerByTIN(companyId, tin, transaction);
|
||||
@ -59,7 +44,7 @@ export function buildCustomerServices(
|
||||
createCustomer: async (
|
||||
id: UniqueID,
|
||||
props: ICustomerCreateProps,
|
||||
context: CustomerServicesContext
|
||||
context: ICustomerServicesContext
|
||||
) => {
|
||||
const { companyId, transaction } = context;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user