From 1fc9af3be9b9197d9a0d91505a8f8472c93f236b Mon Sep 17 00:00:00 2001 From: seaznCode Date: Tue, 13 Jan 2026 20:42:54 +0100 Subject: [PATCH] feat: enhance user data handling in PDF generation with improved fallback logic for company and personal profiles --- .../DocumentTemplateController.js | 274 +++++++++++------- 1 file changed, 163 insertions(+), 111 deletions(-) diff --git a/controller/documentTemplate/DocumentTemplateController.js b/controller/documentTemplate/DocumentTemplateController.js index 5aa9e5f..efb38ac 100644 --- a/controller/documentTemplate/DocumentTemplateController.js +++ b/controller/documentTemplate/DocumentTemplateController.js @@ -730,6 +730,27 @@ exports.generatePdfWithSignature = async (req, res) => { 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 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); @@ -759,6 +780,12 @@ exports.generatePdfWithSignature = async (req, res) => { // add formatted currentDate into normalized map 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) try { // Build candidate identifiers: various authUser/rawUserData id/email shapes @@ -798,34 +825,25 @@ exports.generatePdfWithSignature = async (req, res) => { } } if (prof) { - if (!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 || ''; - } - if (!userData.city) { - userData.city = pick(prof, ['city','town','municipality']) || userData.city || ''; - } - if (!userData.personalAddress) { - userData.personalAddress = pick(prof, ['personal_address','personalAddress']) || userData.personalAddress || ''; - } - if (!userData.phone) { - 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']); - if (fn || ln) userData.fullName = `${fn || ''} ${ln || ''}`.trim(); - } - } - userData.fullAddress = pick(rawUserData, ['fullAddress','full_address']) || - pick(authUser, ['fullAddress','full_address']) || - pick(prof, ['full_address','fullAddress']) || - (userData.address ? `${userData.address}${userData.zip_code || userData.city ? ', ' : ''}${userData.zip_code ? userData.zip_code + ' ' : ''}${userData.city || ''}`.trim() : (userData.personalAddress || '')) || ''; - logger.debug(`[generatePdfWithSignature] Filled missing userData from personal_profiles for profile id=${prof.id || prof.user_id || 'unknown'}`); + // 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.zip_code = pick(prof, ['zip','zip_code','postal_code','postalCode']) || userData.zip_code || ''; + userData.city = pick(prof, ['city','town','municipality']) || userData.city || ''; + userData.personalAddress = pick(prof, ['personal_address','personalAddress']) || userData.personalAddress || ''; + userData.phone = pick(prof, ['phone','telephone','mobile','contact_phone','contactPhone','phone_secondary']) || userData.phone || ''; + const fn = pick(prof, ['full_name','fullName','name','first_name','firstName']); + const ln = pick(prof, ['last_name','lastName']); + if (fn || ln) userData.fullName = `${fn || ''} ${ln || ''}`.trim(); + if (!userData.fullName) userData.fullName = pick(prof, ['full_name','fullName','name']) || userData.fullName || ''; + + // Compose full address primarily from DB columns + 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(', ') || 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 --- @@ -915,80 +933,38 @@ exports.generatePdfWithSignature = async (req, res) => { } } if (comp) { - // Primary company fields - const compName = pick(comp, ['company_name','companyName','name','company']); - const compReg = pick(comp, ['registration_number','registrationNumber','registrationNo']); - const compAddr = pick(comp, ['address','company_address','street','streetAddress']); - const compZip = pick(comp, ['zip','zip_code','postal_code','postcode']); - const compCity = pick(comp, ['city','town']); - const compCountry = pick(comp, ['country','country_code','countryName']); - const compContactName = pick(comp, ['contact_person_name','contactPersonName','contactName','contact']); - const compContactPhone = pick(comp, ['contact_person_phone','contactPersonPhone','phone','telephone','mobile']); - const compEmail = pick(comp, ['email','company_email','contact_email']); - // ADDED: possible generic phone field - const compGenericPhone = pick(comp, ['phone','telephone','mobile']); + // Prefer DB values directly + const compName = pick(comp, ['company_name']); + const compReg = pick(comp, ['registration_number']); + const compAddr = pick(comp, ['address']); + const compZip = pick(comp, ['zip_code']); + const compCity = pick(comp, ['city']); + const compCountry = pick(comp, ['country']); + const compContactName = pick(comp, ['contact_person_name']); + const compContactPhone = pick(comp, ['contact_person_phone']); + const compPhone = pick(comp, ['phone']); - if (!userData.companyName && compName) userData.companyName = compName; - if (!userData.registrationNumber && compReg) userData.registrationNumber = compReg; - if (!userData.companyAddress && compAddr) userData.companyAddress = compAddr; - // Also populate the generic address key used in many templates - if ((!userData.address || String(userData.address).trim() === '') && compAddr) userData.address = compAddr; - if (!userData.zip_code && compZip) userData.zip_code = compZip; - if (!userData.city && compCity) userData.city = compCity; - // populate country & contact phone (generic) - if (!userData.country && compCountry) userData.country = compCountry; - 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; + if (compName) { userData.companyName = compName; userData.companyCompanyName = compName; } + if (compReg) { userData.registrationNumber = compReg; userData.companyRegistrationNumber = compReg; } + if (compAddr) { userData.companyAddress = compAddr; userData.address = userData.address || compAddr; } + if (compZip) { userData.companyZipCode = compZip; userData.zip_code = userData.zip_code || compZip; } + if (compCity) { userData.companyCity = compCity; userData.city = userData.city || compCity; } + if (compCountry) { userData.companyCountry = compCountry; userData.country = userData.country || compCountry; } + if (compContactName) { userData.companyContactPersonName = compContactName; userData.contactPersonName = userData.contactPersonName || compContactName; } + if (compContactPhone) { userData.companyContactPersonPhone = compContactPhone; userData.contactPersonPhone = userData.contactPersonPhone || compContactPhone; } + if (compPhone) { userData.companyPhone = compPhone; userData.phone = userData.phone || compPhone; } - // ALSO populate company-prefixed keys so templates using companyXXX placeholders are satisfied - 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; + // Compose company full address from DB fields + const addrParts = []; + if (compAddr) addrParts.push(compAddr); + const zipCity = [compZip, compCity].filter(Boolean).join(' '); + if (zipCity) addrParts.push(zipCity); + const composed = addrParts.join(', '); + if (composed) userData.companyFullAddress = composed; - // ADDED: fill required placeholders - if (!userData.companyEmail && compEmail) userData.companyEmail = compEmail; - if (!userData.companyPhone && (compContactPhone || compGenericPhone)) - userData.companyPhone = compContactPhone || compGenericPhone; + // Keep email fallback behavior (no email column in company_profiles) - // ADDED: compute companyFullAddress if not provided - if (!userData.companyFullAddress) { - const addrParts = []; - if (compAddr) addrParts.push(compAddr); - const zipCity = [compZip, compCity].filter(Boolean).join(' '); - if (zipCity) addrParts.push(zipCity); - userData.companyFullAddress = addrParts.join(', '); - } - // 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 - const populated = []; - if (compName) populated.push('companyName'); - 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(',')}`); + logger.debug(`[generatePdfWithSignature] Applied company_profiles data id=${comp.id || comp.company_id || 'unknown'}`); } await uow.commit(); } catch (errU) { @@ -1062,16 +1038,36 @@ exports.generatePdfWithSignature = async (req, res) => { } 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) if (userData && typeof userData === 'object') { Object.entries(userData).forEach(([key, value]) => { - const beforeCount = (html.match(new RegExp(`{{\\s*${key}\\s*}}`, 'g')) || []).length; - html = html.replace(new RegExp(`{{\\s*${key}\\s*}}`, 'g'), value); - const afterCount = (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); + const afterCount = (html.match(new RegExp(`{{\s*${key}\s*}}`, 'g')) || []).length; if (beforeCount || 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`); } @@ -1482,6 +1478,16 @@ exports.previewLatestForMe = async (req, res) => { // 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(); @@ -1490,22 +1496,52 @@ exports.previewLatestForMe = async (req, res) => { if (userType === 'personal') { try { - const [rows] = await db.execute('SELECT * FROM personal_profiles WHERE user_id = ? LIMIT 1', [targetUserId]); - const p = rows && rows[0] ? rows[0] : null; + 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) { - const fullName = `${p.first_name || ''} ${p.last_name || ''}`.trim(); - vars.fullName = fullName; - vars.address = p.address || ''; - vars.zip_code = p.zip_code || ''; - vars.city = p.city || ''; - vars.country = p.country || ''; - vars.phone = p.phone || ''; + 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); } @@ -1536,7 +1572,11 @@ exports.previewLatestForMe = async (req, res) => { 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 || ''; } + logger.debug('[previewLatestForMe] company vars', { userId: targetUserId, found: !!c, vars }); } catch (e) { logger.warn('[previewLatestForMe] company profile lookup failed', e && e.message); } @@ -1544,9 +1584,21 @@ exports.previewLatestForMe = async (req, res) => { // 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 ?? '')); }); + // 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 try { html = await applyCompanyStampPlaceholders(html, req); } catch (e) {} try { html = await applyProfitPlanetSignature(html); } catch (e) {}