feat: refactor template variable handling and add Profit Planet signature application logic

This commit is contained in:
seaznCode 2026-01-19 21:50:58 +01:00
parent 25c67783d4
commit 7b2eb4dbf0
2 changed files with 31 additions and 136 deletions

View File

@ -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)

View File

@ -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;