import { Injectable, OnModuleInit } from '@nestjs/common'; import * as fs from 'fs'; import * as path from 'path'; import Handlebars from 'handlebars'; import { interpolateTemplateText, parseArticleBody, RenderedArticle, } from './contract-article.util'; import { ContractViewModel } from './contract-view-model.builder'; @Injectable() export class ContractRendererService implements OnModuleInit { private readonly templatesDir = path.join(__dirname, 'templates'); private readonly compiled = new Map(); onModuleInit(): void { Handlebars.registerHelper('eq', (a: unknown, b: unknown) => a === b); const partialsDir = path.join(this.templatesDir, '_partials'); if (fs.existsSync(partialsDir)) { for (const file of fs.readdirSync(partialsDir)) { if (!file.endsWith('.hbs')) continue; const name = file.replace(/\.hbs$/, ''); const content = fs.readFileSync(path.join(partialsDir, file), 'utf-8'); Handlebars.registerPartial(name, content); } } } render(view: ContractViewModel): string { const fileName = view.template.templateFile ?? 'generic.hbs'; const template = this.getCompiled(fileName); return template({ ...view, paymentArticle: view.pricing.currency === 'ETB' ? 'ETB' : 'USD', ...this.buildDynamicSections(view), }); } /** * Turn the DB-backed dynamic template (when present) into render-ready data: * interpolate placeholders against the view model, then parse each article * body into numbered clauses with nested bullets. */ private buildDynamicSections(view: ContractViewModel): { dynamicDocumentTitle?: string; dynamicWhereas?: string[]; dynamicArticles?: RenderedArticle[]; } { const dyn = view.dynamicTemplate; if (!dyn || dyn.articles.length === 0) return {}; const articles = [...dyn.articles] .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) .map((article, index) => ({ number: index + 1, id: article.id, title: interpolateTemplateText(article.title, view), ...parseArticleBody(interpolateTemplateText(article.body, view)), })); return { dynamicDocumentTitle: interpolateTemplateText(dyn.documentTitle, view), dynamicWhereas: dyn.whereasClauses.map((clause) => interpolateTemplateText(clause, view), ), dynamicArticles: articles, }; } private getCompiled(fileName: string): Handlebars.TemplateDelegate { const cached = this.compiled.get(fileName); if (cached) return cached; const filePath = path.join(this.templatesDir, fileName); const fallbackPath = path.join(this.templatesDir, 'generic.hbs'); const source = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : fs.readFileSync(fallbackPath, 'utf-8'); const compiled = Handlebars.compile(source); this.compiled.set(fileName, compiled); return compiled; } }