CentralBackend/services/ContractUploadService.js
2025-09-07 12:44:01 +02:00

200 lines
7.9 KiB
JavaScript

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;