Facturas de cliente

This commit is contained in:
David Arranz 2025-09-05 13:23:45 +02:00
parent b7a58ebad5
commit d40c2279bd
27 changed files with 362 additions and 189 deletions

View File

@ -5,7 +5,7 @@ import { Invoice } from "../aggregates";
export interface IInvoiceRepository { export interface IInvoiceRepository {
findAll(transaction?: any): Promise<Result<Collection<Invoice>, Error>>; findAll(transaction?: any): Promise<Result<Collection<Invoice>, Error>>;
getById(id: UniqueID, transaction?: any): Promise<Result<Invoice, Error>>; getById(id: UniqueID, transaction?: any): Promise<Result<Invoice, Error>>;
deleteById(id: UniqueID, transaction?: any): Promise<Result<boolean, Error>>; deleteByIdInCompany(id: UniqueID, transaction?: any): Promise<Result<boolean, Error>>;
create(invoice: Invoice, transaction?: any): Promise<void>; create(invoice: Invoice, transaction?: any): Promise<void>;
update(invoice: Invoice, transaction?: any): Promise<void>; update(invoice: Invoice, transaction?: any): Promise<void>;

View File

@ -7,6 +7,9 @@ import { IInvoiceService } from "./invoice-service.interface";
export class InvoiceService implements IInvoiceService { export class InvoiceService implements IInvoiceService {
constructor(private readonly repo: IInvoiceRepository) {} constructor(private readonly repo: IInvoiceRepository) {}
deleteInvoiceById(invoiceId: UniqueID, transaction?: any): Promise<Result<boolean, Error>> {
throw new Error("Method not implemented.");
}
async findInvoices(transaction?: Transaction): Promise<Result<Collection<Invoice>, Error>> { async findInvoices(transaction?: Transaction): Promise<Result<Collection<Invoice>, Error>> {
const invoicesOrError = await this.repo.findAll(transaction); const invoicesOrError = await this.repo.findAll(transaction);
@ -71,10 +74,11 @@ export class InvoiceService implements IInvoiceService {
return Result.ok(newInvoice); return Result.ok(newInvoice);
} }
async deleteInvoiceById( async deleteInvoiceByIdInCompnay(
companyId: UniqueID,
invoiceId: UniqueID, invoiceId: UniqueID,
transaction?: Transaction transaction?: Transaction
): Promise<Result<boolean, Error>> { ): Promise<Result<boolean, Error>> {
return this.repo.deleteById(invoiceId, transaction); return this.repo.deleteByIdInCompany(companyId, invoiceId, transaction);
} }
} }

View File

@ -1,15 +1,35 @@
import { Result } from "@repo/rdx-utils";
import { ILogger } from "../../logger";
import { ITransactionManager } from "./transaction-manager.interface"; import { ITransactionManager } from "./transaction-manager.interface";
export abstract class TransactionManager implements ITransactionManager { export abstract class TransactionManager implements ITransactionManager {
protected _transaction: any | null = null; protected _transaction: any | null = null;
protected _isCompleted = false;
protected readonly logger!: ILogger;
constructor(logger?: ILogger) {
// Si no hay logger, usa console adaptado
this.logger = logger ?? {
info: (msg, meta) => console.info(msg, meta),
warn: (msg, meta) => console.warn(msg, meta),
error: (msg, err) => console.error(msg, err),
debug: (msg, meta) => console.debug(msg, meta),
};
}
/** /**
* 🔹 Inicia una transacción si no hay una activa * 🔹 Inicia una transacción si no hay una activa
*/ */
async start(): Promise<void> { async start(): Promise<void> {
if (!this._transaction) { if (this._transaction) {
this._transaction = await this._startTransaction(); this.logger.error("❌ Transaction already started. Nested transactions are not allowed.", {
label: "TransactionManager.start",
});
throw new Error("A transaction is already active. Nested transactions are not allowed.");
} }
this._transaction = await this._startTransaction();
this._isCompleted = false;
this.logger.debug("Transaction started", { label: "TransactionManager.start" });
} }
/** /**
@ -17,8 +37,16 @@ export abstract class TransactionManager implements ITransactionManager {
*/ */
getTransaction(): any { getTransaction(): any {
if (!this._transaction) { if (!this._transaction) {
this.logger.error("❌ No active transaction. Call start() first.", {
label: "TransactionManager.getTransaction",
});
throw new Error("No active transaction. Call start() first."); throw new Error("No active transaction. Call start() first.");
} }
if (this._isCompleted) {
this.logger.error("❌ Transaction already completed.");
throw new Error("Transaction already completed.");
}
return this._transaction; return this._transaction;
} }
@ -26,13 +54,30 @@ export abstract class TransactionManager implements ITransactionManager {
* 🔹 Ejecuta una función dentro de una transacción * 🔹 Ejecuta una función dentro de una transacción
*/ */
async complete<T>(work: (transaction: any) => Promise<T>): Promise<T> { async complete<T>(work: (transaction: any) => Promise<T>): Promise<T> {
if (this._transaction) {
this.logger.error(
"❌ Cannot start a new transaction inside another. Nested transactions are not allowed.",
{ label: "TransactionManager.complete" }
);
throw new Error("A transaction is already active. Nested transactions are not allowed.");
}
await this.start(); await this.start();
try { try {
const result = await work(this.getTransaction()); const result = await work(this.getTransaction());
if (result instanceof Result && result.isFailure) {
throw result.error;
}
await this.commit(); await this.commit();
return result; return result;
} catch (error) { } catch (err) {
await this.rollback(); await this.rollback();
const error = err as Error;
this.logger.error(`❌ Transaction rolled back due to error: ${error.message}`, {
stack: error.stack,
label: "TransactionManager.start",
});
throw error; throw error;
} }
} }
@ -45,16 +90,59 @@ export abstract class TransactionManager implements ITransactionManager {
protected abstract _rollbackTransaction(): Promise<void>; protected abstract _rollbackTransaction(): Promise<void>;
async commit(): Promise<void> { async commit(): Promise<void> {
if (this._transaction) { if (!this._transaction) {
this.logger.error("❌ No transaction to commit.", { label: "TransactionManager.commit" });
throw new Error("No transaction to commit.");
}
if (this._isCompleted) {
this.logger.error("❌ Transaction already completed. Cannot commit again.", {
label: "TransactionManager.commit",
});
throw new Error("Transaction already completed.");
}
try {
await this._commitTransaction(); await this._commitTransaction();
this.logger.info("Transaction committed.", { label: "TransactionManager.commit" });
} catch (err) {
const error = err as Error;
this.logger.error(`❌ Error during commit: ${error.message}`, {
stack: error.stack,
label: "TransactionManager.commit",
});
throw error;
} finally {
// Siempre limpia el estado interno
this._transaction = null; this._transaction = null;
this._isCompleted = true;
} }
} }
async rollback(): Promise<void> { async rollback(): Promise<void> {
if (this._transaction) { if (!this._transaction) {
this.logger.error("❌ No transaction to rollback.", { label: "TransactionManager.rollback" });
throw new Error("No transaction to rollback.");
}
if (this._isCompleted) {
this.logger.error("❌ Transaction already completed. Cannot rollback again.", {
label: "TransactionManager.rollback",
});
throw new Error("Transaction already completed.");
}
try {
await this._rollbackTransaction(); await this._rollbackTransaction();
this.logger.info("Transaction rolled back.");
} catch (err) {
const error = err as Error;
this.logger.error(`❌ Error during rollback: ${error.message}`, {
stack: error.stack,
label: "TransactionManager.rollback",
});
throw error;
} finally {
this._transaction = null; this._transaction = null;
this._isCompleted = true;
} }
} }
} }

View File

@ -1,9 +1,17 @@
import { DatabaseError } from "sequelize";
import { InfrastructureError } from "./infrastructure-errors"; import { InfrastructureError } from "./infrastructure-errors";
export class InfrastructureRepositoryError extends InfrastructureError { export class InfrastructureRepositoryError extends InfrastructureError {
public readonly code = "REPOSITORY_ERROR" as const; public readonly code = "REPOSITORY_ERROR" as const;
constructor(message = "Repository operation failed", options?: ErrorOptions) { constructor(message = "Repository operation failed", options?: ErrorOptions) {
super(message, options); const { cause } = options || {};
let _message = message;
if (cause && cause instanceof DatabaseError) {
_message = `${message}: ${(cause as DatabaseError).message}`;
}
super(_message, options);
} }
} }

View File

@ -17,14 +17,14 @@ import { Result } from "@repo/rdx-utils";
import { CreateCustomerInvoiceRequestDTO } from "../../../common/dto"; import { CreateCustomerInvoiceRequestDTO } from "../../../common/dto";
import { import {
CustomerInvoiceItemDescription, CustomerInvoiceItemDescription,
CustomerInvoiceItemDiscount,
CustomerInvoiceItemProps, CustomerInvoiceItemProps,
CustomerInvoiceItemQuantity, CustomerInvoiceItemQuantity,
CustomerInvoiceItemUnitAmount,
CustomerInvoiceNumber, CustomerInvoiceNumber,
CustomerInvoiceProps, CustomerInvoiceProps,
CustomerInvoiceSerie, CustomerInvoiceSerie,
CustomerInvoiceStatus, CustomerInvoiceStatus,
ItemAmount,
ItemDiscount,
} from "../../domain"; } from "../../domain";
/** /**
@ -162,15 +162,13 @@ function mapDTOToCreateCustomerInvoiceItemsProps(
); );
const unitAmount = extractOrPushError( const unitAmount = extractOrPushError(
maybeFromNullableVO(item.unit_amount, (value) => CustomerInvoiceItemUnitAmount.create(value)), maybeFromNullableVO(item.unit_amount, (value) => ItemAmount.create(value)),
"unit_amount", "unit_amount",
errors errors
); );
const discountPercentage = extractOrPushError( const discountPercentage = extractOrPushError(
maybeFromNullableVO(item.discount_percentage, (value) => maybeFromNullableVO(item.discount_percentage, (value) => ItemDiscount.create(value)),
CustomerInvoiceItemDiscount.create(value)
),
"discount_percentage", "discount_percentage",
errors errors
); );

View File

@ -1,22 +1,23 @@
import { EntityNotFoundError, ITransactionManager } from "@erp/core/api"; import { EntityNotFoundError, ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { ICustomerInvoiceService } from "../../domain"; import { CustomerInvoiceService } from "../../domain";
type DeleteCustomerInvoiceUseCaseInput = { type DeleteCustomerInvoiceUseCaseInput = {
tenantId: string; companyId: UniqueID;
id: string; invoice_id: string;
}; };
export class DeleteCustomerInvoiceUseCase { export class DeleteCustomerInvoiceUseCase {
constructor( constructor(
private readonly service: ICustomerInvoiceService, private readonly service: CustomerInvoiceService,
private readonly transactionManager: ITransactionManager private readonly transactionManager: ITransactionManager
) {} ) {}
public execute(params: DeleteCustomerInvoiceUseCaseInput) { public execute(params: DeleteCustomerInvoiceUseCaseInput) {
const { id, tenantId } = params; const { invoice_id, companyId } = params;
const idOrError = UniqueID.create(id);
const idOrError = UniqueID.create(invoice_id);
if (idOrError.isFailure) { if (idOrError.isFailure) {
return Result.fail(idOrError.error); return Result.fail(idOrError.error);
@ -26,17 +27,25 @@ export class DeleteCustomerInvoiceUseCase {
return this.transactionManager.complete(async (transaction) => { return this.transactionManager.complete(async (transaction) => {
try { try {
const existsCheck = await this.service.existsById(invoiceId, transaction); const existsCheck = await this.service.existsByIdInCompany(
companyId,
invoiceId,
transaction
);
if (existsCheck.isFailure) { if (existsCheck.isFailure) {
return Result.fail(existsCheck.error); return Result.fail(existsCheck.error);
} }
if (!existsCheck.data) { const invoiceExists = existsCheck.data;
return Result.fail(new EntityNotFoundError("CustomerInvoice", "id", id.toString()));
if (!invoiceExists) {
return Result.fail(
new EntityNotFoundError("Customer invoice", "id", invoiceId.toString())
);
} }
return await this.service.deleteById(invoiceId, transaction); return await this.service.deleteInvoiceByIdInCompany(companyId, invoiceId, transaction);
} catch (error: unknown) { } catch (error: unknown) {
return Result.fail(error as Error); return Result.fail(error as Error);
} }

View File

@ -4,9 +4,9 @@ import { Result } from "@repo/rdx-utils";
import { import {
CustomerInvoiceItem, CustomerInvoiceItem,
CustomerInvoiceItemDescription, CustomerInvoiceItemDescription,
CustomerInvoiceItemDiscount,
CustomerInvoiceItemQuantity, CustomerInvoiceItemQuantity,
CustomerInvoiceItemUnitAmount, ItemAmount,
ItemDiscount,
} from "../../domain"; } from "../../domain";
import { extractOrPushError } from "./extract-or-push-error"; import { extractOrPushError } from "./extract-or-push-error";
import { hasNoUndefinedFields } from "./has-no-undefined-fields"; import { hasNoUndefinedFields } from "./has-no-undefined-fields";
@ -36,7 +36,7 @@ export function mapDTOToCustomerInvoiceItemsProps(
); );
const unitPrice = extractOrPushError( const unitPrice = extractOrPushError(
CustomerInvoiceItemUnitAmount.create({ ItemAmount.create({
value: item.unitPrice.amount, value: item.unitPrice.amount,
scale: item.unitPrice.scale, scale: item.unitPrice.scale,
currency_code: item.unitPrice.currency, currency_code: item.unitPrice.currency,
@ -46,7 +46,7 @@ export function mapDTOToCustomerInvoiceItemsProps(
); );
const discount = extractOrPushError( const discount = extractOrPushError(
CustomerInvoiceItemDiscount.create({ ItemDiscount.create({
value: item.discount.amount, value: item.discount.amount,
scale: item.discount.scale, scale: item.discount.scale,
}), }),

View File

@ -9,26 +9,50 @@ import {
import { Maybe, Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import { import {
CustomerInvoiceItemDescription, CustomerInvoiceItemDescription,
CustomerInvoiceItemDiscount, ItemAmount,
CustomerInvoiceItemQuantity, ItemDiscount,
CustomerInvoiceItemSubtotalAmount, ItemQuantity,
CustomerInvoiceItemTotalAmount,
CustomerInvoiceItemUnitAmount,
} from "../../value-objects"; } from "../../value-objects";
export interface CustomerInvoiceItemProps { export interface CustomerInvoiceItemProps {
description: Maybe<CustomerInvoiceItemDescription>; description: Maybe<CustomerInvoiceItemDescription>;
quantity: Maybe<CustomerInvoiceItemQuantity>; // Cantidad de unidades quantity: Maybe<ItemQuantity>; // Cantidad de unidades
unitAmount: Maybe<CustomerInvoiceItemUnitAmount>; // Precio unitario en la moneda de la factura unitAmount: Maybe<ItemAmount>; // Precio unitario en la moneda de la factura
discountPercentage: Maybe<CustomerInvoiceItemDiscount>; // % descuento discountPercentage: Maybe<ItemDiscount>; // % descuento
languageCode: LanguageCode; languageCode: LanguageCode;
currencyCode: CurrencyCode; currencyCode: CurrencyCode;
} }
export class CustomerInvoiceItem extends DomainEntity<CustomerInvoiceItemProps> { export interface ICustomerInvoiceItem {
private _subtotalAmount!: CustomerInvoiceItemSubtotalAmount; description: Maybe<CustomerInvoiceItemDescription>;
private _totalAmount!: CustomerInvoiceItemTotalAmount;
quantity: Maybe<ItemQuantity>; // Cantidad de unidades
unitAmount: Maybe<ItemAmount>; // Precio unitario en la moneda de la factura
subtotalAmount: ItemAmount;
discountPercentage: Maybe<ItemDiscount>; // % descuento
discountAmount: Maybe<ItemAmount>;
taxableAmount: ItemAmount;
taxesAmount: ItemAmount;
totalAmount: ItemAmount;
languageCode: LanguageCode;
currencyCode: CurrencyCode;
calculateSubtotal(): ItemAmount;
calculateTotal(): ItemAmount;
}
export class CustomerInvoiceItem
extends DomainEntity<CustomerInvoiceItemProps>
implements ICustomerInvoiceItem
{
private _subtotalAmount!: ItemAmount;
private _totalAmount!: ItemAmount;
public static create( public static create(
props: CustomerInvoiceItemProps, props: CustomerInvoiceItemProps,
@ -51,19 +75,16 @@ export class CustomerInvoiceItem extends DomainEntity<CustomerInvoiceItemProps>
return this.props.description; return this.props.description;
} }
get quantity(): Maybe<CustomerInvoiceItemQuantity> { get quantity(): Maybe<ItemQuantity> {
return this.props.quantity; return this.props.quantity;
} }
get unitAmount(): Maybe<CustomerInvoiceItemUnitAmount> { get unitAmount(): Maybe<ItemAmount> {
return this.props.unitAmount; return this.props.unitAmount;
} }
get subtotalAmount(): CustomerInvoiceItemSubtotalAmount { get subtotalAmount(): ItemAmount {
if (!this._subtotalAmount) { throw new Error("Not implemented");
this._subtotalAmount = this.calculateSubtotal();
}
return this._subtotalAmount;
} }
get discountPercentage(): Maybe<Percentage> { get discountPercentage(): Maybe<Percentage> {
@ -74,11 +95,16 @@ export class CustomerInvoiceItem extends DomainEntity<CustomerInvoiceItemProps>
throw new Error("Not implemented"); throw new Error("Not implemented");
} }
get totalAmount(): CustomerInvoiceItemTotalAmount { get taxableAmount(): ItemAmount {
if (!this._totalAmount) { throw new Error("Not implemented");
this._totalAmount = this.calculateTotal(); }
}
return this._totalAmount; get taxesAmount(): ItemAmount {
throw new Error("Not implemented");
}
get totalAmount(): ItemAmount {
throw new Error("Not implemented");
} }
public get languageCode(): LanguageCode { public get languageCode(): LanguageCode {
@ -89,15 +115,15 @@ export class CustomerInvoiceItem extends DomainEntity<CustomerInvoiceItemProps>
return this.props.currencyCode; return this.props.currencyCode;
} }
getValue(): CustomerInvoiceItemProps { getProps(): CustomerInvoiceItemProps {
return this.props; return this.props;
} }
toPrimitive() { toPrimitive() {
return this.getValue(); return this.getProps();
} }
calculateSubtotal(): CustomerInvoiceItemSubtotalAmount { calculateSubtotal(): ItemAmount {
throw new Error("Not implemented"); throw new Error("Not implemented");
/*const unitPrice = this.unitPrice.isSome() /*const unitPrice = this.unitPrice.isSome()
@ -106,7 +132,7 @@ export class CustomerInvoiceItem extends DomainEntity<CustomerInvoiceItemProps>
return this.unitPrice.multiply(this.quantity.toNumber()); // Precio unitario * Cantidad*/ return this.unitPrice.multiply(this.quantity.toNumber()); // Precio unitario * Cantidad*/
} }
calculateTotal(): CustomerInvoiceItemTotalAmount { calculateTotal(): ItemAmount {
throw new Error("Not implemented"); throw new Error("Not implemented");
//return this.subtotalPrice.subtract(this.subtotalPrice.percentage(this.discount.toNumber())); //return this.subtotalPrice.subtract(this.subtotalPrice.percentage(this.discount.toNumber()));
} }

View File

@ -128,7 +128,7 @@ export class CustomerInvoiceService {
* @param transaction - Transacción activa para la operación. * @param transaction - Transacción activa para la operación.
* @returns Result<boolean, Error> - Resultado de la operación. * @returns Result<boolean, Error> - Resultado de la operación.
*/ */
async deleteById( async deleteInvoiceByIdInCompany(
companyId: UniqueID, companyId: UniqueID,
invoiceId: UniqueID, invoiceId: UniqueID,
transaction?: Transaction transaction?: Transaction

View File

@ -1,3 +0,0 @@
import { Percentage } from "@repo/rdx-ddd";
export class CustomerInvoiceItemDiscount extends Percentage {}

View File

@ -1,14 +0,0 @@
import { MoneyValue, MoneyValueProps } from "@repo/rdx-ddd";
export class CustomerInvoiceItemSubtotalAmount extends MoneyValue {
public static DEFAULT_SCALE = 4;
static create({ value, currency_code }: MoneyValueProps) {
const props = {
value: Number(value),
scale: CustomerInvoiceItemSubtotalAmount.DEFAULT_SCALE,
currency_code,
};
return MoneyValue.create(props);
}
}

View File

@ -1,14 +0,0 @@
import { MoneyValue, MoneyValueProps } from "@repo/rdx-ddd";
export class ItemTotalAmount extends MoneyValue {
public static DEFAULT_SCALE = 4;
static create({ value, currency_code, scale }: MoneyValueProps) {
const props = {
value: Number(value),
scale: scale ?? MoneyValue.DEFAULT_SCALE,
currency_code,
};
return MoneyValue.create(props);
}
}

View File

@ -1,23 +0,0 @@
import { MoneyValue, MoneyValueProps } from "@repo/rdx-ddd";
export class CustomerInvoiceItemUnitAmount extends MoneyValue {
public static DEFAULT_SCALE = 4;
static create({ value: amount, currency_code, scale }: MoneyValueProps) {
const props = {
amount: Number(amount),
scale: scale ?? MoneyValue.DEFAULT_SCALE,
currency_code,
};
return MoneyValue.create(props);
}
static zero(currency_code: string, scale: number = CustomerInvoiceItemUnitAmount.DEFAULT_SCALE) {
const props: MoneyValueProps = {
value: 0,
scale,
currency_code,
};
return MoneyValue.create(props);
}
}

View File

@ -1,10 +1,8 @@
export * from "./customer-invoice-address-type"; export * from "./customer-invoice-address-type";
export * from "./customer-invoice-item-description"; export * from "./customer-invoice-item-description";
export * from "./customer-invoice-item-discount";
export * from "./customer-invoice-item-subtotal-amount";
export * from "./customer-invoice-item-total-amount";
export * from "./customer-invoice-item-unit-amount";
export * from "./customer-invoice-number"; export * from "./customer-invoice-number";
export * from "./customer-invoice-serie"; export * from "./customer-invoice-serie";
export * from "./customer-invoice-status"; export * from "./customer-invoice-status";
export * from "./item-amount";
export * from "./item-discount";
export * from "./item-quantity"; export * from "./item-quantity";

View File

@ -0,0 +1,24 @@
import { MoneyValue, MoneyValueProps } from "@repo/rdx-ddd";
type ItemAmountProps = Pick<MoneyValueProps, "value" | "currency_code">;
export class ItemAmount extends MoneyValue {
public static DEFAULT_SCALE = 4;
static create({ value, currency_code }: ItemAmountProps) {
const props = {
value: Number(value),
scale: ItemAmount.DEFAULT_SCALE,
currency_code,
};
return MoneyValue.create(props);
}
static zero(currency_code: string) {
const props = {
value: 0,
currency_code,
};
return ItemAmount.create(props);
}
}

View File

@ -0,0 +1,19 @@
import { Percentage, PercentageProps } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
type ItemDiscountProps = Pick<PercentageProps, "value">;
export class ItemDiscount extends Percentage {
static DEFAULT_SCALE = 2;
static create({ value }: ItemDiscountProps): Result<Percentage> {
return Percentage.create({
value,
scale: ItemDiscount.DEFAULT_SCALE,
});
}
static zero() {
return ItemDiscount.create({ value: 0 });
}
}

View File

@ -1,13 +1,18 @@
import { Quantity, QuantityProps } from "@repo/rdx-ddd"; import { Quantity, QuantityProps } from "@repo/rdx-ddd";
type ItemQuantityProps = Pick<QuantityProps, "value">;
export class ItemQuantity extends Quantity { export class ItemQuantity extends Quantity {
public static DEFAULT_SCALE = 2; public static DEFAULT_SCALE = 2;
static create({ value }: QuantityProps) { static create({ value }: ItemQuantityProps) {
const props = { return Quantity.create({
value: Number(value), value,
scale: ItemQuantity.DEFAULT_SCALE, scale: ItemQuantity.DEFAULT_SCALE,
}; });
return Quantity.create(props); }
static zero() {
return ItemQuantity.create({ value: 0 });
} }
} }

View File

@ -5,16 +5,22 @@ import {
ValidationErrorDetail, ValidationErrorDetail,
extractOrPushError, extractOrPushError,
} from "@erp/core/api"; } from "@erp/core/api";
import { CurrencyCode, LanguageCode, Quantity, UniqueID, maybeFromNullableVO, toNullable } from "@repo/rdx-ddd"; import {
CurrencyCode,
LanguageCode,
UniqueID,
maybeFromNullableVO,
toNullable,
} from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { InferCreationAttributes } from "sequelize"; import { InferCreationAttributes } from "sequelize";
import { import {
CustomerInvoice, CustomerInvoice,
CustomerInvoiceItem, CustomerInvoiceItem,
CustomerInvoiceItemDescription, CustomerInvoiceItemDescription,
CustomerInvoiceItemDiscount, ItemAmount,
CustomerInvoiceItemQuantity, ItemDiscount,
CustomerInvoiceItemUnitAmount, ItemQuantity,
} from "../../domain"; } from "../../domain";
import { import {
CustomerInvoiceItemCreationAttributes, CustomerInvoiceItemCreationAttributes,
@ -48,17 +54,17 @@ export class CustomerInvoiceItemMapper
const itemId = extractOrPushError(UniqueID.create(source.item_id), "item_id", errors); const itemId = extractOrPushError(UniqueID.create(source.item_id), "item_id", errors);
const languageCode = extractOrPushError( const languageCode = extractOrPushError(
LanguageCode.create(sourceParent.language_code), LanguageCode.create(sourceParent.language_code),
"language_code", "language_code",
errors errors
); );
const currencyCode = extractOrPushError( const currencyCode = extractOrPushError(
CurrencyCode.create(sourceParent.currency_code), CurrencyCode.create(sourceParent.currency_code),
"currency_code", "currency_code",
errors errors
); );
const description = extractOrPushError( const description = extractOrPushError(
maybeFromNullableVO(source.description, (value) => maybeFromNullableVO(source.description, (value) =>
@ -69,36 +75,27 @@ export class CustomerInvoiceItemMapper
); );
const quantity = extractOrPushError( const quantity = extractOrPushError(
maybeFromNullableVO(source.quantity_value, (value) => maybeFromNullableVO(source.quantity_value, (value) => ItemQuantity.create({ value })),
Quantity.create({ value, })
CustomerInvoiceItemQuantity.create({
value: source.quantity_amouwnt,
scale: source.quantity_scale,
}),
"discount_percentage", "discount_percentage",
errors errors
); );
const unitAmount = extractOrPushError( const unitAmount = extractOrPushError(
CustomerInvoiceItemUnitAmount.create({ maybeFromNullableVO(source.unit_amount_value, (value) =>
value: source.unit_price_amount, ItemAmount.create({ value, currency_code: currencyCode!.code })
scale: source.unit_price_scale, ),
}), "unit_amount",
"discount_percentage",
errors errors
); );
const discountPercentage = extractOrPushError( const discountPercentage = extractOrPushError(
CustomerInvoiceItemDiscount.create({ maybeFromNullableVO(source.discount_percentage_value, (value) =>
value: source.discount_amount, ItemDiscount.create({ value })
scale: source.discount_scale, ),
}),
"discount_percentage", "discount_percentage",
errors errors
); );
// Creación del objeto de dominio // Creación del objeto de dominio
return CustomerInvoiceItem.create( return CustomerInvoiceItem.create(
{ {
@ -108,8 +105,8 @@ export class CustomerInvoiceItemMapper
quantity: quantity!, quantity: quantity!,
unitAmount: unitAmount!, unitAmount: unitAmount!,
discountPercentage: discountPercentage!, discountPercentage: discountPercentage!,
},º },
id itemId
); );
} }
@ -117,14 +114,12 @@ export class CustomerInvoiceItemMapper
source: CustomerInvoiceItem, source: CustomerInvoiceItem,
params?: MapperParamsType params?: MapperParamsType
): InferCreationAttributes<CustomerInvoiceItemModel, {}> { ): InferCreationAttributes<CustomerInvoiceItemModel, {}> {
1 1;
const { index, sourceParent } = params as { const { index, sourceParent } = params as {
index: number; index: number;
sourceParent: CustomerInvoice; sourceParent: CustomerInvoice;
}; };
return { return {
item_id: source.id.toPrimitive(), item_id: source.id.toPrimitive(),
invoice_id: sourceParent.id.toPrimitive(), invoice_id: sourceParent.id.toPrimitive(),
@ -135,23 +130,41 @@ export class CustomerInvoiceItemMapper
quantity_value: toNullable(source.quantity, (value) => value.toPrimitive().value), quantity_value: toNullable(source.quantity, (value) => value.toPrimitive().value),
quantity_scale: source.quantity.match( quantity_scale: source.quantity.match(
(value) => value.toPrimitive().scale, (value) => value.toPrimitive().scale,
() => CustomerInvoiceItemQuantity.DEFAULT_SCALE), () => ItemQuantity.DEFAULT_SCALE
),
unit_amount_value: toNullable(source.unitAmount, (value) => value.toPrimitive().value), unit_amount_value: toNullable(source.unitAmount, (value) => value.toPrimitive().value),
unit_amount_scale: source.unitAmount.match( unit_amount_scale: source.unitAmount.match(
(value) => value.toPrimitive().scale, (value) => value.toPrimitive().scale,
() => CustomerInvoiceItemUnitAmount.DEFAULT_SCALE), () => ItemAmount.DEFAULT_SCALE
),
subtotal_amount_value: source.subtotalAmount.toPrimitive().value, subtotal_amount_value: source.subtotalAmount.toPrimitive().value,
subtotal_amount_scale: source.subtotalAmount.toPrimitive().scale, subtotal_amount_scale: source.subtotalAmount.toPrimitive().scale,
discount_percentage_value: toNullable(source.discountPercentage, (value) => value.toPrimitive().value), discount_percentage_value: toNullable(
source.discountPercentage,
(value) => value.toPrimitive().value
),
discount_percentage_scale: source.discountPercentage.match( discount_percentage_scale: source.discountPercentage.match(
(value) => value.toPrimitive().scale, (value) => value.toPrimitive().scale,
() => CustomerInvoiceItemUnitAmount.DEFAULT_SCALE), () => ItemAmount.DEFAULT_SCALE
),
discount_amount_value: source.subtotalAmount.toPrimitive().value, discount_amount_value: toNullable(
discount_amount_scale: source.subtotalAmount.toPrimitive().scale, source.discountAmount,
(value) => value.toPrimitive().value
),
discount_amount_scale: source.discountAmount.match(
(value) => value.toPrimitive().scale,
() => ItemDiscount.DEFAULT_SCALE
),
taxable_amount_value: source.taxableAmount.toPrimitive().value,
taxable_amount_scale: source.taxableAmount.toPrimitive().value,
taxes_amount_value: source.taxesAmount.toPrimitive().value,
taxes_amount_scale: source.taxesAmount.toPrimitive().value,
total_amount_value: source.totalAmount.toPrimitive().value, total_amount_value: source.totalAmount.toPrimitive().value,
total_amount_scale: source.totalAmount.toPrimitive().scale, total_amount_scale: source.totalAmount.toPrimitive().scale,

View File

@ -14,10 +14,12 @@ import {
UniqueID, UniqueID,
UtcDate, UtcDate,
maybeFromNullableVO, maybeFromNullableVO,
toNullable,
} from "@repo/rdx-ddd"; } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { import {
CustomerInvoice, CustomerInvoice,
CustomerInvoiceItems,
CustomerInvoiceNumber, CustomerInvoiceNumber,
CustomerInvoiceProps, CustomerInvoiceProps,
CustomerInvoiceSerie, CustomerInvoiceSerie,
@ -149,7 +151,10 @@ export class CustomerInvoiceMapper
discountPercentage: discountPercentage!, discountPercentage: discountPercentage!,
items: items!, items: CustomerInvoiceItems.create({
languageCode: languageCode!,
currencyCode: currencyCode!,
}),
}; };
return CustomerInvoice.create(invoiceProps, invoiceId); return CustomerInvoice.create(invoiceProps, invoiceId);
@ -162,26 +167,41 @@ export class CustomerInvoiceMapper
source: CustomerInvoice, source: CustomerInvoice,
params?: MapperParamsType params?: MapperParamsType
): CustomerInvoiceCreationAttributes { ): CustomerInvoiceCreationAttributes {
const subtotal = source.calculateSubtotal(); //const subtotal = source.calculateSubtotal();
const total = source.calculateTotal(); //const total = source.calculateTotal();
const items = this._itemsMapper.mapCollectionToPersistence(source.items, params); const items = this._itemsMapper.mapCollectionToPersistence(source.items, params);
return { return {
id: source.id.toString(), id: source.id.toPrimitive(),
invoice_status: source.status.toPrimitive(), company_id: source.companyId.toPrimitive(),
invoice_series: source.invoiceSeries.toPrimitive(),
status: source.status.toPrimitive(),
series: toNullable(source.series, (series) => series.toPrimitive()),
invoice_number: source.invoiceNumber.toPrimitive(), invoice_number: source.invoiceNumber.toPrimitive(),
invoice_date: source.invoiceDate.toPrimitive(), invoice_date: source.invoiceDate.toPrimitive(),
operation_date: source.operationDate.toPrimitive(),
invoice_language: "es",
invoice_currency: source.currency || "EUR",
subtotal_amount: subtotal.amount, operation_date: toNullable(source.operationDate, (operationDate) =>
subtotal_scale: subtotal.scale, operationDate.toPrimitive()
),
total_amount: total.amount, notes: toNullable(source.notes, (notes) => notes.toPrimitive()),
total_scale: total.scale,
language_code: source.languageCode.code,
currency_code: source.currencyCode.code,
subtotal_amount_value: 0, //subtotal.amount,
subtotal_amount_scale: 2, //subtotal.scale,
/*discount_percentage_value: source.discountPercentage.value,
discount_percentage_scale: source.discountPercentage.scale,
discount_amount_value: source.discountAmount.value,
discount_amount_scale: source.discountAmount.scale,*/
total_amount_value: 0, //total.amount,
total_amount_scale: 2, //total.scale,
items, items,
}; };

View File

@ -17,7 +17,7 @@ export class CustomerInvoiceItemTaxesModel extends Model<
InferAttributes<CustomerInvoiceItemTaxesModel>, InferAttributes<CustomerInvoiceItemTaxesModel>,
InferCreationAttributes<CustomerInvoiceItemTaxesModel> InferCreationAttributes<CustomerInvoiceItemTaxesModel>
> { > {
declare id: string; declare tax_id: string;
declare item_id: string; declare item_id: string;
declare tax_code: string; //"iva_21" declare tax_code: string; //"iva_21"
@ -38,7 +38,7 @@ export class CustomerInvoiceItemTaxesModel extends Model<
CustomerInvoiceItemTaxesModel.belongsTo(CustomerInvoiceItemModel, { CustomerInvoiceItemTaxesModel.belongsTo(CustomerInvoiceItemModel, {
as: "item", as: "item",
targetKey: "id", targetKey: "item_id",
foreignKey: "item_id", foreignKey: "item_id",
onDelete: "CASCADE", onDelete: "CASCADE",
}); });
@ -50,7 +50,7 @@ export class CustomerInvoiceItemTaxesModel extends Model<
export default (database: Sequelize) => { export default (database: Sequelize) => {
CustomerInvoiceItemTaxesModel.init( CustomerInvoiceItemTaxesModel.init(
{ {
id: { tax_id: {
type: new DataTypes.UUID(), type: new DataTypes.UUID(),
primaryKey: true, primaryKey: true,
}, },

View File

@ -195,10 +195,7 @@ export default (database: Sequelize) => {
underscored: true, underscored: true,
indexes: [ indexes: [{ name: "invoice_idx", fields: ["invoice_id"], unique: false }],
{ name: "invoice_idx", fields: ["invoice_id"], unique: false },
{ name: "parent_idx", fields: ["parent_id"], unique: false },
],
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope whereMergeStrategy: "and", // <- cómo tratar el merge de un scope

View File

@ -17,7 +17,7 @@ export class CustomerInvoiceTaxesModel extends Model<
InferAttributes<CustomerInvoiceTaxesModel>, InferAttributes<CustomerInvoiceTaxesModel>,
InferCreationAttributes<CustomerInvoiceTaxesModel> InferCreationAttributes<CustomerInvoiceTaxesModel>
> { > {
declare id: string; declare tax_id: string;
declare invoice_id: string; declare invoice_id: string;
declare tax_code: string; //"iva_21" declare tax_code: string; //"iva_21"
@ -50,7 +50,7 @@ export class CustomerInvoiceTaxesModel extends Model<
export default (database: Sequelize) => { export default (database: Sequelize) => {
CustomerInvoiceTaxesModel.init( CustomerInvoiceTaxesModel.init(
{ {
id: { tax_id: {
type: new DataTypes.UUID(), type: new DataTypes.UUID(),
primaryKey: true, primaryKey: true,
}, },

View File

@ -1,9 +1,19 @@
import customerInvoiceItemTaxesModelInit from "./customer-invoice-item-taxes.model";
import customerInvoiceItemModelInit from "./customer-invoice-item.model"; import customerInvoiceItemModelInit from "./customer-invoice-item.model";
import customerInvoiceTaxesModelInit from "./customer-invoice-taxes.model";
import customerInvoiceModelInit from "./customer-invoice.model"; import customerInvoiceModelInit from "./customer-invoice.model";
export * from "./customer-invoice-item.model"; // exporta las clases, tipos export * from "./customer-invoice-item-taxes.model";
export * from "./customer-invoice-item.model";
export * from "./customer-invoice-taxes.model";
export * from "./customer-invoice.model"; export * from "./customer-invoice.model";
export * from "./customer-invoice.repository"; export * from "./customer-invoice.repository";
// Array de inicializadores para que registerModels() lo use // Array de inicializadores para que registerModels() lo use
export const models = [customerInvoiceItemModelInit, customerInvoiceModelInit]; export const models = [
customerInvoiceModelInit,
customerInvoiceItemModelInit,
customerInvoiceTaxesModelInit,
customerInvoiceItemTaxesModelInit,
];

View File

@ -38,6 +38,10 @@ export class CurrencyCode extends ValueObject<CurrencyCodeProps> {
return Result.ok(new CurrencyCode({ value: valueIsValid.data })); return Result.ok(new CurrencyCode({ value: valueIsValid.data }));
} }
get code(): string {
return this.props.value;
}
getProps(): string { getProps(): string {
return this.props.value; return this.props.value;
} }

View File

@ -38,6 +38,10 @@ export class LanguageCode extends ValueObject<LanguageCodeProps> {
return Result.ok(new LanguageCode({ value: valueIsValid.data })); return Result.ok(new LanguageCode({ value: valueIsValid.data }));
} }
get code(): string {
return this.props.value;
}
getProps(): string { getProps(): string {
return this.props.value; return this.props.value;
} }

View File

@ -34,6 +34,6 @@ export class TextValue extends ValueObject<TextValueProps> {
} }
toPrimitive(): string { toPrimitive(): string {
return this.getProps(); return String(this.getProps());
} }
} }

View File

@ -37,10 +37,10 @@ export class UniqueID extends ValueObject<string> {
} }
toString(): string { toString(): string {
return this.getProps(); return String(this.getProps());
} }
toPrimitive() { toPrimitive() {
return this.getProps(); return this.toString();
} }
} }