From 7b2eb4dbf0b0c5652fb09eedd1495aa3d711b994 Mon Sep 17 00:00:00 2001 From: seaznCode Date: Mon, 19 Jan 2026 21:50:58 +0100 Subject: [PATCH] feat: refactor template variable handling and add Profit Planet signature application logic --- .../DocumentTemplateController.js | 136 ++---------------- services/contracts/ContractUploadService.js | 31 ++-- 2 files changed, 31 insertions(+), 136 deletions(-) diff --git a/controller/documentTemplate/DocumentTemplateController.js b/controller/documentTemplate/DocumentTemplateController.js index d355752..928398b 100644 --- a/controller/documentTemplate/DocumentTemplateController.js +++ b/controller/documentTemplate/DocumentTemplateController.js @@ -1515,7 +1515,7 @@ exports.previewLatestForUser = async (req, res) => { exports.previewLatestForMe = async (req, res) => { if (!req.user) return res.status(401).json({ error: 'Unauthorized' }); const targetUserId = req.user.id || req.user.userId; - const userType = req.user.user_type || req.user.userType; + const userType = (req.user.user_type || req.user.userType || '').toString().toLowerCase(); if (!targetUserId || !userType) return res.status(400).json({ error: 'Invalid authenticated user' }); const contractTypeParam = (req.query.contract_type || req.query.contractType || '').toString().toLowerCase(); @@ -1545,132 +1545,20 @@ exports.previewLatestForMe = async (req, res) => { if (!fileObj.Body) return res.status(404).json({ error: 'Template file not available' }); let html = await streamToString(fileObj.Body, latest.id); - // Build variables from the authenticated user's DB data - const vars = {}; - // initialize common placeholders to empty so they get stripped from template even when data is missing - ['fullName','address','zip_code','city','country','phone','fullAddress','companyFullAddress','companyName','registrationNumber','companyAddress','companyZipCode','companyCity','companyEmail','companyPhone','contactPersonName','contactPersonPhone','companyCompanyName','companyRegistrationNumber'].forEach(k => { vars[k] = ''; }); - 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; - } - }; - // base fields - vars.email = req.user.email || ''; - const d = new Date(); - const pad = (n) => String(n).padStart(2, '0'); - vars.currentDate = `${pad(d.getDate())}.${pad(d.getMonth() + 1)}.${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; - - if (userType === 'personal') { - try { - let p = null; - try { - const [rows] = await db.execute('SELECT * FROM personal_profiles WHERE user_id = ? LIMIT 1', [targetUserId]); - p = Array.isArray(rows) ? rows[0] : rows; - } catch (ignored) {} - - // Fallback lookup via users.email join (personal_profiles has no email column) - if (!p && req.user.email) { - try { - const [rowsEmail] = await db.execute( - 'SELECT pp.* FROM personal_profiles pp JOIN users u ON pp.user_id = u.id WHERE u.email = ? LIMIT 1', - [req.user.email] - ); - p = Array.isArray(rowsEmail) ? rowsEmail[0] : rowsEmail; - } catch (ignored) {} - } - - if (p) { - setIfEmpty('fullName', `${p.first_name || ''} ${p.last_name || ''}`.trim()); - 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); - } - - // Fallbacks from authenticated user payload (helps dummy/local users without profile rows) - setIfEmpty('fullName', req.user.fullName || req.user.full_name); - setIfEmpty('fullName', `${req.user.first_name || ''} ${req.user.last_name || ''}`); - setIfEmpty('fullName', `${req.user.firstName || ''} ${req.user.lastName || ''}`); - setIfEmpty('address', req.user.address || req.user.street || req.user.street_address); - setIfEmpty('zip_code', req.user.zip_code || req.user.zip || req.user.postalCode || req.user.postal_code); - setIfEmpty('city', req.user.city || req.user.town); - setIfEmpty('country', req.user.country); - setIfEmpty('phone', req.user.phone || req.user.phone_secondary || req.user.mobile); - - 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(', '); - } - - logger.info('[previewLatestForMe] personal vars', { userId: targetUserId, found: !!p, vars }); - } catch (e) { - logger.warn('[previewLatestForMe] personal profile lookup failed', e && e.message); - } - } else if (userType === 'company') { - try { - const [rows] = await db.execute('SELECT * FROM company_profiles WHERE user_id = ? LIMIT 1', [targetUserId]); - const c = rows && rows[0] ? rows[0] : null; - if (c) { - setIfEmpty('companyName', c.company_name); - setIfEmpty('registrationNumber', c.registration_number); - setIfEmpty('companyAddress', c.address); - setIfEmpty('address', c.address); - setIfEmpty('zip_code', c.zip_code); - setIfEmpty('city', c.city); - setIfEmpty('country', c.country); - setIfEmpty('contactPersonName', c.contact_person_name); - setIfEmpty('contactPersonPhone', c.contact_person_phone || c.phone); - setIfEmpty('companyEmail', c.email || c.company_email || c.contact_email || req.user.email); - setIfEmpty('companyPhone', c.phone || c.contact_person_phone); - } - - // Fallbacks from authenticated user payload (helps dummy/local users without profile rows) - setIfEmpty('companyName', req.user.companyName || req.user.company_name || req.user.company); - setIfEmpty('registrationNumber', req.user.registrationNumber || req.user.registration_number || req.user.vatNumber); - setIfEmpty('companyAddress', req.user.address || req.user.street || req.user.street_address); - setIfEmpty('address', req.user.address || req.user.street || req.user.street_address); - setIfEmpty('zip_code', req.user.zip_code || req.user.zip || req.user.postalCode || req.user.postal_code); - setIfEmpty('city', req.user.city || req.user.town); - setIfEmpty('country', req.user.country); - setIfEmpty('contactPersonName', req.user.contactPersonName || req.user.contact_person_name); - setIfEmpty('contactPersonPhone', req.user.contactPersonPhone || req.user.contact_person_phone || req.user.companyPhone || req.user.phone); - setIfEmpty('companyEmail', req.user.companyEmail || req.user.email); - setIfEmpty('companyPhone', req.user.companyPhone || req.user.phone); - - if (!vars.companyAddress) setIfEmpty('companyAddress', vars.address); - if (!vars.address) setIfEmpty('address', vars.companyAddress); - - 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(', '); - - // Ensure template-prefixed company placeholders are populated - vars.companyCompanyName = vars.companyName || ''; - vars.companyRegistrationNumber = vars.registrationNumber || ''; - vars.companyZipCode = vars.zip_code || ''; - vars.companyCity = vars.city || ''; - - // Signature/display name for company preview - vars.fullName = vars.contactPersonName || vars.companyName || vars.fullName || ''; - logger.debug('[previewLatestForMe] company vars', { userId: targetUserId, found: !!c, vars }); - } catch (e) { - logger.warn('[previewLatestForMe] company profile lookup failed', e && e.message); - } - } + // Build variables using the same logic as contract upload + const uow = new UnitOfWork(); + await uow.start(); + const vars = await ContractUploadService.buildTemplateVars({ + userId: targetUserId, + user_type: userType, + contractData: {}, + unitOfWork: uow + }); + await uow.commit(); // Replace placeholders Object.entries(vars).forEach(([k, v]) => { - html = html.replace(new RegExp(`{{\\s*${k}\\s*}}`, 'g'), String(v ?? '')); + html = html.replace(new RegExp(`{{\s*${k}\s*}}`, 'g'), String(v ?? '')); }); // Show a friendly placeholder for signature in preview (not signed yet) diff --git a/services/contracts/ContractUploadService.js b/services/contracts/ContractUploadService.js index d00b383..c17fbf4 100644 --- a/services/contracts/ContractUploadService.js +++ b/services/contracts/ContractUploadService.js @@ -50,7 +50,19 @@ async function streamToBuffer(body) { } function fillTemplate(template, data) { - return template.replace(/{{\s*(\w+)\s*}}/g, (_, key) => data[key] || ''); + return template.replace(/{{(\w+)}}/g, (_, key) => data[key] || ''); +} + +async function applyProfitPlanetSignatureForHtml({ html, userId }) { + if (!html) return html; + if (!/{{\s*profitplanetSignature\s*}}/i.test(html)) return html; + try { + const { tag } = await DocumentTemplateService.getProfitPlanetSignatureTag({ maxW: 300, maxH: 300 }); + return html.replace(/{{\s*profitplanetSignature\s*}}/gi, tag || ''); + } catch (e) { + logger.warn('ContractUploadService.uploadContract:profitplanetSignature failed', { userId, msg: e.message }); + return html.replace(/{{\s*profitplanetSignature\s*}}/gi, ''); + } } // Build placeholder variables by combining DB profile data with any contractData overrides (for both personal/company). @@ -311,7 +323,7 @@ class ContractUploadService { // 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'); + const re = new RegExp(`{{\s*${key}\s*}}`, 'g'); htmlTemplate = htmlTemplate.replace(re, String(value ?? '')); }); if (signatureImage) { @@ -320,16 +332,8 @@ class ContractUploadService { htmlTemplate = htmlTemplate.replace(/{{\s*signatureImage\s*}}/g, tag); } - // Apply Profit Planet signature stamp if placeholder exists - try { - if (htmlTemplate.match(/{{\s*profitplanetSignature\s*}}/i)) { - const tag = await CompanyStampService.getProfitPlanetSignatureTag({ maxW: 300, maxH: 300 }); - htmlTemplate = htmlTemplate.replace(/{{\s*profitplanetSignature\s*}}/gi, tag || ''); - } - } catch (e) { - logger.warn('ContractUploadService.uploadContract:profitplanetSignature failed', { userId, msg: e.message }); - htmlTemplate = htmlTemplate.replace(/{{\s*profitplanetSignature\s*}}/gi, ''); - } + // Apply Profit Planet signature placeholder (if any) + htmlTemplate = await applyProfitPlanetSignatureForHtml({ html: htmlTemplate, userId }); // Render HTML to PDF via Puppeteer (closest to preview output) const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox', '--disable-setuid-sandbox'] }); @@ -392,4 +396,7 @@ class ContractUploadService { } } +// Reuse helper for preview endpoints +ContractUploadService.buildTemplateVars = buildTemplateVars; + module.exports = ContractUploadService;