.
This commit is contained in:
parent
821b4d3ff7
commit
941ad25401
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -47,7 +47,8 @@
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports.biome": "explicit",
|
||||
"source.fixAll.biome": "explicit",
|
||||
"source.removeUnusedImports": "always"
|
||||
"source.removeUnusedImports": "always",
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
|
||||
// other vscode settings
|
||||
|
||||
@ -8,7 +8,8 @@
|
||||
"dev": "node --import=tsx --watch src/index.ts",
|
||||
"clean": "rimraf .turbo node_modules dist",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "biome lint --fix",
|
||||
"lint": "biome check . && eslint .",
|
||||
"lint:fix": "biome check --write . && eslint . --fix",
|
||||
"format": "biome format --write"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -39,7 +39,9 @@
|
||||
"indentWidth": 2,
|
||||
"lineWidth": 100,
|
||||
"lineEnding": "lf",
|
||||
"attributePosition": "auto"
|
||||
"attributePosition": "auto",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": true
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
|
||||
59
eslint.config.mjs
Normal file
59
eslint.config.mjs
Normal 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",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -5,25 +5,22 @@ import { z } from "zod/v4";
|
||||
|
||||
import { TaxPercentage } from "./tax-percentage.vo";
|
||||
|
||||
const DEFAULT_SCALE = TaxPercentage.DEFAULT_SCALE;
|
||||
const DEFAULT_MIN_VALUE = TaxPercentage.MIN_VALUE;
|
||||
const DEFAULT_MAX_VALUE = TaxPercentage.MAX_VALUE;
|
||||
|
||||
const DEFAULT_MIN_SCALE = TaxPercentage.MIN_SCALE;
|
||||
const DEFAULT_MAX_SCALE = TaxPercentage.MAX_SCALE;
|
||||
const TAX_GROUPS = ["IVA", "IPSI", "IGIC", "retention", "rec"] as const;
|
||||
type TaxGroup = (typeof TAX_GROUPS)[number];
|
||||
|
||||
export interface TaxProps {
|
||||
code: string; // iva_21
|
||||
name: string; // 21% IVA
|
||||
value: number; // 2100
|
||||
group: TaxGroup;
|
||||
}
|
||||
|
||||
export class Tax extends ValueObject<TaxProps> {
|
||||
static DEFAULT_SCALE = DEFAULT_SCALE;
|
||||
static MIN_VALUE = DEFAULT_MIN_VALUE;
|
||||
static MAX_VALUE = DEFAULT_MAX_VALUE;
|
||||
static MIN_SCALE = DEFAULT_MIN_SCALE;
|
||||
static MAX_SCALE = DEFAULT_MAX_SCALE;
|
||||
static readonly DEFAULT_SCALE = TaxPercentage.DEFAULT_SCALE;
|
||||
static readonly MIN_VALUE = TaxPercentage.MIN_VALUE;
|
||||
static readonly MAX_VALUE = TaxPercentage.MAX_VALUE;
|
||||
static readonly MIN_SCALE = TaxPercentage.MIN_SCALE;
|
||||
static readonly MAX_SCALE = TaxPercentage.MAX_SCALE;
|
||||
|
||||
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.")
|
||||
.max(40, "El código del impuesto no puede exceder 40 caracteres.")
|
||||
.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);
|
||||
}
|
||||
|
||||
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) {
|
||||
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();
|
||||
|
||||
// Delegamos en create para reusar validación y límites
|
||||
return Tax.create({
|
||||
value: Number(item.value),
|
||||
name: item.name,
|
||||
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 {
|
||||
return this.props.value;
|
||||
}
|
||||
@ -117,8 +110,24 @@ export class Tax extends ValueObject<TaxProps> {
|
||||
return this.props.code;
|
||||
}
|
||||
|
||||
get group(): string {
|
||||
return this.props.group;
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -129,21 +138,20 @@ export class Tax extends ValueObject<TaxProps> {
|
||||
return this.getProps();
|
||||
}
|
||||
|
||||
/** Devuelve el valor real de la tasa como número decimal (ej: 21.00) */
|
||||
toNumber(): number {
|
||||
return this.value / 10 ** this.scale;
|
||||
}
|
||||
|
||||
/** Devuelve la tasa formateada como porcentaje (ej: "21.00%") */
|
||||
toString(): string {
|
||||
return `${this.toNumber().toFixed(this.scale)}%`;
|
||||
}
|
||||
|
||||
isZero(): boolean {
|
||||
return this.toNumber() === 0;
|
||||
return this.value === 0;
|
||||
}
|
||||
|
||||
isPositive(): boolean {
|
||||
return this.toNumber() > 0;
|
||||
return this.value > 0;
|
||||
}
|
||||
|
||||
equalsTo(other: Tax): boolean {
|
||||
|
||||
13
modules/core/src/api/infrastructure/di/catalogs.di.ts
Normal file
13
modules/core/src/api/infrastructure/di/catalogs.di.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./catalogs.di";
|
||||
export * from "./documents.di";
|
||||
export * from "./transactions.di";
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { Sequelize } from "sequelize";
|
||||
|
||||
import { SequelizeTransactionManager } from "../sequelize";
|
||||
import { SequelizeTransactionManager } from "../persistence/sequelize";
|
||||
|
||||
export const buildTransactionManager = (database: Sequelize) =>
|
||||
new SequelizeTransactionManager(database);
|
||||
|
||||
@ -6,4 +6,4 @@ export * from "./errors";
|
||||
export * from "./express";
|
||||
export * from "./logger";
|
||||
export * from "./mappers";
|
||||
export * from "./sequelize";
|
||||
export * from "./persistence";
|
||||
|
||||
1
modules/core/src/api/infrastructure/persistence/index.ts
Normal file
1
modules/core/src/api/infrastructure/persistence/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./sequelize";
|
||||
@ -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 "../../../../domain";
|
||||
|
||||
import type { ISequelizeDomainMapper } from "./sequelize-mapper.interface";
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { DomainMapperWithBulk, IQueryMapperWithBulk } from "../../../domain";
|
||||
import type { DomainMapperWithBulk, IQueryMapperWithBulk } from "../../../../domain";
|
||||
|
||||
export interface ISequelizeDomainMapper<TModel, TModelAttributes, TEntity>
|
||||
extends DomainMapperWithBulk<TModel | TModelAttributes, TEntity> {}
|
||||
@ -1,7 +1,9 @@
|
||||
import { Collection, Result } from "@repo/rdx-utils";
|
||||
import { Model } from "sequelize";
|
||||
import { MapperParamsType } from "../../../domain";
|
||||
import { ISequelizeQueryMapper } from "./sequelize-mapper.interface";
|
||||
import type { Model } from "sequelize";
|
||||
|
||||
import type { MapperParamsType } from "../../../../domain";
|
||||
|
||||
import type { ISequelizeQueryMapper } from "./sequelize-mapper.interface";
|
||||
|
||||
export abstract class SequelizeQueryMapper<TModel extends Model, TEntity>
|
||||
implements ISequelizeQueryMapper<TModel, TEntity>
|
||||
@ -7,9 +7,9 @@ import {
|
||||
UniqueConstraintError,
|
||||
} from "sequelize";
|
||||
|
||||
import { DuplicateEntityError, EntityNotFoundError } from "../../domain";
|
||||
import { InfrastructureRepositoryError } from "../errors/infrastructure-repository-error";
|
||||
import { InfrastructureUnavailableError } from "../errors/infrastructure-unavailable-error";
|
||||
import { DuplicateEntityError, EntityNotFoundError } from "../../../domain";
|
||||
import { InfrastructureRepositoryError } from "../../errors/infrastructure-repository-error";
|
||||
import { InfrastructureUnavailableError } from "../../errors/infrastructure-unavailable-error";
|
||||
|
||||
/**
|
||||
* Traduce errores específicos de Sequelize a errores de dominio/infraestructura
|
||||
@ -1,9 +1,9 @@
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import { type Sequelize, Transaction } from "sequelize";
|
||||
|
||||
import { TransactionManager } from "../database";
|
||||
import { InfrastructureError, InfrastructureUnavailableError } from "../errors";
|
||||
import { logger } from "../logger";
|
||||
import { TransactionManager } from "../../database";
|
||||
import { InfrastructureError, InfrastructureUnavailableError } from "../../errors";
|
||||
import { logger } from "../../logger";
|
||||
|
||||
export class SequelizeTransactionManager extends TransactionManager {
|
||||
protected _database: Sequelize | null = null;
|
||||
@ -134,13 +134,12 @@
|
||||
"description": "Inversión del sujeto pasivo.",
|
||||
"aeat_code": "09"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Retenc. 35%",
|
||||
"code": "retencion_35",
|
||||
"value": "3500",
|
||||
"scale": "2",
|
||||
"group": "Retención",
|
||||
"group": "retention",
|
||||
"description": "Retenc. profesional o fiscal tipo máximo.",
|
||||
"aeat_code": null
|
||||
},
|
||||
@ -149,7 +148,7 @@
|
||||
"code": "retencion_19",
|
||||
"value": "1900",
|
||||
"scale": "2",
|
||||
"group": "Retención",
|
||||
"group": "retention",
|
||||
"description": "Retenc. IRPF general.",
|
||||
"aeat_code": "R1"
|
||||
},
|
||||
@ -158,7 +157,7 @@
|
||||
"code": "retencion_15",
|
||||
"value": "1500",
|
||||
"scale": "2",
|
||||
"group": "Retención",
|
||||
"group": "retention",
|
||||
"description": "Retenc. para autónomos y profesionales.",
|
||||
"aeat_code": "R2"
|
||||
},
|
||||
@ -167,7 +166,7 @@
|
||||
"code": "retencion_7",
|
||||
"value": "700",
|
||||
"scale": "2",
|
||||
"group": "Retención",
|
||||
"group": "retention",
|
||||
"description": "Retenc. para nuevos autónomos.",
|
||||
"aeat_code": null
|
||||
},
|
||||
@ -176,17 +175,16 @@
|
||||
"code": "retencion_2",
|
||||
"value": "200",
|
||||
"scale": "2",
|
||||
"group": "Retención",
|
||||
"group": "retention",
|
||||
"description": "Retenc. sobre arrendamientos de inmuebles urbanos.",
|
||||
"aeat_code": "R3"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Rec. 5,2%",
|
||||
"code": "rec_5_2",
|
||||
"value": "520",
|
||||
"scale": "2",
|
||||
"group": "Recargo de equivalencia",
|
||||
"group": "rec",
|
||||
"description": "Recargo general para IVA 21%.",
|
||||
"aeat_code": "51"
|
||||
},
|
||||
@ -195,7 +193,7 @@
|
||||
"code": "rec_1_75",
|
||||
"value": "175",
|
||||
"scale": "2",
|
||||
"group": "Recargo de equivalencia",
|
||||
"group": "rec",
|
||||
"description": "Recargo para IVA 10%.",
|
||||
"aeat_code": "52"
|
||||
},
|
||||
@ -204,7 +202,7 @@
|
||||
"code": "rec_1_4",
|
||||
"value": "140",
|
||||
"scale": "2",
|
||||
"group": "Recargo de equivalencia",
|
||||
"group": "rec",
|
||||
"description": "Recargo para IVA 5%.",
|
||||
"aeat_code": null
|
||||
},
|
||||
@ -213,7 +211,7 @@
|
||||
"code": "rec_1",
|
||||
"value": "100",
|
||||
"scale": "2",
|
||||
"group": "Recargo de equivalencia",
|
||||
"group": "rec",
|
||||
"description": "Recargo especial.",
|
||||
"aeat_code": null
|
||||
},
|
||||
@ -222,7 +220,7 @@
|
||||
"code": "rec_0_62",
|
||||
"value": "62",
|
||||
"scale": "2",
|
||||
"group": "Recargo de equivalencia",
|
||||
"group": "rec",
|
||||
"description": "Recargo para IVA reducido especial.",
|
||||
"aeat_code": null
|
||||
},
|
||||
@ -231,7 +229,7 @@
|
||||
"code": "rec_0_5",
|
||||
"value": "50",
|
||||
"scale": "2",
|
||||
"group": "Recargo de equivalencia",
|
||||
"group": "rec",
|
||||
"description": "Recargo especial.",
|
||||
"aeat_code": null
|
||||
},
|
||||
@ -240,7 +238,7 @@
|
||||
"code": "rec_0_26",
|
||||
"value": "26",
|
||||
"scale": "2",
|
||||
"group": "Recargo de equivalencia",
|
||||
"group": "rec",
|
||||
"description": "Recargo mínimo.",
|
||||
"aeat_code": null
|
||||
},
|
||||
@ -249,11 +247,10 @@
|
||||
"code": "rec_0",
|
||||
"value": "0",
|
||||
"scale": "2",
|
||||
"group": "Recargo de equivalencia",
|
||||
"group": "rec",
|
||||
"description": "Sin recargo.",
|
||||
"aeat_code": null
|
||||
},
|
||||
|
||||
{
|
||||
"name": "IGIC 7%",
|
||||
"code": "igic_7",
|
||||
@ -335,7 +332,6 @@
|
||||
"description": "Operación exenta de IGIC.",
|
||||
"aeat_code": "12"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "IPSI 10%",
|
||||
"code": "ipsi_10",
|
||||
@ -372,4 +368,4 @@
|
||||
"description": "Operación exenta de IPSI.",
|
||||
"aeat_code": null
|
||||
}
|
||||
]
|
||||
]
|
||||
@ -1,17 +1,17 @@
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Collection, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { IssuedInvoiceProps, Proforma } from "../../../domain";
|
||||
import type { IIssuedInvoiceProps, Proforma } from "../../../domain";
|
||||
|
||||
export interface IProformaToIssuedInvoiceMaterializer {
|
||||
materialize(proforma: Proforma, issuedInvoiceId: UniqueID): Result<IssuedInvoiceProps, Error>;
|
||||
materialize(proforma: Proforma, issuedInvoiceId: UniqueID): Result<IIssuedInvoiceProps, Error>;
|
||||
}
|
||||
|
||||
export class ProformaToIssuedInvoiceMaterializer implements IProformaToIssuedInvoiceMaterializer {
|
||||
public materialize(
|
||||
proforma: Proforma,
|
||||
issuedInvoiceId: UniqueID
|
||||
): Result<IssuedInvoiceProps, Error> {
|
||||
): Result<IIssuedInvoiceProps, Error> {
|
||||
const amounts = proforma.calculateAllAmounts();
|
||||
const taxGroups = proforma.getTaxes();
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
export * from "./proforma-creator.di";
|
||||
export * from "./proforma-finder.di";
|
||||
export * from "./proforma-input-mappers.di";
|
||||
export * from "./proforma-snapshot-builders.di";
|
||||
export * from "./proforma-use-cases.di";
|
||||
|
||||
@ -2,13 +2,16 @@ import { ProformaFactory } from "../factories";
|
||||
import type { IProformaRepository } from "../repositories";
|
||||
import { type IProformaCreator, type IProformaNumberGenerator, ProformaCreator } from "../services";
|
||||
|
||||
export const buildProformaCreator = (
|
||||
numberService: IProformaNumberGenerator,
|
||||
repository: IProformaRepository
|
||||
): IProformaCreator => {
|
||||
export const buildProformaCreator = (params: {
|
||||
numberService: IProformaNumberGenerator;
|
||||
repository: IProformaRepository;
|
||||
}): IProformaCreator => {
|
||||
const { numberService, repository } = params;
|
||||
const factory = new ProformaFactory();
|
||||
|
||||
return new ProformaCreator({
|
||||
numberService,
|
||||
factory: new ProformaFactory(),
|
||||
factory,
|
||||
repository,
|
||||
});
|
||||
};
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -1,12 +1,18 @@
|
||||
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 {
|
||||
IProformaListItemSnapshotBuilder,
|
||||
IProformaReportSnapshotBuilder,
|
||||
} from "../snapshot-builders";
|
||||
import type { IProformaFullSnapshotBuilder } from "../snapshot-builders/full";
|
||||
import { GetProformaByIdUseCase, ListProformasUseCase, ReportProformaUseCase } from "../use-cases";
|
||||
import { CreateProformaUseCase } from "../use-cases/create-proforma";
|
||||
|
||||
export function buildGetProformaByIdUseCase(deps: {
|
||||
finder: IProformaFinder;
|
||||
@ -40,18 +46,19 @@ export function buildReportProformaUseCase(deps: {
|
||||
);
|
||||
}
|
||||
|
||||
/*export function buildCreateProformaUseCase(deps: {
|
||||
export function buildCreateProformaUseCase(deps: {
|
||||
creator: IProformaCreator;
|
||||
dtoMapper: ICreateProformaInputMapper;
|
||||
fullSnapshotBuilder: IProformaFullSnapshotBuilder;
|
||||
transactionManager: ITransactionManager;
|
||||
}) {
|
||||
return new CreateProformaUseCase({
|
||||
mapper: new CreateProformaPropsMapper(),
|
||||
dtoMapper: deps.dtoMapper,
|
||||
creator: deps.creator,
|
||||
fullSnapshotBuilder: deps.fullSnapshotBuilder,
|
||||
transactionManager: deps.transactionManager,
|
||||
});
|
||||
}*/
|
||||
}
|
||||
|
||||
/*export function buildUpdateProformaUseCase(deps: {
|
||||
finder: IProformaFinder;
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
export * from "./create-proforma-props.mapper";
|
||||
export * from "./inputs";
|
||||
export * from "./proforma-domain-mapper.interface";
|
||||
export * from "./proforma-list-mapper.interface";
|
||||
//export * from "./update-proforma-props.mapper";
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./create-proforma-input.mapper";
|
||||
export * from "./update-proforma-input.mapper";
|
||||
@ -1,3 +1,4 @@
|
||||
import { InvoiceSerie, type ProformaPatchProps } from "@erp/customer-invoices/api/domain";
|
||||
import {
|
||||
CurrencyCode,
|
||||
DomainError,
|
||||
@ -13,7 +14,6 @@ import {
|
||||
import { Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils";
|
||||
|
||||
import type { UpdateProformaByIdRequestDTO } from "../../../../../common/dto";
|
||||
import { type CustomerInvoicePatchProps, CustomerInvoiceSerie } from "../../../../domain";
|
||||
|
||||
/**
|
||||
* UpdateProformaPropsMapper
|
||||
@ -29,14 +29,14 @@ import { type CustomerInvoicePatchProps, CustomerInvoiceSerie } from "../../../.
|
||||
*
|
||||
*/
|
||||
|
||||
export function UpdateProformaPropsMapper(dto: UpdateProformaByIdRequestDTO) {
|
||||
export function UpdateProformaInputMapper(dto: UpdateProformaByIdRequestDTO) {
|
||||
try {
|
||||
const errors: ValidationErrorDetail[] = [];
|
||||
const props: CustomerInvoicePatchProps = {};
|
||||
const props: ProformaPatchProps = {};
|
||||
|
||||
toPatchField(dto.series).ifSet((series) => {
|
||||
props.series = extractOrPushError(
|
||||
maybeFromNullableResult(series, (value) => CustomerInvoiceSerie.create(value)),
|
||||
maybeFromNullableResult(series, (value) => InvoiceSerie.create(value)),
|
||||
"reference",
|
||||
errors
|
||||
);
|
||||
@ -3,7 +3,7 @@ import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { CreateProformaRequestDTO } from "../../../../../common";
|
||||
import type { CreateProformaPropsMapper } from "../../mappers";
|
||||
import type { ICreateProformaInputMapper } from "../../mappers";
|
||||
import type { IProformaCreator } from "../../services";
|
||||
import type { IProformaFullSnapshotBuilder } from "../../snapshot-builders";
|
||||
|
||||
@ -13,20 +13,20 @@ type CreateProformaUseCaseInput = {
|
||||
};
|
||||
|
||||
type CreateProformaUseCaseDeps = {
|
||||
mapper: CreateProformaPropsMapper;
|
||||
dtoMapper: ICreateProformaInputMapper;
|
||||
creator: IProformaCreator;
|
||||
fullSnapshotBuilder: IProformaFullSnapshotBuilder;
|
||||
transactionManager: ITransactionManager;
|
||||
};
|
||||
|
||||
export class CreateProformaUseCase {
|
||||
private readonly mapper: CreateProformaPropsMapper;
|
||||
private readonly dtoMapper: ICreateProformaInputMapper;
|
||||
private readonly creator: IProformaCreator;
|
||||
private readonly fullSnapshotBuilder: IProformaFullSnapshotBuilder;
|
||||
private readonly transactionManager: ITransactionManager;
|
||||
|
||||
constructor(deps: CreateProformaUseCaseDeps) {
|
||||
this.mapper = deps.mapper;
|
||||
this.dtoMapper = deps.dtoMapper;
|
||||
this.creator = deps.creator;
|
||||
this.fullSnapshotBuilder = deps.fullSnapshotBuilder;
|
||||
this.transactionManager = deps.transactionManager;
|
||||
@ -36,7 +36,7 @@ export class CreateProformaUseCase {
|
||||
const { dto, companyId } = params;
|
||||
|
||||
// 1) Mapear DTO → props de dominio
|
||||
const mappedResult = this.mapper.map(dto, companyId);
|
||||
const mappedResult = this.dtoMapper.map(dto, companyId);
|
||||
if (mappedResult.isFailure) {
|
||||
return Result.fail(mappedResult.error);
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
//export * from "./change-status-proforma.use-case";
|
||||
//export * from "./create-proforma";
|
||||
export * from "./create-proforma";
|
||||
//export * from "./delete-proforma.use-case";
|
||||
export * from "./get-proforma-by-id.use-case";
|
||||
//export * from "./issue-proforma.use-case";
|
||||
|
||||
@ -21,7 +21,7 @@ import type {
|
||||
} from "../../common";
|
||||
import { IssuedInvoiceItems, type IssuedInvoiceTaxes, type VerifactuRecord } from "../entities";
|
||||
|
||||
export type IssuedInvoiceProps = {
|
||||
export interface IIssuedInvoiceProps {
|
||||
companyId: UniqueID;
|
||||
status: InvoiceStatus;
|
||||
|
||||
@ -65,12 +65,12 @@ export type IssuedInvoiceProps = {
|
||||
totalAmount: InvoiceAmount;
|
||||
|
||||
verifactu: Maybe<VerifactuRecord>;
|
||||
};
|
||||
}
|
||||
|
||||
export class IssuedInvoice extends AggregateRoot<IssuedInvoiceProps> {
|
||||
export class IssuedInvoice extends AggregateRoot<IIssuedInvoiceProps> {
|
||||
private _items!: IssuedInvoiceItems;
|
||||
|
||||
protected constructor(props: IssuedInvoiceProps, id?: UniqueID) {
|
||||
protected constructor(props: IIssuedInvoiceProps, id?: UniqueID) {
|
||||
super(props, id);
|
||||
this._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) {
|
||||
return Result.fail(
|
||||
new DomainValidationError(
|
||||
@ -231,7 +231,7 @@ export class IssuedInvoice extends AggregateRoot<IssuedInvoiceProps> {
|
||||
return this.paymentMethod.isSome();
|
||||
}
|
||||
|
||||
public getProps(): IssuedInvoiceProps {
|
||||
public getProps(): IIssuedInvoiceProps {
|
||||
return this.props;
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,10 +20,18 @@ import {
|
||||
type InvoiceStatus,
|
||||
type ItemAmount,
|
||||
} 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 { ProformaItemTaxes } from "../value-objects";
|
||||
|
||||
export type ProformaProps = {
|
||||
export interface IProformaProps {
|
||||
companyId: UniqueID;
|
||||
status: InvoiceStatus;
|
||||
|
||||
@ -45,9 +53,9 @@ export type ProformaProps = {
|
||||
|
||||
paymentMethod: Maybe<InvoicePaymentMethod>;
|
||||
|
||||
items: ProformaItems;
|
||||
items: IProformaItemsProps[];
|
||||
globalDiscountPercentage: DiscountPercentage;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IProformaTotals {
|
||||
subtotalAmount: InvoiceAmount;
|
||||
@ -88,31 +96,31 @@ export interface IProforma {
|
||||
|
||||
paymentMethod: Maybe<InvoicePaymentMethod>;
|
||||
|
||||
items: ProformaItems;
|
||||
items: IProformaItems;
|
||||
taxes(): Collection<IProformaTaxTotals>;
|
||||
totals(): IProformaTotals;
|
||||
}
|
||||
|
||||
export type ProformaPatchProps = Partial<Omit<ProformaProps, "companyId" | "items">> & {
|
||||
export type ProformaPatchProps = Partial<Omit<IProformaProps, "companyId" | "items">> & {
|
||||
items?: ProformaItems;
|
||||
};
|
||||
|
||||
export class Proforma extends AggregateRoot<ProformaProps> implements IProforma {
|
||||
private _items!: ProformaItems;
|
||||
type CreateProformaProps = IProformaProps;
|
||||
type InternalProformaProps = Omit<IProformaProps, "items">;
|
||||
|
||||
protected constructor(props: ProformaProps, id?: UniqueID) {
|
||||
super(props, id);
|
||||
this._items =
|
||||
props.items ||
|
||||
ProformaItems.create({
|
||||
languageCode: props.languageCode,
|
||||
currencyCode: props.currencyCode,
|
||||
globalDiscountPercentage: props.globalDiscountPercentage,
|
||||
});
|
||||
}
|
||||
export class Proforma extends AggregateRoot<InternalProformaProps> implements IProforma {
|
||||
private readonly _items: ProformaItems;
|
||||
|
||||
static create(props: ProformaProps, id?: UniqueID): Result<Proforma, Error> {
|
||||
const proforma = new Proforma(props, id);
|
||||
// Creación funcional
|
||||
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
|
||||
|
||||
@ -123,15 +131,30 @@ export class Proforma extends AggregateRoot<ProformaProps> implements IProforma
|
||||
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(
|
||||
partialProforma: Partial<Omit<ProformaProps, "companyId">>
|
||||
partialProforma: Partial<Omit<IProformaProps, "companyId">>
|
||||
): Result<Proforma, Error> {
|
||||
const updatedProps = {
|
||||
...this.props,
|
||||
...partialProforma,
|
||||
} as ProformaProps;
|
||||
} as IProformaProps;
|
||||
|
||||
return Proforma.create(updatedProps, this.id);
|
||||
}
|
||||
@ -214,8 +237,7 @@ export class Proforma extends AggregateRoot<ProformaProps> implements IProforma
|
||||
return this.props.globalDiscountPercentage;
|
||||
}
|
||||
|
||||
// Method to get the complete list of line items
|
||||
public get items(): ProformaItems {
|
||||
public get items(): IProformaItems {
|
||||
return this._items;
|
||||
}
|
||||
|
||||
@ -258,12 +280,64 @@ export class Proforma extends AggregateRoot<ProformaProps> implements IProforma
|
||||
return new ProformaTaxesCalculator(this.items).calculate();
|
||||
}
|
||||
|
||||
public getProps(): ProformaProps {
|
||||
return this.props;
|
||||
public addItem(props: IProformaItemProps): Result<void, Error> {
|
||||
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
|
||||
|
||||
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).
|
||||
*/
|
||||
|
||||
@ -3,7 +3,10 @@ import { type CurrencyCode, DomainEntity, type LanguageCode, type UniqueID } fro
|
||||
import { type Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
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>;
|
||||
|
||||
quantity: Maybe<ItemQuantity>; // Cantidad de unidades
|
||||
@ -30,14 +33,14 @@ export type ProformaItemProps = {
|
||||
|
||||
itemDiscountPercentage: Maybe<DiscountPercentage>; // % descuento de línea
|
||||
|
||||
taxes: ProformaItemTaxes;
|
||||
taxes: ProformaItemTaxesProps;
|
||||
|
||||
// Estos campos vienen de la cabecera,
|
||||
// pero se necesitan para cálculos y representaciones de la línea.
|
||||
globalDiscountPercentage: DiscountPercentage; // % descuento de la cabecera
|
||||
languageCode: LanguageCode; // Para formateos específicos de idioma
|
||||
currencyCode: CurrencyCode; // Para cálculos y formateos de moneda
|
||||
};
|
||||
}
|
||||
|
||||
export interface IProformaItemTotals {
|
||||
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"
|
||||
}
|
||||
|
||||
export class ProformaItem extends DomainEntity<ProformaItemProps> implements IProformaItem {
|
||||
public static create(props: ProformaItemProps, id?: UniqueID): Result<ProformaItem, Error> {
|
||||
const item = new ProformaItem(props, id);
|
||||
type CreateProformaItemProps = IProformaItemProps;
|
||||
|
||||
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
|
||||
// ...
|
||||
@ -95,7 +115,11 @@ export class ProformaItem extends DomainEntity<ProformaItemProps> implements IPr
|
||||
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);
|
||||
}
|
||||
|
||||
@ -131,7 +155,7 @@ export class ProformaItem extends DomainEntity<ProformaItemProps> implements IPr
|
||||
return this.props.taxes;
|
||||
}
|
||||
|
||||
getProps(): ProformaItemProps {
|
||||
getProps(): IProformaItemProps {
|
||||
return this.props;
|
||||
}
|
||||
|
||||
|
||||
@ -1,28 +1,37 @@
|
||||
import type { DiscountPercentage } from "@erp/core/api";
|
||||
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 type { IProformaItem, IProformaItemTotals, ProformaItem } from "./proforma-item.entity";
|
||||
import type {
|
||||
ICreateProformaItemProps,
|
||||
IProformaItem,
|
||||
IProformaItemTotals,
|
||||
ProformaItem,
|
||||
} from "./proforma-item.entity";
|
||||
|
||||
export type ProformaItemsProps = {
|
||||
items?: ProformaItem[];
|
||||
export interface IProformaItemsProps {
|
||||
items?: ICreateProformaItemProps[];
|
||||
|
||||
// Estos campos vienen de la cabecera,
|
||||
// pero se necesitan para cálculos y representaciones de la línea.
|
||||
globalDiscountPercentage: DiscountPercentage; // % descuento de la cabecera
|
||||
languageCode: LanguageCode; // Para formateos específicos de idioma
|
||||
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.
|
||||
totals(): IProformaItemTotals;
|
||||
|
||||
globalDiscountPercentage: DiscountPercentage; // % descuento de la cabecera
|
||||
languageCode: LanguageCode; // Para formateos específicos de idioma
|
||||
currencyCode: CurrencyCode; // Para cálculos y formateos de moneda
|
||||
readonly globalDiscountPercentage: DiscountPercentage; // % descuento de la cabecera
|
||||
readonly languageCode: LanguageCode; // Para formateos específicos de idioma
|
||||
readonly currencyCode: CurrencyCode; // Para cálculos y formateos de moneda
|
||||
}
|
||||
|
||||
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 globalDiscountPercentage!: DiscountPercentage;
|
||||
|
||||
constructor(props: ProformaItemsProps) {
|
||||
constructor(props: IProformaItemsProps) {
|
||||
super(props.items ?? []);
|
||||
this.languageCode = props.languageCode;
|
||||
this.currencyCode = props.currencyCode;
|
||||
@ -39,7 +48,7 @@ export class ProformaItems extends Collection<ProformaItem> implements IProforma
|
||||
this.ensureSameCurrencyAndLanguage(this.items);
|
||||
}
|
||||
|
||||
public static create(props: ProformaItemsProps): ProformaItems {
|
||||
public static create(props: IProformaItemsProps): ProformaItems {
|
||||
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.
|
||||
* @remarks
|
||||
* 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.
|
||||
*/
|
||||
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
|
||||
// tiene el mismo "currencyCode" y "languageCode" que la colección de items.
|
||||
const same =
|
||||
@ -64,9 +73,11 @@ export class ProformaItems extends Collection<ProformaItem> implements IProforma
|
||||
this.currencyCode.equals(item.currencyCode) &&
|
||||
this.globalDiscountPercentage.equals(item.globalDiscountPercentage);
|
||||
|
||||
if (!same) return false;
|
||||
|
||||
return super.add(item);
|
||||
if (!same) {
|
||||
return Result.fail(new ProformaItemMismatch(this.size()));
|
||||
}
|
||||
super.add(item);
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
// Cálculos
|
||||
|
||||
@ -3,3 +3,4 @@ export * from "./entity-is-not-proforma-error";
|
||||
export * from "./invalid-proforma-transition-error";
|
||||
export * from "./proforma-cannot-be-converted-to-invoice-error";
|
||||
export * from "./proforma-cannot-be-deleted-error";
|
||||
export * from "./proforma-item-not-valid-error";
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
import { DomainError } from "@repo/rdx-ddd";
|
||||
|
||||
/**
|
||||
* Error de dominio que indica que al añadir un nuevo item a la lista
|
||||
* de detalles, este no tiene el mismo "currencyCode" y "languageCode"
|
||||
* que la colección de items.
|
||||
*
|
||||
*/
|
||||
export class 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;
|
||||
@ -2,14 +2,14 @@ import type { IModuleServer } from "@erp/core/api";
|
||||
|
||||
import {
|
||||
type IssuedInvoicesInternalDeps,
|
||||
type ProformasInternalDeps,
|
||||
buildIssuedInvoiceServices,
|
||||
buildIssuedInvoicesDependencies,
|
||||
buildProformaServices,
|
||||
buildProformasDependencies,
|
||||
models,
|
||||
} from "./infrastructure";
|
||||
import { issuedInvoicesRouter } from "./infrastructure/express";
|
||||
import { proformasRouter } from './infrastructure/express';
|
||||
import { issuedInvoicesRouter, proformasRouter } from "./infrastructure/express";
|
||||
|
||||
export const customerInvoicesAPIModule: IModuleServer = {
|
||||
name: "customer-invoices",
|
||||
@ -43,13 +43,13 @@ export const customerInvoicesAPIModule: IModuleServer = {
|
||||
// Servicios expuestos a otros módulos
|
||||
services: {
|
||||
issuedInvoices: issuedInvoicesServices,
|
||||
proformas: proformasServices
|
||||
proformas: proformasServices,
|
||||
},
|
||||
|
||||
// Implementación privada del módulo
|
||||
internal: {
|
||||
issuedInvoices: issuedInvoicesInternalDeps,
|
||||
proformas: proformasInternalDeps
|
||||
proformas: proformasInternalDeps,
|
||||
},
|
||||
};
|
||||
},
|
||||
@ -69,8 +69,11 @@ export const customerInvoicesAPIModule: IModuleServer = {
|
||||
"customer-invoices",
|
||||
"issuedInvoices"
|
||||
);
|
||||
|
||||
const proformasInternalDeps = getInternal("customer-invoices", "proformas");
|
||||
|
||||
const proformasInternalDeps = getInternal<ProformasInternalDeps>(
|
||||
"customer-invoices",
|
||||
"proformas"
|
||||
);
|
||||
|
||||
// Registro de rutas HTTP
|
||||
issuedInvoicesRouter(params, issuedInvoicesInternalDeps);
|
||||
|
||||
@ -13,11 +13,13 @@ import {
|
||||
type EntityIsNotProformaError,
|
||||
type InvalidProformaTransitionError,
|
||||
type ProformaCannotBeConvertedToInvoiceError,
|
||||
type ProformaItemMismatch,
|
||||
isCustomerInvoiceIdAlreadyExistsError,
|
||||
isEntityIsNotProformaError,
|
||||
isInvalidProformaTransitionError,
|
||||
isProformaCannotBeConvertedToInvoiceError,
|
||||
isProformaCannotBeDeletedError,
|
||||
isProformaItemMismatch,
|
||||
} from "../../../domain";
|
||||
|
||||
// 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 = {
|
||||
priority: 120,
|
||||
matches: (e) => isInvalidProformaTransitionError(e),
|
||||
@ -71,6 +83,7 @@ const proformaCannotBeDeletedRule: ErrorToApiRule = {
|
||||
// Cómo aplicarla: crea una nueva instancia del mapper con la regla extra
|
||||
export const customerInvoicesApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default()
|
||||
.register(invoiceDuplicateRule)
|
||||
.register(proformaItemMismatchError)
|
||||
.register(entityIsNotProformaError)
|
||||
.register(proformaConversionRule)
|
||||
.register(proformaCannotBeDeletedRule)
|
||||
|
||||
@ -16,6 +16,7 @@ import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils";
|
||||
import type { IIssuedInvoiceDomainMapper } from "../../../../../../application";
|
||||
import {
|
||||
DiscountPercentage,
|
||||
type IIssuedInvoiceProps,
|
||||
InvoiceAmount,
|
||||
InvoiceNumber,
|
||||
InvoicePaymentMethod,
|
||||
@ -23,7 +24,6 @@ import {
|
||||
InvoiceStatus,
|
||||
IssuedInvoice,
|
||||
IssuedInvoiceItems,
|
||||
type IssuedInvoiceProps,
|
||||
IssuedInvoiceTaxes,
|
||||
} from "../../../../../../domain";
|
||||
import type {
|
||||
@ -351,7 +351,7 @@ export class SequelizeIssuedInvoiceDomainMapper
|
||||
currencyCode: attributes.currencyCode!,
|
||||
});
|
||||
|
||||
const invoiceProps: IssuedInvoiceProps = {
|
||||
const invoiceProps: IIssuedInvoiceProps = {
|
||||
companyId: attributes.companyId!,
|
||||
|
||||
proformaId: attributes.proformaId!,
|
||||
|
||||
@ -14,10 +14,10 @@ import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import {
|
||||
DiscountPercentage,
|
||||
type IIssuedInvoiceProps,
|
||||
type IssuedInvoice,
|
||||
IssuedInvoiceItem,
|
||||
type IssuedInvoiceItemProps,
|
||||
type IssuedInvoiceProps,
|
||||
ItemAmount,
|
||||
ItemDescription,
|
||||
ItemDiscountPercentage,
|
||||
@ -56,7 +56,7 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
|
||||
const { errors, index, attributes } = params as {
|
||||
index: number;
|
||||
errors: ValidationErrorDetail[];
|
||||
attributes: Partial<IssuedInvoiceProps>;
|
||||
attributes: Partial<IIssuedInvoiceProps>;
|
||||
};
|
||||
|
||||
const itemId = extractOrPushError(
|
||||
@ -263,7 +263,7 @@ export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMappe
|
||||
const { errors, index } = params as {
|
||||
index: number;
|
||||
errors: ValidationErrorDetail[];
|
||||
attributes: Partial<IssuedInvoiceProps>;
|
||||
attributes: Partial<IIssuedInvoiceProps>;
|
||||
};
|
||||
|
||||
// 1) Valores escalares (atributos generales)
|
||||
|
||||
@ -16,9 +16,9 @@ import {
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import {
|
||||
type IIssuedInvoiceProps,
|
||||
InvoiceRecipient,
|
||||
type IssuedInvoice,
|
||||
type IssuedInvoiceProps,
|
||||
} from "../../../../../../domain";
|
||||
import type { CustomerInvoiceModel } from "../../../../../common";
|
||||
|
||||
@ -33,7 +33,7 @@ export class SequelizeIssuedInvoiceRecipientDomainMapper {
|
||||
|
||||
const { errors, attributes } = params as {
|
||||
errors: ValidationErrorDetail[];
|
||||
attributes: Partial<IssuedInvoiceProps>;
|
||||
attributes: Partial<IIssuedInvoiceProps>;
|
||||
};
|
||||
|
||||
const _name = source.customer_name!;
|
||||
|
||||
@ -14,9 +14,9 @@ import {
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import {
|
||||
type IIssuedInvoiceProps,
|
||||
InvoiceAmount,
|
||||
type IssuedInvoice,
|
||||
type IssuedInvoiceProps,
|
||||
IssuedInvoiceTax,
|
||||
ItemAmount,
|
||||
ItemDiscountPercentage,
|
||||
@ -66,7 +66,7 @@ export class SequelizeIssuedInvoiceTaxesDomainMapper extends SequelizeDomainMapp
|
||||
const { errors, index, attributes } = params as {
|
||||
index: number;
|
||||
errors: ValidationErrorDetail[];
|
||||
attributes: Partial<IssuedInvoiceProps>;
|
||||
attributes: Partial<IIssuedInvoiceProps>;
|
||||
};
|
||||
|
||||
const taxableAmount = extractOrPushError(
|
||||
|
||||
@ -12,8 +12,8 @@ import {
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import {
|
||||
type IIssuedInvoiceProps,
|
||||
type IssuedInvoice,
|
||||
type IssuedInvoiceProps,
|
||||
VerifactuRecord,
|
||||
VerifactuRecordEstado,
|
||||
} from "../../../../../../domain";
|
||||
@ -33,7 +33,7 @@ export class SequelizeIssuedInvoiceVerifactuDomainMapper extends SequelizeDomain
|
||||
): Result<Maybe<VerifactuRecord>, Error> {
|
||||
const { errors, attributes } = params as {
|
||||
errors: ValidationErrorDetail[];
|
||||
attributes: Partial<IssuedInvoiceProps>;
|
||||
attributes: Partial<IIssuedInvoiceProps>;
|
||||
};
|
||||
|
||||
if (!source) {
|
||||
|
||||
@ -5,7 +5,7 @@ import {
|
||||
type ProformaDocumentPipelineFactoryDeps,
|
||||
} from "../documents";
|
||||
|
||||
export const buildproformaDocumentService = (params: ModuleParams) => {
|
||||
export const buildProformaDocumentService = (params: ModuleParams) => {
|
||||
const { documentRenderers, documentSigning, documentStorage } = buildCoreDocumentsDI(params);
|
||||
|
||||
const pipelineDeps: ProformaDocumentPipelineFactoryDeps = {
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -1,19 +1,14 @@
|
||||
import { SpainTaxCatalogProvider } from "@erp/core";
|
||||
import type { Sequelize } from "sequelize";
|
||||
|
||||
import {
|
||||
ProformaRepository,
|
||||
SequelizeProformaDomainMapper,
|
||||
SequelizeProformaListMapper,
|
||||
} from "../persistence";
|
||||
import { ProformaRepository } from "../persistence";
|
||||
|
||||
export const buildProformaRepository = (database: Sequelize) => {
|
||||
const taxCatalog = SpainTaxCatalogProvider();
|
||||
import type { IProformaPersistenceMappers } from "./proforma-persistence-mappers.di";
|
||||
|
||||
const domainMapper = new SequelizeProformaDomainMapper({
|
||||
taxCatalog,
|
||||
});
|
||||
const listMapper = new SequelizeProformaListMapper();
|
||||
export const buildProformaRepository = (params: {
|
||||
database: Sequelize;
|
||||
mappers: IProformaPersistenceMappers;
|
||||
}) => {
|
||||
const { database, mappers } = params;
|
||||
|
||||
return new ProformaRepository(domainMapper, listMapper, database);
|
||||
return new ProformaRepository(mappers.domainMapper, mappers.listMapper, database);
|
||||
};
|
||||
|
||||
@ -1,17 +1,23 @@
|
||||
import { type ModuleParams, buildTransactionManager } from "@erp/core/api";
|
||||
import { type ModuleParams, buildCatalogs, buildTransactionManager } from "@erp/core/api";
|
||||
|
||||
import {
|
||||
type CreateProformaUseCase,
|
||||
type GetProformaByIdUseCase,
|
||||
type ListProformasUseCase,
|
||||
type ReportProformaUseCase,
|
||||
buildCreateProformaUseCase,
|
||||
buildGetProformaByIdUseCase,
|
||||
buildListProformasUseCase,
|
||||
buildProformaCreator,
|
||||
buildProformaFinder,
|
||||
buildProformaInputMappers,
|
||||
buildProformaSnapshotBuilders,
|
||||
buildReportProformaUseCase,
|
||||
} 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";
|
||||
|
||||
export type ProformasInternalDeps = {
|
||||
@ -19,8 +25,9 @@ export type ProformasInternalDeps = {
|
||||
listProformas: () => ListProformasUseCase;
|
||||
getProformaById: () => GetProformaByIdUseCase;
|
||||
reportProforma: () => ReportProformaUseCase;
|
||||
createProforma: () => CreateProformaUseCase;
|
||||
|
||||
/*createProforma: () => CreateProformaUseCase;
|
||||
/*
|
||||
updateProforma: () => UpdateProformaUseCase;
|
||||
deleteProforma: () => DeleteProformaUseCase;
|
||||
issueProforma: () => IssueProformaUseCase;
|
||||
@ -33,14 +40,19 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
|
||||
|
||||
// Infrastructure
|
||||
const transactionManager = buildTransactionManager(database);
|
||||
const repository = buildProformaRepository(database);
|
||||
//const numberService = buildProformaNumberGenerator();
|
||||
const catalogs = buildCatalogs();
|
||||
const persistenceMappers = buildProformaPersistenceMappers(catalogs);
|
||||
|
||||
const repository = buildProformaRepository({ database, mappers: persistenceMappers });
|
||||
const numberService = buildProformaNumberGenerator();
|
||||
|
||||
// Application helpers
|
||||
const inputMappers = buildProformaInputMappers(catalogs);
|
||||
const finder = buildProformaFinder(repository);
|
||||
//const creator = buildProformaCreator(numberService, repository);
|
||||
const creator = buildProformaCreator({ numberService, repository });
|
||||
|
||||
const snapshotBuilders = buildProformaSnapshotBuilders();
|
||||
const documentGeneratorPipeline = buildproformaDocumentService(params);
|
||||
const documentGeneratorPipeline = buildProformaDocumentService(params);
|
||||
|
||||
// Internal use cases (factories)
|
||||
return {
|
||||
@ -68,12 +80,13 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
|
||||
transactionManager,
|
||||
}),
|
||||
|
||||
/*createProforma: () =>
|
||||
createProforma: () =>
|
||||
buildCreateProformaUseCase({
|
||||
creator,
|
||||
dtoMapper: inputMappers.createInputMapper,
|
||||
fullSnapshotBuilder: snapshotBuilders.full,
|
||||
transactionManager,
|
||||
}),*/
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import type { JsonTaxCatalogProvider } from "@erp/core";
|
||||
import { Tax } from "@erp/core/api";
|
||||
import {
|
||||
CurrencyCode,
|
||||
DomainError,
|
||||
@ -15,8 +14,10 @@ import {
|
||||
} from "@repo/rdx-ddd";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { CreateProformaItemRequestDTO, CreateProformaRequestDTO } from "../../../../common";
|
||||
import type { CreateProformaItemRequestDTO, CreateProformaRequestDTO } from "../../../../../common";
|
||||
import {
|
||||
type IProformaItemProps,
|
||||
type IProformaProps,
|
||||
InvoiceNumber,
|
||||
InvoicePaymentMethod,
|
||||
type InvoiceRecipient,
|
||||
@ -26,9 +27,8 @@ import {
|
||||
type IssuedInvoiceItemProps,
|
||||
ItemAmount,
|
||||
ItemDescription,
|
||||
ItemDiscountPercentage,
|
||||
ItemQuantity,
|
||||
} from "../../../domain";
|
||||
} from "../../../../domain";
|
||||
|
||||
/**
|
||||
* CreateProformaPropsMapper
|
||||
@ -41,7 +41,8 @@ import {
|
||||
*
|
||||
*/
|
||||
|
||||
export class CreateProformaPropsMapper {
|
||||
|
||||
export class CreateProformaRequestMapper {
|
||||
private readonly taxCatalog: JsonTaxCatalogProvider;
|
||||
private errors: ValidationErrorDetail[] = [];
|
||||
private languageCode?: LanguageCode;
|
||||
@ -52,7 +53,8 @@ export class CreateProformaPropsMapper {
|
||||
this.errors = [];
|
||||
}
|
||||
|
||||
public map(dto: CreateProformaRequestDTO, companyId: UniqueID) {
|
||||
public map(dto: CreateProformaRequestDTO, params: { companyId: UniqueID }) {
|
||||
const { companyId } = params;
|
||||
try {
|
||||
this.errors = [];
|
||||
|
||||
@ -60,8 +62,6 @@ export class CreateProformaPropsMapper {
|
||||
|
||||
const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", this.errors);
|
||||
|
||||
const isProforma = true;
|
||||
|
||||
const customerId = extractOrPushError(
|
||||
UniqueID.create(dto.customer_id),
|
||||
"customer_id",
|
||||
@ -132,7 +132,7 @@ export class CreateProformaPropsMapper {
|
||||
this.errors
|
||||
);
|
||||
|
||||
const discountPercentage = extractOrPushError(
|
||||
const globalDiscountPercentage = extractOrPushError(
|
||||
Percentage.create({
|
||||
value: Number(dto.discount_percentage.value),
|
||||
scale: Number(dto.discount_percentage.scale),
|
||||
@ -149,10 +149,8 @@ export class CreateProformaPropsMapper {
|
||||
);
|
||||
}
|
||||
|
||||
const proformaProps: IProformaProps = {
|
||||
const proformaProps: Omit<IProformaProps, "items"> & { items: IProformaItemProps[] } = {
|
||||
companyId,
|
||||
isProforma,
|
||||
proformaId: Maybe.none(),
|
||||
status: defaultStatus!,
|
||||
|
||||
invoiceNumber: proformaNumber!,
|
||||
@ -169,13 +167,13 @@ export class CreateProformaPropsMapper {
|
||||
notes: notes!,
|
||||
|
||||
languageCode: this.languageCode!,
|
||||
currencyCode: this.currencyCode!,
|
||||
|
||||
items: items,
|
||||
currencyCode: this.currencyCode!,
|
||||
|
||||
paymentMethod: paymentMethod!,
|
||||
|
||||
discountPercentage: discountPercentage!,
|
||||
globalDiscountPercentage: globalDiscountPercentage!,
|
||||
|
||||
items:
|
||||
};
|
||||
|
||||
return Result.ok({ id: proformaId!, props: proformaProps });
|
||||
@ -184,8 +182,8 @@ export class CreateProformaPropsMapper {
|
||||
}
|
||||
}
|
||||
|
||||
private mapItems(items: CreateProformaItemRequestDTO[]) {
|
||||
const invoiceItems = CustomerInvoiceItems.create({
|
||||
private mapItems(items: CreateProformaItemRequestDTO[]): IProformaItemProps[] {
|
||||
const proformaItems = CustomerInvoiceItems.create({
|
||||
currencyCode: this.currencyCode!,
|
||||
languageCode: this.languageCode!,
|
||||
items: [],
|
||||
@ -232,7 +230,7 @@ export class CreateProformaPropsMapper {
|
||||
|
||||
const itemResult = IssuedInvoiceItem.create(itemProps);
|
||||
if (itemResult.isSuccess) {
|
||||
invoiceItems.add(itemResult.data);
|
||||
proformaItems.add(itemResult.data);
|
||||
} else {
|
||||
this.errors.push({
|
||||
path: `items[${index}]`,
|
||||
@ -240,7 +238,7 @@ export class CreateProformaPropsMapper {
|
||||
});
|
||||
}
|
||||
});
|
||||
return invoiceItems;
|
||||
return proformaItems;
|
||||
}
|
||||
|
||||
private mapTaxes(item: CreateProformaItemRequestDTO, itemIndex: number) {
|
||||
@ -15,13 +15,13 @@ import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils";
|
||||
|
||||
import type { IProformaDomainMapper } from "../../../../../../application";
|
||||
import {
|
||||
type IProformaProps,
|
||||
InvoiceNumber,
|
||||
InvoicePaymentMethod,
|
||||
InvoiceSerie,
|
||||
InvoiceStatus,
|
||||
Proforma,
|
||||
ProformaItems,
|
||||
type ProformaProps,
|
||||
} from "../../../../../../domain";
|
||||
import type {
|
||||
CustomerInvoiceCreationAttributes,
|
||||
@ -217,7 +217,7 @@ export class SequelizeProformaDomainMapper
|
||||
items: itemsResults.data.getAll(),
|
||||
});
|
||||
|
||||
const invoiceProps: ProformaProps = {
|
||||
const invoiceProps: IProformaProps = {
|
||||
companyId: attributes.companyId!,
|
||||
|
||||
status: attributes.status!,
|
||||
|
||||
@ -16,15 +16,15 @@ import {
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import {
|
||||
type IProformaItemProps,
|
||||
type IProformaProps,
|
||||
ItemAmount,
|
||||
ItemDescription,
|
||||
ItemQuantity,
|
||||
type Proforma,
|
||||
ProformaItem,
|
||||
type ProformaItemProps,
|
||||
ProformaItemTaxes,
|
||||
type ProformaItemTaxesProps,
|
||||
type ProformaProps,
|
||||
} from "../../../../../../domain";
|
||||
import type {
|
||||
CustomerInvoiceItemCreationAttributes,
|
||||
@ -54,11 +54,11 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
|
||||
private mapAttributesToDomain(
|
||||
raw: CustomerInvoiceItemModel,
|
||||
params?: MapperParamsType
|
||||
): Partial<ProformaItemProps & ProformaItemTaxesProps> & { itemId?: UniqueID } {
|
||||
): Partial<IProformaItemProps & ProformaItemTaxesProps> & { itemId?: UniqueID } {
|
||||
const { errors, index, parent } = params as {
|
||||
index: number;
|
||||
errors: ValidationErrorDetail[];
|
||||
parent: Partial<ProformaProps>;
|
||||
parent: Partial<IProformaProps>;
|
||||
};
|
||||
|
||||
const itemId = extractOrPushError(
|
||||
@ -139,7 +139,7 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
|
||||
const { errors, index } = params as {
|
||||
index: number;
|
||||
errors: ValidationErrorDetail[];
|
||||
parent: Partial<ProformaProps>;
|
||||
parent: Partial<IProformaProps>;
|
||||
};
|
||||
|
||||
// 1) Valores escalares (atributos generales)
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
} from "@repo/rdx-ddd";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import { InvoiceRecipient, type ProformaProps } from "../../../../../../domain";
|
||||
import { type IProformaProps, InvoiceRecipient } from "../../../../../../domain";
|
||||
import type { CustomerInvoiceModel } from "../../../../../common";
|
||||
|
||||
export class SequelizeProformaRecipientDomainMapper {
|
||||
@ -28,7 +28,7 @@ export class SequelizeProformaRecipientDomainMapper {
|
||||
|
||||
const { errors, parent } = params as {
|
||||
errors: ValidationErrorDetail[];
|
||||
parent: Partial<ProformaProps>;
|
||||
parent: Partial<IProformaProps>;
|
||||
};
|
||||
|
||||
/* if (!source.current_customer) {
|
||||
|
||||
@ -9,22 +9,26 @@ 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 { IProformaRepository, ProformaListDTO } from "../../../../../application";
|
||||
import type {
|
||||
IProformaDomainMapper,
|
||||
IProformaListMapper,
|
||||
IProformaRepository,
|
||||
ProformaListDTO,
|
||||
} from "../../../../../application";
|
||||
import type { InvoiceStatus, Proforma } from "../../../../../domain";
|
||||
import {
|
||||
CustomerInvoiceItemModel,
|
||||
CustomerInvoiceModel,
|
||||
CustomerInvoiceTaxModel,
|
||||
} from "../../../../common";
|
||||
import type { SequelizeProformaDomainMapper, SequelizeProformaListMapper } from "../mappers";
|
||||
|
||||
export class ProformaRepository
|
||||
extends SequelizeRepository<Proforma>
|
||||
implements IProformaRepository
|
||||
{
|
||||
constructor(
|
||||
private readonly domainMapper: SequelizeProformaDomainMapper,
|
||||
private readonly listMapper: SequelizeProformaListMapper,
|
||||
private readonly domainMapper: IProformaDomainMapper,
|
||||
private readonly listMapper: IProformaListMapper,
|
||||
database: Sequelize
|
||||
) {
|
||||
super({ database });
|
||||
|
||||
@ -31,7 +31,8 @@
|
||||
"include": [
|
||||
"src",
|
||||
"../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"]
|
||||
}
|
||||
|
||||
@ -13,6 +13,8 @@
|
||||
"dev": "turbo dev",
|
||||
"dev:server": "turbo dev --filter=server",
|
||||
"dev:client": "turbo dev --filter=client",
|
||||
"lint": "turbo run lint",
|
||||
"lint:fix": "turbo run lint:fix",
|
||||
"format-and-lint": "biome check .",
|
||||
"format-and-lint:fix": "biome check . --write",
|
||||
"ui:add": "pnpm --filter @repo/shadcn-ui ui:add",
|
||||
@ -23,7 +25,10 @@
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.3.1",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
||||
"@typescript-eslint/parser": "^8.56.1",
|
||||
"change-case": "^5.4.4",
|
||||
"eslint": "^10.0.2",
|
||||
"inquirer": "^12.10.0",
|
||||
"plop": "^4.0.4",
|
||||
"rimraf": "^5.0.5",
|
||||
|
||||
624
pnpm-lock.yaml
624
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
29
turbo.json
29
turbo.json
@ -1,8 +1,16 @@
|
||||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"ui": "tui",
|
||||
"globalDependencies": ["**/.env.*local"],
|
||||
"globalDependencies": [
|
||||
"**/.env.*local"
|
||||
],
|
||||
"tasks": {
|
||||
"lint": {
|
||||
"outputs": []
|
||||
},
|
||||
"lint:fix": {
|
||||
"outputs": []
|
||||
},
|
||||
"dev": {
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
@ -13,16 +21,25 @@
|
||||
},
|
||||
"build": {
|
||||
"cache": false,
|
||||
"dependsOn": ["^build"],
|
||||
"inputs": ["$TURBO_DEFAULT$", ".env*"],
|
||||
"outputs": ["dist/**"]
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"inputs": [
|
||||
"$TURBO_DEFAULT$",
|
||||
".env*"
|
||||
],
|
||||
"outputs": [
|
||||
"dist/**"
|
||||
]
|
||||
},
|
||||
"build:templates": {
|
||||
"dependsOn": [],
|
||||
"outputs": ["dist/templates/**"]
|
||||
"outputs": [
|
||||
"dist/templates/**"
|
||||
]
|
||||
},
|
||||
"clean": {
|
||||
"cache": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user