Proformas preview
This commit is contained in:
parent
cc14cdacf1
commit
588e9dd733
@ -39,6 +39,7 @@
|
|||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"@erp/companies": "workspace:*",
|
||||||
"@erp/core": "workspace:*",
|
"@erp/core": "workspace:*",
|
||||||
"@erp/catalogs": "workspace:*",
|
"@erp/catalogs": "workspace:*",
|
||||||
"@erp/customers": "workspace:*",
|
"@erp/customers": "workspace:*",
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import type { IIssuedInvoicePublicServices } from "../../issued-invoices";
|
|||||||
import type { ICreateProformaInputMapper, IUpdateProformaInputMapper } from "../mappers";
|
import type { ICreateProformaInputMapper, IUpdateProformaInputMapper } from "../mappers";
|
||||||
import type { IProformaRepository } from "../repositories";
|
import type { IProformaRepository } from "../repositories";
|
||||||
import type {
|
import type {
|
||||||
|
ICompanyReportProfileFinder,
|
||||||
IProformaCreator,
|
IProformaCreator,
|
||||||
IProformaFinder,
|
IProformaFinder,
|
||||||
IProformaFullReadModelAssembler,
|
IProformaFullReadModelAssembler,
|
||||||
@ -24,7 +25,8 @@ import {
|
|||||||
GetProformaByIdUseCase,
|
GetProformaByIdUseCase,
|
||||||
IssueProformaUseCase,
|
IssueProformaUseCase,
|
||||||
ListProformasUseCase,
|
ListProformasUseCase,
|
||||||
ReportProformaUseCase,
|
PreviewProformaReportUseCase,
|
||||||
|
ReportProformaPdfUseCase,
|
||||||
UpdateProformaByIdUseCase,
|
UpdateProformaByIdUseCase,
|
||||||
} from "../use-cases";
|
} from "../use-cases";
|
||||||
|
|
||||||
@ -54,16 +56,38 @@ export function buildListProformasUseCase(deps: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildReportProformaUseCase(deps: {
|
export function buildReportProformaPdfUseCase(deps: {
|
||||||
finder: IProformaFinder;
|
finder: IProformaFinder;
|
||||||
|
companyReportProfileFinder: ICompanyReportProfileFinder;
|
||||||
fullReadModelAssembler: IProformaFullReadModelAssembler;
|
fullReadModelAssembler: IProformaFullReadModelAssembler;
|
||||||
fullSnapshotBuilder: IProformaFullSnapshotBuilder;
|
fullSnapshotBuilder: IProformaFullSnapshotBuilder;
|
||||||
reportSnapshotBuilder: IProformaReportSnapshotBuilder;
|
reportSnapshotBuilder: IProformaReportSnapshotBuilder;
|
||||||
documentService: ProformaDocumentGeneratorService;
|
documentService: ProformaDocumentGeneratorService;
|
||||||
transactionManager: ITransactionManager;
|
transactionManager: ITransactionManager;
|
||||||
}) {
|
}) {
|
||||||
return new ReportProformaUseCase({
|
return new ReportProformaPdfUseCase({
|
||||||
finder: deps.finder,
|
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,
|
fullReadModelAssembler: deps.fullReadModelAssembler,
|
||||||
fullSnapshotBuilder: deps.fullSnapshotBuilder,
|
fullSnapshotBuilder: deps.fullSnapshotBuilder,
|
||||||
reportSnapshotBuilder: deps.reportSnapshotBuilder,
|
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 "./di";
|
||||||
|
export * from "./errors";
|
||||||
export * from "./mappers";
|
export * from "./mappers";
|
||||||
export * from "./models";
|
export * from "./models";
|
||||||
export * from "./repositories";
|
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-create-input.model";
|
||||||
export * from "./proforma-full-read.model";
|
export * from "./proforma-full-read.model";
|
||||||
export * from "./proforma-item-tax-codes-input.model";
|
export * from "./proforma-item-tax-codes-input.model";
|
||||||
export * from "./proforma-summary";
|
export * from "./proforma-summary";
|
||||||
export * from "./proforma-update-input.model";
|
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 "./assemblers";
|
||||||
export * from "./catalog-resolver";
|
export * from "./catalog-resolver";
|
||||||
|
export * from "./company-report-profile-finder";
|
||||||
export * from "./proforma-creator";
|
export * from "./proforma-creator";
|
||||||
export * from "./proforma-document-generator.interface";
|
export * from "./proforma-document-generator.interface";
|
||||||
export * from "./proforma-document-metadata-factory";
|
|
||||||
export * from "./proforma-document-properties-factory";
|
export * from "./proforma-document-properties-factory";
|
||||||
export * from "./proforma-finder";
|
export * from "./proforma-finder";
|
||||||
export * from "./proforma-issuer";
|
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 {
|
build(snapshot: ProformaReportSnapshot): IDocumentProperties {
|
||||||
return {
|
return {
|
||||||
title: snapshot.reference,
|
title: snapshot.reference ?? `Proforma ${snapshot.series}${snapshot.invoice_number}`,
|
||||||
subject: "proforma",
|
subject: "proforma",
|
||||||
author: snapshot.company_slug,
|
author: snapshot.company.legalName,
|
||||||
creator: "FactuGES ERP",
|
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-item-snapshot.interface";
|
||||||
export * from "./proforma-report-snapshot.interface";
|
export * from "./proforma-report-snapshot.interface";
|
||||||
export * from "./proforma-report-snapshot-builder";
|
export * from "./proforma-report-snapshot-builder";
|
||||||
export * from "./proforma-report-snapshot-builder";
|
|
||||||
export * from "./proforma-report-tax-snapshot.interface";
|
export * from "./proforma-report-tax-snapshot.interface";
|
||||||
export * from "./proforma-tax-report-snapshot-builder";
|
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 { ISnapshotBuilder, ISnapshotBuilderParams } from "@erp/core/api";
|
||||||
|
|
||||||
import type { ProformaFullSnapshot } from "../full";
|
import type { ProformaFullSnapshot } from "../full";
|
||||||
|
|
||||||
import type { ProformaReportItemSnapshot } from "./proforma-report-item-snapshot.interface";
|
import type { ProformaReportItemSnapshot } from "./proforma-report-item-snapshot.interface";
|
||||||
|
|
||||||
|
type ProformaItemReportSnapshotBuilderParams = ISnapshotBuilderParams & {
|
||||||
|
locale: string;
|
||||||
|
};
|
||||||
|
|
||||||
export interface IProformaItemReportSnapshotBuilder
|
export interface IProformaItemReportSnapshotBuilder
|
||||||
extends ISnapshotBuilder<ProformaFullSnapshot["items"], ProformaReportItemSnapshot[]> {}
|
extends ISnapshotBuilder<ProformaFullSnapshot["items"], ProformaReportItemSnapshot[]> {}
|
||||||
|
|
||||||
export class ProformaItemReportSnapshotBuilder implements IProformaItemReportSnapshotBuilder {
|
export class ProformaItemReportSnapshotBuilder implements IProformaItemReportSnapshotBuilder {
|
||||||
toOutput(
|
toOutput(
|
||||||
items: ProformaFullSnapshot["items"],
|
items: ProformaFullSnapshot["items"],
|
||||||
params?: ISnapshotBuilderParams
|
params?: ProformaItemReportSnapshotBuilderParams
|
||||||
): ProformaReportItemSnapshot[] {
|
): ProformaReportItemSnapshot[] {
|
||||||
const locale = params?.locale as string;
|
const locale = params?.locale ?? "es-ES";
|
||||||
|
|
||||||
const moneyOptions = {
|
const moneyOptions = {
|
||||||
hideZeros: true,
|
hideZeros: true,
|
||||||
@ -21,14 +24,18 @@ export class ProformaItemReportSnapshotBuilder implements IProformaItemReportSna
|
|||||||
};
|
};
|
||||||
|
|
||||||
return items.map((item) => ({
|
return items.map((item) => ({
|
||||||
description: item.description,
|
description: item.description ?? "",
|
||||||
quantity: QuantityDTOHelper.format(item.quantity, locale, { minimumFractionDigits: 0 }),
|
quantity: item.quantity
|
||||||
unit_amount: MoneyDTOHelper.format(item.unit_amount, locale, moneyOptions),
|
? 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),
|
subtotal_amount: MoneyDTOHelper.format(item.subtotal_amount, locale, moneyOptions),
|
||||||
discount_percentage: PercentageDTOHelper.format(item.discount_percentage, locale, {
|
discount_percentage: item.item_discount_percentage
|
||||||
|
? PercentageDTOHelper.format(item.item_discount_percentage, locale, {
|
||||||
minimumFractionDigits: 0,
|
minimumFractionDigits: 0,
|
||||||
}),
|
})
|
||||||
discount_amount: MoneyDTOHelper.format(item.discount_amount, locale, moneyOptions),
|
: "",
|
||||||
|
discount_amount: MoneyDTOHelper.format(item.item_discount_amount, locale, moneyOptions),
|
||||||
taxable_amount: MoneyDTOHelper.format(item.taxable_amount, locale, moneyOptions),
|
taxable_amount: MoneyDTOHelper.format(item.taxable_amount, locale, moneyOptions),
|
||||||
taxes_amount: MoneyDTOHelper.format(item.taxes_amount, locale, moneyOptions),
|
taxes_amount: MoneyDTOHelper.format(item.taxes_amount, locale, moneyOptions),
|
||||||
total_amount: MoneyDTOHelper.format(item.total_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 type { ISnapshotBuilder, ISnapshotBuilderParams } from "@erp/core/api";
|
||||||
import { DateHelper } from "@repo/rdx-utils";
|
import { DateHelper } from "@repo/rdx-utils";
|
||||||
|
|
||||||
|
import type { CompanyReportProfile, ReportLocalization } from "../../models";
|
||||||
import type { ProformaFullSnapshot } from "../full";
|
import type { ProformaFullSnapshot } from "../full";
|
||||||
|
|
||||||
import type { ProformaReportItemSnapshot } from "./proforma-report-item-snapshot.interface";
|
import type { ProformaReportItemSnapshot } from "./proforma-report-item-snapshot.interface";
|
||||||
import type { ProformaReportSnapshot } from "./proforma-report-snapshot.interface";
|
import type { ProformaReportSnapshot } from "./proforma-report-snapshot.interface";
|
||||||
import type { ProformaReportTaxSnapshot } from "./proforma-report-tax-snapshot.interface";
|
import type { ProformaReportTaxSnapshot } from "./proforma-report-tax-snapshot.interface";
|
||||||
|
|
||||||
|
export type ProformaReportSnapshotBuilderParams = ISnapshotBuilderParams & {
|
||||||
|
company: CompanyReportProfile;
|
||||||
|
localization: ReportLocalization;
|
||||||
|
};
|
||||||
|
|
||||||
export interface IProformaReportSnapshotBuilder
|
export interface IProformaReportSnapshotBuilder
|
||||||
extends ISnapshotBuilder<ProformaFullSnapshot, ProformaReportSnapshot> {}
|
extends ISnapshotBuilder<ProformaFullSnapshot, ProformaReportSnapshot> {}
|
||||||
|
|
||||||
@ -25,9 +30,20 @@ export class ProformaReportSnapshotBuilder implements IProformaReportSnapshotBui
|
|||||||
|
|
||||||
toOutput(
|
toOutput(
|
||||||
snapshot: ProformaFullSnapshot,
|
snapshot: ProformaFullSnapshot,
|
||||||
params?: ISnapshotBuilderParams
|
params?: ProformaReportSnapshotBuilderParams
|
||||||
): ProformaReportSnapshot {
|
): 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 = {
|
const moneyOptions = {
|
||||||
hideZeros: true,
|
hideZeros: true,
|
||||||
@ -37,22 +53,23 @@ export class ProformaReportSnapshotBuilder implements IProformaReportSnapshotBui
|
|||||||
return {
|
return {
|
||||||
id: snapshot.id,
|
id: snapshot.id,
|
||||||
company_id: snapshot.company_id,
|
company_id: snapshot.company_id,
|
||||||
company_slug: "rodax",
|
|
||||||
invoice_number: snapshot.invoice_number,
|
invoice_number: snapshot.invoice_number,
|
||||||
series: snapshot.series,
|
series: snapshot.series,
|
||||||
status: snapshot.status,
|
status: snapshot.status,
|
||||||
reference: snapshot.reference,
|
reference: snapshot.reference,
|
||||||
|
localization,
|
||||||
|
company,
|
||||||
|
|
||||||
language_code: snapshot.language_code,
|
language_code: snapshot.language_code,
|
||||||
currency_code: snapshot.currency_code,
|
currency_code: snapshot.currency_code,
|
||||||
|
|
||||||
invoice_date: DateHelper.format(snapshot.invoice_date, locale),
|
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,
|
notes: snapshot.notes,
|
||||||
|
|
||||||
recipient: {
|
recipient: {
|
||||||
name: snapshot.recipient.name,
|
name: snapshot.recipient.name ?? "",
|
||||||
tin: snapshot.recipient.tin,
|
tin: snapshot.recipient.tin,
|
||||||
format_address: this.formatAddress(snapshot.recipient),
|
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 { ProformaReportItemSnapshot } from "./proforma-report-item-snapshot.interface";
|
||||||
import type { ProformaReportTaxSnapshot } from "./proforma-report-tax-snapshot.interface";
|
import type { ProformaReportTaxSnapshot } from "./proforma-report-tax-snapshot.interface";
|
||||||
|
|
||||||
export interface ProformaReportSnapshot {
|
export interface ProformaReportSnapshot {
|
||||||
id: string;
|
id: string;
|
||||||
company_id: string;
|
company_id: string;
|
||||||
company_slug: string;
|
|
||||||
invoice_number: string;
|
invoice_number: string;
|
||||||
series: string;
|
series: string | null;
|
||||||
status: string;
|
status: string;
|
||||||
reference: string;
|
reference: string | null;
|
||||||
|
|
||||||
|
localization: ReportLocalization;
|
||||||
|
company: CompanyReportProfile;
|
||||||
|
|
||||||
language_code: string;
|
language_code: string;
|
||||||
currency_code: string;
|
currency_code: string;
|
||||||
|
|
||||||
invoice_date: string;
|
invoice_date: string;
|
||||||
payment_method: string;
|
payment_method: string;
|
||||||
notes: string;
|
notes: string | null;
|
||||||
|
|
||||||
recipient: {
|
recipient: {
|
||||||
name: string;
|
name: string;
|
||||||
tin: string;
|
tin: string | null;
|
||||||
format_address: string;
|
format_address: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
import { MoneyDTOHelper, PercentageDTOHelper } from "@erp/core";
|
import { MoneyDTOHelper, PercentageDTOHelper } from "@erp/core";
|
||||||
import type { ISnapshotBuilder, ISnapshotBuilderParams } from "@erp/core/api";
|
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
|
export interface IProformaTaxReportSnapshotBuilder
|
||||||
extends ISnapshotBuilder<ProformaFullSnapshot["taxes"], ProformaReportTaxSnapshot[]> {}
|
extends ISnapshotBuilder<ProformaFullSnapshot["taxes"], ProformaReportTaxSnapshot[]> {}
|
||||||
@ -9,9 +14,9 @@ export interface IProformaTaxReportSnapshotBuilder
|
|||||||
export class ProformaTaxReportSnapshotBuilder implements IProformaTaxReportSnapshotBuilder {
|
export class ProformaTaxReportSnapshotBuilder implements IProformaTaxReportSnapshotBuilder {
|
||||||
toOutput(
|
toOutput(
|
||||||
taxes: ProformaFullSnapshot["taxes"],
|
taxes: ProformaFullSnapshot["taxes"],
|
||||||
params?: ISnapshotBuilderParams
|
params?: ProformaTaxReportSnapshotBuilderParams
|
||||||
): ProformaReportTaxSnapshot[] {
|
): ProformaReportTaxSnapshot[] {
|
||||||
const locale = params?.locale as string;
|
const locale = params?.locale ?? "es-ES";
|
||||||
|
|
||||||
const moneyOptions = {
|
const moneyOptions = {
|
||||||
hideZeros: true,
|
hideZeros: true,
|
||||||
@ -21,16 +26,20 @@ export class ProformaTaxReportSnapshotBuilder implements IProformaTaxReportSnaps
|
|||||||
return taxes.map((tax) => ({
|
return taxes.map((tax) => ({
|
||||||
taxable_amount: MoneyDTOHelper.format(tax.taxable_amount, locale, moneyOptions),
|
taxable_amount: MoneyDTOHelper.format(tax.taxable_amount, locale, moneyOptions),
|
||||||
|
|
||||||
iva_code: tax.iva_code,
|
iva_code: tax.iva_code ?? "",
|
||||||
iva_percentage: PercentageDTOHelper.format(tax.iva_percentage, locale),
|
iva_percentage: tax.iva_percentage
|
||||||
|
? PercentageDTOHelper.format(tax.iva_percentage, locale)
|
||||||
|
: "",
|
||||||
iva_amount: MoneyDTOHelper.format(tax.iva_amount, locale, moneyOptions),
|
iva_amount: MoneyDTOHelper.format(tax.iva_amount, locale, moneyOptions),
|
||||||
|
|
||||||
rec_code: tax.rec_code,
|
rec_code: tax.rec_code ?? "",
|
||||||
rec_percentage: PercentageDTOHelper.format(tax.rec_percentage, locale),
|
rec_percentage: tax.rec_percentage ? PercentageDTOHelper.format(tax.rec_percentage, locale) : "",
|
||||||
rec_amount: MoneyDTOHelper.format(tax.rec_amount, locale, moneyOptions),
|
rec_amount: MoneyDTOHelper.format(tax.rec_amount, locale, moneyOptions),
|
||||||
|
|
||||||
retention_code: tax.retention_code,
|
retention_code: tax.retention_code ?? "",
|
||||||
retention_percentage: PercentageDTOHelper.format(tax.retention_percentage, locale),
|
retention_percentage: tax.retention_percentage
|
||||||
|
? PercentageDTOHelper.format(tax.retention_percentage, locale)
|
||||||
|
: "",
|
||||||
retention_amount: MoneyDTOHelper.format(tax.retention_amount, locale, moneyOptions),
|
retention_amount: MoneyDTOHelper.format(tax.retention_amount, locale, moneyOptions),
|
||||||
|
|
||||||
taxes_amount: MoneyDTOHelper.format(tax.taxes_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 "./get-proforma-by-id.use-case";
|
||||||
export * from "./issue-proforma.use-case";
|
export * from "./issue-proforma.use-case";
|
||||||
export * from "./list-proformas.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";
|
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 { type ModuleParams, buildCoreDocumentsDI } from "@erp/core/api";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ProformaDocumentPipelineFactory,
|
ProformaHtmlPreviewPipelineFactory,
|
||||||
type ProformaDocumentPipelineFactoryDeps,
|
type ProformaHtmlPreviewPipelineFactoryDeps,
|
||||||
|
ProformaPdfDocumentPipelineFactory,
|
||||||
|
type ProformaPdfDocumentPipelineFactoryDeps,
|
||||||
} from "../documents";
|
} from "../documents";
|
||||||
|
|
||||||
export const buildProformaDocumentService = (params: ModuleParams) => {
|
export const buildProformaDocumentServices = (params: ModuleParams) => {
|
||||||
const { documentRenderers, documentSigning, documentStorage } = buildCoreDocumentsDI(params);
|
const { documentRenderers, documentStorage } = buildCoreDocumentsDI(params);
|
||||||
|
|
||||||
const pipelineDeps: ProformaDocumentPipelineFactoryDeps = {
|
const pdfPipelineDeps: ProformaPdfDocumentPipelineFactoryDeps = {
|
||||||
fastReportRenderer: documentRenderers.fastReportRenderer,
|
fastReportRenderer: documentRenderers.fastReportRenderer,
|
||||||
|
|
||||||
//
|
|
||||||
signingContextResolver: documentSigning.signingContextResolver,
|
|
||||||
documentSigningService: documentSigning.signingService,
|
|
||||||
|
|
||||||
//
|
|
||||||
documentStorage: documentStorage.storage,
|
documentStorage: documentStorage.storage,
|
||||||
|
|
||||||
templateResolver: documentRenderers.fastReportTemplateResolver,
|
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,
|
taxDefinitionFinder: catalogs.taxDefinition.finder,
|
||||||
taxRegimeFinder: catalogs.taxRegime.finder,
|
taxRegimeFinder: catalogs.taxRegime.finder,
|
||||||
paymentMethodFinder: catalogs.paymentMethod.finder,
|
paymentMethodFinder: catalogs.paymentMethod.finder,
|
||||||
|
paymentTermFinder: catalogs.paymentTerm.finder,
|
||||||
});
|
});
|
||||||
|
|
||||||
const readModelAssemblers = buildProformaReadModelAssemblers({
|
const readModelAssemblers = buildProformaReadModelAssemblers({
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { type ModuleParams, buildTransactionManager } from "@erp/core/api";
|
import { type ModuleParams, buildTransactionManager } from "@erp/core/api";
|
||||||
|
import type { ICompanyPublicServices } from "../../../../../../companies/src/api";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type ChangeStatusProformaUseCase,
|
type ChangeStatusProformaUseCase,
|
||||||
@ -7,13 +8,15 @@ import {
|
|||||||
type IIssuedInvoicePublicServices,
|
type IIssuedInvoicePublicServices,
|
||||||
type IssueProformaUseCase,
|
type IssueProformaUseCase,
|
||||||
type ListProformasUseCase,
|
type ListProformasUseCase,
|
||||||
type ReportProformaUseCase,
|
type PreviewProformaReportUseCase,
|
||||||
|
type ReportProformaPdfUseCase,
|
||||||
type UpdateProformaByIdUseCase,
|
type UpdateProformaByIdUseCase,
|
||||||
buildChangeStatusProformaUseCase,
|
buildChangeStatusProformaUseCase,
|
||||||
buildCreateProformaUseCase,
|
buildCreateProformaUseCase,
|
||||||
buildGetProformaByIdUseCase,
|
buildGetProformaByIdUseCase,
|
||||||
buildIssueProformaUseCase,
|
buildIssueProformaUseCase,
|
||||||
buildListProformasUseCase,
|
buildListProformasUseCase,
|
||||||
|
buildPreviewProformaReportUseCase,
|
||||||
buildProformaCatalogResolvers,
|
buildProformaCatalogResolvers,
|
||||||
buildProformaCreator,
|
buildProformaCreator,
|
||||||
buildProformaFinder,
|
buildProformaFinder,
|
||||||
@ -25,11 +28,12 @@ import {
|
|||||||
buildProformaStatusChanger,
|
buildProformaStatusChanger,
|
||||||
buildProformaToIssuedInvoicePropsConverter,
|
buildProformaToIssuedInvoicePropsConverter,
|
||||||
buildProformaUpdater,
|
buildProformaUpdater,
|
||||||
buildReportProformaUseCase,
|
buildReportProformaPdfUseCase,
|
||||||
buildUpdateProformaUseCase,
|
buildUpdateProformaUseCase,
|
||||||
} from "../../../application";
|
} 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 { buildProformaNumberGenerator } from "./proforma-number-generator.di";
|
||||||
import { buildProformaPersistenceMappers } from "./proforma-persistence-mappers.di";
|
import { buildProformaPersistenceMappers } from "./proforma-persistence-mappers.di";
|
||||||
import { buildProformaRepository } from "./proforma-repositories.di";
|
import { buildProformaRepository } from "./proforma-repositories.di";
|
||||||
@ -39,7 +43,8 @@ export type ProformasInternalDeps = {
|
|||||||
useCases: {
|
useCases: {
|
||||||
listProformas: () => ListProformasUseCase;
|
listProformas: () => ListProformasUseCase;
|
||||||
getProformaById: () => GetProformaByIdUseCase;
|
getProformaById: () => GetProformaByIdUseCase;
|
||||||
reportProforma: () => ReportProformaUseCase;
|
reportProformaPdf: () => ReportProformaPdfUseCase;
|
||||||
|
previewProformaReport: () => PreviewProformaReportUseCase;
|
||||||
createProforma: () => CreateProformaUseCase;
|
createProforma: () => CreateProformaUseCase;
|
||||||
issueProforma: (publicServices: {
|
issueProforma: (publicServices: {
|
||||||
issuedInvoiceServices: IIssuedInvoicePublicServices;
|
issuedInvoiceServices: IIssuedInvoicePublicServices;
|
||||||
@ -58,6 +63,7 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
|
|||||||
const { database } = params;
|
const { database } = params;
|
||||||
|
|
||||||
const catalogs = resolveProformaCatalogsDeps(params);
|
const catalogs = resolveProformaCatalogsDeps(params);
|
||||||
|
const companiesServices = params.getService<ICompanyPublicServices>("companies:general");
|
||||||
|
|
||||||
const transactionManager = buildTransactionManager(database);
|
const transactionManager = buildTransactionManager(database);
|
||||||
const persistenceMappers = buildProformaPersistenceMappers();
|
const persistenceMappers = buildProformaPersistenceMappers();
|
||||||
@ -113,7 +119,10 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
|
|||||||
paymentMethodFinder: catalogs.paymentMethod.finder,
|
paymentMethodFinder: catalogs.paymentMethod.finder,
|
||||||
});
|
});
|
||||||
|
|
||||||
const documentGeneratorPipeline = buildProformaDocumentService(params);
|
const companyReportProfileFinder = new CompanyReportProfileFinder({
|
||||||
|
companiesServices,
|
||||||
|
});
|
||||||
|
const documentGeneratorServices = buildProformaDocumentServices(params);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
useCases: {
|
useCases: {
|
||||||
@ -150,13 +159,25 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
|
|||||||
transactionManager,
|
transactionManager,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
reportProforma: () =>
|
reportProformaPdf: () =>
|
||||||
buildReportProformaUseCase({
|
buildReportProformaPdfUseCase({
|
||||||
finder,
|
finder,
|
||||||
|
companyReportProfileFinder,
|
||||||
fullReadModelAssembler: readModelAssemblers.full,
|
fullReadModelAssembler: readModelAssemblers.full,
|
||||||
fullSnapshotBuilder: snapshotBuilders.full,
|
fullSnapshotBuilder: snapshotBuilders.full,
|
||||||
reportSnapshotBuilder: snapshotBuilders.report,
|
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,
|
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 "./get-proforma.controller";
|
||||||
export * from "./issue-proforma.controller";
|
export * from "./issue-proforma.controller";
|
||||||
export * from "./list-proformas.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";
|
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,
|
ValidationApiError,
|
||||||
} from "@erp/core/api";
|
} from "@erp/core/api";
|
||||||
|
|
||||||
|
import { CompanyReportProfileNotFoundError, isCompanyReportProfileNotFoundError } from "../../../application";
|
||||||
import {
|
import {
|
||||||
type CustomerInvoiceIdAlreadyExistsError,
|
type CustomerInvoiceIdAlreadyExistsError,
|
||||||
type EntityIsNotProformaError,
|
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
|
// Cómo aplicarla: crea una nueva instancia del mapper con la regla extra
|
||||||
export const proformasApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default()
|
export const proformasApiErrorMapper: ApiErrorMapper = ApiErrorMapper.default()
|
||||||
|
.register(companyReportProfileNotFoundRule)
|
||||||
.register(proformaDuplicateRule)
|
.register(proformaDuplicateRule)
|
||||||
.register(proformaItemMismatchError)
|
.register(proformaItemMismatchError)
|
||||||
.register(entityIsNotProformaError)
|
.register(entityIsNotProformaError)
|
||||||
|
|||||||
@ -6,8 +6,9 @@ import {
|
|||||||
GetProformaController,
|
GetProformaController,
|
||||||
IssueProformaController,
|
IssueProformaController,
|
||||||
ListProformasController,
|
ListProformasController,
|
||||||
|
PreviewProformaReportController,
|
||||||
type ProformasInternalDeps,
|
type ProformasInternalDeps,
|
||||||
ReportProformaController,
|
ReportProformaPdfController,
|
||||||
} from "..";
|
} from "..";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -17,8 +18,8 @@ import {
|
|||||||
GetProformaByIdRequestSchema,
|
GetProformaByIdRequestSchema,
|
||||||
IssueProformaByIdParamsRequestSchema,
|
IssueProformaByIdParamsRequestSchema,
|
||||||
ListProformasRequestSchema,
|
ListProformasRequestSchema,
|
||||||
ReportProformaByIdParamsRequestSchema,
|
PreviewProformaReportByIdParamsRequestSchema,
|
||||||
ReportProformaByIdQueryRequestSchema,
|
ReportProformaPdfByIdParamsRequestSchema,
|
||||||
UpdateProformaByIdParamsRequestSchema,
|
UpdateProformaByIdParamsRequestSchema,
|
||||||
UpdateProformaByIdRequestSchema,
|
UpdateProformaByIdRequestSchema,
|
||||||
} from "../../../../common";
|
} from "../../../../common";
|
||||||
@ -70,14 +71,21 @@ export const proformasRouter = (params: StartParams) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
"/:proforma_id/report",
|
"/:proforma_id/report/pdf",
|
||||||
//checkTabContext,
|
validateRequest(ReportProformaPdfByIdParamsRequestSchema, "params"),
|
||||||
validateRequest(ReportProformaByIdParamsRequestSchema, "params"),
|
|
||||||
validateRequest(ReportProformaByIdQueryRequestSchema, "query"),
|
|
||||||
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
const useCase = deps.useCases.reportProforma();
|
const useCase = deps.useCases.reportProformaPdf();
|
||||||
const controller = new ReportProformaController(useCase);
|
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);
|
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 "./get-proforma-by-id.request.dto";
|
||||||
export * from "./issue-proforma-by-id.request.dto";
|
export * from "./issue-proforma-by-id.request.dto";
|
||||||
export * from "./list-proformas.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";
|
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 "./get-proforma-by-id.response.dto";
|
||||||
export * from "./issue-proforma-by-id.response.dto";
|
export * from "./issue-proforma-by-id.response.dto";
|
||||||
export * from "./list-proformas.response.dto";
|
export * from "./list-proformas.response.dto";
|
||||||
export * from "./report-proforma-by-id.response.dto";
|
|
||||||
export * from "./update-proformas-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 { DateHelper } from "@repo/rdx-utils";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
Spinner,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
@ -11,6 +13,7 @@ import {
|
|||||||
import type { ColumnDef } from "@tanstack/react-table";
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
import {
|
import {
|
||||||
ExternalLinkIcon,
|
ExternalLinkIcon,
|
||||||
|
FileDownIcon,
|
||||||
FileTextIcon,
|
FileTextIcon,
|
||||||
PencilIcon,
|
PencilIcon,
|
||||||
RefreshCwIcon,
|
RefreshCwIcon,
|
||||||
@ -39,6 +42,10 @@ type GridActionHandlers = {
|
|||||||
canIssue?: (proforma: ProformaListRow) => boolean;
|
canIssue?: (proforma: ProformaListRow) => boolean;
|
||||||
canEdit?: (proforma: ProformaListRow) => boolean;
|
canEdit?: (proforma: ProformaListRow) => boolean;
|
||||||
canDelete?: (proforma: ProformaListRow) => boolean;
|
canDelete?: (proforma: ProformaListRow) => boolean;
|
||||||
|
|
||||||
|
onDownloadPdf?: (proforma: ProformaListRow) => void;
|
||||||
|
pdfDownloadingId?: string | null;
|
||||||
|
isPdfDownloading?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useProformasGridColumns(
|
export function useProformasGridColumns(
|
||||||
@ -269,8 +276,12 @@ export function useProformasGridColumns(
|
|||||||
const availableTransitions =
|
const availableTransitions =
|
||||||
PROFORMA_STATUS_TRANSITIONS[proforma.status as ProformaStatus] ?? [];
|
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 (
|
return (
|
||||||
<div className="flex items-center gap-1 justify-end">
|
<ButtonGroup>
|
||||||
{!isIssued && actionHandlers.onEditClick && (
|
{!isIssued && actionHandlers.onEditClick && (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@ -352,6 +363,34 @@ export function useProformasGridColumns(
|
|||||||
</TooltipProvider>
|
</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 */}
|
{/* Eliminar */}
|
||||||
{!isIssued && actionHandlers.onDeleteClick && (
|
{!isIssued && actionHandlers.onDeleteClick && (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
@ -377,7 +416,7 @@ export function useProformasGridColumns(
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
</div>
|
</ButtonGroup>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -64,6 +64,8 @@ export const ListProformasPage = () => {
|
|||||||
deleteDialogCtrl.openDialog(prepareDeleteProformaTargets(proformaRow)),
|
deleteDialogCtrl.openDialog(prepareDeleteProformaTargets(proformaRow)),
|
||||||
onChangeStatusClick: (proforma) =>
|
onChangeStatusClick: (proforma) =>
|
||||||
changeStatusDialogCtrl.openDialog(prepareChangeProformaStatusTargets(proforma)),
|
changeStatusDialogCtrl.openDialog(prepareChangeProformaStatusTargets(proforma)),
|
||||||
|
|
||||||
|
onDownloadPdf: (proforma) => null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isPanelOpen = panelCtrl.panelState.isOpen;
|
const isPanelOpen = panelCtrl.panelState.isOpen;
|
||||||
|
|||||||
@ -1,35 +1,22 @@
|
|||||||
import type { IDataSource } from "@erp/core/client";
|
import type { IDataSource } from "@erp/core/client";
|
||||||
|
|
||||||
import type {
|
|
||||||
ReportProformaByIdQueryRequestDTO,
|
|
||||||
ReportProformaByIdResponseDTO,
|
|
||||||
} from "../../../../common";
|
|
||||||
|
|
||||||
export type ProformaReportFormat = "PDF" | "HTML" | "JSON";
|
|
||||||
|
|
||||||
export interface ReportProformaByIdParams {
|
export interface ReportProformaByIdParams {
|
||||||
id: string;
|
id: string;
|
||||||
data: ReportProformaByIdQueryRequestDTO;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ReportProformaByIdResult = ReportProformaByIdResponseDTO;
|
export type ReportProformaByIdResult = Blob;
|
||||||
|
|
||||||
export const reportProformaById = (
|
export const reportProformaById = (
|
||||||
dataSource: IDataSource,
|
dataSource: IDataSource,
|
||||||
params: ReportProformaByIdParams
|
params: ReportProformaByIdParams
|
||||||
): Promise<ReportProformaByIdResult> => {
|
): Promise<ReportProformaByIdResult> => {
|
||||||
const {
|
const { id } = params;
|
||||||
id,
|
|
||||||
data: { format = "PDF" },
|
|
||||||
} = params;
|
|
||||||
|
|
||||||
if (!id) throw new Error("proformaId is required");
|
if (!id) throw new Error("proformaId is required");
|
||||||
|
|
||||||
return dataSource.custom<ReportProformaByIdQueryRequestDTO, ReportProformaByIdResponseDTO>({
|
return dataSource.custom<undefined, Blob>({
|
||||||
path: `proformas/${id}/report`,
|
path: `proformas/${id}/report/pdf`,
|
||||||
method: "get",
|
method: "get",
|
||||||
data: {
|
data: undefined,
|
||||||
format,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user