document upload + user mgmt preview
This commit is contained in:
parent
66d1ede207
commit
dfbd731f53
@ -12,6 +12,7 @@ const db = require('../../database/database');
|
|||||||
const UnitOfWork = require('../../database/UnitOfWork');
|
const UnitOfWork = require('../../database/UnitOfWork');
|
||||||
const { logger } = require('../../middleware/logger');
|
const { logger } = require('../../middleware/logger');
|
||||||
const CompanyStampService = require('../../services/stamp/company/CompanyStampService');
|
const CompanyStampService = require('../../services/stamp/company/CompanyStampService');
|
||||||
|
const { s3: sharedExoscaleClient } = require('../../utils/exoscaleUploader');
|
||||||
|
|
||||||
// Ensure debug directory exists and helper to save files
|
// Ensure debug directory exists and helper to save files
|
||||||
function ensureDebugDir() {
|
function ensureDebugDir() {
|
||||||
@ -154,6 +155,45 @@ async function enrichTemplate(template, s3, serverBaseUrl = null) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hardened reader for S3 bodies (Node stream, async iterable, web stream, or Buffer)
|
||||||
|
async function s3BodyToBuffer(body) {
|
||||||
|
if (!body) return null;
|
||||||
|
if (typeof body.transformToByteArray === 'function') {
|
||||||
|
const arr = await body.transformToByteArray();
|
||||||
|
return Buffer.from(arr);
|
||||||
|
}
|
||||||
|
if (typeof body.getReader === 'function') {
|
||||||
|
const reader = body.getReader();
|
||||||
|
const chunks = [];
|
||||||
|
// eslint-disable-next-line no-constant-condition
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
chunks.push(Buffer.from(value));
|
||||||
|
}
|
||||||
|
return Buffer.concat(chunks);
|
||||||
|
}
|
||||||
|
if (body[Symbol.asyncIterator]) {
|
||||||
|
const chunks = [];
|
||||||
|
for await (const chunk of body) {
|
||||||
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
||||||
|
}
|
||||||
|
return Buffer.concat(chunks);
|
||||||
|
}
|
||||||
|
if (typeof body.on === 'function') {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const chunks = [];
|
||||||
|
body.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
||||||
|
body.on('end', () => resolve(Buffer.concat(chunks)));
|
||||||
|
body.on('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (Buffer.isBuffer(body)) return body;
|
||||||
|
if (ArrayBuffer.isView(body)) return Buffer.from(body.buffer, body.byteOffset, body.byteLength);
|
||||||
|
if (body instanceof ArrayBuffer) return Buffer.from(body);
|
||||||
|
throw new Error('Unsupported S3 Body type');
|
||||||
|
}
|
||||||
|
|
||||||
// Helper to convert S3 stream to string (hardened + debug)
|
// Helper to convert S3 stream to string (hardened + debug)
|
||||||
function streamToString(s3BodyStream, templateId) {
|
function streamToString(s3BodyStream, templateId) {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
@ -281,6 +321,7 @@ exports.listTemplatesPublic = async (req, res) => {
|
|||||||
|
|
||||||
exports.uploadTemplate = async (req, res) => {
|
exports.uploadTemplate = async (req, res) => {
|
||||||
const { name, type, description, lang } = req.body;
|
const { name, type, description, lang } = req.body;
|
||||||
|
const rawContractType = req.body.contract_type || req.body.contractType;
|
||||||
const rawUserType = req.body.user_type || req.body.userType;
|
const rawUserType = req.body.user_type || req.body.userType;
|
||||||
const allowed = ['personal','company','both'];
|
const allowed = ['personal','company','both'];
|
||||||
const user_type = allowed.includes(rawUserType) ? rawUserType : 'both';
|
const user_type = allowed.includes(rawUserType) ? rawUserType : 'both';
|
||||||
@ -288,6 +329,11 @@ exports.uploadTemplate = async (req, res) => {
|
|||||||
if (!file) return res.status(400).json({ error: 'No file uploaded' });
|
if (!file) return res.status(400).json({ error: 'No file uploaded' });
|
||||||
if (!lang || !['en', 'de'].includes(lang)) return res.status(400).json({ error: 'Invalid or missing language' });
|
if (!lang || !['en', 'de'].includes(lang)) return res.status(400).json({ error: 'Invalid or missing language' });
|
||||||
|
|
||||||
|
const allowedContractTypes = ['contract', 'gdpr'];
|
||||||
|
const contract_type = (type === 'contract' && allowedContractTypes.includes(rawContractType))
|
||||||
|
? rawContractType
|
||||||
|
: (type === 'contract' ? 'contract' : null);
|
||||||
|
|
||||||
// Use "english" for en, "german" for de
|
// Use "english" for en, "german" for de
|
||||||
const langFolder = lang === 'en' ? 'english' : 'german';
|
const langFolder = lang === 'en' ? 'english' : 'german';
|
||||||
const key = `DocumentTemplates/${langFolder}/${Date.now()}_${file.originalname}`;
|
const key = `DocumentTemplates/${langFolder}/${Date.now()}_${file.originalname}`;
|
||||||
@ -307,7 +353,7 @@ exports.uploadTemplate = async (req, res) => {
|
|||||||
ContentType: file.mimetype
|
ContentType: file.mimetype
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const template = await DocumentTemplateService.uploadTemplate({ name, type, storageKey: key, description, lang, version: 1, user_type });
|
const template = await DocumentTemplateService.uploadTemplate({ name, type, contract_type, storageKey: key, description, lang, version: 1, user_type });
|
||||||
// Enrich with previewUrl, fileUrl, html
|
// Enrich with previewUrl, fileUrl, html
|
||||||
const enriched = await enrichTemplate(template, s3);
|
const enriched = await enrichTemplate(template, s3);
|
||||||
res.status(201).json(enriched);
|
res.status(201).json(enriched);
|
||||||
@ -337,6 +383,7 @@ exports.updateTemplate = async (req, res) => {
|
|||||||
const id = req.params.id;
|
const id = req.params.id;
|
||||||
const { name, type, description, lang } = req.body;
|
const { name, type, description, lang } = req.body;
|
||||||
const rawUserType = req.body.user_type || req.body.userType;
|
const rawUserType = req.body.user_type || req.body.userType;
|
||||||
|
const rawContractType = req.body.contract_type || req.body.contractType;
|
||||||
const allowed = ['personal','company','both'];
|
const allowed = ['personal','company','both'];
|
||||||
const user_type = allowed.includes(rawUserType) ? rawUserType : undefined;
|
const user_type = allowed.includes(rawUserType) ? rawUserType : undefined;
|
||||||
let storageKey;
|
let storageKey;
|
||||||
@ -345,6 +392,18 @@ exports.updateTemplate = async (req, res) => {
|
|||||||
const current = await DocumentTemplateService.getTemplate(id);
|
const current = await DocumentTemplateService.getTemplate(id);
|
||||||
if (!current) return res.status(404).json({ error: 'Template not found' });
|
if (!current) return res.status(404).json({ error: 'Template not found' });
|
||||||
|
|
||||||
|
const nextType = type !== undefined ? type : current.type;
|
||||||
|
const allowedContractTypes = ['contract', 'gdpr'];
|
||||||
|
let contract_type = null;
|
||||||
|
if (nextType === 'contract') {
|
||||||
|
const candidate = rawContractType !== undefined ? rawContractType : current.contract_type;
|
||||||
|
if (candidate && allowedContractTypes.includes(candidate)) {
|
||||||
|
contract_type = candidate;
|
||||||
|
} else {
|
||||||
|
contract_type = 'contract';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Use "english" for en, "german" for de
|
// Use "english" for en, "german" for de
|
||||||
const langFolder = lang ? (lang === 'en' ? 'english' : 'german') : (current.lang === 'en' ? 'english' : 'german');
|
const langFolder = lang ? (lang === 'en' ? 'english' : 'german') : (current.lang === 'en' ? 'english' : 'german');
|
||||||
if (file) {
|
if (file) {
|
||||||
@ -367,7 +426,8 @@ exports.updateTemplate = async (req, res) => {
|
|||||||
|
|
||||||
const updateData = {
|
const updateData = {
|
||||||
name: name !== undefined ? name : current.name,
|
name: name !== undefined ? name : current.name,
|
||||||
type: type !== undefined ? type : current.type,
|
type: nextType,
|
||||||
|
contract_type,
|
||||||
description: description !== undefined ? description : current.description,
|
description: description !== undefined ? description : current.description,
|
||||||
lang: lang !== undefined ? lang : current.lang,
|
lang: lang !== undefined ? lang : current.lang,
|
||||||
storageKey: storageKey || current.storageKey,
|
storageKey: storageKey || current.storageKey,
|
||||||
@ -1278,19 +1338,15 @@ exports.previewPdf = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// NEW: Admin-only endpoint to preview latest active contract for a specific user with DB-filled placeholders
|
// NEW: Admin-only endpoint to preview the user's uploaded/signed contract (contract or gdpr) from object storage only
|
||||||
// GET /api/admin/contracts/:id/preview?userType=personal|company&type=contract
|
// GET /api/admin/contracts/:id/preview?userType=personal|company&contract_type=contract|gdpr
|
||||||
exports.previewLatestForUser = async (req, res) => {
|
exports.previewLatestForUser = async (req, res) => {
|
||||||
const targetUserId = parseInt(req.params.id, 10);
|
const targetUserId = parseInt(req.params.id, 10);
|
||||||
const overrideUserType = (req.query.userType || req.query.user_type || '').toString().toLowerCase();
|
const contractTypeParam = (req.query.contract_type || req.query.contractType || '').toString().toLowerCase();
|
||||||
const templateType = (req.query.type || 'contract').toString();
|
const allowedContractTypes = ['contract', 'gdpr'];
|
||||||
|
const contractType = allowedContractTypes.includes(contractTypeParam) ? contractTypeParam : 'contract';
|
||||||
|
|
||||||
logger.info('[previewLatestForUser] start', {
|
logger.info('[previewLatestForUser] start', { targetUserId, contractType, requestId: req.id });
|
||||||
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' });
|
||||||
@ -1300,7 +1356,7 @@ exports.previewLatestForUser = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Resolve target user and determine user_type if not overridden
|
// Resolve target user (only for logging and email fallback if needed)
|
||||||
let userRow = null;
|
let userRow = null;
|
||||||
try {
|
try {
|
||||||
const [uRows] = await db.execute('SELECT id, email, user_type, role FROM users WHERE id = ? LIMIT 1', [targetUserId]);
|
const [uRows] = await db.execute('SELECT id, email, user_type, role FROM users WHERE id = ? LIMIT 1', [targetUserId]);
|
||||||
@ -1308,40 +1364,44 @@ exports.previewLatestForUser = async (req, res) => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('[previewLatestForUser] failed to load users row', e && e.message);
|
logger.error('[previewLatestForUser] failed to load users row', e && e.message);
|
||||||
}
|
}
|
||||||
if (!userRow) {
|
|
||||||
// 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')
|
// Choose document_type set based on contractType (aligned with ContractUploadService paths)
|
||||||
? overrideUserType
|
// uploadContract stores under contracts/<category>/<userId>/<contract_type> with document_type 'contract'
|
||||||
: (userRow ? userRow.user_type : 'personal');
|
// so use contract_type column to disambiguate between contract vs gdpr
|
||||||
logger.info('[previewLatestForUser] user resolved/fallback', { targetUserId, userType, requestId: req.id });
|
const docTypesMap = {
|
||||||
|
contract: ['contract', 'signed_contract', 'contract_pdf', 'signed_contract_pdf'],
|
||||||
|
gdpr: ['contract', 'signed_contract', 'contract_pdf', 'signed_contract_pdf']
|
||||||
|
};
|
||||||
|
const docTypes = docTypesMap[contractType] || docTypesMap.contract;
|
||||||
|
const placeholders = docTypes.map(() => '?').join(',');
|
||||||
|
|
||||||
// NEW: Preview the actual signed contract the user uploaded/signed (latest by created_at)
|
// Fetch latest uploaded document for this type from user_documents
|
||||||
|
let doc = null;
|
||||||
try {
|
try {
|
||||||
// order by upload_at (actual column) then id as tiebreaker
|
const [rows] = await db.execute(
|
||||||
const [docRows] = await db.execute(
|
|
||||||
`SELECT object_storage_id
|
`SELECT object_storage_id
|
||||||
FROM user_documents
|
FROM user_documents
|
||||||
WHERE user_id = ?
|
WHERE user_id = ?
|
||||||
AND document_type IN ('contract','signed_contract','contract_pdf','signed_contract_pdf')
|
AND document_type IN (${placeholders})
|
||||||
AND object_storage_id IS NOT NULL
|
AND object_storage_id IS NOT NULL
|
||||||
|
AND (contract_type = ? OR (contract_type IS NULL AND ? = 'contract'))
|
||||||
ORDER BY upload_at DESC, id DESC
|
ORDER BY upload_at DESC, id DESC
|
||||||
LIMIT 1`,
|
LIMIT 1`,
|
||||||
[targetUserId]
|
[targetUserId, ...docTypes, contractType, contractType]
|
||||||
);
|
);
|
||||||
const docRowsArr = Array.isArray(docRows) ? docRows : (docRows ? [docRows] : []);
|
const arr = Array.isArray(rows) ? rows : (rows ? [rows] : []);
|
||||||
logger.info('[previewLatestForUser] contract rows fetched', {
|
doc = arr[0] || null;
|
||||||
targetUserId,
|
logger.info('[previewLatestForUser] user_documents lookup', { targetUserId, contractType, count: arr.length });
|
||||||
count: docRowsArr.length,
|
} catch (e) {
|
||||||
rows: docRowsArr,
|
logger.warn('[previewLatestForUser] user_documents lookup failed', e && (e.stack || e.message));
|
||||||
requestId: req.id
|
}
|
||||||
});
|
|
||||||
const doc = docRowsArr[0];
|
if (!doc || !doc.object_storage_id) {
|
||||||
if (doc && doc.object_storage_id) {
|
return res.status(404).json({ message: `No uploaded ${contractType.toUpperCase()} file found for this user` });
|
||||||
// Use explicit endpoint + path-style to match Exoscale object storage
|
}
|
||||||
const s3File = new S3Client({
|
|
||||||
|
try {
|
||||||
|
const s3File = sharedExoscaleClient || new S3Client({
|
||||||
region: process.env.EXOSCALE_REGION,
|
region: process.env.EXOSCALE_REGION,
|
||||||
endpoint: process.env.EXOSCALE_ENDPOINT,
|
endpoint: process.env.EXOSCALE_ENDPOINT,
|
||||||
forcePathStyle: true,
|
forcePathStyle: true,
|
||||||
@ -1353,133 +1413,34 @@ exports.previewLatestForUser = async (req, res) => {
|
|||||||
logger.info('[previewLatestForUser] attempting S3 fetch', {
|
logger.info('[previewLatestForUser] attempting S3 fetch', {
|
||||||
bucket: process.env.EXOSCALE_BUCKET,
|
bucket: process.env.EXOSCALE_BUCKET,
|
||||||
key: doc.object_storage_id,
|
key: doc.object_storage_id,
|
||||||
userId: targetUserId
|
userId: targetUserId,
|
||||||
|
contractType
|
||||||
});
|
});
|
||||||
const cmd = new GetObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: doc.object_storage_id });
|
const cmd = new GetObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: doc.object_storage_id });
|
||||||
const fileObj = await s3File.send(cmd);
|
const fileObj = await s3File.send(cmd);
|
||||||
if (!fileObj.Body) {
|
const pdfBuffer = await s3BodyToBuffer(fileObj.Body);
|
||||||
logger.warn('[previewLatestForUser] S3 returned empty Body', {
|
if (!pdfBuffer || !pdfBuffer.length) {
|
||||||
bucket: process.env.EXOSCALE_BUCKET,
|
logger.warn('[previewLatestForUser] S3 returned empty Body', { key: doc.object_storage_id, userId: targetUserId, contractType });
|
||||||
key: doc.object_storage_id,
|
return res.status(404).json({ message: `${contractType.toUpperCase()} file not available` });
|
||||||
userId: targetUserId
|
|
||||||
});
|
|
||||||
return res.status(404).json({ message: 'Contract file not available' });
|
|
||||||
}
|
}
|
||||||
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 b64 = pdfBuffer.toString('base64');
|
||||||
const html = ensureHtmlDocument(`
|
const html = ensureHtmlDocument(`
|
||||||
<html><head><meta charset="utf-8" /><title>Contract Preview</title></head>
|
<html><head><meta charset="utf-8" /><title>${contractType.toUpperCase()} Preview</title></head>
|
||||||
<body style="margin:0;padding:0;height:100vh;background:#0f172a;">
|
<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;" />
|
<embed src="data:application/pdf;base64,${b64}" type="application/pdf" style="width:100%;height:100%;border:none;" />
|
||||||
</body></html>
|
</body></html>
|
||||||
`);
|
`);
|
||||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||||
return res.send(html);
|
return res.send(html);
|
||||||
}
|
|
||||||
logger.warn('[previewLatestForUser] no contract row with object_storage_id', { targetUserId, requestId: req.id });
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn('[previewLatestForUser] reading user_documents failed', e && e.message);
|
if (e && (e.name === 'NoSuchKey' || (e.$metadata && e.$metadata.httpStatusCode === 404))) {
|
||||||
return res.status(500).json({ message: 'Failed to load user contract file' });
|
logger.warn('[previewLatestForUser] object missing in storage', { key: doc.object_storage_id, userId: targetUserId, contractType });
|
||||||
|
return res.status(404).json({ message: `${contractType.toUpperCase()} file not available` });
|
||||||
}
|
}
|
||||||
|
logger.error('[previewLatestForUser] S3 fetch failed', e && (e.stack || e.message));
|
||||||
// If no uploaded/signed contract exists, return not found (do not fallback to template)
|
return res.status(500).json({ message: 'Failed to load user document' });
|
||||||
return res.status(404).json({ message: 'No signed contract found for this user' });
|
|
||||||
|
|
||||||
// Build variable map from DB for target user
|
|
||||||
const vars = {};
|
|
||||||
// Always include email and currentDate
|
|
||||||
vars.email = userRow.email || '';
|
|
||||||
const now = new Date();
|
|
||||||
const pad = (n) => String(n).padStart(2, '0');
|
|
||||||
vars.currentDate = `${pad(now.getDate())}.${pad(now.getMonth() + 1)}.${now.getFullYear()} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
|
|
||||||
|
|
||||||
// Personal profile fields
|
|
||||||
if (userType === 'personal') {
|
|
||||||
try {
|
|
||||||
const [pRows] = await db.execute('SELECT * FROM personal_profiles WHERE user_id = ? LIMIT 1', [targetUserId]);
|
|
||||||
const p = (pRows && pRows[0]) ? pRows[0] : null;
|
|
||||||
if (p) {
|
|
||||||
const first = p.first_name || '';
|
|
||||||
const last = p.last_name || '';
|
|
||||||
const fullName = `${first} ${last}`.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 || '';
|
|
||||||
const fullAddressParts = [];
|
|
||||||
if (vars.address) fullAddressParts.push(vars.address);
|
|
||||||
const zipCity = [vars.zip_code, vars.city].filter(Boolean).join(' ');
|
|
||||||
if (zipCity) fullAddressParts.push(zipCity);
|
|
||||||
vars.fullAddress = fullAddressParts.join(', ');
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
logger.warn('[previewLatestForUser] load personal_profiles failed', e && e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Company profile fields
|
|
||||||
if (userType === 'company') {
|
|
||||||
try {
|
|
||||||
const [cRows] = await db.execute('SELECT * FROM company_profiles WHERE user_id = ? LIMIT 1', [targetUserId]);
|
|
||||||
const c = (cRows && cRows[0]) ? cRows[0] : null;
|
|
||||||
if (c) {
|
|
||||||
vars.companyName = c.company_name || '';
|
|
||||||
vars.registrationNumber = c.registration_number || '';
|
|
||||||
vars.companyAddress = c.address || '';
|
|
||||||
// generic address keys used by some templates
|
|
||||||
vars.address = vars.companyAddress;
|
|
||||||
vars.zip_code = c.zip_code || '';
|
|
||||||
vars.city = c.city || '';
|
|
||||||
vars.country = c.country || '';
|
|
||||||
vars.contactPersonName = c.contact_person_name || '';
|
|
||||||
vars.contactPersonPhone = c.contact_person_phone || c.phone || '';
|
|
||||||
vars.companyEmail = c.email || c.company_email || c.contact_email || userRow.email || '';
|
|
||||||
vars.companyPhone = c.phone || c.contact_person_phone || '';
|
|
||||||
const addrParts = [];
|
|
||||||
if (vars.companyAddress) addrParts.push(vars.companyAddress);
|
|
||||||
const zipCity = [vars.zip_code, vars.city].filter(Boolean).join(' ');
|
|
||||||
if (zipCity) addrParts.push(zipCity);
|
|
||||||
vars.companyFullAddress = addrParts.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;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.warn('[previewLatestForUser] load company_profiles failed', e && e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace placeholders with resolved variables (simple global replace)
|
|
||||||
if (vars && typeof vars === 'object') {
|
|
||||||
Object.entries(vars).forEach(([key, value]) => {
|
|
||||||
const before = (html.match(new RegExp(`{{\\s*${key}\\s*}}`, 'g')) || []).length;
|
|
||||||
if (before) {
|
|
||||||
html = html.replace(new RegExp(`{{\\s*${key}\\s*}}`, 'g'), String(value ?? ''));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply stamp/signature placeholders (harmless if placeholders are absent)
|
|
||||||
try { html = await applyCompanyStampPlaceholders(html, { ...req, user: { ...userRow, user_type: userType } }); } catch (e) { logger.warn('[previewLatestForUser] applyCompanyStampPlaceholders failed', e && e.message); }
|
|
||||||
try { html = await applyProfitPlanetSignature(html); } catch (e) { logger.warn('[previewLatestForUser] applyProfitPlanetSignature failed', e && e.message); }
|
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
||||||
return res.send(ensureHtmlDocument(html));
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('[previewLatestForUser] error', err && err.stack ? err.stack : err);
|
logger.error('[previewLatestForUser] error', err && err.stack ? err.stack : err);
|
||||||
return res.status(500).json({ error: 'Failed to render preview' });
|
return res.status(500).json({ error: 'Failed to render preview' });
|
||||||
@ -1494,9 +1455,13 @@ exports.previewLatestForMe = async (req, res) => {
|
|||||||
const userType = req.user.user_type || req.user.userType;
|
const userType = req.user.user_type || req.user.userType;
|
||||||
if (!targetUserId || !userType) return res.status(400).json({ error: 'Invalid authenticated user' });
|
if (!targetUserId || !userType) return res.status(400).json({ error: 'Invalid authenticated user' });
|
||||||
|
|
||||||
|
const contractTypeParam = (req.query.contract_type || req.query.contractType || '').toString().toLowerCase();
|
||||||
|
const allowedContractTypes = ['contract', 'gdpr'];
|
||||||
|
const contractType = allowedContractTypes.includes(contractTypeParam) ? contractTypeParam : 'contract';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Find the latest active template for this user type
|
// Find the latest active template for this user type
|
||||||
const latest = await DocumentTemplateService.getLatestActiveForUserType(userType, 'contract');
|
const latest = await DocumentTemplateService.getLatestActiveForUserType(userType, 'contract', contractType);
|
||||||
if (!latest) {
|
if (!latest) {
|
||||||
return res.status(404).json({ error: 'No active template found for your user type' });
|
return res.status(404).json({ error: 'No active template found for your user type' });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,25 +6,42 @@ class ContractUploadController {
|
|||||||
static async uploadPersonalContract(req, res) {
|
static async uploadPersonalContract(req, res) {
|
||||||
const userId = req.user.userId;
|
const userId = req.user.userId;
|
||||||
logger.info(`[ContractUploadController] uploadPersonalContract called for userId: ${userId}`);
|
logger.info(`[ContractUploadController] uploadPersonalContract called for userId: ${userId}`);
|
||||||
const file = req.file;
|
const file = req.file; // optional, we now generate from templates when absent
|
||||||
// Accept contractData and signatureImage from body (JSON or multipart)
|
|
||||||
const contractData = req.body.contractData ? JSON.parse(req.body.contractData) : undefined;
|
const contractData = req.body.contractData ? JSON.parse(req.body.contractData) : undefined;
|
||||||
const signatureImage = req.body.signatureImage; // base64 string or Buffer
|
const signatureImage = req.body.signatureImage;
|
||||||
|
|
||||||
const unitOfWork = new UnitOfWork();
|
const unitOfWork = new UnitOfWork();
|
||||||
await unitOfWork.start();
|
await unitOfWork.start();
|
||||||
try {
|
try {
|
||||||
const result = await ContractUploadService.uploadContract({
|
const uploads = [];
|
||||||
|
// Primary contract
|
||||||
|
uploads.push(await ContractUploadService.uploadContract({
|
||||||
userId,
|
userId,
|
||||||
file,
|
file,
|
||||||
documentType: 'contract',
|
documentType: 'contract',
|
||||||
contractCategory: 'personal',
|
contractCategory: 'personal',
|
||||||
unitOfWork,
|
unitOfWork,
|
||||||
contractData,
|
contractData,
|
||||||
signatureImage
|
signatureImage,
|
||||||
});
|
contract_type: 'contract',
|
||||||
|
user_type: 'personal'
|
||||||
|
}));
|
||||||
|
|
||||||
|
// GDPR contract (auto-generated from latest GDPR template)
|
||||||
|
uploads.push(await ContractUploadService.uploadContract({
|
||||||
|
userId,
|
||||||
|
documentType: 'contract',
|
||||||
|
contractCategory: 'personal',
|
||||||
|
unitOfWork,
|
||||||
|
contractData,
|
||||||
|
signatureImage,
|
||||||
|
contract_type: 'gdpr',
|
||||||
|
user_type: 'personal'
|
||||||
|
}));
|
||||||
|
|
||||||
await unitOfWork.commit();
|
await unitOfWork.commit();
|
||||||
logger.info(`[ContractUploadController] uploadPersonalContract success for userId: ${userId}`);
|
logger.info(`[ContractUploadController] uploadPersonalContract success for userId: ${userId}`);
|
||||||
res.json({ success: true, upload: result, downloadUrl: result.url || null });
|
res.json({ success: true, uploads, downloadUrls: uploads.map(u => u.url || null) });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[ContractUploadController] uploadPersonalContract error for userId: ${userId}`, { error });
|
logger.error(`[ContractUploadController] uploadPersonalContract error for userId: ${userId}`, { error });
|
||||||
await unitOfWork.rollback(error);
|
await unitOfWork.rollback(error);
|
||||||
@ -48,7 +65,9 @@ class ContractUploadController {
|
|||||||
contractCategory: 'company',
|
contractCategory: 'company',
|
||||||
unitOfWork,
|
unitOfWork,
|
||||||
contractData,
|
contractData,
|
||||||
signatureImage
|
signatureImage,
|
||||||
|
contract_type: 'contract',
|
||||||
|
user_type: 'company'
|
||||||
});
|
});
|
||||||
await unitOfWork.commit();
|
await unitOfWork.commit();
|
||||||
logger.info(`[ContractUploadController] uploadCompanyContract success for userId: ${userId}`);
|
logger.info(`[ContractUploadController] uploadCompanyContract success for userId: ${userId}`);
|
||||||
|
|||||||
@ -270,6 +270,7 @@ async function createDatabase() {
|
|||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
user_id INT NOT NULL,
|
user_id INT NOT NULL,
|
||||||
document_type ENUM('personal_id', 'company_id', 'signature', 'contract', 'other') NOT NULL,
|
document_type ENUM('personal_id', 'company_id', 'signature', 'contract', 'other') NOT NULL,
|
||||||
|
contract_type ENUM('contract','gdpr') NOT NULL DEFAULT 'contract',
|
||||||
object_storage_id VARCHAR(255) UNIQUE NOT NULL,
|
object_storage_id VARCHAR(255) UNIQUE NOT NULL,
|
||||||
original_filename VARCHAR(255),
|
original_filename VARCHAR(255),
|
||||||
file_size INT,
|
file_size INT,
|
||||||
@ -279,9 +280,18 @@ async function createDatabase() {
|
|||||||
admin_verified_at TIMESTAMP NULL,
|
admin_verified_at TIMESTAMP NULL,
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
INDEX idx_user_document_type (user_id, document_type),
|
INDEX idx_user_document_type (user_id, document_type),
|
||||||
|
INDEX idx_user_contract_type (user_id, contract_type),
|
||||||
INDEX idx_object_storage_id (object_storage_id)
|
INDEX idx_object_storage_id (object_storage_id)
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
// Backfill in case table already existed without contract_type
|
||||||
|
await connection.query(`
|
||||||
|
ALTER TABLE user_documents
|
||||||
|
ADD COLUMN IF NOT EXISTS contract_type ENUM('contract','gdpr') NOT NULL DEFAULT 'contract' AFTER document_type;
|
||||||
|
`);
|
||||||
|
await connection.query(`
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_contract_type ON user_documents (user_id, contract_type);
|
||||||
|
`);
|
||||||
console.log('✅ User documents table created/verified');
|
console.log('✅ User documents table created/verified');
|
||||||
|
|
||||||
// 8c. document_templates table: Stores template metadata and object storage keys
|
// 8c. document_templates table: Stores template metadata and object storage keys
|
||||||
@ -290,6 +300,7 @@ async function createDatabase() {
|
|||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
name VARCHAR(255) NOT NULL,
|
name VARCHAR(255) NOT NULL,
|
||||||
type VARCHAR(100) NOT NULL,
|
type VARCHAR(100) NOT NULL,
|
||||||
|
contract_type ENUM('contract','gdpr') NULL DEFAULT NULL,
|
||||||
storageKey VARCHAR(255) NOT NULL,
|
storageKey VARCHAR(255) NOT NULL,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
lang VARCHAR(10) NOT NULL,
|
lang VARCHAR(10) NOT NULL,
|
||||||
@ -297,7 +308,11 @@ async function createDatabase() {
|
|||||||
version INT DEFAULT 1,
|
version INT DEFAULT 1,
|
||||||
state ENUM('active','inactive') DEFAULT 'inactive',
|
state ENUM('active','inactive') DEFAULT 'inactive',
|
||||||
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
CHECK (
|
||||||
|
(type <> 'contract' AND contract_type IS NULL)
|
||||||
|
OR (type = 'contract' AND contract_type IN ('contract','gdpr'))
|
||||||
|
)
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
console.log('✅ Document templates table created/verified');
|
console.log('✅ Document templates table created/verified');
|
||||||
|
|||||||
@ -8,6 +8,7 @@ class UserDocumentRepository {
|
|||||||
async insertDocument({
|
async insertDocument({
|
||||||
userId,
|
userId,
|
||||||
documentType,
|
documentType,
|
||||||
|
contractType = 'contract',
|
||||||
objectStorageId,
|
objectStorageId,
|
||||||
originalFilename,
|
originalFilename,
|
||||||
fileSize,
|
fileSize,
|
||||||
@ -17,14 +18,15 @@ class UserDocumentRepository {
|
|||||||
const conn = this.unitOfWork.connection;
|
const conn = this.unitOfWork.connection;
|
||||||
const query = `
|
const query = `
|
||||||
INSERT INTO user_documents (
|
INSERT INTO user_documents (
|
||||||
user_id, document_type, object_storage_id,
|
user_id, document_type, contract_type, object_storage_id,
|
||||||
original_filename, file_size, mime_type
|
original_filename, file_size, mime_type
|
||||||
) VALUES (?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
`;
|
`;
|
||||||
try {
|
try {
|
||||||
const [result] = await conn.query(query, [
|
const [result] = await conn.query(query, [
|
||||||
userId,
|
userId,
|
||||||
documentType,
|
documentType,
|
||||||
|
contractType,
|
||||||
objectStorageId,
|
objectStorageId,
|
||||||
originalFilename,
|
originalFilename,
|
||||||
fileSize,
|
fileSize,
|
||||||
|
|||||||
@ -22,22 +22,27 @@ class DocumentTemplateRepository {
|
|||||||
const lang = String(data.lang);
|
const lang = String(data.lang);
|
||||||
const allowedUserTypes = new Set(['personal', 'company', 'both']);
|
const allowedUserTypes = new Set(['personal', 'company', 'both']);
|
||||||
const user_type = allowedUserTypes.has(data.user_type || data.userType) ? (data.user_type || data.userType) : 'both';
|
const user_type = allowedUserTypes.has(data.user_type || data.userType) ? (data.user_type || data.userType) : 'both';
|
||||||
|
const allowedContractTypes = new Set(['contract', 'gdpr']);
|
||||||
|
const contract_type = type === 'contract'
|
||||||
|
? (allowedContractTypes.has(data.contract_type || data.contractType) ? (data.contract_type || data.contractType) : null)
|
||||||
|
: null;
|
||||||
|
const finalContractType = type === 'contract' ? (contract_type || 'contract') : null;
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
INSERT INTO document_templates (name, type, storageKey, description, lang, user_type, version, state, createdAt, updatedAt)
|
INSERT INTO document_templates (name, type, contract_type, storageKey, description, lang, user_type, version, state, createdAt, updatedAt)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, 1, 'inactive', NOW(), NOW())
|
VALUES (?, ?, ?, ?, ?, ?, ?, 1, 'inactive', NOW(), NOW())
|
||||||
`;
|
`;
|
||||||
try {
|
try {
|
||||||
if (conn) {
|
if (conn) {
|
||||||
const [res] = await conn.execute(query, [name, type, storageKey, description, lang, user_type]);
|
const [res] = await conn.execute(query, [name, type, finalContractType, storageKey, description, lang, user_type]);
|
||||||
const insertId = res && (res.insertId || res[0]?.insertId);
|
const insertId = res && (res.insertId || res[0]?.insertId);
|
||||||
logger.info('DocumentTemplateRepository.create:success', { id: insertId || res.insertId });
|
logger.info('DocumentTemplateRepository.create:success', { id: insertId || res.insertId });
|
||||||
return { id: insertId || res.insertId, name, type, storageKey, description, lang, user_type, version: 1, state: 'inactive' };
|
return { id: insertId || res.insertId, name, type, contract_type: finalContractType, storageKey, description, lang, user_type, version: 1, state: 'inactive' };
|
||||||
}
|
}
|
||||||
const result = await db.execute(query, [name, type, storageKey, description, lang, user_type]);
|
const result = await db.execute(query, [name, type, finalContractType, storageKey, description, lang, user_type]);
|
||||||
const insertId = result && result.insertId ? result.insertId : (Array.isArray(result) && result[0] && result[0].insertId ? result[0].insertId : undefined);
|
const insertId = result && result.insertId ? result.insertId : (Array.isArray(result) && result[0] && result[0].insertId ? result[0].insertId : undefined);
|
||||||
logger.info('DocumentTemplateRepository.create:success', { id: insertId });
|
logger.info('DocumentTemplateRepository.create:success', { id: insertId });
|
||||||
return { id: insertId, name, type, storageKey, description, lang, user_type, version: 1, state: 'inactive' };
|
return { id: insertId, name, type, contract_type: finalContractType, storageKey, description, lang, user_type, version: 1, state: 'inactive' };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('DocumentTemplateRepository.create:error', { error: error.message });
|
logger.error('DocumentTemplateRepository.create:error', { error: error.message });
|
||||||
throw error;
|
throw error;
|
||||||
@ -90,7 +95,7 @@ class DocumentTemplateRepository {
|
|||||||
// data: { name, type, storageKey, description, lang, version }
|
// data: { name, type, storageKey, description, lang, version }
|
||||||
const fields = [];
|
const fields = [];
|
||||||
const values = [];
|
const values = [];
|
||||||
for (const key of ['name', 'type', 'storageKey', 'description', 'lang', 'version', 'user_type']) {
|
for (const key of ['name', 'type', 'contract_type', 'storageKey', 'description', 'lang', 'version', 'user_type']) {
|
||||||
if (data[key] !== undefined) {
|
if (data[key] !== undefined) {
|
||||||
fields.push(`${key} = ?`);
|
fields.push(`${key} = ?`);
|
||||||
values.push(data[key]);
|
values.push(data[key]);
|
||||||
@ -150,8 +155,8 @@ class DocumentTemplateRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async findActiveByUserType(userType, templateType = null, conn) {
|
async findActiveByUserType(userType, templateType = null, contractType = null, conn) {
|
||||||
logger.info('DocumentTemplateRepository.findActiveByUserType:start', { userType, templateType });
|
logger.info('DocumentTemplateRepository.findActiveByUserType:start', { userType, templateType, contractType });
|
||||||
const safeType = (userType === 'personal' || userType === 'company') ? userType : 'personal';
|
const safeType = (userType === 'personal' || userType === 'company') ? userType : 'personal';
|
||||||
let query = `SELECT * FROM document_templates WHERE state = 'active' AND (user_type = ? OR user_type = 'both')`;
|
let query = `SELECT * FROM document_templates WHERE state = 'active' AND (user_type = ? OR user_type = 'both')`;
|
||||||
const params = [safeType];
|
const params = [safeType];
|
||||||
@ -159,6 +164,10 @@ class DocumentTemplateRepository {
|
|||||||
query += ` AND type = ?`;
|
query += ` AND type = ?`;
|
||||||
params.push(templateType);
|
params.push(templateType);
|
||||||
}
|
}
|
||||||
|
if (contractType && templateType === 'contract') {
|
||||||
|
query += ` AND contract_type = ?`;
|
||||||
|
params.push(contractType);
|
||||||
|
}
|
||||||
query += ` ORDER BY createdAt DESC`;
|
query += ` ORDER BY createdAt DESC`;
|
||||||
try {
|
try {
|
||||||
if (conn) {
|
if (conn) {
|
||||||
|
|||||||
@ -3,10 +3,11 @@ const UnitOfWork = require('../database/UnitOfWork');
|
|||||||
const argon2 = require('argon2');
|
const argon2 = require('argon2');
|
||||||
|
|
||||||
async function createAdminUser() {
|
async function createAdminUser() {
|
||||||
const adminEmail = process.env.ADMIN_EMAIL || 'alexander.ibrahim.ai@gmail.com';
|
// const adminEmail = process.env.ADMIN_EMAIL || 'office@profit-planet.com';
|
||||||
// const adminEmail = process.env.ADMIN_EMAIL || 'loki.aahi@gmail.com';
|
// const adminEmail = process.env.ADMIN_EMAIL || 'alexander.ibrahim.ai@gmail.com';
|
||||||
// const adminPassword = process.env.ADMIN_PASSWORD || 'W.profit-planet.com.2025';
|
const adminEmail = process.env.ADMIN_EMAIL || 'loki.aahi@gmail.com';
|
||||||
const adminPassword = process.env.ADMIN_PASSWORD || 'Chalanger75$%';
|
const adminPassword = process.env.ADMIN_PASSWORD || 'Chalanger75$%';
|
||||||
|
// const adminPassword = process.env.ADMIN_PASSWORD || 'W.profit-planet.com.2025';
|
||||||
const firstName = process.env.ADMIN_FIRST_NAME || 'Admin';
|
const firstName = process.env.ADMIN_FIRST_NAME || 'Admin';
|
||||||
const lastName = process.env.ADMIN_LAST_NAME || 'User';
|
const lastName = process.env.ADMIN_LAST_NAME || 'User';
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,51 @@
|
|||||||
const UserDocumentRepository = require('../../repositories/documents/UserDocumentRepository');
|
const UserDocumentRepository = require('../../repositories/documents/UserDocumentRepository');
|
||||||
const { uploadBuffer } = require('../../utils/exoscaleUploader');
|
const { uploadBuffer, s3: exoS3 } = require('../../utils/exoscaleUploader');
|
||||||
const PDFDocument = require('pdfkit');
|
const PDFDocument = require('pdfkit');
|
||||||
const getStream = require('get-stream');
|
|
||||||
const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
|
const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
|
||||||
const DocumentTemplateService = require('../template/DocumentTemplateService');
|
const DocumentTemplateService = require('../template/DocumentTemplateService');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { logger } = require('../../middleware/logger');
|
const { logger } = require('../../middleware/logger');
|
||||||
|
const puppeteer = require('puppeteer');
|
||||||
|
|
||||||
|
// Robust stream/Body -> Buffer reader (supports async iterable, web streams, node streams, buffers)
|
||||||
|
async function streamToBuffer(body) {
|
||||||
|
if (!body) return Buffer.alloc(0);
|
||||||
|
if (typeof body.transformToByteArray === 'function') {
|
||||||
|
const arr = await body.transformToByteArray();
|
||||||
|
return Buffer.from(arr);
|
||||||
|
}
|
||||||
|
if (typeof body.getReader === 'function') {
|
||||||
|
const reader = body.getReader();
|
||||||
|
const chunks = [];
|
||||||
|
// eslint-disable-next-line no-constant-condition
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
chunks.push(Buffer.from(value));
|
||||||
|
}
|
||||||
|
return Buffer.concat(chunks);
|
||||||
|
}
|
||||||
|
if (body[Symbol.asyncIterator]) {
|
||||||
|
const chunks = [];
|
||||||
|
for await (const chunk of body) {
|
||||||
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
||||||
|
}
|
||||||
|
return Buffer.concat(chunks);
|
||||||
|
}
|
||||||
|
if (typeof body.on === 'function') {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const chunks = [];
|
||||||
|
body.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
||||||
|
body.on('end', () => resolve(Buffer.concat(chunks)));
|
||||||
|
body.on('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (Buffer.isBuffer(body)) return body;
|
||||||
|
if (ArrayBuffer.isView(body)) return Buffer.from(body.buffer, body.byteOffset, body.byteLength);
|
||||||
|
if (body instanceof ArrayBuffer) return Buffer.from(body);
|
||||||
|
throw new Error('Unsupported body type for streamToBuffer');
|
||||||
|
}
|
||||||
|
|
||||||
function fillTemplate(template, data) {
|
function fillTemplate(template, data) {
|
||||||
return template.replace(/{{(\w+)}}/g, (_, key) => data[key] || '');
|
return template.replace(/{{(\w+)}}/g, (_, key) => data[key] || '');
|
||||||
@ -61,7 +100,9 @@ class ContractUploadService {
|
|||||||
signatureImage,
|
signatureImage,
|
||||||
contractTemplate,
|
contractTemplate,
|
||||||
templateId,
|
templateId,
|
||||||
lang
|
lang,
|
||||||
|
contract_type = 'contract',
|
||||||
|
user_type = 'personal'
|
||||||
}) {
|
}) {
|
||||||
logger.info('ContractUploadService.uploadContract:start', { userId, documentType, contractCategory, templateId, lang });
|
logger.info('ContractUploadService.uploadContract:start', { userId, documentType, contractCategory, templateId, lang });
|
||||||
let pdfBuffer, originalFilename, mimeType, fileSize;
|
let pdfBuffer, originalFilename, mimeType, fileSize;
|
||||||
@ -79,7 +120,7 @@ class ContractUploadService {
|
|||||||
Bucket: process.env.AWS_BUCKET,
|
Bucket: process.env.AWS_BUCKET,
|
||||||
Key: templateMeta.storageKey
|
Key: templateMeta.storageKey
|
||||||
}));
|
}));
|
||||||
const htmlBuffer = await getStream.buffer(getObj.Body);
|
const htmlBuffer = await streamToBuffer(getObj.Body);
|
||||||
let htmlTemplate = htmlBuffer.toString('utf-8');
|
let htmlTemplate = htmlBuffer.toString('utf-8');
|
||||||
// Fill variables in HTML template
|
// Fill variables in HTML template
|
||||||
contractBody = fillTemplate(htmlTemplate, contractData);
|
contractBody = fillTemplate(htmlTemplate, contractData);
|
||||||
@ -103,10 +144,6 @@ class ContractUploadService {
|
|||||||
// Header
|
// Header
|
||||||
doc.font('Helvetica-Bold').fontSize(20).text('Contract Agreement', { align: 'center' });
|
doc.font('Helvetica-Bold').fontSize(20).text('Contract Agreement', { align: 'center' });
|
||||||
doc.moveDown(1.5);
|
doc.moveDown(1.5);
|
||||||
|
|
||||||
// User Info Section
|
|
||||||
doc.font('Helvetica').fontSize(12);
|
|
||||||
doc.text(`Name: ${contractData.name}`);
|
|
||||||
doc.text(`Email: ${contractData.email}`);
|
doc.text(`Email: ${contractData.email}`);
|
||||||
doc.text(`Date: ${contractData.date}`);
|
doc.text(`Date: ${contractData.date}`);
|
||||||
doc.moveDown();
|
doc.moveDown();
|
||||||
@ -133,7 +170,7 @@ class ContractUploadService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
doc.end();
|
doc.end();
|
||||||
pdfBuffer = await getStream.buffer(doc);
|
pdfBuffer = await streamToBuffer(doc);
|
||||||
originalFilename = `signed_contract_${userId}_${Date.now()}.pdf`;
|
originalFilename = `signed_contract_${userId}_${Date.now()}.pdf`;
|
||||||
mimeType = 'application/pdf';
|
mimeType = 'application/pdf';
|
||||||
fileSize = pdfBuffer.length;
|
fileSize = pdfBuffer.length;
|
||||||
@ -146,8 +183,29 @@ class ContractUploadService {
|
|||||||
mimeType = file.mimetype;
|
mimeType = file.mimetype;
|
||||||
fileSize = file.size;
|
fileSize = file.size;
|
||||||
} else {
|
} else {
|
||||||
logger.warn('ContractUploadService.uploadContract:no_contract_file_or_data', { userId });
|
// NEW: auto-generate PDF from latest active template (contract_type) when no file provided
|
||||||
throw new Error('No contract file or data uploaded');
|
const tmpl = await DocumentTemplateService.getLatestActiveForUserType(user_type, 'contract', contract_type);
|
||||||
|
if (!tmpl) {
|
||||||
|
throw new Error(`No active ${contract_type} template found for user type ${user_type}`);
|
||||||
|
}
|
||||||
|
logger.info('ContractUploadService.uploadContract:generate_from_template', { userId, tmplId: tmpl.id, contract_type });
|
||||||
|
// Fetch HTML from Exoscale
|
||||||
|
const client = exoS3 || new S3Client({ region: process.env.EXOSCALE_REGION, endpoint: process.env.EXOSCALE_ENDPOINT, forcePathStyle: true, credentials: { accessKeyId: process.env.EXOSCALE_ACCESS_KEY, secretAccessKey: process.env.EXOSCALE_SECRET_KEY } });
|
||||||
|
const obj = await client.send(new GetObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: tmpl.storageKey }));
|
||||||
|
const htmlBuffer = await streamToBuffer(obj.Body);
|
||||||
|
const htmlTemplate = htmlBuffer.toString('utf-8');
|
||||||
|
|
||||||
|
// Render HTML to PDF via Puppeteer (closest to preview output)
|
||||||
|
const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox', '--disable-setuid-sandbox'] });
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.setContent(htmlTemplate, { waitUntil: 'networkidle0' });
|
||||||
|
pdfBuffer = await page.pdf({ format: 'A4', printBackground: true });
|
||||||
|
await browser.close();
|
||||||
|
|
||||||
|
originalFilename = `${contract_type}_contract_${userId}_${Date.now()}.pdf`;
|
||||||
|
mimeType = 'application/pdf';
|
||||||
|
fileSize = pdfBuffer.length;
|
||||||
|
logger.info('ContractUploadService.uploadContract:pdf_generated_from_template', { userId, contract_type, filename: originalFilename, fileSize });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload to Exoscale
|
// Upload to Exoscale
|
||||||
@ -156,7 +214,7 @@ class ContractUploadService {
|
|||||||
pdfBuffer,
|
pdfBuffer,
|
||||||
originalFilename,
|
originalFilename,
|
||||||
mimeType,
|
mimeType,
|
||||||
`contracts/${contractCategory}/${userId}`
|
`contracts/${contractCategory}/${userId}/${contract_type}`
|
||||||
);
|
);
|
||||||
logger.info('ContractUploadService.uploadContract:uploaded', { userId, objectKey: uploadResult.objectKey });
|
logger.info('ContractUploadService.uploadContract:uploaded', { userId, objectKey: uploadResult.objectKey });
|
||||||
|
|
||||||
@ -165,6 +223,7 @@ class ContractUploadService {
|
|||||||
await repo.insertDocument({
|
await repo.insertDocument({
|
||||||
userId,
|
userId,
|
||||||
documentType,
|
documentType,
|
||||||
|
contractType: contract_type || 'contract',
|
||||||
objectStorageId: uploadResult.objectKey,
|
objectStorageId: uploadResult.objectKey,
|
||||||
idType: null,
|
idType: null,
|
||||||
idNumber: null,
|
idNumber: null,
|
||||||
|
|||||||
@ -25,7 +25,11 @@ class DocumentTemplateService {
|
|||||||
await uow.start();
|
await uow.start();
|
||||||
const allowed = ['personal','company','both'];
|
const allowed = ['personal','company','both'];
|
||||||
const user_type = allowed.includes(data.user_type || data.userType) ? (data.user_type || data.userType) : 'both';
|
const user_type = allowed.includes(data.user_type || data.userType) ? (data.user_type || data.userType) : 'both';
|
||||||
const created = await DocumentTemplateRepository.create({ ...data, user_type }, uow.connection);
|
const allowedContractTypes = ['contract', 'gdpr'];
|
||||||
|
const contract_type = (data.type === 'contract' && allowedContractTypes.includes(data.contract_type || data.contractType))
|
||||||
|
? (data.contract_type || data.contractType)
|
||||||
|
: (data.type === 'contract' ? 'contract' : null);
|
||||||
|
const created = await DocumentTemplateRepository.create({ ...data, user_type, contract_type }, uow.connection);
|
||||||
await uow.commit();
|
await uow.commit();
|
||||||
logger.info('DocumentTemplateService.uploadTemplate:success', { id: created.id });
|
logger.info('DocumentTemplateService.uploadTemplate:success', { id: created.id });
|
||||||
return created;
|
return created;
|
||||||
@ -83,10 +87,17 @@ class DocumentTemplateService {
|
|||||||
const allowed = ['personal','company','both'];
|
const allowed = ['personal','company','both'];
|
||||||
if (data.userType && !allowed.includes(data.userType)) delete data.userType;
|
if (data.userType && !allowed.includes(data.userType)) delete data.userType;
|
||||||
if (data.user_type && !allowed.includes(data.user_type)) delete data.user_type;
|
if (data.user_type && !allowed.includes(data.user_type)) delete data.user_type;
|
||||||
|
const nextType = data.type !== undefined ? data.type : current.type;
|
||||||
|
const allowedContractTypes = ['contract', 'gdpr'];
|
||||||
|
const contract_type = nextType === 'contract'
|
||||||
|
? (allowedContractTypes.includes(data.contract_type || data.contractType || current.contract_type)
|
||||||
|
? (data.contract_type || data.contractType || current.contract_type)
|
||||||
|
: 'contract')
|
||||||
|
: null;
|
||||||
const newVersion = (current.version || 1) + 1;
|
const newVersion = (current.version || 1) + 1;
|
||||||
await DocumentTemplateRepository.update(
|
await DocumentTemplateRepository.update(
|
||||||
id,
|
id,
|
||||||
{ ...data, version: newVersion, user_type: data.user_type || data.userType || current.user_type },
|
{ ...data, version: newVersion, user_type: data.user_type || data.userType || current.user_type, contract_type },
|
||||||
uow.connection
|
uow.connection
|
||||||
);
|
);
|
||||||
const updated = await DocumentTemplateRepository.findById(id, uow.connection);
|
const updated = await DocumentTemplateRepository.findById(id, uow.connection);
|
||||||
@ -117,10 +128,10 @@ class DocumentTemplateService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getActiveTemplatesForUserType(userType, templateType = null) {
|
async getActiveTemplatesForUserType(userType, templateType = null, contractType = null) {
|
||||||
logger.info('DocumentTemplateService.getActiveTemplatesForUserType:start', { userType, templateType });
|
logger.info('DocumentTemplateService.getActiveTemplatesForUserType:start', { userType, templateType, contractType });
|
||||||
try {
|
try {
|
||||||
const rows = await DocumentTemplateRepository.findActiveByUserType(userType, templateType);
|
const rows = await DocumentTemplateRepository.findActiveByUserType(userType, templateType, contractType);
|
||||||
logger.info('DocumentTemplateService.getActiveTemplatesForUserType:success', { count: rows.length });
|
logger.info('DocumentTemplateService.getActiveTemplatesForUserType:success', { count: rows.length });
|
||||||
return rows.map(t => ({ ...t, lang: t.lang }));
|
return rows.map(t => ({ ...t, lang: t.lang }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -130,10 +141,10 @@ class DocumentTemplateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convenience: return the most recent active template for a user type (by createdAt desc)
|
// Convenience: return the most recent active template for a user type (by createdAt desc)
|
||||||
async getLatestActiveForUserType(userType, templateType = 'contract') {
|
async getLatestActiveForUserType(userType, templateType = 'contract', contractType = null) {
|
||||||
logger.info('DocumentTemplateService.getLatestActiveForUserType:start', { userType, templateType });
|
logger.info('DocumentTemplateService.getLatestActiveForUserType:start', { userType, templateType, contractType });
|
||||||
try {
|
try {
|
||||||
const list = await DocumentTemplateRepository.findActiveByUserType(userType, templateType);
|
const list = await DocumentTemplateRepository.findActiveByUserType(userType, templateType, contractType);
|
||||||
const latest = Array.isArray(list) && list.length ? list[0] : null;
|
const latest = Array.isArray(list) && list.length ? list[0] : null;
|
||||||
logger.info('DocumentTemplateService.getLatestActiveForUserType:result', { found: !!latest, id: latest?.id });
|
logger.info('DocumentTemplateService.getLatestActiveForUserType:result', { found: !!latest, id: latest?.id });
|
||||||
return latest;
|
return latest;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user