Alta de proformas

This commit is contained in:
David Arranz 2026-03-03 17:00:32 +01:00
parent 941ad25401
commit bba38e67f2
30 changed files with 208 additions and 187 deletions

View File

@ -8,8 +8,7 @@
"dev": "node --import=tsx --watch src/index.ts", "dev": "node --import=tsx --watch src/index.ts",
"clean": "rimraf .turbo node_modules dist", "clean": "rimraf .turbo node_modules dist",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"lint": "biome check . && eslint .", "lint": "biome lint --fix",
"lint:fix": "biome check --write . && eslint . --fix",
"format": "biome format --write" "format": "biome format --write"
}, },
"devDependencies": { "devDependencies": {

View File

@ -41,13 +41,20 @@ import {
* *
*/ */
export interface ICreateProformaInputMapper /*export interface ICreateProformaInputMapper
extends IDTOInputToPropsMapper< extends IDTOInputToPropsMapper<
CreateProformaRequestDTO, CreateProformaRequestDTO,
{ id: UniqueID; props: Omit<IProformaProps, "items"> & { items: IProformaItemProps[] } } { id: UniqueID; props: Omit<IProformaProps, "items"> & { items: IProformaItemProps[] } }
> {} > {}*/
export class CreateProformaInputMapper implements ICreateProformaInputMapper { export interface ICreateProformaInputMapper {
map(
dto: CreateProformaRequestDTO,
params: { companyId: UniqueID }
): Result<{ id: UniqueID; props: IProformaProps }>;
}
export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/ {
private readonly taxCatalog: JsonTaxCatalogProvider; private readonly taxCatalog: JsonTaxCatalogProvider;
constructor(params: { taxCatalog: JsonTaxCatalogProvider }) { constructor(params: { taxCatalog: JsonTaxCatalogProvider }) {
@ -138,14 +145,14 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
const globalDiscountPercentage = extractOrPushError( const globalDiscountPercentage = extractOrPushError(
Percentage.create({ Percentage.create({
value: Number(dto.discount_percentage.value), value: Number(dto.global_discount_percentage.value),
scale: Number(dto.discount_percentage.scale), scale: Number(dto.global_discount_percentage.scale),
}), }),
"discount_percentage", "discount_percentage",
errors errors
); );
const items = this.mapItems(dto, { const itemsProps = this.mapItemsProps(dto, {
languageCode: languageCode!, languageCode: languageCode!,
currencyCode: currencyCode!, currencyCode: currencyCode!,
globalDiscountPercentage: globalDiscountPercentage!, globalDiscountPercentage: globalDiscountPercentage!,
@ -158,7 +165,7 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
); );
} }
const props: Omit<IProformaProps, "items"> & { items: IProformaItemProps[] } = { const props: IProformaProps = {
companyId, companyId,
status: defaultStatus, status: defaultStatus,
@ -181,7 +188,7 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
paymentMethod: paymentMethod!, paymentMethod: paymentMethod!,
globalDiscountPercentage: globalDiscountPercentage!, globalDiscountPercentage: globalDiscountPercentage!,
items, // ← IProformaItemProps[] items: itemsProps, // ← IProformaItemProps[]
}; };
return Result.ok({ return Result.ok({
@ -193,7 +200,7 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
} }
} }
private mapItems( private mapItemsProps(
dto: CreateProformaRequestDTO, dto: CreateProformaRequestDTO,
params: { params: {
languageCode: LanguageCode; languageCode: LanguageCode;
@ -224,12 +231,12 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
); );
const discountPercentage = extractOrPushError( const discountPercentage = extractOrPushError(
maybeFromNullableResult(item.discount_percentage, (v) => DiscountPercentage.create(v)), maybeFromNullableResult(item.item_discount_percentage, (v) => DiscountPercentage.create(v)),
`items[${index}].discount_percentage`, `items[${index}].discount_percentage`,
params.errors params.errors
); );
const taxes = this.mapTaxes(item.taxes, { const taxes = this.mapTaxesProps(item.taxes, {
itemIndex: index, itemIndex: index,
errors: params.errors, errors: params.errors,
}); });
@ -252,7 +259,7 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
/* Devuelve las propiedades de los impustos de una línea de detalle */ /* Devuelve las propiedades de los impustos de una línea de detalle */
private mapTaxes( private mapTaxesProps(
taxesDTO: Pick<CreateProformaItemRequestDTO, "taxes">["taxes"], taxesDTO: Pick<CreateProformaItemRequestDTO, "taxes">["taxes"],
params: { itemIndex: number; errors: ValidationErrorDetail[] } params: { itemIndex: number; errors: ValidationErrorDetail[] }
): ProformaItemTaxesProps { ): ProformaItemTaxesProps {

View File

@ -1,4 +1,3 @@
import { InvoiceSerie, type ProformaPatchProps } from "@erp/customer-invoices/api/domain";
import { import {
CurrencyCode, CurrencyCode,
DomainError, DomainError,
@ -14,6 +13,7 @@ import {
import { Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils"; import { Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils";
import type { UpdateProformaByIdRequestDTO } from "../../../../../common/dto"; import type { UpdateProformaByIdRequestDTO } from "../../../../../common/dto";
import type { ProformaPatchProps } from "../../../../domain";
/** /**
* UpdateProformaPropsMapper * UpdateProformaPropsMapper

View File

@ -2,31 +2,31 @@ import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize"; import type { Transaction } from "sequelize";
import type { ICustomerInvoiceRepository } from "../../../domain"; import type { IProformaProps, Proforma } from "../../../domain";
import type { CustomerInvoice, CustomerInvoiceProps } from "../../../domain/aggregates";
import type { IProformaFactory } from "../factories"; import type { IProformaFactory } from "../factories";
import type { IProformaRepository } from "../repositories";
import type { IProformaNumberGenerator } from "./proforma-number-generator.interface"; import type { IProformaNumberGenerator } from "./proforma-number-generator.interface";
export interface IProformaCreator { export interface IProformaCreator {
create( create(params: {
companyId: UniqueID, companyId: UniqueID;
id: UniqueID, id: UniqueID;
props: CustomerInvoiceProps, props: IProformaProps;
transaction: Transaction transaction: Transaction;
): Promise<Result<CustomerInvoice, Error>>; }): Promise<Result<Proforma, Error>>;
} }
type ProformaCreatorDeps = { type ProformaCreatorDeps = {
numberService: IProformaNumberGenerator; numberService: IProformaNumberGenerator;
factory: IProformaFactory; factory: IProformaFactory;
repository: ICustomerInvoiceRepository; repository: IProformaRepository;
}; };
export class ProformaCreator implements IProformaCreator { export class ProformaCreator implements IProformaCreator {
private readonly numberService: IProformaNumberGenerator; private readonly numberService: IProformaNumberGenerator;
private readonly factory: IProformaFactory; private readonly factory: IProformaFactory;
private readonly repository: ICustomerInvoiceRepository; private readonly repository: IProformaRepository;
constructor(deps: ProformaCreatorDeps) { constructor(deps: ProformaCreatorDeps) {
this.numberService = deps.numberService; this.numberService = deps.numberService;
@ -34,12 +34,14 @@ export class ProformaCreator implements IProformaCreator {
this.repository = deps.repository; this.repository = deps.repository;
} }
async create( async create(params: {
companyId: UniqueID, companyId: UniqueID;
id: UniqueID, id: UniqueID;
props: CustomerInvoiceProps, props: IProformaProps;
transaction: Transaction transaction: Transaction;
): Promise<Result<CustomerInvoice, Error>> { }): Promise<Result<Proforma, Error>> {
const { companyId, id, props, transaction } = params;
// 1. Obtener siguiente número // 1. Obtener siguiente número
const { series } = props; const { series } = props;
const numberResult = await this.numberService.getNextForCompany(companyId, series, transaction); const numberResult = await this.numberService.getNextForCompany(companyId, series, transaction);

View File

@ -36,16 +36,16 @@ export class CreateProformaUseCase {
const { dto, companyId } = params; const { dto, companyId } = params;
// 1) Mapear DTO → props de dominio // 1) Mapear DTO → props de dominio
const mappedResult = this.dtoMapper.map(dto, companyId); const mappedPropsResult = this.dtoMapper.map(dto, { companyId });
if (mappedResult.isFailure) { if (mappedPropsResult.isFailure) {
return Result.fail(mappedResult.error); return Result.fail(mappedPropsResult.error);
} }
const { props, id } = mappedResult.data; const { props, id } = mappedPropsResult.data;
return this.transactionManager.complete(async (transaction) => { return this.transactionManager.complete(async (transaction) => {
try { try {
const createResult = await this.creator.create(companyId, id, props, transaction); const createResult = await this.creator.create({ companyId, id, props, transaction });
if (createResult.isFailure) { if (createResult.isFailure) {
return Result.fail(createResult.error); return Result.fail(createResult.error);

View File

@ -23,10 +23,9 @@ import {
import { import {
type IProformaItemProps, type IProformaItemProps,
type IProformaItems, type IProformaItems,
type IProformaItemsProps,
ProformaItem, ProformaItem,
ProformaItems, ProformaItems,
} from "../entities/proforma-items"; } from "../entities";
import { ProformaItemMismatch } from "../errors"; import { ProformaItemMismatch } from "../errors";
import { type IProformaTaxTotals, ProformaTaxesCalculator } from "../services"; import { type IProformaTaxTotals, ProformaTaxesCalculator } from "../services";
import { ProformaItemTaxes } from "../value-objects"; import { ProformaItemTaxes } from "../value-objects";
@ -53,7 +52,7 @@ export interface IProformaProps {
paymentMethod: Maybe<InvoicePaymentMethod>; paymentMethod: Maybe<InvoicePaymentMethod>;
items: IProformaItemsProps[]; items: IProformaItemProps[];
globalDiscountPercentage: DiscountPercentage; globalDiscountPercentage: DiscountPercentage;
} }
@ -96,21 +95,19 @@ export interface IProforma {
paymentMethod: Maybe<InvoicePaymentMethod>; paymentMethod: Maybe<InvoicePaymentMethod>;
items: IProformaItems; items: IProformaItems; // <- Colección
taxes(): Collection<IProformaTaxTotals>; taxes(): Collection<IProformaTaxTotals>;
totals(): IProformaTotals; totals(): IProformaTotals;
} }
export type ProformaPatchProps = Partial<Omit<IProformaProps, "companyId" | "items">> & { export type ProformaPatchProps = Partial<Omit<IProformaProps, "companyId" | "items">> & {
items?: ProformaItems; //items?: ProformaItems;
}; };
type CreateProformaProps = IProformaProps; type CreateProformaProps = IProformaProps;
type InternalProformaProps = Omit<IProformaProps, "items">; type InternalProformaProps = Omit<IProformaProps, "items">;
export class Proforma extends AggregateRoot<InternalProformaProps> implements IProforma { export class Proforma extends AggregateRoot<InternalProformaProps> implements IProforma {
private readonly _items: ProformaItems;
// Creación funcional // Creación funcional
static create(props: CreateProformaProps, id?: UniqueID): Result<Proforma, Error> { static create(props: CreateProformaProps, id?: UniqueID): Result<Proforma, Error> {
const { items, ...internalProps } = props; const { items, ...internalProps } = props;
@ -136,6 +133,8 @@ export class Proforma extends AggregateRoot<InternalProformaProps> implements IP
return new Proforma(props, id); return new Proforma(props, id);
} }
private readonly _items: ProformaItems;
protected constructor(props: InternalProformaProps, id?: UniqueID) { protected constructor(props: InternalProformaProps, id?: UniqueID) {
super(props, id); super(props, id);
@ -147,36 +146,7 @@ export class Proforma extends AggregateRoot<InternalProformaProps> implements IP
}); });
} }
// Mutabilidad
public update(
partialProforma: Partial<Omit<IProformaProps, "companyId">>
): Result<Proforma, Error> {
const updatedProps = {
...this.props,
...partialProforma,
} as IProformaProps;
return Proforma.create(updatedProps, this.id);
}
public issue(): Result<void, Error> {
if (!this.props.status.canTransitionTo("ISSUED")) {
return Result.fail(
new DomainValidationError(
"INVALID_STATE",
"status",
"Proforma cannot be issued from current state"
)
);
}
// Falta
//this.props.status = this.props.status.canTransitionTo("ISSUED");
return Result.ok();
}
// Getters // Getters
public get companyId(): UniqueID { public get companyId(): UniqueID {
return this.props.companyId; return this.props.companyId;
} }
@ -249,6 +219,34 @@ export class Proforma extends AggregateRoot<InternalProformaProps> implements IP
return this.paymentMethod.isSome(); return this.paymentMethod.isSome();
} }
// Mutabilidad
public update(
partialProforma: Partial<Omit<IProformaProps, "companyId">>
): Result<Proforma, Error> {
const updatedProps = {
...this.props,
...partialProforma,
} as IProformaProps;
return Proforma.create(updatedProps, this.id);
}
public issue(): Result<void, Error> {
if (!this.props.status.canTransitionTo("ISSUED")) {
return Result.fail(
new DomainValidationError(
"INVALID_STATE",
"status",
"Proforma cannot be issued from current state"
)
);
}
// Falta
//this.props.status = this.props.status.canTransitionTo("ISSUED");
return Result.ok();
}
// Cálculos // Cálculos
/** /**

View File

@ -217,6 +217,60 @@ export class ProformaItem extends DomainEntity<InternalProformaItemProps> implem
return this.taxes.retention.map((tax) => tax.percentage); return this.taxes.retention.map((tax) => tax.percentage);
} }
/**
* @summary Cálculo centralizado de todos los valores intermedios.
* @returns Devuelve un objeto inmutable con todos los valores necesarios:
* - subtotal
* - itemDiscount
* - globalDiscount
* - totalDiscount
* - taxableAmount
* - ivaAmount
* - recAmount
* - retentionAmount
* - taxesAmount
* - totalAmount
*
*/
public totals(): IProformaItemTotals {
const subtotalAmount = this._calculateSubtotalAmount();
const itemDiscountAmount = this._calculateItemDiscountAmount(subtotalAmount);
const globalDiscountAmount = this._calculateGlobalDiscountAmount(
subtotalAmount,
itemDiscountAmount
);
const totalDiscountAmount = this._calculateTotalDiscountAmount(
itemDiscountAmount,
globalDiscountAmount
);
const taxableAmount = subtotalAmount.subtract(totalDiscountAmount);
// Calcular impuestos individuales a partir de la base imponible
const { ivaAmount, recAmount, retentionAmount } = this.taxes.totals(taxableAmount);
const taxesAmount = ivaAmount.add(recAmount).add(retentionAmount);
const totalAmount = taxableAmount.add(taxesAmount);
return {
subtotalAmount,
itemDiscountAmount,
globalDiscountAmount,
totalDiscountAmount,
taxableAmount,
ivaAmount,
recAmount,
retentionAmount,
taxesAmount,
totalAmount,
};
}
// Cálculos / Ayudantes // Cálculos / Ayudantes
/** /**
@ -276,58 +330,4 @@ export class ProformaItem extends DomainEntity<InternalProformaItemProps> implem
) { ) {
return itemDiscountAmount.add(globalDiscountAmount); return itemDiscountAmount.add(globalDiscountAmount);
} }
/**
* @summary Cálculo centralizado de todos los valores intermedios.
* @returns Devuelve un objeto inmutable con todos los valores necesarios:
* - subtotal
* - itemDiscount
* - globalDiscount
* - totalDiscount
* - taxableAmount
* - ivaAmount
* - recAmount
* - retentionAmount
* - taxesAmount
* - totalAmount
*
*/
public totals(): IProformaItemTotals {
const subtotalAmount = this._calculateSubtotalAmount();
const itemDiscountAmount = this._calculateItemDiscountAmount(subtotalAmount);
const globalDiscountAmount = this._calculateGlobalDiscountAmount(
subtotalAmount,
itemDiscountAmount
);
const totalDiscountAmount = this._calculateTotalDiscountAmount(
itemDiscountAmount,
globalDiscountAmount
);
const taxableAmount = subtotalAmount.subtract(totalDiscountAmount);
// Calcular impuestos individuales a partir de la base imponible
const { ivaAmount, recAmount, retentionAmount } = this.taxes.totals(taxableAmount);
const taxesAmount = ivaAmount.add(recAmount).add(retentionAmount);
const totalAmount = taxableAmount.add(taxesAmount);
return {
subtotalAmount,
itemDiscountAmount,
globalDiscountAmount,
totalDiscountAmount,
taxableAmount,
ivaAmount,
recAmount,
retentionAmount,
taxesAmount,
totalAmount,
};
}
} }

View File

@ -6,14 +6,14 @@ import { ProformaItemMismatch } from "../../errors";
import { ProformaItemsTotalsCalculator } from "../../services/proforma-items-totals-calculator"; import { ProformaItemsTotalsCalculator } from "../../services/proforma-items-totals-calculator";
import type { import type {
ICreateProformaItemProps,
IProformaItem, IProformaItem,
IProformaItemProps,
IProformaItemTotals, IProformaItemTotals,
ProformaItem, ProformaItem,
} from "./proforma-item.entity"; } from "./proforma-item.entity";
export interface IProformaItemsProps { export interface IProformaItemsProps {
items?: ICreateProformaItemProps[]; items: IProformaItemProps[];
// Estos campos vienen de la cabecera, // Estos campos vienen de la cabecera,
// pero se necesitan para cálculos y representaciones de la línea. // pero se necesitan para cálculos y representaciones de la línea.
@ -22,6 +22,9 @@ export interface IProformaItemsProps {
currencyCode: CurrencyCode; // Para cálculos y formateos de moneda currencyCode: CurrencyCode; // Para cálculos y formateos de moneda
} }
type CreateProformaProps = IProformaItemsProps;
type InternalProformaProps = Omit<IProformaItemsProps, "items">;
export interface IProformaItems { export interface IProformaItems {
// OJO, no extendemos de Collection<IProformaItem> para no exponer // OJO, no extendemos de Collection<IProformaItem> para no exponer
// públicamente métodos para manipular la colección. // públicamente métodos para manipular la colección.
@ -35,12 +38,16 @@ export interface IProformaItems {
} }
export class ProformaItems extends Collection<ProformaItem> implements IProformaItems { export class ProformaItems extends Collection<ProformaItem> implements IProformaItems {
static create(props: CreateProformaProps): ProformaItems {
return new ProformaItems(props);
}
public readonly languageCode!: LanguageCode; public readonly languageCode!: LanguageCode;
public readonly currencyCode!: CurrencyCode; public readonly currencyCode!: CurrencyCode;
public readonly globalDiscountPercentage!: DiscountPercentage; public readonly globalDiscountPercentage!: DiscountPercentage;
constructor(props: IProformaItemsProps) { protected constructor(props: InternalProformaProps) {
super(props.items ?? []); super([]);
this.languageCode = props.languageCode; this.languageCode = props.languageCode;
this.currencyCode = props.currencyCode; this.currencyCode = props.currencyCode;
this.globalDiscountPercentage = props.globalDiscountPercentage; this.globalDiscountPercentage = props.globalDiscountPercentage;
@ -48,8 +55,15 @@ export class ProformaItems extends Collection<ProformaItem> implements IProforma
this.ensureSameCurrencyAndLanguage(this.items); this.ensureSameCurrencyAndLanguage(this.items);
} }
public static create(props: IProformaItemsProps): ProformaItems { public add(item: ProformaItem): boolean {
return new ProformaItems(props); const same =
this.languageCode.equals(item.languageCode) &&
this.currencyCode.equals(item.currencyCode) &&
this.globalDiscountPercentage.equals(item.globalDiscountPercentage);
if (!same) return false;
return super.add(item);
} }
public valued(): IProformaItem[] { public valued(): IProformaItem[] {

View File

@ -7,9 +7,10 @@ import {
buildIssuedInvoicesDependencies, buildIssuedInvoicesDependencies,
buildProformaServices, buildProformaServices,
buildProformasDependencies, buildProformasDependencies,
issuedInvoicesRouter,
models, models,
proformasRouter,
} from "./infrastructure"; } from "./infrastructure";
import { issuedInvoicesRouter, proformasRouter } from "./infrastructure/express";
export const customerInvoicesAPIModule: IModuleServer = { export const customerInvoicesAPIModule: IModuleServer = {
name: "customer-invoices", name: "customer-invoices",

View File

@ -1,2 +0,0 @@
export * from "./issued-invoices";
export * from "./proformas";

View File

@ -1,2 +0,0 @@
export * from "../../issued-invoices/express/controllers";
export * from "../../issued-invoices/express/issued-invoices.routes";

View File

@ -1,4 +0,0 @@
export * from "../../proformas/express/controllers";
export * from "./proformas.routes";
export * from "./proformas-api-error-mapper";

View File

@ -7,12 +7,12 @@ import {
import { GetIssuedInvoiceByIdResponseSchema } from "../../../../../common/index.ts"; import { GetIssuedInvoiceByIdResponseSchema } from "../../../../../common/index.ts";
import type { GetIssuedInvoiceByIdUseCase } from "../../../../application/issued-invoices/index.ts"; import type { GetIssuedInvoiceByIdUseCase } from "../../../../application/issued-invoices/index.ts";
import { customerInvoicesApiErrorMapper } from "../../../express/proformas/proformas-api-error-mapper.ts"; import { proformasApiErrorMapper } from "../../../proformas/express/proformas-api-error-mapper.ts";
export class GetIssuedInvoiceByIdController extends ExpressController { export class GetIssuedInvoiceByIdController extends ExpressController {
public constructor(private readonly useCase: GetIssuedInvoiceByIdUseCase) { public constructor(private readonly useCase: GetIssuedInvoiceByIdUseCase) {
super(); super();
this.errorMapper = customerInvoicesApiErrorMapper; this.errorMapper = proformasApiErrorMapper;
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.registerGuards( this.registerGuards(

View File

@ -8,12 +8,12 @@ import { Criteria } from "@repo/rdx-criteria/server";
import { ListIssuedInvoicesResponseSchema } from "../../../../../common/index.ts"; import { ListIssuedInvoicesResponseSchema } from "../../../../../common/index.ts";
import type { ListIssuedInvoicesUseCase } from "../../../../application/issued-invoices/index.ts"; import type { ListIssuedInvoicesUseCase } from "../../../../application/issued-invoices/index.ts";
import { customerInvoicesApiErrorMapper } from "../../../express/proformas/proformas-api-error-mapper.ts"; import { proformasApiErrorMapper } from "../../../proformas/express/proformas-api-error-mapper.ts";
export class ListIssuedInvoicesController extends ExpressController { export class ListIssuedInvoicesController extends ExpressController {
public constructor(private readonly useCase: ListIssuedInvoicesUseCase) { public constructor(private readonly useCase: ListIssuedInvoicesUseCase) {
super(); super();
this.errorMapper = customerInvoicesApiErrorMapper; this.errorMapper = proformasApiErrorMapper;
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.registerGuards( this.registerGuards(

View File

@ -8,12 +8,12 @@ import {
import type { ReportIssueInvoiceByIdQueryRequestDTO } from "../../../../../common"; import type { ReportIssueInvoiceByIdQueryRequestDTO } from "../../../../../common";
import type { ReportIssuedInvoiceUseCase } from "../../../../application/index.ts"; import type { ReportIssuedInvoiceUseCase } from "../../../../application/index.ts";
import { customerInvoicesApiErrorMapper } from "../../../express/proformas/proformas-api-error-mapper.ts"; import { proformasApiErrorMapper } from "../../../proformas/express/proformas-api-error-mapper.ts";
export class ReportIssuedInvoiceController extends ExpressController { export class ReportIssuedInvoiceController extends ExpressController {
public constructor(private readonly useCase: ReportIssuedInvoiceUseCase) { public constructor(private readonly useCase: ReportIssuedInvoiceUseCase) {
super(); super();
this.errorMapper = customerInvoicesApiErrorMapper; this.errorMapper = proformasApiErrorMapper;
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.registerGuards( this.registerGuards(

View File

@ -18,7 +18,7 @@ import {
isInvalidProformaTransitionError, isInvalidProformaTransitionError,
isProformaCannotBeConvertedToInvoiceError, isProformaCannotBeConvertedToInvoiceError,
isProformaCannotBeDeletedError, isProformaCannotBeDeletedError,
} from "../../domain"; } from "../../../domain";
// Crea una regla específica (prioridad alta para sobreescribir mensajes) // Crea una regla específica (prioridad alta para sobreescribir mensajes)
const invoiceDuplicateRule: ErrorToApiRule = { const invoiceDuplicateRule: ErrorToApiRule = {

View File

@ -1,4 +1,9 @@
import type { ICatalogs, IProformaDomainMapper, IProformaListMapper } from "../../../application"; import {
CreateProformaInputMapper,
type ICatalogs,
type IProformaDomainMapper,
type IProformaListMapper,
} from "../../../application";
import { SequelizeProformaDomainMapper, SequelizeProformaListMapper } from "../persistence"; import { SequelizeProformaDomainMapper, SequelizeProformaListMapper } from "../persistence";
export interface IProformaPersistenceMappers { export interface IProformaPersistenceMappers {

View File

@ -7,12 +7,12 @@ import {
import type { CreateProformaRequestDTO } from "../../../../../common/dto/index.ts"; import type { CreateProformaRequestDTO } from "../../../../../common/dto/index.ts";
import type { CreateProformaUseCase } from "../../../../application/index.ts"; import type { CreateProformaUseCase } from "../../../../application/index.ts";
import { customerInvoicesApiErrorMapper } from "../../../express/proformas/proformas-api-error-mapper.ts"; import { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts";
export class CreateProformaController extends ExpressController { export class CreateProformaController extends ExpressController {
public constructor(private readonly useCase: CreateProformaUseCase) { public constructor(private readonly useCase: CreateProformaUseCase) {
super(); super();
this.errorMapper = customerInvoicesApiErrorMapper; this.errorMapper = proformasApiErrorMapper;
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.registerGuards( this.registerGuards(

View File

@ -6,12 +6,12 @@ import {
} from "@erp/core/api"; } from "@erp/core/api";
import type { DeleteProformaUseCase } from "../../../../application/index.ts"; import type { DeleteProformaUseCase } from "../../../../application/index.ts";
import { customerInvoicesApiErrorMapper } from "../../../express/proformas/proformas-api-error-mapper.ts"; import { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts";
export class DeleteProformaController extends ExpressController { export class DeleteProformaController extends ExpressController {
public constructor(private readonly useCase: DeleteProformaUseCase) { public constructor(private readonly useCase: DeleteProformaUseCase) {
super(); super();
this.errorMapper = customerInvoicesApiErrorMapper; this.errorMapper = proformasApiErrorMapper;
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.registerGuards( this.registerGuards(

View File

@ -5,13 +5,13 @@ import {
requireCompanyContextGuard, requireCompanyContextGuard,
} from "@erp/core/api"; } from "@erp/core/api";
import type { GetProformaUseCase } from "../../../../application/index.ts"; import type { GetProformaByIdUseCase } from "../../../../application";
import { customerInvoicesApiErrorMapper } from "../../../express/proformas/proformas-api-error-mapper.ts"; import { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts";
export class GetProformaController extends ExpressController { export class GetProformaController extends ExpressController {
public constructor(private readonly useCase: GetProformaUseCase) { public constructor(private readonly useCase: GetProformaByIdUseCase) {
super(); super();
this.errorMapper = customerInvoicesApiErrorMapper; this.errorMapper = proformasApiErrorMapper;
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.registerGuards( this.registerGuards(

View File

@ -6,12 +6,12 @@ import {
} from "@erp/core/api"; } from "@erp/core/api";
import type { IssueProformaUseCase } from "../../../../application/index.ts"; import type { IssueProformaUseCase } from "../../../../application/index.ts";
import { customerInvoicesApiErrorMapper } from "../../../express/proformas/proformas-api-error-mapper.ts"; import { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts";
export class IssueProformaController extends ExpressController { export class IssueProformaController extends ExpressController {
public constructor(private readonly useCase: IssueProformaUseCase) { public constructor(private readonly useCase: IssueProformaUseCase) {
super(); super();
this.errorMapper = customerInvoicesApiErrorMapper; this.errorMapper = proformasApiErrorMapper;
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.registerGuards( this.registerGuards(

View File

@ -7,12 +7,12 @@ import {
import { Criteria } from "@repo/rdx-criteria/server"; import { Criteria } from "@repo/rdx-criteria/server";
import type { ListProformasUseCase } from "../../../../application/index.ts"; import type { ListProformasUseCase } from "../../../../application/index.ts";
import { customerInvoicesApiErrorMapper } from "../../../express/proformas/proformas-api-error-mapper.ts"; import { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts";
export class ListProformasController extends ExpressController { export class ListProformasController extends ExpressController {
public constructor(private readonly useCase: ListProformasUseCase) { public constructor(private readonly useCase: ListProformasUseCase) {
super(); super();
this.errorMapper = customerInvoicesApiErrorMapper; this.errorMapper = proformasApiErrorMapper;
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.registerGuards( this.registerGuards(

View File

@ -8,12 +8,12 @@ import {
import type { ReportProformaByIdQueryRequestDTO } from "../../../../../common"; import type { ReportProformaByIdQueryRequestDTO } from "../../../../../common";
import type { ReportProformaUseCase } from "../../../../application/index.ts"; import type { ReportProformaUseCase } from "../../../../application/index.ts";
import { customerInvoicesApiErrorMapper } from "../../../express/proformas/proformas-api-error-mapper.ts"; import { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts";
export class ReportProformaController extends ExpressController { export class ReportProformaController extends ExpressController {
public constructor(private readonly useCase: ReportProformaUseCase) { public constructor(private readonly useCase: ReportProformaUseCase) {
super(); super();
this.errorMapper = customerInvoicesApiErrorMapper; this.errorMapper = proformasApiErrorMapper;
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.registerGuards( this.registerGuards(

View File

@ -7,12 +7,12 @@ import {
import type { UpdateProformaByIdRequestDTO } from "../../../../../common/dto/index.ts"; import type { UpdateProformaByIdRequestDTO } from "../../../../../common/dto/index.ts";
import type { UpdateProformaUseCase } from "../../../../application/index.ts"; import type { UpdateProformaUseCase } from "../../../../application/index.ts";
import { customerInvoicesApiErrorMapper } from "../../../express/proformas/proformas-api-error-mapper.ts"; import { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts";
export class UpdateProformaController extends ExpressController { export class UpdateProformaController extends ExpressController {
public constructor(private readonly useCase: UpdateProformaUseCase) { public constructor(private readonly useCase: UpdateProformaUseCase) {
super(); super();
this.errorMapper = customerInvoicesApiErrorMapper; this.errorMapper = proformasApiErrorMapper;
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query // 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.registerGuards( this.registerGuards(

View File

@ -1 +1,2 @@
export * from "./controllers"; export * from "./controllers";
export * from "./proformas.routes";

View File

@ -134,8 +134,8 @@ export class CreateProformaRequestMapper {
const globalDiscountPercentage = extractOrPushError( const globalDiscountPercentage = extractOrPushError(
Percentage.create({ Percentage.create({
value: Number(dto.discount_percentage.value), value: Number(dto.global_discount_percentage.value),
scale: Number(dto.discount_percentage.scale), scale: Number(dto.global_discount_percentage.scale),
}), }),
"discount_percentage", "discount_percentage",
this.errors this.errors
@ -209,7 +209,7 @@ export class CreateProformaRequestMapper {
); );
const discountPercentage = extractOrPushError( const discountPercentage = extractOrPushError(
maybeFromNullableResult(item.discount_percentage, (value) => maybeFromNullableResult(item.item_discount_percentage, (value) =>
ItemDiscountPercentage.create(value) ItemDiscountPercentage.create(value)
), ),
"discount_percentage", "discount_percentage",

View File

@ -1,6 +1,3 @@
// Ejemplo: regla específica para Billing → InvoiceIdAlreadyExistsError
// (si defines un error más ubicuo dentro del BC con su propia clase)
import { import {
ApiErrorMapper, ApiErrorMapper,
ConflictApiError, ConflictApiError,
@ -23,7 +20,7 @@ import {
} from "../../../domain"; } from "../../../domain";
// Crea una regla específica (prioridad alta para sobreescribir mensajes) // Crea una regla específica (prioridad alta para sobreescribir mensajes)
const invoiceDuplicateRule: ErrorToApiRule = { const proformaDuplicateRule: ErrorToApiRule = {
priority: 120, priority: 120,
matches: (e) => isCustomerInvoiceIdAlreadyExistsError(e), matches: (e) => isCustomerInvoiceIdAlreadyExistsError(e),
build: (e) => build: (e) =>
@ -81,8 +78,8 @@ const proformaCannotBeDeletedRule: ErrorToApiRule = {
}; };
// Cómo aplicarla: crea una nueva instancia del mapper con la regla extra // Cómo aplicarla: crea una nueva instancia del mapper con la regla extra
export const customerInvoicesApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default() export const proformasApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default()
.register(invoiceDuplicateRule) .register(proformaDuplicateRule)
.register(proformaItemMismatchError) .register(proformaItemMismatchError)
.register(entityIsNotProformaError) .register(entityIsNotProformaError)
.register(proformaConversionRule) .register(proformaConversionRule)

View File

@ -3,17 +3,21 @@ import { type ModuleParams, type RequestWithAuth, validateRequest } from "@erp/c
import { type NextFunction, type Request, type Response, Router } from "express"; import { type NextFunction, type Request, type Response, Router } from "express";
import { import {
GetProformaController,
ListProformasController,
type ProformasInternalDeps,
ReportProformaController,
} from "..";
import {
CreateProformaRequestSchema,
GetProformaByIdRequestSchema, GetProformaByIdRequestSchema,
ListProformasRequestSchema, ListProformasRequestSchema,
ReportProformaByIdParamsRequestSchema, ReportProformaByIdParamsRequestSchema,
ReportProformaByIdQueryRequestSchema, ReportProformaByIdQueryRequestSchema,
} from "../../../../common"; } from "../../../../common";
import {
GetProformaController, import { CreateProformaController } from "./controllers/create-proforma.controller";
ListProformasController,
type ProformasInternalDeps,
ReportProformaController,
} from "../../proformas";
export const proformasRouter = (params: ModuleParams, deps: ProformasInternalDeps) => { export const proformasRouter = (params: ModuleParams, deps: ProformasInternalDeps) => {
const { app, config } = params; const { app, config } = params;
@ -73,18 +77,19 @@ export const proformasRouter = (params: ModuleParams, deps: ProformasInternalDep
} }
); );
/*router.post( router.post(
"/", "/",
//checkTabContext, //checkTabContext,
validateRequest(CreateProformaRequestSchema, "body"), validateRequest(CreateProformaRequestSchema, "body"),
(req: Request, res: Response, next: NextFunction) => { (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.useCases.create_proforma(); const useCase = deps.useCases.createProforma();
const controller = new CreateProformaController(useCase); const controller = new CreateProformaController(useCase);
return controller.execute(req, res, next); return controller.execute(req, res, next);
} }
); );
/*
router.put( router.put(
"/:proforma_id", "/:proforma_id",
//checkTabContext, //checkTabContext,

View File

@ -7,7 +7,10 @@ export const CreateProformaItemRequestSchema = z.object({
description: z.string().default(""), description: z.string().default(""),
quantity: NumericStringSchema.default(""), quantity: NumericStringSchema.default(""),
unit_amount: NumericStringSchema.default(""), unit_amount: NumericStringSchema.default(""),
discount_percentage: NumericStringSchema.default(""), item_discount_percentage: PercentageSchema.default({
value: "0",
scale: "2",
}),
taxes: z.string().default(""), taxes: z.string().default(""),
}); });
export type CreateProformaItemRequestDTO = z.infer<typeof CreateProformaItemRequestSchema>; export type CreateProformaItemRequestDTO = z.infer<typeof CreateProformaItemRequestSchema>;
@ -29,7 +32,7 @@ export const CreateProformaRequestSchema = z.object({
language_code: z.string().toLowerCase().default("es"), language_code: z.string().toLowerCase().default("es"),
currency_code: z.string().toUpperCase().default("EUR"), currency_code: z.string().toUpperCase().default("EUR"),
discount_percentage: PercentageSchema.default({ global_discount_percentage: PercentageSchema.default({
value: "0", value: "0",
scale: "2", scale: "2",
}), }),

View File

@ -13,8 +13,6 @@
"dev": "turbo dev", "dev": "turbo dev",
"dev:server": "turbo dev --filter=server", "dev:server": "turbo dev --filter=server",
"dev:client": "turbo dev --filter=client", "dev:client": "turbo dev --filter=client",
"lint": "turbo run lint",
"lint:fix": "turbo run lint:fix",
"format-and-lint": "biome check .", "format-and-lint": "biome check .",
"format-and-lint:fix": "biome check . --write", "format-and-lint:fix": "biome check . --write",
"ui:add": "pnpm --filter @repo/shadcn-ui ui:add", "ui:add": "pnpm --filter @repo/shadcn-ui ui:add",
@ -28,7 +26,6 @@
"@typescript-eslint/eslint-plugin": "^8.56.1", "@typescript-eslint/eslint-plugin": "^8.56.1",
"@typescript-eslint/parser": "^8.56.1", "@typescript-eslint/parser": "^8.56.1",
"change-case": "^5.4.4", "change-case": "^5.4.4",
"eslint": "^10.0.2",
"inquirer": "^12.10.0", "inquirer": "^12.10.0",
"plop": "^4.0.4", "plop": "^4.0.4",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",