Proformas preview
This commit is contained in:
parent
cc14cdacf1
commit
588e9dd733
@ -39,6 +39,7 @@
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@erp/companies": "workspace:*",
|
||||
"@erp/core": "workspace:*",
|
||||
"@erp/catalogs": "workspace:*",
|
||||
"@erp/customers": "workspace:*",
|
||||
|
||||
@ -4,6 +4,7 @@ import type { IIssuedInvoicePublicServices } from "../../issued-invoices";
|
||||
import type { ICreateProformaInputMapper, IUpdateProformaInputMapper } from "../mappers";
|
||||
import type { IProformaRepository } from "../repositories";
|
||||
import type {
|
||||
ICompanyReportProfileFinder,
|
||||
IProformaCreator,
|
||||
IProformaFinder,
|
||||
IProformaFullReadModelAssembler,
|
||||
@ -24,7 +25,8 @@ import {
|
||||
GetProformaByIdUseCase,
|
||||
IssueProformaUseCase,
|
||||
ListProformasUseCase,
|
||||
ReportProformaUseCase,
|
||||
PreviewProformaReportUseCase,
|
||||
ReportProformaPdfUseCase,
|
||||
UpdateProformaByIdUseCase,
|
||||
} from "../use-cases";
|
||||
|
||||
@ -54,16 +56,38 @@ export function buildListProformasUseCase(deps: {
|
||||
);
|
||||
}
|
||||
|
||||
export function buildReportProformaUseCase(deps: {
|
||||
export function buildReportProformaPdfUseCase(deps: {
|
||||
finder: IProformaFinder;
|
||||
companyReportProfileFinder: ICompanyReportProfileFinder;
|
||||
fullReadModelAssembler: IProformaFullReadModelAssembler;
|
||||
fullSnapshotBuilder: IProformaFullSnapshotBuilder;
|
||||
reportSnapshotBuilder: IProformaReportSnapshotBuilder;
|
||||
documentService: ProformaDocumentGeneratorService;
|
||||
transactionManager: ITransactionManager;
|
||||
}) {
|
||||
return new ReportProformaUseCase({
|
||||
return new ReportProformaPdfUseCase({
|
||||
finder: deps.finder,
|
||||
companyReportProfileFinder: deps.companyReportProfileFinder,
|
||||
fullReadModelAssembler: deps.fullReadModelAssembler,
|
||||
fullSnapshotBuilder: deps.fullSnapshotBuilder,
|
||||
reportSnapshotBuilder: deps.reportSnapshotBuilder,
|
||||
documentService: deps.documentService,
|
||||
transactionManager: deps.transactionManager,
|
||||
});
|
||||
}
|
||||
|
||||
export function buildPreviewProformaReportUseCase(deps: {
|
||||
finder: IProformaFinder;
|
||||
companyReportProfileFinder: ICompanyReportProfileFinder;
|
||||
fullReadModelAssembler: IProformaFullReadModelAssembler;
|
||||
fullSnapshotBuilder: IProformaFullSnapshotBuilder;
|
||||
reportSnapshotBuilder: IProformaReportSnapshotBuilder;
|
||||
documentService: ProformaDocumentGeneratorService;
|
||||
transactionManager: ITransactionManager;
|
||||
}) {
|
||||
return new PreviewProformaReportUseCase({
|
||||
finder: deps.finder,
|
||||
companyReportProfileFinder: deps.companyReportProfileFinder,
|
||||
fullReadModelAssembler: deps.fullReadModelAssembler,
|
||||
fullSnapshotBuilder: deps.fullSnapshotBuilder,
|
||||
reportSnapshotBuilder: deps.reportSnapshotBuilder,
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
export class CompanyReportProfileNotFoundError extends Error {
|
||||
public readonly code = "COMPANY_REPORT_PROFILE_NOT_FOUND";
|
||||
|
||||
public constructor(companyId: string) {
|
||||
super(
|
||||
`Cannot generate the proforma report because the company document profile is missing for company ${companyId}.`
|
||||
);
|
||||
this.name = "CompanyReportProfileNotFoundError";
|
||||
}
|
||||
}
|
||||
|
||||
export const isCompanyReportProfileNotFoundError = (
|
||||
error: unknown
|
||||
): error is CompanyReportProfileNotFoundError =>
|
||||
error instanceof CompanyReportProfileNotFoundError;
|
||||
@ -0,0 +1 @@
|
||||
export * from "./company-report-profile-not-found.error";
|
||||
@ -1,4 +1,5 @@
|
||||
export * from "./di";
|
||||
export * from "./errors";
|
||||
export * from "./mappers";
|
||||
export * from "./models";
|
||||
export * from "./repositories";
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
import type { Maybe } from "@repo/rdx-utils";
|
||||
|
||||
export type CompanyReportLogo = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type CompanyReportAddress = {
|
||||
street: string;
|
||||
street2: Maybe<string>;
|
||||
postalCode: Maybe<string>;
|
||||
city: Maybe<string>;
|
||||
province: Maybe<string>;
|
||||
country: Maybe<string>;
|
||||
};
|
||||
|
||||
export type CompanyReportProfile = {
|
||||
companyId: string;
|
||||
slug: string;
|
||||
legalName: string;
|
||||
tradeName: Maybe<string>;
|
||||
tin: Maybe<string>;
|
||||
email: Maybe<string>;
|
||||
phone: Maybe<string>;
|
||||
website: Maybe<string>;
|
||||
locale: Maybe<string>;
|
||||
currencyCode: Maybe<string>;
|
||||
logo: Maybe<CompanyReportLogo>;
|
||||
address: Maybe<CompanyReportAddress>;
|
||||
version: Maybe<string>;
|
||||
};
|
||||
@ -1,5 +1,7 @@
|
||||
export * from "./company-report-profile.model";
|
||||
export * from "./proforma-create-input.model";
|
||||
export * from "./proforma-full-read.model";
|
||||
export * from "./proforma-item-tax-codes-input.model";
|
||||
export * from "./proforma-summary";
|
||||
export * from "./proforma-update-input.model";
|
||||
export * from "./report-localization.model";
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
export type ReportLocalization = {
|
||||
languageCode: string;
|
||||
locale: string;
|
||||
currencyCode: string;
|
||||
};
|
||||
@ -0,0 +1,11 @@
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import type { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { CompanyReportProfile } from "../models";
|
||||
|
||||
export interface ICompanyReportProfileFinder {
|
||||
findByCompanyId(
|
||||
companyId: UniqueID,
|
||||
transaction?: unknown
|
||||
): Promise<Result<Maybe<CompanyReportProfile>, Error>>;
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
export * from "./assemblers";
|
||||
export * from "./catalog-resolver";
|
||||
export * from "./company-report-profile-finder";
|
||||
export * from "./proforma-creator";
|
||||
export * from "./proforma-document-generator.interface";
|
||||
export * from "./proforma-document-metadata-factory";
|
||||
export * from "./proforma-document-properties-factory";
|
||||
export * from "./proforma-finder";
|
||||
export * from "./proforma-issuer";
|
||||
|
||||
@ -1,47 +0,0 @@
|
||||
import type { IDocumentMetadata, IDocumentMetadataFactory } from "@erp/core/api";
|
||||
|
||||
import type { ProformaReportSnapshot } from "../snapshot-builders";
|
||||
|
||||
/**
|
||||
* Construye los metadatos del documento PDF de una factura emitida.
|
||||
*
|
||||
* - Application-level
|
||||
* - Determinista
|
||||
* - Sin IO
|
||||
*/
|
||||
export class ProformaDocumentMetadataFactory
|
||||
implements IDocumentMetadataFactory<ProformaReportSnapshot>
|
||||
{
|
||||
build(snapshot: ProformaReportSnapshot): IDocumentMetadata {
|
||||
if (!snapshot.id) {
|
||||
throw new Error("ProformaReportSnapshot.id is required");
|
||||
}
|
||||
|
||||
if (!snapshot.company_id) {
|
||||
throw new Error("ProformaReportSnapshot.companyId is required");
|
||||
}
|
||||
|
||||
return {
|
||||
documentType: "proforma",
|
||||
documentId: snapshot.id,
|
||||
companyId: snapshot.company_id,
|
||||
companySlug: snapshot.company_slug,
|
||||
format: "PDF",
|
||||
languageCode: snapshot.language_code ?? "es",
|
||||
filename: this.buildFilename(snapshot),
|
||||
storageKey: this.buildCacheKey(snapshot),
|
||||
};
|
||||
}
|
||||
|
||||
private buildFilename(snapshot: ProformaReportSnapshot): string {
|
||||
// Ejemplo: factura-F2024-000123-FULANITO.pdf
|
||||
return `factura-${snapshot.series}${snapshot.invoice_number}-${snapshot.recipient.name}.pdf`;
|
||||
}
|
||||
|
||||
private buildCacheKey(snapshot: ProformaReportSnapshot): string {
|
||||
// Versionado explícito para invalidaciones futuras
|
||||
return ["proforma", snapshot.company_id, snapshot.series, snapshot.invoice_number, "v1"].join(
|
||||
":"
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -14,9 +14,9 @@ export class ProformaDocumentPropertiesFactory
|
||||
{
|
||||
build(snapshot: ProformaReportSnapshot): IDocumentProperties {
|
||||
return {
|
||||
title: snapshot.reference,
|
||||
title: snapshot.reference ?? `Proforma ${snapshot.series}${snapshot.invoice_number}`,
|
||||
subject: "proforma",
|
||||
author: snapshot.company_slug,
|
||||
author: snapshot.company.legalName,
|
||||
creator: "FactuGES ERP",
|
||||
};
|
||||
}
|
||||
|
||||
@ -2,6 +2,5 @@ export * from "./proforma-items-report-snapshot-builder";
|
||||
export * from "./proforma-report-item-snapshot.interface";
|
||||
export * from "./proforma-report-snapshot.interface";
|
||||
export * from "./proforma-report-snapshot-builder";
|
||||
export * from "./proforma-report-snapshot-builder";
|
||||
export * from "./proforma-report-tax-snapshot.interface";
|
||||
export * from "./proforma-tax-report-snapshot-builder";
|
||||
|
||||
@ -2,18 +2,21 @@ import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/cor
|
||||
import type { ISnapshotBuilder, ISnapshotBuilderParams } from "@erp/core/api";
|
||||
|
||||
import type { ProformaFullSnapshot } from "../full";
|
||||
|
||||
import type { ProformaReportItemSnapshot } from "./proforma-report-item-snapshot.interface";
|
||||
|
||||
type ProformaItemReportSnapshotBuilderParams = ISnapshotBuilderParams & {
|
||||
locale: string;
|
||||
};
|
||||
|
||||
export interface IProformaItemReportSnapshotBuilder
|
||||
extends ISnapshotBuilder<ProformaFullSnapshot["items"], ProformaReportItemSnapshot[]> {}
|
||||
|
||||
export class ProformaItemReportSnapshotBuilder implements IProformaItemReportSnapshotBuilder {
|
||||
toOutput(
|
||||
items: ProformaFullSnapshot["items"],
|
||||
params?: ISnapshotBuilderParams
|
||||
params?: ProformaItemReportSnapshotBuilderParams
|
||||
): ProformaReportItemSnapshot[] {
|
||||
const locale = params?.locale as string;
|
||||
const locale = params?.locale ?? "es-ES";
|
||||
|
||||
const moneyOptions = {
|
||||
hideZeros: true,
|
||||
@ -21,14 +24,18 @@ export class ProformaItemReportSnapshotBuilder implements IProformaItemReportSna
|
||||
};
|
||||
|
||||
return items.map((item) => ({
|
||||
description: item.description,
|
||||
quantity: QuantityDTOHelper.format(item.quantity, locale, { minimumFractionDigits: 0 }),
|
||||
unit_amount: MoneyDTOHelper.format(item.unit_amount, locale, moneyOptions),
|
||||
description: item.description ?? "",
|
||||
quantity: item.quantity
|
||||
? QuantityDTOHelper.format(item.quantity, locale, { minimumFractionDigits: 0 })
|
||||
: "",
|
||||
unit_amount: item.unit_amount ? MoneyDTOHelper.format(item.unit_amount, locale, moneyOptions) : "",
|
||||
subtotal_amount: MoneyDTOHelper.format(item.subtotal_amount, locale, moneyOptions),
|
||||
discount_percentage: PercentageDTOHelper.format(item.discount_percentage, locale, {
|
||||
minimumFractionDigits: 0,
|
||||
}),
|
||||
discount_amount: MoneyDTOHelper.format(item.discount_amount, locale, moneyOptions),
|
||||
discount_percentage: item.item_discount_percentage
|
||||
? PercentageDTOHelper.format(item.item_discount_percentage, locale, {
|
||||
minimumFractionDigits: 0,
|
||||
})
|
||||
: "",
|
||||
discount_amount: MoneyDTOHelper.format(item.item_discount_amount, locale, moneyOptions),
|
||||
taxable_amount: MoneyDTOHelper.format(item.taxable_amount, locale, moneyOptions),
|
||||
taxes_amount: MoneyDTOHelper.format(item.taxes_amount, locale, moneyOptions),
|
||||
total_amount: MoneyDTOHelper.format(item.total_amount, locale, moneyOptions),
|
||||
|
||||
@ -2,12 +2,17 @@ import { MoneyDTOHelper, PercentageDTOHelper } from "@erp/core";
|
||||
import type { ISnapshotBuilder, ISnapshotBuilderParams } from "@erp/core/api";
|
||||
import { DateHelper } from "@repo/rdx-utils";
|
||||
|
||||
import type { CompanyReportProfile, ReportLocalization } from "../../models";
|
||||
import type { ProformaFullSnapshot } from "../full";
|
||||
|
||||
import type { ProformaReportItemSnapshot } from "./proforma-report-item-snapshot.interface";
|
||||
import type { ProformaReportSnapshot } from "./proforma-report-snapshot.interface";
|
||||
import type { ProformaReportTaxSnapshot } from "./proforma-report-tax-snapshot.interface";
|
||||
|
||||
export type ProformaReportSnapshotBuilderParams = ISnapshotBuilderParams & {
|
||||
company: CompanyReportProfile;
|
||||
localization: ReportLocalization;
|
||||
};
|
||||
|
||||
export interface IProformaReportSnapshotBuilder
|
||||
extends ISnapshotBuilder<ProformaFullSnapshot, ProformaReportSnapshot> {}
|
||||
|
||||
@ -25,9 +30,20 @@ export class ProformaReportSnapshotBuilder implements IProformaReportSnapshotBui
|
||||
|
||||
toOutput(
|
||||
snapshot: ProformaFullSnapshot,
|
||||
params?: ISnapshotBuilderParams
|
||||
params?: ProformaReportSnapshotBuilderParams
|
||||
): ProformaReportSnapshot {
|
||||
const locale = params?.locale as string;
|
||||
const localization = params?.localization;
|
||||
const company = params?.company;
|
||||
|
||||
if (!localization) {
|
||||
throw new Error("Report localization is required");
|
||||
}
|
||||
|
||||
if (!company) {
|
||||
throw new Error("Company report profile is required");
|
||||
}
|
||||
|
||||
const { locale } = localization;
|
||||
|
||||
const moneyOptions = {
|
||||
hideZeros: true,
|
||||
@ -37,22 +53,23 @@ export class ProformaReportSnapshotBuilder implements IProformaReportSnapshotBui
|
||||
return {
|
||||
id: snapshot.id,
|
||||
company_id: snapshot.company_id,
|
||||
company_slug: "rodax",
|
||||
invoice_number: snapshot.invoice_number,
|
||||
series: snapshot.series,
|
||||
status: snapshot.status,
|
||||
reference: snapshot.reference,
|
||||
localization,
|
||||
company,
|
||||
|
||||
language_code: snapshot.language_code,
|
||||
currency_code: snapshot.currency_code,
|
||||
|
||||
invoice_date: DateHelper.format(snapshot.invoice_date, locale),
|
||||
|
||||
payment_method: snapshot.payment_method?.payment_description ?? "",
|
||||
payment_method: snapshot.payment_method?.description ?? snapshot.payment_method?.name ?? "",
|
||||
notes: snapshot.notes,
|
||||
|
||||
recipient: {
|
||||
name: snapshot.recipient.name,
|
||||
name: snapshot.recipient.name ?? "",
|
||||
tin: snapshot.recipient.tin,
|
||||
format_address: this.formatAddress(snapshot.recipient),
|
||||
},
|
||||
|
||||
@ -1,25 +1,28 @@
|
||||
import type { CompanyReportProfile, ReportLocalization } from "../../models";
|
||||
import type { ProformaReportItemSnapshot } from "./proforma-report-item-snapshot.interface";
|
||||
import type { ProformaReportTaxSnapshot } from "./proforma-report-tax-snapshot.interface";
|
||||
|
||||
export interface ProformaReportSnapshot {
|
||||
id: string;
|
||||
company_id: string;
|
||||
company_slug: string;
|
||||
invoice_number: string;
|
||||
series: string;
|
||||
series: string | null;
|
||||
status: string;
|
||||
reference: string;
|
||||
reference: string | null;
|
||||
|
||||
localization: ReportLocalization;
|
||||
company: CompanyReportProfile;
|
||||
|
||||
language_code: string;
|
||||
currency_code: string;
|
||||
|
||||
invoice_date: string;
|
||||
payment_method: string;
|
||||
notes: string;
|
||||
notes: string | null;
|
||||
|
||||
recipient: {
|
||||
name: string;
|
||||
tin: string;
|
||||
tin: string | null;
|
||||
format_address: string;
|
||||
};
|
||||
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
import { MoneyDTOHelper, PercentageDTOHelper } from "@erp/core";
|
||||
import type { ISnapshotBuilder, ISnapshotBuilderParams } from "@erp/core/api";
|
||||
|
||||
import type { ProformaFullSnapshot, ProformaReportTaxSnapshot } from "../../application-models";
|
||||
import type { ProformaFullSnapshot } from "../full";
|
||||
import type { ProformaReportTaxSnapshot } from "./proforma-report-tax-snapshot.interface";
|
||||
|
||||
type ProformaTaxReportSnapshotBuilderParams = ISnapshotBuilderParams & {
|
||||
locale: string;
|
||||
};
|
||||
|
||||
export interface IProformaTaxReportSnapshotBuilder
|
||||
extends ISnapshotBuilder<ProformaFullSnapshot["taxes"], ProformaReportTaxSnapshot[]> {}
|
||||
@ -9,9 +14,9 @@ export interface IProformaTaxReportSnapshotBuilder
|
||||
export class ProformaTaxReportSnapshotBuilder implements IProformaTaxReportSnapshotBuilder {
|
||||
toOutput(
|
||||
taxes: ProformaFullSnapshot["taxes"],
|
||||
params?: ISnapshotBuilderParams
|
||||
params?: ProformaTaxReportSnapshotBuilderParams
|
||||
): ProformaReportTaxSnapshot[] {
|
||||
const locale = params?.locale as string;
|
||||
const locale = params?.locale ?? "es-ES";
|
||||
|
||||
const moneyOptions = {
|
||||
hideZeros: true,
|
||||
@ -21,16 +26,20 @@ export class ProformaTaxReportSnapshotBuilder implements IProformaTaxReportSnaps
|
||||
return taxes.map((tax) => ({
|
||||
taxable_amount: MoneyDTOHelper.format(tax.taxable_amount, locale, moneyOptions),
|
||||
|
||||
iva_code: tax.iva_code,
|
||||
iva_percentage: PercentageDTOHelper.format(tax.iva_percentage, locale),
|
||||
iva_code: tax.iva_code ?? "",
|
||||
iva_percentage: tax.iva_percentage
|
||||
? PercentageDTOHelper.format(tax.iva_percentage, locale)
|
||||
: "",
|
||||
iva_amount: MoneyDTOHelper.format(tax.iva_amount, locale, moneyOptions),
|
||||
|
||||
rec_code: tax.rec_code,
|
||||
rec_percentage: PercentageDTOHelper.format(tax.rec_percentage, locale),
|
||||
rec_code: tax.rec_code ?? "",
|
||||
rec_percentage: tax.rec_percentage ? PercentageDTOHelper.format(tax.rec_percentage, locale) : "",
|
||||
rec_amount: MoneyDTOHelper.format(tax.rec_amount, locale, moneyOptions),
|
||||
|
||||
retention_code: tax.retention_code,
|
||||
retention_percentage: PercentageDTOHelper.format(tax.retention_percentage, locale),
|
||||
retention_code: tax.retention_code ?? "",
|
||||
retention_percentage: tax.retention_percentage
|
||||
? PercentageDTOHelper.format(tax.retention_percentage, locale)
|
||||
: "",
|
||||
retention_amount: MoneyDTOHelper.format(tax.retention_amount, locale, moneyOptions),
|
||||
|
||||
taxes_amount: MoneyDTOHelper.format(tax.taxes_amount, locale, moneyOptions),
|
||||
|
||||
@ -4,5 +4,6 @@ export * from "./create-proforma.use-case";
|
||||
export * from "./get-proforma-by-id.use-case";
|
||||
export * from "./issue-proforma.use-case";
|
||||
export * from "./list-proformas.use-case";
|
||||
export * from "./report-proforma.use-case";
|
||||
export * from "./preview-proforma-report.use-case";
|
||||
export * from "./report-proforma-pdf.use-case";
|
||||
export * from "./update-proforma-by-id.use-case";
|
||||
|
||||
@ -0,0 +1,124 @@
|
||||
import type { ITransactionManager } from "@erp/core/api";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import { CompanyReportProfileNotFoundError } from "../errors";
|
||||
import type {
|
||||
ICompanyReportProfileFinder,
|
||||
IProformaFinder,
|
||||
IProformaFullReadModelAssembler,
|
||||
ProformaDocumentGeneratorService,
|
||||
} from "../services";
|
||||
import type { ReportLocalization } from "../models";
|
||||
import type { IProformaFullSnapshotBuilder } from "../snapshot-builders";
|
||||
import type { IProformaReportSnapshotBuilder } from "../snapshot-builders/report";
|
||||
|
||||
type PreviewProformaReportUseCaseInput = {
|
||||
companyId: UniqueID;
|
||||
proforma_id: string;
|
||||
};
|
||||
|
||||
export class PreviewProformaReportUseCase {
|
||||
public constructor(
|
||||
private readonly deps: {
|
||||
finder: IProformaFinder;
|
||||
companyReportProfileFinder: ICompanyReportProfileFinder;
|
||||
fullReadModelAssembler: IProformaFullReadModelAssembler;
|
||||
fullSnapshotBuilder: IProformaFullSnapshotBuilder;
|
||||
reportSnapshotBuilder: IProformaReportSnapshotBuilder;
|
||||
documentService: ProformaDocumentGeneratorService;
|
||||
transactionManager: ITransactionManager;
|
||||
}
|
||||
) {}
|
||||
|
||||
public execute(params: PreviewProformaReportUseCaseInput) {
|
||||
const { proforma_id, companyId } = params;
|
||||
|
||||
const idOrError = UniqueID.create(proforma_id);
|
||||
|
||||
if (idOrError.isFailure) {
|
||||
return Result.fail(idOrError.error);
|
||||
}
|
||||
|
||||
const proformaId = idOrError.data;
|
||||
|
||||
return this.deps.transactionManager.complete(async (transaction) => {
|
||||
try {
|
||||
const proformaResult = await this.deps.finder.findProformaById(
|
||||
companyId,
|
||||
proformaId,
|
||||
transaction
|
||||
);
|
||||
|
||||
if (proformaResult.isFailure) {
|
||||
return Result.fail(proformaResult.error);
|
||||
}
|
||||
|
||||
const readModelResult = await this.deps.fullReadModelAssembler.assemble({
|
||||
companyId,
|
||||
proforma: proformaResult.data,
|
||||
transaction,
|
||||
});
|
||||
|
||||
if (readModelResult.isFailure) {
|
||||
return Result.fail(readModelResult.error);
|
||||
}
|
||||
|
||||
const companyProfileResult = await this.deps.companyReportProfileFinder.findByCompanyId(
|
||||
companyId,
|
||||
transaction
|
||||
);
|
||||
|
||||
if (companyProfileResult.isFailure) {
|
||||
return Result.fail(companyProfileResult.error);
|
||||
}
|
||||
|
||||
if (companyProfileResult.data.isNone()) {
|
||||
return Result.fail(new CompanyReportProfileNotFoundError(companyId.toString()));
|
||||
}
|
||||
|
||||
const fullSnapshot = this.deps.fullSnapshotBuilder.toOutput(readModelResult.data);
|
||||
const localization = this.resolveLocalization({
|
||||
languageCode: fullSnapshot.language_code,
|
||||
currencyCode: fullSnapshot.currency_code,
|
||||
companyLocale: companyProfileResult.data.unwrap().locale,
|
||||
companyCurrencyCode: companyProfileResult.data.unwrap().currencyCode,
|
||||
});
|
||||
|
||||
const reportSnapshot = this.deps.reportSnapshotBuilder.toOutput(fullSnapshot, {
|
||||
localization,
|
||||
company: companyProfileResult.data.unwrap(),
|
||||
});
|
||||
|
||||
const documentResult = await this.deps.documentService.generate(reportSnapshot, {
|
||||
companySlug: reportSnapshot.company.slug,
|
||||
});
|
||||
|
||||
if (documentResult.isFailure) {
|
||||
return Result.fail(documentResult.error);
|
||||
}
|
||||
|
||||
return Result.ok({
|
||||
payload: documentResult.data.payload.toString("utf-8"),
|
||||
mimeType: "text/html; charset=utf-8" as const,
|
||||
filename: documentResult.data.filename,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
return Result.fail(error as Error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private resolveLocalization(params: {
|
||||
languageCode: string;
|
||||
currencyCode: string;
|
||||
companyLocale: Maybe<string>;
|
||||
companyCurrencyCode: Maybe<string>;
|
||||
}): ReportLocalization {
|
||||
return {
|
||||
languageCode: params.languageCode || "es",
|
||||
locale: params.companyLocale.getOrUndefined() ?? "es-ES",
|
||||
currencyCode: params.currencyCode || params.companyCurrencyCode.getOrUndefined() || "EUR",
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,124 @@
|
||||
import type { ITransactionManager } from "@erp/core/api";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import { CompanyReportProfileNotFoundError } from "../errors";
|
||||
import type {
|
||||
ICompanyReportProfileFinder,
|
||||
IProformaFinder,
|
||||
IProformaFullReadModelAssembler,
|
||||
ProformaDocumentGeneratorService,
|
||||
} from "../services";
|
||||
import type { ReportLocalization } from "../models";
|
||||
import type { IProformaFullSnapshotBuilder } from "../snapshot-builders";
|
||||
import type { IProformaReportSnapshotBuilder } from "../snapshot-builders/report";
|
||||
|
||||
type ReportProformaPdfUseCaseInput = {
|
||||
companyId: UniqueID;
|
||||
proforma_id: string;
|
||||
};
|
||||
|
||||
export class ReportProformaPdfUseCase {
|
||||
public constructor(
|
||||
private readonly deps: {
|
||||
finder: IProformaFinder;
|
||||
companyReportProfileFinder: ICompanyReportProfileFinder;
|
||||
fullReadModelAssembler: IProformaFullReadModelAssembler;
|
||||
fullSnapshotBuilder: IProformaFullSnapshotBuilder;
|
||||
reportSnapshotBuilder: IProformaReportSnapshotBuilder;
|
||||
documentService: ProformaDocumentGeneratorService;
|
||||
transactionManager: ITransactionManager;
|
||||
}
|
||||
) {}
|
||||
|
||||
public execute(params: ReportProformaPdfUseCaseInput) {
|
||||
const { proforma_id, companyId } = params;
|
||||
|
||||
const idOrError = UniqueID.create(proforma_id);
|
||||
|
||||
if (idOrError.isFailure) {
|
||||
return Result.fail(idOrError.error);
|
||||
}
|
||||
|
||||
const proformaId = idOrError.data;
|
||||
|
||||
return this.deps.transactionManager.complete(async (transaction) => {
|
||||
try {
|
||||
const proformaResult = await this.deps.finder.findProformaById(
|
||||
companyId,
|
||||
proformaId,
|
||||
transaction
|
||||
);
|
||||
|
||||
if (proformaResult.isFailure) {
|
||||
return Result.fail(proformaResult.error);
|
||||
}
|
||||
|
||||
const readModelResult = await this.deps.fullReadModelAssembler.assemble({
|
||||
companyId,
|
||||
proforma: proformaResult.data,
|
||||
transaction,
|
||||
});
|
||||
|
||||
if (readModelResult.isFailure) {
|
||||
return Result.fail(readModelResult.error);
|
||||
}
|
||||
|
||||
const companyProfileResult = await this.deps.companyReportProfileFinder.findByCompanyId(
|
||||
companyId,
|
||||
transaction
|
||||
);
|
||||
|
||||
if (companyProfileResult.isFailure) {
|
||||
return Result.fail(companyProfileResult.error);
|
||||
}
|
||||
|
||||
if (companyProfileResult.data.isNone()) {
|
||||
return Result.fail(new CompanyReportProfileNotFoundError(companyId.toString()));
|
||||
}
|
||||
|
||||
const fullSnapshot = this.deps.fullSnapshotBuilder.toOutput(readModelResult.data);
|
||||
const localization = this.resolveLocalization({
|
||||
languageCode: fullSnapshot.language_code,
|
||||
currencyCode: fullSnapshot.currency_code,
|
||||
companyLocale: companyProfileResult.data.unwrap().locale,
|
||||
companyCurrencyCode: companyProfileResult.data.unwrap().currencyCode,
|
||||
});
|
||||
|
||||
const reportSnapshot = this.deps.reportSnapshotBuilder.toOutput(fullSnapshot, {
|
||||
localization,
|
||||
company: companyProfileResult.data.unwrap(),
|
||||
});
|
||||
|
||||
const documentResult = await this.deps.documentService.generate(reportSnapshot, {
|
||||
companySlug: reportSnapshot.company.slug,
|
||||
});
|
||||
|
||||
if (documentResult.isFailure) {
|
||||
return Result.fail(documentResult.error);
|
||||
}
|
||||
|
||||
return Result.ok({
|
||||
payload: documentResult.data.payload,
|
||||
mimeType: "application/pdf" as const,
|
||||
filename: documentResult.data.filename,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
return Result.fail(error as Error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private resolveLocalization(params: {
|
||||
languageCode: string;
|
||||
currencyCode: string;
|
||||
companyLocale: Maybe<string>;
|
||||
companyCurrencyCode: Maybe<string>;
|
||||
}): ReportLocalization {
|
||||
return {
|
||||
languageCode: params.languageCode || "es",
|
||||
locale: params.companyLocale.getOrUndefined() ?? "es-ES",
|
||||
currencyCode: params.currencyCode || params.companyCurrencyCode.getOrUndefined() || "EUR",
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -1,80 +0,0 @@
|
||||
import { type ITransactionManager, type RendererFormat, logger } from "@erp/core/api";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { IProformaFinder, ProformaDocumentGeneratorService } from "../services";
|
||||
import type { IProformaFullSnapshotBuilder } from "../snapshot-builders";
|
||||
import type { IProformaReportSnapshotBuilder } from "../snapshot-builders/report";
|
||||
|
||||
type ReportProformaUseCaseInput = {
|
||||
companyId: UniqueID;
|
||||
proforma_id: string;
|
||||
format: RendererFormat;
|
||||
};
|
||||
|
||||
export class ReportProformaUseCase {
|
||||
constructor(
|
||||
private readonly finder: IProformaFinder,
|
||||
private readonly fullSnapshotBuilder: IProformaFullSnapshotBuilder,
|
||||
private readonly reportSnapshotBuilder: IProformaReportSnapshotBuilder,
|
||||
private readonly documentGenerationService: ProformaDocumentGeneratorService,
|
||||
private readonly transactionManager: ITransactionManager
|
||||
) {}
|
||||
|
||||
public async execute(params: ReportProformaUseCaseInput) {
|
||||
const { proforma_id, companyId } = params;
|
||||
|
||||
const idOrError = UniqueID.create(proforma_id);
|
||||
|
||||
if (idOrError.isFailure) {
|
||||
return Result.fail(idOrError.error);
|
||||
}
|
||||
|
||||
const proformaId = idOrError.data;
|
||||
|
||||
return this.transactionManager.complete(async (transaction) => {
|
||||
try {
|
||||
const proformaResult = await this.finder.findProformaById(
|
||||
companyId,
|
||||
proformaId,
|
||||
transaction
|
||||
);
|
||||
|
||||
if (proformaResult.isFailure) {
|
||||
return Result.fail(proformaResult.error);
|
||||
}
|
||||
|
||||
const invoice = proformaResult.data;
|
||||
|
||||
// Snapshot completo de la entidad
|
||||
const fullSnapshot = this.fullSnapshotBuilder.toOutput(invoice);
|
||||
|
||||
// Snapshot para informe a partir del anterior
|
||||
const reportSnapshot = this.reportSnapshotBuilder.toOutput(fullSnapshot, {
|
||||
languageCode: invoice.languageCode,
|
||||
});
|
||||
|
||||
// Llamar al servicio y que se apañe
|
||||
const documentResult = await this.documentGenerationService.generate(reportSnapshot, {
|
||||
companySlug: "rodax",
|
||||
});
|
||||
|
||||
if (documentResult.isFailure) {
|
||||
return Result.fail(documentResult.error);
|
||||
}
|
||||
11;
|
||||
|
||||
// 5. Devolver artefacto firmado
|
||||
return Result.ok({
|
||||
payload: documentResult.data.payload,
|
||||
mimeType: "application/pdf",
|
||||
filename: documentResult.data.filename,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const err = error as Error;
|
||||
logger.error(err.message, { label: "ReportProformaUseCase.execure" });
|
||||
return Result.fail(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
export * from "./report-proforma.use-case";
|
||||
export * from "./reporter";
|
||||
@ -1,67 +0,0 @@
|
||||
import type { IPresenterRegistry, ITransactionManager } from "@erp/core/api";
|
||||
import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { CustomerInvoiceApplicationService } from "../../../services/customer-invoice-application.service";
|
||||
|
||||
type ReportProformaUseCaseInput = {
|
||||
companyId: UniqueID;
|
||||
companySlug: string;
|
||||
proforma_id: string;
|
||||
format: "pdf" | "html";
|
||||
};
|
||||
|
||||
export class ReportProformaUseCase {
|
||||
constructor(
|
||||
private readonly service: CustomerInvoiceApplicationService,
|
||||
private readonly transactionManager: ITransactionManager,
|
||||
private readonly presenterRegistry: IPresenterRegistry
|
||||
) {}
|
||||
|
||||
public async execute(params: ReportProformaUseCaseInput) {
|
||||
const { proforma_id, companySlug, companyId, format } = params;
|
||||
|
||||
const idOrError = UniqueID.create(proforma_id);
|
||||
|
||||
if (idOrError.isFailure) {
|
||||
return Result.fail(idOrError.error);
|
||||
}
|
||||
|
||||
const proformaId = idOrError.data;
|
||||
const reportPresenter = this.presenterRegistry.getPresenter({
|
||||
resource: "proforma",
|
||||
projection: "REPORT",
|
||||
format,
|
||||
});
|
||||
|
||||
return this.transactionManager.complete(async (transaction) => {
|
||||
try {
|
||||
const proformaOrError = await this.service.getProformaByIdInCompany(
|
||||
companyId,
|
||||
proformaId,
|
||||
transaction
|
||||
);
|
||||
if (proformaOrError.isFailure) {
|
||||
return Result.fail(proformaOrError.error);
|
||||
}
|
||||
|
||||
const proforma = proformaOrError.data;
|
||||
const reportData = await reportPresenter.toOutput(proforma, { companySlug });
|
||||
|
||||
if (format === "html") {
|
||||
return Result.ok({
|
||||
data: String(reportData),
|
||||
filename: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return Result.ok({
|
||||
data: reportData as Buffer<ArrayBuffer>,
|
||||
filename: `proforma-${proforma.invoiceNumber}.pdf`,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
return Result.fail(error as Error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
import { TemplatePresenter } from "@erp/core/api";
|
||||
|
||||
import type { Proforma } from "../../../../../domain";
|
||||
import type { ProformaFullPresenter, ProformaReportPresenter } from "../../../../snapshot-builders";
|
||||
|
||||
export class ProformaReportHTMLPresenter extends TemplatePresenter {
|
||||
toOutput(proforma: Proforma, params: { companySlug: string }): string {
|
||||
const { companySlug } = params;
|
||||
const dtoPresenter = this.presenterRegistry.getPresenter({
|
||||
resource: "proforma",
|
||||
projection: "FULL",
|
||||
}) as ProformaFullPresenter;
|
||||
|
||||
const prePresenter = this.presenterRegistry.getPresenter({
|
||||
resource: "proforma",
|
||||
projection: "REPORT",
|
||||
format: "JSON",
|
||||
}) as ProformaReportPresenter;
|
||||
|
||||
const invoiceDTO = dtoPresenter.toOutput(proforma);
|
||||
const prettyDTO = prePresenter.toOutput(invoiceDTO);
|
||||
|
||||
// Obtener y compilar la plantilla HTML
|
||||
const template = this.templateResolver.compileTemplate(
|
||||
"customer-invoices",
|
||||
companySlug,
|
||||
"proforma.hbs"
|
||||
);
|
||||
|
||||
return template(prettyDTO);
|
||||
}
|
||||
}
|
||||
@ -1,76 +0,0 @@
|
||||
import { Presenter } from "@erp/core/api";
|
||||
import puppeteer from "puppeteer";
|
||||
|
||||
import type { Proforma } from "../../../../../domain";
|
||||
|
||||
import type { ProformaReportHTMLPresenter } from "./proforma.report.html";
|
||||
|
||||
// https://plnkr.co/edit/lWk6Yd?preview
|
||||
// https://latenode.com/es/blog/web-automation-scraping/puppeteer-fundamentals-setup/complete-guide-to-pdf-generation-with-puppeteer-from-simple-documents-to-complex-reports
|
||||
|
||||
export class ProformaReportPDFPresenter extends Presenter<Proforma, Promise<Buffer<ArrayBuffer>>> {
|
||||
async toOutput(
|
||||
proforma: Proforma,
|
||||
params: { companySlug: string }
|
||||
): Promise<Buffer<ArrayBuffer>> {
|
||||
try {
|
||||
const htmlPresenter = this.presenterRegistry.getPresenter({
|
||||
resource: "proforma",
|
||||
projection: "REPORT",
|
||||
format: "HTML",
|
||||
}) as ProformaReportHTMLPresenter;
|
||||
|
||||
const htmlData = htmlPresenter.toOutput(proforma, params);
|
||||
|
||||
// Generar el PDF con Puppeteer
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
args: [
|
||||
"--no-sandbox",
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
"--disable-gpu",
|
||||
"--disable-extensions",
|
||||
"--font-render-hinting=medium",
|
||||
],
|
||||
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH,
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
page.setDefaultNavigationTimeout(60000);
|
||||
page.setDefaultTimeout(60000);
|
||||
|
||||
await page.setContent(htmlData, {
|
||||
waitUntil: "networkidle2",
|
||||
});
|
||||
|
||||
// Espera extra opcional si hay imágenes base64 muy grandes
|
||||
await page.waitForNetworkIdle({ idleTime: 200, timeout: 5000 });
|
||||
|
||||
const reportPDF = await page.pdf({
|
||||
format: "A4",
|
||||
margin: {
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
},
|
||||
landscape: false,
|
||||
preferCSSPageSize: true,
|
||||
omitBackground: false,
|
||||
printBackground: true,
|
||||
displayHeaderFooter: true,
|
||||
headerTemplate: "<div />",
|
||||
footerTemplate:
|
||||
'<div style="text-align: center;width: 297mm;font-size: 10px;">Página <span style="margin-right: 1cm"><span class="pageNumber"></span> de <span class="totalPages"></span></span></span></div>',
|
||||
});
|
||||
|
||||
await browser.close();
|
||||
|
||||
return Buffer.from(reportPDF);
|
||||
} catch (err: unknown) {
|
||||
console.error(err);
|
||||
throw err as Error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Maybe, Result, type Result as ResultType } from "@repo/rdx-utils";
|
||||
|
||||
import type { ICompanyPublicServices } from "../../../../../../companies/src/api";
|
||||
import type { CompanyReportProfile, ICompanyReportProfileFinder } from "../../../application";
|
||||
|
||||
export class CompanyReportProfileFinder implements ICompanyReportProfileFinder {
|
||||
public constructor(
|
||||
private readonly deps: {
|
||||
companiesServices: ICompanyPublicServices;
|
||||
}
|
||||
) {}
|
||||
|
||||
public async findByCompanyId(
|
||||
companyId: UniqueID,
|
||||
transaction?: unknown
|
||||
): Promise<ResultType<Maybe<CompanyReportProfile>, Error>> {
|
||||
const result = await this.deps.companiesServices.finder.findById({
|
||||
companyId,
|
||||
transaction,
|
||||
});
|
||||
|
||||
if (result.isFailure) {
|
||||
return Result.fail(result.error);
|
||||
}
|
||||
|
||||
if (result.data.isNone()) {
|
||||
return Result.ok(Maybe.none());
|
||||
}
|
||||
|
||||
const company = result.data.unwrap();
|
||||
|
||||
const profile: CompanyReportProfile = {
|
||||
companyId: company.id,
|
||||
slug: company.slug,
|
||||
legalName: company.legalName,
|
||||
tradeName: Maybe.fromNullable(company.tradeName ?? undefined),
|
||||
tin: Maybe.fromNullable(company.tin ?? undefined),
|
||||
email: Maybe.fromNullable(company.email ?? undefined),
|
||||
phone: Maybe.fromNullable(company.phone ?? undefined),
|
||||
website: Maybe.fromNullable(company.website ?? undefined),
|
||||
locale: Maybe.none(),
|
||||
currencyCode: Maybe.none(),
|
||||
logo: Maybe.none(),
|
||||
address: Maybe.none(),
|
||||
version: Maybe.none(),
|
||||
};
|
||||
|
||||
return Result.ok(Maybe.some(profile));
|
||||
}
|
||||
}
|
||||
@ -1,27 +1,28 @@
|
||||
import { type ModuleParams, buildCoreDocumentsDI } from "@erp/core/api";
|
||||
|
||||
import {
|
||||
ProformaDocumentPipelineFactory,
|
||||
type ProformaDocumentPipelineFactoryDeps,
|
||||
ProformaHtmlPreviewPipelineFactory,
|
||||
type ProformaHtmlPreviewPipelineFactoryDeps,
|
||||
ProformaPdfDocumentPipelineFactory,
|
||||
type ProformaPdfDocumentPipelineFactoryDeps,
|
||||
} from "../documents";
|
||||
|
||||
export const buildProformaDocumentService = (params: ModuleParams) => {
|
||||
const { documentRenderers, documentSigning, documentStorage } = buildCoreDocumentsDI(params);
|
||||
export const buildProformaDocumentServices = (params: ModuleParams) => {
|
||||
const { documentRenderers, documentStorage } = buildCoreDocumentsDI(params);
|
||||
|
||||
const pipelineDeps: ProformaDocumentPipelineFactoryDeps = {
|
||||
const pdfPipelineDeps: ProformaPdfDocumentPipelineFactoryDeps = {
|
||||
fastReportRenderer: documentRenderers.fastReportRenderer,
|
||||
|
||||
//
|
||||
signingContextResolver: documentSigning.signingContextResolver,
|
||||
documentSigningService: documentSigning.signingService,
|
||||
|
||||
//
|
||||
documentStorage: documentStorage.storage,
|
||||
|
||||
templateResolver: documentRenderers.fastReportTemplateResolver,
|
||||
};
|
||||
|
||||
const documentGeneratorPipeline = ProformaDocumentPipelineFactory.create(pipelineDeps);
|
||||
const htmlPreviewPipelineDeps: ProformaHtmlPreviewPipelineFactoryDeps = {
|
||||
fastReportRenderer: documentRenderers.fastReportRenderer,
|
||||
templateResolver: documentRenderers.fastReportTemplateResolver,
|
||||
};
|
||||
|
||||
return documentGeneratorPipeline;
|
||||
return {
|
||||
pdf: ProformaPdfDocumentPipelineFactory.create(pdfPipelineDeps),
|
||||
htmlPreview: ProformaHtmlPreviewPipelineFactory.create(htmlPreviewPipelineDeps),
|
||||
};
|
||||
};
|
||||
|
||||
@ -65,6 +65,7 @@ export function buildProformaPublicServices(
|
||||
taxDefinitionFinder: catalogs.taxDefinition.finder,
|
||||
taxRegimeFinder: catalogs.taxRegime.finder,
|
||||
paymentMethodFinder: catalogs.paymentMethod.finder,
|
||||
paymentTermFinder: catalogs.paymentTerm.finder,
|
||||
});
|
||||
|
||||
const readModelAssemblers = buildProformaReadModelAssemblers({
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { type ModuleParams, buildTransactionManager } from "@erp/core/api";
|
||||
import type { ICompanyPublicServices } from "../../../../../../companies/src/api";
|
||||
|
||||
import {
|
||||
type ChangeStatusProformaUseCase,
|
||||
@ -7,13 +8,15 @@ import {
|
||||
type IIssuedInvoicePublicServices,
|
||||
type IssueProformaUseCase,
|
||||
type ListProformasUseCase,
|
||||
type ReportProformaUseCase,
|
||||
type PreviewProformaReportUseCase,
|
||||
type ReportProformaPdfUseCase,
|
||||
type UpdateProformaByIdUseCase,
|
||||
buildChangeStatusProformaUseCase,
|
||||
buildCreateProformaUseCase,
|
||||
buildGetProformaByIdUseCase,
|
||||
buildIssueProformaUseCase,
|
||||
buildListProformasUseCase,
|
||||
buildPreviewProformaReportUseCase,
|
||||
buildProformaCatalogResolvers,
|
||||
buildProformaCreator,
|
||||
buildProformaFinder,
|
||||
@ -25,11 +28,12 @@ import {
|
||||
buildProformaStatusChanger,
|
||||
buildProformaToIssuedInvoicePropsConverter,
|
||||
buildProformaUpdater,
|
||||
buildReportProformaUseCase,
|
||||
buildReportProformaPdfUseCase,
|
||||
buildUpdateProformaUseCase,
|
||||
} from "../../../application";
|
||||
|
||||
import { buildProformaDocumentService } from "./proforma-documents.di";
|
||||
import { CompanyReportProfileFinder } from "../adapters/company-report-profile-finder";
|
||||
import { buildProformaDocumentServices } from "./proforma-documents.di";
|
||||
import { buildProformaNumberGenerator } from "./proforma-number-generator.di";
|
||||
import { buildProformaPersistenceMappers } from "./proforma-persistence-mappers.di";
|
||||
import { buildProformaRepository } from "./proforma-repositories.di";
|
||||
@ -39,7 +43,8 @@ export type ProformasInternalDeps = {
|
||||
useCases: {
|
||||
listProformas: () => ListProformasUseCase;
|
||||
getProformaById: () => GetProformaByIdUseCase;
|
||||
reportProforma: () => ReportProformaUseCase;
|
||||
reportProformaPdf: () => ReportProformaPdfUseCase;
|
||||
previewProformaReport: () => PreviewProformaReportUseCase;
|
||||
createProforma: () => CreateProformaUseCase;
|
||||
issueProforma: (publicServices: {
|
||||
issuedInvoiceServices: IIssuedInvoicePublicServices;
|
||||
@ -58,6 +63,7 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
|
||||
const { database } = params;
|
||||
|
||||
const catalogs = resolveProformaCatalogsDeps(params);
|
||||
const companiesServices = params.getService<ICompanyPublicServices>("companies:general");
|
||||
|
||||
const transactionManager = buildTransactionManager(database);
|
||||
const persistenceMappers = buildProformaPersistenceMappers();
|
||||
@ -113,7 +119,10 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
|
||||
paymentMethodFinder: catalogs.paymentMethod.finder,
|
||||
});
|
||||
|
||||
const documentGeneratorPipeline = buildProformaDocumentService(params);
|
||||
const companyReportProfileFinder = new CompanyReportProfileFinder({
|
||||
companiesServices,
|
||||
});
|
||||
const documentGeneratorServices = buildProformaDocumentServices(params);
|
||||
|
||||
return {
|
||||
useCases: {
|
||||
@ -150,13 +159,25 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
|
||||
transactionManager,
|
||||
}),
|
||||
|
||||
reportProforma: () =>
|
||||
buildReportProformaUseCase({
|
||||
reportProformaPdf: () =>
|
||||
buildReportProformaPdfUseCase({
|
||||
finder,
|
||||
companyReportProfileFinder,
|
||||
fullReadModelAssembler: readModelAssemblers.full,
|
||||
fullSnapshotBuilder: snapshotBuilders.full,
|
||||
reportSnapshotBuilder: snapshotBuilders.report,
|
||||
documentService: documentGeneratorPipeline,
|
||||
documentService: documentGeneratorServices.pdf,
|
||||
transactionManager,
|
||||
}),
|
||||
|
||||
previewProformaReport: () =>
|
||||
buildPreviewProformaReportUseCase({
|
||||
finder,
|
||||
companyReportProfileFinder,
|
||||
fullReadModelAssembler: readModelAssemblers.full,
|
||||
fullSnapshotBuilder: snapshotBuilders.full,
|
||||
reportSnapshotBuilder: snapshotBuilders.report,
|
||||
documentService: documentGeneratorServices.htmlPreview,
|
||||
transactionManager,
|
||||
}),
|
||||
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export * from "./proforma-document-pipeline-factory.ts";
|
||||
export * from "./proforma-html-preview-pipeline-factory.ts";
|
||||
export * from "./proforma-pdf-document-pipeline-factory.ts";
|
||||
|
||||
@ -1,71 +0,0 @@
|
||||
import {
|
||||
DocumentGenerationService,
|
||||
DocumentPostProcessorChain,
|
||||
type FastReportRenderer,
|
||||
type FastReportTemplateResolver,
|
||||
type IDocumentPostProcessor,
|
||||
type IDocumentSideEffect,
|
||||
type IDocumentSigningService,
|
||||
type IDocumentStorage,
|
||||
type ISigningContextResolver,
|
||||
} from "@erp/core/api";
|
||||
|
||||
import {
|
||||
type ProformaDocumentGeneratorService,
|
||||
ProformaDocumentMetadataFactory,
|
||||
ProformaDocumentPropertiesFactory,
|
||||
type ProformaReportSnapshot,
|
||||
} from "../../../../application";
|
||||
import { ProformaSignedDocumentCachePreProcessor } from "../pre-processors";
|
||||
import { ProformaDocumentRenderer } from "../renderers";
|
||||
import { PersistProformaDocumentSideEffect } from "../side-effects";
|
||||
|
||||
/**
|
||||
* Factory de pipeline de generación de documentos
|
||||
* para facturas emitidas (Issued Invoice).
|
||||
*/
|
||||
|
||||
export interface ProformaDocumentPipelineFactoryDeps {
|
||||
// Core / Infra
|
||||
fastReportRenderer: FastReportRenderer;
|
||||
templateResolver: FastReportTemplateResolver;
|
||||
|
||||
signingContextResolver: ISigningContextResolver;
|
||||
documentSigningService: IDocumentSigningService;
|
||||
|
||||
documentStorage: IDocumentStorage;
|
||||
}
|
||||
|
||||
export class ProformaDocumentPipelineFactory {
|
||||
static create(deps: ProformaDocumentPipelineFactoryDeps): ProformaDocumentGeneratorService {
|
||||
// 1. Pre-processors (cache firmado)
|
||||
const preProcessors = [new ProformaSignedDocumentCachePreProcessor(deps.documentStorage)];
|
||||
|
||||
// 2. Renderer (FastReport)
|
||||
const renderer = new ProformaDocumentRenderer(deps.fastReportRenderer, deps.templateResolver);
|
||||
|
||||
// 3) Metadata and properties factory (Application)
|
||||
const metadataFactory = new ProformaDocumentMetadataFactory();
|
||||
const propertiesFactory = new ProformaDocumentPropertiesFactory();
|
||||
|
||||
// 3) Firma real (Core / Infra)
|
||||
const postProcessor: IDocumentPostProcessor = new DocumentPostProcessorChain([
|
||||
// Aquí podrían ir más post-procesadores, como uno de validación o similar
|
||||
]);
|
||||
|
||||
// 4. Side-effects (persistencia best-effort)
|
||||
const sideEffects: IDocumentSideEffect[] = [
|
||||
new PersistProformaDocumentSideEffect(deps.documentStorage),
|
||||
];
|
||||
|
||||
// 5. Pipeline final
|
||||
return new DocumentGenerationService<ProformaReportSnapshot>({
|
||||
renderer,
|
||||
preProcessors,
|
||||
postProcessor,
|
||||
sideEffects,
|
||||
metadataFactory,
|
||||
propertiesFactory,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
import {
|
||||
DocumentGenerationService,
|
||||
DocumentPostProcessorChain,
|
||||
type FastReportRenderer,
|
||||
type FastReportTemplateResolver,
|
||||
} from "@erp/core/api";
|
||||
|
||||
import {
|
||||
type ProformaDocumentGeneratorService,
|
||||
ProformaDocumentPropertiesFactory,
|
||||
type ProformaReportSnapshot,
|
||||
} from "../../../../application";
|
||||
import { ProformaHtmlPreviewRenderer } from "../renderers";
|
||||
import { ProformaHtmlPreviewDocumentMetadataFactory } from "../services/proforma-html-preview-document-metadata-factory";
|
||||
|
||||
export interface ProformaHtmlPreviewPipelineFactoryDeps {
|
||||
fastReportRenderer: FastReportRenderer;
|
||||
templateResolver: FastReportTemplateResolver;
|
||||
}
|
||||
|
||||
export class ProformaHtmlPreviewPipelineFactory {
|
||||
static create(deps: ProformaHtmlPreviewPipelineFactoryDeps): ProformaDocumentGeneratorService {
|
||||
const renderer = new ProformaHtmlPreviewRenderer(
|
||||
deps.fastReportRenderer,
|
||||
deps.templateResolver
|
||||
);
|
||||
const metadataFactory = new ProformaHtmlPreviewDocumentMetadataFactory();
|
||||
const propertiesFactory = new ProformaDocumentPropertiesFactory();
|
||||
const postProcessor = new DocumentPostProcessorChain([]);
|
||||
|
||||
return new DocumentGenerationService<ProformaReportSnapshot>({
|
||||
renderer,
|
||||
preProcessors: [],
|
||||
postProcessor,
|
||||
sideEffects: [],
|
||||
metadataFactory,
|
||||
propertiesFactory,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
import {
|
||||
DocumentGenerationService,
|
||||
DocumentPostProcessorChain,
|
||||
type FastReportRenderer,
|
||||
type FastReportTemplateResolver,
|
||||
type IDocumentSideEffect,
|
||||
type IDocumentStorage,
|
||||
} from "@erp/core/api";
|
||||
|
||||
import {
|
||||
type ProformaDocumentGeneratorService,
|
||||
ProformaDocumentPropertiesFactory,
|
||||
type ProformaReportSnapshot,
|
||||
} from "../../../../application";
|
||||
import { ProformaPdfCachePreProcessor } from "../pre-processors";
|
||||
import { ProformaPdfDocumentRenderer } from "../renderers";
|
||||
import { ProformaPdfDocumentMetadataFactory } from "../services/proforma-pdf-document-metadata-factory";
|
||||
import { PersistProformaPdfSideEffect } from "../side-effects";
|
||||
|
||||
export interface ProformaPdfDocumentPipelineFactoryDeps {
|
||||
fastReportRenderer: FastReportRenderer;
|
||||
templateResolver: FastReportTemplateResolver;
|
||||
documentStorage: IDocumentStorage;
|
||||
}
|
||||
|
||||
export class ProformaPdfDocumentPipelineFactory {
|
||||
static create(deps: ProformaPdfDocumentPipelineFactoryDeps): ProformaDocumentGeneratorService {
|
||||
const preProcessors = [new ProformaPdfCachePreProcessor(deps.documentStorage)];
|
||||
const renderer = new ProformaPdfDocumentRenderer(
|
||||
deps.fastReportRenderer,
|
||||
deps.templateResolver
|
||||
);
|
||||
const metadataFactory = new ProformaPdfDocumentMetadataFactory();
|
||||
const propertiesFactory = new ProformaDocumentPropertiesFactory();
|
||||
const postProcessor = new DocumentPostProcessorChain([]);
|
||||
const sideEffects: IDocumentSideEffect[] = [
|
||||
new PersistProformaPdfSideEffect(deps.documentStorage),
|
||||
];
|
||||
|
||||
return new DocumentGenerationService<ProformaReportSnapshot>({
|
||||
renderer,
|
||||
preProcessors,
|
||||
postProcessor,
|
||||
sideEffects,
|
||||
metadataFactory,
|
||||
propertiesFactory,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1 +1 @@
|
||||
export * from "./proforma-document-cache-pre-processor";
|
||||
export * from "./proforma-pdf-cache-pre-processor";
|
||||
|
||||
@ -1,71 +0,0 @@
|
||||
import {
|
||||
DocumentStorageKeyFactory,
|
||||
type IDocument,
|
||||
type IDocumentMetadata,
|
||||
type IDocumentPreProcessor,
|
||||
type IDocumentStorage,
|
||||
logger,
|
||||
} from "@erp/core/api";
|
||||
|
||||
/**
|
||||
* Pre-processor de cache técnico para documentos firmados
|
||||
* de facturas emitidas.
|
||||
*
|
||||
* - Best-effort
|
||||
* - Nunca rompe el flujo
|
||||
* - Invalida cache corrupto
|
||||
*/
|
||||
export class ProformaSignedDocumentCachePreProcessor implements IDocumentPreProcessor {
|
||||
constructor(private readonly docStorage: IDocumentStorage) {}
|
||||
|
||||
async tryResolve(metadata: IDocumentMetadata): Promise<IDocument | null> {
|
||||
const metadataRecord = metadata as unknown as Record<string, unknown>;
|
||||
try {
|
||||
const { paths, storageKey } = DocumentStorageKeyFactory.fromMetadataRecord(metadataRecord);
|
||||
|
||||
if (!storageKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const exists = await this.docStorage.existsKeyStorage(storageKey, paths);
|
||||
|
||||
if (!exists) {
|
||||
return null;
|
||||
}
|
||||
|
||||
logger.info(`✅ Found Server cached document for key ${storageKey}`);
|
||||
|
||||
const document = await this.docStorage.readDocument(storageKey, paths);
|
||||
|
||||
if (!this.isValid(document)) {
|
||||
logger.warn(`Corrupted or invalid cached document for key ${storageKey}`, {
|
||||
label: "ProformaSignedDocumentCachePreProcessor",
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
return document;
|
||||
} catch {
|
||||
// best-effort: cualquier fallo se trata como cache miss
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validación mínima de integridad.
|
||||
* No valida firma criptográfica.
|
||||
*/
|
||||
private isValid(document: IDocument | null): boolean {
|
||||
if (!document) return false;
|
||||
|
||||
if (!document.payload || document.payload.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (document.mimeType !== "application/pdf") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
import {
|
||||
DocumentStorageKeyFactory,
|
||||
type IDocument,
|
||||
type IDocumentMetadata,
|
||||
type IDocumentPreProcessor,
|
||||
type IDocumentStorage,
|
||||
} from "@erp/core/api";
|
||||
|
||||
export class ProformaPdfCachePreProcessor implements IDocumentPreProcessor {
|
||||
public constructor(private readonly docStorage: IDocumentStorage) {}
|
||||
|
||||
public async tryResolve(metadata: IDocumentMetadata): Promise<IDocument | null> {
|
||||
const metadataRecord = metadata as unknown as Record<string, unknown>;
|
||||
const { paths, storageKey } = DocumentStorageKeyFactory.fromMetadataRecord(metadataRecord);
|
||||
|
||||
if (!storageKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const exists = await this.docStorage.existsKeyStorage(storageKey, paths);
|
||||
|
||||
if (!exists) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const document = await this.docStorage.readDocument(storageKey, paths);
|
||||
|
||||
if (!document || document.mimeType !== "application/pdf" || document.payload.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return document;
|
||||
}
|
||||
}
|
||||
@ -1 +1,2 @@
|
||||
export * from "./proforma-document-renderer.ts";
|
||||
export * from "./proforma-html-preview-renderer.ts";
|
||||
export * from "./proforma-pdf-document-renderer.ts";
|
||||
|
||||
@ -1,72 +0,0 @@
|
||||
import type {
|
||||
FastReportRenderer,
|
||||
FastReportTemplateResolver,
|
||||
IDocument,
|
||||
IDocumentProperties,
|
||||
IDocumentRenderer,
|
||||
} from "@erp/core/api";
|
||||
|
||||
import type { ProformaReportSnapshot } from "../../../../../application";
|
||||
|
||||
/**
|
||||
* Adaptador Application → Infra para la generación del documento
|
||||
* PDF de una factura emitida (Issued Invoice).
|
||||
*
|
||||
* - Recibe snapshot de report
|
||||
* - Invoca FastReportRenderer
|
||||
* - Devuelve IDocument
|
||||
*
|
||||
* NO captura errores: FastReportError se propaga.
|
||||
*/
|
||||
|
||||
export type ProformaDocumentRenderParams = {
|
||||
companySlug: string;
|
||||
format: string;
|
||||
languageCode: string;
|
||||
filename: string;
|
||||
mimeType: string;
|
||||
properties: IDocumentProperties;
|
||||
};
|
||||
|
||||
export class ProformaDocumentRenderer implements IDocumentRenderer<ProformaReportSnapshot> {
|
||||
constructor(
|
||||
private readonly fastReportRenderer: FastReportRenderer,
|
||||
private readonly templateResolver: FastReportTemplateResolver
|
||||
) {}
|
||||
|
||||
async render(
|
||||
snapshot: ProformaReportSnapshot,
|
||||
params: ProformaDocumentRenderParams
|
||||
): Promise<IDocument> {
|
||||
const { companySlug, format, languageCode, filename, mimeType, properties } = params;
|
||||
|
||||
// Template
|
||||
const templatePath = this.templateResolver.resolveTemplatePath({
|
||||
module: "customer-invoices",
|
||||
companySlug,
|
||||
languageCode,
|
||||
templateFilename: "proforma.frx",
|
||||
});
|
||||
|
||||
const output = await this.fastReportRenderer.render({
|
||||
templatePath,
|
||||
inputData: snapshot,
|
||||
format,
|
||||
properties,
|
||||
});
|
||||
|
||||
return {
|
||||
payload: this.normalizePayload(output.payload),
|
||||
mimeType,
|
||||
filename,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Normaliza la salida de FastReport a Buffer.
|
||||
* FastReport puede devolver string o Buffer.
|
||||
*/
|
||||
private normalizePayload(payload: Buffer | string): Buffer {
|
||||
return Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
import type {
|
||||
FastReportRenderer,
|
||||
FastReportTemplateResolver,
|
||||
IDocument,
|
||||
IDocumentProperties,
|
||||
IDocumentRenderer,
|
||||
} from "@erp/core/api";
|
||||
|
||||
import type { ProformaReportSnapshot } from "../../../../../application";
|
||||
|
||||
type ProformaHtmlPreviewRenderParams = {
|
||||
companySlug: string;
|
||||
format: string;
|
||||
languageCode: string;
|
||||
filename: string;
|
||||
mimeType: string;
|
||||
properties: IDocumentProperties;
|
||||
};
|
||||
|
||||
export class ProformaHtmlPreviewRenderer implements IDocumentRenderer<ProformaReportSnapshot> {
|
||||
public constructor(
|
||||
private readonly fastReportRenderer: FastReportRenderer,
|
||||
private readonly templateResolver: FastReportTemplateResolver
|
||||
) {}
|
||||
|
||||
public async render(
|
||||
snapshot: ProformaReportSnapshot,
|
||||
params: ProformaHtmlPreviewRenderParams
|
||||
): Promise<IDocument> {
|
||||
const templatePath = this.templateResolver.resolveTemplatePath({
|
||||
module: "customer-invoices",
|
||||
companySlug: params.companySlug,
|
||||
languageCode: params.languageCode,
|
||||
templateFilename: "proforma.frx",
|
||||
});
|
||||
|
||||
const output = await this.fastReportRenderer.render({
|
||||
templatePath,
|
||||
inputData: snapshot,
|
||||
format: params.format,
|
||||
properties: params.properties,
|
||||
});
|
||||
|
||||
return {
|
||||
payload: Buffer.isBuffer(output.payload) ? output.payload : Buffer.from(output.payload),
|
||||
mimeType: params.mimeType,
|
||||
filename: params.filename,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
import type {
|
||||
FastReportRenderer,
|
||||
FastReportTemplateResolver,
|
||||
IDocument,
|
||||
IDocumentProperties,
|
||||
IDocumentRenderer,
|
||||
} from "@erp/core/api";
|
||||
|
||||
import type { ProformaReportSnapshot } from "../../../../../application";
|
||||
|
||||
type ProformaPdfDocumentRenderParams = {
|
||||
companySlug: string;
|
||||
format: string;
|
||||
languageCode: string;
|
||||
filename: string;
|
||||
mimeType: string;
|
||||
properties: IDocumentProperties;
|
||||
};
|
||||
|
||||
export class ProformaPdfDocumentRenderer implements IDocumentRenderer<ProformaReportSnapshot> {
|
||||
public constructor(
|
||||
private readonly fastReportRenderer: FastReportRenderer,
|
||||
private readonly templateResolver: FastReportTemplateResolver
|
||||
) {}
|
||||
|
||||
public async render(
|
||||
snapshot: ProformaReportSnapshot,
|
||||
params: ProformaPdfDocumentRenderParams
|
||||
): Promise<IDocument> {
|
||||
const templatePath = this.templateResolver.resolveTemplatePath({
|
||||
module: "customer-invoices",
|
||||
companySlug: params.companySlug,
|
||||
languageCode: params.languageCode,
|
||||
templateFilename: "proforma.frx",
|
||||
});
|
||||
|
||||
const output = await this.fastReportRenderer.render({
|
||||
templatePath,
|
||||
inputData: snapshot,
|
||||
format: params.format,
|
||||
properties: params.properties,
|
||||
});
|
||||
|
||||
return {
|
||||
payload: Buffer.isBuffer(output.payload) ? output.payload : Buffer.from(output.payload),
|
||||
mimeType: params.mimeType,
|
||||
filename: params.filename,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
import type { IDocumentMetadata, IDocumentMetadataFactory } from "@erp/core/api";
|
||||
|
||||
import type { ProformaReportSnapshot } from "../../../../application";
|
||||
|
||||
export class ProformaHtmlPreviewDocumentMetadataFactory
|
||||
implements IDocumentMetadataFactory<ProformaReportSnapshot>
|
||||
{
|
||||
build(snapshot: ProformaReportSnapshot): IDocumentMetadata {
|
||||
return {
|
||||
documentType: "proforma",
|
||||
documentId: snapshot.id,
|
||||
companyId: snapshot.company_id,
|
||||
companySlug: snapshot.company.slug,
|
||||
format: "HTML",
|
||||
languageCode: snapshot.localization.languageCode,
|
||||
filename: `proforma-${snapshot.series}${snapshot.invoice_number}.html`,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
import type { IDocumentMetadata, IDocumentMetadataFactory } from "@erp/core/api";
|
||||
|
||||
import type { ProformaReportSnapshot } from "../../../../application";
|
||||
|
||||
export class ProformaPdfDocumentMetadataFactory
|
||||
implements IDocumentMetadataFactory<ProformaReportSnapshot>
|
||||
{
|
||||
build(snapshot: ProformaReportSnapshot): IDocumentMetadata {
|
||||
return {
|
||||
documentType: "proforma",
|
||||
documentId: snapshot.id,
|
||||
companyId: snapshot.company_id,
|
||||
companySlug: snapshot.company.slug,
|
||||
format: "PDF",
|
||||
languageCode: snapshot.localization.languageCode,
|
||||
filename: `proforma-${snapshot.series}${snapshot.invoice_number}.pdf`,
|
||||
storageKey: this.buildStorageKey(snapshot),
|
||||
};
|
||||
}
|
||||
|
||||
private buildStorageKey(snapshot: ProformaReportSnapshot): string | undefined {
|
||||
if (snapshot.status === "draft") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// La cache de proformas no draft es segura respecto al contenido de la proforma
|
||||
// porque el dominio no permite modificar proformas fuera de draft. Queda pendiente
|
||||
// invalidar por version de plantilla/branding cuando esos datos existan.
|
||||
const keyParts = [
|
||||
"proforma",
|
||||
snapshot.company_id,
|
||||
snapshot.id,
|
||||
snapshot.status,
|
||||
"pdf",
|
||||
snapshot.localization.languageCode,
|
||||
snapshot.localization.currencyCode,
|
||||
snapshot.company.slug,
|
||||
];
|
||||
|
||||
const companyVersion = snapshot.company.version.getOrUndefined();
|
||||
|
||||
if (companyVersion) {
|
||||
keyParts.push(companyVersion);
|
||||
}
|
||||
|
||||
return keyParts.join(":");
|
||||
}
|
||||
}
|
||||
@ -1 +1 @@
|
||||
export * from "./persist-proforma-document-side-effect";
|
||||
export * from "./persist-proforma-pdf-side-effect";
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
import type {
|
||||
IDocument,
|
||||
IDocumentMetadata,
|
||||
IDocumentSideEffect,
|
||||
IDocumentStorage,
|
||||
} from "@erp/core/api";
|
||||
|
||||
/**
|
||||
* Side-effect de persistencia best-effort del documento final
|
||||
* de una factura emitida.
|
||||
*
|
||||
* - Nunca rompe el flujo
|
||||
* - Usa cacheKey/metadata para decidir la clave
|
||||
*/
|
||||
export class PersistProformaDocumentSideEffect implements IDocumentSideEffect {
|
||||
constructor(private readonly storage: IDocumentStorage) {}
|
||||
|
||||
async execute(document: IDocument, metadata: IDocumentMetadata): Promise<void> {
|
||||
// Si no hay cacheKey, no se persiste
|
||||
if (!metadata.storageKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Persistencia best-effort
|
||||
await this.storage.saveDocument(document, metadata as unknown as Record<string, unknown>);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
import type {
|
||||
IDocument,
|
||||
IDocumentMetadata,
|
||||
IDocumentSideEffect,
|
||||
IDocumentStorage,
|
||||
} from "@erp/core/api";
|
||||
|
||||
export class PersistProformaPdfSideEffect implements IDocumentSideEffect {
|
||||
public constructor(private readonly storage: IDocumentStorage) {}
|
||||
|
||||
public async execute(document: IDocument, metadata: IDocumentMetadata): Promise<void> {
|
||||
if (!metadata.storageKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.storage.saveDocument(document, metadata as unknown as Record<string, unknown>);
|
||||
}
|
||||
}
|
||||
@ -4,5 +4,6 @@
|
||||
export * from "./get-proforma.controller";
|
||||
export * from "./issue-proforma.controller";
|
||||
export * from "./list-proformas.controller";
|
||||
export * from "./report-proforma.controller";
|
||||
export * from "./preview-proforma-report.controller";
|
||||
export * from "./report-proforma-pdf.controller";
|
||||
export * from "./update-proforma.controller";
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
import {
|
||||
ExpressController,
|
||||
forbidQueryFieldGuard,
|
||||
requireAuthenticatedGuard,
|
||||
requireCompanyContextGuard,
|
||||
} from "@erp/core/api";
|
||||
|
||||
import type { PreviewProformaReportUseCase } from "../../../../application";
|
||||
import { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts";
|
||||
|
||||
export class PreviewProformaReportController extends ExpressController {
|
||||
public constructor(private readonly useCase: PreviewProformaReportUseCase) {
|
||||
super();
|
||||
this.errorMapper = proformasApiErrorMapper;
|
||||
this.registerGuards(
|
||||
requireAuthenticatedGuard(),
|
||||
requireCompanyContextGuard(),
|
||||
forbidQueryFieldGuard("companyId")
|
||||
);
|
||||
}
|
||||
|
||||
protected async executeImpl() {
|
||||
const companyId = this.getTenantId();
|
||||
|
||||
if (!companyId) {
|
||||
return this.forbiddenError("Tenant ID not found");
|
||||
}
|
||||
|
||||
const { proforma_id } = this.req.params;
|
||||
|
||||
const result = await this.useCase.execute({
|
||||
proforma_id,
|
||||
companyId,
|
||||
});
|
||||
|
||||
return result.match(
|
||||
({ payload }) => this.downloadHTML(payload),
|
||||
(err) => this.handleError(err)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
import {
|
||||
ExpressController,
|
||||
forbidQueryFieldGuard,
|
||||
requireAuthenticatedGuard,
|
||||
requireCompanyContextGuard,
|
||||
} from "@erp/core/api";
|
||||
|
||||
import type { ReportProformaPdfUseCase } from "../../../../application";
|
||||
import { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts";
|
||||
|
||||
export class ReportProformaPdfController extends ExpressController {
|
||||
public constructor(private readonly useCase: ReportProformaPdfUseCase) {
|
||||
super();
|
||||
this.errorMapper = proformasApiErrorMapper;
|
||||
this.registerGuards(
|
||||
requireAuthenticatedGuard(),
|
||||
requireCompanyContextGuard(),
|
||||
forbidQueryFieldGuard("companyId")
|
||||
);
|
||||
}
|
||||
|
||||
protected async executeImpl() {
|
||||
const companyId = this.getTenantId();
|
||||
|
||||
if (!companyId) {
|
||||
return this.forbiddenError("Tenant ID not found");
|
||||
}
|
||||
|
||||
const { proforma_id } = this.req.params;
|
||||
|
||||
const result = await this.useCase.execute({
|
||||
proforma_id,
|
||||
companyId,
|
||||
});
|
||||
|
||||
return result.match(
|
||||
({ payload, filename }) => this.downloadPDF(payload, String(filename)),
|
||||
(err) => this.handleError(err)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,55 +0,0 @@
|
||||
import {
|
||||
ExpressController,
|
||||
type RendererFormat,
|
||||
forbidQueryFieldGuard,
|
||||
requireAuthenticatedGuard,
|
||||
requireCompanyContextGuard,
|
||||
} from "@erp/core/api";
|
||||
|
||||
import type { ReportProformaByIdQueryRequestDTO } from "../../../../../common";
|
||||
import type { ReportProformaUseCase } from "../../../../application/index.ts";
|
||||
import { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts";
|
||||
|
||||
export class ReportProformaController extends ExpressController {
|
||||
public constructor(private readonly useCase: ReportProformaUseCase) {
|
||||
super();
|
||||
this.errorMapper = proformasApiErrorMapper;
|
||||
|
||||
// 🔐 Reutiliza guards de auth/tenant y prohíbe 'companyId' en query
|
||||
this.registerGuards(
|
||||
requireAuthenticatedGuard(),
|
||||
requireCompanyContextGuard(),
|
||||
forbidQueryFieldGuard("companyId")
|
||||
);
|
||||
}
|
||||
|
||||
protected async executeImpl() {
|
||||
const companyId = this.getTenantId();
|
||||
if (!companyId) {
|
||||
return this.forbiddenError("Tenant ID not found");
|
||||
}
|
||||
|
||||
const { proforma_id } = this.req.params;
|
||||
const { format } = this.req.query as ReportProformaByIdQueryRequestDTO;
|
||||
|
||||
const result = await this.useCase.execute({
|
||||
proforma_id,
|
||||
companyId,
|
||||
format: format as RendererFormat,
|
||||
});
|
||||
|
||||
return result.match(
|
||||
({ payload, filename }) => {
|
||||
if (format === "PDF") {
|
||||
return this.downloadPDF(payload as Buffer<ArrayBuffer>, String(filename));
|
||||
}
|
||||
if (format === "HTML") {
|
||||
return this.downloadHTML(payload as unknown as string);
|
||||
}
|
||||
// JSON
|
||||
return this.json(payload);
|
||||
},
|
||||
(err) => this.handleError(err)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@ import {
|
||||
ValidationApiError,
|
||||
} from "@erp/core/api";
|
||||
|
||||
import { CompanyReportProfileNotFoundError, isCompanyReportProfileNotFoundError } from "../../../application";
|
||||
import {
|
||||
type CustomerInvoiceIdAlreadyExistsError,
|
||||
type EntityIsNotProformaError,
|
||||
@ -77,8 +78,19 @@ const proformaCannotBeDeletedRule: ErrorToApiRule = {
|
||||
),
|
||||
};
|
||||
|
||||
const companyReportProfileNotFoundRule: ErrorToApiRule = {
|
||||
priority: 130,
|
||||
matches: (e) => isCompanyReportProfileNotFoundError(e),
|
||||
build: (e) =>
|
||||
new ConflictApiError(
|
||||
(e as CompanyReportProfileNotFoundError).message ||
|
||||
"Cannot generate the proforma report because the company document profile is missing."
|
||||
),
|
||||
};
|
||||
|
||||
// Cómo aplicarla: crea una nueva instancia del mapper con la regla extra
|
||||
export const proformasApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default()
|
||||
.register(companyReportProfileNotFoundRule)
|
||||
.register(proformaDuplicateRule)
|
||||
.register(proformaItemMismatchError)
|
||||
.register(entityIsNotProformaError)
|
||||
|
||||
@ -6,8 +6,9 @@ import {
|
||||
GetProformaController,
|
||||
IssueProformaController,
|
||||
ListProformasController,
|
||||
PreviewProformaReportController,
|
||||
type ProformasInternalDeps,
|
||||
ReportProformaController,
|
||||
ReportProformaPdfController,
|
||||
} from "..";
|
||||
|
||||
import {
|
||||
@ -17,8 +18,8 @@ import {
|
||||
GetProformaByIdRequestSchema,
|
||||
IssueProformaByIdParamsRequestSchema,
|
||||
ListProformasRequestSchema,
|
||||
ReportProformaByIdParamsRequestSchema,
|
||||
ReportProformaByIdQueryRequestSchema,
|
||||
PreviewProformaReportByIdParamsRequestSchema,
|
||||
ReportProformaPdfByIdParamsRequestSchema,
|
||||
UpdateProformaByIdParamsRequestSchema,
|
||||
UpdateProformaByIdRequestSchema,
|
||||
} from "../../../../common";
|
||||
@ -70,14 +71,21 @@ export const proformasRouter = (params: StartParams) => {
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/:proforma_id/report",
|
||||
//checkTabContext,
|
||||
validateRequest(ReportProformaByIdParamsRequestSchema, "params"),
|
||||
validateRequest(ReportProformaByIdQueryRequestSchema, "query"),
|
||||
|
||||
"/:proforma_id/report/pdf",
|
||||
validateRequest(ReportProformaPdfByIdParamsRequestSchema, "params"),
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const useCase = deps.useCases.reportProforma();
|
||||
const controller = new ReportProformaController(useCase);
|
||||
const useCase = deps.useCases.reportProformaPdf();
|
||||
const controller = new ReportProformaPdfController(useCase);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/:proforma_id/report/preview",
|
||||
validateRequest(PreviewProformaReportByIdParamsRequestSchema, "params"),
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const useCase = deps.useCases.previewProformaReport();
|
||||
const controller = new PreviewProformaReportController(useCase);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
);
|
||||
|
||||
@ -4,5 +4,6 @@ export * from "./delete-proforma-by-id.request.dto";
|
||||
export * from "./get-proforma-by-id.request.dto";
|
||||
export * from "./issue-proforma-by-id.request.dto";
|
||||
export * from "./list-proformas.request.dto";
|
||||
export * from "./report-proforma-by-id.request.dto";
|
||||
export * from "./preview-proforma-report-by-id.request.dto";
|
||||
export * from "./report-proforma-pdf-by-id.request.dto";
|
||||
export * from "./update-proforma-by-id.request.dto";
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const PreviewProformaReportByIdParamsRequestSchema = z.object({
|
||||
proforma_id: z.string(),
|
||||
});
|
||||
|
||||
export type PreviewProformaReportByIdParamsRequestDTO = z.infer<
|
||||
typeof PreviewProformaReportByIdParamsRequestSchema
|
||||
>;
|
||||
@ -1,21 +0,0 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const ReportProformaByIdParamsRequestSchema = z.object({
|
||||
proforma_id: z.string(),
|
||||
});
|
||||
|
||||
export type ReportProformaByIdParamsRequestDTO = z.infer<
|
||||
typeof ReportProformaByIdParamsRequestSchema
|
||||
>;
|
||||
export const ReportProformaByIdQueryRequestSchema = z.object({
|
||||
format: z
|
||||
.string()
|
||||
.default("pdf")
|
||||
.transform((v) => v.trim().toLowerCase())
|
||||
.pipe(z.enum(["pdf", "html", "json"]))
|
||||
.transform((v) => v.toUpperCase() as "PDF" | "HTML" | "JSON"),
|
||||
});
|
||||
|
||||
export type ReportProformaByIdQueryRequestDTO = z.infer<
|
||||
typeof ReportProformaByIdQueryRequestSchema
|
||||
>;
|
||||
@ -0,0 +1,9 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const ReportProformaPdfByIdParamsRequestSchema = z.object({
|
||||
proforma_id: z.string(),
|
||||
});
|
||||
|
||||
export type ReportProformaPdfByIdParamsRequestDTO = z.infer<
|
||||
typeof ReportProformaPdfByIdParamsRequestSchema
|
||||
>;
|
||||
@ -2,5 +2,4 @@ export * from "./create-proforma.response.dto";
|
||||
export * from "./get-proforma-by-id.response.dto";
|
||||
export * from "./issue-proforma-by-id.response.dto";
|
||||
export * from "./list-proformas.response.dto";
|
||||
export * from "./report-proforma-by-id.response.dto";
|
||||
export * from "./update-proformas-by-id.response.dto";
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export type ReportProformaByIdResponseDTO = Blob;
|
||||
@ -2,7 +2,9 @@ import { DataTableColumnHeader } from "@repo/rdx-ui/components";
|
||||
import { DateHelper } from "@repo/rdx-utils";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Checkbox,
|
||||
Spinner,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
@ -11,6 +13,7 @@ import {
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import {
|
||||
ExternalLinkIcon,
|
||||
FileDownIcon,
|
||||
FileTextIcon,
|
||||
PencilIcon,
|
||||
RefreshCwIcon,
|
||||
@ -39,6 +42,10 @@ type GridActionHandlers = {
|
||||
canIssue?: (proforma: ProformaListRow) => boolean;
|
||||
canEdit?: (proforma: ProformaListRow) => boolean;
|
||||
canDelete?: (proforma: ProformaListRow) => boolean;
|
||||
|
||||
onDownloadPdf?: (proforma: ProformaListRow) => void;
|
||||
pdfDownloadingId?: string | null;
|
||||
isPdfDownloading?: boolean;
|
||||
};
|
||||
|
||||
export function useProformasGridColumns(
|
||||
@ -269,8 +276,12 @@ export function useProformasGridColumns(
|
||||
const availableTransitions =
|
||||
PROFORMA_STATUS_TRANSITIONS[proforma.status as ProformaStatus] ?? [];
|
||||
|
||||
const isPDFLoading =
|
||||
actionHandlers.isPdfDownloading && actionHandlers.pdfDownloadingId === proforma.id;
|
||||
const stop = (e: React.MouseEvent | React.KeyboardEvent) => e.stopPropagation();
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1 justify-end">
|
||||
<ButtonGroup>
|
||||
{!isIssued && actionHandlers.onEditClick && (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
@ -352,6 +363,34 @@ export function useProformasGridColumns(
|
||||
</TooltipProvider>
|
||||
)}
|
||||
|
||||
{/* Descargar en PDF */}
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<Button
|
||||
className={"size-8"}
|
||||
disabled={isPDFLoading}
|
||||
onClick={(e) => {
|
||||
stop(e);
|
||||
actionHandlers.onDownloadPdf?.(proforma);
|
||||
}}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
{isPDFLoading ? (
|
||||
<Spinner className="size-4 cursor-progress" />
|
||||
) : (
|
||||
<FileDownIcon className="size-4 cursor-pointer" />
|
||||
)}
|
||||
<span className="sr-only">Descargar PDF</span>
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>Descargar PDF</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
|
||||
{/* Eliminar */}
|
||||
{!isIssued && actionHandlers.onDeleteClick && (
|
||||
<TooltipProvider>
|
||||
@ -377,7 +416,7 @@ export function useProformasGridColumns(
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
</ButtonGroup>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
@ -64,6 +64,8 @@ export const ListProformasPage = () => {
|
||||
deleteDialogCtrl.openDialog(prepareDeleteProformaTargets(proformaRow)),
|
||||
onChangeStatusClick: (proforma) =>
|
||||
changeStatusDialogCtrl.openDialog(prepareChangeProformaStatusTargets(proforma)),
|
||||
|
||||
onDownloadPdf: (proforma) => null,
|
||||
});
|
||||
|
||||
const isPanelOpen = panelCtrl.panelState.isOpen;
|
||||
|
||||
@ -1,35 +1,22 @@
|
||||
import type { IDataSource } from "@erp/core/client";
|
||||
|
||||
import type {
|
||||
ReportProformaByIdQueryRequestDTO,
|
||||
ReportProformaByIdResponseDTO,
|
||||
} from "../../../../common";
|
||||
|
||||
export type ProformaReportFormat = "PDF" | "HTML" | "JSON";
|
||||
|
||||
export interface ReportProformaByIdParams {
|
||||
id: string;
|
||||
data: ReportProformaByIdQueryRequestDTO;
|
||||
}
|
||||
|
||||
export type ReportProformaByIdResult = ReportProformaByIdResponseDTO;
|
||||
export type ReportProformaByIdResult = Blob;
|
||||
|
||||
export const reportProformaById = (
|
||||
dataSource: IDataSource,
|
||||
params: ReportProformaByIdParams
|
||||
): Promise<ReportProformaByIdResult> => {
|
||||
const {
|
||||
id,
|
||||
data: { format = "PDF" },
|
||||
} = params;
|
||||
const { id } = params;
|
||||
|
||||
if (!id) throw new Error("proformaId is required");
|
||||
|
||||
return dataSource.custom<ReportProformaByIdQueryRequestDTO, ReportProformaByIdResponseDTO>({
|
||||
path: `proformas/${id}/report`,
|
||||
return dataSource.custom<undefined, Blob>({
|
||||
path: `proformas/${id}/report/pdf`,
|
||||
method: "get",
|
||||
data: {
|
||||
format,
|
||||
},
|
||||
data: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user