This commit is contained in:
David Arranz 2026-03-03 12:05:09 +01:00
parent 821b4d3ff7
commit 941ad25401
58 changed files with 1501 additions and 223 deletions

View File

@ -47,7 +47,8 @@
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.organizeImports.biome": "explicit", "source.organizeImports.biome": "explicit",
"source.fixAll.biome": "explicit", "source.fixAll.biome": "explicit",
"source.removeUnusedImports": "always" "source.removeUnusedImports": "always",
"source.fixAll.eslint": "explicit"
}, },
// other vscode settings // other vscode settings

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

@ -39,7 +39,9 @@
"indentWidth": 2, "indentWidth": 2,
"lineWidth": 100, "lineWidth": 100,
"lineEnding": "lf", "lineEnding": "lf",
"attributePosition": "auto" "attributePosition": "auto",
"bracketSpacing": true,
"bracketSameLine": true
}, },
"linter": { "linter": {
"enabled": true, "enabled": true,

59
eslint.config.mjs Normal file
View File

@ -0,0 +1,59 @@
import tseslint from "@typescript-eslint/eslint-plugin";
import parser from "@typescript-eslint/parser";
export default [
{
files: ["**/*.ts", "**/*.tsx"],
ignores: [
"**/dist/**",
"**/.turbo/**",
"**/node_modules/**"
],
languageOptions: {
parser,
},
plugins: {
"@typescript-eslint": tseslint,
},
rules: {
"@typescript-eslint/member-ordering": [
"error",
{
default: [
"signature",
// Static
"public-static-field",
"protected-static-field",
"private-static-field",
"public-static-method",
"protected-static-method",
"private-static-method",
// Instance fields
"public-instance-field",
"protected-instance-field",
"private-instance-field",
"constructor",
// Accessors
"public-instance-get",
"protected-instance-get",
"private-instance-get",
"public-instance-set",
"protected-instance-set",
"private-instance-set",
// Methods
"public-instance-method",
"protected-instance-method",
"private-instance-method",
],
},
],
},
},
];

View File

@ -5,25 +5,22 @@ import { z } from "zod/v4";
import { TaxPercentage } from "./tax-percentage.vo"; import { TaxPercentage } from "./tax-percentage.vo";
const DEFAULT_SCALE = TaxPercentage.DEFAULT_SCALE; const TAX_GROUPS = ["IVA", "IPSI", "IGIC", "retention", "rec"] as const;
const DEFAULT_MIN_VALUE = TaxPercentage.MIN_VALUE; type TaxGroup = (typeof TAX_GROUPS)[number];
const DEFAULT_MAX_VALUE = TaxPercentage.MAX_VALUE;
const DEFAULT_MIN_SCALE = TaxPercentage.MIN_SCALE;
const DEFAULT_MAX_SCALE = TaxPercentage.MAX_SCALE;
export interface TaxProps { export interface TaxProps {
code: string; // iva_21 code: string; // iva_21
name: string; // 21% IVA name: string; // 21% IVA
value: number; // 2100 value: number; // 2100
group: TaxGroup;
} }
export class Tax extends ValueObject<TaxProps> { export class Tax extends ValueObject<TaxProps> {
static DEFAULT_SCALE = DEFAULT_SCALE; static readonly DEFAULT_SCALE = TaxPercentage.DEFAULT_SCALE;
static MIN_VALUE = DEFAULT_MIN_VALUE; static readonly MIN_VALUE = TaxPercentage.MIN_VALUE;
static MAX_VALUE = DEFAULT_MAX_VALUE; static readonly MAX_VALUE = TaxPercentage.MAX_VALUE;
static MIN_SCALE = DEFAULT_MIN_SCALE; static readonly MIN_SCALE = TaxPercentage.MIN_SCALE;
static MAX_SCALE = DEFAULT_MAX_SCALE; static readonly MAX_SCALE = TaxPercentage.MAX_SCALE;
private static CODE_REGEX = /^[a-z0-9_:-]+$/; private static CODE_REGEX = /^[a-z0-9_:-]+$/;
@ -45,20 +42,21 @@ export class Tax extends ValueObject<TaxProps> {
.min(1, "El código del impuesto es obligatorio.") .min(1, "El código del impuesto es obligatorio.")
.max(40, "El código del impuesto no puede exceder 40 caracteres.") .max(40, "El código del impuesto no puede exceder 40 caracteres.")
.regex(Tax.CODE_REGEX, "El código contiene caracteres no permitidos."), .regex(Tax.CODE_REGEX, "El código contiene caracteres no permitidos."),
group: z.enum(TAX_GROUPS, "El impuesto debe ser un IVA, retención o rec. equivalencia"),
}); });
return schema.safeParse(values); return schema.safeParse(values);
} }
static create(props: TaxProps): Result<Tax> { static create(props: TaxProps): Result<Tax> {
const { value, name, code } = props; const { value, name, code, group } = props;
const validationResult = Tax.validate({ value, name, code }); const validationResult = Tax.validate({ value, name, code, group });
if (!validationResult.success) { if (!validationResult.success) {
return Result.fail(new Error(validationResult.error.issues.map((e) => e.message).join(", "))); return Result.fail(new Error(validationResult.error.issues.map((e) => e.message).join(", ")));
} }
return Result.ok(new Tax({ value, name, code })); return Result.ok(new Tax({ value, name, code, group }));
} }
/** /**
@ -89,21 +87,16 @@ export class Tax extends ValueObject<TaxProps> {
} }
const item = maybeItem.unwrap(); const item = maybeItem.unwrap();
// Delegamos en create para reusar validación y límites // Delegamos en create para reusar validación y límites
return Tax.create({ return Tax.create({
value: Number(item.value), value: Number(item.value),
name: item.name, name: item.name,
code: item.code, // guardamos el code tal cual viene del catálogo code: item.code, // guardamos el code tal cual viene del catálogo
group: item.group as TaxGroup,
}); });
} }
protected constructor(props: TaxProps) {
super(props);
this._percentage = TaxPercentage.create({
value: this.props.value,
}).data;
}
get value(): number { get value(): number {
return this.props.value; return this.props.value;
} }
@ -117,8 +110,24 @@ export class Tax extends ValueObject<TaxProps> {
return this.props.code; return this.props.code;
} }
get group(): string {
return this.props.group;
}
get percentage(): TaxPercentage { get percentage(): TaxPercentage {
return this._percentage; return TaxPercentage.create({ value: this.value }).data;
}
isVATLike(): boolean {
return this.group === "IVA" || this.group === "IGIC" || this.group === "IPSI";
}
isRetention(): boolean {
return this.group === "retention";
}
isRec(): boolean {
return this.group === "rec";
} }
getProps(): TaxProps { getProps(): TaxProps {
@ -129,21 +138,20 @@ export class Tax extends ValueObject<TaxProps> {
return this.getProps(); return this.getProps();
} }
/** Devuelve el valor real de la tasa como número decimal (ej: 21.00) */
toNumber(): number { toNumber(): number {
return this.value / 10 ** this.scale; return this.value / 10 ** this.scale;
} }
/** Devuelve la tasa formateada como porcentaje (ej: "21.00%") */
toString(): string { toString(): string {
return `${this.toNumber().toFixed(this.scale)}%`; return `${this.toNumber().toFixed(this.scale)}%`;
} }
isZero(): boolean { isZero(): boolean {
return this.toNumber() === 0; return this.value === 0;
} }
isPositive(): boolean { isPositive(): boolean {
return this.toNumber() > 0; return this.value > 0;
} }
equalsTo(other: Tax): boolean { equalsTo(other: Tax): boolean {

View File

@ -0,0 +1,13 @@
import { type JsonTaxCatalogProvider, SpainTaxCatalogProvider } from "../../../common";
export interface ICatalogs {
taxCatalog: JsonTaxCatalogProvider;
}
export const buildCatalogs = (): ICatalogs => {
const taxCatalog = SpainTaxCatalogProvider();
return {
taxCatalog,
};
};

View File

@ -1,2 +1,3 @@
export * from "./catalogs.di";
export * from "./documents.di"; export * from "./documents.di";
export * from "./transactions.di"; export * from "./transactions.di";

View File

@ -1,6 +1,6 @@
import type { Sequelize } from "sequelize"; import type { Sequelize } from "sequelize";
import { SequelizeTransactionManager } from "../sequelize"; import { SequelizeTransactionManager } from "../persistence/sequelize";
export const buildTransactionManager = (database: Sequelize) => export const buildTransactionManager = (database: Sequelize) =>
new SequelizeTransactionManager(database); new SequelizeTransactionManager(database);

View File

@ -6,4 +6,4 @@ export * from "./errors";
export * from "./express"; export * from "./express";
export * from "./logger"; export * from "./logger";
export * from "./mappers"; export * from "./mappers";
export * from "./sequelize"; export * from "./persistence";

View File

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

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 "../../../domain"; 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 "../../../domain"; 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,7 +1,9 @@
import { Collection, Result } from "@repo/rdx-utils"; import { Collection, Result } from "@repo/rdx-utils";
import { Model } from "sequelize"; import type { Model } from "sequelize";
import { MapperParamsType } from "../../../domain";
import { ISequelizeQueryMapper } from "./sequelize-mapper.interface"; import type { MapperParamsType } from "../../../../domain";
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>

View File

@ -7,9 +7,9 @@ import {
UniqueConstraintError, UniqueConstraintError,
} from "sequelize"; } from "sequelize";
import { DuplicateEntityError, EntityNotFoundError } from "../../domain"; import { DuplicateEntityError, EntityNotFoundError } from "../../../domain";
import { InfrastructureRepositoryError } from "../errors/infrastructure-repository-error"; import { InfrastructureRepositoryError } from "../../errors/infrastructure-repository-error";
import { InfrastructureUnavailableError } from "../errors/infrastructure-unavailable-error"; import { InfrastructureUnavailableError } from "../../errors/infrastructure-unavailable-error";
/** /**
* Traduce errores específicos de Sequelize a errores de dominio/infraestructura * Traduce errores específicos de Sequelize a errores de dominio/infraestructura

View File

@ -1,9 +1,9 @@
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { type Sequelize, Transaction } from "sequelize"; import { type Sequelize, Transaction } from "sequelize";
import { TransactionManager } from "../database"; import { TransactionManager } from "../../database";
import { InfrastructureError, InfrastructureUnavailableError } from "../errors"; import { InfrastructureError, InfrastructureUnavailableError } from "../../errors";
import { logger } from "../logger"; import { logger } from "../../logger";
export class SequelizeTransactionManager extends TransactionManager { export class SequelizeTransactionManager extends TransactionManager {
protected _database: Sequelize | null = null; protected _database: Sequelize | null = null;

View File

@ -134,13 +134,12 @@
"description": "Inversión del sujeto pasivo.", "description": "Inversión del sujeto pasivo.",
"aeat_code": "09" "aeat_code": "09"
}, },
{ {
"name": "Retenc. 35%", "name": "Retenc. 35%",
"code": "retencion_35", "code": "retencion_35",
"value": "3500", "value": "3500",
"scale": "2", "scale": "2",
"group": "Retención", "group": "retention",
"description": "Retenc. profesional o fiscal tipo máximo.", "description": "Retenc. profesional o fiscal tipo máximo.",
"aeat_code": null "aeat_code": null
}, },
@ -149,7 +148,7 @@
"code": "retencion_19", "code": "retencion_19",
"value": "1900", "value": "1900",
"scale": "2", "scale": "2",
"group": "Retención", "group": "retention",
"description": "Retenc. IRPF general.", "description": "Retenc. IRPF general.",
"aeat_code": "R1" "aeat_code": "R1"
}, },
@ -158,7 +157,7 @@
"code": "retencion_15", "code": "retencion_15",
"value": "1500", "value": "1500",
"scale": "2", "scale": "2",
"group": "Retención", "group": "retention",
"description": "Retenc. para autónomos y profesionales.", "description": "Retenc. para autónomos y profesionales.",
"aeat_code": "R2" "aeat_code": "R2"
}, },
@ -167,7 +166,7 @@
"code": "retencion_7", "code": "retencion_7",
"value": "700", "value": "700",
"scale": "2", "scale": "2",
"group": "Retención", "group": "retention",
"description": "Retenc. para nuevos autónomos.", "description": "Retenc. para nuevos autónomos.",
"aeat_code": null "aeat_code": null
}, },
@ -176,17 +175,16 @@
"code": "retencion_2", "code": "retencion_2",
"value": "200", "value": "200",
"scale": "2", "scale": "2",
"group": "Retención", "group": "retention",
"description": "Retenc. sobre arrendamientos de inmuebles urbanos.", "description": "Retenc. sobre arrendamientos de inmuebles urbanos.",
"aeat_code": "R3" "aeat_code": "R3"
}, },
{ {
"name": "Rec. 5,2%", "name": "Rec. 5,2%",
"code": "rec_5_2", "code": "rec_5_2",
"value": "520", "value": "520",
"scale": "2", "scale": "2",
"group": "Recargo de equivalencia", "group": "rec",
"description": "Recargo general para IVA 21%.", "description": "Recargo general para IVA 21%.",
"aeat_code": "51" "aeat_code": "51"
}, },
@ -195,7 +193,7 @@
"code": "rec_1_75", "code": "rec_1_75",
"value": "175", "value": "175",
"scale": "2", "scale": "2",
"group": "Recargo de equivalencia", "group": "rec",
"description": "Recargo para IVA 10%.", "description": "Recargo para IVA 10%.",
"aeat_code": "52" "aeat_code": "52"
}, },
@ -204,7 +202,7 @@
"code": "rec_1_4", "code": "rec_1_4",
"value": "140", "value": "140",
"scale": "2", "scale": "2",
"group": "Recargo de equivalencia", "group": "rec",
"description": "Recargo para IVA 5%.", "description": "Recargo para IVA 5%.",
"aeat_code": null "aeat_code": null
}, },
@ -213,7 +211,7 @@
"code": "rec_1", "code": "rec_1",
"value": "100", "value": "100",
"scale": "2", "scale": "2",
"group": "Recargo de equivalencia", "group": "rec",
"description": "Recargo especial.", "description": "Recargo especial.",
"aeat_code": null "aeat_code": null
}, },
@ -222,7 +220,7 @@
"code": "rec_0_62", "code": "rec_0_62",
"value": "62", "value": "62",
"scale": "2", "scale": "2",
"group": "Recargo de equivalencia", "group": "rec",
"description": "Recargo para IVA reducido especial.", "description": "Recargo para IVA reducido especial.",
"aeat_code": null "aeat_code": null
}, },
@ -231,7 +229,7 @@
"code": "rec_0_5", "code": "rec_0_5",
"value": "50", "value": "50",
"scale": "2", "scale": "2",
"group": "Recargo de equivalencia", "group": "rec",
"description": "Recargo especial.", "description": "Recargo especial.",
"aeat_code": null "aeat_code": null
}, },
@ -240,7 +238,7 @@
"code": "rec_0_26", "code": "rec_0_26",
"value": "26", "value": "26",
"scale": "2", "scale": "2",
"group": "Recargo de equivalencia", "group": "rec",
"description": "Recargo mínimo.", "description": "Recargo mínimo.",
"aeat_code": null "aeat_code": null
}, },
@ -249,11 +247,10 @@
"code": "rec_0", "code": "rec_0",
"value": "0", "value": "0",
"scale": "2", "scale": "2",
"group": "Recargo de equivalencia", "group": "rec",
"description": "Sin recargo.", "description": "Sin recargo.",
"aeat_code": null "aeat_code": null
}, },
{ {
"name": "IGIC 7%", "name": "IGIC 7%",
"code": "igic_7", "code": "igic_7",
@ -335,7 +332,6 @@
"description": "Operación exenta de IGIC.", "description": "Operación exenta de IGIC.",
"aeat_code": "12" "aeat_code": "12"
}, },
{ {
"name": "IPSI 10%", "name": "IPSI 10%",
"code": "ipsi_10", "code": "ipsi_10",
@ -372,4 +368,4 @@
"description": "Operación exenta de IPSI.", "description": "Operación exenta de IPSI.",
"aeat_code": null "aeat_code": null
} }
] ]

View File

@ -1,17 +1,17 @@
import type { UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { Collection, Result } from "@repo/rdx-utils"; import { Collection, Result } from "@repo/rdx-utils";
import type { IssuedInvoiceProps, Proforma } from "../../../domain"; import type { IIssuedInvoiceProps, Proforma } from "../../../domain";
export interface IProformaToIssuedInvoiceMaterializer { export interface IProformaToIssuedInvoiceMaterializer {
materialize(proforma: Proforma, issuedInvoiceId: UniqueID): Result<IssuedInvoiceProps, Error>; materialize(proforma: Proforma, issuedInvoiceId: UniqueID): Result<IIssuedInvoiceProps, Error>;
} }
export class ProformaToIssuedInvoiceMaterializer implements IProformaToIssuedInvoiceMaterializer { export class ProformaToIssuedInvoiceMaterializer implements IProformaToIssuedInvoiceMaterializer {
public materialize( public materialize(
proforma: Proforma, proforma: Proforma,
issuedInvoiceId: UniqueID issuedInvoiceId: UniqueID
): Result<IssuedInvoiceProps, Error> { ): Result<IIssuedInvoiceProps, Error> {
const amounts = proforma.calculateAllAmounts(); const amounts = proforma.calculateAllAmounts();
const taxGroups = proforma.getTaxes(); const taxGroups = proforma.getTaxes();

View File

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

View File

@ -2,13 +2,16 @@ import { ProformaFactory } from "../factories";
import type { IProformaRepository } from "../repositories"; import type { IProformaRepository } from "../repositories";
import { type IProformaCreator, type IProformaNumberGenerator, ProformaCreator } from "../services"; import { type IProformaCreator, type IProformaNumberGenerator, ProformaCreator } from "../services";
export const buildProformaCreator = ( export const buildProformaCreator = (params: {
numberService: IProformaNumberGenerator, numberService: IProformaNumberGenerator;
repository: IProformaRepository repository: IProformaRepository;
): IProformaCreator => { }): IProformaCreator => {
const { numberService, repository } = params;
const factory = new ProformaFactory();
return new ProformaCreator({ return new ProformaCreator({
numberService, numberService,
factory: new ProformaFactory(), factory,
repository, repository,
}); });
}; };

View File

@ -0,0 +1,19 @@
import type { ICatalogs } from "@erp/core/api";
import { CreateProformaInputMapper, type ICreateProformaInputMapper } from "../mappers";
export interface IProformaInputMappers {
createInputMapper: ICreateProformaInputMapper;
}
export const buildProformaInputMappers = (catalogs: ICatalogs): IProformaInputMappers => {
const { taxCatalog } = catalogs;
// Mappers el DTO a las props validadas (CustomerProps) y luego construir agregado
const createInputMapper = new CreateProformaInputMapper({ taxCatalog });
//const updateProformaInputMapper = new UpdateProformaInputMapper();
return {
createInputMapper,
};
};

View File

@ -1,12 +1,18 @@
import type { ITransactionManager } from "@erp/core/api"; import type { ITransactionManager } from "@erp/core/api";
import type { IProformaFinder, ProformaDocumentGeneratorService } from "../services"; import type { ICreateProformaInputMapper } from "../mappers";
import type {
IProformaCreator,
IProformaFinder,
ProformaDocumentGeneratorService,
} from "../services";
import type { import type {
IProformaListItemSnapshotBuilder, IProformaListItemSnapshotBuilder,
IProformaReportSnapshotBuilder, IProformaReportSnapshotBuilder,
} from "../snapshot-builders"; } from "../snapshot-builders";
import type { IProformaFullSnapshotBuilder } from "../snapshot-builders/full"; import type { IProformaFullSnapshotBuilder } from "../snapshot-builders/full";
import { GetProformaByIdUseCase, ListProformasUseCase, ReportProformaUseCase } from "../use-cases"; import { GetProformaByIdUseCase, ListProformasUseCase, ReportProformaUseCase } from "../use-cases";
import { CreateProformaUseCase } from "../use-cases/create-proforma";
export function buildGetProformaByIdUseCase(deps: { export function buildGetProformaByIdUseCase(deps: {
finder: IProformaFinder; finder: IProformaFinder;
@ -40,18 +46,19 @@ export function buildReportProformaUseCase(deps: {
); );
} }
/*export function buildCreateProformaUseCase(deps: { export function buildCreateProformaUseCase(deps: {
creator: IProformaCreator; creator: IProformaCreator;
dtoMapper: ICreateProformaInputMapper;
fullSnapshotBuilder: IProformaFullSnapshotBuilder; fullSnapshotBuilder: IProformaFullSnapshotBuilder;
transactionManager: ITransactionManager; transactionManager: ITransactionManager;
}) { }) {
return new CreateProformaUseCase({ return new CreateProformaUseCase({
mapper: new CreateProformaPropsMapper(), dtoMapper: deps.dtoMapper,
creator: deps.creator, creator: deps.creator,
fullSnapshotBuilder: deps.fullSnapshotBuilder, fullSnapshotBuilder: deps.fullSnapshotBuilder,
transactionManager: deps.transactionManager, transactionManager: deps.transactionManager,
}); });
}*/ }
/*export function buildUpdateProformaUseCase(deps: { /*export function buildUpdateProformaUseCase(deps: {
finder: IProformaFinder; finder: IProformaFinder;

View File

@ -1,4 +1,3 @@
export * from "./create-proforma-props.mapper"; export * from "./inputs";
export * from "./proforma-domain-mapper.interface"; export * from "./proforma-domain-mapper.interface";
export * from "./proforma-list-mapper.interface"; export * from "./proforma-list-mapper.interface";
//export * from "./update-proforma-props.mapper";

View File

@ -0,0 +1,319 @@
import type { JsonTaxCatalogProvider } from "@erp/core";
import { DiscountPercentage, Tax } from "@erp/core/api";
import {
CurrencyCode,
DomainError,
LanguageCode,
Percentage,
TextValue,
UniqueID,
UtcDate,
ValidationErrorCollection,
type ValidationErrorDetail,
extractOrPushError,
maybeFromNullableResult,
} from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
import type { CreateProformaItemRequestDTO, CreateProformaRequestDTO } from "../../../../../common";
import {
type IProformaItemProps,
type IProformaProps,
InvoiceNumber,
InvoicePaymentMethod,
type InvoiceRecipient,
InvoiceSerie,
InvoiceStatus,
ItemAmount,
ItemDescription,
ItemQuantity,
type ProformaItemTaxesProps,
} from "../../../../domain";
/**
* CreateProformaPropsMapper
* Convierte el DTO a las props validadas (CustomerProps).
* No construye directamente el agregado.
*
* @param dto - DTO con los datos de la factura de cliente
* @returns
*
*/
export interface ICreateProformaInputMapper
extends IDTOInputToPropsMapper<
CreateProformaRequestDTO,
{ id: UniqueID; props: Omit<IProformaProps, "items"> & { items: IProformaItemProps[] } }
> {}
export class CreateProformaInputMapper implements ICreateProformaInputMapper {
private readonly taxCatalog: JsonTaxCatalogProvider;
constructor(params: { taxCatalog: JsonTaxCatalogProvider }) {
this.taxCatalog = params.taxCatalog;
}
public map(
dto: CreateProformaRequestDTO,
params: { companyId: UniqueID }
): Result<{ id: UniqueID; props: IProformaProps }> {
const errors: ValidationErrorDetail[] = [];
const { companyId } = params;
try {
const defaultStatus = InvoiceStatus.createDraft();
const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
const customerId = extractOrPushError(
UniqueID.create(dto.customer_id),
"customer_id",
errors
);
const recipient = Maybe.none<InvoiceRecipient>();
const proformaNumber = extractOrPushError(
InvoiceNumber.create(dto.invoice_number),
"invoice_number",
errors
);
const series = extractOrPushError(
maybeFromNullableResult(dto.series, (value) => InvoiceSerie.create(value)),
"series",
errors
);
const invoiceDate = extractOrPushError(
UtcDate.createFromISO(dto.invoice_date),
"invoice_date",
errors
);
const operationDate = extractOrPushError(
maybeFromNullableResult(dto.operation_date, (value) => UtcDate.createFromISO(value)),
"operation_date",
errors
);
const reference = extractOrPushError(
maybeFromNullableResult(dto.reference, (value) => Result.ok(String(value))),
"reference",
errors
);
const description = extractOrPushError(
maybeFromNullableResult(dto.reference, (value) => Result.ok(String(value))),
"description",
errors
);
const notes = extractOrPushError(
maybeFromNullableResult(dto.notes, (value) => TextValue.create(value)),
"notes",
errors
);
const languageCode = extractOrPushError(
LanguageCode.create(dto.language_code),
"language_code",
errors
);
const currencyCode = extractOrPushError(
CurrencyCode.create(dto.currency_code),
"currency_code",
errors
);
const paymentMethod = extractOrPushError(
maybeFromNullableResult(dto.payment_method, (value) =>
InvoicePaymentMethod.create({ paymentDescription: value })
),
"payment_method",
errors
);
const globalDiscountPercentage = extractOrPushError(
Percentage.create({
value: Number(dto.discount_percentage.value),
scale: Number(dto.discount_percentage.scale),
}),
"discount_percentage",
errors
);
const items = this.mapItems(dto, {
languageCode: languageCode!,
currencyCode: currencyCode!,
globalDiscountPercentage: globalDiscountPercentage!,
errors,
});
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("Customer invoice props mapping failed", errors)
);
}
const props: Omit<IProformaProps, "items"> & { items: IProformaItemProps[] } = {
companyId,
status: defaultStatus,
invoiceNumber: proformaNumber!,
series: series!,
invoiceDate: invoiceDate!,
operationDate: operationDate!,
customerId: customerId!,
recipient,
reference: reference!,
description: description!,
notes: notes!,
languageCode: languageCode!,
currencyCode: currencyCode!,
paymentMethod: paymentMethod!,
globalDiscountPercentage: globalDiscountPercentage!,
items, // ← IProformaItemProps[]
};
return Result.ok({
id: proformaId!,
props,
});
} catch (err: unknown) {
return Result.fail(new DomainError("Customer invoice props mapping failed", { cause: err }));
}
}
private mapItems(
dto: CreateProformaRequestDTO,
params: {
languageCode: LanguageCode;
currencyCode: CurrencyCode;
globalDiscountPercentage: DiscountPercentage;
errors: ValidationErrorDetail[];
}
): IProformaItemProps[] {
const itemsProps: IProformaItemProps[] = [];
dto.items.forEach((item, index) => {
const description = extractOrPushError(
maybeFromNullableResult(item.description, (v) => ItemDescription.create(v)),
`items[${index}].description`,
params.errors
);
const quantity = extractOrPushError(
maybeFromNullableResult(item.quantity, (v) => ItemQuantity.create(v)),
`items[${index}].quantity`,
params.errors
);
const unitAmount = extractOrPushError(
maybeFromNullableResult(item.unit_amount, (v) => ItemAmount.create(v)),
`items[${index}].unit_amount`,
params.errors
);
const discountPercentage = extractOrPushError(
maybeFromNullableResult(item.discount_percentage, (v) => DiscountPercentage.create(v)),
`items[${index}].discount_percentage`,
params.errors
);
const taxes = this.mapTaxes(item.taxes, {
itemIndex: index,
errors: params.errors,
});
itemsProps.push({
globalDiscountPercentage: params.globalDiscountPercentage,
languageCode: params.languageCode,
currencyCode: params.currencyCode,
description: description!,
quantity: quantity!,
unitAmount: unitAmount!,
itemDiscountPercentage: discountPercentage!,
taxes,
});
});
return itemsProps;
}
/* Devuelve las propiedades de los impustos de una línea de detalle */
private mapTaxes(
taxesDTO: Pick<CreateProformaItemRequestDTO, "taxes">["taxes"],
params: { itemIndex: number; errors: ValidationErrorDetail[] }
): ProformaItemTaxesProps {
const { itemIndex, errors } = params;
const taxesProps: ProformaItemTaxesProps = {
iva: Maybe.none(),
retention: Maybe.none(),
rec: Maybe.none(),
};
// Normaliza: "" -> []
const taxStrCodes = taxesDTO
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0);
taxStrCodes.forEach((strCode, taxIndex) => {
const taxResult = Tax.createFromCode(strCode, this.taxCatalog);
if (!taxResult.isSuccess) {
errors.push({
path: `items[${itemIndex}].taxes[${taxIndex}]`,
message: taxResult.error.message,
});
return;
}
const tax = taxResult.data;
if (tax.isVATLike()) {
if (taxesProps.iva.isSome()) {
errors.push({
path: `items[${itemIndex}].taxes`,
message: "Multiple taxes for group VAT are not allowed",
});
}
taxesProps.iva = Maybe.some(tax);
}
if (tax.isRetention()) {
if (taxesProps.retention.isSome()) {
errors.push({
path: `items[${itemIndex}].taxes`,
message: "Multiple taxes for group retention are not allowed",
});
}
taxesProps.retention = Maybe.some(tax);
}
if (tax.isRec()) {
if (taxesProps.rec.isSome()) {
errors.push({
path: `items[${itemIndex}].taxes`,
message: "Multiple taxes for group rec are not allowed",
});
}
taxesProps.rec = Maybe.some(tax);
}
});
return taxesProps;
}
}

View File

@ -0,0 +1,2 @@
export * from "./create-proforma-input.mapper";
export * from "./update-proforma-input.mapper";

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 CustomerInvoicePatchProps, CustomerInvoiceSerie } from "../../../../domain";
/** /**
* UpdateProformaPropsMapper * UpdateProformaPropsMapper
@ -29,14 +29,14 @@ import { type CustomerInvoicePatchProps, CustomerInvoiceSerie } from "../../../.
* *
*/ */
export function UpdateProformaPropsMapper(dto: UpdateProformaByIdRequestDTO) { export function UpdateProformaInputMapper(dto: UpdateProformaByIdRequestDTO) {
try { try {
const errors: ValidationErrorDetail[] = []; const errors: ValidationErrorDetail[] = [];
const props: CustomerInvoicePatchProps = {}; const props: ProformaPatchProps = {};
toPatchField(dto.series).ifSet((series) => { toPatchField(dto.series).ifSet((series) => {
props.series = extractOrPushError( props.series = extractOrPushError(
maybeFromNullableResult(series, (value) => CustomerInvoiceSerie.create(value)), maybeFromNullableResult(series, (value) => InvoiceSerie.create(value)),
"reference", "reference",
errors errors
); );

View File

@ -3,7 +3,7 @@ import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { CreateProformaRequestDTO } from "../../../../../common"; import type { CreateProformaRequestDTO } from "../../../../../common";
import type { CreateProformaPropsMapper } from "../../mappers"; import type { ICreateProformaInputMapper } from "../../mappers";
import type { IProformaCreator } from "../../services"; import type { IProformaCreator } from "../../services";
import type { IProformaFullSnapshotBuilder } from "../../snapshot-builders"; import type { IProformaFullSnapshotBuilder } from "../../snapshot-builders";
@ -13,20 +13,20 @@ type CreateProformaUseCaseInput = {
}; };
type CreateProformaUseCaseDeps = { type CreateProformaUseCaseDeps = {
mapper: CreateProformaPropsMapper; dtoMapper: ICreateProformaInputMapper;
creator: IProformaCreator; creator: IProformaCreator;
fullSnapshotBuilder: IProformaFullSnapshotBuilder; fullSnapshotBuilder: IProformaFullSnapshotBuilder;
transactionManager: ITransactionManager; transactionManager: ITransactionManager;
}; };
export class CreateProformaUseCase { export class CreateProformaUseCase {
private readonly mapper: CreateProformaPropsMapper; private readonly dtoMapper: ICreateProformaInputMapper;
private readonly creator: IProformaCreator; private readonly creator: IProformaCreator;
private readonly fullSnapshotBuilder: IProformaFullSnapshotBuilder; private readonly fullSnapshotBuilder: IProformaFullSnapshotBuilder;
private readonly transactionManager: ITransactionManager; private readonly transactionManager: ITransactionManager;
constructor(deps: CreateProformaUseCaseDeps) { constructor(deps: CreateProformaUseCaseDeps) {
this.mapper = deps.mapper; this.dtoMapper = deps.dtoMapper;
this.creator = deps.creator; this.creator = deps.creator;
this.fullSnapshotBuilder = deps.fullSnapshotBuilder; this.fullSnapshotBuilder = deps.fullSnapshotBuilder;
this.transactionManager = deps.transactionManager; this.transactionManager = deps.transactionManager;
@ -36,7 +36,7 @@ export class CreateProformaUseCase {
const { dto, companyId } = params; const { dto, companyId } = params;
// 1) Mapear DTO → props de dominio // 1) Mapear DTO → props de dominio
const mappedResult = this.mapper.map(dto, companyId); const mappedResult = this.dtoMapper.map(dto, companyId);
if (mappedResult.isFailure) { if (mappedResult.isFailure) {
return Result.fail(mappedResult.error); return Result.fail(mappedResult.error);
} }

View File

@ -1,5 +1,5 @@
//export * from "./change-status-proforma.use-case"; //export * from "./change-status-proforma.use-case";
//export * from "./create-proforma"; export * from "./create-proforma";
//export * from "./delete-proforma.use-case"; //export * from "./delete-proforma.use-case";
export * from "./get-proforma-by-id.use-case"; export * from "./get-proforma-by-id.use-case";
//export * from "./issue-proforma.use-case"; //export * from "./issue-proforma.use-case";

View File

@ -21,7 +21,7 @@ import type {
} from "../../common"; } from "../../common";
import { IssuedInvoiceItems, type IssuedInvoiceTaxes, type VerifactuRecord } from "../entities"; import { IssuedInvoiceItems, type IssuedInvoiceTaxes, type VerifactuRecord } from "../entities";
export type IssuedInvoiceProps = { export interface IIssuedInvoiceProps {
companyId: UniqueID; companyId: UniqueID;
status: InvoiceStatus; status: InvoiceStatus;
@ -65,12 +65,12 @@ export type IssuedInvoiceProps = {
totalAmount: InvoiceAmount; totalAmount: InvoiceAmount;
verifactu: Maybe<VerifactuRecord>; verifactu: Maybe<VerifactuRecord>;
}; }
export class IssuedInvoice extends AggregateRoot<IssuedInvoiceProps> { export class IssuedInvoice extends AggregateRoot<IIssuedInvoiceProps> {
private _items!: IssuedInvoiceItems; private _items!: IssuedInvoiceItems;
protected constructor(props: IssuedInvoiceProps, id?: UniqueID) { protected constructor(props: IIssuedInvoiceProps, id?: UniqueID) {
super(props, id); super(props, id);
this._items = this._items =
props.items || props.items ||
@ -81,7 +81,7 @@ export class IssuedInvoice extends AggregateRoot<IssuedInvoiceProps> {
}); });
} }
static create(props: IssuedInvoiceProps, id?: UniqueID): Result<IssuedInvoice, Error> { static create(props: IIssuedInvoiceProps, id?: UniqueID): Result<IssuedInvoice, Error> {
if (!props.recipient) { if (!props.recipient) {
return Result.fail( return Result.fail(
new DomainValidationError( new DomainValidationError(
@ -231,7 +231,7 @@ export class IssuedInvoice extends AggregateRoot<IssuedInvoiceProps> {
return this.paymentMethod.isSome(); return this.paymentMethod.isSome();
} }
public getProps(): IssuedInvoiceProps { public getProps(): IIssuedInvoiceProps {
return this.props; return this.props;
} }
} }

View File

@ -20,10 +20,18 @@ import {
type InvoiceStatus, type InvoiceStatus,
type ItemAmount, type ItemAmount,
} from "../../common/value-objects"; } from "../../common/value-objects";
import { ProformaItems } from "../entities/proforma-items"; import {
type IProformaItemProps,
type IProformaItems,
type IProformaItemsProps,
ProformaItem,
ProformaItems,
} from "../entities/proforma-items";
import { ProformaItemMismatch } from "../errors";
import { type IProformaTaxTotals, ProformaTaxesCalculator } from "../services"; import { type IProformaTaxTotals, ProformaTaxesCalculator } from "../services";
import { ProformaItemTaxes } from "../value-objects";
export type ProformaProps = { export interface IProformaProps {
companyId: UniqueID; companyId: UniqueID;
status: InvoiceStatus; status: InvoiceStatus;
@ -45,9 +53,9 @@ export type ProformaProps = {
paymentMethod: Maybe<InvoicePaymentMethod>; paymentMethod: Maybe<InvoicePaymentMethod>;
items: ProformaItems; items: IProformaItemsProps[];
globalDiscountPercentage: DiscountPercentage; globalDiscountPercentage: DiscountPercentage;
}; }
export interface IProformaTotals { export interface IProformaTotals {
subtotalAmount: InvoiceAmount; subtotalAmount: InvoiceAmount;
@ -88,31 +96,31 @@ export interface IProforma {
paymentMethod: Maybe<InvoicePaymentMethod>; paymentMethod: Maybe<InvoicePaymentMethod>;
items: ProformaItems; items: IProformaItems;
taxes(): Collection<IProformaTaxTotals>; taxes(): Collection<IProformaTaxTotals>;
totals(): IProformaTotals; totals(): IProformaTotals;
} }
export type ProformaPatchProps = Partial<Omit<ProformaProps, "companyId" | "items">> & { export type ProformaPatchProps = Partial<Omit<IProformaProps, "companyId" | "items">> & {
items?: ProformaItems; items?: ProformaItems;
}; };
export class Proforma extends AggregateRoot<ProformaProps> implements IProforma { type CreateProformaProps = IProformaProps;
private _items!: ProformaItems; type InternalProformaProps = Omit<IProformaProps, "items">;
protected constructor(props: ProformaProps, id?: UniqueID) { export class Proforma extends AggregateRoot<InternalProformaProps> implements IProforma {
super(props, id); private readonly _items: ProformaItems;
this._items =
props.items ||
ProformaItems.create({
languageCode: props.languageCode,
currencyCode: props.currencyCode,
globalDiscountPercentage: props.globalDiscountPercentage,
});
}
static create(props: ProformaProps, id?: UniqueID): Result<Proforma, Error> { // Creación funcional
const proforma = new Proforma(props, id); static create(props: CreateProformaProps, id?: UniqueID): Result<Proforma, Error> {
const { items, ...internalProps } = props;
const proforma = new Proforma(internalProps, id);
const addItemsResult = proforma.initializeItems(items);
if (addItemsResult.isFailure) {
return Result.fail(addItemsResult.error);
}
// Reglas de negocio / validaciones // Reglas de negocio / validaciones
@ -123,15 +131,30 @@ export class Proforma extends AggregateRoot<ProformaProps> implements IProforma
return Result.ok(proforma); return Result.ok(proforma);
} }
// Mutabilidad // Rehidratación desde persistencia
static rehydrate(props: InternalProformaProps, id: UniqueID): Proforma {
return new Proforma(props, id);
}
protected constructor(props: InternalProformaProps, id?: UniqueID) {
super(props, id);
this._items = ProformaItems.create({
languageCode: props.languageCode,
currencyCode: props.currencyCode,
globalDiscountPercentage: props.globalDiscountPercentage,
items: [],
});
}
// Mutabilidad
public update( public update(
partialProforma: Partial<Omit<ProformaProps, "companyId">> partialProforma: Partial<Omit<IProformaProps, "companyId">>
): Result<Proforma, Error> { ): Result<Proforma, Error> {
const updatedProps = { const updatedProps = {
...this.props, ...this.props,
...partialProforma, ...partialProforma,
} as ProformaProps; } as IProformaProps;
return Proforma.create(updatedProps, this.id); return Proforma.create(updatedProps, this.id);
} }
@ -214,8 +237,7 @@ export class Proforma extends AggregateRoot<ProformaProps> implements IProforma
return this.props.globalDiscountPercentage; return this.props.globalDiscountPercentage;
} }
// Method to get the complete list of line items public get items(): IProformaItems {
public get items(): ProformaItems {
return this._items; return this._items;
} }
@ -258,12 +280,64 @@ export class Proforma extends AggregateRoot<ProformaProps> implements IProforma
return new ProformaTaxesCalculator(this.items).calculate(); return new ProformaTaxesCalculator(this.items).calculate();
} }
public getProps(): ProformaProps { public addItem(props: IProformaItemProps): Result<void, Error> {
return this.props; const taxesResult = ProformaItemTaxes.create(props.taxes);
if (taxesResult.isFailure) return Result.fail(taxesResult.error);
const itemResult = ProformaItem.create({
...props,
taxes: taxesResult.data,
});
if (itemResult.isFailure) return Result.fail(itemResult.error);
const added = this._items.add(itemResult.data);
if (!added) {
return Result.fail(new Error("Item rejected due to currency/language mismatch"));
}
return Result.ok();
} }
/*public updateItem(itemId: UniqueID, props: IProformaItemProps): Result<void, Error> {
const item = this._items.find((i) => i.id.equals(itemId));
if (!item) {
return Result.fail(new Error("Item not found"));
}
return item.update(props);
}*/
/*public removeItem(itemId: UniqueID): Result<void, Error> {
const removed = this._items.removeWhere(i => i.id.equals(itemId));
if (!removed) {
return Result.fail(new Error("Item not found"));
}
return Result.ok();
}*/
// Helpers // Helpers
private initializeItems(itemsProps: IProformaItemProps[]): Result<void, Error> {
for (const [index, itemProps] of itemsProps.entries()) {
const itemResult = ProformaItem.create(itemProps);
if (itemResult.isFailure) {
return Result.fail(itemResult.error);
}
const added = this._items.add(itemResult.data);
if (!added) {
return Result.fail(new ProformaItemMismatch(index));
}
}
return Result.ok();
}
/** /**
* @summary Convierte un ItemAmount a InvoiceAmount (mantiene moneda y escala homogénea). * @summary Convierte un ItemAmount a InvoiceAmount (mantiene moneda y escala homogénea).
*/ */

View File

@ -3,7 +3,10 @@ import { type CurrencyCode, DomainEntity, type LanguageCode, type UniqueID } fro
import { type Maybe, Result } from "@repo/rdx-utils"; import { type Maybe, Result } from "@repo/rdx-utils";
import { ItemAmount, type ItemDescription, type ItemQuantity } from "../../../common"; import { ItemAmount, type ItemDescription, type ItemQuantity } from "../../../common";
import type { ProformaItemTaxes } from "../../value-objects/proforma-item-taxes.vo"; import {
ProformaItemTaxes,
type ProformaItemTaxesProps,
} from "../../value-objects/proforma-item-taxes.vo";
/** /**
* *
@ -22,7 +25,7 @@ import type { ProformaItemTaxes } from "../../value-objects/proforma-item-taxes.
* *
*/ */
export type ProformaItemProps = { export interface IProformaItemProps {
description: Maybe<ItemDescription>; description: Maybe<ItemDescription>;
quantity: Maybe<ItemQuantity>; // Cantidad de unidades quantity: Maybe<ItemQuantity>; // Cantidad de unidades
@ -30,14 +33,14 @@ export type ProformaItemProps = {
itemDiscountPercentage: Maybe<DiscountPercentage>; // % descuento de línea itemDiscountPercentage: Maybe<DiscountPercentage>; // % descuento de línea
taxes: ProformaItemTaxes; taxes: ProformaItemTaxesProps;
// 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.
globalDiscountPercentage: DiscountPercentage; // % descuento de la cabecera globalDiscountPercentage: DiscountPercentage; // % descuento de la cabecera
languageCode: LanguageCode; // Para formateos específicos de idioma languageCode: LanguageCode; // Para formateos específicos de idioma
currencyCode: CurrencyCode; // Para cálculos y formateos de moneda currencyCode: CurrencyCode; // Para cálculos y formateos de moneda
}; }
export interface IProformaItemTotals { export interface IProformaItemTotals {
subtotalAmount: ItemAmount; subtotalAmount: ItemAmount;
@ -84,9 +87,26 @@ export interface IProformaItem {
isValued(): boolean; // Indica si el item tiene cantidad o precio (o ambos) para ser considerado "valorizado" isValued(): boolean; // Indica si el item tiene cantidad o precio (o ambos) para ser considerado "valorizado"
} }
export class ProformaItem extends DomainEntity<ProformaItemProps> implements IProformaItem { type CreateProformaItemProps = IProformaItemProps;
public static create(props: ProformaItemProps, id?: UniqueID): Result<ProformaItem, Error> {
const item = new ProformaItem(props, id); type InternalProformaItemProps = Omit<IProformaItemProps, "taxes"> & {
taxes: ProformaItemTaxes;
};
export class ProformaItem extends DomainEntity<InternalProformaItemProps> implements IProformaItem {
public static create(props: CreateProformaItemProps, id?: UniqueID): Result<ProformaItem, Error> {
const taxesResult = ProformaItemTaxes.create(props.taxes);
if (taxesResult.isFailure) {
return Result.fail(taxesResult.error);
}
const item = new ProformaItem(
{
...props,
taxes: taxesResult.data,
},
id
);
// Reglas de negocio / validaciones // Reglas de negocio / validaciones
// ... // ...
@ -95,7 +115,11 @@ export class ProformaItem extends DomainEntity<ProformaItemProps> implements IPr
return Result.ok(item); return Result.ok(item);
} }
protected constructor(props: ProformaItemProps, id?: UniqueID) { static rehydrate(props: InternalProformaItemProps, id: UniqueID): ProformaItem {
return new ProformaItem(props, id);
}
protected constructor(props: InternalProformaItemProps, id?: UniqueID) {
super(props, id); super(props, id);
} }
@ -131,7 +155,7 @@ export class ProformaItem extends DomainEntity<ProformaItemProps> implements IPr
return this.props.taxes; return this.props.taxes;
} }
getProps(): ProformaItemProps { getProps(): IProformaItemProps {
return this.props; return this.props;
} }

View File

@ -1,28 +1,37 @@
import type { DiscountPercentage } from "@erp/core/api"; import type { DiscountPercentage } from "@erp/core/api";
import type { CurrencyCode, LanguageCode } from "@repo/rdx-ddd"; import type { CurrencyCode, LanguageCode } from "@repo/rdx-ddd";
import { Collection } from "@repo/rdx-utils"; import { Collection, Result } from "@repo/rdx-utils";
import { ProformaItemMismatch } from "../../errors";
import { ProformaItemsTotalsCalculator } from "../../services/proforma-items-totals-calculator"; import { ProformaItemsTotalsCalculator } from "../../services/proforma-items-totals-calculator";
import type { IProformaItem, IProformaItemTotals, ProformaItem } from "./proforma-item.entity"; import type {
ICreateProformaItemProps,
IProformaItem,
IProformaItemTotals,
ProformaItem,
} from "./proforma-item.entity";
export type ProformaItemsProps = { export interface IProformaItemsProps {
items?: ProformaItem[]; 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.
globalDiscountPercentage: DiscountPercentage; // % descuento de la cabecera globalDiscountPercentage: DiscountPercentage; // % descuento de la cabecera
languageCode: LanguageCode; // Para formateos específicos de idioma languageCode: LanguageCode; // Para formateos específicos de idioma
currencyCode: CurrencyCode; // Para cálculos y formateos de moneda currencyCode: CurrencyCode; // Para cálculos y formateos de moneda
}; }
export interface IProformaItems {
// OJO, no extendemos de Collection<IProformaItem> para no exponer
// públicamente métodos para manipular la colección.
export interface IProformaItems extends Collection<IProformaItem> {
valued(): IProformaItem[]; // Devuelve solo las líneas valoradas. valued(): IProformaItem[]; // Devuelve solo las líneas valoradas.
totals(): IProformaItemTotals; totals(): IProformaItemTotals;
globalDiscountPercentage: DiscountPercentage; // % descuento de la cabecera readonly globalDiscountPercentage: DiscountPercentage; // % descuento de la cabecera
languageCode: LanguageCode; // Para formateos específicos de idioma readonly languageCode: LanguageCode; // Para formateos específicos de idioma
currencyCode: CurrencyCode; // Para cálculos y formateos de moneda readonly currencyCode: CurrencyCode; // Para cálculos y formateos de moneda
} }
export class ProformaItems extends Collection<ProformaItem> implements IProformaItems { export class ProformaItems extends Collection<ProformaItem> implements IProformaItems {
@ -30,7 +39,7 @@ export class ProformaItems extends Collection<ProformaItem> implements IProforma
public readonly currencyCode!: CurrencyCode; public readonly currencyCode!: CurrencyCode;
public readonly globalDiscountPercentage!: DiscountPercentage; public readonly globalDiscountPercentage!: DiscountPercentage;
constructor(props: ProformaItemsProps) { constructor(props: IProformaItemsProps) {
super(props.items ?? []); super(props.items ?? []);
this.languageCode = props.languageCode; this.languageCode = props.languageCode;
this.currencyCode = props.currencyCode; this.currencyCode = props.currencyCode;
@ -39,7 +48,7 @@ export class ProformaItems extends Collection<ProformaItem> implements IProforma
this.ensureSameCurrencyAndLanguage(this.items); this.ensureSameCurrencyAndLanguage(this.items);
} }
public static create(props: ProformaItemsProps): ProformaItems { public static create(props: IProformaItemsProps): ProformaItems {
return new ProformaItems(props); return new ProformaItems(props);
} }
@ -53,10 +62,10 @@ export class ProformaItems extends Collection<ProformaItem> implements IProforma
* @returns `true` si el ítem fue añadido correctamente; `false` si fue rechazado. * @returns `true` si el ítem fue añadido correctamente; `false` si fue rechazado.
* @remarks * @remarks
* Sólo se aceptan ítems cuyo `LanguageCode` y `CurrencyCode` coincidan con * Sólo se aceptan ítems cuyo `LanguageCode` y `CurrencyCode` coincidan con
* los de la colección. Si no coinciden, el método devuelve `false` sin modificar * los de la colección. Si no coinciden, el método devuelve un resultado fallido sin modificar
* la colección. * la colección.
*/ */
public add(item: ProformaItem): boolean { public addItem(item: ProformaItem): Result<void, Error> {
// Antes de añadir un nuevo item, debo comprobar que el item a añadir // Antes de añadir un nuevo item, debo comprobar que el item a añadir
// tiene el mismo "currencyCode" y "languageCode" que la colección de items. // tiene el mismo "currencyCode" y "languageCode" que la colección de items.
const same = const same =
@ -64,9 +73,11 @@ export class ProformaItems extends Collection<ProformaItem> implements IProforma
this.currencyCode.equals(item.currencyCode) && this.currencyCode.equals(item.currencyCode) &&
this.globalDiscountPercentage.equals(item.globalDiscountPercentage); this.globalDiscountPercentage.equals(item.globalDiscountPercentage);
if (!same) return false; if (!same) {
return Result.fail(new ProformaItemMismatch(this.size()));
return super.add(item); }
super.add(item);
return Result.ok();
} }
// Cálculos // Cálculos

View File

@ -3,3 +3,4 @@ export * from "./entity-is-not-proforma-error";
export * from "./invalid-proforma-transition-error"; export * from "./invalid-proforma-transition-error";
export * from "./proforma-cannot-be-converted-to-invoice-error"; export * from "./proforma-cannot-be-converted-to-invoice-error";
export * from "./proforma-cannot-be-deleted-error"; export * from "./proforma-cannot-be-deleted-error";
export * from "./proforma-item-not-valid-error";

View File

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

View File

@ -2,14 +2,14 @@ import type { IModuleServer } from "@erp/core/api";
import { import {
type IssuedInvoicesInternalDeps, type IssuedInvoicesInternalDeps,
type ProformasInternalDeps,
buildIssuedInvoiceServices, buildIssuedInvoiceServices,
buildIssuedInvoicesDependencies, buildIssuedInvoicesDependencies,
buildProformaServices, buildProformaServices,
buildProformasDependencies, buildProformasDependencies,
models, models,
} from "./infrastructure"; } from "./infrastructure";
import { issuedInvoicesRouter } from "./infrastructure/express"; import { issuedInvoicesRouter, proformasRouter } from "./infrastructure/express";
import { proformasRouter } from './infrastructure/express';
export const customerInvoicesAPIModule: IModuleServer = { export const customerInvoicesAPIModule: IModuleServer = {
name: "customer-invoices", name: "customer-invoices",
@ -43,13 +43,13 @@ export const customerInvoicesAPIModule: IModuleServer = {
// Servicios expuestos a otros módulos // Servicios expuestos a otros módulos
services: { services: {
issuedInvoices: issuedInvoicesServices, issuedInvoices: issuedInvoicesServices,
proformas: proformasServices proformas: proformasServices,
}, },
// Implementación privada del módulo // Implementación privada del módulo
internal: { internal: {
issuedInvoices: issuedInvoicesInternalDeps, issuedInvoices: issuedInvoicesInternalDeps,
proformas: proformasInternalDeps proformas: proformasInternalDeps,
}, },
}; };
}, },
@ -69,8 +69,11 @@ export const customerInvoicesAPIModule: IModuleServer = {
"customer-invoices", "customer-invoices",
"issuedInvoices" "issuedInvoices"
); );
const proformasInternalDeps = getInternal("customer-invoices", "proformas"); const proformasInternalDeps = getInternal<ProformasInternalDeps>(
"customer-invoices",
"proformas"
);
// Registro de rutas HTTP // Registro de rutas HTTP
issuedInvoicesRouter(params, issuedInvoicesInternalDeps); issuedInvoicesRouter(params, issuedInvoicesInternalDeps);

View File

@ -13,11 +13,13 @@ import {
type EntityIsNotProformaError, type EntityIsNotProformaError,
type InvalidProformaTransitionError, type InvalidProformaTransitionError,
type ProformaCannotBeConvertedToInvoiceError, type ProformaCannotBeConvertedToInvoiceError,
type ProformaItemMismatch,
isCustomerInvoiceIdAlreadyExistsError, isCustomerInvoiceIdAlreadyExistsError,
isEntityIsNotProformaError, isEntityIsNotProformaError,
isInvalidProformaTransitionError, isInvalidProformaTransitionError,
isProformaCannotBeConvertedToInvoiceError, isProformaCannotBeConvertedToInvoiceError,
isProformaCannotBeDeletedError, isProformaCannotBeDeletedError,
isProformaItemMismatch,
} from "../../../domain"; } from "../../../domain";
// Crea una regla específica (prioridad alta para sobreescribir mensajes) // Crea una regla específica (prioridad alta para sobreescribir mensajes)
@ -40,6 +42,16 @@ const entityIsNotProformaError: ErrorToApiRule = {
), ),
}; };
const proformaItemMismatchError: ErrorToApiRule = {
priority: 120,
matches: (e) => isProformaItemMismatch(e),
build: (e) =>
new ValidationApiError(
(e as ProformaItemMismatch).message ||
"Proforma item rejected due to currency/language mismatch"
),
};
const proformaTransitionRule: ErrorToApiRule = { const proformaTransitionRule: ErrorToApiRule = {
priority: 120, priority: 120,
matches: (e) => isInvalidProformaTransitionError(e), matches: (e) => isInvalidProformaTransitionError(e),
@ -71,6 +83,7 @@ const proformaCannotBeDeletedRule: ErrorToApiRule = {
// Cómo aplicarla: crea una nueva instancia del mapper con la regla extra // Cómo aplicarla: crea una nueva instancia del mapper con la regla extra
export const customerInvoicesApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default() export const customerInvoicesApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default()
.register(invoiceDuplicateRule) .register(invoiceDuplicateRule)
.register(proformaItemMismatchError)
.register(entityIsNotProformaError) .register(entityIsNotProformaError)
.register(proformaConversionRule) .register(proformaConversionRule)
.register(proformaCannotBeDeletedRule) .register(proformaCannotBeDeletedRule)

View File

@ -16,6 +16,7 @@ import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils";
import type { IIssuedInvoiceDomainMapper } from "../../../../../../application"; import type { IIssuedInvoiceDomainMapper } from "../../../../../../application";
import { import {
DiscountPercentage, DiscountPercentage,
type IIssuedInvoiceProps,
InvoiceAmount, InvoiceAmount,
InvoiceNumber, InvoiceNumber,
InvoicePaymentMethod, InvoicePaymentMethod,
@ -23,7 +24,6 @@ import {
InvoiceStatus, InvoiceStatus,
IssuedInvoice, IssuedInvoice,
IssuedInvoiceItems, IssuedInvoiceItems,
type IssuedInvoiceProps,
IssuedInvoiceTaxes, IssuedInvoiceTaxes,
} from "../../../../../../domain"; } from "../../../../../../domain";
import type { import type {
@ -351,7 +351,7 @@ export class SequelizeIssuedInvoiceDomainMapper
currencyCode: attributes.currencyCode!, currencyCode: attributes.currencyCode!,
}); });
const invoiceProps: IssuedInvoiceProps = { const invoiceProps: IIssuedInvoiceProps = {
companyId: attributes.companyId!, companyId: attributes.companyId!,
proformaId: attributes.proformaId!, proformaId: attributes.proformaId!,

View File

@ -14,10 +14,10 @@ import { Result } from "@repo/rdx-utils";
import { import {
DiscountPercentage, DiscountPercentage,
type IIssuedInvoiceProps,
type IssuedInvoice, type IssuedInvoice,
IssuedInvoiceItem, IssuedInvoiceItem,
type IssuedInvoiceItemProps, type IssuedInvoiceItemProps,
type IssuedInvoiceProps,
ItemAmount, ItemAmount,
ItemDescription, ItemDescription,
ItemDiscountPercentage, ItemDiscountPercentage,
@ -56,7 +56,7 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
const { errors, index, attributes } = params as { const { errors, index, attributes } = params as {
index: number; index: number;
errors: ValidationErrorDetail[]; errors: ValidationErrorDetail[];
attributes: Partial<IssuedInvoiceProps>; attributes: Partial<IIssuedInvoiceProps>;
}; };
const itemId = extractOrPushError( const itemId = extractOrPushError(
@ -263,7 +263,7 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
const { errors, index } = params as { const { errors, index } = params as {
index: number; index: number;
errors: ValidationErrorDetail[]; errors: ValidationErrorDetail[];
attributes: Partial<IssuedInvoiceProps>; attributes: Partial<IIssuedInvoiceProps>;
}; };
// 1) Valores escalares (atributos generales) // 1) Valores escalares (atributos generales)

View File

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

View File

@ -14,9 +14,9 @@ import {
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { import {
type IIssuedInvoiceProps,
InvoiceAmount, InvoiceAmount,
type IssuedInvoice, type IssuedInvoice,
type IssuedInvoiceProps,
IssuedInvoiceTax, IssuedInvoiceTax,
ItemAmount, ItemAmount,
ItemDiscountPercentage, ItemDiscountPercentage,
@ -66,7 +66,7 @@ export class SequelizeIssuedInvoiceTaxesDomainMapper extends SequelizeDomainMapp
const { errors, index, attributes } = params as { const { errors, index, attributes } = params as {
index: number; index: number;
errors: ValidationErrorDetail[]; errors: ValidationErrorDetail[];
attributes: Partial<IssuedInvoiceProps>; attributes: Partial<IIssuedInvoiceProps>;
}; };
const taxableAmount = extractOrPushError( const taxableAmount = extractOrPushError(

View File

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

View File

@ -5,7 +5,7 @@ import {
type ProformaDocumentPipelineFactoryDeps, type ProformaDocumentPipelineFactoryDeps,
} from "../documents"; } from "../documents";
export const buildproformaDocumentService = (params: ModuleParams) => { export const buildProformaDocumentService = (params: ModuleParams) => {
const { documentRenderers, documentSigning, documentStorage } = buildCoreDocumentsDI(params); const { documentRenderers, documentSigning, documentStorage } = buildCoreDocumentsDI(params);
const pipelineDeps: ProformaDocumentPipelineFactoryDeps = { const pipelineDeps: ProformaDocumentPipelineFactoryDeps = {

View File

@ -0,0 +1,31 @@
import type { ICatalogs, IProformaDomainMapper, IProformaListMapper } from "../../../application";
import { SequelizeProformaDomainMapper, SequelizeProformaListMapper } from "../persistence";
export interface IProformaPersistenceMappers {
domainMapper: IProformaDomainMapper;
listMapper: IProformaListMapper;
createMapper: CreateProformaInputMapper;
}
export const buildProformaPersistenceMappers = (
catalogs: ICatalogs
): IProformaPersistenceMappers => {
const { taxCatalog } = catalogs;
// Mappers para el repositorio
const domainMapper = new SequelizeProformaDomainMapper({
taxCatalog,
});
const listMapper = new SequelizeProformaListMapper();
// Mappers el DTO a las props validadas (CustomerProps) y luego construir agregado
const createMapper = new CreateProformaInputMapper({ taxCatalog });
return {
domainMapper,
listMapper,
createMapper,
};
};

View File

@ -1,19 +1,14 @@
import { SpainTaxCatalogProvider } from "@erp/core";
import type { Sequelize } from "sequelize"; import type { Sequelize } from "sequelize";
import { import { ProformaRepository } from "../persistence";
ProformaRepository,
SequelizeProformaDomainMapper,
SequelizeProformaListMapper,
} from "../persistence";
export const buildProformaRepository = (database: Sequelize) => { import type { IProformaPersistenceMappers } from "./proforma-persistence-mappers.di";
const taxCatalog = SpainTaxCatalogProvider();
const domainMapper = new SequelizeProformaDomainMapper({ export const buildProformaRepository = (params: {
taxCatalog, database: Sequelize;
}); mappers: IProformaPersistenceMappers;
const listMapper = new SequelizeProformaListMapper(); }) => {
const { database, mappers } = params;
return new ProformaRepository(domainMapper, listMapper, database); return new ProformaRepository(mappers.domainMapper, mappers.listMapper, database);
}; };

View File

@ -1,17 +1,23 @@
import { type ModuleParams, buildTransactionManager } from "@erp/core/api"; import { type ModuleParams, buildCatalogs, buildTransactionManager } from "@erp/core/api";
import { import {
type CreateProformaUseCase,
type GetProformaByIdUseCase, type GetProformaByIdUseCase,
type ListProformasUseCase, type ListProformasUseCase,
type ReportProformaUseCase, type ReportProformaUseCase,
buildCreateProformaUseCase,
buildGetProformaByIdUseCase, buildGetProformaByIdUseCase,
buildListProformasUseCase, buildListProformasUseCase,
buildProformaCreator,
buildProformaFinder, buildProformaFinder,
buildProformaInputMappers,
buildProformaSnapshotBuilders, buildProformaSnapshotBuilders,
buildReportProformaUseCase, buildReportProformaUseCase,
} from "../../../application"; } from "../../../application";
import { buildproformaDocumentService } from "./proforma-documents.di"; import { buildProformaDocumentService } from "./proforma-documents.di";
import { buildProformaNumberGenerator } from "./proforma-number-generator.di";
import { buildProformaPersistenceMappers } from "./proforma-persistence-mappers.di";
import { buildProformaRepository } from "./proforma-repositories.di"; import { buildProformaRepository } from "./proforma-repositories.di";
export type ProformasInternalDeps = { export type ProformasInternalDeps = {
@ -19,8 +25,9 @@ export type ProformasInternalDeps = {
listProformas: () => ListProformasUseCase; listProformas: () => ListProformasUseCase;
getProformaById: () => GetProformaByIdUseCase; getProformaById: () => GetProformaByIdUseCase;
reportProforma: () => ReportProformaUseCase; reportProforma: () => ReportProformaUseCase;
createProforma: () => CreateProformaUseCase;
/*createProforma: () => CreateProformaUseCase; /*
updateProforma: () => UpdateProformaUseCase; updateProforma: () => UpdateProformaUseCase;
deleteProforma: () => DeleteProformaUseCase; deleteProforma: () => DeleteProformaUseCase;
issueProforma: () => IssueProformaUseCase; issueProforma: () => IssueProformaUseCase;
@ -33,14 +40,19 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
// Infrastructure // Infrastructure
const transactionManager = buildTransactionManager(database); const transactionManager = buildTransactionManager(database);
const repository = buildProformaRepository(database); const catalogs = buildCatalogs();
//const numberService = buildProformaNumberGenerator(); const persistenceMappers = buildProformaPersistenceMappers(catalogs);
const repository = buildProformaRepository({ database, mappers: persistenceMappers });
const numberService = buildProformaNumberGenerator();
// Application helpers // Application helpers
const inputMappers = buildProformaInputMappers(catalogs);
const finder = buildProformaFinder(repository); const finder = buildProformaFinder(repository);
//const creator = buildProformaCreator(numberService, repository); const creator = buildProformaCreator({ numberService, repository });
const snapshotBuilders = buildProformaSnapshotBuilders(); const snapshotBuilders = buildProformaSnapshotBuilders();
const documentGeneratorPipeline = buildproformaDocumentService(params); const documentGeneratorPipeline = buildProformaDocumentService(params);
// Internal use cases (factories) // Internal use cases (factories)
return { return {
@ -68,12 +80,13 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
transactionManager, transactionManager,
}), }),
/*createProforma: () => createProforma: () =>
buildCreateProformaUseCase({ buildCreateProformaUseCase({
creator, creator,
dtoMapper: inputMappers.createInputMapper,
fullSnapshotBuilder: snapshotBuilders.full, fullSnapshotBuilder: snapshotBuilders.full,
transactionManager, transactionManager,
}),*/ }),
}, },
}; };
} }

View File

@ -1,5 +1,4 @@
import type { JsonTaxCatalogProvider } from "@erp/core"; import type { JsonTaxCatalogProvider } from "@erp/core";
import { Tax } from "@erp/core/api";
import { import {
CurrencyCode, CurrencyCode,
DomainError, DomainError,
@ -15,8 +14,10 @@ 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 { CreateProformaItemRequestDTO, CreateProformaRequestDTO } from "../../../../common"; import type { CreateProformaItemRequestDTO, CreateProformaRequestDTO } from "../../../../../common";
import { import {
type IProformaItemProps,
type IProformaProps,
InvoiceNumber, InvoiceNumber,
InvoicePaymentMethod, InvoicePaymentMethod,
type InvoiceRecipient, type InvoiceRecipient,
@ -26,9 +27,8 @@ import {
type IssuedInvoiceItemProps, type IssuedInvoiceItemProps,
ItemAmount, ItemAmount,
ItemDescription, ItemDescription,
ItemDiscountPercentage,
ItemQuantity, ItemQuantity,
} from "../../../domain"; } from "../../../../domain";
/** /**
* CreateProformaPropsMapper * CreateProformaPropsMapper
@ -41,7 +41,8 @@ import {
* *
*/ */
export class CreateProformaPropsMapper {
export class CreateProformaRequestMapper {
private readonly taxCatalog: JsonTaxCatalogProvider; private readonly taxCatalog: JsonTaxCatalogProvider;
private errors: ValidationErrorDetail[] = []; private errors: ValidationErrorDetail[] = [];
private languageCode?: LanguageCode; private languageCode?: LanguageCode;
@ -52,7 +53,8 @@ export class CreateProformaPropsMapper {
this.errors = []; this.errors = [];
} }
public map(dto: CreateProformaRequestDTO, companyId: UniqueID) { public map(dto: CreateProformaRequestDTO, params: { companyId: UniqueID }) {
const { companyId } = params;
try { try {
this.errors = []; this.errors = [];
@ -60,8 +62,6 @@ export class CreateProformaPropsMapper {
const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", this.errors); const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", this.errors);
const isProforma = true;
const customerId = extractOrPushError( const customerId = extractOrPushError(
UniqueID.create(dto.customer_id), UniqueID.create(dto.customer_id),
"customer_id", "customer_id",
@ -132,7 +132,7 @@ export class CreateProformaPropsMapper {
this.errors this.errors
); );
const discountPercentage = extractOrPushError( const globalDiscountPercentage = extractOrPushError(
Percentage.create({ Percentage.create({
value: Number(dto.discount_percentage.value), value: Number(dto.discount_percentage.value),
scale: Number(dto.discount_percentage.scale), scale: Number(dto.discount_percentage.scale),
@ -149,10 +149,8 @@ export class CreateProformaPropsMapper {
); );
} }
const proformaProps: IProformaProps = { const proformaProps: Omit<IProformaProps, "items"> & { items: IProformaItemProps[] } = {
companyId, companyId,
isProforma,
proformaId: Maybe.none(),
status: defaultStatus!, status: defaultStatus!,
invoiceNumber: proformaNumber!, invoiceNumber: proformaNumber!,
@ -169,13 +167,13 @@ export class CreateProformaPropsMapper {
notes: notes!, notes: notes!,
languageCode: this.languageCode!, languageCode: this.languageCode!,
currencyCode: this.currencyCode!, currencyCode: this.currencyCode!,
items: items,
paymentMethod: paymentMethod!, paymentMethod: paymentMethod!,
discountPercentage: discountPercentage!, globalDiscountPercentage: globalDiscountPercentage!,
items:
}; };
return Result.ok({ id: proformaId!, props: proformaProps }); return Result.ok({ id: proformaId!, props: proformaProps });
@ -184,8 +182,8 @@ export class CreateProformaPropsMapper {
} }
} }
private mapItems(items: CreateProformaItemRequestDTO[]) { private mapItems(items: CreateProformaItemRequestDTO[]): IProformaItemProps[] {
const invoiceItems = CustomerInvoiceItems.create({ const proformaItems = CustomerInvoiceItems.create({
currencyCode: this.currencyCode!, currencyCode: this.currencyCode!,
languageCode: this.languageCode!, languageCode: this.languageCode!,
items: [], items: [],
@ -232,7 +230,7 @@ export class CreateProformaPropsMapper {
const itemResult = IssuedInvoiceItem.create(itemProps); const itemResult = IssuedInvoiceItem.create(itemProps);
if (itemResult.isSuccess) { if (itemResult.isSuccess) {
invoiceItems.add(itemResult.data); proformaItems.add(itemResult.data);
} else { } else {
this.errors.push({ this.errors.push({
path: `items[${index}]`, path: `items[${index}]`,
@ -240,7 +238,7 @@ export class CreateProformaPropsMapper {
}); });
} }
}); });
return invoiceItems; return proformaItems;
} }
private mapTaxes(item: CreateProformaItemRequestDTO, itemIndex: number) { private mapTaxes(item: CreateProformaItemRequestDTO, itemIndex: number) {

View File

@ -15,13 +15,13 @@ import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils";
import type { IProformaDomainMapper } from "../../../../../../application"; import type { IProformaDomainMapper } from "../../../../../../application";
import { import {
type IProformaProps,
InvoiceNumber, InvoiceNumber,
InvoicePaymentMethod, InvoicePaymentMethod,
InvoiceSerie, InvoiceSerie,
InvoiceStatus, InvoiceStatus,
Proforma, Proforma,
ProformaItems, ProformaItems,
type ProformaProps,
} from "../../../../../../domain"; } from "../../../../../../domain";
import type { import type {
CustomerInvoiceCreationAttributes, CustomerInvoiceCreationAttributes,
@ -217,7 +217,7 @@ export class SequelizeProformaDomainMapper
items: itemsResults.data.getAll(), items: itemsResults.data.getAll(),
}); });
const invoiceProps: ProformaProps = { const invoiceProps: IProformaProps = {
companyId: attributes.companyId!, companyId: attributes.companyId!,
status: attributes.status!, status: attributes.status!,

View File

@ -16,15 +16,15 @@ import {
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { import {
type IProformaItemProps,
type IProformaProps,
ItemAmount, ItemAmount,
ItemDescription, ItemDescription,
ItemQuantity, ItemQuantity,
type Proforma, type Proforma,
ProformaItem, ProformaItem,
type ProformaItemProps,
ProformaItemTaxes, ProformaItemTaxes,
type ProformaItemTaxesProps, type ProformaItemTaxesProps,
type ProformaProps,
} from "../../../../../../domain"; } from "../../../../../../domain";
import type { import type {
CustomerInvoiceItemCreationAttributes, CustomerInvoiceItemCreationAttributes,
@ -54,11 +54,11 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
private mapAttributesToDomain( private mapAttributesToDomain(
raw: CustomerInvoiceItemModel, raw: CustomerInvoiceItemModel,
params?: MapperParamsType params?: MapperParamsType
): Partial<ProformaItemProps & ProformaItemTaxesProps> & { itemId?: UniqueID } { ): Partial<IProformaItemProps & ProformaItemTaxesProps> & { itemId?: UniqueID } {
const { errors, index, parent } = params as { const { errors, index, parent } = params as {
index: number; index: number;
errors: ValidationErrorDetail[]; errors: ValidationErrorDetail[];
parent: Partial<ProformaProps>; parent: Partial<IProformaProps>;
}; };
const itemId = extractOrPushError( const itemId = extractOrPushError(
@ -139,7 +139,7 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
const { errors, index } = params as { const { errors, index } = params as {
index: number; index: number;
errors: ValidationErrorDetail[]; errors: ValidationErrorDetail[];
parent: Partial<ProformaProps>; parent: Partial<IProformaProps>;
}; };
// 1) Valores escalares (atributos generales) // 1) Valores escalares (atributos generales)

View File

@ -14,7 +14,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 { InvoiceRecipient, type ProformaProps } from "../../../../../../domain"; import { type IProformaProps, InvoiceRecipient } from "../../../../../../domain";
import type { CustomerInvoiceModel } from "../../../../../common"; import type { CustomerInvoiceModel } from "../../../../../common";
export class SequelizeProformaRecipientDomainMapper { export class SequelizeProformaRecipientDomainMapper {
@ -28,7 +28,7 @@ export class SequelizeProformaRecipientDomainMapper {
const { errors, parent } = params as { const { errors, parent } = params as {
errors: ValidationErrorDetail[]; errors: ValidationErrorDetail[];
parent: Partial<ProformaProps>; parent: Partial<IProformaProps>;
}; };
/* if (!source.current_customer) { /* if (!source.current_customer) {

View File

@ -9,22 +9,26 @@ 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 { IProformaRepository, ProformaListDTO } from "../../../../../application"; import type {
IProformaDomainMapper,
IProformaListMapper,
IProformaRepository,
ProformaListDTO,
} from "../../../../../application";
import type { InvoiceStatus, Proforma } from "../../../../../domain"; import type { InvoiceStatus, Proforma } from "../../../../../domain";
import { import {
CustomerInvoiceItemModel, CustomerInvoiceItemModel,
CustomerInvoiceModel, CustomerInvoiceModel,
CustomerInvoiceTaxModel, CustomerInvoiceTaxModel,
} from "../../../../common"; } from "../../../../common";
import type { SequelizeProformaDomainMapper, SequelizeProformaListMapper } from "../mappers";
export class ProformaRepository export class ProformaRepository
extends SequelizeRepository<Proforma> extends SequelizeRepository<Proforma>
implements IProformaRepository implements IProformaRepository
{ {
constructor( constructor(
private readonly domainMapper: SequelizeProformaDomainMapper, private readonly domainMapper: IProformaDomainMapper,
private readonly listMapper: SequelizeProformaListMapper, private readonly listMapper: IProformaListMapper,
database: Sequelize database: Sequelize
) { ) {
super({ database }); super({ database });

View File

@ -31,7 +31,8 @@
"include": [ "include": [
"src", "src",
"../core/src/api/domain/value-objects/tax-percentage.vo.ts", "../core/src/api/domain/value-objects/tax-percentage.vo.ts",
"../core/src/api/domain/value-objects/discount-percentage.vo.ts" "../core/src/api/domain/value-objects/discount-percentage.vo.ts",
"../core/src/api/infrastructure/di/catalogs.di.ts"
], ],
"exclude": ["node_modules"] "exclude": ["node_modules"]
} }

View File

@ -13,6 +13,8 @@
"dev": "turbo dev", "dev": "turbo dev",
"dev:server": "turbo dev --filter=server", "dev:server": "turbo dev --filter=server",
"dev:client": "turbo dev --filter=client", "dev:client": "turbo dev --filter=client",
"lint": "turbo run lint",
"lint:fix": "turbo run lint:fix",
"format-and-lint": "biome check .", "format-and-lint": "biome check .",
"format-and-lint:fix": "biome check . --write", "format-and-lint:fix": "biome check . --write",
"ui:add": "pnpm --filter @repo/shadcn-ui ui:add", "ui:add": "pnpm --filter @repo/shadcn-ui ui:add",
@ -23,7 +25,10 @@
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.3.1", "@biomejs/biome": "2.3.1",
"@repo/typescript-config": "workspace:*", "@repo/typescript-config": "workspace:*",
"@typescript-eslint/eslint-plugin": "^8.56.1",
"@typescript-eslint/parser": "^8.56.1",
"change-case": "^5.4.4", "change-case": "^5.4.4",
"eslint": "^10.0.2",
"inquirer": "^12.10.0", "inquirer": "^12.10.0",
"plop": "^4.0.4", "plop": "^4.0.4",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,16 @@
{ {
"$schema": "https://turbo.build/schema.json", "$schema": "https://turbo.build/schema.json",
"ui": "tui", "ui": "tui",
"globalDependencies": ["**/.env.*local"], "globalDependencies": [
"**/.env.*local"
],
"tasks": { "tasks": {
"lint": {
"outputs": []
},
"lint:fix": {
"outputs": []
},
"dev": { "dev": {
"cache": false, "cache": false,
"persistent": true "persistent": true
@ -13,16 +21,25 @@
}, },
"build": { "build": {
"cache": false, "cache": false,
"dependsOn": ["^build"], "dependsOn": [
"inputs": ["$TURBO_DEFAULT$", ".env*"], "^build"
"outputs": ["dist/**"] ],
"inputs": [
"$TURBO_DEFAULT$",
".env*"
],
"outputs": [
"dist/**"
]
}, },
"build:templates": { "build:templates": {
"dependsOn": [], "dependsOn": [],
"outputs": ["dist/templates/**"] "outputs": [
"dist/templates/**"
]
}, },
"clean": { "clean": {
"cache": false "cache": false
} }
} }
} }