feat: enhance contract preview functionality with user-specific document retrieval

This commit is contained in:
seaznCode 2026-01-13 16:14:20 +01:00
parent 92a54866b4
commit b2a3132bea

View File

@ -1285,6 +1285,13 @@ exports.previewLatestForUser = async (req, res) => {
const overrideUserType = (req.query.userType || req.query.user_type || '').toString().toLowerCase(); const overrideUserType = (req.query.userType || req.query.user_type || '').toString().toLowerCase();
const templateType = (req.query.type || 'contract').toString(); const templateType = (req.query.type || 'contract').toString();
logger.info('[previewLatestForUser] start', {
targetUserId,
overrideUserType,
templateType,
requestId: req.id
});
if (!req.user || !['admin', 'super_admin'].includes(req.user.role)) { if (!req.user || !['admin', 'super_admin'].includes(req.user.role)) {
return res.status(403).json({ error: 'Forbidden: Admins only' }); return res.status(403).json({ error: 'Forbidden: Admins only' });
} }
@ -1302,32 +1309,91 @@ exports.previewLatestForUser = async (req, res) => {
logger.error('[previewLatestForUser] failed to load users row', e && e.message); logger.error('[previewLatestForUser] failed to load users row', e && e.message);
} }
if (!userRow) { if (!userRow) {
return res.status(404).json({ error: 'User not found' }); // Fallback: allow preview based solely on existing contract file even if user row is missing
logger.warn('[previewLatestForUser] user not found, continuing with contract lookup only', { targetUserId, requestId: req.id });
} }
const userType = (overrideUserType === 'personal' || overrideUserType === 'company') ? overrideUserType : userRow.user_type; const userType = (overrideUserType === 'personal' || overrideUserType === 'company')
? overrideUserType
: (userRow ? userRow.user_type : 'personal');
logger.info('[previewLatestForUser] user resolved/fallback', { targetUserId, userType, requestId: req.id });
// Find the latest active template for this user type // NEW: Preview the actual signed contract the user uploaded/signed (latest by created_at)
const latest = await DocumentTemplateService.getLatestActiveForUserType(userType, templateType); try {
if (!latest) { // order by upload_at (actual column) then id as tiebreaker
return res.status(404).json({ error: 'No active template found for user type' }); const [docRows] = await db.execute(
} `SELECT object_storage_id
FROM user_documents
// Fetch template HTML from S3 WHERE user_id = ?
const s3 = new S3Client({ AND document_type IN ('contract','signed_contract','contract_pdf','signed_contract_pdf')
AND object_storage_id IS NOT NULL
ORDER BY upload_at DESC, id DESC
LIMIT 1`,
[targetUserId]
);
const docRowsArr = Array.isArray(docRows) ? docRows : (docRows ? [docRows] : []);
logger.info('[previewLatestForUser] contract rows fetched', {
targetUserId,
count: docRowsArr.length,
rows: docRowsArr,
requestId: req.id
});
const doc = docRowsArr[0];
if (doc && doc.object_storage_id) {
// Use explicit endpoint + path-style to match Exoscale object storage
const s3File = new S3Client({
region: process.env.EXOSCALE_REGION, region: process.env.EXOSCALE_REGION,
endpoint: process.env.EXOSCALE_ENDPOINT, endpoint: process.env.EXOSCALE_ENDPOINT,
forcePathStyle: true,
credentials: { credentials: {
accessKeyId: process.env.EXOSCALE_ACCESS_KEY, accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
secretAccessKey: process.env.EXOSCALE_SECRET_KEY secretAccessKey: process.env.EXOSCALE_SECRET_KEY
} }
}); });
const command = new GetObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: latest.storageKey }); logger.info('[previewLatestForUser] attempting S3 fetch', {
const fileObj = await s3.send(command); bucket: process.env.EXOSCALE_BUCKET,
key: doc.object_storage_id,
userId: targetUserId
});
const cmd = new GetObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: doc.object_storage_id });
const fileObj = await s3File.send(cmd);
if (!fileObj.Body) { if (!fileObj.Body) {
return res.status(404).json({ error: 'Template file not available' }); logger.warn('[previewLatestForUser] S3 returned empty Body', {
bucket: process.env.EXOSCALE_BUCKET,
key: doc.object_storage_id,
userId: targetUserId
});
return res.status(404).json({ message: 'Contract file not available' });
} }
let html = await streamToString(fileObj.Body, latest.id); const chunks = [];
for await (const chunk of fileObj.Body) {
chunks.push(Buffer.from(chunk));
}
const pdfBuffer = Buffer.concat(chunks);
logger.info('[previewLatestForUser] S3 fetch success', {
bucket: process.env.EXOSCALE_BUCKET,
key: doc.object_storage_id,
userId: targetUserId,
size: pdfBuffer.length
});
const b64 = pdfBuffer.toString('base64');
const html = ensureHtmlDocument(`
<html><head><meta charset="utf-8" /><title>Contract Preview</title></head>
<body style="margin:0;padding:0;height:100vh;background:#0f172a;">
<embed src="data:application/pdf;base64,${b64}" type="application/pdf" style="width:100%;height:100%;border:none;" />
</body></html>
`);
res.setHeader('Content-Type', 'text/html; charset=utf-8');
return res.send(html);
}
logger.warn('[previewLatestForUser] no contract row with object_storage_id', { targetUserId, requestId: req.id });
} catch (e) {
logger.warn('[previewLatestForUser] reading user_documents failed', e && e.message);
return res.status(500).json({ message: 'Failed to load user contract file' });
}
// If no uploaded/signed contract exists, return not found (do not fallback to template)
return res.status(404).json({ message: 'No signed contract found for this user' });
// Build variable map from DB for target user // Build variable map from DB for target user
const vars = {}; const vars = {};