Files
alla-allaos-fullstack/src/features/crm/quotations/document/server/quotation-pdf-runtime.ts
phaichayon aa73b9aed2 task-p.4.4
2026-06-29 14:47:24 +07:00

191 lines
5.7 KiB
TypeScript

import type { DocumentTemplateMappingWithColumns, ResolvedDocumentTemplate } from '@/features/foundation/document-template/types';
import type { QuotationDocumentData } from '../types';
import { buildPdfTopicPages } from './pdf-topic-engine';
import { resolveTemplatePages } from './page-resolver';
import { buildProductItemSectionModel } from './product-item-engine';
import { createRenderContext, resolveRenderPolicies } from './render-context';
import { resolveQuotationStaticTemplateInputs } from './static-input-resolver';
import type {
BuiltSection,
ProductItemSection,
QuotationPdfRuntimeResult,
RenderContext,
ResolvedTemplate,
SectionBuilder,
} from './runtime-contracts';
import { composeSections } from './section-composer';
import { adaptTemplateForSectionRuntime } from './template-compatibility-adapter';
import { assembleTemplate } from './template-assembler';
function cloneTemplate<T>(value: T): T {
return JSON.parse(JSON.stringify(value)) as T;
}
export function createCustomerSectionBuilder(): SectionBuilder {
return {
role: 'customer',
build(context: RenderContext): BuiltSection<'customer'> {
const page = context.pages.byRole.customer?.[0] ?? null;
const policy = context.policyBySection.get('customer');
if (!page) {
return {
role: 'customer',
enabled: policy?.enabled ?? false,
rendered: false,
anchorPageIndex: null,
pages: [],
templateInputPatch: {},
issues: [
{
code: 'MISSING_MARKER',
severity: policy?.required ? 'error' : 'warning',
message: 'Customer section page could not be resolved.',
},
],
};
}
return {
role: 'customer',
enabled: true,
rendered: true,
anchorPageIndex: page.pageIndex,
pages: [cloneTemplate(page.schema)],
templateInputPatch: {},
issues: [],
};
},
};
}
export function createProductItemSectionBuilder(): SectionBuilder {
return {
role: 'product_items',
build(context: RenderContext): ProductItemSection {
const built = buildProductItemSectionModel({
items: context.documentData.items,
currencyCode: context.documentData.quotation.currencyCode,
});
return {
role: 'product_items',
enabled: true,
rendered: false,
anchorPageIndex: null,
pages: [],
templateInputPatch: {},
issues: built.issues,
rows: built.rows,
tableModel: built.tableModel,
pagination: built.pagination,
};
},
};
}
export function createTopicSectionBuilder(): SectionBuilder {
return {
role: 'topics',
build(context: RenderContext): BuiltSection<'topics'> {
const page = context.pages.byRole.topics?.[0] ?? null;
const policy = context.policyBySection.get('topics');
if (!page) {
return {
role: 'topics',
enabled: policy?.enabled ?? false,
rendered: false,
anchorPageIndex: null,
pages: [],
templateInputPatch: {},
issues: [
{
code: 'MISSING_MARKER',
severity: policy?.required ? 'error' : 'warning',
message: 'Topic section page could not be resolved.',
},
],
};
}
const built = buildPdfTopicPages(page.schema, context.documentData.topics.all, {
basePageIndex: page.pageIndex,
});
return {
role: 'topics',
enabled: true,
rendered: true,
anchorPageIndex: page.pageIndex,
pages: built.pages,
templateInputPatch: built.topicInputs,
issues: built.issues,
};
},
};
}
export function getQuotationSectionBuilders(): SectionBuilder[] {
return [
createCustomerSectionBuilder(),
createProductItemSectionBuilder(),
createTopicSectionBuilder(),
];
}
function createResolvedTemplate(template: ResolvedDocumentTemplate): ResolvedTemplate {
return {
documentTemplate: template,
schema: cloneTemplate(template.version.schemaJson as import('@pdfme/common').Template),
};
}
function createRuntimeErrorMessage(): string {
return 'Quotation PDF runtime invalid active document template.';
}
export async function buildQuotationPdfRuntime(args: {
documentData: QuotationDocumentData;
template: ResolvedDocumentTemplate;
mappings: DocumentTemplateMappingWithColumns[];
templateInput: Record<string, unknown>;
}): Promise<QuotationPdfRuntimeResult> {
const resolvedTemplate = createResolvedTemplate(args.template);
const compatibleTemplate = adaptTemplateForSectionRuntime(resolvedTemplate);
const pages = resolveTemplatePages(compatibleTemplate);
const policies = resolveRenderPolicies();
const staticInputPatch = resolveQuotationStaticTemplateInputs(args.documentData);
const context = createRenderContext({
documentData: args.documentData,
template: resolvedTemplate,
mappings: args.mappings,
pages,
policies,
issues: pages.issues,
});
const composed = await composeSections(context, getQuotationSectionBuilders());
const assembled = assembleTemplate({
template: resolvedTemplate.schema,
templateInput: args.templateInput,
staticInputPatch,
sections: composed.sections,
});
const issues = [...pages.issues, ...composed.issues, ...assembled.issues];
const topicSection = composed.sections.find((section) => section.role === 'topics');
if (issues.some((issue) => issue.severity === 'error')) {
throw new Error(createRuntimeErrorMessage(), { cause: issues });
}
return {
assembled,
topicInputs: topicSection
? (topicSection.templateInputPatch as Record<string, string[][]>)
: {},
issues,
pages,
policies,
};
}