feat: enhance user data handling in PDF generation with improved fallback logic for company and personal profiles

This commit is contained in:
seaznCode 2026-01-13 20:42:54 +01:00
parent dfbd731f53
commit 1fc9af3be9

View File

@ -730,6 +730,27 @@ exports.generatePdfWithSignature = async (req, res) => {
companyPhone: '' companyPhone: ''
}; };
// Map company-prefixed placeholders from raw request early (helps when no company_profile row exists)
const rawCompanyName = pick(rawUserData, ['companyCompanyName','companyName','company_name','company']);
const rawCompanyReg = pick(rawUserData, ['companyRegistrationNumber','registrationNumber','registration_number','registrationNo']);
const rawCompanyAddr = pick(rawUserData, ['companyAddress','company_address','address','streetAddress']);
const rawCompanyZip = pick(rawUserData, ['companyZipCode','company_zip','zip','zip_code','postalCode','postal_code']);
const rawCompanyCity = pick(rawUserData, ['companyCity','city','town']);
const rawCompanyEmail = pick(rawUserData, ['companyEmail','company_email','contact_email','email']);
const rawCompanyPhone = pick(rawUserData, ['companyPhone','company_phone','contactPhone','contact_phone','phone','telephone','mobile']);
const rawCompanyContactName = pick(rawUserData, ['companyContactPersonName','contactPersonName','contact_person_name','contactPerson','contactName','contact']);
const rawCompanyContactPhone = pick(rawUserData, ['companyContactPersonPhone','contactPersonPhone','contact_person_phone','contactPhone','contact_phone','phone','telephone','mobile']);
userData.companyCompanyName = userData.companyCompanyName || rawCompanyName || userData.companyName || '';
userData.companyRegistrationNumber = userData.companyRegistrationNumber || rawCompanyReg || userData.registrationNumber || '';
userData.companyAddress = userData.companyAddress || rawCompanyAddr || userData.address || '';
userData.companyZipCode = userData.companyZipCode || rawCompanyZip || userData.zip_code || '';
userData.companyCity = userData.companyCity || rawCompanyCity || userData.city || '';
userData.companyEmail = userData.companyEmail || rawCompanyEmail || userData.email || '';
userData.companyPhone = userData.companyPhone || rawCompanyPhone || userData.contactPersonPhone || userData.phone || '';
userData.companyContactPersonName = userData.companyContactPersonName || rawCompanyContactName || userData.contactPersonName || '';
userData.companyContactPersonPhone = userData.companyContactPersonPhone || rawCompanyContactPhone || userData.contactPersonPhone || '';
// Add personal-user specific fallbacks: fullName and personalAddress and phone // Add personal-user specific fallbacks: fullName and personalAddress and phone
const rawFull = pick(rawUserData, ['fullName','full_name','name']) || const rawFull = pick(rawUserData, ['fullName','full_name','name']) ||
((pick(rawUserData, ['firstName','first_name']) && pick(rawUserData, ['lastName','last_name'])) ? `${pick(rawUserData, ['firstName','first_name'])} ${pick(rawUserData, ['lastName','last_name'])}` : undefined); ((pick(rawUserData, ['firstName','first_name']) && pick(rawUserData, ['lastName','last_name'])) ? `${pick(rawUserData, ['firstName','first_name'])} ${pick(rawUserData, ['lastName','last_name'])}` : undefined);
@ -759,6 +780,12 @@ exports.generatePdfWithSignature = async (req, res) => {
// add formatted currentDate into normalized map // add formatted currentDate into normalized map
userData.currentDate = formattedCurrentDate; userData.currentDate = formattedCurrentDate;
// Derive fullName from company contact if still missing (helps company contracts)
if (!userData.fullName) {
const contactName = rawCompanyContactName || rawCompanyName || userData.contactPersonName || userData.companyContactPersonName;
if (contactName) userData.fullName = contactName;
}
// If frontend didn't send personalAddress/phone, try to fetch from personal_profiles in DB (authenticated user) // If frontend didn't send personalAddress/phone, try to fetch from personal_profiles in DB (authenticated user)
try { try {
// Build candidate identifiers: various authUser/rawUserData id/email shapes // Build candidate identifiers: various authUser/rawUserData id/email shapes
@ -798,34 +825,25 @@ exports.generatePdfWithSignature = async (req, res) => {
} }
} }
if (prof) { if (prof) {
if (!userData.address) { // Use DB values as canonical when present
userData.address = pick(prof, ['address','street','street_address','streetAddress','address_line1','addressLine1','addr','personal_address']) || userData.address || ''; userData.address = pick(prof, ['address','street','street_address','streetAddress','address_line1','addressLine1','addr','personal_address']) || userData.address || '';
}
if (!userData.zip_code) {
userData.zip_code = pick(prof, ['zip','zip_code','postal_code','postalCode']) || userData.zip_code || ''; userData.zip_code = pick(prof, ['zip','zip_code','postal_code','postalCode']) || userData.zip_code || '';
}
if (!userData.city) {
userData.city = pick(prof, ['city','town','municipality']) || userData.city || ''; userData.city = pick(prof, ['city','town','municipality']) || userData.city || '';
}
if (!userData.personalAddress) {
userData.personalAddress = pick(prof, ['personal_address','personalAddress']) || userData.personalAddress || ''; userData.personalAddress = pick(prof, ['personal_address','personalAddress']) || userData.personalAddress || '';
} userData.phone = pick(prof, ['phone','telephone','mobile','contact_phone','contactPhone','phone_secondary']) || userData.phone || '';
if (!userData.phone) { const fn = pick(prof, ['full_name','fullName','name','first_name','firstName']);
userData.phone = pick(prof, ['phone','telephone','mobile','contact_phone','contactPhone']) || userData.phone || '';
}
if (!userData.fullName) {
userData.fullName = pick(prof, ['full_name','fullName','name']) || userData.fullName || '';
if (!userData.fullName) {
const fn = pick(prof, ['first_name','firstName']);
const ln = pick(prof, ['last_name','lastName']); const ln = pick(prof, ['last_name','lastName']);
if (fn || ln) userData.fullName = `${fn || ''} ${ln || ''}`.trim(); if (fn || ln) userData.fullName = `${fn || ''} ${ln || ''}`.trim();
} if (!userData.fullName) userData.fullName = pick(prof, ['full_name','fullName','name']) || userData.fullName || '';
}
userData.fullAddress = pick(rawUserData, ['fullAddress','full_address']) || // Compose full address primarily from DB columns
pick(authUser, ['fullAddress','full_address']) || const addrParts = [];
pick(prof, ['full_address','fullAddress']) || if (userData.address) addrParts.push(userData.address);
(userData.address ? `${userData.address}${userData.zip_code || userData.city ? ', ' : ''}${userData.zip_code ? userData.zip_code + ' ' : ''}${userData.city || ''}`.trim() : (userData.personalAddress || '')) || ''; const zipCity = [userData.zip_code, userData.city].filter(Boolean).join(' ');
logger.debug(`[generatePdfWithSignature] Filled missing userData from personal_profiles for profile id=${prof.id || prof.user_id || 'unknown'}`); if (zipCity) addrParts.push(zipCity);
userData.fullAddress = addrParts.join(', ') || userData.fullAddress || userData.personalAddress || '';
logger.debug(`[generatePdfWithSignature] Applied personal_profiles data id=${prof.id || prof.user_id || 'unknown'}`);
} }
// --- company_profiles lookup (UnitOfWork) for company-related placeholders if still missing --- // --- company_profiles lookup (UnitOfWork) for company-related placeholders if still missing ---
@ -915,80 +933,38 @@ exports.generatePdfWithSignature = async (req, res) => {
} }
} }
if (comp) { if (comp) {
// Primary company fields // Prefer DB values directly
const compName = pick(comp, ['company_name','companyName','name','company']); const compName = pick(comp, ['company_name']);
const compReg = pick(comp, ['registration_number','registrationNumber','registrationNo']); const compReg = pick(comp, ['registration_number']);
const compAddr = pick(comp, ['address','company_address','street','streetAddress']); const compAddr = pick(comp, ['address']);
const compZip = pick(comp, ['zip','zip_code','postal_code','postcode']); const compZip = pick(comp, ['zip_code']);
const compCity = pick(comp, ['city','town']); const compCity = pick(comp, ['city']);
const compCountry = pick(comp, ['country','country_code','countryName']); const compCountry = pick(comp, ['country']);
const compContactName = pick(comp, ['contact_person_name','contactPersonName','contactName','contact']); const compContactName = pick(comp, ['contact_person_name']);
const compContactPhone = pick(comp, ['contact_person_phone','contactPersonPhone','phone','telephone','mobile']); const compContactPhone = pick(comp, ['contact_person_phone']);
const compEmail = pick(comp, ['email','company_email','contact_email']); const compPhone = pick(comp, ['phone']);
// ADDED: possible generic phone field
const compGenericPhone = pick(comp, ['phone','telephone','mobile']);
if (!userData.companyName && compName) userData.companyName = compName; if (compName) { userData.companyName = compName; userData.companyCompanyName = compName; }
if (!userData.registrationNumber && compReg) userData.registrationNumber = compReg; if (compReg) { userData.registrationNumber = compReg; userData.companyRegistrationNumber = compReg; }
if (!userData.companyAddress && compAddr) userData.companyAddress = compAddr; if (compAddr) { userData.companyAddress = compAddr; userData.address = userData.address || compAddr; }
// Also populate the generic address key used in many templates if (compZip) { userData.companyZipCode = compZip; userData.zip_code = userData.zip_code || compZip; }
if ((!userData.address || String(userData.address).trim() === '') && compAddr) userData.address = compAddr; if (compCity) { userData.companyCity = compCity; userData.city = userData.city || compCity; }
if (!userData.zip_code && compZip) userData.zip_code = compZip; if (compCountry) { userData.companyCountry = compCountry; userData.country = userData.country || compCountry; }
if (!userData.city && compCity) userData.city = compCity; if (compContactName) { userData.companyContactPersonName = compContactName; userData.contactPersonName = userData.contactPersonName || compContactName; }
// populate country & contact phone (generic) if (compContactPhone) { userData.companyContactPersonPhone = compContactPhone; userData.contactPersonPhone = userData.contactPersonPhone || compContactPhone; }
if (!userData.country && compCountry) userData.country = compCountry; if (compPhone) { userData.companyPhone = compPhone; userData.phone = userData.phone || compPhone; }
if (!userData.contactPersonName && compContactName) userData.contactPersonName = compContactName;
if (!userData.contactPersonPhone && compContactPhone) userData.contactPersonPhone = compContactPhone;
// Mirror contact phone into generic phone too (helps templates using phone)
if ((!userData.phone || String(userData.phone).trim() === '') && compContactPhone) userData.phone = compContactPhone;
// Fill email if missing
if ((!userData.email || String(userData.email).trim() === '') && compEmail) userData.email = compEmail;
// ALSO populate company-prefixed keys so templates using companyXXX placeholders are satisfied // Compose company full address from DB fields
if (compName) userData.companyCompanyName = compName;
if (compReg) userData.companyRegistrationNumber = compReg;
if (compAddr) userData.companyAddress = compAddr; // already set but ensure presence
if (compZip) userData.companyZipCode = compZip;
if (compCity) userData.companyCity = compCity;
if (compCountry) userData.companyCountry = compCountry;
if (compContactName) userData.companyContactPersonName = compContactName;
if (compContactPhone) userData.companyContactPersonPhone = compContactPhone;
// ADDED: fill required placeholders
if (!userData.companyEmail && compEmail) userData.companyEmail = compEmail;
if (!userData.companyPhone && (compContactPhone || compGenericPhone))
userData.companyPhone = compContactPhone || compGenericPhone;
// ADDED: compute companyFullAddress if not provided
if (!userData.companyFullAddress) {
const addrParts = []; const addrParts = [];
if (compAddr) addrParts.push(compAddr); if (compAddr) addrParts.push(compAddr);
const zipCity = [compZip, compCity].filter(Boolean).join(' '); const zipCity = [compZip, compCity].filter(Boolean).join(' ');
if (zipCity) addrParts.push(zipCity); if (zipCity) addrParts.push(zipCity);
userData.companyFullAddress = addrParts.join(', '); const composed = addrParts.join(', ');
} if (composed) userData.companyFullAddress = composed;
// Fallback: if still empty try any supplied raw/full address fields
if (!userData.companyFullAddress) {
userData.companyFullAddress =
pick(rawUserData, ['companyFullAddress','company_full_address','fullAddress','full_address']) ||
pick(authUser, ['companyFullAddress','company_full_address']) || '';
}
// Safe debug log of which company-derived keys we populated // Keep email fallback behavior (no email column in company_profiles)
const populated = [];
if (compName) populated.push('companyName'); logger.debug(`[generatePdfWithSignature] Applied company_profiles data id=${comp.id || comp.company_id || 'unknown'}`);
if (compReg) populated.push('registrationNumber');
if (compAddr) populated.push('companyAddress/address');
if (compZip) populated.push('zip_code/companyZipCode');
if (compCity) populated.push('city/companyCity');
if (compCountry) populated.push('country/companyCountry');
if (compContactName) populated.push('contactPersonName/companyContactPersonName');
if (compContactPhone) populated.push('contactPersonPhone/companyContactPersonPhone');
if (compEmail) populated.push('email/companyEmail');
if (userData.companyFullAddress) populated.push('companyFullAddress'); // ADDED
if (userData.companyEmail) populated.push('companyEmail'); // ADDED
if (userData.companyPhone) populated.push('companyPhone'); // ADDED
logger.debug(`[generatePdfWithSignature] Filled missing company userData from company_profiles id=${comp.id || comp.company_id || 'unknown'} - populated: ${populated.join(',')}`);
} }
await uow.commit(); await uow.commit();
} catch (errU) { } catch (errU) {
@ -1062,16 +1038,36 @@ exports.generatePdfWithSignature = async (req, res) => {
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
} }
// Final fallbacks for composed address fields (still prefer DB values set above)
if (!userData.fullAddress) {
const addrParts = [];
if (userData.address) addrParts.push(userData.address);
const zipCity = [userData.zip_code, userData.city].filter(Boolean).join(' ');
if (zipCity) addrParts.push(zipCity);
userData.fullAddress = addrParts.join(', ');
}
if (!userData.companyFullAddress) {
const addrParts = [];
if (userData.companyAddress) addrParts.push(userData.companyAddress);
const zipCity = [userData.companyZipCode, userData.companyCity].filter(Boolean).join(' ');
if (zipCity) addrParts.push(zipCity);
userData.companyFullAddress = addrParts.join(', ');
}
// Insert userData into HTML (simple replace, adjust as needed) // Insert userData into HTML (simple replace, adjust as needed)
if (userData && typeof userData === 'object') { if (userData && typeof userData === 'object') {
Object.entries(userData).forEach(([key, value]) => { Object.entries(userData).forEach(([key, value]) => {
const beforeCount = (html.match(new RegExp(`{{\\s*${key}\\s*}}`, 'g')) || []).length; const beforeCount = (html.match(new RegExp(`{{\s*${key}\s*}}`, 'g')) || []).length;
html = html.replace(new RegExp(`{{\\s*${key}\\s*}}`, 'g'), value); html = html.replace(new RegExp(`{{\s*${key}\s*}}`, 'g'), value);
const afterCount = (html.match(new RegExp(`{{\\s*${key}\\s*}}`, 'g')) || []).length; const afterCount = (html.match(new RegExp(`{{\s*${key}\s*}}`, 'g')) || []).length;
if (beforeCount || afterCount) { if (beforeCount || afterCount) {
logger.debug(`[generatePdfWithSignature] replaced ${key}: before=${beforeCount} after=${afterCount}`); logger.debug(`[generatePdfWithSignature] replaced ${key}: before=${beforeCount} after=${afterCount}`);
} }
}); });
const remainingPlaceholders = html.match(/{{\s*([^}\s]+)\s*}}/g) || [];
if (remainingPlaceholders.length) {
logger.debug('[generatePdfWithSignature] Unreplaced placeholders detected', { count: remainingPlaceholders.length, samples: remainingPlaceholders.slice(0, 5) });
}
logger.debug(`[generatePdfWithSignature] User data replaced in HTML`); logger.debug(`[generatePdfWithSignature] User data replaced in HTML`);
} }
@ -1482,6 +1478,16 @@ exports.previewLatestForMe = async (req, res) => {
// Build variables from the authenticated user's DB data // Build variables from the authenticated user's DB data
const vars = {}; 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 // base fields
vars.email = req.user.email || ''; vars.email = req.user.email || '';
const d = new Date(); const d = new Date();
@ -1489,23 +1495,53 @@ exports.previewLatestForMe = async (req, res) => {
vars.currentDate = `${pad(d.getDate())}.${pad(d.getMonth() + 1)}.${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; vars.currentDate = `${pad(d.getDate())}.${pad(d.getMonth() + 1)}.${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
if (userType === 'personal') { if (userType === 'personal') {
try {
let p = null;
try { try {
const [rows] = await db.execute('SELECT * FROM personal_profiles WHERE user_id = ? LIMIT 1', [targetUserId]); const [rows] = await db.execute('SELECT * FROM personal_profiles WHERE user_id = ? LIMIT 1', [targetUserId]);
const p = rows && rows[0] ? rows[0] : null; 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) { if (p) {
const fullName = `${p.first_name || ''} ${p.last_name || ''}`.trim(); setIfEmpty('fullName', `${p.first_name || ''} ${p.last_name || ''}`.trim());
vars.fullName = fullName; setIfEmpty('address', p.address);
vars.address = p.address || ''; setIfEmpty('zip_code', p.zip_code);
vars.zip_code = p.zip_code || ''; setIfEmpty('city', p.city);
vars.city = p.city || ''; setIfEmpty('country', p.country);
vars.country = p.country || ''; setIfEmpty('phone', p.phone || p.phone_secondary);
vars.phone = p.phone || ''; 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 = []; const parts = [];
if (vars.address) parts.push(vars.address); if (vars.address) parts.push(vars.address);
const zipCity = [vars.zip_code, vars.city].filter(Boolean).join(' '); const zipCity = [vars.zip_code, vars.city].filter(Boolean).join(' ');
if (zipCity) parts.push(zipCity); if (zipCity) parts.push(zipCity);
vars.fullAddress = parts.join(', '); vars.fullAddress = parts.join(', ');
} }
logger.info('[previewLatestForMe] personal vars', { userId: targetUserId, found: !!p, vars });
} catch (e) { } catch (e) {
logger.warn('[previewLatestForMe] personal profile lookup failed', e && e.message); logger.warn('[previewLatestForMe] personal profile lookup failed', e && e.message);
} }
@ -1536,7 +1572,11 @@ exports.previewLatestForMe = async (req, res) => {
vars.companyRegistrationNumber = vars.registrationNumber; vars.companyRegistrationNumber = vars.registrationNumber;
vars.companyZipCode = vars.zip_code; vars.companyZipCode = vars.zip_code;
vars.companyCity = vars.city; vars.companyCity = vars.city;
// Signature/display name for company preview
vars.fullName = vars.contactPersonName || vars.companyName || '';
} }
logger.debug('[previewLatestForMe] company vars', { userId: targetUserId, found: !!c, vars });
} catch (e) { } catch (e) {
logger.warn('[previewLatestForMe] company profile lookup failed', e && e.message); logger.warn('[previewLatestForMe] company profile lookup failed', e && e.message);
} }
@ -1544,9 +1584,21 @@ exports.previewLatestForMe = async (req, res) => {
// Replace placeholders // Replace placeholders
Object.entries(vars).forEach(([k, v]) => { 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 ?? ''));
}); });
// Strip signature placeholder in preview (not signed yet)
html = html.replace(/{{\s*signatureImage\s*}}/g, '');
// Remove any remaining placeholders except stamp/signature markers
html = sanitizePlaceholders(html, ['companyStamp','companyStampInline','companyStampSmall','profitplanetSignature']);
// Log any remaining placeholders to aid debugging
const remaining = html.match(/{{\s*([^}\s]+)\s*}}/g) || [];
if (remaining.length) {
logger.debug('[previewLatestForMe] unreplaced placeholders', { count: remaining.length, samples: remaining.slice(0, 5) });
}
// Apply company stamp and signature where applicable // Apply company stamp and signature where applicable
try { html = await applyCompanyStampPlaceholders(html, req); } catch (e) {} try { html = await applyCompanyStampPlaceholders(html, req); } catch (e) {}
try { html = await applyProfitPlanetSignature(html); } catch (e) {} try { html = await applyProfitPlanetSignature(html); } catch (e) {}