259 lines
11 KiB
JavaScript
259 lines
11 KiB
JavaScript
const UserDocumentRepository = require('../../repositories/documents/UserDocumentRepository');
|
|
const { uploadBuffer, s3: exoS3 } = require('../../utils/exoscaleUploader');
|
|
const PDFDocument = require('pdfkit');
|
|
const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
|
|
const DocumentTemplateService = require('../template/DocumentTemplateService');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
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) {
|
|
return template.replace(/{{(\w+)}}/g, (_, key) => data[key] || '');
|
|
}
|
|
|
|
const DEBUG_PDF_FILES = !!process.env.DEBUG_PDF_FILES;
|
|
function ensureDebugDir() {
|
|
const debugDir = path.join(__dirname, '../debug-pdf');
|
|
if (!fs.existsSync(debugDir)) fs.mkdirSync(debugDir, { recursive: true });
|
|
return debugDir;
|
|
}
|
|
function saveDebugFile(filename, data) {
|
|
if (!DEBUG_PDF_FILES) return;
|
|
const p = path.join(ensureDebugDir(), filename);
|
|
try {
|
|
fs.writeFileSync(p, data);
|
|
console.log(`[ContractUploadService][DEBUG] wrote ${p}`);
|
|
} catch (e) {
|
|
console.error('[ContractUploadService][DEBUG] failed to write', p, e);
|
|
}
|
|
}
|
|
|
|
// Minimal stream reader supporting AWS SDK Body
|
|
async function streamToString(stream, id) {
|
|
const chunks = [];
|
|
let total = 0;
|
|
if (stream[Symbol.asyncIterator]) {
|
|
for await (const chunk of stream) {
|
|
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
chunks.push(buf); total += buf.length;
|
|
}
|
|
} else {
|
|
for await (const chunk of stream) {
|
|
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
chunks.push(buf); total += buf.length;
|
|
}
|
|
}
|
|
const buffer = Buffer.concat(chunks);
|
|
if (DEBUG_PDF_FILES || total < 32) saveDebugFile(`contract_template_${id}_html_raw.bin`, buffer);
|
|
console.log(`[ContractUploadService] read template ${id} bytes=${total}`);
|
|
return buffer.toString('utf-8');
|
|
}
|
|
|
|
class ContractUploadService {
|
|
static async uploadContract({
|
|
userId,
|
|
file,
|
|
documentType,
|
|
contractCategory,
|
|
unitOfWork,
|
|
contractData,
|
|
signatureImage,
|
|
contractTemplate,
|
|
templateId,
|
|
lang,
|
|
contract_type = 'contract',
|
|
user_type = 'personal'
|
|
}) {
|
|
logger.info('ContractUploadService.uploadContract:start', { userId, documentType, contractCategory, templateId, lang });
|
|
let pdfBuffer, originalFilename, mimeType, fileSize;
|
|
let contractBody;
|
|
|
|
try {
|
|
// If templateId and lang are provided, fetch HTML template from object storage
|
|
if (templateId && lang) {
|
|
logger.info('ContractUploadService.uploadContract:fetching_template', { templateId, lang });
|
|
const templateMeta = await DocumentTemplateService.getTemplate(templateId);
|
|
if (!templateMeta || templateMeta.lang !== lang) throw new Error('Template not found for specified language');
|
|
// Fetch HTML from object storage
|
|
const s3 = new S3Client({ region: process.env.EXOSCALE_REGION });
|
|
const getObj = await s3.send(new GetObjectCommand({
|
|
Bucket: process.env.AWS_BUCKET,
|
|
Key: templateMeta.storageKey
|
|
}));
|
|
const htmlBuffer = await streamToBuffer(getObj.Body);
|
|
let htmlTemplate = htmlBuffer.toString('utf-8');
|
|
// Fill variables in HTML template
|
|
contractBody = fillTemplate(htmlTemplate, contractData);
|
|
logger.info('ContractUploadService.uploadContract:template_fetched', { templateId });
|
|
} else if (contractTemplate) {
|
|
logger.info('ContractUploadService.uploadContract:using_contractTemplate');
|
|
contractBody = fillTemplate(contractTemplate, contractData);
|
|
} else if (contractData) {
|
|
logger.info('ContractUploadService.uploadContract:using_contractData');
|
|
contractBody = `Contract for ${contractData.name}\nDate: ${contractData.date}\nEmail: ${contractData.email}\n\nTerms and Conditions...\n`;
|
|
}
|
|
|
|
if (contractData && signatureImage) {
|
|
logger.info('ContractUploadService.uploadContract:generating_pdf', { userId });
|
|
// Generate styled PDF
|
|
const doc = new PDFDocument({
|
|
size: 'A4',
|
|
margins: { top: 60, bottom: 60, left: 72, right: 72 }
|
|
});
|
|
|
|
// Header
|
|
doc.font('Helvetica-Bold').fontSize(20).text('Contract Agreement', { align: 'center' });
|
|
doc.moveDown(1.5);
|
|
doc.text(`Email: ${contractData.email}`);
|
|
doc.text(`Date: ${contractData.date}`);
|
|
doc.moveDown();
|
|
|
|
// Contract Body Section
|
|
doc.font('Times-Roman').fontSize(13);
|
|
doc.text(contractBody, {
|
|
align: 'justify',
|
|
lineGap: 4
|
|
});
|
|
doc.moveDown(2);
|
|
|
|
// Signature Section
|
|
doc.font('Helvetica-Bold').fontSize(12).text('Signature:', { continued: true });
|
|
doc.moveDown(0.5);
|
|
const imgBuffer = Buffer.isBuffer(signatureImage) ? signatureImage : Buffer.from(signatureImage, 'base64');
|
|
doc.image(imgBuffer, doc.x, doc.y, { fit: [180, 60], align: 'left', valign: 'center' });
|
|
doc.moveDown(2);
|
|
|
|
// Footer
|
|
doc.font('Helvetica-Oblique').fontSize(10).fillColor('gray')
|
|
.text('This contract is generated electronically and is valid without a physical signature.', 72, 780, {
|
|
align: 'center'
|
|
});
|
|
|
|
doc.end();
|
|
pdfBuffer = await streamToBuffer(doc);
|
|
originalFilename = `signed_contract_${userId}_${Date.now()}.pdf`;
|
|
mimeType = 'application/pdf';
|
|
fileSize = pdfBuffer.length;
|
|
logger.info('ContractUploadService.uploadContract:pdf_generated', { userId, filename: originalFilename, fileSize });
|
|
} else if (file) {
|
|
logger.info('ContractUploadService.uploadContract:using_uploaded_pdf', { userId, filename: file.originalname });
|
|
if (file.mimetype !== 'application/pdf') throw new Error('Only PDF files are allowed');
|
|
pdfBuffer = file.buffer;
|
|
originalFilename = file.originalname;
|
|
mimeType = file.mimetype;
|
|
fileSize = file.size;
|
|
} else {
|
|
// NEW: auto-generate PDF from latest active template (contract_type) when no file provided
|
|
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
|
|
logger.info('ContractUploadService.uploadContract:uploading_to_exoscale', { userId, filename: originalFilename });
|
|
const uploadResult = await uploadBuffer(
|
|
pdfBuffer,
|
|
originalFilename,
|
|
mimeType,
|
|
`contracts/${contractCategory}/${userId}/${contract_type}`
|
|
);
|
|
logger.info('ContractUploadService.uploadContract:uploaded', { userId, objectKey: uploadResult.objectKey });
|
|
|
|
// Insert metadata in user_documents
|
|
const repo = new UserDocumentRepository(unitOfWork);
|
|
await repo.insertDocument({
|
|
userId,
|
|
documentType,
|
|
contractType: contract_type || 'contract',
|
|
objectStorageId: uploadResult.objectKey,
|
|
idType: null,
|
|
idNumber: null,
|
|
expiryDate: null,
|
|
originalFilename,
|
|
fileSize,
|
|
mimeType
|
|
});
|
|
logger.info('ContractUploadService.uploadContract:document_metadata_inserted', { userId });
|
|
|
|
// Optionally update user_status (contract_signed)
|
|
await unitOfWork.connection.query(
|
|
`UPDATE user_status SET contract_signed = 1, contract_signed_at = NOW() WHERE user_id = ?`,
|
|
[userId]
|
|
);
|
|
logger.info('ContractUploadService.uploadContract:user_status_updated', { userId });
|
|
|
|
// Check if all steps are complete and set status to 'pending' if so
|
|
const UserStatusService = require('../status/UserStatusService');
|
|
await UserStatusService.checkAndSetPendingIfComplete(userId, unitOfWork);
|
|
logger.info('ContractUploadService.uploadContract:pending_check_complete', { userId });
|
|
|
|
logger.info('ContractUploadService.uploadContract:success', { userId });
|
|
return uploadResult;
|
|
} catch (error) {
|
|
logger.error('ContractUploadService.uploadContract:error', { userId, error: error.message });
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = ContractUploadService;
|