perplexity-hackathon-LawMitra / perplexity_hackathon / src / services / documentService.ts
documentService.ts
Raw
import { PDFDocument, rgb, StandardFonts } from 'pdf-lib';
import fontkit from '@pdf-lib/fontkit';
import fs from 'fs-extra';
import path from 'path';
import Handlebars from 'handlebars';
import { Language } from '../types';

interface DocumentRequest {
  type: 'fir' | 'rent_agreement' | 'affidavit';
  details: Record<string, any>;
  language: string;
}

interface DocumentTemplate {
  name: string;
  content: string;
  fonts?: {
    [key: string]: string;
  };
}

export class DocumentService {
  private readonly templatesDir: string;
  private readonly fontsDir: string;
  private readonly outputDir: string;
  private templates: Map<string, DocumentTemplate>;

  constructor() {
    this.templatesDir = path.join(__dirname, '../../templates');
    this.fontsDir = path.join(__dirname, '../../fonts');
    this.outputDir = path.join(__dirname, '../../tmp/documents');
    this.templates = new Map();

    // Ensure directories exist
    fs.ensureDirSync(this.templatesDir);
    fs.ensureDirSync(this.fontsDir);
    fs.ensureDirSync(this.outputDir);

    // Load templates
    this.loadTemplates();
  }

  private async loadTemplates() {
    const templateFiles = await fs.readdir(this.templatesDir);
    
    for (const file of templateFiles) {
      if (file.endsWith('.json')) {
        const templatePath = path.join(this.templatesDir, file);
        const template = await fs.readJSON(templatePath);
        this.templates.set(template.name, template);
      }
    }
  }

  private async loadFont(fontPath: string) {
    const fontBytes = await fs.readFile(fontPath);
    return fontBytes;
  }

  async generateDocument({ type, details, language }: DocumentRequest): Promise<Buffer> {
    const template = this.templates.get(type);
    if (!template) {
      throw new Error(`Template not found for document type: ${type}`);
    }

    // Create a new PDF document
    const pdfDoc = await PDFDocument.create();
    pdfDoc.registerFontkit(fontkit);

    // Load fonts based on language
    const fontPath = path.join(this.fontsDir, `${language}.ttf`);
    let font;
    try {
      const fontBytes = await this.loadFont(fontPath);
      font = await pdfDoc.embedFont(fontBytes);
    } catch (error) {
      // Fallback to standard font if language-specific font not found
      font = await pdfDoc.embedFont(StandardFonts.TimesRoman);
    }

    // Create template with Handlebars
    const compiledTemplate = Handlebars.compile(template.content);
    const content = compiledTemplate(details);

    // Add a page to the document
    const page = pdfDoc.addPage();
    const { width, height } = page.getSize();

    // Add content to the page
    page.drawText(content, {
      x: 50,
      y: height - 50,
      size: 12,
      font,
      color: rgb(0, 0, 0),
    });

    // Save the PDF
    const pdfBytes = await pdfDoc.save();
    
    // Generate unique filename
    const filename = `${type}_${Date.now()}.pdf`;
    const outputPath = path.join(this.outputDir, filename);
    
    // Write to file
    await fs.writeFile(outputPath, pdfBytes);

    return pdfBytes;
  }

  async getTemplateFields(type: string): Promise<string[]> {
    const template = this.templates.get(type);
    if (!template) {
      throw new Error(`Template not found for document type: ${type}`);
    }

    // Extract field names from template using Handlebars AST
    const ast = Handlebars.parse(template.content);
    const fields = new Set<string>();

    function extractFields(node: any) {
      if (node.type === 'MustacheStatement') {
        fields.add(node.path.original);
      }
      if (node.program) {
        node.program.body.forEach(extractFields);
      }
    }

    ast.body.forEach(extractFields);
    return Array.from(fields);
  }

  async cleanup(olderThan: number = 24 * 60 * 60 * 1000): Promise<void> {
    const files = await fs.readdir(this.outputDir);
    const now = Date.now();

    for (const file of files) {
      const filePath = path.join(this.outputDir, file);
      const stats = await fs.stat(filePath);

      if (now - stats.mtimeMs > olderThan) {
        await fs.unlink(filePath);
      }
    }
  }
}

export const documentService = new DocumentService();