Compare commits
2 Commits
941ad25401
...
bf9ed99a90
| Author | SHA1 | Date | |
|---|---|---|---|
| bf9ed99a90 | |||
| bba38e67f2 |
@ -8,8 +8,7 @@
|
||||
"dev": "node --import=tsx --watch src/index.ts",
|
||||
"clean": "rimraf .turbo node_modules dist",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "biome check . && eslint .",
|
||||
"lint:fix": "biome check --write . && eslint . --fix",
|
||||
"lint": "biome lint --fix",
|
||||
"format": "biome format --write"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import { IModuleClient } from "@erp/core/client";
|
||||
import type { IModuleClient } from "@erp/core/client";
|
||||
import { AppLayout } from "@repo/rdx-ui/components";
|
||||
import { createBrowserRouter, createRoutesFromElements, Navigate, Route } from "react-router-dom";
|
||||
import { Navigate, Route, createBrowserRouter, createRoutesFromElements } from "react-router-dom";
|
||||
|
||||
import { ModuleRoutes } from "@/components/module-routes";
|
||||
|
||||
import { ErrorPage, LoginForm } from "../pages";
|
||||
import { modules } from "../register-modules"; // Aquí ca
|
||||
|
||||
@ -33,24 +35,24 @@ export const getAppRouter = () => {
|
||||
|
||||
return createBrowserRouter(
|
||||
createRoutesFromElements(
|
||||
<Route path='/'>
|
||||
<Route path="/">
|
||||
{/* Auth Layout */}
|
||||
<Route path='/auth'>
|
||||
<Route index element={<Navigate to='login' />} />
|
||||
<Route path='login' element={<LoginForm />} />
|
||||
<Route path='*' element={<ModuleRoutes modules={grouped.auth} params={params} />} />
|
||||
<Route path="/auth">
|
||||
<Route element={<Navigate to="login" />} index />
|
||||
<Route element={<LoginForm />} path="login" />
|
||||
<Route element={<ModuleRoutes modules={grouped.auth} params={params} />} path="*" />
|
||||
</Route>
|
||||
|
||||
{/* App Layout */}
|
||||
<Route element={<AppLayout />}>
|
||||
{/* Dynamic Module Routes */}
|
||||
<Route path='*' element={<ModuleRoutes modules={grouped.app} params={params} />} />
|
||||
<Route element={<ModuleRoutes modules={grouped.app} params={params} />} path="*" />
|
||||
|
||||
{/* Main Layout */}
|
||||
<Route path='/dashboard' element={<ErrorPage />} />
|
||||
<Route path='/settings' element={<ErrorPage />} />
|
||||
<Route path='/catalog' element={<ErrorPage />} />
|
||||
<Route path='/quotes' element={<ErrorPage />} />
|
||||
<Route element={<ErrorPage />} path="/dashboard" />
|
||||
<Route element={<ErrorPage />} path="/settings" />
|
||||
<Route element={<ErrorPage />} path="/catalog" />
|
||||
<Route element={<ErrorPage />} path="/quotes" />
|
||||
</Route>
|
||||
</Route>
|
||||
)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
export * from "./documents";
|
||||
export * from "./mappers";
|
||||
export * from "./presenters";
|
||||
export * from "./renderers";
|
||||
export * from "./snapshot-builders";
|
||||
|
||||
1
modules/core/src/api/application/mappers/index.ts
Normal file
1
modules/core/src/api/application/mappers/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./mapper.interface";
|
||||
@ -56,23 +56,21 @@ export type DomainMapperWithBulk<TPersistence, TDomain> = IDomainMapper<TPersist
|
||||
Partial<IBulkDomainMapper<TPersistence, TDomain>>;
|
||||
|
||||
/**
|
||||
*
|
||||
* 👓 Mapper de Read Model (Persistencia ↔ DTO/Proyección de Lectura)
|
||||
* - Responsabilidad: transformar registros de persistencia en DTOs para lectura (listados, resúmenes, informes).
|
||||
* 👓 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).
|
||||
* - No intenta reconstruir agregados ni validar value objects de dominio.
|
||||
**/
|
||||
export interface IQueryMapperWithBulk<TPersistence, TDTO> {
|
||||
export interface IQueryMapperWithBulk<TPersistence, TReadModel> {
|
||||
/**
|
||||
* Convierte un registro crudo en un DTO de lectura.
|
||||
* Convierte un registro crudo en un read model de lectura.
|
||||
*/
|
||||
mapToDTO(raw: TPersistence, params?: MapperParamsType): Result<TDTO, Error>;
|
||||
|
||||
mapToReadModel(raw: TPersistence, params?: MapperParamsType): Result<TReadModel, Error>;
|
||||
/**
|
||||
* Convierte múltiples registros crudos en una Collection de DTOs de lectura.
|
||||
* Convierte múltiples registros crudos en una Collection de read models de lectura.
|
||||
*/
|
||||
mapToDTOCollection(
|
||||
mapToReadModelCollection(
|
||||
raws: TPersistence[],
|
||||
totalCount: number,
|
||||
params?: MapperParamsType
|
||||
): Result<Collection<TDTO>, Error>;
|
||||
): Result<Collection<TReadModel>, Error>;
|
||||
}
|
||||
@ -1,3 +1,2 @@
|
||||
export * from "./errors";
|
||||
export * from "./repositories";
|
||||
export * from "./value-objects";
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export * from "./repository.interface";
|
||||
@ -1,7 +1,7 @@
|
||||
import { Collection, Result, ResultCollection } from "@repo/rdx-utils";
|
||||
import type { Model } from "sequelize";
|
||||
|
||||
import type { MapperParamsType } from "../../../../domain";
|
||||
import type { MapperParamsType } from "../../../../application";
|
||||
|
||||
import type { ISequelizeDomainMapper } from "./sequelize-mapper.interface";
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { DomainMapperWithBulk, IQueryMapperWithBulk } from "../../../../domain";
|
||||
import type { DomainMapperWithBulk, IQueryMapperWithBulk } from "@erp/core/api/application";
|
||||
|
||||
export interface ISequelizeDomainMapper<TModel, TModelAttributes, TEntity>
|
||||
extends DomainMapperWithBulk<TModel | TModelAttributes, TEntity> {}
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
import { Collection, Result } from "@repo/rdx-utils";
|
||||
import type { Model } from "sequelize";
|
||||
|
||||
import type { MapperParamsType } from "../../../../domain";
|
||||
import type { MapperParamsType } from "../../../../application";
|
||||
|
||||
import type { ISequelizeQueryMapper } from "./sequelize-mapper.interface";
|
||||
|
||||
export abstract class SequelizeQueryMapper<TModel extends Model, TEntity>
|
||||
implements ISequelizeQueryMapper<TModel, TEntity>
|
||||
{
|
||||
public abstract mapToDTO(raw: TModel, params?: MapperParamsType): Result<TEntity, Error>;
|
||||
public abstract mapToReadModel(raw: TModel, params?: MapperParamsType): Result<TEntity, Error>;
|
||||
|
||||
public mapToDTOCollection(
|
||||
public mapToReadModelCollection(
|
||||
raws: TModel[],
|
||||
totalCount: number,
|
||||
params?: MapperParamsType
|
||||
@ -23,7 +23,7 @@ export abstract class SequelizeQueryMapper<TModel extends Model, TEntity>
|
||||
}
|
||||
|
||||
const items = _source.map((value, index) => {
|
||||
const result = this.mapToDTO(value as TModel, { index, ...params });
|
||||
const result = this.mapToReadModel(value as TModel, { index, ...params });
|
||||
if (result.isFailure) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export * from "./issued-invoice-list.dto";
|
||||
@ -1,7 +1,6 @@
|
||||
export * from "./application-models";
|
||||
export * from "./di";
|
||||
export * from "./dtos";
|
||||
export * from "./mappers";
|
||||
export * from "./models";
|
||||
export * from "./repositories";
|
||||
export * from "./services";
|
||||
export * from "./snapshot-builders";
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type { MapperParamsType } from "@erp/core/api";
|
||||
import type { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { IssuedInvoiceListDTO } from "../dtos";
|
||||
import type { IssuedInvoiceSummary } from "../models";
|
||||
|
||||
export interface IIssuedInvoiceListMapper {
|
||||
mapToDTO(raw: unknown, params?: MapperParamsType): Result<IssuedInvoiceListDTO, Error>;
|
||||
export interface IIssuedInvoiceSummaryMapper {
|
||||
mapToDTO(raw: unknown, params?: MapperParamsType): Result<IssuedInvoiceSummary, Error>;
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from "./issued-invoice-summary";
|
||||
@ -10,7 +10,7 @@ import type {
|
||||
VerifactuRecord,
|
||||
} from "../../../domain";
|
||||
|
||||
export type IssuedInvoiceListDTO = {
|
||||
export type IssuedInvoiceSummary = {
|
||||
id: UniqueID;
|
||||
companyId: UniqueID;
|
||||
|
||||
@ -3,7 +3,7 @@ import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import type { Collection, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { IssuedInvoice } from "../../../domain";
|
||||
import type { IssuedInvoiceListDTO } from "../dtos";
|
||||
import type { IssuedInvoiceSummary } from "../models";
|
||||
|
||||
export interface IIssuedInvoiceRepository {
|
||||
create(invoice: IssuedInvoice, transaction?: unknown): Promise<Result<void, Error>>;
|
||||
@ -24,5 +24,5 @@ export interface IIssuedInvoiceRepository {
|
||||
companyId: UniqueID,
|
||||
criteria: Criteria,
|
||||
transaction: unknown
|
||||
): Promise<Result<Collection<IssuedInvoiceListDTO>, Error>>;
|
||||
): Promise<Result<Collection<IssuedInvoiceSummary>, Error>>;
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import type { Collection, Result } from "@repo/rdx-utils";
|
||||
import type { Transaction } from "sequelize";
|
||||
|
||||
import type { IssuedInvoice } from "../../../domain";
|
||||
import type { IssuedInvoiceListDTO } from "../dtos";
|
||||
import type { IssuedInvoiceSummary } from "../models";
|
||||
import type { IIssuedInvoiceRepository } from "../repositories";
|
||||
|
||||
export interface IIssuedInvoiceFinder {
|
||||
@ -24,7 +24,7 @@ export interface IIssuedInvoiceFinder {
|
||||
companyId: UniqueID,
|
||||
criteria: Criteria,
|
||||
transaction?: Transaction
|
||||
): Promise<Result<Collection<IssuedInvoiceListDTO>, Error>>;
|
||||
): Promise<Result<Collection<IssuedInvoiceSummary>, Error>>;
|
||||
}
|
||||
|
||||
export class IssuedInvoiceFinder implements IIssuedInvoiceFinder {
|
||||
@ -50,7 +50,7 @@ export class IssuedInvoiceFinder implements IIssuedInvoiceFinder {
|
||||
companyId: UniqueID,
|
||||
criteria: Criteria,
|
||||
transaction?: Transaction
|
||||
): Promise<Result<Collection<IssuedInvoiceListDTO>, Error>> {
|
||||
): Promise<Result<Collection<IssuedInvoiceSummary>, Error>> {
|
||||
return this.repository.findByCriteriaInCompany(companyId, criteria, transaction);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import type { ISnapshotBuilder } from "@erp/core/api";
|
||||
import { maybeToEmptyString } from "@repo/rdx-ddd";
|
||||
|
||||
import type { IssuedInvoiceListDTO } from "../../dtos";
|
||||
import type { IssuedInvoiceSummary } from "../../models";
|
||||
|
||||
import type { IIssuedInvoiceListItemSnapshot } from "./issued-invoice-list-item-snapshot.interface";
|
||||
|
||||
export interface IIssuedInvoiceListItemSnapshotBuilder
|
||||
extends ISnapshotBuilder<IssuedInvoiceListDTO, IIssuedInvoiceListItemSnapshot> {}
|
||||
extends ISnapshotBuilder<IssuedInvoiceSummary, IIssuedInvoiceListItemSnapshot> {}
|
||||
|
||||
export class IssuedInvoiceListItemSnapshotBuilder implements IIssuedInvoiceListItemSnapshotBuilder {
|
||||
toOutput(invoice: IssuedInvoiceListDTO): IIssuedInvoiceListItemSnapshot {
|
||||
toOutput(invoice: IssuedInvoiceSummary): IIssuedInvoiceListItemSnapshot {
|
||||
const recipient = invoice.recipient.toObjectString();
|
||||
|
||||
const verifactu = invoice.verifactu.match(
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export * from "./proforma-list.dto";
|
||||
@ -1,7 +1,6 @@
|
||||
export * from "./application-models";
|
||||
export * from "./di";
|
||||
export * from "./dtos";
|
||||
export * from "./mappers";
|
||||
export * from "./models";
|
||||
export * from "./repositories";
|
||||
export * from "./services";
|
||||
export * from "./snapshot-builders";
|
||||
|
||||
@ -41,13 +41,20 @@ import {
|
||||
*
|
||||
*/
|
||||
|
||||
export interface ICreateProformaInputMapper
|
||||
/*export interface ICreateProformaInputMapper
|
||||
extends IDTOInputToPropsMapper<
|
||||
CreateProformaRequestDTO,
|
||||
{ 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;
|
||||
|
||||
constructor(params: { taxCatalog: JsonTaxCatalogProvider }) {
|
||||
@ -138,14 +145,14 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
|
||||
|
||||
const globalDiscountPercentage = extractOrPushError(
|
||||
Percentage.create({
|
||||
value: Number(dto.discount_percentage.value),
|
||||
scale: Number(dto.discount_percentage.scale),
|
||||
value: Number(dto.global_discount_percentage.value),
|
||||
scale: Number(dto.global_discount_percentage.scale),
|
||||
}),
|
||||
"discount_percentage",
|
||||
errors
|
||||
);
|
||||
|
||||
const items = this.mapItems(dto, {
|
||||
const itemsProps = this.mapItemsProps(dto, {
|
||||
languageCode: languageCode!,
|
||||
currencyCode: currencyCode!,
|
||||
globalDiscountPercentage: globalDiscountPercentage!,
|
||||
@ -158,7 +165,7 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
|
||||
);
|
||||
}
|
||||
|
||||
const props: Omit<IProformaProps, "items"> & { items: IProformaItemProps[] } = {
|
||||
const props: IProformaProps = {
|
||||
companyId,
|
||||
status: defaultStatus,
|
||||
|
||||
@ -181,7 +188,7 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
|
||||
paymentMethod: paymentMethod!,
|
||||
globalDiscountPercentage: globalDiscountPercentage!,
|
||||
|
||||
items, // ← IProformaItemProps[]
|
||||
items: itemsProps, // ← IProformaItemProps[]
|
||||
};
|
||||
|
||||
return Result.ok({
|
||||
@ -193,7 +200,7 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
|
||||
}
|
||||
}
|
||||
|
||||
private mapItems(
|
||||
private mapItemsProps(
|
||||
dto: CreateProformaRequestDTO,
|
||||
params: {
|
||||
languageCode: LanguageCode;
|
||||
@ -224,12 +231,12 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
|
||||
);
|
||||
|
||||
const discountPercentage = extractOrPushError(
|
||||
maybeFromNullableResult(item.discount_percentage, (v) => DiscountPercentage.create(v)),
|
||||
maybeFromNullableResult(item.item_discount_percentage, (v) => DiscountPercentage.create(v)),
|
||||
`items[${index}].discount_percentage`,
|
||||
params.errors
|
||||
);
|
||||
|
||||
const taxes = this.mapTaxes(item.taxes, {
|
||||
const taxes = this.mapTaxesProps(item.taxes, {
|
||||
itemIndex: index,
|
||||
errors: params.errors,
|
||||
});
|
||||
@ -252,7 +259,7 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
|
||||
|
||||
/* Devuelve las propiedades de los impustos de una línea de detalle */
|
||||
|
||||
private mapTaxes(
|
||||
private mapTaxesProps(
|
||||
taxesDTO: Pick<CreateProformaItemRequestDTO, "taxes">["taxes"],
|
||||
params: { itemIndex: number; errors: ValidationErrorDetail[] }
|
||||
): ProformaItemTaxesProps {
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { InvoiceSerie, type ProformaPatchProps } from "@erp/customer-invoices/api/domain";
|
||||
import {
|
||||
CurrencyCode,
|
||||
DomainError,
|
||||
@ -14,6 +13,7 @@ import {
|
||||
import { Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils";
|
||||
|
||||
import type { UpdateProformaByIdRequestDTO } from "../../../../../common/dto";
|
||||
import type { ProformaPatchProps } from "../../../../domain";
|
||||
|
||||
/**
|
||||
* UpdateProformaPropsMapper
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { MapperParamsType } from "@erp/core/api";
|
||||
import type { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { ProformaListDTO } from "../dtos";
|
||||
import type { ProformaListDTO } from "../models";
|
||||
|
||||
export interface IProformaListMapper {
|
||||
mapToDTO(raw: unknown, params?: MapperParamsType): Result<ProformaListDTO, Error>;
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export * from "./proforma-resume";
|
||||
@ -3,7 +3,7 @@ import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import type { Collection, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { InvoiceStatus, Proforma } from "../../../domain";
|
||||
import type { ProformaListDTO } from "../dtos";
|
||||
import type { ProformaListDTO } from "../models";
|
||||
|
||||
export interface IProformaRepository {
|
||||
create(proforma: Proforma, transaction?: unknown): Promise<Result<void, Error>>;
|
||||
|
||||
@ -2,31 +2,31 @@ import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import type { Transaction } from "sequelize";
|
||||
|
||||
import type { ICustomerInvoiceRepository } from "../../../domain";
|
||||
import type { CustomerInvoice, CustomerInvoiceProps } from "../../../domain/aggregates";
|
||||
import type { IProformaProps, Proforma } from "../../../domain";
|
||||
import type { IProformaFactory } from "../factories";
|
||||
import type { IProformaRepository } from "../repositories";
|
||||
|
||||
import type { IProformaNumberGenerator } from "./proforma-number-generator.interface";
|
||||
|
||||
export interface IProformaCreator {
|
||||
create(
|
||||
companyId: UniqueID,
|
||||
id: UniqueID,
|
||||
props: CustomerInvoiceProps,
|
||||
transaction: Transaction
|
||||
): Promise<Result<CustomerInvoice, Error>>;
|
||||
create(params: {
|
||||
companyId: UniqueID;
|
||||
id: UniqueID;
|
||||
props: IProformaProps;
|
||||
transaction: Transaction;
|
||||
}): Promise<Result<Proforma, Error>>;
|
||||
}
|
||||
|
||||
type ProformaCreatorDeps = {
|
||||
numberService: IProformaNumberGenerator;
|
||||
factory: IProformaFactory;
|
||||
repository: ICustomerInvoiceRepository;
|
||||
repository: IProformaRepository;
|
||||
};
|
||||
|
||||
export class ProformaCreator implements IProformaCreator {
|
||||
private readonly numberService: IProformaNumberGenerator;
|
||||
private readonly factory: IProformaFactory;
|
||||
private readonly repository: ICustomerInvoiceRepository;
|
||||
private readonly repository: IProformaRepository;
|
||||
|
||||
constructor(deps: ProformaCreatorDeps) {
|
||||
this.numberService = deps.numberService;
|
||||
@ -34,12 +34,14 @@ export class ProformaCreator implements IProformaCreator {
|
||||
this.repository = deps.repository;
|
||||
}
|
||||
|
||||
async create(
|
||||
companyId: UniqueID,
|
||||
id: UniqueID,
|
||||
props: CustomerInvoiceProps,
|
||||
transaction: Transaction
|
||||
): Promise<Result<CustomerInvoice, Error>> {
|
||||
async create(params: {
|
||||
companyId: UniqueID;
|
||||
id: UniqueID;
|
||||
props: IProformaProps;
|
||||
transaction: Transaction;
|
||||
}): Promise<Result<Proforma, Error>> {
|
||||
const { companyId, id, props, transaction } = params;
|
||||
|
||||
// 1. Obtener siguiente número
|
||||
const { series } = props;
|
||||
const numberResult = await this.numberService.getNextForCompany(companyId, series, transaction);
|
||||
|
||||
@ -4,7 +4,7 @@ import type { Collection, Result } from "@repo/rdx-utils";
|
||||
import type { Transaction } from "sequelize";
|
||||
|
||||
import type { Proforma } from "../../../domain";
|
||||
import type { ProformaListDTO } from "../dtos";
|
||||
import type { ProformaListDTO } from "../models";
|
||||
import type { IProformaRepository } from "../repositories";
|
||||
|
||||
export interface IProformaFinder {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { ISnapshotBuilder } from "@erp/core/api";
|
||||
import { maybeToEmptyString } from "@repo/rdx-ddd";
|
||||
|
||||
import type { ProformaListDTO } from "../../dtos";
|
||||
import type { ProformaListDTO } from "../../models";
|
||||
|
||||
import type { IProformaListItemSnapshot } from "./proforma-list-item-snapshot.interface";
|
||||
|
||||
|
||||
@ -36,16 +36,16 @@ export class CreateProformaUseCase {
|
||||
const { dto, companyId } = params;
|
||||
|
||||
// 1) Mapear DTO → props de dominio
|
||||
const mappedResult = this.dtoMapper.map(dto, companyId);
|
||||
if (mappedResult.isFailure) {
|
||||
return Result.fail(mappedResult.error);
|
||||
const mappedPropsResult = this.dtoMapper.map(dto, { companyId });
|
||||
if (mappedPropsResult.isFailure) {
|
||||
return Result.fail(mappedPropsResult.error);
|
||||
}
|
||||
|
||||
const { props, id } = mappedResult.data;
|
||||
const { props, id } = mappedPropsResult.data;
|
||||
|
||||
return this.transactionManager.complete(async (transaction) => {
|
||||
try {
|
||||
const createResult = await this.creator.create(companyId, id, props, transaction);
|
||||
const createResult = await this.creator.create({ companyId, id, props, transaction });
|
||||
|
||||
if (createResult.isFailure) {
|
||||
return Result.fail(createResult.error);
|
||||
|
||||
@ -192,6 +192,7 @@ export class CustomerInvoice
|
||||
}
|
||||
|
||||
// Method to get the complete list of line items
|
||||
|
||||
public get items(): CustomerInvoiceItems {
|
||||
return this._items;
|
||||
}
|
||||
|
||||
@ -23,10 +23,9 @@ import {
|
||||
import {
|
||||
type IProformaItemProps,
|
||||
type IProformaItems,
|
||||
type IProformaItemsProps,
|
||||
ProformaItem,
|
||||
ProformaItems,
|
||||
} from "../entities/proforma-items";
|
||||
} from "../entities";
|
||||
import { ProformaItemMismatch } from "../errors";
|
||||
import { type IProformaTaxTotals, ProformaTaxesCalculator } from "../services";
|
||||
import { ProformaItemTaxes } from "../value-objects";
|
||||
@ -53,7 +52,7 @@ export interface IProformaProps {
|
||||
|
||||
paymentMethod: Maybe<InvoicePaymentMethod>;
|
||||
|
||||
items: IProformaItemsProps[];
|
||||
items: IProformaItemProps[];
|
||||
globalDiscountPercentage: DiscountPercentage;
|
||||
}
|
||||
|
||||
@ -96,21 +95,19 @@ export interface IProforma {
|
||||
|
||||
paymentMethod: Maybe<InvoicePaymentMethod>;
|
||||
|
||||
items: IProformaItems;
|
||||
items: IProformaItems; // <- Colección
|
||||
taxes(): Collection<IProformaTaxTotals>;
|
||||
totals(): IProformaTotals;
|
||||
}
|
||||
|
||||
export type ProformaPatchProps = Partial<Omit<IProformaProps, "companyId" | "items">> & {
|
||||
items?: ProformaItems;
|
||||
//items?: ProformaItems;
|
||||
};
|
||||
|
||||
type CreateProformaProps = IProformaProps;
|
||||
type InternalProformaProps = Omit<IProformaProps, "items">;
|
||||
|
||||
export class Proforma extends AggregateRoot<InternalProformaProps> implements IProforma {
|
||||
private readonly _items: ProformaItems;
|
||||
|
||||
// Creación funcional
|
||||
static create(props: CreateProformaProps, id?: UniqueID): Result<Proforma, Error> {
|
||||
const { items, ...internalProps } = props;
|
||||
@ -136,6 +133,8 @@ export class Proforma extends AggregateRoot<InternalProformaProps> implements IP
|
||||
return new Proforma(props, id);
|
||||
}
|
||||
|
||||
private readonly _items: ProformaItems;
|
||||
|
||||
protected constructor(props: InternalProformaProps, id?: UniqueID) {
|
||||
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
|
||||
|
||||
public get companyId(): UniqueID {
|
||||
return this.props.companyId;
|
||||
}
|
||||
@ -249,6 +219,34 @@ export class Proforma extends AggregateRoot<InternalProformaProps> implements IP
|
||||
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
|
||||
|
||||
/**
|
||||
|
||||
@ -217,6 +217,60 @@ export class ProformaItem extends DomainEntity<InternalProformaItemProps> implem
|
||||
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
|
||||
|
||||
/**
|
||||
@ -276,58 +330,4 @@ export class ProformaItem extends DomainEntity<InternalProformaItemProps> implem
|
||||
) {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,14 +6,14 @@ import { ProformaItemMismatch } from "../../errors";
|
||||
import { ProformaItemsTotalsCalculator } from "../../services/proforma-items-totals-calculator";
|
||||
|
||||
import type {
|
||||
ICreateProformaItemProps,
|
||||
IProformaItem,
|
||||
IProformaItemProps,
|
||||
IProformaItemTotals,
|
||||
ProformaItem,
|
||||
} from "./proforma-item.entity";
|
||||
|
||||
export interface IProformaItemsProps {
|
||||
items?: ICreateProformaItemProps[];
|
||||
items: IProformaItemProps[];
|
||||
|
||||
// Estos campos vienen de la cabecera,
|
||||
// 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
|
||||
}
|
||||
|
||||
type CreateProformaProps = IProformaItemsProps;
|
||||
type InternalProformaProps = Omit<IProformaItemsProps, "items">;
|
||||
|
||||
export interface IProformaItems {
|
||||
// OJO, no extendemos de Collection<IProformaItem> para no exponer
|
||||
// públicamente métodos para manipular la colección.
|
||||
@ -35,12 +38,16 @@ export interface IProformaItems {
|
||||
}
|
||||
|
||||
export class ProformaItems extends Collection<ProformaItem> implements IProformaItems {
|
||||
static create(props: CreateProformaProps): ProformaItems {
|
||||
return new ProformaItems(props);
|
||||
}
|
||||
|
||||
public readonly languageCode!: LanguageCode;
|
||||
public readonly currencyCode!: CurrencyCode;
|
||||
public readonly globalDiscountPercentage!: DiscountPercentage;
|
||||
|
||||
constructor(props: IProformaItemsProps) {
|
||||
super(props.items ?? []);
|
||||
protected constructor(props: InternalProformaProps) {
|
||||
super([]);
|
||||
this.languageCode = props.languageCode;
|
||||
this.currencyCode = props.currencyCode;
|
||||
this.globalDiscountPercentage = props.globalDiscountPercentage;
|
||||
@ -48,8 +55,15 @@ export class ProformaItems extends Collection<ProformaItem> implements IProforma
|
||||
this.ensureSameCurrencyAndLanguage(this.items);
|
||||
}
|
||||
|
||||
public static create(props: IProformaItemsProps): ProformaItems {
|
||||
return new ProformaItems(props);
|
||||
public add(item: ProformaItem): boolean {
|
||||
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[] {
|
||||
|
||||
@ -7,9 +7,10 @@ import {
|
||||
buildIssuedInvoicesDependencies,
|
||||
buildProformaServices,
|
||||
buildProformasDependencies,
|
||||
issuedInvoicesRouter,
|
||||
models,
|
||||
proformasRouter,
|
||||
} from "./infrastructure";
|
||||
import { issuedInvoicesRouter, proformasRouter } from "./infrastructure/express";
|
||||
|
||||
export const customerInvoicesAPIModule: IModuleServer = {
|
||||
name: "customer-invoices",
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
export * from "./issued-invoices";
|
||||
export * from "./proformas";
|
||||
@ -1,2 +0,0 @@
|
||||
export * from "../../issued-invoices/express/controllers";
|
||||
export * from "../../issued-invoices/express/issued-invoices.routes";
|
||||
@ -1,4 +0,0 @@
|
||||
export * from "../../proformas/express/controllers";
|
||||
|
||||
export * from "./proformas.routes";
|
||||
export * from "./proformas-api-error-mapper";
|
||||
@ -7,12 +7,12 @@ import {
|
||||
|
||||
import { GetIssuedInvoiceByIdResponseSchema } from "../../../../../common/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 {
|
||||
public constructor(private readonly useCase: GetIssuedInvoiceByIdUseCase) {
|
||||
super();
|
||||
this.errorMapper = customerInvoicesApiErrorMapper;
|
||||
this.errorMapper = proformasApiErrorMapper;
|
||||
|
||||
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||
this.registerGuards(
|
||||
|
||||
@ -8,12 +8,12 @@ import { Criteria } from "@repo/rdx-criteria/server";
|
||||
|
||||
import { ListIssuedInvoicesResponseSchema } from "../../../../../common/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 {
|
||||
public constructor(private readonly useCase: ListIssuedInvoicesUseCase) {
|
||||
super();
|
||||
this.errorMapper = customerInvoicesApiErrorMapper;
|
||||
this.errorMapper = proformasApiErrorMapper;
|
||||
|
||||
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||
this.registerGuards(
|
||||
|
||||
@ -8,12 +8,12 @@ import {
|
||||
|
||||
import type { ReportIssueInvoiceByIdQueryRequestDTO } from "../../../../../common";
|
||||
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 {
|
||||
public constructor(private readonly useCase: ReportIssuedInvoiceUseCase) {
|
||||
super();
|
||||
this.errorMapper = customerInvoicesApiErrorMapper;
|
||||
this.errorMapper = proformasApiErrorMapper;
|
||||
|
||||
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||
this.registerGuards(
|
||||
|
||||
@ -18,7 +18,7 @@ import {
|
||||
isInvalidProformaTransitionError,
|
||||
isProformaCannotBeConvertedToInvoiceError,
|
||||
isProformaCannotBeDeletedError,
|
||||
} from "../../domain";
|
||||
} from "../../../domain";
|
||||
|
||||
// Crea una regla específica (prioridad alta para sobreescribir mensajes)
|
||||
const invoiceDuplicateRule: ErrorToApiRule = {
|
||||
@ -14,7 +14,7 @@ import {
|
||||
} from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { IssuedInvoiceListDTO } from "../../../../../../application";
|
||||
import type { IssuedInvoiceSummary } from "../../../../../../application";
|
||||
import { InvoiceRecipient } from "../../../../../../domain";
|
||||
import type { CustomerInvoiceModel } from "../../../../../common";
|
||||
|
||||
@ -32,7 +32,7 @@ export class SequelizeIssuedInvoiceRecipientListMapper extends SequelizeQueryMap
|
||||
|
||||
const { errors, attributes } = params as {
|
||||
errors: ValidationErrorDetail[];
|
||||
attributes: Partial<IssuedInvoiceListDTO>;
|
||||
attributes: Partial<IssuedInvoiceSummary>;
|
||||
};
|
||||
|
||||
const { isProforma } = attributes;
|
||||
|
||||
@ -11,7 +11,10 @@ import {
|
||||
} from "@repo/rdx-ddd";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { IIssuedInvoiceListMapper, IssuedInvoiceListDTO } from "../../../../../../application";
|
||||
import type {
|
||||
IIssuedInvoiceSummaryMapper,
|
||||
IssuedInvoiceSummary,
|
||||
} from "../../../../../../application";
|
||||
import {
|
||||
InvoiceAmount,
|
||||
InvoiceNumber,
|
||||
@ -25,8 +28,8 @@ import { SequelizeIssuedInvoiceRecipientListMapper } from "./sequelize-issued-in
|
||||
import { SequelizeVerifactuRecordListMapper } from "./sequelize-verifactu-record.list.mapper";
|
||||
|
||||
export class SequelizeIssuedInvoiceListMapper
|
||||
extends SequelizeQueryMapper<CustomerInvoiceModel, IssuedInvoiceListDTO>
|
||||
implements IIssuedInvoiceListMapper
|
||||
extends SequelizeQueryMapper<CustomerInvoiceModel, IssuedInvoiceSummary>
|
||||
implements IIssuedInvoiceSummaryMapper
|
||||
{
|
||||
private _recipientMapper: SequelizeIssuedInvoiceRecipientListMapper;
|
||||
private _verifactuMapper: SequelizeVerifactuRecordListMapper;
|
||||
@ -37,14 +40,14 @@ export class SequelizeIssuedInvoiceListMapper
|
||||
this._verifactuMapper = new SequelizeVerifactuRecordListMapper();
|
||||
}
|
||||
|
||||
public mapToDTO(
|
||||
public mapToReadModel(
|
||||
raw: CustomerInvoiceModel,
|
||||
params?: MapperParamsType
|
||||
): Result<IssuedInvoiceListDTO, Error> {
|
||||
): Result<IssuedInvoiceSummary, Error> {
|
||||
const errors: ValidationErrorDetail[] = [];
|
||||
|
||||
// 1) Valores escalares (atributos generales)
|
||||
const attributes = this.mapAttributesToDTO(raw, { errors, ...params });
|
||||
const attributes = this._mapAttributesToReadModel(raw, { errors, ...params });
|
||||
|
||||
// 2) Recipient (snapshot en la factura o include)
|
||||
const recipientResult = this._recipientMapper.mapToDTO(raw, {
|
||||
@ -111,7 +114,7 @@ export class SequelizeIssuedInvoiceListMapper
|
||||
});
|
||||
}
|
||||
|
||||
private mapAttributesToDTO(raw: CustomerInvoiceModel, params?: MapperParamsType) {
|
||||
private _mapAttributesToReadModel(raw: CustomerInvoiceModel, params?: MapperParamsType) {
|
||||
const { errors } = params as {
|
||||
errors: ValidationErrorDetail[];
|
||||
};
|
||||
|
||||
@ -4,7 +4,7 @@ import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { type Collection, Result } from "@repo/rdx-utils";
|
||||
import type { FindOptions, InferAttributes, OrderItem, Sequelize, Transaction } from "sequelize";
|
||||
|
||||
import type { IIssuedInvoiceRepository, IssuedInvoiceListDTO } from "../../../../../application";
|
||||
import type { IIssuedInvoiceRepository, IssuedInvoiceSummary } from "../../../../../application";
|
||||
import type { IssuedInvoice } from "../../../../../domain";
|
||||
import {
|
||||
CustomerInvoiceItemModel,
|
||||
@ -212,7 +212,7 @@ export class IssuedInvoiceRepository
|
||||
criteria: Criteria,
|
||||
transaction: Transaction,
|
||||
options: FindOptions<InferAttributes<CustomerInvoiceModel>> = {}
|
||||
): Promise<Result<Collection<IssuedInvoiceListDTO>, Error>> {
|
||||
): Promise<Result<Collection<IssuedInvoiceSummary>, Error>> {
|
||||
const { CustomerModel } = this.database.models;
|
||||
|
||||
try {
|
||||
@ -301,7 +301,7 @@ export class IssuedInvoiceRepository
|
||||
}),
|
||||
]);
|
||||
|
||||
return this.listMapper.mapToDTOCollection(rows, count);
|
||||
return this.listMapper.mapToReadModelCollection(rows, count);
|
||||
} catch (err: unknown) {
|
||||
return Result.fail(translateSequelizeError(err));
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
export interface IProformaPersistenceMappers {
|
||||
|
||||
@ -2,23 +2,23 @@ import type { ProformasInternalDeps } from "./proformas.di";
|
||||
|
||||
export type ProformasServicesDeps = {
|
||||
services: {
|
||||
listIssuedInvoices: (filters: unknown, context: unknown) => null;
|
||||
getIssuedInvoiceById: (id: unknown, context: unknown) => null;
|
||||
generateIssuedInvoiceReport: (id: unknown, options: unknown, context: unknown) => null;
|
||||
listProformas: (filters: unknown, context: unknown) => null;
|
||||
getProformaById: (id: unknown, context: unknown) => null;
|
||||
generateProformaReport: (id: unknown, options: unknown, context: unknown) => null;
|
||||
};
|
||||
};
|
||||
|
||||
export function buildProformaServices(deps: ProformasInternalDeps): ProformasServicesDeps {
|
||||
return {
|
||||
services: {
|
||||
listIssuedInvoices: (filters, context) => null,
|
||||
//internal.useCases.listIssuedInvoices().execute(filters, context),
|
||||
listProformas: (filters, context) => null,
|
||||
//internal.useCases.listProformas().execute(filters, context),
|
||||
|
||||
getIssuedInvoiceById: (id, context) => null,
|
||||
//internal.useCases.getIssuedInvoiceById().execute(id, context),
|
||||
getProformaById: (id, context) => null,
|
||||
//internal.useCases.getProformaById().execute(id, context),
|
||||
|
||||
generateIssuedInvoiceReport: (id, options, context) => null,
|
||||
//internal.useCases.reportIssuedInvoice().execute(id, options, context),
|
||||
generateProformaReport: (id, options, context) => null,
|
||||
//internal.useCases.reportProforma().execute(id, options, context),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -7,12 +7,12 @@ import {
|
||||
|
||||
import type { CreateProformaRequestDTO } from "../../../../../common/dto/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 {
|
||||
public constructor(private readonly useCase: CreateProformaUseCase) {
|
||||
super();
|
||||
this.errorMapper = customerInvoicesApiErrorMapper;
|
||||
this.errorMapper = proformasApiErrorMapper;
|
||||
|
||||
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||
this.registerGuards(
|
||||
|
||||
@ -6,12 +6,12 @@ import {
|
||||
} from "@erp/core/api";
|
||||
|
||||
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 {
|
||||
public constructor(private readonly useCase: DeleteProformaUseCase) {
|
||||
super();
|
||||
this.errorMapper = customerInvoicesApiErrorMapper;
|
||||
this.errorMapper = proformasApiErrorMapper;
|
||||
|
||||
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||
this.registerGuards(
|
||||
|
||||
@ -5,13 +5,13 @@ import {
|
||||
requireCompanyContextGuard,
|
||||
} from "@erp/core/api";
|
||||
|
||||
import type { GetProformaUseCase } from "../../../../application/index.ts";
|
||||
import { customerInvoicesApiErrorMapper } from "../../../express/proformas/proformas-api-error-mapper.ts";
|
||||
import type { GetProformaByIdUseCase } from "../../../../application";
|
||||
import { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts";
|
||||
|
||||
export class GetProformaController extends ExpressController {
|
||||
public constructor(private readonly useCase: GetProformaUseCase) {
|
||||
public constructor(private readonly useCase: GetProformaByIdUseCase) {
|
||||
super();
|
||||
this.errorMapper = customerInvoicesApiErrorMapper;
|
||||
this.errorMapper = proformasApiErrorMapper;
|
||||
|
||||
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||
this.registerGuards(
|
||||
|
||||
@ -6,12 +6,12 @@ import {
|
||||
} from "@erp/core/api";
|
||||
|
||||
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 {
|
||||
public constructor(private readonly useCase: IssueProformaUseCase) {
|
||||
super();
|
||||
this.errorMapper = customerInvoicesApiErrorMapper;
|
||||
this.errorMapper = proformasApiErrorMapper;
|
||||
|
||||
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||
this.registerGuards(
|
||||
|
||||
@ -7,12 +7,12 @@ import {
|
||||
import { Criteria } from "@repo/rdx-criteria/server";
|
||||
|
||||
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 {
|
||||
public constructor(private readonly useCase: ListProformasUseCase) {
|
||||
super();
|
||||
this.errorMapper = customerInvoicesApiErrorMapper;
|
||||
this.errorMapper = proformasApiErrorMapper;
|
||||
|
||||
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||
this.registerGuards(
|
||||
|
||||
@ -8,12 +8,12 @@ import {
|
||||
|
||||
import type { ReportProformaByIdQueryRequestDTO } from "../../../../../common";
|
||||
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 {
|
||||
public constructor(private readonly useCase: ReportProformaUseCase) {
|
||||
super();
|
||||
this.errorMapper = customerInvoicesApiErrorMapper;
|
||||
this.errorMapper = proformasApiErrorMapper;
|
||||
|
||||
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||
this.registerGuards(
|
||||
|
||||
@ -7,12 +7,12 @@ import {
|
||||
|
||||
import type { UpdateProformaByIdRequestDTO } from "../../../../../common/dto/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 {
|
||||
public constructor(private readonly useCase: UpdateProformaUseCase) {
|
||||
super();
|
||||
this.errorMapper = customerInvoicesApiErrorMapper;
|
||||
this.errorMapper = proformasApiErrorMapper;
|
||||
|
||||
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||
this.registerGuards(
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export * from "./controllers";
|
||||
export * from "./proformas.routes";
|
||||
|
||||
@ -134,8 +134,8 @@ export class CreateProformaRequestMapper {
|
||||
|
||||
const globalDiscountPercentage = extractOrPushError(
|
||||
Percentage.create({
|
||||
value: Number(dto.discount_percentage.value),
|
||||
scale: Number(dto.discount_percentage.scale),
|
||||
value: Number(dto.global_discount_percentage.value),
|
||||
scale: Number(dto.global_discount_percentage.scale),
|
||||
}),
|
||||
"discount_percentage",
|
||||
this.errors
|
||||
@ -209,7 +209,7 @@ export class CreateProformaRequestMapper {
|
||||
);
|
||||
|
||||
const discountPercentage = extractOrPushError(
|
||||
maybeFromNullableResult(item.discount_percentage, (value) =>
|
||||
maybeFromNullableResult(item.item_discount_percentage, (value) =>
|
||||
ItemDiscountPercentage.create(value)
|
||||
),
|
||||
"discount_percentage",
|
||||
|
||||
@ -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 {
|
||||
ApiErrorMapper,
|
||||
ConflictApiError,
|
||||
@ -23,7 +20,7 @@ import {
|
||||
} from "../../../domain";
|
||||
|
||||
// Crea una regla específica (prioridad alta para sobreescribir mensajes)
|
||||
const invoiceDuplicateRule: ErrorToApiRule = {
|
||||
const proformaDuplicateRule: ErrorToApiRule = {
|
||||
priority: 120,
|
||||
matches: (e) => isCustomerInvoiceIdAlreadyExistsError(e),
|
||||
build: (e) =>
|
||||
@ -81,8 +78,8 @@ const proformaCannotBeDeletedRule: ErrorToApiRule = {
|
||||
};
|
||||
|
||||
// Cómo aplicarla: crea una nueva instancia del mapper con la regla extra
|
||||
export const customerInvoicesApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default()
|
||||
.register(invoiceDuplicateRule)
|
||||
export const proformasApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default()
|
||||
.register(proformaDuplicateRule)
|
||||
.register(proformaItemMismatchError)
|
||||
.register(entityIsNotProformaError)
|
||||
.register(proformaConversionRule)
|
||||
@ -3,17 +3,21 @@ import { type ModuleParams, type RequestWithAuth, validateRequest } from "@erp/c
|
||||
import { type NextFunction, type Request, type Response, Router } from "express";
|
||||
|
||||
import {
|
||||
GetProformaController,
|
||||
ListProformasController,
|
||||
type ProformasInternalDeps,
|
||||
ReportProformaController,
|
||||
} from "..";
|
||||
|
||||
import {
|
||||
CreateProformaRequestSchema,
|
||||
GetProformaByIdRequestSchema,
|
||||
ListProformasRequestSchema,
|
||||
ReportProformaByIdParamsRequestSchema,
|
||||
ReportProformaByIdQueryRequestSchema,
|
||||
} from "../../../../common";
|
||||
import {
|
||||
GetProformaController,
|
||||
ListProformasController,
|
||||
type ProformasInternalDeps,
|
||||
ReportProformaController,
|
||||
} from "../../proformas";
|
||||
|
||||
import { CreateProformaController } from "./controllers/create-proforma.controller";
|
||||
|
||||
export const proformasRouter = (params: ModuleParams, deps: ProformasInternalDeps) => {
|
||||
const { app, config } = params;
|
||||
@ -73,18 +77,19 @@ export const proformasRouter = (params: ModuleParams, deps: ProformasInternalDep
|
||||
}
|
||||
);
|
||||
|
||||
/*router.post(
|
||||
router.post(
|
||||
"/",
|
||||
//checkTabContext,
|
||||
|
||||
validateRequest(CreateProformaRequestSchema, "body"),
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const useCase = deps.useCases.create_proforma();
|
||||
const useCase = deps.useCases.createProforma();
|
||||
const controller = new CreateProformaController(useCase);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
);
|
||||
|
||||
/*
|
||||
router.put(
|
||||
"/:proforma_id",
|
||||
//checkTabContext,
|
||||
@ -368,10 +368,12 @@ export class ProformaRepository
|
||||
? [options.include]
|
||||
: [];
|
||||
|
||||
console.log(query.where);
|
||||
|
||||
query.where = {
|
||||
...query.where,
|
||||
...(options.where ?? {}),
|
||||
is_proforma: false,
|
||||
is_proforma: true,
|
||||
company_id: companyId.toString(),
|
||||
deleted_at: null,
|
||||
};
|
||||
@ -422,7 +424,7 @@ export class ProformaRepository
|
||||
}),
|
||||
]);
|
||||
|
||||
return this.listMapper.mapToDTOCollection(rows, count);
|
||||
return this.listMapper.mapToReadModelCollection(rows, count);
|
||||
} catch (err: unknown) {
|
||||
return Result.fail(translateSequelizeError(err));
|
||||
}
|
||||
|
||||
@ -7,7 +7,10 @@ export const CreateProformaItemRequestSchema = z.object({
|
||||
description: z.string().default(""),
|
||||
quantity: NumericStringSchema.default(""),
|
||||
unit_amount: NumericStringSchema.default(""),
|
||||
discount_percentage: NumericStringSchema.default(""),
|
||||
item_discount_percentage: PercentageSchema.default({
|
||||
value: "0",
|
||||
scale: "2",
|
||||
}),
|
||||
taxes: z.string().default(""),
|
||||
});
|
||||
export type CreateProformaItemRequestDTO = z.infer<typeof CreateProformaItemRequestSchema>;
|
||||
@ -29,7 +32,7 @@ export const CreateProformaRequestSchema = z.object({
|
||||
language_code: z.string().toLowerCase().default("es"),
|
||||
currency_code: z.string().toUpperCase().default("EUR"),
|
||||
|
||||
discount_percentage: PercentageSchema.default({
|
||||
global_discount_percentage: PercentageSchema.default({
|
||||
value: "0",
|
||||
scale: "2",
|
||||
}),
|
||||
|
||||
@ -2,29 +2,32 @@ import type { ModuleClientParams } from "@erp/core/client";
|
||||
import { lazy } from "react";
|
||||
import { Outlet, type RouteObject } from "react-router-dom";
|
||||
|
||||
import { ProformaCreatePage } from "./proformas/create";
|
||||
|
||||
const ProformaLayout = lazy(() =>
|
||||
import("./proformas/ui").then((m) => ({ default: m.ProformaLayout }))
|
||||
);
|
||||
|
||||
const IssuedInvoicesLayout = lazy(() =>
|
||||
import("./issued-invoices/ui").then((m) => ({ default: m.IssuedInvoicesLayout }))
|
||||
);
|
||||
|
||||
const ProformasListPage = lazy(() =>
|
||||
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(() =>
|
||||
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[] => {
|
||||
return [
|
||||
{
|
||||
@ -37,7 +40,7 @@ export const CustomerInvoiceRoutes = (params: ModuleClientParams): RouteObject[]
|
||||
children: [
|
||||
{ path: "", index: true, element: <ProformasListPage /> }, // index
|
||||
{ path: "list", element: <ProformasListPage /> },
|
||||
//{ path: "create", element: <CustomerInvoiceAdd /> },
|
||||
{ path: "create", element: <ProformaCreatePage /> },
|
||||
//{ path: ":id/edit", element: <InvoiceUpdatePage /> },
|
||||
],
|
||||
},
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { MoneyDTOHelper, PercentageDTOHelper, formatCurrency } from "@erp/core";
|
||||
import { MoneyDTOHelper, formatCurrency } from "@erp/core";
|
||||
|
||||
import type {
|
||||
IssuedInvoiceSummaryData,
|
||||
@ -26,14 +26,14 @@ export const IssuedInvoiceSummaryDtoAdapter = {
|
||||
summaryDto.language_code
|
||||
),
|
||||
|
||||
discount_percentage: PercentageDTOHelper.toNumber(summaryDto.discount_percentage),
|
||||
/*discount_percentage: PercentageDTOHelper.toNumber(summaryDto.discount_percentage),
|
||||
discount_percentage_fmt: PercentageDTOHelper.toNumericString(
|
||||
summaryDto.discount_percentage
|
||||
),
|
||||
),*/
|
||||
|
||||
discount_amount: MoneyDTOHelper.toNumber(summaryDto.discount_amount),
|
||||
discount_amount: MoneyDTOHelper.toNumber(summaryDto.total_discount_amount),
|
||||
discount_amount_fmt: formatCurrency(
|
||||
MoneyDTOHelper.toNumber(summaryDto.discount_amount),
|
||||
MoneyDTOHelper.toNumber(summaryDto.total_discount_amount),
|
||||
Number(summaryDto.total_amount.scale || 2),
|
||||
summaryDto.currency_code,
|
||||
summaryDto.language_code
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export * from "./ui";
|
||||
@ -0,0 +1 @@
|
||||
export * from "./pages";
|
||||
@ -0,0 +1 @@
|
||||
export * from "./proforma-create-page";
|
||||
@ -0,0 +1,47 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -63,7 +63,6 @@ export const ProformaListPage = () => {
|
||||
rightSlot={
|
||||
<Button
|
||||
aria-label={t("pages.proformas.create.title")}
|
||||
className="hidden"
|
||||
onClick={() => navigate("/proformas/create")}
|
||||
>
|
||||
<PlusIcon aria-hidden className="mr-2 size-4" />
|
||||
|
||||
@ -3,7 +3,7 @@ import { Criteria } from "@repo/rdx-criteria/server";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Collection, Result } from "@repo/rdx-utils";
|
||||
import { Transaction } from "sequelize";
|
||||
import { Customer, CustomerPatchProps, CustomerProps, ICustomerRepository } from "../domain";
|
||||
import { Customer, CustomerPatchProps, ICustomerProps, ICustomerRepository } from "../domain";
|
||||
import { CustomerListDTO } from "../infrastructure";
|
||||
|
||||
export class CustomerApplicationService {
|
||||
@ -19,7 +19,7 @@ export class CustomerApplicationService {
|
||||
*/
|
||||
buildCustomerInCompany(
|
||||
companyId: UniqueID,
|
||||
props: Omit<CustomerProps, "companyId">,
|
||||
props: Omit<ICustomerProps, "companyId">,
|
||||
customerId?: UniqueID
|
||||
): Result<Customer, Error> {
|
||||
return Customer.create({ ...props, companyId }, customerId);
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
import type { IProformaRepository } from "../repositories";
|
||||
import { type IProformaFinder, ProformaFinder } from "../services";
|
||||
|
||||
export function buildProformaFinder(repository: IProformaRepository): IProformaFinder {
|
||||
return new ProformaFinder(repository);
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
// 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,
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,83 @@
|
||||
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);
|
||||
}*/
|
||||
5
modules/customers/src/api/application/di/index.ts
Normal file
5
modules/customers/src/api/application/di/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
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";
|
||||
@ -1,3 +1,7 @@
|
||||
//export * from "./customer-application.service";
|
||||
//export * from "./presenters";
|
||||
export * from "./di";
|
||||
export * from "./mappers";
|
||||
export * from "./models";
|
||||
export * from "./repositories";
|
||||
export * from "./services";
|
||||
export * from "./snapshot-builders";
|
||||
export * from "./use-cases";
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
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> {}
|
||||
@ -0,0 +1,7 @@
|
||||
import type { IQueryMapperWithBulk } from "@erp/core/api";
|
||||
|
||||
import type { CustomerModel } from "../../infrastructure";
|
||||
import type { CustomerSummary } from "../models";
|
||||
|
||||
export interface ICustomerSummaryMapper
|
||||
extends IQueryMapperWithBulk<CustomerModel, CustomerSummary> {}
|
||||
2
modules/customers/src/api/application/mappers/index.ts
Normal file
2
modules/customers/src/api/application/mappers/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./customer-domain-mapper.interface";
|
||||
export * from "./customer-summary-mapper.interface";
|
||||
@ -0,0 +1,41 @@
|
||||
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;
|
||||
};
|
||||
1
modules/customers/src/api/application/models/index.ts
Normal file
1
modules/customers/src/api/application/models/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./customer-summary";
|
||||
@ -1,8 +1,9 @@
|
||||
import { Criteria } from "@repo/rdx-criteria/server";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Collection, Result } from "@repo/rdx-utils";
|
||||
import { CustomerListDTO } from "../../infrastructure/mappers";
|
||||
import { Customer } from "../aggregates";
|
||||
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 { Customer } from "../../domain/aggregates";
|
||||
import type { CustomerSummary } from "../models";
|
||||
|
||||
/**
|
||||
* Interfaz del repositorio para el agregado `Customer`.
|
||||
@ -56,7 +57,7 @@ export interface ICustomerRepository {
|
||||
companyId: UniqueID,
|
||||
criteria: Criteria,
|
||||
transaction: unknown
|
||||
): Promise<Result<Collection<CustomerListDTO>, Error>>;
|
||||
): Promise<Result<Collection<CustomerSummary>, Error>>;
|
||||
|
||||
/**
|
||||
* Elimina un Customer por su ID, dentro de una empresa.
|
||||
@ -0,0 +1,56 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
1
modules/customers/src/api/application/services/index.ts
Normal file
1
modules/customers/src/api/application/services/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./customer-finder";
|
||||
@ -0,0 +1,3 @@
|
||||
//export * from "./full";
|
||||
export * from "./list";
|
||||
//export * from "./report";
|
||||
@ -0,0 +1,14 @@
|
||||
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 {};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export type ICustomerListItemSnapshot = {};
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./customer-list-item-snapshot.interface";
|
||||
export * from "./customer-list-item-snapshot-builder";
|
||||
@ -24,7 +24,7 @@ import {
|
||||
import { Collection, Result, isNullishOrEmpty } from "@repo/rdx-utils";
|
||||
|
||||
import type { CreateCustomerRequestDTO } from "../../../../common";
|
||||
import { type CustomerProps, CustomerStatus } from "../../../domain";
|
||||
import { type ICustomerProps, CustomerStatus } from "../../../domain";
|
||||
|
||||
/**
|
||||
* Convierte el DTO a las props validadas (CustomerProps).
|
||||
@ -197,7 +197,7 @@ export function mapDTOToCreateCustomerProps(dto: CreateCustomerRequestDTO) {
|
||||
errors
|
||||
);
|
||||
|
||||
const customerProps: Omit<CustomerProps, "companyId"> = {
|
||||
const customerProps: Omit<ICustomerProps, "companyId"> = {
|
||||
status: status!,
|
||||
reference: reference!,
|
||||
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
|
||||
import { Criteria } from "@repo/rdx-criteria/server";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import type { ITransactionManager } from "@erp/core/api";
|
||||
import type { Criteria } from "@repo/rdx-criteria/server";
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import { Transaction } from "sequelize";
|
||||
import { ListCustomersResponseDTO } from "../../../common/dto";
|
||||
import { CustomerApplicationService } from "../../application";
|
||||
import { ListCustomersPresenter } from "../presenters";
|
||||
import type { Transaction } from "sequelize";
|
||||
|
||||
import type { ListCustomersResponseDTO } from "../../../common/dto";
|
||||
import type { ICustomerFinder } from "../services";
|
||||
import type { ICustomerListItemSnapshotBuilder } from "../snapshot-builders/list";
|
||||
|
||||
type ListCustomersUseCaseInput = {
|
||||
companyId: UniqueID;
|
||||
@ -14,39 +15,42 @@ type ListCustomersUseCaseInput = {
|
||||
|
||||
export class ListCustomersUseCase {
|
||||
constructor(
|
||||
private readonly service: CustomerApplicationService,
|
||||
private readonly transactionManager: ITransactionManager,
|
||||
private readonly presenterRegistry: IPresenterRegistry
|
||||
private readonly finder: ICustomerFinder,
|
||||
private readonly listItemSnapshotBuilder: ICustomerListItemSnapshotBuilder,
|
||||
private readonly transactionManager: ITransactionManager
|
||||
) {}
|
||||
|
||||
public execute(
|
||||
params: ListCustomersUseCaseInput
|
||||
): Promise<Result<ListCustomersResponseDTO, Error>> {
|
||||
const { criteria, companyId } = params;
|
||||
const presenter = this.presenterRegistry.getPresenter({
|
||||
resource: "customer",
|
||||
projection: "LIST",
|
||||
}) as ListCustomersPresenter;
|
||||
|
||||
return this.transactionManager.complete(async (transaction: Transaction) => {
|
||||
try {
|
||||
const result = await this.service.findCustomerByCriteriaInCompany(
|
||||
companyId,
|
||||
criteria,
|
||||
transaction
|
||||
);
|
||||
const result = await this.finder.findCustomersByCriteria(companyId, criteria, transaction);
|
||||
|
||||
if (result.isFailure) {
|
||||
return Result.fail(result.error);
|
||||
}
|
||||
|
||||
const customers = result.data;
|
||||
const dto = presenter.toOutput({
|
||||
customers,
|
||||
criteria,
|
||||
});
|
||||
const totalCustomers = customers.total();
|
||||
|
||||
return Result.ok(dto);
|
||||
const items = customers.map((item) => this.listItemSnapshotBuilder.toOutput(item));
|
||||
|
||||
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) {
|
||||
return Result.fail(error as Error);
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ import { type Collection, type Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { CustomerStatus } from "../value-objects";
|
||||
|
||||
export interface CustomerProps {
|
||||
export interface ICustomerProps {
|
||||
companyId: UniqueID;
|
||||
status: CustomerStatus;
|
||||
reference: Maybe<Name>;
|
||||
@ -49,7 +49,7 @@ export interface CustomerProps {
|
||||
currencyCode: CurrencyCode;
|
||||
}
|
||||
|
||||
export type CustomerPatchProps = Partial<Omit<CustomerProps, "companyId" | "address">> & {
|
||||
export type CustomerPatchProps = Partial<Omit<ICustomerProps, "companyId" | "address">> & {
|
||||
address?: PostalAddressPatchProps;
|
||||
};
|
||||
|
||||
@ -90,8 +90,12 @@ export interface ICustomer {
|
||||
readonly currencyCode: CurrencyCode;
|
||||
}
|
||||
|
||||
export class Customer extends AggregateRoot<CustomerProps> implements ICustomer {
|
||||
static create(props: CustomerProps, id?: UniqueID): Result<Customer, Error> {
|
||||
type CreateCustomerProps = ICustomerProps;
|
||||
type InternalCustomerProps = ICustomerProps;
|
||||
|
||||
export class Customer extends AggregateRoot<InternalCustomerProps> implements ICustomer {
|
||||
|
||||
static create(props: CreateCustomerProps, id?: UniqueID): Result<Customer, Error> {
|
||||
const contact = new Customer(props, id);
|
||||
|
||||
// Reglas de negocio / validaciones
|
||||
@ -105,12 +109,18 @@ export class Customer extends AggregateRoot<CustomerProps> implements ICustomer
|
||||
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> {
|
||||
const { address: partialAddress, ...rest } = partialCustomer;
|
||||
const updatedProps = {
|
||||
...this.props,
|
||||
...rest,
|
||||
} as CustomerProps;
|
||||
} as ICustomerProps;
|
||||
|
||||
if (partialAddress) {
|
||||
const updatedAddressOrError = this.address.update(partialAddress);
|
||||
@ -124,6 +134,8 @@ export class Customer extends AggregateRoot<CustomerProps> implements ICustomer
|
||||
return Customer.create(updatedProps, this.id);
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
public get isIndividual(): boolean {
|
||||
return !this.props.isCompany;
|
||||
}
|
||||
@ -1 +1 @@
|
||||
export * from "./customer";
|
||||
export * from "./customer.aggregate";
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
export * from "./aggregates";
|
||||
export * from "./errors";
|
||||
export * from "./repositories";
|
||||
export * from "./value-objects";
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
export * from "./customer-address-type";
|
||||
export * from "./customer-number";
|
||||
export * from "./customer-serie";
|
||||
export * from "./customer-status";
|
||||
export * from "./customer-address-type.vo";
|
||||
export * from "./customer-number.vo";
|
||||
export * from "./customer-serie.vo";
|
||||
export * from "./customer-status.vo";
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { IModuleServer } from "@erp/core/api";
|
||||
|
||||
import { models } from "./infrastructure";
|
||||
import { customersRouter, models } from "./infrastructure";
|
||||
|
||||
export * from "./infrastructure/sequelize";
|
||||
|
||||
@ -19,25 +19,29 @@ export const customersAPIModule: IModuleServer = {
|
||||
async setup(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", {
|
||||
label: this.name,
|
||||
});
|
||||
|
||||
return {
|
||||
// Modelos Sequelize del módulo
|
||||
models,
|
||||
|
||||
// Servicios expuestos a otros módulos
|
||||
services: {
|
||||
customers: {
|
||||
/*...*/
|
||||
},
|
||||
//customers: customerServices,
|
||||
},
|
||||
|
||||
// Implementación privada del módulo
|
||||
internal: {
|
||||
customers: {
|
||||
/*...*/
|
||||
},
|
||||
//customers: customerInternalDeps,
|
||||
},
|
||||
};
|
||||
},
|
||||
@ -56,7 +60,7 @@ export const customersAPIModule: IModuleServer = {
|
||||
const customersInternalDeps = getInternal("customers", "customers");
|
||||
|
||||
// Registro de rutas HTTP
|
||||
//customersRouter(params, customersInternalDeps);
|
||||
customersRouter(params, customersInternalDeps);
|
||||
|
||||
logger.info("🚀 Customers module started", {
|
||||
label: this.name,
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
import { CustomerApplicationService } from "../application/customer-application.service";
|
||||
import { CustomerFullPresenter, ListCustomersPresenter } from "../application/presenters";
|
||||
|
||||
import { CustomerDomainMapper, CustomerListMapper } from "./mappers";
|
||||
import { CustomerDomainMapper, CustomerSummaryMapper } from "./mappers";
|
||||
import { CustomerRepository } from "./sequelize";
|
||||
|
||||
export type CustomerDeps = {
|
||||
@ -40,7 +40,7 @@ export function buildCustomerDependencies(params: ModuleParams): CustomerDeps {
|
||||
const mapperRegistry = new InMemoryMapperRegistry();
|
||||
mapperRegistry
|
||||
.registerDomainMapper({ resource: "customer" }, new CustomerDomainMapper())
|
||||
.registerQueryMapper({ resource: "customer", query: "LIST" }, new CustomerListMapper());
|
||||
.registerQueryMapper({ resource: "customer", query: "LIST" }, new CustomerSummaryMapper());
|
||||
|
||||
// Repository & Services
|
||||
const repo = new CustomerRepository({ mapperRegistry, database });
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user