const UserDocumentRepository = require('../repositories/UserDocumentRepository'); const { uploadBuffer } = require('../utils/exoscaleUploader'); const PDFDocument = require('pdfkit'); const getStream = require('get-stream'); const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3'); const DocumentTemplateService = require('./DocumentTemplateService'); const fs = require('fs'); const path = require('path'); const { logger } = require('../middleware/logger'); 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 }) { 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 getStream.buffer(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); // User Info Section doc.font('Helvetica').fontSize(12); doc.text(`Name: ${contractData.name}`); 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 getStream.buffer(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 { logger.warn('ContractUploadService.uploadContract:no_contract_file_or_data', { userId }); throw new Error('No contract file or data uploaded'); } // Upload to Exoscale logger.info('ContractUploadService.uploadContract:uploading_to_exoscale', { userId, filename: originalFilename }); const uploadResult = await uploadBuffer( pdfBuffer, originalFilename, mimeType, `contracts/${contractCategory}/${userId}` ); logger.info('ContractUploadService.uploadContract:uploaded', { userId, objectKey: uploadResult.objectKey }); // Insert metadata in user_documents const repo = new UserDocumentRepository(unitOfWork); await repo.insertDocument({ userId, documentType, 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('./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;