Compare commits

..

No commits in common. "bf9ed99a90fa56ac26476013a79c8f634cb2d4ec" and "941ad254015e18d14819bb13dcc70d3d1169c620" have entirely different histories.

165 changed files with 1265 additions and 2191 deletions

View File

@ -8,7 +8,8 @@
"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 lint --fix", "lint": "biome check . && eslint .",
"lint:fix": "biome check --write . && eslint . --fix",
"format": "biome format --write" "format": "biome format --write"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,9 +1,7 @@
import type { IModuleClient } from "@erp/core/client"; import { IModuleClient } from "@erp/core/client";
import { AppLayout } from "@repo/rdx-ui/components"; import { AppLayout } from "@repo/rdx-ui/components";
import { Navigate, Route, createBrowserRouter, createRoutesFromElements } from "react-router-dom"; import { createBrowserRouter, createRoutesFromElements, Navigate, Route } from "react-router-dom";
import { ModuleRoutes } from "@/components/module-routes"; import { ModuleRoutes } from "@/components/module-routes";
import { ErrorPage, LoginForm } from "../pages"; import { ErrorPage, LoginForm } from "../pages";
import { modules } from "../register-modules"; // Aquí ca import { modules } from "../register-modules"; // Aquí ca
@ -35,24 +33,24 @@ export const getAppRouter = () => {
return createBrowserRouter( return createBrowserRouter(
createRoutesFromElements( createRoutesFromElements(
<Route path="/"> <Route path='/'>
{/* Auth Layout */} {/* Auth Layout */}
<Route path="/auth"> <Route path='/auth'>
<Route element={<Navigate to="login" />} index /> <Route index element={<Navigate to='login' />} />
<Route element={<LoginForm />} path="login" /> <Route path='login' element={<LoginForm />} />
<Route element={<ModuleRoutes modules={grouped.auth} params={params} />} path="*" /> <Route path='*' element={<ModuleRoutes modules={grouped.auth} params={params} />} />
</Route> </Route>
{/* App Layout */} {/* App Layout */}
<Route element={<AppLayout />}> <Route element={<AppLayout />}>
{/* Dynamic Module Routes */} {/* Dynamic Module Routes */}
<Route element={<ModuleRoutes modules={grouped.app} params={params} />} path="*" /> <Route path='*' element={<ModuleRoutes modules={grouped.app} params={params} />} />
{/* Main Layout */} {/* Main Layout */}
<Route element={<ErrorPage />} path="/dashboard" /> <Route path='/dashboard' element={<ErrorPage />} />
<Route element={<ErrorPage />} path="/settings" /> <Route path='/settings' element={<ErrorPage />} />
<Route element={<ErrorPage />} path="/catalog" /> <Route path='/catalog' element={<ErrorPage />} />
<Route element={<ErrorPage />} path="/quotes" /> <Route path='/quotes' element={<ErrorPage />} />
</Route> </Route>
</Route> </Route>
) )

View File

@ -1,5 +1,4 @@
export * from "./documents"; export * from "./documents";
export * from "./mappers";
export * from "./presenters"; export * from "./presenters";
export * from "./renderers"; export * from "./renderers";
export * from "./snapshot-builders"; export * from "./snapshot-builders";

View File

@ -1 +0,0 @@
export * from "./mapper.interface";

View File

@ -1,2 +1,3 @@
export * from "./errors"; export * from "./errors";
export * from "./repositories";
export * from "./value-objects"; export * from "./value-objects";

View File

@ -0,0 +1 @@
export * from "./repository.interface";

View File

@ -56,21 +56,23 @@ export type DomainMapperWithBulk<TPersistence, TDomain> = IDomainMapper<TPersist
Partial<IBulkDomainMapper<TPersistence, TDomain>>; Partial<IBulkDomainMapper<TPersistence, TDomain>>;
/** /**
* 👓 Mapper de Read Model (Persistencia Read Model/Proyección de Lectura) *
* - Responsabilidad: transformar registros de persistencia en read models para lectura (listados, resúmenes, informes). * 👓 Mapper de Read Model (Persistencia DTO/Proyección de Lectura)
* - Responsabilidad: transformar registros de persistencia en DTOs para lectura (listados, resúmenes, informes).
* - No intenta reconstruir agregados ni validar value objects de dominio. * - No intenta reconstruir agregados ni validar value objects de dominio.
**/ **/
export interface IQueryMapperWithBulk<TPersistence, TReadModel> { export interface IQueryMapperWithBulk<TPersistence, TDTO> {
/** /**
* Convierte un registro crudo en un read model de lectura. * Convierte un registro crudo en un DTO de lectura.
*/ */
mapToReadModel(raw: TPersistence, params?: MapperParamsType): Result<TReadModel, Error>; mapToDTO(raw: TPersistence, params?: MapperParamsType): Result<TDTO, Error>;
/** /**
* Convierte múltiples registros crudos en una Collection de read models de lectura. * Convierte múltiples registros crudos en una Collection de DTOs de lectura.
*/ */
mapToReadModelCollection( mapToDTOCollection(
raws: TPersistence[], raws: TPersistence[],
totalCount: number, totalCount: number,
params?: MapperParamsType params?: MapperParamsType
): Result<Collection<TReadModel>, Error>; ): Result<Collection<TDTO>, Error>;
} }

View File

@ -1,7 +1,7 @@
import { Collection, Result, ResultCollection } from "@repo/rdx-utils"; import { Collection, Result, ResultCollection } from "@repo/rdx-utils";
import type { Model } from "sequelize"; import type { Model } from "sequelize";
import type { MapperParamsType } from "../../../../application"; import type { MapperParamsType } from "../../../../domain";
import type { ISequelizeDomainMapper } from "./sequelize-mapper.interface"; import type { ISequelizeDomainMapper } from "./sequelize-mapper.interface";

View File

@ -1,4 +1,4 @@
import type { DomainMapperWithBulk, IQueryMapperWithBulk } from "@erp/core/api/application"; import type { DomainMapperWithBulk, IQueryMapperWithBulk } from "../../../../domain";
export interface ISequelizeDomainMapper<TModel, TModelAttributes, TEntity> export interface ISequelizeDomainMapper<TModel, TModelAttributes, TEntity>
extends DomainMapperWithBulk<TModel | TModelAttributes, TEntity> {} extends DomainMapperWithBulk<TModel | TModelAttributes, TEntity> {}

View File

@ -1,16 +1,16 @@
import { Collection, Result } from "@repo/rdx-utils"; import { Collection, Result } from "@repo/rdx-utils";
import type { Model } from "sequelize"; import type { Model } from "sequelize";
import type { MapperParamsType } from "../../../../application"; import type { MapperParamsType } from "../../../../domain";
import type { ISequelizeQueryMapper } from "./sequelize-mapper.interface"; import type { ISequelizeQueryMapper } from "./sequelize-mapper.interface";
export abstract class SequelizeQueryMapper<TModel extends Model, TEntity> export abstract class SequelizeQueryMapper<TModel extends Model, TEntity>
implements ISequelizeQueryMapper<TModel, TEntity> implements ISequelizeQueryMapper<TModel, TEntity>
{ {
public abstract mapToReadModel(raw: TModel, params?: MapperParamsType): Result<TEntity, Error>; public abstract mapToDTO(raw: TModel, params?: MapperParamsType): Result<TEntity, Error>;
public mapToReadModelCollection( public mapToDTOCollection(
raws: TModel[], raws: TModel[],
totalCount: number, totalCount: number,
params?: MapperParamsType params?: MapperParamsType
@ -23,7 +23,7 @@ export abstract class SequelizeQueryMapper<TModel extends Model, TEntity>
} }
const items = _source.map((value, index) => { const items = _source.map((value, index) => {
const result = this.mapToReadModel(value as TModel, { index, ...params }); const result = this.mapToDTO(value as TModel, { index, ...params });
if (result.isFailure) { if (result.isFailure) {
throw result.error; throw result.error;
} }

View File

@ -0,0 +1 @@
export * from "./issued-invoice-list.dto";

View File

@ -10,7 +10,7 @@ import type {
VerifactuRecord, VerifactuRecord,
} from "../../../domain"; } from "../../../domain";
export type IssuedInvoiceSummary = { export type IssuedInvoiceListDTO = {
id: UniqueID; id: UniqueID;
companyId: UniqueID; companyId: UniqueID;

View File

@ -1,6 +1,7 @@
export * from "./application-models";
export * from "./di"; export * from "./di";
export * from "./dtos";
export * from "./mappers"; export * from "./mappers";
export * from "./models";
export * from "./repositories"; export * from "./repositories";
export * from "./services"; export * from "./services";
export * from "./snapshot-builders"; export * from "./snapshot-builders";

View File

@ -1,8 +1,8 @@
import type { MapperParamsType } from "@erp/core/api"; import type { MapperParamsType } from "@erp/core/api";
import type { Result } from "@repo/rdx-utils"; import type { Result } from "@repo/rdx-utils";
import type { IssuedInvoiceSummary } from "../models"; import type { IssuedInvoiceListDTO } from "../dtos";
export interface IIssuedInvoiceSummaryMapper { export interface IIssuedInvoiceListMapper {
mapToDTO(raw: unknown, params?: MapperParamsType): Result<IssuedInvoiceSummary, Error>; mapToDTO(raw: unknown, params?: MapperParamsType): Result<IssuedInvoiceListDTO, Error>;
} }

View File

@ -1 +0,0 @@
export * from "./issued-invoice-summary";

View File

@ -3,7 +3,7 @@ import type { UniqueID } from "@repo/rdx-ddd";
import type { Collection, Result } from "@repo/rdx-utils"; import type { Collection, Result } from "@repo/rdx-utils";
import type { IssuedInvoice } from "../../../domain"; import type { IssuedInvoice } from "../../../domain";
import type { IssuedInvoiceSummary } from "../models"; import type { IssuedInvoiceListDTO } from "../dtos";
export interface IIssuedInvoiceRepository { export interface IIssuedInvoiceRepository {
create(invoice: IssuedInvoice, transaction?: unknown): Promise<Result<void, Error>>; create(invoice: IssuedInvoice, transaction?: unknown): Promise<Result<void, Error>>;
@ -24,5 +24,5 @@ export interface IIssuedInvoiceRepository {
companyId: UniqueID, companyId: UniqueID,
criteria: Criteria, criteria: Criteria,
transaction: unknown transaction: unknown
): Promise<Result<Collection<IssuedInvoiceSummary>, Error>>; ): Promise<Result<Collection<IssuedInvoiceListDTO>, Error>>;
} }

View File

@ -4,7 +4,7 @@ import type { Collection, Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize"; import type { Transaction } from "sequelize";
import type { IssuedInvoice } from "../../../domain"; import type { IssuedInvoice } from "../../../domain";
import type { IssuedInvoiceSummary } from "../models"; import type { IssuedInvoiceListDTO } from "../dtos";
import type { IIssuedInvoiceRepository } from "../repositories"; import type { IIssuedInvoiceRepository } from "../repositories";
export interface IIssuedInvoiceFinder { export interface IIssuedInvoiceFinder {
@ -24,7 +24,7 @@ export interface IIssuedInvoiceFinder {
companyId: UniqueID, companyId: UniqueID,
criteria: Criteria, criteria: Criteria,
transaction?: Transaction transaction?: Transaction
): Promise<Result<Collection<IssuedInvoiceSummary>, Error>>; ): Promise<Result<Collection<IssuedInvoiceListDTO>, Error>>;
} }
export class IssuedInvoiceFinder implements IIssuedInvoiceFinder { export class IssuedInvoiceFinder implements IIssuedInvoiceFinder {
@ -50,7 +50,7 @@ export class IssuedInvoiceFinder implements IIssuedInvoiceFinder {
companyId: UniqueID, companyId: UniqueID,
criteria: Criteria, criteria: Criteria,
transaction?: Transaction transaction?: Transaction
): Promise<Result<Collection<IssuedInvoiceSummary>, Error>> { ): Promise<Result<Collection<IssuedInvoiceListDTO>, Error>> {
return this.repository.findByCriteriaInCompany(companyId, criteria, transaction); return this.repository.findByCriteriaInCompany(companyId, criteria, transaction);
} }
} }

View File

@ -1,15 +1,15 @@
import type { ISnapshotBuilder } from "@erp/core/api"; import type { ISnapshotBuilder } from "@erp/core/api";
import { maybeToEmptyString } from "@repo/rdx-ddd"; import { maybeToEmptyString } from "@repo/rdx-ddd";
import type { IssuedInvoiceSummary } from "../../models"; import type { IssuedInvoiceListDTO } from "../../dtos";
import type { IIssuedInvoiceListItemSnapshot } from "./issued-invoice-list-item-snapshot.interface"; import type { IIssuedInvoiceListItemSnapshot } from "./issued-invoice-list-item-snapshot.interface";
export interface IIssuedInvoiceListItemSnapshotBuilder export interface IIssuedInvoiceListItemSnapshotBuilder
extends ISnapshotBuilder<IssuedInvoiceSummary, IIssuedInvoiceListItemSnapshot> {} extends ISnapshotBuilder<IssuedInvoiceListDTO, IIssuedInvoiceListItemSnapshot> {}
export class IssuedInvoiceListItemSnapshotBuilder implements IIssuedInvoiceListItemSnapshotBuilder { export class IssuedInvoiceListItemSnapshotBuilder implements IIssuedInvoiceListItemSnapshotBuilder {
toOutput(invoice: IssuedInvoiceSummary): IIssuedInvoiceListItemSnapshot { toOutput(invoice: IssuedInvoiceListDTO): IIssuedInvoiceListItemSnapshot {
const recipient = invoice.recipient.toObjectString(); const recipient = invoice.recipient.toObjectString();
const verifactu = invoice.verifactu.match( const verifactu = invoice.verifactu.match(

View File

@ -0,0 +1 @@
export * from "./proforma-list.dto";

View File

@ -1,6 +1,7 @@
export * from "./application-models";
export * from "./di"; export * from "./di";
export * from "./dtos";
export * from "./mappers"; export * from "./mappers";
export * from "./models";
export * from "./repositories"; export * from "./repositories";
export * from "./services"; export * from "./services";
export * from "./snapshot-builders"; export * from "./snapshot-builders";

View File

@ -41,20 +41,13 @@ 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 interface ICreateProformaInputMapper { export class CreateProformaInputMapper implements 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 }) {
@ -145,14 +138,14 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
const globalDiscountPercentage = extractOrPushError( const globalDiscountPercentage = extractOrPushError(
Percentage.create({ Percentage.create({
value: Number(dto.global_discount_percentage.value), value: Number(dto.discount_percentage.value),
scale: Number(dto.global_discount_percentage.scale), scale: Number(dto.discount_percentage.scale),
}), }),
"discount_percentage", "discount_percentage",
errors errors
); );
const itemsProps = this.mapItemsProps(dto, { const items = this.mapItems(dto, {
languageCode: languageCode!, languageCode: languageCode!,
currencyCode: currencyCode!, currencyCode: currencyCode!,
globalDiscountPercentage: globalDiscountPercentage!, globalDiscountPercentage: globalDiscountPercentage!,
@ -165,7 +158,7 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
); );
} }
const props: IProformaProps = { const props: Omit<IProformaProps, "items"> & { items: IProformaItemProps[] } = {
companyId, companyId,
status: defaultStatus, status: defaultStatus,
@ -188,7 +181,7 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
paymentMethod: paymentMethod!, paymentMethod: paymentMethod!,
globalDiscountPercentage: globalDiscountPercentage!, globalDiscountPercentage: globalDiscountPercentage!,
items: itemsProps, // ← IProformaItemProps[] items, // ← IProformaItemProps[]
}; };
return Result.ok({ return Result.ok({
@ -200,7 +193,7 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
} }
} }
private mapItemsProps( private mapItems(
dto: CreateProformaRequestDTO, dto: CreateProformaRequestDTO,
params: { params: {
languageCode: LanguageCode; languageCode: LanguageCode;
@ -231,12 +224,12 @@ export class CreateProformaInputMapper /*implements ICreateProformaInputMapper*/
); );
const discountPercentage = extractOrPushError( const discountPercentage = extractOrPushError(
maybeFromNullableResult(item.item_discount_percentage, (v) => DiscountPercentage.create(v)), maybeFromNullableResult(item.discount_percentage, (v) => DiscountPercentage.create(v)),
`items[${index}].discount_percentage`, `items[${index}].discount_percentage`,
params.errors params.errors
); );
const taxes = this.mapTaxesProps(item.taxes, { const taxes = this.mapTaxes(item.taxes, {
itemIndex: index, itemIndex: index,
errors: params.errors, errors: params.errors,
}); });
@ -259,7 +252,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 mapTaxesProps( private mapTaxes(
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,3 +1,4 @@
import { InvoiceSerie, type ProformaPatchProps } from "@erp/customer-invoices/api/domain";
import { import {
CurrencyCode, CurrencyCode,
DomainError, DomainError,
@ -13,7 +14,6 @@ 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

@ -1,7 +1,7 @@
import type { MapperParamsType } from "@erp/core/api"; import type { MapperParamsType } from "@erp/core/api";
import type { Result } from "@repo/rdx-utils"; import type { Result } from "@repo/rdx-utils";
import type { ProformaListDTO } from "../models"; import type { ProformaListDTO } from "../dtos";
export interface IProformaListMapper { export interface IProformaListMapper {
mapToDTO(raw: unknown, params?: MapperParamsType): Result<ProformaListDTO, Error>; mapToDTO(raw: unknown, params?: MapperParamsType): Result<ProformaListDTO, Error>;

View File

@ -1 +0,0 @@
export * from "./proforma-resume";

View File

@ -3,7 +3,7 @@ import type { UniqueID } from "@repo/rdx-ddd";
import type { Collection, Result } from "@repo/rdx-utils"; import type { Collection, Result } from "@repo/rdx-utils";
import type { InvoiceStatus, Proforma } from "../../../domain"; import type { InvoiceStatus, Proforma } from "../../../domain";
import type { ProformaListDTO } from "../models"; import type { ProformaListDTO } from "../dtos";
export interface IProformaRepository { export interface IProformaRepository {
create(proforma: Proforma, transaction?: unknown): Promise<Result<void, Error>>; create(proforma: Proforma, transaction?: unknown): Promise<Result<void, Error>>;

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 { IProformaProps, Proforma } from "../../../domain"; import type { ICustomerInvoiceRepository } 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(params: { create(
companyId: UniqueID; companyId: UniqueID,
id: UniqueID; id: UniqueID,
props: IProformaProps; props: CustomerInvoiceProps,
transaction: Transaction; transaction: Transaction
}): Promise<Result<Proforma, Error>>; ): Promise<Result<CustomerInvoice, Error>>;
} }
type ProformaCreatorDeps = { type ProformaCreatorDeps = {
numberService: IProformaNumberGenerator; numberService: IProformaNumberGenerator;
factory: IProformaFactory; factory: IProformaFactory;
repository: IProformaRepository; repository: ICustomerInvoiceRepository;
}; };
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: IProformaRepository; private readonly repository: ICustomerInvoiceRepository;
constructor(deps: ProformaCreatorDeps) { constructor(deps: ProformaCreatorDeps) {
this.numberService = deps.numberService; this.numberService = deps.numberService;
@ -34,14 +34,12 @@ export class ProformaCreator implements IProformaCreator {
this.repository = deps.repository; this.repository = deps.repository;
} }
async create(params: { async create(
companyId: UniqueID; companyId: UniqueID,
id: UniqueID; id: UniqueID,
props: IProformaProps; props: CustomerInvoiceProps,
transaction: Transaction; transaction: Transaction
}): Promise<Result<Proforma, Error>> { ): Promise<Result<CustomerInvoice, 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

@ -4,7 +4,7 @@ import type { Collection, Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize"; import type { Transaction } from "sequelize";
import type { Proforma } from "../../../domain"; import type { Proforma } from "../../../domain";
import type { ProformaListDTO } from "../models"; import type { ProformaListDTO } from "../dtos";
import type { IProformaRepository } from "../repositories"; import type { IProformaRepository } from "../repositories";
export interface IProformaFinder { export interface IProformaFinder {

View File

@ -1,7 +1,7 @@
import type { ISnapshotBuilder } from "@erp/core/api"; import type { ISnapshotBuilder } from "@erp/core/api";
import { maybeToEmptyString } from "@repo/rdx-ddd"; import { maybeToEmptyString } from "@repo/rdx-ddd";
import type { ProformaListDTO } from "../../models"; import type { ProformaListDTO } from "../../dtos";
import type { IProformaListItemSnapshot } from "./proforma-list-item-snapshot.interface"; import type { IProformaListItemSnapshot } from "./proforma-list-item-snapshot.interface";

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 mappedPropsResult = this.dtoMapper.map(dto, { companyId }); const mappedResult = this.dtoMapper.map(dto, companyId);
if (mappedPropsResult.isFailure) { if (mappedResult.isFailure) {
return Result.fail(mappedPropsResult.error); return Result.fail(mappedResult.error);
} }
const { props, id } = mappedPropsResult.data; const { props, id } = mappedResult.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

@ -192,7 +192,6 @@ export class CustomerInvoice
} }
// Method to get the complete list of line items // Method to get the complete list of line items
public get items(): CustomerInvoiceItems { public get items(): CustomerInvoiceItems {
return this._items; return this._items;
} }

View File

@ -23,9 +23,10 @@ import {
import { import {
type IProformaItemProps, type IProformaItemProps,
type IProformaItems, type IProformaItems,
type IProformaItemsProps,
ProformaItem, ProformaItem,
ProformaItems, ProformaItems,
} from "../entities"; } from "../entities/proforma-items";
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";
@ -52,7 +53,7 @@ export interface IProformaProps {
paymentMethod: Maybe<InvoicePaymentMethod>; paymentMethod: Maybe<InvoicePaymentMethod>;
items: IProformaItemProps[]; items: IProformaItemsProps[];
globalDiscountPercentage: DiscountPercentage; globalDiscountPercentage: DiscountPercentage;
} }
@ -95,19 +96,21 @@ export interface IProforma {
paymentMethod: Maybe<InvoicePaymentMethod>; paymentMethod: Maybe<InvoicePaymentMethod>;
items: IProformaItems; // <- Colección items: IProformaItems;
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;
@ -133,8 +136,6 @@ 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);
@ -146,7 +147,36 @@ 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;
} }
@ -219,34 +249,6 @@ 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,60 +217,6 @@ 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
/** /**
@ -330,4 +276,58 @@ 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: IProformaItemProps[]; items?: ICreateProformaItemProps[];
// 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,9 +22,6 @@ 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.
@ -38,16 +35,12 @@ 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;
protected constructor(props: InternalProformaProps) { constructor(props: IProformaItemsProps) {
super([]); super(props.items ?? []);
this.languageCode = props.languageCode; this.languageCode = props.languageCode;
this.currencyCode = props.currencyCode; this.currencyCode = props.currencyCode;
this.globalDiscountPercentage = props.globalDiscountPercentage; this.globalDiscountPercentage = props.globalDiscountPercentage;
@ -55,15 +48,8 @@ export class ProformaItems extends Collection<ProformaItem> implements IProforma
this.ensureSameCurrencyAndLanguage(this.items); this.ensureSameCurrencyAndLanguage(this.items);
} }
public add(item: ProformaItem): boolean { public static create(props: IProformaItemsProps): ProformaItems {
const same = return new ProformaItems(props);
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,10 +7,9 @@ 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

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

View File

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

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

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

View File

@ -1,3 +1,6 @@
// 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,
@ -20,7 +23,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 proformaDuplicateRule: ErrorToApiRule = { const invoiceDuplicateRule: ErrorToApiRule = {
priority: 120, priority: 120,
matches: (e) => isCustomerInvoiceIdAlreadyExistsError(e), matches: (e) => isCustomerInvoiceIdAlreadyExistsError(e),
build: (e) => build: (e) =>
@ -78,8 +81,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 proformasApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default() export const customerInvoicesApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default()
.register(proformaDuplicateRule) .register(invoiceDuplicateRule)
.register(proformaItemMismatchError) .register(proformaItemMismatchError)
.register(entityIsNotProformaError) .register(entityIsNotProformaError)
.register(proformaConversionRule) .register(proformaConversionRule)

View File

@ -3,21 +3,17 @@ 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 {
import { CreateProformaController } from "./controllers/create-proforma.controller"; GetProformaController,
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;
@ -77,19 +73,18 @@ 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.createProforma(); const useCase = deps.useCases.create_proforma();
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,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 { proformasApiErrorMapper } from "../../../proformas/express/proformas-api-error-mapper.ts"; import { customerInvoicesApiErrorMapper } from "../../../express/proformas/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 = proformasApiErrorMapper; this.errorMapper = customerInvoicesApiErrorMapper;
// 🔐 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 { proformasApiErrorMapper } from "../../../proformas/express/proformas-api-error-mapper.ts"; import { customerInvoicesApiErrorMapper } from "../../../express/proformas/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 = proformasApiErrorMapper; this.errorMapper = customerInvoicesApiErrorMapper;
// 🔐 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 { proformasApiErrorMapper } from "../../../proformas/express/proformas-api-error-mapper.ts"; import { customerInvoicesApiErrorMapper } from "../../../express/proformas/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 = proformasApiErrorMapper; this.errorMapper = customerInvoicesApiErrorMapper;
// 🔐 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

@ -14,7 +14,7 @@ import {
} from "@repo/rdx-ddd"; } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { IssuedInvoiceSummary } from "../../../../../../application"; import type { IssuedInvoiceListDTO } from "../../../../../../application";
import { InvoiceRecipient } from "../../../../../../domain"; import { InvoiceRecipient } from "../../../../../../domain";
import type { CustomerInvoiceModel } from "../../../../../common"; import type { CustomerInvoiceModel } from "../../../../../common";
@ -32,7 +32,7 @@ export class SequelizeIssuedInvoiceRecipientListMapper extends SequelizeQueryMap
const { errors, attributes } = params as { const { errors, attributes } = params as {
errors: ValidationErrorDetail[]; errors: ValidationErrorDetail[];
attributes: Partial<IssuedInvoiceSummary>; attributes: Partial<IssuedInvoiceListDTO>;
}; };
const { isProforma } = attributes; const { isProforma } = attributes;

View File

@ -11,10 +11,7 @@ import {
} from "@repo/rdx-ddd"; } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import type { import type { IIssuedInvoiceListMapper, IssuedInvoiceListDTO } from "../../../../../../application";
IIssuedInvoiceSummaryMapper,
IssuedInvoiceSummary,
} from "../../../../../../application";
import { import {
InvoiceAmount, InvoiceAmount,
InvoiceNumber, InvoiceNumber,
@ -28,8 +25,8 @@ import { SequelizeIssuedInvoiceRecipientListMapper } from "./sequelize-issued-in
import { SequelizeVerifactuRecordListMapper } from "./sequelize-verifactu-record.list.mapper"; import { SequelizeVerifactuRecordListMapper } from "./sequelize-verifactu-record.list.mapper";
export class SequelizeIssuedInvoiceListMapper export class SequelizeIssuedInvoiceListMapper
extends SequelizeQueryMapper<CustomerInvoiceModel, IssuedInvoiceSummary> extends SequelizeQueryMapper<CustomerInvoiceModel, IssuedInvoiceListDTO>
implements IIssuedInvoiceSummaryMapper implements IIssuedInvoiceListMapper
{ {
private _recipientMapper: SequelizeIssuedInvoiceRecipientListMapper; private _recipientMapper: SequelizeIssuedInvoiceRecipientListMapper;
private _verifactuMapper: SequelizeVerifactuRecordListMapper; private _verifactuMapper: SequelizeVerifactuRecordListMapper;
@ -40,14 +37,14 @@ export class SequelizeIssuedInvoiceListMapper
this._verifactuMapper = new SequelizeVerifactuRecordListMapper(); this._verifactuMapper = new SequelizeVerifactuRecordListMapper();
} }
public mapToReadModel( public mapToDTO(
raw: CustomerInvoiceModel, raw: CustomerInvoiceModel,
params?: MapperParamsType params?: MapperParamsType
): Result<IssuedInvoiceSummary, Error> { ): Result<IssuedInvoiceListDTO, Error> {
const errors: ValidationErrorDetail[] = []; const errors: ValidationErrorDetail[] = [];
// 1) Valores escalares (atributos generales) // 1) Valores escalares (atributos generales)
const attributes = this._mapAttributesToReadModel(raw, { errors, ...params }); const attributes = this.mapAttributesToDTO(raw, { errors, ...params });
// 2) Recipient (snapshot en la factura o include) // 2) Recipient (snapshot en la factura o include)
const recipientResult = this._recipientMapper.mapToDTO(raw, { const recipientResult = this._recipientMapper.mapToDTO(raw, {
@ -114,7 +111,7 @@ export class SequelizeIssuedInvoiceListMapper
}); });
} }
private _mapAttributesToReadModel(raw: CustomerInvoiceModel, params?: MapperParamsType) { private mapAttributesToDTO(raw: CustomerInvoiceModel, params?: MapperParamsType) {
const { errors } = params as { const { errors } = params as {
errors: ValidationErrorDetail[]; errors: ValidationErrorDetail[];
}; };

View File

@ -4,7 +4,7 @@ import type { UniqueID } from "@repo/rdx-ddd";
import { type Collection, Result } from "@repo/rdx-utils"; import { type Collection, Result } from "@repo/rdx-utils";
import type { FindOptions, InferAttributes, OrderItem, Sequelize, Transaction } from "sequelize"; import type { FindOptions, InferAttributes, OrderItem, Sequelize, Transaction } from "sequelize";
import type { IIssuedInvoiceRepository, IssuedInvoiceSummary } from "../../../../../application"; import type { IIssuedInvoiceRepository, IssuedInvoiceListDTO } from "../../../../../application";
import type { IssuedInvoice } from "../../../../../domain"; import type { IssuedInvoice } from "../../../../../domain";
import { import {
CustomerInvoiceItemModel, CustomerInvoiceItemModel,
@ -212,7 +212,7 @@ export class IssuedInvoiceRepository
criteria: Criteria, criteria: Criteria,
transaction: Transaction, transaction: Transaction,
options: FindOptions<InferAttributes<CustomerInvoiceModel>> = {} options: FindOptions<InferAttributes<CustomerInvoiceModel>> = {}
): Promise<Result<Collection<IssuedInvoiceSummary>, Error>> { ): Promise<Result<Collection<IssuedInvoiceListDTO>, Error>> {
const { CustomerModel } = this.database.models; const { CustomerModel } = this.database.models;
try { try {
@ -301,7 +301,7 @@ export class IssuedInvoiceRepository
}), }),
]); ]);
return this.listMapper.mapToReadModelCollection(rows, count); return this.listMapper.mapToDTOCollection(rows, count);
} catch (err: unknown) { } catch (err: unknown) {
return Result.fail(translateSequelizeError(err)); return Result.fail(translateSequelizeError(err));
} }

View File

@ -1,9 +1,4 @@
import { import type { ICatalogs, IProformaDomainMapper, IProformaListMapper } from "../../../application";
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

@ -2,23 +2,23 @@ import type { ProformasInternalDeps } from "./proformas.di";
export type ProformasServicesDeps = { export type ProformasServicesDeps = {
services: { services: {
listProformas: (filters: unknown, context: unknown) => null; listIssuedInvoices: (filters: unknown, context: unknown) => null;
getProformaById: (id: unknown, context: unknown) => null; getIssuedInvoiceById: (id: unknown, context: unknown) => null;
generateProformaReport: (id: unknown, options: unknown, context: unknown) => null; generateIssuedInvoiceReport: (id: unknown, options: unknown, context: unknown) => null;
}; };
}; };
export function buildProformaServices(deps: ProformasInternalDeps): ProformasServicesDeps { export function buildProformaServices(deps: ProformasInternalDeps): ProformasServicesDeps {
return { return {
services: { services: {
listProformas: (filters, context) => null, listIssuedInvoices: (filters, context) => null,
//internal.useCases.listProformas().execute(filters, context), //internal.useCases.listIssuedInvoices().execute(filters, context),
getProformaById: (id, context) => null, getIssuedInvoiceById: (id, context) => null,
//internal.useCases.getProformaById().execute(id, context), //internal.useCases.getIssuedInvoiceById().execute(id, context),
generateProformaReport: (id, options, context) => null, generateIssuedInvoiceReport: (id, options, context) => null,
//internal.useCases.reportProforma().execute(id, options, context), //internal.useCases.reportIssuedInvoice().execute(id, options, context),
}, },
}; };
} }

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 { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts"; import { customerInvoicesApiErrorMapper } from "../../../express/proformas/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 = proformasApiErrorMapper; this.errorMapper = customerInvoicesApiErrorMapper;
// 🔐 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 { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts"; import { customerInvoicesApiErrorMapper } from "../../../express/proformas/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 = proformasApiErrorMapper; this.errorMapper = customerInvoicesApiErrorMapper;
// 🔐 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 { GetProformaByIdUseCase } from "../../../../application"; import type { GetProformaUseCase } from "../../../../application/index.ts";
import { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts"; import { customerInvoicesApiErrorMapper } from "../../../express/proformas/proformas-api-error-mapper.ts";
export class GetProformaController extends ExpressController { export class GetProformaController extends ExpressController {
public constructor(private readonly useCase: GetProformaByIdUseCase) { public constructor(private readonly useCase: GetProformaUseCase) {
super(); super();
this.errorMapper = proformasApiErrorMapper; this.errorMapper = customerInvoicesApiErrorMapper;
// 🔐 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 { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts"; import { customerInvoicesApiErrorMapper } from "../../../express/proformas/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 = proformasApiErrorMapper; this.errorMapper = customerInvoicesApiErrorMapper;
// 🔐 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 { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts"; import { customerInvoicesApiErrorMapper } from "../../../express/proformas/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 = proformasApiErrorMapper; this.errorMapper = customerInvoicesApiErrorMapper;
// 🔐 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 { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts"; import { customerInvoicesApiErrorMapper } from "../../../express/proformas/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 = proformasApiErrorMapper; this.errorMapper = customerInvoicesApiErrorMapper;
// 🔐 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 { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts"; import { customerInvoicesApiErrorMapper } from "../../../express/proformas/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 = proformasApiErrorMapper; this.errorMapper = customerInvoicesApiErrorMapper;
// 🔐 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,2 +1 @@
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.global_discount_percentage.value), value: Number(dto.discount_percentage.value),
scale: Number(dto.global_discount_percentage.scale), scale: Number(dto.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.item_discount_percentage, (value) => maybeFromNullableResult(item.discount_percentage, (value) =>
ItemDiscountPercentage.create(value) ItemDiscountPercentage.create(value)
), ),
"discount_percentage", "discount_percentage",

View File

@ -368,12 +368,10 @@ export class ProformaRepository
? [options.include] ? [options.include]
: []; : [];
console.log(query.where);
query.where = { query.where = {
...query.where, ...query.where,
...(options.where ?? {}), ...(options.where ?? {}),
is_proforma: true, is_proforma: false,
company_id: companyId.toString(), company_id: companyId.toString(),
deleted_at: null, deleted_at: null,
}; };
@ -424,7 +422,7 @@ export class ProformaRepository
}), }),
]); ]);
return this.listMapper.mapToReadModelCollection(rows, count); return this.listMapper.mapToDTOCollection(rows, count);
} catch (err: unknown) { } catch (err: unknown) {
return Result.fail(translateSequelizeError(err)); return Result.fail(translateSequelizeError(err));
} }

View File

@ -7,10 +7,7 @@ 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(""),
item_discount_percentage: PercentageSchema.default({ discount_percentage: NumericStringSchema.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>;
@ -32,7 +29,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"),
global_discount_percentage: PercentageSchema.default({ discount_percentage: PercentageSchema.default({
value: "0", value: "0",
scale: "2", scale: "2",
}), }),

View File

@ -2,32 +2,29 @@ import type { ModuleClientParams } from "@erp/core/client";
import { lazy } from "react"; import { lazy } from "react";
import { Outlet, type RouteObject } from "react-router-dom"; import { Outlet, type RouteObject } from "react-router-dom";
import { ProformaCreatePage } from "./proformas/create";
const ProformaLayout = lazy(() => const ProformaLayout = lazy(() =>
import("./proformas/ui").then((m) => ({ default: m.ProformaLayout })) import("./proformas/ui").then((m) => ({ default: m.ProformaLayout }))
); );
const IssuedInvoicesLayout = lazy(() =>
import("./issued-invoices/ui").then((m) => ({ default: m.IssuedInvoicesLayout }))
);
const ProformasListPage = lazy(() => const ProformasListPage = lazy(() =>
import("./proformas/list").then((m) => ({ default: m.ProformaListPage })) import("./proformas/list").then((m) => ({ default: m.ProformaListPage }))
); );
const ProformasCreatePage = lazy(() =>
import("./proformas/create").then((m) => ({ default: m.ProformaCreatePage }))
);
/*const InvoiceUpdatePage = lazy(() =>
import("./pages").then((m) => ({ default: m.InvoiceUpdatePage }))
);*/
const IssuedInvoicesLayout = lazy(() =>
import("./issued-invoices/ui").then((m) => ({ default: m.IssuedInvoicesLayout }))
);
const IssuedInvoiceListPage = lazy(() => const IssuedInvoiceListPage = lazy(() =>
import("./issued-invoices/list").then((m) => ({ default: m.IssuedInvoiceListPage })) import("./issued-invoices/list").then((m) => ({ default: m.IssuedInvoiceListPage }))
); );
/*const CustomerInvoiceAdd = lazy(() =>
import("./pages").then((m) => ({ default: m.CustomerInvoiceCreate }))
);
const InvoiceUpdatePage = lazy(() =>
import("./pages").then((m) => ({ default: m.InvoiceUpdatePage }))
);*/
export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[] => { export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[] => {
return [ return [
{ {
@ -40,7 +37,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
children: [ children: [
{ path: "", index: true, element: <ProformasListPage /> }, // index { path: "", index: true, element: <ProformasListPage /> }, // index
{ path: "list", element: <ProformasListPage /> }, { path: "list", element: <ProformasListPage /> },
{ path: "create", element: <ProformaCreatePage /> }, //{ path: "create", element: <CustomerInvoiceAdd /> },
//{ path: ":id/edit", element: <InvoiceUpdatePage /> }, //{ path: ":id/edit", element: <InvoiceUpdatePage /> },
], ],
}, },

View File

@ -1,4 +1,4 @@
import { MoneyDTOHelper, formatCurrency } from "@erp/core"; import { MoneyDTOHelper, PercentageDTOHelper, formatCurrency } from "@erp/core";
import type { import type {
IssuedInvoiceSummaryData, IssuedInvoiceSummaryData,
@ -26,14 +26,14 @@ export const IssuedInvoiceSummaryDtoAdapter = {
summaryDto.language_code summaryDto.language_code
), ),
/*discount_percentage: PercentageDTOHelper.toNumber(summaryDto.discount_percentage), discount_percentage: PercentageDTOHelper.toNumber(summaryDto.discount_percentage),
discount_percentage_fmt: PercentageDTOHelper.toNumericString( discount_percentage_fmt: PercentageDTOHelper.toNumericString(
summaryDto.discount_percentage summaryDto.discount_percentage
),*/ ),
discount_amount: MoneyDTOHelper.toNumber(summaryDto.total_discount_amount), discount_amount: MoneyDTOHelper.toNumber(summaryDto.discount_amount),
discount_amount_fmt: formatCurrency( discount_amount_fmt: formatCurrency(
MoneyDTOHelper.toNumber(summaryDto.total_discount_amount), MoneyDTOHelper.toNumber(summaryDto.discount_amount),
Number(summaryDto.total_amount.scale || 2), Number(summaryDto.total_amount.scale || 2),
summaryDto.currency_code, summaryDto.currency_code,
summaryDto.language_code summaryDto.language_code

View File

@ -1 +0,0 @@
export * from "./ui";

View File

@ -1 +0,0 @@
export * from "./pages";

View File

@ -1 +0,0 @@
export * from "./proforma-create-page";

View File

@ -1,47 +0,0 @@
import { PageHeader } from "@erp/core/components";
import { AppContent, AppHeader } from "@repo/rdx-ui/components";
import { Button } from "@repo/shadcn-ui/components";
import { PlusIcon } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "../../../../i18n";
export const ProformaCreatePage = () => {
const { t } = useTranslation();
const navigate = useNavigate();
return (
<>
<AppHeader>
<PageHeader
description={t("pages.proformas.list.description")}
rightSlot={
<Button
aria-label={t("pages.proformas.create.title")}
onClick={() => navigate("/proformas/create")}
>
<PlusIcon aria-hidden className="mr-2 size-4" />
{t("pages.proformas.create.title")}
</Button>
}
title={t("pages.proformas.list.title")}
/>
</AppHeader>
<AppContent>
<div className="flex items-center justify-between space-y-2">
<div>
<h2 className="text-2xl font-bold tracking-tight">{t("pages.create.title")}</h2>
<p className="text-muted-foreground">{t("pages.create.description")}</p>
</div>
<div className="flex items-center justify-end mb-4">
<Button className="cursor-pointer" onClick={() => navigate("/customer-invoices/list")}>
{t("pages.create.back_to_list")}
</Button>
</div>
</div>
<div className="flex flex-1 flex-col gap-4 p-4">hola</div>
</AppContent>
</>
);
};

View File

@ -63,6 +63,7 @@ export const ProformaListPage = () => {
rightSlot={ rightSlot={
<Button <Button
aria-label={t("pages.proformas.create.title")} aria-label={t("pages.proformas.create.title")}
className="hidden"
onClick={() => navigate("/proformas/create")} onClick={() => navigate("/proformas/create")}
> >
<PlusIcon aria-hidden className="mr-2 size-4" /> <PlusIcon aria-hidden className="mr-2 size-4" />

View File

@ -3,7 +3,7 @@ import { Criteria } from "@repo/rdx-criteria/server";
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Collection, Result } from "@repo/rdx-utils"; import { Collection, Result } from "@repo/rdx-utils";
import { Transaction } from "sequelize"; import { Transaction } from "sequelize";
import { Customer, CustomerPatchProps, ICustomerProps, ICustomerRepository } from "../domain"; import { Customer, CustomerPatchProps, CustomerProps, ICustomerRepository } from "../domain";
import { CustomerListDTO } from "../infrastructure"; import { CustomerListDTO } from "../infrastructure";
export class CustomerApplicationService { export class CustomerApplicationService {
@ -19,7 +19,7 @@ export class CustomerApplicationService {
*/ */
buildCustomerInCompany( buildCustomerInCompany(
companyId: UniqueID, companyId: UniqueID,
props: Omit<ICustomerProps, "companyId">, props: Omit<CustomerProps, "companyId">,
customerId?: UniqueID customerId?: UniqueID
): Result<Customer, Error> { ): Result<Customer, Error> {
return Customer.create({ ...props, companyId }, customerId); return Customer.create({ ...props, companyId }, customerId);

View File

@ -1,6 +0,0 @@
import type { IProformaRepository } from "../repositories";
import { type IProformaFinder, ProformaFinder } from "../services";
export function buildProformaFinder(repository: IProformaRepository): IProformaFinder {
return new ProformaFinder(repository);
}

View File

@ -1,41 +0,0 @@
// application/issued-invoices/di/snapshot-builders.di.ts
import {
CustomerFullSnapshotBuilder,
CustomerItemReportSnapshotBuilder,
CustomerItemsFullSnapshotBuilder,
CustomerListItemSnapshotBuilder,
CustomerRecipientFullSnapshotBuilder,
CustomerReportSnapshotBuilder,
CustomerTaxReportSnapshotBuilder,
CustomerTaxesFullSnapshotBuilder,
} from "../snapshot-builders";
export function buildCustomerSnapshotBuilders() {
const itemsBuilder = new CustomerItemsFullSnapshotBuilder();
const taxesBuilder = new CustomerTaxesFullSnapshotBuilder();
const recipientBuilder = new CustomerRecipientFullSnapshotBuilder();
const fullSnapshotBuilder = new CustomerFullSnapshotBuilder(
itemsBuilder,
recipientBuilder,
taxesBuilder
);
const listSnapshotBuilder = new CustomerListItemSnapshotBuilder();
const itemsReportBuilder = new CustomerItemReportSnapshotBuilder();
const taxesReportBuilder = new CustomerTaxReportSnapshotBuilder();
const reportSnapshotBuilder = new CustomerReportSnapshotBuilder(
itemsReportBuilder,
taxesReportBuilder
);
return {
full: fullSnapshotBuilder,
list: listSnapshotBuilder,
report: reportSnapshotBuilder,
};
}

View File

@ -1,83 +0,0 @@
import type { ITransactionManager } from "@erp/core/api";
import type { ICreateCustomerInputMapper } from "../mappers";
import type {
ICustomerCreator,
ICustomerFinder,
CustomerDocumentGeneratorService,
} from "../services";
import type {
ICustomerListItemSnapshotBuilder,
ICustomerReportSnapshotBuilder,
} from "../snapshot-builders";
import type { ICustomerFullSnapshotBuilder } from "../snapshot-builders/full";
import { GetCustomerByIdUseCase, ListCustomersUseCase, ReportCustomerUseCase } from "../use-cases";
import { CreateCustomerUseCase } from "../use-cases/create-customer";
export function buildGetCustomerByIdUseCase(deps: {
finder: ICustomerFinder;
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
transactionManager: ITransactionManager;
}) {
return new GetCustomerByIdUseCase(deps.finder, deps.fullSnapshotBuilder, deps.transactionManager);
}
export function buildListCustomersUseCase(deps: {
finder: ICustomerFinder;
itemSnapshotBuilder: ICustomerListItemSnapshotBuilder;
transactionManager: ITransactionManager;
}) {
return new ListCustomersUseCase(deps.finder, deps.itemSnapshotBuilder, deps.transactionManager);
}
export function buildReportCustomerUseCase(deps: {
finder: ICustomerFinder;
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
reportSnapshotBuilder: ICustomerReportSnapshotBuilder;
documentService: CustomerDocumentGeneratorService;
transactionManager: ITransactionManager;
}) {
return new ReportCustomerUseCase(
deps.finder,
deps.fullSnapshotBuilder,
deps.reportSnapshotBuilder,
deps.documentService,
deps.transactionManager
);
}
export function buildCreateCustomerUseCase(deps: {
creator: ICustomerCreator;
dtoMapper: ICreateCustomerInputMapper;
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
transactionManager: ITransactionManager;
}) {
return new CreateCustomerUseCase({
dtoMapper: deps.dtoMapper,
creator: deps.creator,
fullSnapshotBuilder: deps.fullSnapshotBuilder,
transactionManager: deps.transactionManager,
});
}
/*export function buildUpdateCustomerUseCase(deps: {
finder: ICustomerFinder;
fullSnapshotBuilder: ICustomerFullSnapshotBuilder;
}) {
return new UpdateCustomerUseCase(deps.finder, deps.fullSnapshotBuilder);
}
export function buildDeleteCustomerUseCase(deps: { finder: ICustomerFinder }) {
return new DeleteCustomerUseCase(deps.finder);
}
export function buildIssueCustomerUseCase(deps: { finder: ICustomerFinder }) {
return new IssueCustomerUseCase(deps.finder);
}
export function buildChangeStatusCustomerUseCase(deps: {
finder: ICustomerFinder;
transactionManager: ITransactionManager;
}) {
return new ChangeStatusCustomerUseCase(deps.finder, deps.transactionManager);
}*/

View File

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

View File

@ -1,7 +1,3 @@
export * from "./di"; //export * from "./customer-application.service";
export * from "./mappers"; //export * from "./presenters";
export * from "./models";
export * from "./repositories";
export * from "./services";
export * from "./snapshot-builders";
export * from "./use-cases"; export * from "./use-cases";

View File

@ -1,7 +0,0 @@
import type { DomainMapperWithBulk } from "@erp/core/api";
import type { Customer } from "../../domain";
import type { CustomerCreationAttributes, CustomerModel } from "../../infrastructure";
export interface ICustomerDomainMapper
extends DomainMapperWithBulk<CustomerModel | CustomerCreationAttributes, Customer> {}

View File

@ -1,7 +0,0 @@
import type { IQueryMapperWithBulk } from "@erp/core/api";
import type { CustomerModel } from "../../infrastructure";
import type { CustomerSummary } from "../models";
export interface ICustomerSummaryMapper
extends IQueryMapperWithBulk<CustomerModel, CustomerSummary> {}

View File

@ -1,2 +0,0 @@
export * from "./customer-domain-mapper.interface";
export * from "./customer-summary-mapper.interface";

View File

@ -1,41 +0,0 @@
import type {
CurrencyCode,
EmailAddress,
LanguageCode,
Name,
PhoneNumber,
PostalAddress,
TINNumber,
URLAddress,
UniqueID,
} from "@repo/rdx-ddd";
import type { Maybe } from "@repo/rdx-utils";
import type { CustomerStatus } from "../../domain";
export type CustomerSummary = {
id: UniqueID;
companyId: UniqueID;
status: CustomerStatus;
reference: Maybe<Name>;
isCompany: boolean;
name: Name;
tradeName: Maybe<Name>;
tin: Maybe<TINNumber>;
address: PostalAddress;
emailPrimary: Maybe<EmailAddress>;
emailSecondary: Maybe<EmailAddress>;
phonePrimary: Maybe<PhoneNumber>;
phoneSecondary: Maybe<PhoneNumber>;
mobilePrimary: Maybe<PhoneNumber>;
mobileSecondary: Maybe<PhoneNumber>;
fax: Maybe<PhoneNumber>;
website: Maybe<URLAddress>;
languageCode: LanguageCode;
currencyCode: CurrencyCode;
};

View File

@ -1 +0,0 @@
export * from "./customer-summary";

View File

@ -1,56 +0,0 @@
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 { Customer } from "../../domain";
import type { CustomerListDTO } from "../dtos";
import type { ICustomerRepository } from "../repositories";
export interface ICustomerFinder {
findCustomerById(
companyId: UniqueID,
invoiceId: UniqueID,
transaction?: Transaction
): Promise<Result<Customer, Error>>;
customerExists(
companyId: UniqueID,
invoiceId: UniqueID,
transaction?: Transaction
): Promise<Result<boolean, Error>>;
findCustomersByCriteria(
companyId: UniqueID,
criteria: Criteria,
transaction?: Transaction
): Promise<Result<Collection<CustomerListDTO>, Error>>;
}
export class CustomerFinder implements ICustomerFinder {
constructor(private readonly repository: ICustomerRepository) {}
async findCustomerById(
companyId: UniqueID,
customerId: UniqueID,
transaction?: Transaction
): Promise<Result<Customer, Error>> {
return this.repository.getByIdInCompany(companyId, customerId, transaction);
}
async customerExists(
companyId: UniqueID,
customerId: UniqueID,
transaction?: Transaction
): Promise<Result<boolean, Error>> {
return this.repository.existsByIdInCompany(companyId, customerId, transaction);
}
async findCustomersByCriteria(
companyId: UniqueID,
criteria: Criteria,
transaction?: Transaction
): Promise<Result<Collection<CustomerListDTO>, Error>> {
return this.repository.findByCriteriaInCompany(companyId, criteria, transaction);
}
}

View File

@ -1 +0,0 @@
export * from "./customer-finder";

View File

@ -1,3 +0,0 @@
//export * from "./full";
export * from "./list";
//export * from "./report";

View File

@ -1,14 +0,0 @@
import type { ISnapshotBuilder } from "@erp/core/api";
import type { CustomerListDTO } from "../../dtos";
import type { ICustomerListItemSnapshot } from "./customer-list-item-snapshot.interface";
export interface ICustomerListItemSnapshotBuilder
extends ISnapshotBuilder<CustomerListDTO, ICustomerListItemSnapshot> {}
export class CustomerListItemSnapshotBuilder implements ICustomerListItemSnapshotBuilder {
toOutput(customer: CustomerListDTO): ICustomerListItemSnapshot {
return {};
}
}

View File

@ -1 +0,0 @@
export type ICustomerListItemSnapshot = {};

View File

@ -1,2 +0,0 @@
export * from "./customer-list-item-snapshot.interface";
export * from "./customer-list-item-snapshot-builder";

View File

@ -24,7 +24,7 @@ import {
import { Collection, Result, isNullishOrEmpty } from "@repo/rdx-utils"; import { Collection, Result, isNullishOrEmpty } from "@repo/rdx-utils";
import type { CreateCustomerRequestDTO } from "../../../../common"; import type { CreateCustomerRequestDTO } from "../../../../common";
import { type ICustomerProps, CustomerStatus } from "../../../domain"; import { type CustomerProps, CustomerStatus } from "../../../domain";
/** /**
* Convierte el DTO a las props validadas (CustomerProps). * Convierte el DTO a las props validadas (CustomerProps).
@ -197,7 +197,7 @@ export function mapDTOToCreateCustomerProps(dto: CreateCustomerRequestDTO) {
errors errors
); );
const customerProps: Omit<ICustomerProps, "companyId"> = { const customerProps: Omit<CustomerProps, "companyId"> = {
status: status!, status: status!,
reference: reference!, reference: reference!,

View File

@ -1,12 +1,11 @@
import type { ITransactionManager } from "@erp/core/api"; import { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
import type { Criteria } from "@repo/rdx-criteria/server"; import { Criteria } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize"; import { Transaction } from "sequelize";
import { ListCustomersResponseDTO } from "../../../common/dto";
import type { ListCustomersResponseDTO } from "../../../common/dto"; import { CustomerApplicationService } from "../../application";
import type { ICustomerFinder } from "../services"; import { ListCustomersPresenter } from "../presenters";
import type { ICustomerListItemSnapshotBuilder } from "../snapshot-builders/list";
type ListCustomersUseCaseInput = { type ListCustomersUseCaseInput = {
companyId: UniqueID; companyId: UniqueID;
@ -15,42 +14,39 @@ type ListCustomersUseCaseInput = {
export class ListCustomersUseCase { export class ListCustomersUseCase {
constructor( constructor(
private readonly finder: ICustomerFinder, private readonly service: CustomerApplicationService,
private readonly listItemSnapshotBuilder: ICustomerListItemSnapshotBuilder, private readonly transactionManager: ITransactionManager,
private readonly transactionManager: ITransactionManager private readonly presenterRegistry: IPresenterRegistry
) {} ) {}
public execute( public execute(
params: ListCustomersUseCaseInput params: ListCustomersUseCaseInput
): Promise<Result<ListCustomersResponseDTO, Error>> { ): Promise<Result<ListCustomersResponseDTO, Error>> {
const { criteria, companyId } = params; const { criteria, companyId } = params;
const presenter = this.presenterRegistry.getPresenter({
resource: "customer",
projection: "LIST",
}) as ListCustomersPresenter;
return this.transactionManager.complete(async (transaction: Transaction) => { return this.transactionManager.complete(async (transaction: Transaction) => {
try { try {
const result = await this.finder.findCustomersByCriteria(companyId, criteria, transaction); const result = await this.service.findCustomerByCriteriaInCompany(
companyId,
criteria,
transaction
);
if (result.isFailure) { if (result.isFailure) {
return Result.fail(result.error); return Result.fail(result.error);
} }
const customers = result.data; const customers = result.data;
const totalCustomers = customers.total(); const dto = presenter.toOutput({
customers,
criteria,
});
const items = customers.map((item) => this.listItemSnapshotBuilder.toOutput(item)); return Result.ok(dto);
const snapshot = {
page: criteria.pageNumber,
per_page: criteria.pageSize,
total_pages: Math.ceil(totalCustomers / criteria.pageSize),
total_items: totalCustomers,
items: items,
metadata: {
entity: "customers",
criteria: criteria.toJSON(),
},
};
return Result.ok(snapshot);
} catch (error: unknown) { } catch (error: unknown) {
return Result.fail(error as Error); return Result.fail(error as Error);
} }

View File

@ -17,7 +17,7 @@ import { type Collection, type Maybe, Result } from "@repo/rdx-utils";
import type { CustomerStatus } from "../value-objects"; import type { CustomerStatus } from "../value-objects";
export interface ICustomerProps { export interface CustomerProps {
companyId: UniqueID; companyId: UniqueID;
status: CustomerStatus; status: CustomerStatus;
reference: Maybe<Name>; reference: Maybe<Name>;
@ -49,7 +49,7 @@ export interface ICustomerProps {
currencyCode: CurrencyCode; currencyCode: CurrencyCode;
} }
export type CustomerPatchProps = Partial<Omit<ICustomerProps, "companyId" | "address">> & { export type CustomerPatchProps = Partial<Omit<CustomerProps, "companyId" | "address">> & {
address?: PostalAddressPatchProps; address?: PostalAddressPatchProps;
}; };
@ -90,12 +90,8 @@ export interface ICustomer {
readonly currencyCode: CurrencyCode; readonly currencyCode: CurrencyCode;
} }
type CreateCustomerProps = ICustomerProps; export class Customer extends AggregateRoot<CustomerProps> implements ICustomer {
type InternalCustomerProps = ICustomerProps; static create(props: CustomerProps, id?: UniqueID): Result<Customer, Error> {
export class Customer extends AggregateRoot<InternalCustomerProps> implements ICustomer {
static create(props: CreateCustomerProps, id?: UniqueID): Result<Customer, Error> {
const contact = new Customer(props, id); const contact = new Customer(props, id);
// Reglas de negocio / validaciones // Reglas de negocio / validaciones
@ -109,18 +105,12 @@ export class Customer extends AggregateRoot<InternalCustomerProps> implements IC
return Result.ok(contact); return Result.ok(contact);
} }
// Rehidratación desde persistencia
static rehydrate(props: InternalCustomerProps, id: UniqueID): Customer {
return new Customer(props, id);
}
public update(partialCustomer: CustomerPatchProps): Result<Customer, Error> { public update(partialCustomer: CustomerPatchProps): Result<Customer, Error> {
const { address: partialAddress, ...rest } = partialCustomer; const { address: partialAddress, ...rest } = partialCustomer;
const updatedProps = { const updatedProps = {
...this.props, ...this.props,
...rest, ...rest,
} as ICustomerProps; } as CustomerProps;
if (partialAddress) { if (partialAddress) {
const updatedAddressOrError = this.address.update(partialAddress); const updatedAddressOrError = this.address.update(partialAddress);
@ -134,8 +124,6 @@ export class Customer extends AggregateRoot<InternalCustomerProps> implements IC
return Customer.create(updatedProps, this.id); return Customer.create(updatedProps, this.id);
} }
// Getters
public get isIndividual(): boolean { public get isIndividual(): boolean {
return !this.props.isCompany; return !this.props.isCompany;
} }

View File

@ -1 +1 @@
export * from "./customer.aggregate"; export * from "./customer";

View File

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

View File

@ -1,9 +1,8 @@
import type { Criteria } from "@repo/rdx-criteria/server"; import { Criteria } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import type { Collection, Result } from "@repo/rdx-utils"; import { Collection, Result } from "@repo/rdx-utils";
import { CustomerListDTO } from "../../infrastructure/mappers";
import type { Customer } from "../../domain/aggregates"; import { Customer } from "../aggregates";
import type { CustomerSummary } from "../models";
/** /**
* Interfaz del repositorio para el agregado `Customer`. * Interfaz del repositorio para el agregado `Customer`.
@ -57,7 +56,7 @@ export interface ICustomerRepository {
companyId: UniqueID, companyId: UniqueID,
criteria: Criteria, criteria: Criteria,
transaction: unknown transaction: unknown
): Promise<Result<Collection<CustomerSummary>, Error>>; ): Promise<Result<Collection<CustomerListDTO>, Error>>;
/** /**
* Elimina un Customer por su ID, dentro de una empresa. * Elimina un Customer por su ID, dentro de una empresa.

View File

@ -1,4 +1,4 @@
export * from "./customer-address-type.vo"; export * from "./customer-address-type";
export * from "./customer-number.vo"; export * from "./customer-number";
export * from "./customer-serie.vo"; export * from "./customer-serie";
export * from "./customer-status.vo"; export * from "./customer-status";

View File

@ -1,6 +1,6 @@
import type { IModuleServer } from "@erp/core/api"; import type { IModuleServer } from "@erp/core/api";
import { customersRouter, models } from "./infrastructure"; import { models } from "./infrastructure";
export * from "./infrastructure/sequelize"; export * from "./infrastructure/sequelize";
@ -19,29 +19,25 @@ export const customersAPIModule: IModuleServer = {
async setup(params) { async setup(params) {
const { env: ENV, app, database, baseRoutePath: API_BASE_PATH, logger } = params; const { env: ENV, app, database, baseRoutePath: API_BASE_PATH, logger } = params;
// 1) Dominio interno
//const customerInternalDeps = buildCustomerDependencies(params);
// 2) Servicios públicos (Application Services)
//const customerServices = buildCustomerServices(customerInternalDeps);
logger.info("🚀 Customers module dependencies registered", { logger.info("🚀 Customers module dependencies registered", {
label: this.name, label: this.name,
}); });
return { return {
// Modelos Sequelize del módulo // Modelos Sequelize del módulo
models, models,
// Servicios expuestos a otros módulos // Servicios expuestos a otros módulos
services: { services: {
//customers: customerServices, customers: {
/*...*/
},
}, },
// Implementación privada del módulo // Implementación privada del módulo
internal: { internal: {
//customers: customerInternalDeps, customers: {
/*...*/
},
}, },
}; };
}, },
@ -60,7 +56,7 @@ export const customersAPIModule: IModuleServer = {
const customersInternalDeps = getInternal("customers", "customers"); const customersInternalDeps = getInternal("customers", "customers");
// Registro de rutas HTTP // Registro de rutas HTTP
customersRouter(params, customersInternalDeps); //customersRouter(params, customersInternalDeps);
logger.info("🚀 Customers module started", { logger.info("🚀 Customers module started", {
label: this.name, label: this.name,

View File

@ -14,7 +14,7 @@ import {
import { CustomerApplicationService } from "../application/customer-application.service"; import { CustomerApplicationService } from "../application/customer-application.service";
import { CustomerFullPresenter, ListCustomersPresenter } from "../application/presenters"; import { CustomerFullPresenter, ListCustomersPresenter } from "../application/presenters";
import { CustomerDomainMapper, CustomerSummaryMapper } from "./mappers"; import { CustomerDomainMapper, CustomerListMapper } from "./mappers";
import { CustomerRepository } from "./sequelize"; import { CustomerRepository } from "./sequelize";
export type CustomerDeps = { export type CustomerDeps = {
@ -40,7 +40,7 @@ export function buildCustomerDependencies(params: ModuleParams): CustomerDeps {
const mapperRegistry = new InMemoryMapperRegistry(); const mapperRegistry = new InMemoryMapperRegistry();
mapperRegistry mapperRegistry
.registerDomainMapper({ resource: "customer" }, new CustomerDomainMapper()) .registerDomainMapper({ resource: "customer" }, new CustomerDomainMapper())
.registerQueryMapper({ resource: "customer", query: "LIST" }, new CustomerSummaryMapper()); .registerQueryMapper({ resource: "customer", query: "LIST" }, new CustomerListMapper());
// Repository & Services // Repository & Services
const repo = new CustomerRepository({ mapperRegistry, database }); const repo = new CustomerRepository({ mapperRegistry, database });

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