Facturas de cliente

This commit is contained in:
David Arranz 2025-09-12 18:23:36 +02:00
parent 56b37c4256
commit 817dcff8c5
49 changed files with 569 additions and 291 deletions

View File

@ -19,7 +19,7 @@ export const currentState = {
host: ENV.HOST, host: ENV.HOST,
port: ENV.PORT, port: ENV.PORT,
environment: ENV.NODE_ENV, environment: ENV.NODE_ENV,
connections: {} as Record<string, any>, connections: {} as Record<string, unknown>,
}; };
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────

View File

@ -7,7 +7,7 @@ import { logger } from "../logger";
*/ */
type SequelizeLike = { type SequelizeLike = {
sync: (options?: any) => Promise<void>; sync: (options?: any) => Promise<void>;
models: Record<string, any>; models: Record<string, unknown>;
}; };
type ModelStatic = { type ModelStatic = {

View File

@ -1,4 +1,4 @@
const services: Record<string, any> = {}; const services: Record<string, unknown> = {};
/** /**
* Registra un objeto de servicio (API) bajo un nombre. * Registra un objeto de servicio (API) bajo un nombre.

View File

@ -7,14 +7,11 @@ import { IInvoiceRepository, Invoice } from "../domain";
import { IInvoiceMapper } from "./mappers"; import { IInvoiceMapper } from "./mappers";
export type QueryParams = { export type QueryParams = {
pagination: Record<string, any>; pagination: Record<string, unknown>;
filters: Record<string, any>; filters: Record<string, unknown>;
}; };
export class InvoiceRepository export class InvoiceRepository extends SequelizeRepository<Invoice> implements IInvoiceRepository {
extends SequelizeRepository<Invoice>
implements IInvoiceRepository
{
protected mapper: IInvoiceMapper; protected mapper: IInvoiceMapper;
public constructor(props: { public constructor(props: {
@ -33,10 +30,7 @@ export class InvoiceRepository
{ association: "items" }, { association: "items" },
{ {
association: "participants", association: "participants",
include: [ include: [{ association: "shippingAddress" }, { association: "billingAddress" }],
{ association: "shippingAddress" },
{ association: "billingAddress" },
],
}, },
], ],
}); });
@ -48,28 +42,21 @@ export class InvoiceRepository
return this.mapper.mapToDomain(rawContact); return this.mapper.mapToDomain(rawContact);
} }
public async findAll( public async findAll(queryCriteria?: IQueryCriteria): Promise<ICollection<Invoice>> {
queryCriteria?: IQueryCriteria const { rows, count } = await this._findAll("Invoice_Model", queryCriteria, {
): Promise<ICollection<Invoice>> { include: [
const { rows, count } = await this._findAll( {
"Invoice_Model", association: "participants",
queryCriteria, separate: true,
{ },
include: [ ],
{ });
association: "participants",
separate: true,
},
],
}
);
return this.mapper.mapArrayAndCountToDomain(rows, count); return this.mapper.mapArrayAndCountToDomain(rows, count);
} }
public async save(invoice: Invoice): Promise<void> { public async save(invoice: Invoice): Promise<void> {
const { items, participants, ...invoiceData } = const { items, participants, ...invoiceData } = this.mapper.mapToPersistence(invoice);
this.mapper.mapToPersistence(invoice);
await this.adapter await this.adapter
.getModel("Invoice_Model") .getModel("Invoice_Model")
@ -85,10 +72,9 @@ export class InvoiceRepository
await this.adapter await this.adapter
.getModel("InvoiceParticipantAddress_Model") .getModel("InvoiceParticipantAddress_Model")
.bulkCreate( .bulkCreate([participants[0].billingAddress, participants[0].shippingAddress], {
[participants[0].billingAddress, participants[0].shippingAddress], transaction: this.transaction,
{ transaction: this.transaction } });
);
} }
public removeById(id: UniqueID): Promise<void> { public removeById(id: UniqueID): Promise<void> {

View File

@ -1,3 +1,4 @@
export * from "./presenter";
export * from "./presenter-registry"; export * from "./presenter-registry";
export * from "./presenter-registry.interface"; export * from "./presenter-registry.interface";
export * from "./presenter.interface"; export * from "./presenter.interface";

View File

@ -4,8 +4,9 @@ import { IPresenter } from "./presenter.interface";
* 🔑 Claves de proyección comunes para seleccionar presenters * 🔑 Claves de proyección comunes para seleccionar presenters
*/ */
export type PresenterKey = { export type PresenterKey = {
resource: string;
projection: "FULL" | "LIST" | "REPORT" | (string & {}); projection: "FULL" | "LIST" | "REPORT" | (string & {});
format?: "JSON" | "PDF" | "CSV" | (string & {}); format?: "JSON" | "HTML" | "PDF" | "CSV" | (string & {});
version?: number; // 1 | 2 version?: number; // 1 | 2
locale?: string; // es | en | fr locale?: string; // es | en | fr
}; };

View File

@ -49,10 +49,12 @@ export class InMemoryPresenterRegistry implements IPresenterRegistry {
} }
if (!this.registry.has(exactKey)) { if (!this.registry.has(exactKey)) {
throw new ApplicationError(`Error. Presenter ${key} not registred!`); throw new ApplicationError(
`Error. Presenter ${key.resource} ${key.projection} not registred!`
);
} }
throw new ApplicationError(`Error. Presenter ${key} not registred!`); throw new ApplicationError(`Error. Presenter ${key.resource} ${key.projection} not registred!`);
} }
registerPresenter<TSource, TOutput>( registerPresenter<TSource, TOutput>(
@ -76,8 +78,9 @@ export class InMemoryPresenterRegistry implements IPresenterRegistry {
* 🔹 Construye la clave única para el registro. * 🔹 Construye la clave única para el registro.
*/ */
private _buildKey(key: PresenterKey): string { private _buildKey(key: PresenterKey): string {
const { projection, format, version, locale } = key; const { resource, projection, format, version, locale } = key;
return [ return [
resource.toLowerCase(),
projection.toLowerCase(), projection.toLowerCase(),
format!.toLowerCase(), format!.toLowerCase(),
version ?? "latest", version ?? "latest",

View File

@ -0,0 +1,11 @@
import { IPresenterRegistry } from "./presenter-registry.interface";
import { IPresenter } from "./presenter.interface";
export type IPresenterParams = {
presenterRegistry: IPresenterRegistry;
} & Record<string, unknown>;
export abstract class Presenter implements IPresenter {
constructor(protected presenterRegistry: IPresenterRegistry) {}
abstract toOutput(source: unknown): unknown;
}

View File

@ -4,7 +4,7 @@ import { Collection, Result } from "@repo/rdx-utils";
* Tipo para los parámetros que reciben los métodos de los mappers * Tipo para los parámetros que reciben los métodos de los mappers
* Es un objeto que puede contener cualquier cosa. * Es un objeto que puede contener cualquier cosa.
*/ */
export type MapperParamsType = Record<string, any>; export type MapperParamsType = Record<string, unknown>;
/** /**
* 🧭 Mapper de Dominio (Persistencia Dominio/Agregado) * 🧭 Mapper de Dominio (Persistencia Dominio/Agregado)

View File

@ -111,6 +111,24 @@ export abstract class ExpressController {
return this.res.sendStatus(httpStatus.NO_CONTENT); return this.res.sendStatus(httpStatus.NO_CONTENT);
} }
public sendFile(filepath: string) {
return this.res.sendFile(filepath);
}
public downloadFile(filepath: string, filename: string, done?: any) {
return this.res.download(filepath, filename, done);
}
public downloadPDF(pdfBuffer: Buffer, filename: string) {
this.res.set({
"Content-Type": "application/pdf",
"Content-Disposition": `attachment; filename=${filename}`,
//"Content-Length": buffer.length,
});
return this.res.send(pdfBuffer);
}
protected clientError(message: string, errors?: any[] | any) { protected clientError(message: string, errors?: any[] | any) {
return this.handleApiError( return this.handleApiError(
new ValidationApiError(message, Array.isArray(errors) ? errors : [errors]) new ValidationApiError(message, Array.isArray(errors) ? errors : [errors])

View File

@ -6,7 +6,7 @@ export class InMemoryMapperRegistry implements IMapperRegistry {
private readModelMappers: Map<MapperProjectionKey, any> = new Map(); private readModelMappers: Map<MapperProjectionKey, any> = new Map();
getDomainMapper<T>(key: MapperProjectionKey): T { getDomainMapper<T>(key: MapperProjectionKey): T {
if (!this.readModelMappers.has(key)) { if (!this.domainMappers.has(key)) {
throw new InfrastructureError(`Error. Domain model mapper ${key} not registred!`); throw new InfrastructureError(`Error. Domain model mapper ${key} not registred!`);
} }

View File

@ -1,7 +1,7 @@
import { Collection, Result } from "@repo/rdx-utils"; import { Collection, Result } from "@repo/rdx-utils";
import { Model } from "sequelize"; import { Model } from "sequelize";
export type MapperParamsType = Record<string, any>; export type MapperParamsType = Record<string, unknown>;
interface IDomainMapper<TModel extends Model, TEntity> { interface IDomainMapper<TModel extends Model, TEntity> {
mapToDomain(source: TModel, params?: MapperParamsType): Result<TEntity, Error>; mapToDomain(source: TModel, params?: MapperParamsType): Result<TEntity, Error>;

View File

@ -10,11 +10,11 @@ export interface IErrorResponseDTO {
export interface IErrorContextResponseDTO { export interface IErrorContextResponseDTO {
user?: unknown; user?: unknown;
params?: Record<string, any>; params?: Record<string, unknown>;
query?: Record<string, any>; query?: Record<string, unknown>;
body?: Record<string, any>; body?: Record<string, unknown>;
} }
export interface IErrorExtraResponseDTO { export interface IErrorExtraResponseDTO {
errors: Record<string, any>[]; errors: Record<string, unknown>[];
} }

View File

@ -41,7 +41,7 @@ export const createAxiosDataSource = (client: AxiosInstance): IDataSource => {
return { return {
getBaseUrl: () => (client as AxiosInstance).getUri(), getBaseUrl: () => (client as AxiosInstance).getUri(),
getList: async <T, R>(resource: string, params?: Record<string, any>): Promise<R> => { getList: async <T, R>(resource: string, params?: Record<string, unknown>): Promise<R> => {
const { pagination } = params as any; const { pagination } = params as any;
const res = await (client as AxiosInstance).get<T[]>(resource, { const res = await (client as AxiosInstance).get<T[]>(resource, {

View File

@ -14,7 +14,7 @@ export interface ICustomParams {
export interface IDataSource { export interface IDataSource {
getBaseUrl(): string; getBaseUrl(): string;
getList<T, R>(resource: string, params?: Record<string, any>): Promise<R>; getList<T, R>(resource: string, params?: Record<string, unknown>): Promise<R>;
getOne<T>(resource: string, id: string | number): Promise<T>; getOne<T>(resource: string, id: string | number): Promise<T>;
getMany<T, R>(resource: string, ids: Array<string | number>): Promise<R>; getMany<T, R>(resource: string, ids: Array<string | number>): Promise<R>;
createOne<T>(resource: string, data: Partial<T>): Promise<T>; createOne<T>(resource: string, data: Partial<T>): Promise<T>;

View File

@ -1,8 +1,8 @@
import { ITransactionManager } from "@erp/core/api"; import { IPresenterRegistry, 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 { CustomerInvoiceService } from "../../domain"; import { CustomerInvoiceService } from "../../domain";
import { GetCustomerInvoiceAssembler } from "./assembler"; import { CustomerInvoiceFullPresenter } from "../presenters/full-domain";
type GetCustomerInvoiceUseCaseInput = { type GetCustomerInvoiceUseCaseInput = {
companyId: UniqueID; companyId: UniqueID;
@ -13,19 +13,22 @@ export class GetCustomerInvoiceUseCase {
constructor( constructor(
private readonly service: CustomerInvoiceService, private readonly service: CustomerInvoiceService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
private readonly assembler: GetCustomerInvoiceAssembler, private readonly presenterRegistry: IPresenterRegistry
) {} ) {}
public execute(params: GetCustomerInvoiceUseCaseInput) { public execute(params: GetCustomerInvoiceUseCaseInput) {
const { invoice_id, companyId } = params; const { invoice_id, companyId } = params;
const idOrError = UniqueID.create(invoice_id); const idOrError = UniqueID.create(invoice_id);
if (idOrError.isFailure) { if (idOrError.isFailure) {
return Result.fail(idOrError.error); return Result.fail(idOrError.error);
} }
const invoiceId = idOrError.data; const invoiceId = idOrError.data;
const presenter = this.presenterRegistry.getPresenter({
resource: "customer-invoice",
projection: "FULL",
}) as CustomerInvoiceFullPresenter;
return this.transactionManager.complete(async (transaction) => { return this.transactionManager.complete(async (transaction) => {
try { try {
@ -38,8 +41,10 @@ export class GetCustomerInvoiceUseCase {
return Result.fail(invoiceOrError.error); return Result.fail(invoiceOrError.error);
} }
const getDTO = this.assembler.toDTO(invoiceOrError.data); const customerInvoice = invoiceOrError.data;
return Result.ok(getDTO); const dto = presenter.toOutput(customerInvoice);
return Result.ok(dto);
} catch (error: unknown) { } catch (error: unknown) {
return Result.fail(error as Error); return Result.fail(error as Error);
} }

View File

@ -1,16 +1,23 @@
import { UpdateCustomerInvoiceByIdResponseDTO } from "@erp/customer-invoices/common/dto"; import { IPresenter, IPresenterRegistry } from "@erp/core/api";
import { toEmptyString } from "@repo/rdx-ddd"; import { toEmptyString } from "@repo/rdx-ddd";
import { GetCustomerInvoiceByIdResponseDTO } from "../../../../common/dto";
import { CustomerInvoice } from "../../../domain"; import { CustomerInvoice } from "../../../domain";
import { GetCustomerInvoiceItemsPresenter } from "./get-invoice-items.presenter"; import { GetCustomerInvoiceItemsPresenter } from "./get-invoice-items.presenter";
export class GetCustomerInvoicePresenter { export class GetCustomerInvoicePresentwer implements IPresenter {
private _itemsPresenter!: GetCustomerInvoiceItemsPresenter; private _itemsPresenter!: GetCustomerInvoiceItemsPresenter;
constructor() { constructor(private presenterRegistry: IPresenterRegistry) {
this._itemsPresenter = new GetCustomerInvoiceItemsPresenter(); this._itemsPresenter = this.presenterRegistry.getPresenter({
resource: "customer-invoice-item",
projection: "FULL",
});
} }
public toDTO(invoice: CustomerInvoice): UpdateCustomerInvoiceByIdResponseDTO { toOutput(params: {
invoice: CustomerInvoice;
}): GetCustomerInvoiceByIdResponseDTO {
const { invoice } = params;
const items = this._itemsPresenter.toDTO(invoice); const items = this._itemsPresenter.toDTO(invoice);
return { return {

View File

@ -19,7 +19,7 @@
* @returns true si el objeto no tiene campos undefined, false en caso contrario. * @returns true si el objeto no tiene campos undefined, false en caso contrario.
*/ */
export function hasNoUndefinedFields<T extends Record<string, any>>( export function hasNoUndefinedFields<T extends Record<string, unknown>>(
obj: T obj: T
): obj is { [K in keyof T]-?: Exclude<T[K], undefined> } { ): obj is { [K in keyof T]-?: Exclude<T[K], undefined> } {
return Object.values(obj).every((value) => value !== undefined); return Object.values(obj).every((value) => value !== undefined);
@ -43,7 +43,7 @@ export function hasNoUndefinedFields<T extends Record<string, any>>(
* *
*/ */
export function hasUndefinedFields<T extends Record<string, any>>( export function hasUndefinedFields<T extends Record<string, unknown>>(
obj: T obj: T
): obj is { [K in keyof T]-?: Exclude<T[K], undefined> } { ): obj is { [K in keyof T]-?: Exclude<T[K], undefined> } {
return !hasNoUndefinedFields(obj); return !hasNoUndefinedFields(obj);

View File

@ -4,3 +4,4 @@ export * from "./get-customer-invoice";
export * from "./list-customer-invoices"; export * from "./list-customer-invoices";
export * from "./report-customer-invoice"; export * from "./report-customer-invoice";
//export * from "./update-customer-invoice"; //export * from "./update-customer-invoice";
export * from "./presenters";

View File

@ -1,11 +1,11 @@
import { ITransactionManager } from "@erp/core/api"; import { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
import { Criteria } from "@repo/rdx-criteria/server"; import { Criteria } from "@repo/rdx-criteria/server";
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 { Transaction } from "sequelize"; import { Transaction } from "sequelize";
import { CustomerInvoiceListResponseDTO } from "../../../common/dto"; import { CustomerInvoiceListResponseDTO } from "../../../common/dto";
import { CustomerInvoiceService } from "../../domain"; import { CustomerInvoiceService } from "../../domain";
import { ListCustomerInvoicesAssembler } from "./assembler"; import { CustomerInvoicesListPresenter } from "../presenters/list";
type ListCustomerInvoicesUseCaseInput = { type ListCustomerInvoicesUseCaseInput = {
companyId: UniqueID; companyId: UniqueID;
@ -16,13 +16,17 @@ export class ListCustomerInvoicesUseCase {
constructor( constructor(
private readonly service: CustomerInvoiceService, private readonly service: CustomerInvoiceService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
private readonly assembler: ListCustomerInvoicesAssembler private readonly presenterRegistry: IPresenterRegistry
) {} ) {}
public execute( public execute(
params: ListCustomerInvoicesUseCaseInput params: ListCustomerInvoicesUseCaseInput
): Promise<Result<CustomerInvoiceListResponseDTO, Error>> { ): Promise<Result<CustomerInvoiceListResponseDTO, Error>> {
const { criteria, companyId } = params; const { criteria, companyId } = params;
const presenter = this.presenterRegistry.getPresenter({
resource: "customer-invoice",
projection: "LIST",
}) as CustomerInvoicesListPresenter;
return this.transactionManager.complete(async (transaction: Transaction) => { return this.transactionManager.complete(async (transaction: Transaction) => {
try { try {
@ -36,7 +40,12 @@ export class ListCustomerInvoicesUseCase {
return Result.fail(result.error); return Result.fail(result.error);
} }
const dto = this.assembler.toDTO(result.data, criteria); const customerInvoices = result.data;
const dto = presenter.toOutput({
customerInvoices,
criteria,
});
return Result.ok(dto); return Result.ok(dto);
} catch (error: unknown) { } catch (error: unknown) {
return Result.fail(error as Error); return Result.fail(error as Error);

View File

@ -1,56 +1,57 @@
import { IPresenter } from "@erp/core/api"; import { Presenter } from "@erp/core/api";
import { CustomerInvoiceListDTO } from "@erp/customer-invoices/api/infrastructure"; import { CustomerInvoiceListDTO } from "@erp/customer-invoices/api/infrastructure";
import { Criteria } from "@repo/rdx-criteria/server"; import { Criteria } from "@repo/rdx-criteria/server";
import { toEmptyString } from "@repo/rdx-ddd"; import { toEmptyString } from "@repo/rdx-ddd";
import { ArrayElement, Collection } from "@repo/rdx-utils"; import { ArrayElement, Collection } from "@repo/rdx-utils";
import { CustomerInvoiceListResponseDTO } from "../../../../common/dto"; import { CustomerInvoiceListResponseDTO } from "../../../../common/dto";
export class ListCustomerInvoicesPresenter implements IPresenter { export class ListCustomerInvoicesPresenter extends Presenter {
protected _mapInvoice(invoice: CustomerInvoiceListDTO) {
const recipientDTO = invoice.recipient.toObjectString();
const invoiceDTO: ArrayElement<CustomerInvoiceListResponseDTO["items"]> = {
id: invoice.id.toString(),
company_id: invoice.companyId.toString(),
customer_id: invoice.customerId.toString(),
invoice_number: invoice.invoiceNumber.toString(),
status: invoice.status.toPrimitive(),
series: toEmptyString(invoice.series, (value) => value.toString()),
invoice_date: invoice.invoiceDate.toDateString(),
operation_date: toEmptyString(invoice.operationDate, (value) => value.toDateString()),
recipient: {
customer_id: invoice.customerId.toString(),
...recipientDTO,
},
language_code: invoice.languageCode.code,
currency_code: invoice.currencyCode.code,
taxes: invoice.taxes,
subtotal_amount: invoice.subtotalAmount.toObjectString(),
discount_amount: invoice.discountAmount.toObjectString(),
taxable_amount: invoice.taxableAmount.toObjectString(),
taxes_amount: invoice.taxesAmount.toObjectString(),
total_amount: invoice.totalAmount.toObjectString(),
metadata: {
entity: "customer-invoice",
},
};
return invoiceDTO;
}
toOutput(params: { toOutput(params: {
customerInvoices: Collection<CustomerInvoiceListDTO>; customerInvoices: Collection<CustomerInvoiceListDTO>;
criteria: Criteria; criteria: Criteria;
}): CustomerInvoiceListResponseDTO { }): CustomerInvoiceListResponseDTO {
const { customerInvoices, criteria } = params; const { customerInvoices, criteria } = params;
const invoices = customerInvoices.map((invoice) => { const invoices = customerInvoices.map((invoice) => this._mapInvoice(invoice));
const recipientDTO = invoice.recipient.toObjectString();
const invoiceDTO: ArrayElement<CustomerInvoiceListResponseDTO["items"]> = {
id: invoice.id.toString(),
company_id: invoice.companyId.toString(),
customer_id: invoice.customerId.toString(),
invoice_number: invoice.invoiceNumber.toString(),
status: invoice.status.toPrimitive(),
series: toEmptyString(invoice.series, (value) => value.toString()),
invoice_date: invoice.invoiceDate.toDateString(),
operation_date: toEmptyString(invoice.operationDate, (value) => value.toDateString()),
recipient: {
customer_id: invoice.customerId.toString(),
...recipientDTO,
},
language_code: invoice.languageCode.code,
currency_code: invoice.currencyCode.code,
taxes: invoice.taxes,
subtotal_amount: invoice.subtotalAmount.toObjectString(),
discount_amount: invoice.discountAmount.toObjectString(),
taxable_amount: invoice.taxableAmount.toObjectString(),
taxes_amount: invoice.taxesAmount.toObjectString(),
total_amount: invoice.totalAmount.toObjectString(),
metadata: {
entity: "customer-invoice",
},
};
return invoiceDTO;
});
const totalItems = customerInvoices.total(); const totalItems = customerInvoices.total();
return { return {

View File

@ -0,0 +1,52 @@
import { Presenter } from "@erp/core/api";
import { toEmptyString } from "@repo/rdx-ddd";
import { ArrayElement } from "@repo/rdx-utils";
import { GetCustomerInvoiceByIdResponseDTO } from "../../../../common/dto";
import { CustomerInvoiceItem, CustomerInvoiceItems } from "../../../domain";
type GetCustomerInvoiceItemByInvoiceIdResponseDTO = ArrayElement<
GetCustomerInvoiceByIdResponseDTO["items"]
>;
export class CustomerInvoiceItemsFullPresenter extends Presenter {
protected _map(
invoiceItem: CustomerInvoiceItem,
index: number
): GetCustomerInvoiceItemByInvoiceIdResponseDTO {
const allAmounts = invoiceItem.getAllAmounts();
return {
id: invoiceItem.id.toPrimitive(),
position: String(index),
description: toEmptyString(invoiceItem.description, (value) => value.toPrimitive()),
quantity: invoiceItem.quantity.match(
(quantity) => quantity.toObjectString(),
() => ({ value: "", scale: "" })
),
unit_amount: invoiceItem.unitAmount.match(
(unitAmount) => unitAmount.toObjectString(),
() => ({ value: "", scale: "", currency_code: "" })
),
taxes: invoiceItem.taxes.getCodesToString(),
subtotal_amount: allAmounts.subtotalAmount.toObjectString(),
discount_percentage: invoiceItem.discountPercentage.match(
(discountPercentage) => discountPercentage.toObjectString(),
() => ({ value: "", scale: "" })
),
discount_amount: allAmounts.discountAmount.toObjectString(),
taxable_amount: allAmounts.taxableAmount.toObjectString(),
taxes_amount: allAmounts.taxesAmount.toObjectString(),
total_amount: allAmounts.totalAmount.toObjectString(),
};
}
toOutput(invoiceItems: CustomerInvoiceItems): GetCustomerInvoiceByIdResponseDTO["items"] {
return invoiceItems.map(this._map);
}
}

View File

@ -0,0 +1,51 @@
import { Presenter } from "@erp/core/api";
import { toEmptyString } from "@repo/rdx-ddd";
import { GetCustomerInvoiceByIdResponseDTO } from "../../../../common/dto";
import { CustomerInvoice } from "../../../domain";
import { CustomerInvoiceItemsFullPresenter } from "./customer-invoice-items.full.presenter";
export class CustomerInvoiceFullPresenter extends Presenter {
toOutput(invoice: CustomerInvoice): GetCustomerInvoiceByIdResponseDTO {
const itemsPresenter = this.presenterRegistry.getPresenter({
resource: "customer-invoice-items",
projection: "FULL",
}) as CustomerInvoiceItemsFullPresenter;
const items = itemsPresenter.toOutput(invoice.items);
const allAmounts = invoice.getAllAmounts();
return {
id: invoice.id.toString(),
company_id: invoice.companyId.toString(),
invoice_number: invoice.invoiceNumber.toString(),
status: invoice.status.toPrimitive(),
series: toEmptyString(invoice.series, (value) => value.toString()),
invoice_date: invoice.invoiceDate.toDateString(),
operation_date: toEmptyString(invoice.operationDate, (value) => value.toDateString()),
notes: toEmptyString(invoice.notes, (value) => value.toString()),
language_code: invoice.languageCode.toString(),
currency_code: invoice.currencyCode.toString(),
taxes: invoice.taxes.getCodesToString(),
subtotal_amount: allAmounts.subtotalAmount.toObjectString(),
discount_percentage: invoice.discountPercentage.toObjectString(),
discount_amount: allAmounts.discountAmount.toObjectString(),
taxable_amount: allAmounts.taxableAmount.toObjectString(),
taxes_amount: allAmounts.taxesAmount.toObjectString(),
total_amount: allAmounts.totalAmount.toObjectString(),
items,
metadata: {
entity: "customer-invoices",
},
};
}
}

View File

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

View File

@ -0,0 +1,2 @@
export * from "./full-domain";
export * from "./list";

View File

@ -0,0 +1,74 @@
import { Presenter } from "@erp/core/api";
import { CustomerInvoiceListDTO } from "@erp/customer-invoices/api/infrastructure";
import { Criteria } from "@repo/rdx-criteria/server";
import { toEmptyString } from "@repo/rdx-ddd";
import { ArrayElement, Collection } from "@repo/rdx-utils";
import { CustomerInvoiceListResponseDTO } from "../../../../common/dto";
export class CustomerInvoicesListPresenter extends Presenter {
protected _map(invoice: CustomerInvoiceListDTO) {
const recipientDTO = invoice.recipient.toObjectString();
const invoiceDTO: ArrayElement<CustomerInvoiceListResponseDTO["items"]> = {
id: invoice.id.toString(),
company_id: invoice.companyId.toString(),
customer_id: invoice.customerId.toString(),
invoice_number: invoice.invoiceNumber.toString(),
status: invoice.status.toPrimitive(),
series: toEmptyString(invoice.series, (value) => value.toString()),
invoice_date: invoice.invoiceDate.toDateString(),
operation_date: toEmptyString(invoice.operationDate, (value) => value.toDateString()),
recipient: {
customer_id: invoice.customerId.toString(),
...recipientDTO,
},
language_code: invoice.languageCode.code,
currency_code: invoice.currencyCode.code,
taxes: invoice.taxes,
subtotal_amount: invoice.subtotalAmount.toObjectString(),
discount_amount: invoice.discountAmount.toObjectString(),
taxable_amount: invoice.taxableAmount.toObjectString(),
taxes_amount: invoice.taxesAmount.toObjectString(),
total_amount: invoice.totalAmount.toObjectString(),
metadata: {
entity: "customer-invoice",
},
};
return invoiceDTO;
}
toOutput(params: {
customerInvoices: Collection<CustomerInvoiceListDTO>;
criteria: Criteria;
}): CustomerInvoiceListResponseDTO {
const { customerInvoices, criteria } = params;
const invoices = customerInvoices.map((invoice) => this._map(invoice));
const totalItems = customerInvoices.total();
return {
page: criteria.pageNumber,
per_page: criteria.pageSize,
total_pages: Math.ceil(totalItems / criteria.pageSize),
total_items: totalItems,
items: invoices,
metadata: {
entity: "customer-invoices",
criteria: criteria.toJSON(),
//links: {
// self: `/api/customer-invoices?page=${criteria.pageNumber}&per_page=${criteria.pageSize}`,
// first: `/api/customer-invoices?page=1&per_page=${criteria.pageSize}`,
// last: `/api/customer-invoices?page=${Math.ceil(totalItems / criteria.pageSize)}&per_page=${criteria.pageSize}`,
//},
},
};
}
}

View File

@ -0,0 +1 @@
export * from "./customer-invoices.list.presenter";

View File

@ -1 +1,2 @@
export * from "./report-customer-invoice.use-case"; export * from "./report-customer-invoice.use-case";
export * from "./reporter";

View File

@ -1,8 +1,7 @@
import { ITransactionManager } from "@erp/core/api"; import { IPresenterRegistry, 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 { CustomerInvoiceService } from "../../domain"; import { CustomerInvoiceService } from "../../domain";
import { ReportCustomerInvoiceAssembler } from "./assembler";
type ReportCustomerInvoiceUseCaseInput = { type ReportCustomerInvoiceUseCaseInput = {
companyId: UniqueID; companyId: UniqueID;
@ -13,7 +12,7 @@ export class ReportCustomerInvoiceUseCase {
constructor( constructor(
private readonly service: CustomerInvoiceService, private readonly service: CustomerInvoiceService,
private readonly transactionManager: ITransactionManager, private readonly transactionManager: ITransactionManager,
private readonly assembler: ReportCustomerInvoiceAssembler private readonly presenterRegistry: IPresenterRegistry
) {} ) {}
public execute(params: ReportCustomerInvoiceUseCaseInput) { public execute(params: ReportCustomerInvoiceUseCaseInput) {
@ -26,6 +25,11 @@ export class ReportCustomerInvoiceUseCase {
} }
const invoiceId = idOrError.data; const invoiceId = idOrError.data;
const pdfPresenter = this.presenterRegistry.getPresenter({
resource: "customer-invoice",
projection: "REPORT",
format: "PDF",
});
return this.transactionManager.complete(async (transaction) => { return this.transactionManager.complete(async (transaction) => {
try { try {
@ -38,10 +42,12 @@ export class ReportCustomerInvoiceUseCase {
return Result.fail(invoiceOrError.error); return Result.fail(invoiceOrError.error);
} }
const invoiceDto = this.registry.getPresenter("").toDTO(invoideOIrError.data); const invoice = invoiceOrError.data;
const pdfData = pdfPresenter.toOutput(invoiceOrError.data);
const pdfData = this.assembler.toPDF(invoiceDto); return Result.ok({
return Result.ok(pdfData); data: pdfData,
filename: `invoice-${invoice.invoiceNumber}.pdf`,
});
} catch (error: unknown) { } catch (error: unknown) {
return Result.fail(error as Error); return Result.fail(error as Error);
} }

View File

@ -0,0 +1,23 @@
import { Presenter } from "@erp/core/api";
import * as handlebars from "handlebars";
import { readFileSync } from "node:fs";
import path from "node:path";
import { CustomerInvoice } from "../../../domain";
export class CustomerInvoiceReportHTMLPresenter extends Presenter {
toOutput(customerInvoice: CustomerInvoice): string {
const dtoPresenter = this.presenterRegistry.getPresenter({
resource: "customer-invoice",
projection: "FULL",
});
const invoiceDTO = dtoPresenter.toOutput(customerInvoice);
// Obtener y compilar la plantilla HTML
const templateHtml = readFileSync(
path.join(__dirname, "./templates/quote/template.hbs")
).toString();
const template = handlebars.compile(templateHtml, {});
return template(invoiceDTO);
}
}

View File

@ -0,0 +1,53 @@
import { Presenter } from "@erp/core/api";
import puppeteer from "puppeteer";
import report from "puppeteer-report";
import { CustomerInvoice } from "../../../domain";
import { CustomerInvoiceReportHTMLPresenter } from "./customer-invoice.report.html";
export interface ICustomerInvoiceReporter {
toHTML: (invoice: CustomerInvoice) => Promise<string>;
toPDF: (invoice: CustomerInvoice) => Promise<Buffer>;
}
// https://plnkr.co/edit/lWk6Yd?preview
export class CustomerInvoiceReportPDFPresenter extends Presenter {
async toOutput(customerInvoice: CustomerInvoice): Promise<Buffer> {
const htmlPresenter = this.presenterRegistry.getPresenter({
resource: "customer-invoice",
projection: "REPORT",
format: "HTML",
}) as CustomerInvoiceReportHTMLPresenter;
const htmlData = htmlPresenter.toOutput(customerInvoice);
// Generar el PDF con Puppeteer
const browser = await puppeteer.launch({
args: [
"--disable-extensions",
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
"--disable-gpu",
],
});
const page = await browser.newPage();
const navigationPromise = page.waitForNavigation();
await page.setContent(htmlData, { waitUntil: "networkidle2" });
await navigationPromise;
const reportPDF = await report.pdfPage(page, {
format: "A4",
margin: {
bottom: "10mm",
left: "10mm",
right: "10mm",
top: "10mm",
},
});
await browser.close();
return Buffer.from(reportPDF);
}
}

View File

@ -1,97 +0,0 @@
import { toEmptyString } from "@repo/rdx-ddd";
import * as handlebars from "handlebars";
import { readFileSync } from "node:fs";
import path from "node:path";
import puppeteer from "puppeteer";
import report from "puppeteer-report";
import { CustomerInvoice } from "../../../domain";
export interface ICustomerInvoiceReporter {
toHTML: (invoice: CustomerInvoice) => Promise<string>;
toPDF: (invoice: CustomerInvoice) => Promise<Buffer>;
}
// https://plnkr.co/edit/lWk6Yd?preview
export const CustomerInvoiceReporter: ICustomerInvoiceReporter = {
toHTML: async (invoice: CustomerInvoice): Promise<string> => {
const quote_dto = await map(quote, context);
// Obtener y compilar la plantilla HTML
const templateHtml = readFileSync(
path.join(__dirname, "./templates/quote/template.hbs")
).toString();
const template = handlebars.compile(templateHtml, {});
return template(quote_dto);
},
toPDF: async (quote: CustomerInvoice, context: ISalesContext): Promise<Buffer> => {
const html = await CustomerInvoiceReporter.toHTML(quote, context);
// Generar el PDF con Puppeteer
const browser = await puppeteer.launch({
args: [
"--disable-extensions",
"--no-sandbox",
"--disable-setuid-sandbox",
"--disable-dev-shm-usage",
"--disable-gpu",
],
});
const page = await browser.newPage();
const navigationPromise = page.waitForNavigation();
await page.setContent(html, { waitUntil: "networkidle2" });
await navigationPromise;
const reportPDF = await report.pdfPage(page, {
format: "A4",
margin: {
bottom: "10mm",
left: "10mm",
right: "10mm",
top: "10mm",
},
});
await browser.close();
return Buffer.from(reportPDF);
},
};
const map = async (invoice: CustomerInvoice) => {
return {
id: invoice.id.toString(),
company_id: invoice.companyId.toString(),
invoice_number: invoice.invoiceNumber.toString(),
status: invoice.status.toPrimitive(),
series: toEmptyString(invoice.series, (value) => value.toString()),
invoice_date: invoice.invoiceDate.toDateString(),
operation_date: toEmptyString(invoice.operationDate, (value) => value.toDateString()),
notes: toEmptyString(invoice.notes, (value) => value.toString()),
language_code: invoice.languageCode.toString(),
currency_code: invoice.currencyCode.toString(),
};
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const quoteItemPresenter = (
items: ICollection<CustomerInvoiceItem>,
context: ISalesContext
): any[] =>
items.totalCount > 0
? items.items.map((item: CustomerInvoiceItem) => ({
id_article: item.idArticle.toString(),
description: item.description.toString(),
quantity: item.quantity.toFormat(),
unit_price: item.unitPrice.toFormat(),
subtotal_price: item.subtotalPrice.toFormat(),
discount: item.discount.toFormat(),
total_price: item.totalPrice.toFormat(),
}))
: [];
2;

View File

@ -1 +1,2 @@
export * from "./customer-invoice.reporter"; export * from "./customer-invoice.report.html";
export * from "./customer-invoice.report.pdf";

View File

@ -22,4 +22,10 @@ export class InvoiceTaxes extends Collection<InvoiceTax> {
InvoiceAmount.zero(taxableAmount.currencyCode) InvoiceAmount.zero(taxableAmount.currencyCode)
) as InvoiceAmount; ) as InvoiceAmount;
} }
public getCodesToString(): string {
return this.getAll()
.map((taxItem) => taxItem.tax.code)
.join(", ");
}
} }

View File

@ -22,4 +22,10 @@ export class ItemTaxes extends Collection<ItemTax> {
ItemAmount.zero(taxableAmount.currencyCode) ItemAmount.zero(taxableAmount.currencyCode)
); );
} }
public getCodesToString(): string {
return this.getAll()
.map((taxItem) => taxItem.tax.code)
.join(", ");
}
} }

View File

@ -23,6 +23,14 @@ export class ItemAmount extends MoneyValue {
return ItemAmount.create(props).data; return ItemAmount.create(props).data;
} }
toObjectString() {
return {
value: String(this.value),
scale: String(this.scale),
currency_code: this.currencyCode,
};
}
// Ensure fluent operations keep the subclass type // Ensure fluent operations keep the subclass type
convertScale(newScale: number) { convertScale(newScale: number) {
const mv = super.convertScale(newScale); const mv = super.convertScale(newScale);

View File

@ -7,13 +7,17 @@ import {
} from "@erp/core/api"; } from "@erp/core/api";
import { import {
CreateCustomerInvoiceUseCase, CreateCustomerInvoiceUseCase,
CustomerInvoiceFullPresenter,
CustomerInvoiceItemsFullPresenter,
CustomerInvoiceReportHTMLPresenter,
CustomerInvoiceReportPDFPresenter,
DeleteCustomerInvoiceUseCase, DeleteCustomerInvoiceUseCase,
GetCustomerInvoiceUseCase, GetCustomerInvoiceUseCase,
ListCustomerInvoicesPresenter, ListCustomerInvoicesPresenter,
ListCustomerInvoicesUseCase, ListCustomerInvoicesUseCase,
ReportCustomerInvoiceUseCase, ReportCustomerInvoiceUseCase,
UpdateCustomerInvoiceUseCase,
} from "../application"; } from "../application";
import { CustomerInvoiceService } from "../domain"; import { CustomerInvoiceService } from "../domain";
import { CustomerInvoiceFullMapper, CustomerInvoiceListMapper } from "./mappers"; import { CustomerInvoiceFullMapper, CustomerInvoiceListMapper } from "./mappers";
import { CustomerInvoiceRepository } from "./sequelize"; import { CustomerInvoiceRepository } from "./sequelize";
@ -31,7 +35,7 @@ type InvoiceDeps = {
list: () => ListCustomerInvoicesUseCase; list: () => ListCustomerInvoicesUseCase;
get: () => GetCustomerInvoiceUseCase; get: () => GetCustomerInvoiceUseCase;
create: () => CreateCustomerInvoiceUseCase; create: () => CreateCustomerInvoiceUseCase;
update: () => UpdateCustomerInvoiceUseCase; //update: () => UpdateCustomerInvoiceUseCase;
delete: () => DeleteCustomerInvoiceUseCase; delete: () => DeleteCustomerInvoiceUseCase;
report: () => ReportCustomerInvoiceUseCase; report: () => ReportCustomerInvoiceUseCase;
}; };
@ -64,12 +68,48 @@ export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps {
if (!_presenterRegistry) { if (!_presenterRegistry) {
_presenterRegistry = new InMemoryPresenterRegistry(); _presenterRegistry = new InMemoryPresenterRegistry();
_presenterRegistry.registerPresenter( _presenterRegistry.registerPresenters([
{ {
projection: "LIST", key: {
resource: "customer-invoice-items",
projection: "FULL",
},
presenter: new CustomerInvoiceItemsFullPresenter(_presenterRegistry),
}, },
new ListCustomerInvoicesPresenter() {
); key: {
resource: "customer-invoice",
projection: "FULL",
},
presenter: new CustomerInvoiceFullPresenter(_presenterRegistry),
},
{
key: {
resource: "customer-invoice",
projection: "LIST",
},
presenter: new ListCustomerInvoicesPresenter(_presenterRegistry),
},
{
key: {
resource: "customer-invoice",
projection: "REPORT",
format: "HTML",
},
presenter: new CustomerInvoiceReportHTMLPresenter(_presenterRegistry),
},
{
key: {
resource: "customer-invoice",
projection: "REPORT",
format: "PDF",
},
presenter: new CustomerInvoiceReportPDFPresenter(_presenterRegistry),
},
]);
} }
return { return {
@ -82,38 +122,25 @@ export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps {
build: { build: {
list: () => list: () =>
new ListCustomerInvoicesUseCase( new ListCustomerInvoicesUseCase(_service!, transactionManager!, _presenterRegistry!),
_service!, get: () => new GetCustomerInvoiceUseCase(_service!, transactionManager!, _presenterRegistry!),
transactionManager!,
_presenterRegistry?.getPresenter({ projection: "LIST" })
),
get: () =>
new GetCustomerInvoiceUseCase(
_service!,
transactionManager!,
_presenterRegistry?.getPresenter({ projection: "FULL" })
),
create: () => create: () =>
new CreateCustomerInvoiceUseCase( new CreateCustomerInvoiceUseCase(
_service!, _service!,
transactionManager!, transactionManager!,
_presenterRegistry?.getPresenter({ projection: "FULL" }), _presenterRegistry!,
_catalogs!.taxes _catalogs!.taxes
), ),
update: () => /*update: () =>
new UpdateCustomerInvoiceUseCase( new UpdateCustomerInvoiceUseCase(
_service!, _service!,
transactionManager!, transactionManager!,
_presenterRegistry?.getPresenter({ projection: "FULL" }), _presenterRegistry!,
_catalogs!.taxes _catalogs!.taxes
), ),*/
delete: () => new DeleteCustomerInvoiceUseCase(_service!, transactionManager!), delete: () => new DeleteCustomerInvoiceUseCase(_service!, transactionManager!),
report: () => report: () =>
new ReportCustomerInvoiceUseCase( new ReportCustomerInvoiceUseCase(_service!, transactionManager!, _presenterRegistry!),
_service!,
transactionManager!,
_presenterRegistry?.getPresenter({ projection: "REPORT" })
),
}, },
}; };
} }

View File

@ -3,3 +3,4 @@ export * from "./delete-customer-invoice.controller";
export * from "./get-customer-invoice.controller"; export * from "./get-customer-invoice.controller";
export * from "./list-customer-invoices.controller"; export * from "./list-customer-invoices.controller";
///export * from "./update-customer-invoice.controller"; ///export * from "./update-customer-invoice.controller";
export * from "./report-customer-invoice.controller";

View File

@ -1,5 +1,5 @@
import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api"; import { ExpressController, authGuard, forbidQueryFieldGuard, tenantGuard } from "@erp/core/api";
import { GetCustomerInvoiceUseCase, GetCustomerInvoiceUseCase as ReportCustomerInvoiceUseCase } from "../../../application"; import { GetCustomerInvoiceUseCase as ReportCustomerInvoiceUseCase } from "../../../application";
export class ReportCustomerInvoiceController extends ExpressController { export class ReportCustomerInvoiceController extends ExpressController {
public constructor(private readonly useCase: ReportCustomerInvoiceUseCase) { public constructor(private readonly useCase: ReportCustomerInvoiceUseCase) {
@ -12,13 +12,10 @@ export class ReportCustomerInvoiceController extends ExpressController {
const companyId = this.getTenantId()!; // garantizado por tenantGuard const companyId = this.getTenantId()!; // garantizado por tenantGuard
const { invoice_id } = this.req.params; const { invoice_id } = this.req.params;
const getUseCase = getUsecaasdasd; const result = await this.useCase.execute({ invoice_id, companyId });
const invoiceDto = await
const result = await this.useCase.execute({ invoice_id, companyId, });
return result.match( return result.match(
(data) => this.downloadPDF(result.data), ({ pdfData, filename }) => this.downloadPDF(pdfData, filename),
(err) => this.handleError(err) (err) => this.handleError(err)
); );
} }

View File

@ -15,6 +15,7 @@ import {
DeleteCustomerInvoiceController, DeleteCustomerInvoiceController,
GetCustomerInvoiceController, GetCustomerInvoiceController,
ListCustomerInvoicesController, ListCustomerInvoicesController,
ReportCustomerInvoiceController,
} from "./controllers"; } from "./controllers";
export const customerInvoicesRouter = (params: ModuleParams) => { export const customerInvoicesRouter = (params: ModuleParams) => {

View File

@ -134,7 +134,7 @@ export class CustomerInvoiceRepository
transaction: Transaction transaction: Transaction
): Promise<Result<CustomerInvoice, Error>> { ): Promise<Result<CustomerInvoice, Error>> {
try { try {
const mapper: ICustomerInvoiceFullMapper = this._registry.getReadModelMapper("FULL"); const mapper: ICustomerInvoiceFullMapper = this._registry.getDomainMapper("FULL");
const { CustomerModel } = this._database.models; const { CustomerModel } = this._database.models;
const row = await CustomerInvoiceModel.findOne({ const row = await CustomerInvoiceModel.findOne({

View File

@ -1,4 +1,10 @@
import { AmountSchema, MetadataSchema, PercentageSchema, QuantitySchema } from "@erp/core"; import {
AmountSchema,
MetadataSchema,
MoneySchema,
PercentageSchema,
QuantitySchema,
} from "@erp/core";
import * as z from "zod/v4"; import * as z from "zod/v4";
export const CreateCustomerInvoiceResponseSchema = z.object({ export const CreateCustomerInvoiceResponseSchema = z.object({
@ -17,12 +23,12 @@ export const CreateCustomerInvoiceResponseSchema = z.object({
language_code: z.string(), language_code: z.string(),
currency_code: z.string(), currency_code: z.string(),
subtotal_amount: AmountSchema, subtotal_amount: MoneySchema,
discount_percentage: PercentageSchema, discount_percentage: PercentageSchema,
discount_amount: AmountSchema, discount_amount: MoneySchema,
taxable_amount: AmountSchema, taxable_amount: MoneySchema,
tax_amount: AmountSchema, taxes_amount: MoneySchema,
total_amount: AmountSchema, total_amount: MoneySchema,
items: z.array( items: z.array(
z.object({ z.object({
@ -31,8 +37,15 @@ export const CreateCustomerInvoiceResponseSchema = z.object({
description: z.string(), description: z.string(),
quantity: QuantitySchema, quantity: QuantitySchema,
unit_amount: AmountSchema, unit_amount: AmountSchema,
taxes: z.string(),
subtotal_amount: MoneySchema,
discount_percentage: PercentageSchema, discount_percentage: PercentageSchema,
total_amount: AmountSchema, discount_amount: MoneySchema,
taxable_amount: MoneySchema,
taxes_amount: MoneySchema,
total_amount: MoneySchema,
}) })
), ),

View File

@ -1,4 +1,4 @@
import { AmountSchema, MetadataSchema, PercentageSchema, QuantitySchema } from "@erp/core"; import { MetadataSchema, MoneySchema, PercentageSchema, QuantitySchema } from "@erp/core";
import * as z from "zod/v4"; import * as z from "zod/v4";
export const GetCustomerInvoiceByIdResponseSchema = z.object({ export const GetCustomerInvoiceByIdResponseSchema = z.object({
@ -19,11 +19,12 @@ export const GetCustomerInvoiceByIdResponseSchema = z.object({
taxes: z.string(), taxes: z.string(),
subtotal_amount: AmountSchema, subtotal_amount: MoneySchema,
discount_percentage: PercentageSchema, discount_percentage: PercentageSchema,
discount_amount: AmountSchema, discount_amount: MoneySchema,
taxable_amount: AmountSchema, taxable_amount: MoneySchema,
total_amount: AmountSchema, taxes_amount: MoneySchema,
total_amount: MoneySchema,
items: z.array( items: z.array(
z.object({ z.object({
@ -31,10 +32,16 @@ export const GetCustomerInvoiceByIdResponseSchema = z.object({
position: z.string(), position: z.string(),
description: z.string(), description: z.string(),
quantity: QuantitySchema, quantity: QuantitySchema,
unit_amount: AmountSchema, unit_amount: MoneySchema,
discount_percentage: PercentageSchema,
taxes: z.string(), taxes: z.string(),
total_amount: AmountSchema,
subtotal_amount: MoneySchema,
discount_percentage: PercentageSchema,
discount_amount: MoneySchema,
taxable_amount: MoneySchema,
taxes_amount: MoneySchema,
total_amount: MoneySchema,
}) })
), ),

View File

@ -1,4 +1,4 @@
import { AmountSchema, MetadataSchema, createListViewResponseSchema } from "@erp/core"; import { MetadataSchema, MoneySchema, createListViewResponseSchema } from "@erp/core";
import * as z from "zod/v4"; import * as z from "zod/v4";
export const ListCustomerInvoiceResponseSchema = createListViewResponseSchema( export const ListCustomerInvoiceResponseSchema = createListViewResponseSchema(
@ -30,11 +30,11 @@ export const ListCustomerInvoiceResponseSchema = createListViewResponseSchema(
taxes: z.string(), taxes: z.string(),
subtotal_amount: AmountSchema, subtotal_amount: MoneySchema,
discount_amount: AmountSchema, discount_amount: MoneySchema,
taxable_amount: AmountSchema, taxable_amount: MoneySchema,
taxes_amount: AmountSchema, taxes_amount: MoneySchema,
total_amount: AmountSchema, total_amount: MoneySchema,
metadata: MetadataSchema.optional(), metadata: MetadataSchema.optional(),
}) })

View File

@ -7,7 +7,7 @@ import { CSSProperties, PropsWithChildren, createContext, useMemo } from "react"
import { CustomerInvoiceItemsSortableProps } from "./customer-invoice-items-sortable-datatable"; import { CustomerInvoiceItemsSortableProps } from "./customer-invoice-items-sortable-datatable";
interface Context { interface Context {
attributes: Record<string, any>; attributes: Record<string, unknown>;
listeners: DraggableSyntheticListeners; listeners: DraggableSyntheticListeners;
ref(node: HTMLElement | null): void; ref(node: HTMLElement | null): void;
} }

View File

@ -1,16 +1,10 @@
import type { IMapperRegistry, IPresenterRegistry, ModuleParams } from "@erp/core/api"; import type { IMapperRegistry, IPresenterRegistry, ModuleParams } from "@erp/core/api";
import { import { InMemoryMapperRegistry, SequelizeTransactionManager } from "@erp/core/api";
InMemoryMapperRegistry,
InMemoryPresenterRegistry,
SequelizeTransactionManager,
} from "@erp/core/api";
import { import {
CreateCustomerUseCase, CreateCustomerUseCase,
DeleteCustomerUseCase, DeleteCustomerUseCase,
GetCustomerAssembler,
GetCustomerUseCase, GetCustomerUseCase,
ListCustomersAssembler,
ListCustomersUseCase, ListCustomersUseCase,
UpdateCustomerUseCase, UpdateCustomerUseCase,
} from "../application"; } from "../application";
@ -33,7 +27,7 @@ type CustomerDeps = {
}; };
}; };
let _presenterRegistry: IPresenterRegistry | null = null; const _presenterRegistry: IPresenterRegistry | null = null;
let _mapperRegistry: IMapperRegistry | null = null; let _mapperRegistry: IMapperRegistry | null = null;
let _repo: CustomerRepository | null = null; let _repo: CustomerRepository | null = null;
@ -51,42 +45,34 @@ export function getCustomerDependencies(params: ModuleParams): CustomerDeps {
if (!_repo) _repo = new CustomerRepository({ mapperRegistry: _mapperRegistry }); if (!_repo) _repo = new CustomerRepository({ mapperRegistry: _mapperRegistry });
if (!_service) _service = new CustomerService(_repo); if (!_service) _service = new CustomerService(_repo);
if (!_presenterRegistry) { /*if (!_presenterRegistry) {
_presenterRegistry = new InMemoryPresenterRegistry(); _presenterRegistry = new InMemoryPresenterRegistry();
_presenterRegistry.registerPresenters([ _presenterRegistry.registerPresenters([
{ {
key: { projection: "FULL" }, key: { resource: "customer", projection: "FULL" },
presenter: new ListCustomersAssembler(), presenter: new ListCustomersAssembler(),
}, },
{ {
key: { projection: "LIST" }, key: { resource: "customer", projection: "LIST" },
presenter: new GetCustomerAssembler(), presenter: new GetCustomerAssembler(),
}, },
]); ]);
}*/
/*if (!_assemblers) {
_assemblers = {
list: new ListCustomersAssembler(), // transforma domain → ListDTO
get: new GetCustomerAssembler(), // transforma domain → DetailDTO
create: new CreateCustomersAssembler(), // transforma domain → CreatedDTO
update: new UpdateCustomerAssembler(), // transforma domain -> UpdateDTO
};*/
}
return { return {
transactionManager, transactionManager,
repo: _repo, repo: _repo,
mapperRegistry: _mapperRegistry, mapperRegistry: _mapperRegistry,
presenterRegistry: _presenterRegistry, //presenterRegistry: _presenterRegistry,
service: _service, service: _service,
build: { build: {
list: () => new ListCustomersUseCase(_service!, transactionManager!, _assemblers!.list), /*list: () => new ListCustomersUseCase(_service!, transactionManager!, presenterRegistry!),
get: () => new GetCustomerUseCase(_service!, transactionManager!, _assemblers!.get), get: () => new GetCustomerUseCase(_service!, transactionManager!, presenterRegistry!),
create: () => new CreateCustomerUseCase(_service!, transactionManager!, _assemblers!.create), create: () => new CreateCustomerUseCase(_service!, transactionManager!, presenterRegistry!),
update: () => new UpdateCustomerUseCase(_service!, transactionManager!, _assemblers!.update), update: () => new UpdateCustomerUseCase(_service!, transactionManager!, presenterRegistry!),
delete: () => new DeleteCustomerUseCase(_service!, transactionManager!), delete: () => new DeleteCustomerUseCase(_service!, transactionManager!),*/
}, },
}; };
} }

View File

@ -81,4 +81,11 @@ export class Percentage extends ValueObject<PercentageProps> {
toString(): string { toString(): string {
return `${this.toNumber().toFixed(this.scale)}%`; return `${this.toNumber().toFixed(this.scale)}%`;
} }
toObjectString() {
return {
value: String(this.value),
scale: String(this.scale),
};
}
} }

View File

@ -62,6 +62,13 @@ export class Quantity extends ValueObject<QuantityProps> {
return this.toNumber().toFixed(this.scale); return this.toNumber().toFixed(this.scale);
} }
toObjectString() {
return {
value: String(this.value),
scale: String(this.scale),
};
}
isZero(): boolean { isZero(): boolean {
return this.value === 0; return this.value === 0;
} }