Facturas de cliente

This commit is contained in:
David Arranz 2025-09-03 20:04:09 +02:00
parent db4a79422e
commit 5064494b12
35 changed files with 444 additions and 219 deletions

View File

@ -0,0 +1,23 @@
import * as z from "zod/v4";
import { AmountBaseSchema } from "./base.schemas";
/**
Esquema del DTO para valores monetarios con/sin código de moneda.
No aplica defaults ni correciones: solo valida.
- Con moneda -> "Money"
- Sin moneda -> "Amount"
*/
// 🔹 Con moneda
export const MoneySchema = AmountBaseSchema.extend({
currency_code: z.string(),
});
// 🔹 Sin moneda
export const AmountSchema = AmountBaseSchema;
// 🔹 Tipos DTO
export type MoneyDTO = z.infer<typeof MoneySchema>;
export type AmountDTO = z.infer<typeof AmountSchema>;

View File

@ -0,0 +1,19 @@
import * as z from "zod/v4";
/**
* Cadena con valor numérico:
*
* - Acepta: "" o "123456" (solo dígitos).
* - Rechaza: "1 23", "abc123", "12v34", "+123", "12.3"
*
* */
export const NumericStringSchema = z
.string()
.regex(/^\d$/, { message: "Must be empty or contain only digits (0-9)." });
// Cantidad de dinero (base): solo para la cantidad y la escala, sin moneda
export const AmountBaseSchema = z.object({
amount: NumericStringSchema,
scale: NumericStringSchema,
});

View File

@ -1,8 +1,8 @@
import * as z from "zod/v4";
/**
Esquema del objeto normalizado esperado por Criteria.fromPrimitives(...)
No aplica defaults ni correciones: solo valida.
Esquema del DTO para Criteria.fromPrimitives(...)
No aplica defaults ni correciones: solo valida.
*/
export const FilterPrimitiveSchema = z.object({
// Campos mínimos ya normalizados por el conversor

View File

@ -1,8 +1,9 @@
export * from "./amount-money.dto";
export * from "./base.schemas";
export * from "./critera.dto";
export * from "./error.dto";
export * from "./list-view.response.dto";
export * from "./metadata.dto";
export * from "./money.dto";
export * from "./percentage.dto";
export * from "./quantity.dto";
export * from "./tax-type.dto";

View File

@ -1,5 +0,0 @@
export type MoneyDTO = {
amount: number | null;
scale: number;
currency_code: string;
};

View File

@ -1,4 +1,14 @@
export type IPercentageDTO = {
amount: number | null;
scale: number;
};
import * as z from "zod/v4";
import { NumericStringSchema } from "./base.schemas";
/**
Esquema del DTO para valores de porcentajes.
No aplica defaults ni correciones: solo valida.
*/
export const PercentageSchema = z.object({
amount: NumericStringSchema,
scale: NumericStringSchema,
});
export type PercentageDTO = z.infer<typeof PercentageSchema>;

View File

@ -1,4 +1,14 @@
export type IQuantityDTO = {
amount: number | null;
scale: number;
};
import * as z from "zod/v4";
import { NumericStringSchema } from "./base.schemas";
/**
Esquema del DTO para valores de cantidades.
No aplica defaults ni correciones: solo valida.
*/
export const QuantitySchema = z.object({
amount: NumericStringSchema,
scale: NumericStringSchema,
});
export type QuantityDTO = z.infer<typeof QuantitySchema>;

View File

@ -1,11 +1,11 @@
import { IPercentageDTO } from "./percentage.dto";
import { PercentageDTO } from "./percentage.dto";
export interface ITaxTypeDTO {
id: string;
typecode: string;
taxslug: string;
taxrate: IPercentageDTO;
equivalencesurcharge: IPercentageDTO;
taxrate: PercentageDTO;
equivalencesurcharge: PercentageDTO;
}
export interface ITaxTypeResponseDTO extends ITaxTypeDTO {}

View File

@ -0,0 +1,48 @@
import { GetCustomerInvoiceByIdResponseDTO } from "@erp/customer-invoices/common/dto";
import { toEmptyString } from "@repo/rdx-ddd";
import { CustomerInvoice } from "../../../domain";
type GetCustomerInvoiceItemsByInvoiceIdResponseDTO = GetCustomerInvoiceByIdResponseDTO["items"];
export class GetCustomerInvoiceItemsAssembler {
toDTO(invoice: CustomerInvoice): GetCustomerInvoiceItemsByInvoiceIdResponseDTO {
const { items } = invoice;
return items.map((item, index) => ({
//id: item.
position: index,
description: toEmptyString(item.description, (value) => value.toPrimitive()),
quantity: item.quantity.match(
(quantity) => {
const { amount, scale } = quantity.toPrimitive();
return { amount: amount.toString(), scale: scale.toString() };
},
() => ({ amount: "", scale: "" })
),
unit_price_amount: item.unitAmount.match(
(unitPrice) => {
const { amount, scale } = unitPrice.toPrimitive();
return { amount: amount.toString(), scale: scale.toString() };
},
() => ({ amount: "", scale: "" })
),
discount: item.discount.match(
(discount) => {
const { amount, scale } = discount.toPrimitive();
return { amount: amount.toString(), scale: scale.toString() };
},
() => ({ amount: "", scale: "" })
),
total_amount: item.totalPrice.match(
(discount) => {
const { amount, scale } = discount.toPrimitive();
return { amount: amount.toString(), scale: scale.toString() };
},
() => ({ amount: "", scale: "" })
),
}));
}
}

View File

@ -1,18 +1,29 @@
import { GetCustomerInvoiceByIdResponseDTO } from "@erp/customer-invoices/common/dto";
import { toEmptyString } from "@repo/rdx-ddd";
import { CustomerInvoice } from "../../../domain";
export class GetCustomerInvoiceAssembler {
toDTO(customerInvoice: CustomerInvoice): GetCustomerInvoiceByIdResponseDTO {
return {
id: customerInvoice.id.toPrimitive(),
private _itemsAssembler!: GetCu;
constructor() {}
invoice_status: customerInvoice.status.toString(),
invoice_number: customerInvoice.invoiceNumber.toString(),
invoice_series: customerInvoice.invoiceSeries.toString(),
issue_date: customerInvoice.issueDate.toDateString(),
operation_date: customerInvoice.operationDate.toDateString(),
language_code: "ES",
currency: customerInvoice.currency,
public toDTO(invoice: CustomerInvoice): GetCustomerInvoiceByIdResponseDTO {
//const items = invoice.items.
return {
id: invoice.id.toPrimitive(),
company_id: invoice.companyId.toPrimitive(),
invoice_number: invoice.invoiceNumber.toString(),
status: invoice.status.toPrimitive(),
series: invoice.series.toString(),
issue_date: invoice.issueDate.toDateString(),
operation_date: toEmptyString(invoice.operationDate, (value) => value.toDateString()),
notes: toEmptyString(invoice.notes, (value) => value.toPrimitive()),
language_code: invoice.languageCode.toPrimitive(),
currency_code: invoice.currencyCode.toPrimitive(),
metadata: {
entity: "customer-invoices",

View File

@ -1,32 +1,39 @@
import { ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { ICustomerInvoiceService } from "../../domain";
import { GetCustomerInvoiceAssembler } from "./assembler";
import { CustomerInvoiceService } from "../../domain";
import { GetCustomerInvoiceItemsAssembler } from "./assembler";
type GetCustomerInvoiceUseCaseInput = {
tenantId: string;
id: string;
companyId: UniqueID;
invoice_id: string;
};
export class GetCustomerInvoiceUseCase {
constructor(
private readonly service: ICustomerInvoiceService,
private readonly service: CustomerInvoiceService,
private readonly transactionManager: ITransactionManager,
private readonly assembler: GetCustomerInvoiceAssembler
private readonly assembler: GetCustomerInvoiceItemsAssembler
) {}
public execute(params: GetCustomerInvoiceUseCaseInput) {
const { id, tenantId } = params;
const idOrError = UniqueID.create(id);
const { invoice_id, companyId } = params;
const idOrError = UniqueID.create(invoice_id);
if (idOrError.isFailure) {
return Result.fail(idOrError.error);
}
const invoiceId = idOrError.data;
return this.transactionManager.complete(async (transaction) => {
try {
const invoiceOrError = await this.service.getById(idOrError.data, transaction);
const invoiceOrError = await this.service.getInvoiceByIdInCompany(
companyId,
invoiceId,
transaction
);
if (invoiceOrError.isFailure) {
return Result.fail(invoiceOrError.error);
}

View File

@ -6,7 +6,7 @@ import {
CustomerInvoiceItemDescription,
CustomerInvoiceItemDiscount,
CustomerInvoiceItemQuantity,
CustomerInvoiceItemUnitPrice,
CustomerInvoiceItemUnitAmount,
} from "../../domain";
import { extractOrPushError } from "./extract-or-push-error";
import { hasNoUndefinedFields } from "./has-no-undefined-fields";
@ -36,7 +36,7 @@ export function mapDTOToCustomerInvoiceItemsProps(
);
const unitPrice = extractOrPushError(
CustomerInvoiceItemUnitPrice.create({
CustomerInvoiceItemUnitAmount.create({
amount: item.unitPrice.amount,
scale: item.unitPrice.scale,
currency_code: item.unitPrice.currency,

View File

@ -2,6 +2,8 @@ import {
AggregateRoot,
CurrencyCode,
LanguageCode,
MoneyValue,
Percentage,
TextValue,
UniqueID,
UtcDate,
@ -16,9 +18,10 @@ import {
export interface CustomerInvoiceProps {
companyId: UniqueID;
invoiceNumber: CustomerInvoiceNumber;
status: CustomerInvoiceStatus;
series: Maybe<CustomerInvoiceSerie>;
invoiceNumber: CustomerInvoiceNumber;
issueDate: UtcDate;
operationDate: Maybe<UtcDate>;
@ -40,6 +43,16 @@ export interface CustomerInvoiceProps {
languageCode: LanguageCode;
currencyCode: CurrencyCode;
subtotalAmount: MoneyValue;
discountPercentage: Percentage;
//discountAmount: MoneyValue;
//taxableAmount: MoneyValue;
taxAmount: MoneyValue;
totalAmount: MoneyValue;
//customer?: CustomerInvoiceCustomer;
items?: CustomerInvoiceItems;
}
@ -82,6 +95,10 @@ export class CustomerInvoice extends AggregateRoot<CustomerInvoiceProps> {
return this.props.companyId;
}
public get status(): CustomerInvoiceStatus {
return this.props.status;
}
public get series(): Maybe<CustomerInvoiceSerie> {
return this.props.series;
}
@ -110,8 +127,32 @@ export class CustomerInvoice extends AggregateRoot<CustomerInvoiceProps> {
return this.props.currencyCode;
}
public get subtotalAmount(): MoneyValue {
return this.props.subtotalAmount;
}
public get discountPercentage(): Percentage {
return this.props.discountPercentage;
}
public get discountAmount(): MoneyValue {
throw new Error("discountAmount not implemented");
}
public get taxableAmount(): MoneyValue {
throw new Error("taxableAmount not implemented");
}
public get taxAmount(): MoneyValue {
return this.props.taxAmount;
}
public get totalAmount(): MoneyValue {
throw new Error("totalAmount not implemented");
}
// Method to get the complete list of line items
get lineItems(): CustomerInvoiceItems {
get items(): CustomerInvoiceItems {
return this._items;
}

View File

@ -4,15 +4,15 @@ import {
CustomerInvoiceItemDescription,
CustomerInvoiceItemDiscount,
CustomerInvoiceItemQuantity,
CustomerInvoiceItemSubtotalPrice,
CustomerInvoiceItemTotalPrice,
CustomerInvoiceItemUnitPrice,
CustomerInvoiceItemSubtotalAmount,
CustomerInvoiceItemTotalAmount,
CustomerInvoiceItemUnitAmount,
} from "../../value-objects";
export interface CustomerInvoiceItemProps {
description: Maybe<CustomerInvoiceItemDescription>;
quantity: Maybe<CustomerInvoiceItemQuantity>; // Cantidad de unidades
unitPrice: Maybe<CustomerInvoiceItemUnitPrice>; // Precio unitario en la moneda de la factura
unitPrice: Maybe<CustomerInvoiceItemUnitAmount>; // Precio unitario en la moneda de la factura
discount: Maybe<CustomerInvoiceItemDiscount>; // % descuento
languageCode: LanguageCode;
@ -20,8 +20,8 @@ export interface CustomerInvoiceItemProps {
}
export class CustomerInvoiceItem extends DomainEntity<CustomerInvoiceItemProps> {
private _subtotalPrice!: CustomerInvoiceItemSubtotalPrice;
private _totalPrice!: CustomerInvoiceItemTotalPrice;
private _subtotalAmount!: CustomerInvoiceItemSubtotalAmount;
private _totalAmount!: CustomerInvoiceItemTotalAmount;
public static create(
props: CustomerInvoiceItemProps,
@ -48,26 +48,26 @@ export class CustomerInvoiceItem extends DomainEntity<CustomerInvoiceItemProps>
return this.props.quantity;
}
get unitPrice(): Maybe<CustomerInvoiceItemUnitPrice> {
get unitAmount(): Maybe<CustomerInvoiceItemUnitAmount> {
return this.props.unitPrice;
}
get subtotalPrice(): CustomerInvoiceItemSubtotalPrice {
if (!this._subtotalPrice) {
this._subtotalPrice = this.calculateSubtotal();
get subtotalAmount(): CustomerInvoiceItemSubtotalAmount {
if (!this._subtotalAmount) {
this._subtotalAmount = this.calculateSubtotal();
}
return this._subtotalPrice;
return this._subtotalAmount;
}
get discount(): Maybe<CustomerInvoiceItemDiscount> {
return this.props.discount;
}
get totalPrice(): CustomerInvoiceItemTotalPrice {
if (!this._totalPrice) {
this._totalPrice = this.calculateTotal();
get totalPrice(): CustomerInvoiceItemTotalAmount {
if (!this._totalAmount) {
this._totalAmount = this.calculateTotal();
}
return this._totalPrice;
return this._totalAmount;
}
public get languageCode(): LanguageCode {
@ -86,7 +86,7 @@ export class CustomerInvoiceItem extends DomainEntity<CustomerInvoiceItemProps>
return this.getValue();
}
calculateSubtotal(): CustomerInvoiceItemSubtotalPrice {
calculateSubtotal(): CustomerInvoiceItemSubtotalAmount {
throw new Error("Not implemented");
/*const unitPrice = this.unitPrice.isSome()
@ -95,7 +95,7 @@ export class CustomerInvoiceItem extends DomainEntity<CustomerInvoiceItemProps>
return this.unitPrice.multiply(this.quantity.toNumber()); // Precio unitario * Cantidad*/
}
calculateTotal(): CustomerInvoiceItemTotalPrice {
calculateTotal(): CustomerInvoiceItemTotalAmount {
throw new Error("Not implemented");
//return this.subtotalPrice.subtract(this.subtotalPrice.percentage(this.discount.toNumber()));
}

View File

@ -1,2 +1,2 @@
export * from "./customer-invoice-items";
export * from "./invoice-customer";
export * from "./invoice-items";

View File

@ -1,33 +0,0 @@
import { Criteria } from "@repo/rdx-criteria/server";
import { UniqueID } from "@repo/rdx-ddd";
import { Collection, Result } from "@repo/rdx-utils";
import { CustomerInvoice, CustomerInvoiceProps } from "../aggregates";
export interface ICustomerInvoiceService {
build(props: CustomerInvoiceProps, id?: UniqueID): Result<CustomerInvoice, Error>;
save(invoice: CustomerInvoice, transaction: any): Promise<Result<CustomerInvoice, Error>>;
existsById(id: UniqueID, transaction?: any): Promise<Result<boolean, Error>>;
findByCriteria(
criteria: Criteria,
transaction?: any
): Promise<Result<Collection<CustomerInvoice>, Error>>;
getById(id: UniqueID, transaction?: any): Promise<Result<CustomerInvoice>>;
updateById(
id: UniqueID,
data: Partial<CustomerInvoiceProps>,
transaction?: any
): Promise<Result<CustomerInvoice, Error>>;
createCustomerInvoice(
id: UniqueID,
data: CustomerInvoiceProps,
transaction?: any
): Promise<Result<CustomerInvoice, Error>>;
deleteById(id: UniqueID, transaction?: any): Promise<Result<void, Error>>;
}

View File

@ -2,22 +2,26 @@ import { Criteria } from "@repo/rdx-criteria/server";
import { UniqueID } from "@repo/rdx-ddd";
import { Collection, Result } from "@repo/rdx-utils";
import { Transaction } from "sequelize";
import { CustomerInvoice, CustomerInvoiceProps } from "../aggregates";
import { CustomerInvoice, CustomerInvoicePatchProps, CustomerInvoiceProps } from "../aggregates";
import { ICustomerInvoiceRepository } from "../repositories";
import { ICustomerInvoiceService } from "./customer-invoice-service.interface";
export class CustomerInvoiceService implements ICustomerInvoiceService {
export class CustomerInvoiceService {
constructor(private readonly repository: ICustomerInvoiceRepository) {}
/**
* Construye un nuevo agregado CustomerInvoice a partir de props validadas.
*
* @param companyId - Identificador de la empresa a la que pertenece el cliente.
* @param props - Las propiedades ya validadas para crear la factura.
* @param id - Identificador UUID de la factura (opcional).
* @param invoiceId - Identificador UUID de la factura (opcional).
* @returns Result<CustomerInvoice, Error> - El agregado construido o un error si falla la creación.
*/
build(props: CustomerInvoiceProps, id?: UniqueID): Result<CustomerInvoice, Error> {
return CustomerInvoice.create(props, id);
buildInvoiceInCompany(
companyId: UniqueID,
props: Omit<CustomerInvoiceProps, "companyId">,
invoiceId?: UniqueID
): Result<CustomerInvoice, Error> {
return CustomerInvoice.create({ ...props, companyId }, invoiceId);
}
/**
@ -27,124 +31,108 @@ export class CustomerInvoiceService implements ICustomerInvoiceService {
* @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice, Error> - El agregado guardado o un error si falla la operación.
*/
async save(invoice: CustomerInvoice, transaction: any): Promise<Result<CustomerInvoice, Error>> {
const saved = await this.repository.save(invoice, transaction);
return saved.isSuccess ? Result.ok(invoice) : Result.fail(saved.error);
async saveInvoice(
invoice: CustomerInvoice,
transaction: any
): Promise<Result<CustomerInvoice, Error>> {
return this.repository.save(invoice, transaction);
}
/**
*
* Comprueba si existe o no en persistencia una factura con el ID proporcionado
*
* @param id - Identificador UUID de la factura.
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
* @param invoiceId - Identificador UUID de la factura.
* @param transaction - Transacción activa para la operación.
* @returns Result<Boolean, Error> - Existe la factura o no.
*/
async existsById(id: UniqueID, transaction?: any): Promise<Result<boolean, Error>> {
return this.repository.existsById(id, transaction);
async existsByIdInCompany(
companyId: UniqueID,
invoiceId: UniqueID,
transaction?: any
): Promise<Result<boolean, Error>> {
return this.repository.existsByIdInCompany(companyId, invoiceId, transaction);
}
/**
* Obtiene una colección de facturas que cumplen con los filtros definidos en un objeto Criteria.
*
* @param companyId - Identificador UUID de la empresa a la que pertenece el cliente.
* @param criteria - Objeto con condiciones de filtro, paginación y orden.
* @param transaction - Transacción activa para la operación.
* @returns Result<Collection<CustomerInvoice>, Error> - Colección de facturas o error.
*/
async findByCriteria(
async findInvoiceByCriteriaInCompany(
companyId: UniqueID,
criteria: Criteria,
transaction?: Transaction
): Promise<Result<Collection<CustomerInvoice>, Error>> {
const customerInvoicesOrError = await this.repository.findByCriteria(criteria, transaction);
if (customerInvoicesOrError.isFailure) {
return Result.fail(customerInvoicesOrError.error);
}
// Solo devolver usuarios activos
//const allCustomerInvoices = customerInvoicesOrError.data.filter((customerInvoice) => customerInvoice.isActive);
//return Result.ok(new Collection(allCustomerInvoices));
return customerInvoicesOrError;
return this.repository.findByCriteriaInCompany(companyId, criteria, transaction);
}
/**
* Recupera una factura por su identificador único.
*
* @param id - Identificador UUID de la factura.
* @param invoiceId - Identificador UUID de la factura.
* @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice, Error> - Factura encontrada o error.
*/
async getById(id: UniqueID, transaction?: Transaction): Promise<Result<CustomerInvoice>> {
return await this.repository.findById(id, transaction);
async getInvoiceByIdInCompany(
companyId: UniqueID,
invoiceId: UniqueID,
transaction?: Transaction
): Promise<Result<CustomerInvoice>> {
return await this.repository.getByIdInCompany(companyId, invoiceId, transaction);
}
/**
* Actualiza parcialmente una factura existente con nuevos datos.
* No lo guarda en el repositorio.
*
* @param id - Identificador de la factura a actualizar.
* @param companyId - Identificador de la empresa a la que pertenece el cliente.
* @param invoiceId - Identificador de la factura a actualizar.
* @param changes - Subconjunto de props válidas para aplicar.
* @param transaction - Transacción activa para la operación.
* @returns Result<CustomerInvoice, Error> - Factura actualizada o error.
*/
async updateById(
customerInvoiceId: UniqueID,
changes: Partial<CustomerInvoiceProps>,
async updateInvoiceByIdInCompany(
companyId: UniqueID,
invoiceId: UniqueID,
changes: CustomerInvoicePatchProps,
transaction?: Transaction
): Promise<Result<CustomerInvoice, Error>> {
// Verificar si la factura existe
const customerInvoiceOrError = await this.repository.getById(customerInvoiceId, transaction);
if (customerInvoiceOrError.isFailure) {
return Result.fail(new Error("CustomerInvoice not found"));
const invoiceResult = await this.getInvoiceByIdInCompany(companyId, invoiceId, transaction);
if (invoiceResult.isFailure) {
return Result.fail(invoiceResult.error);
}
return Result.fail(new Error("No implementado"));
const invoice = invoiceResult.data;
const updatedInvoice = invoice.update(changes);
/*const updatedCustomerInvoiceOrError = CustomerInvoice.update(customerInvoiceOrError.data, data);
if (updatedCustomerInvoiceOrError.isFailure) {
return Result.fail(
new Error(`Error updating customerInvoice: ${updatedCustomerInvoiceOrError.error.message}`)
);
if (updatedInvoice.isFailure) {
return Result.fail(updatedInvoice.error);
}
const updateCustomerInvoice = updatedCustomerInvoiceOrError.data;
await this.repo.update(updateCustomerInvoice, transaction);
return Result.ok(updateCustomerInvoice);*/
}
async createCustomerInvoice(
customerInvoiceId: UniqueID,
data: CustomerInvoiceProps,
transaction?: Transaction
): Promise<Result<CustomerInvoice, Error>> {
// Verificar si la factura existe
const customerInvoiceOrError = await this.repository.getById(customerInvoiceId, transaction);
if (customerInvoiceOrError.isSuccess) {
return Result.fail(new Error("CustomerInvoice exists"));
}
const newCustomerInvoiceOrError = CustomerInvoice.create(data, customerInvoiceId);
if (newCustomerInvoiceOrError.isFailure) {
return Result.fail(
new Error(`Error creating customerInvoice: ${newCustomerInvoiceOrError.error.message}`)
);
}
const newCustomerInvoice = newCustomerInvoiceOrError.data;
await this.repository.create(newCustomerInvoice, transaction);
return Result.ok(newCustomerInvoice);
return Result.ok(updatedInvoice.data);
}
/**
* Elimina (o marca como eliminada) una factura según su ID.
*
* @param id - Identificador UUID de la factura.
* @param companyId - Identificador de la empresa a la que pertenece el cliente.
* @param invoiceId - Identificador UUID de la factura.
* @param transaction - Transacción activa para la operación.
* @returns Result<boolean, Error> - Resultado de la operación.
*/
async deleteById(id: UniqueID, transaction?: Transaction): Promise<Result<void, Error>> {
return this.repository.deleteById(id, transaction);
async deleteById(
companyId: UniqueID,
invoiceId: UniqueID,
transaction?: Transaction
): Promise<Result<void, Error>> {
return this.repository.deleteByIdInCompany(companyId, invoiceId, transaction);
}
}

View File

@ -1,2 +1 @@
export * from "./customer-invoice-service.interface";
export * from "./customer-invoice.service";

View File

@ -1,6 +1,6 @@
import { MoneyValue, MoneyValueProps } from "@repo/rdx-ddd";
export class CustomerInvoiceItemSubtotalPrice extends MoneyValue {
export class CustomerInvoiceItemSubtotalAmount extends MoneyValue {
public static DEFAULT_SCALE = 4;
static create({ amount, currency_code, scale }: MoneyValueProps) {

View File

@ -1,6 +1,6 @@
import { MoneyValue, MoneyValueProps } from "@repo/rdx-ddd";
export class CustomerInvoiceItemTotalPrice extends MoneyValue {
export class CustomerInvoiceItemTotalAmount extends MoneyValue {
public static DEFAULT_SCALE = 4;
static create({ amount, currency_code, scale }: MoneyValueProps) {

View File

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

View File

@ -2,9 +2,9 @@ export * from "./customer-invoice-address-type";
export * from "./customer-invoice-item-description";
export * from "./customer-invoice-item-discount";
export * from "./customer-invoice-item-quantity";
export * from "./customer-invoice-item-subtotal-price";
export * from "./customer-invoice-item-total-price";
export * from "./customer-invoice-item-unit-price";
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-serie";
export * from "./customer-invoice-status";

View File

@ -4,7 +4,7 @@ import {
CreateCustomerInvoiceUseCase,
CreateCustomerInvoicesAssembler,
DeleteCustomerInvoiceUseCase,
GetCustomerInvoiceAssembler,
GetCustomerInvoiceItemsAssembler,
GetCustomerInvoiceUseCase,
ListCustomerInvoicesAssembler,
ListCustomerInvoicesUseCase,
@ -22,7 +22,7 @@ type InvoiceDeps = {
service: ICustomerInvoiceService;
assemblers: {
list: ListCustomerInvoicesAssembler;
get: GetCustomerInvoiceAssembler;
get: GetCustomerInvoiceItemsAssembler;
create: CreateCustomerInvoicesAssembler;
update: UpdateCustomerInvoiceAssembler;
};
@ -54,7 +54,7 @@ export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps {
if (!_assemblers) {
_assemblers = {
list: new ListCustomerInvoicesAssembler(), // transforma domain → ListDTO
get: new GetCustomerInvoiceAssembler(), // transforma domain → DetailDTO
get: new GetCustomerInvoiceItemsAssembler(), // transforma domain → DetailDTO
create: new CreateCustomerInvoicesAssembler(), // transforma domain → CreatedDTO
update: new UpdateCustomerInvoiceAssembler(), // transforma domain -> UpdateDTO
};

View File

@ -2,20 +2,17 @@ import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from
import { GetCustomerInvoiceUseCase } from "../../../application";
export class GetCustomerInvoiceController extends ExpressController {
public constructor(
private readonly useCase: GetCustomerInvoiceUseCase
/* private readonly presenter: any */
) {
public constructor(private readonly useCase: GetCustomerInvoiceUseCase) {
super();
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
this.useGuards(authGuard(), tenantGuard(), forbidQueryFieldGuard("companyId"));
}
protected async executeImpl() {
const tenantId = this.getTenantId()!; // garantizado por tenantGuard
const { id } = this.req.params;
const companyId = this.getTenantId()!; // garantizado por tenantGuard
const { invoice_id } = this.req.params;
const result = await this.useCase.execute({ id, tenantId });
const result = await this.useCase.execute({ invoice_id, companyId });
return result.match(
(data) => this.ok(data),

View File

@ -1,4 +1,4 @@
import { RequestWithAuth, enforceTenant } from "@erp/auth/api";
import { RequestWithAuth, enforceTenant, enforceUser, mockUser } from "@erp/auth/api";
import { ILogger, ModuleParams, validateRequest } from "@erp/core/api";
import { Application, NextFunction, Request, Response, Router } from "express";
import { Sequelize } from "sequelize";
@ -24,17 +24,33 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
logger: ILogger;
};
const router: Router = Router({ mergeParams: true });
const deps = getInvoiceDependencies(params);
const router: Router = Router({ mergeParams: true });
// 🔐 Autenticación + Tenancy para TODO el router
router.use(/* authenticateJWT(), */ enforceTenant() /*checkTabContext*/);
if (process.env.NODE_ENV === "development") {
router.use(
(req: Request, res: Response, next: NextFunction) =>
mockUser(req as RequestWithAuth, res, next) // Debe ir antes de las rutas protegidas
);
}
router.use([
(req: Request, res: Response, next: NextFunction) =>
enforceUser()(req as RequestWithAuth, res, next), // Debe ir antes de las rutas protegidas
(req: Request, res: Response, next: NextFunction) =>
enforceTenant()(req as RequestWithAuth, res, next), // Debe ir antes de las rutas protegidas
]);
// ----------------------------------------------
router.get(
"/",
//checkTabContext,
validateRequest(CustomerInvoiceListRequestSchema, "params"),
async (req: RequestWithAuth, res: Response, next: NextFunction) => {
async (req: Request, res: Response, next: NextFunction) => {
const useCase = deps.build.list();
const controller = new ListCustomerInvoicesController(useCase /*, deps.presenters.list */);
return controller.execute(req, res, next);
@ -64,13 +80,15 @@ export const customerInvoicesRouter = (params: ModuleParams) => {
}
);
/*routes.put(
"/:customerInvoiceId",
validateAndParseBody(IUpdateCustomerInvoiceRequestSchema),
checkTabContext,
/*router.put(
"/:customer_id",
//checkTabContext,
validateRequest(UpdateCustomerInvoiceByIdParamsRequestSchema, "params"),
validateRequest(UpdateCustomerInvoiceByIdRequestSchema, "body"),
(req: Request, res: Response, next: NextFunction) => {
buildUpdateCustomerInvoiceController().execute(req, res, next);
const useCase = deps.build.update();
const controller = new UpdateCustomerInvoiceController(useCase);
return controller.execute(req, res, next);
}
);*/

View File

@ -8,7 +8,7 @@ import {
CustomerInvoiceItemDescription,
CustomerInvoiceItemDiscount,
CustomerInvoiceItemQuantity,
CustomerInvoiceItemUnitPrice,
CustomerInvoiceItemUnitAmount,
} from "../../domain";
import {
CustomerInvoiceItemCreationAttributes,
@ -59,7 +59,7 @@ export class CustomerInvoiceItemMapper
}
// Validación y creación de precio unitario
const unitPriceOrError = CustomerInvoiceItemUnitPrice.create({
const unitPriceOrError = CustomerInvoiceItemUnitAmount.create({
amount: source.unit_price_amount,
scale: source.unit_price_scale,
currency_code: sourceParent.invoice_currency,
@ -123,11 +123,11 @@ export class CustomerInvoiceItemMapper
quantity_amount: source.quantity.toPrimitive().amount,
quantity_scale: source.quantity.toPrimitive().scale,
unit_price_amount: source.unitPrice.toPrimitive().amount,
unit_price_scale: source.unitPrice.toPrimitive().scale,
unit_price_amount: source.unitAmount.toPrimitive().amount,
unit_price_scale: source.unitAmount.toPrimitive().scale,
subtotal_amount: source.subtotalPrice.toPrimitive().amount,
subtotal_scale: source.subtotalPrice.toPrimitive().scale,
subtotal_amount: source.subtotalAmount.toPrimitive().amount,
subtotal_scale: source.subtotalAmount.toPrimitive().scale,
discount_amount: source.discount.toPrimitive().amount,
discount_scale: source.discount.toPrimitive().scale,

View File

@ -34,12 +34,28 @@ export class CustomerInvoiceModel extends Model<
declare currency_code: string;
// Subtotal
declare subtotal_amount: number;
declare subtotal_scale: number;
declare subtotal_amount_value: number;
declare subtotal_amount_scale: number;
// Discount percentage
declare discount_percentage_value: number;
declare discount_percentage_scale: number;
// Discount amount
declare discount_amount_value: number;
declare discount_amount_scale: number;
// Taxable amount (base imponible)
declare taxable_amount_value: number;
declare taxable_amount_scale: number;
// Total tax amount / taxes total
declare tax_amount_value: number;
declare tax_amount_scale: number;
// Total
declare total_amount: number;
declare total_scale: number;
declare total_amount_value: number;
declare total_amount_scale: number;
// Relaciones
declare items: NonAttribute<CustomerInvoiceItemModel[]>;
@ -125,23 +141,70 @@ export default (database: Sequelize) => {
defaultValue: "EUR",
},
subtotal_amount: {
subtotal_amount_value: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true,
defaultValue: null,
},
subtotal_scale: {
subtotal_amount_scale: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
},
total_amount: {
discount_percentage_value: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
},
discount_percentage_scale: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
},
discount_amount_value: {
type: new DataTypes.BIGINT(),
allowNull: true,
defaultValue: null,
},
discount_amount_scale: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
},
taxable_amount_value: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true,
defaultValue: null,
},
total_scale: {
taxable_amount_scale: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
},
tax_amount_value: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true,
defaultValue: null,
},
tax_amount_scale: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
},
total_amount_value: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true,
defaultValue: null,
},
total_amount_scale: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,

View File

@ -1,7 +1,7 @@
import * as z from "zod/v4";
export const GetCustomerInvoiceByIdRequestSchema = z.object({
id: z.string(),
invoice_id: z.string(),
});
export type GetCustomerInvoiceByIdRequestDTO = z.infer<typeof GetCustomerInvoiceByIdRequestSchema>;

View File

@ -1,15 +1,32 @@
import { MetadataSchema } from "@erp/core";
import { AmountSchema, MetadataSchema, PercentageSchema, QuantitySchema } from "@erp/core";
import * as z from "zod/v4";
export const GetCustomerInvoiceByIdResponseSchema = z.object({
id: z.uuid(),
invoice_status: z.string(),
company_id: z.uuid(),
invoice_number: z.string(),
invoice_series: z.string(),
issue_date: z.iso.datetime({ offset: true }),
operation_date: z.iso.datetime({ offset: true }),
status: z.string(),
series: z.string(),
issue_date: z.string(),
operation_date: z.string(),
notes: z.string(),
language_code: z.string(),
currency: z.string(),
currency_code: z.string(),
items: z.array(
z.object({
position: z.string(),
description: z.string(),
quantity: QuantitySchema,
unit_price_amount: AmountSchema,
discount: PercentageSchema,
total_amount: AmountSchema,
})
),
metadata: MetadataSchema.optional(),
});

View File

@ -90,14 +90,14 @@ export class CustomerService {
*
* @param companyId - Identificador de la empresa a la que pertenece el cliente.
* @param customerId - Identificador del cliente a actualizar.
* @param partial - Subconjunto de props válidas para aplicar.
* @param changes - Subconjunto de props válidas para aplicar.
* @param transaction - Transacción activa para la operación.
* @returns Result<Customer, Error> - Cliente actualizado o error.
*/
async updateCustomerByIdInCompany(
companyId: UniqueID,
customerId: UniqueID,
partial: CustomerPatchProps,
changes: CustomerPatchProps,
transaction?: any
): Promise<Result<Customer, Error>> {
const customerResult = await this.getCustomerByIdInCompany(companyId, customerId, transaction);
@ -107,7 +107,7 @@ export class CustomerService {
}
const customer = customerResult.data;
const updatedCustomer = customer.update(partial);
const updatedCustomer = customer.update(changes);
if (updatedCustomer.isFailure) {
return Result.fail(updatedCustomer.error);

View File

@ -1,4 +1,4 @@
import { enforceTenant, enforceUser, mockUser } from "@erp/auth/api";
import { RequestWithAuth, enforceTenant, enforceUser, mockUser } from "@erp/auth/api";
import { ILogger, ModuleParams, validateRequest } from "@erp/core/api";
import { Application, NextFunction, Request, Response, Router } from "express";
import { Sequelize } from "sequelize";
@ -33,11 +33,22 @@ export const customersRouter = (params: ModuleParams) => {
// 🔐 Autenticación + Tenancy para TODO el router
if (process.env.NODE_ENV === "development") {
router.use(mockUser); // Debe ir antes de las rutas protegidas
router.use(
(req: Request, res: Response, next: NextFunction) =>
mockUser(req as RequestWithAuth, res, next) // Debe ir antes de las rutas protegidas
);
}
//router.use(/*authenticateJWT(),*/ enforceTenant() /*checkTabContext*/);
router.use([enforceUser(), enforceTenant()]);
router.use([
(req: Request, res: Response, next: NextFunction) =>
enforceUser()(req as RequestWithAuth, res, next), // Debe ir antes de las rutas protegidas
(req: Request, res: Response, next: NextFunction) =>
enforceTenant()(req as RequestWithAuth, res, next), // Debe ir antes de las rutas protegidas
]);
// ----------------------------------------------
router.get(
"/",

View File

@ -5,7 +5,7 @@ export function isNullishOrEmpty(input: unknown): boolean {
}
// Función genérica para asegurar valores básicos
function ensure<T>(value: T | undefined | null, defaultValue: T): T {
export function ensure<T>(value: T | undefined | null, defaultValue: T): T {
return value ?? defaultValue;
}