feat: enhance user data handling in PDF generation with improved fallback logic for company and personal profiles
This commit is contained in:
parent
dfbd731f53
commit
1fc9af3be9
@ -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) {}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user