Compare commits

...

2 Commits

Author SHA1 Message Date
936c440cf3 . 2026-03-29 22:46:29 +02:00
dd3f53c2b1 . 2026-03-29 21:00:07 +02:00
8 changed files with 122 additions and 45 deletions

View File

@ -96,14 +96,10 @@ export class IssuedInvoice
} }
static create(props: IIssuedInvoiceCreateProps, id?: UniqueID): Result<IssuedInvoice, Error> { static create(props: IIssuedInvoiceCreateProps, id?: UniqueID): Result<IssuedInvoice, Error> {
if (!props.recipient) { const validationResult = IssuedInvoice.validateCreateProps(props);
return Result.fail(
new DomainValidationError( if (validationResult.isFailure) {
"MISSING_RECIPIENT", return Result.fail(validationResult.error);
"recipient",
"Issued invoice requires recipient"
)
);
} }
const internalItems = IssuedInvoiceItems.create({ const internalItems = IssuedInvoiceItems.create({
@ -132,6 +128,20 @@ export class IssuedInvoice
return Result.ok(issuedInvoice); return Result.ok(issuedInvoice);
} }
private static validateCreateProps(props: IIssuedInvoiceCreateProps): Result<void, Error> {
if (!props.recipient) {
return Result.fail(
new DomainValidationError(
"MISSING_RECIPIENT",
"recipient",
"Issued invoice requires recipient"
)
);
}
return Result.ok();
}
// Rehidratación desde persistencia // Rehidratación desde persistencia
static rehydrate( static rehydrate(
props: InternalIssuedInvoiceProps, props: InternalIssuedInvoiceProps,

View File

@ -11,15 +11,15 @@ export type IssuedInvoiceItemsProps = {
}; };
export class IssuedInvoiceItems extends Collection<IssuedInvoiceItem> { export class IssuedInvoiceItems extends Collection<IssuedInvoiceItem> {
private _languageCode!: LanguageCode; private languageCode!: LanguageCode;
private _currencyCode!: CurrencyCode; private currencyCode!: CurrencyCode;
private _globalDiscountPercentage!: Percentage; private globalDiscountPercentage!: Percentage;
constructor(props: IssuedInvoiceItemsProps) { constructor(props: IssuedInvoiceItemsProps) {
super(props.items ?? []); super(props.items ?? []);
this._languageCode = props.languageCode; this.languageCode = props.languageCode;
this._currencyCode = props.currencyCode; this.currencyCode = props.currencyCode;
this._globalDiscountPercentage = props.globalDiscountPercentage; this.globalDiscountPercentage = props.globalDiscountPercentage;
} }
public static create(props: IssuedInvoiceItemsProps): IssuedInvoiceItems { public static create(props: IssuedInvoiceItemsProps): IssuedInvoiceItems {
@ -40,9 +40,9 @@ export class IssuedInvoiceItems extends Collection<IssuedInvoiceItem> {
// tiene el mismo "currencyCode" y "languageCode" que la colección de items. // tiene el mismo "currencyCode" y "languageCode" que la colección de items.
if ( if (
!( !(
this._languageCode.equals(item.languageCode) && this.languageCode.equals(item.languageCode) &&
this._currencyCode.equals(item.currencyCode) && this.currencyCode.equals(item.currencyCode) &&
this._globalDiscountPercentage.equals(item.globalDiscountPercentage) this.globalDiscountPercentage.equals(item.globalDiscountPercentage)
) )
) )
return false; return false;

View File

@ -1,6 +1,8 @@
import type { CurrencyCode, LanguageCode } from "@repo/rdx-ddd"; import type { CurrencyCode, LanguageCode } from "@repo/rdx-ddd";
import { Collection } from "@repo/rdx-utils"; import { Collection } from "@repo/rdx-utils";
import { InvoiceAmount } from "../../../common";
import type { IssuedInvoiceTax } from "./issued-invoice-tax.entity"; import type { IssuedInvoiceTax } from "./issued-invoice-tax.entity";
export type IssuedInvoiceTaxesProps = { export type IssuedInvoiceTaxesProps = {
@ -10,16 +12,59 @@ export type IssuedInvoiceTaxesProps = {
}; };
export class IssuedInvoiceTaxes extends Collection<IssuedInvoiceTax> { export class IssuedInvoiceTaxes extends Collection<IssuedInvoiceTax> {
private _languageCode!: LanguageCode; private languageCode!: LanguageCode;
private _currencyCode!: CurrencyCode; private currencyCode!: CurrencyCode;
constructor(props: IssuedInvoiceTaxesProps) { constructor(props: IssuedInvoiceTaxesProps) {
super(props.taxes ?? []); super(props.taxes ?? []);
this._languageCode = props.languageCode; this.languageCode = props.languageCode;
this._currencyCode = props.currencyCode; this.currencyCode = props.currencyCode;
} }
public static create(props: IssuedInvoiceTaxesProps): IssuedInvoiceTaxes { public static create(props: IssuedInvoiceTaxesProps): IssuedInvoiceTaxes {
return new IssuedInvoiceTaxes(props); return new IssuedInvoiceTaxes(props);
} }
public getTaxableAmount(): InvoiceAmount {
return this.items.reduce(
(acc, tax) => acc.add(tax.taxableAmount),
InvoiceAmount.zero(this.currencyCode.toString())
);
}
public getIvaAmount(): InvoiceAmount {
return this.items.reduce(
(acc, tax) => acc.add(tax.ivaAmount),
InvoiceAmount.zero(this.currencyCode.toString())
);
}
public getRecAmount(): InvoiceAmount {
return this.items.reduce(
(acc, tax) => acc.add(tax.recAmount),
InvoiceAmount.zero(this.currencyCode.toString())
);
}
public getRetentionAmount(): InvoiceAmount {
return this.items.reduce(
(acc, tax) => acc.add(tax.retentionAmount),
InvoiceAmount.zero(this.currencyCode.toString())
);
}
public getTaxesAmount(): InvoiceAmount {
return this.items.reduce(
(acc, tax) => acc.add(tax.taxesAmount),
InvoiceAmount.zero(this.currencyCode.toString())
);
}
public getTransferredTaxesAmount(): InvoiceAmount {
return this.getIvaAmount().add(this.getRecAmount());
}
public getNetTaxesAmount(): InvoiceAmount {
return this.getTransferredTaxesAmount().subtract(this.getRetentionAmount());
}
} }

View File

@ -116,6 +116,12 @@ export class Proforma extends AggregateRoot<InternalProformaProps> implements IP
// Creación funcional // Creación funcional
static create(props: IProformaCreateProps, id?: UniqueID): Result<Proforma, Error> { static create(props: IProformaCreateProps, id?: UniqueID): Result<Proforma, Error> {
const validationResult = Proforma.validateCreateProps(props);
if (validationResult.isFailure) {
return Result.fail(validationResult.error);
}
const internalItems = ProformaItems.create({ const internalItems = ProformaItems.create({
items: [], items: [],
languageCode: props.languageCode, languageCode: props.languageCode,
@ -141,6 +147,10 @@ export class Proforma extends AggregateRoot<InternalProformaProps> implements IP
return Result.ok(proforma); return Result.ok(proforma);
} }
private static validateCreateProps(props: IProformaCreateProps): Result<void, Error> {
return Result.ok();
}
// Rehidratación desde persistencia // Rehidratación desde persistencia
static rehydrate(props: InternalProformaProps, items: ProformaItems, id: UniqueID): Proforma { static rehydrate(props: InternalProformaProps, items: ProformaItems, id: UniqueID): Proforma {
return new Proforma(props, items, id); return new Proforma(props, items, id);
@ -237,15 +247,17 @@ export class Proforma extends AggregateRoot<InternalProformaProps> implements IP
} }
// Mutabilidad // Mutabilidad
public update( public update(patch: ProformaPatchProps): Result<Proforma, Error> {
partialProforma: Partial<Omit<IProformaCreateProps, "companyId">> const candidateProps: InternalProformaProps = {
): Result<Proforma, Error> {
const updatedProps = {
...this.props, ...this.props,
...partialProforma, ...patch,
} as IProformaCreateProps; };
return Proforma.create(updatedProps, this.id); // Validacciones
Object.assign(this.props, candidateProps);
return Result.ok();
} }
public issue(): Result<void, Error> { public issue(): Result<void, Error> {

View File

@ -99,6 +99,12 @@ type CustomerInternalProps = Omit<ICustomerCreateProps, "address" | "defaultTaxe
export class Customer extends AggregateRoot<CustomerInternalProps> implements ICustomer { export class Customer extends AggregateRoot<CustomerInternalProps> implements ICustomer {
static create(props: ICustomerCreateProps, id?: UniqueID): Result<Customer, Error> { static create(props: ICustomerCreateProps, id?: UniqueID): Result<Customer, Error> {
const validationResult = Customer.validateCreateProps(props);
if (validationResult.isFailure) {
return Result.fail(validationResult.error);
}
const { address, defaultTaxes, ...internalProps } = props; const { address, defaultTaxes, ...internalProps } = props;
const postalAddressResult = PostalAddress.create(address); const postalAddressResult = PostalAddress.create(address);
@ -132,6 +138,10 @@ export class Customer extends AggregateRoot<CustomerInternalProps> implements IC
return Result.ok(contact); return Result.ok(contact);
} }
private static validateCreateProps(props: ICustomerCreateProps): Result<void, Error> {
return Result.ok();
}
// Rehidratación desde persistencia // Rehidratación desde persistencia
static rehydrate(props: CustomerInternalProps, id: UniqueID): Customer { static rehydrate(props: CustomerInternalProps, id: UniqueID): Customer {
return new Customer(props, id); return new Customer(props, id);

View File

@ -1,16 +1,11 @@
import type { IModuleServer } from "@erp/core/api"; import type { IModuleServer } from "@erp/core/api";
import { type CustomerPublicServices, customersRouter, models } from "./infrastructure"; import type { ICustomerPublicServices } from "./application";
import { import { customersRouter, models } from "./infrastructure";
type CustomersInternalDeps, import { buildCustomerPublicServices, buildCustomersDependencies } from "./infrastructure/di";
buildCustomerPublicServices,
buildCustomersDependencies,
} from "./infrastructure/di";
export * from "./infrastructure/sequelize"; export * from "./infrastructure/sequelize";
export type { CustomerPublicServices };
export const customersAPIModule: IModuleServer = { export const customersAPIModule: IModuleServer = {
name: "customers", name: "customers",
version: "1.0.0", version: "1.0.0",
@ -30,7 +25,10 @@ export const customersAPIModule: IModuleServer = {
const internal = buildCustomersDependencies(params); const internal = buildCustomersDependencies(params);
// 2) Servicios públicos (Application Services) // 2) Servicios públicos (Application Services)
const customersServices: CustomerPublicServices = buildCustomerPublicServices(params, internal); const customersServices: ICustomerPublicServices = buildCustomerPublicServices(
params,
internal
);
logger.info("🚀 Customers module dependencies registered", { logger.info("🚀 Customers module dependencies registered", {
label: this.name, label: this.name,
@ -60,11 +58,8 @@ export const customersAPIModule: IModuleServer = {
async start(params) { async start(params) {
const { app, baseRoutePath, logger, getInternal } = params; const { app, baseRoutePath, logger, getInternal } = params;
// Recuperamos el dominio interno del módulo
const customersInternalDeps = getInternal<CustomersInternalDeps>("customers");
// Registro de rutas HTTP // Registro de rutas HTTP
customersRouter(params, customersInternalDeps); customersRouter(params);
logger.info("🚀 Customers module started", { logger.info("🚀 Customers module started", {
label: this.name, label: this.name,

View File

@ -1,5 +1,5 @@
import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth/api"; import { mockUser, requireAuthenticated, requireCompanyContext } from "@erp/auth/api";
import { type ModuleParams, type RequestWithAuth, validateRequest } from "@erp/core/api"; import { type RequestWithAuth, type StartParams, validateRequest } from "@erp/core/api";
import { type NextFunction, type Request, type Response, Router } from "express"; import { type NextFunction, type Request, type Response, Router } from "express";
import { import {
@ -18,11 +18,16 @@ import {
UpdateCustomerController, UpdateCustomerController,
} from "./controllers"; } from "./controllers";
export const customersRouter = (params: ModuleParams, deps: CustomersInternalDeps) => { export const customersRouter = (params: StartParams) => {
const { app, config } = params; const { app, config, getInternal } = params;
// Recuperamos el dominio interno del módulo
const deps = getInternal<CustomersInternalDeps>("customers");
const router: Router = Router({ mergeParams: true }); const router: Router = Router({ mergeParams: true });
// ----------------------------------------------
// 🔐 Autenticación + Tenancy para TODO el router // 🔐 Autenticación + Tenancy para TODO el router
if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "production") { if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "production") {
router.use( router.use(

View File

@ -46,7 +46,7 @@ export const factugesAPIModule: IModuleServer = {
* - NO construye dominio * - NO construye dominio
*/ */
async start(params) { async start(params) {
const { app, baseRoutePath, logger, getInternal, getService, listServices } = params; const { logger } = params;
// Registro de rutas HTTP // Registro de rutas HTTP
factugesRouter(params); factugesRouter(params);