From 4aead9bedc614cf2acdaaebe889337e4a403fd9d Mon Sep 17 00:00:00 2001 From: seaznCode Date: Wed, 14 Jan 2026 16:57:58 +0100 Subject: [PATCH] feat: implement buildTemplateVars function to enhance contract data handling with user profile integration --- services/contracts/ContractUploadService.js | 129 +++++++++++++++++++- 1 file changed, 127 insertions(+), 2 deletions(-) diff --git a/services/contracts/ContractUploadService.js b/services/contracts/ContractUploadService.js index b6ff13c..8a99628 100644 --- a/services/contracts/ContractUploadService.js +++ b/services/contracts/ContractUploadService.js @@ -3,6 +3,7 @@ const { uploadBuffer, s3: exoS3 } = require('../../utils/exoscaleUploader'); const PDFDocument = require('pdfkit'); const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3'); const DocumentTemplateService = require('../template/DocumentTemplateService'); +const db = require('../../database/database'); const fs = require('fs'); const path = require('path'); const { logger } = require('../../middleware/logger'); @@ -51,6 +52,116 @@ function fillTemplate(template, data) { return template.replace(/{{(\w+)}}/g, (_, key) => data[key] || ''); } +// Build placeholder variables by combining DB profile data with any contractData overrides (for both personal/company). +async function buildTemplateVars({ userId, user_type, contractData = {}, unitOfWork }) { + const vars = {}; + const setIfEmpty = (key, val) => { + if (val === undefined || val === null) return; + const str = String(val).trim(); + if (!str) return; + if (!vars[key] || String(vars[key]).trim() === '') { + vars[key] = str; + } + }; + + // Initialize known placeholders to empty so replacement removes them if no value exists + [ + 'fullName','address','zip_code','city','country','phone','fullAddress','email', + 'companyFullAddress','companyName','registrationNumber','companyAddress','companyZipCode','companyCity','companyEmail','companyPhone', + 'contactPersonName','contactPersonPhone','companyCompanyName','companyRegistrationNumber','currentDate' + ].forEach((k) => { vars[k] = ''; }); + + // Current date placeholder (matches preview controller format) + const now = new Date(); + const pad = (n) => String(n).padStart(2, '0'); + vars.currentDate = `${pad(now.getDate())}.${pad(now.getMonth() + 1)}.${now.getFullYear()} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`; + + // Load base user row for email fallback + try { + const [userRows] = await unitOfWork.connection.query('SELECT * FROM users WHERE id = ? LIMIT 1', [userId]); + const u = Array.isArray(userRows) ? userRows[0] : userRows; + if (u) { + setIfEmpty('email', u.email); + setIfEmpty('fullName', `${u.first_name || ''} ${u.last_name || ''}`); + setIfEmpty('address', u.address || u.street || u.street_address); + setIfEmpty('zip_code', u.zip_code || u.zip || u.postal_code || u.postalCode); + setIfEmpty('city', u.city || u.town); + setIfEmpty('country', u.country); + setIfEmpty('phone', u.phone || u.phone_secondary || u.mobile); + } + } catch (ignored) {} + + if (user_type === 'personal') { + try { + const [rows] = await unitOfWork.connection.query('SELECT * FROM personal_profiles WHERE user_id = ? LIMIT 1', [userId]); + const p = Array.isArray(rows) ? rows[0] : rows; + if (p) { + setIfEmpty('fullName', `${p.first_name || ''} ${p.last_name || ''}`); + setIfEmpty('address', p.address); + setIfEmpty('zip_code', p.zip_code); + setIfEmpty('city', p.city); + setIfEmpty('country', p.country); + setIfEmpty('phone', p.phone || p.phone_secondary); + setIfEmpty('fullAddress', p.full_address); + } + } catch (ignored) {} + } else if (user_type === 'company') { + try { + const [rows] = await unitOfWork.connection.query('SELECT * FROM company_profiles WHERE user_id = ? LIMIT 1', [userId]); + const c = Array.isArray(rows) ? rows[0] : rows; + if (c) { + vars.companyName = c.company_name || ''; + vars.registrationNumber = c.registration_number || ''; + vars.companyAddress = c.address || ''; + vars.address = vars.companyAddress; + vars.zip_code = c.zip_code || ''; + vars.city = c.city || ''; + vars.country = c.country || ''; + vars.contactPersonName = c.contact_person_name || ''; + vars.contactPersonPhone = c.contact_person_phone || c.phone || ''; + vars.companyEmail = c.email || c.company_email || c.contact_email || vars.email || ''; + vars.companyPhone = c.phone || c.contact_person_phone || ''; + + const parts = []; + if (vars.companyAddress) parts.push(vars.companyAddress); + const zipCity = [vars.zip_code, vars.city].filter(Boolean).join(' '); + if (zipCity) parts.push(zipCity); + vars.companyFullAddress = parts.join(', '); + + // Prefixed variants used in templates + vars.companyCompanyName = vars.companyName; + vars.companyRegistrationNumber = vars.registrationNumber; + vars.companyZipCode = vars.zip_code; + vars.companyCity = vars.city; + + // Display name fallback for signature blocks + setIfEmpty('fullName', vars.contactPersonName || vars.companyName); + } + } catch (ignored) {} + } + + // Build fullAddress if still empty + if (!vars.fullAddress) { + const parts = []; + if (vars.address) parts.push(vars.address); + const zipCity = [vars.zip_code, vars.city].filter(Boolean).join(' '); + if (zipCity) parts.push(zipCity); + vars.fullAddress = parts.join(', '); + } + + // Overlay contractData to allow request-provided values to win + if (contractData && typeof contractData === 'object') { + Object.entries(contractData).forEach(([key, value]) => { + if (value === undefined || value === null) return; + // Ignore nested objects like confirmations + if (typeof value === 'object') return; + vars[key] = String(value); + }); + } + + return vars; +} + const DEBUG_PDF_FILES = !!process.env.DEBUG_PDF_FILES; function ensureDebugDir() { const debugDir = path.join(__dirname, '../debug-pdf'); @@ -133,7 +244,8 @@ class ContractUploadService { contractBody = `Contract for ${contractData.name}\nDate: ${contractData.date}\nEmail: ${contractData.email}\n\nTerms and Conditions...\n`; } - if (contractData && signatureImage) { + const hasExplicitTemplate = !!(templateId || contractTemplate); + if (contractData && signatureImage && hasExplicitTemplate) { logger.info('ContractUploadService.uploadContract:generating_pdf', { userId }); // Generate styled PDF const doc = new PDFDocument({ @@ -193,7 +305,19 @@ class ContractUploadService { const client = exoS3 || new S3Client({ region: process.env.EXOSCALE_REGION, endpoint: process.env.EXOSCALE_ENDPOINT, forcePathStyle: true, credentials: { accessKeyId: process.env.EXOSCALE_ACCESS_KEY, secretAccessKey: process.env.EXOSCALE_SECRET_KEY } }); const obj = await client.send(new GetObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: tmpl.storageKey })); const htmlBuffer = await streamToBuffer(obj.Body); - const htmlTemplate = htmlBuffer.toString('utf-8'); + let htmlTemplate = htmlBuffer.toString('utf-8'); + + // Merge DB-derived vars with request data so placeholders fill like the preview endpoint + const vars = await buildTemplateVars({ userId, user_type, contractData, unitOfWork }); + Object.entries(vars).forEach(([key, value]) => { + const re = new RegExp(`{{\s*${key}\s*}}`, 'g'); + htmlTemplate = htmlTemplate.replace(re, String(value ?? '')); + }); + if (signatureImage) { + const base64 = String(signatureImage).startsWith('data:') ? String(signatureImage).split(',')[1] : String(signatureImage); + const tag = ``; + htmlTemplate = htmlTemplate.replace(/{{\s*signatureImage\s*}}/g, tag); + } // Render HTML to PDF via Puppeteer (closest to preview output) const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox', '--disable-setuid-sandbox'] }); @@ -225,6 +349,7 @@ class ContractUploadService { documentType, contractType: contract_type || 'contract', objectStorageId: uploadResult.objectKey, + signatureBase64: null, idType: null, idNumber: null, expiryDate: null,