fix: contract gen

This commit is contained in:
DeathKaioken 2026-01-18 23:08:36 +01:00
parent 7b06ea93c5
commit 6e28cc3146
4 changed files with 32 additions and 8 deletions

View File

@ -35,6 +35,9 @@ class ContractUploadController {
try { try {
const signatureMeta = normalizeSignature(signatureImage); const signatureMeta = normalizeSignature(signatureImage);
const repo = new UserDocumentRepository(unitOfWork); const repo = new UserDocumentRepository(unitOfWork);
// NOTE: this DB insert is only a TEMP storage of the raw signature image.
// The contract PDF embeds the signature from `signatureMeta.base64` during PDF generation.
if (signatureMeta) { if (signatureMeta) {
await repo.insertDocument({ await repo.insertDocument({
userId, userId,
@ -85,6 +88,8 @@ class ContractUploadController {
} }
// Cleanup standalone signature record after contracts are saved // Cleanup standalone signature record after contracts are saved
// NOTE: deleting the signature row here DOES NOT remove it from the generated contract PDF.
// The PDF already contains the signature pixels and is uploaded to object storage.
if (signatureMeta) { if (signatureMeta) {
await repo.deleteSignatureDocumentsForUser(userId); await repo.deleteSignatureDocumentsForUser(userId);
logger.info('[ContractUploadController] signature cleanup completed for user', { userId }); logger.info('[ContractUploadController] signature cleanup completed for user', { userId });
@ -111,6 +116,8 @@ class ContractUploadController {
try { try {
const signatureMeta = normalizeSignature(signatureImage); const signatureMeta = normalizeSignature(signatureImage);
const repo = new UserDocumentRepository(unitOfWork); const repo = new UserDocumentRepository(unitOfWork);
// NOTE: TEMP storage only; the PDF embeds the signature from `signatureMeta.base64`.
if (signatureMeta) { if (signatureMeta) {
await repo.insertDocument({ await repo.insertDocument({
userId, userId,
@ -160,11 +167,15 @@ class ContractUploadController {
throw new Error('No active documents are available at this moment.'); throw new Error('No active documents are available at this moment.');
} }
await unitOfWork.commit(); // IMPORTANT: cleanup must happen BEFORE commit() because commit closes/nulls the connection.
// NOTE: cleanup only removes the temporary signature row, not the signature embedded in PDFs.
if (signatureMeta) { if (signatureMeta) {
await repo.deleteSignatureDocumentsForUser(userId); await repo.deleteSignatureDocumentsForUser(userId);
logger.info('[ContractUploadController] signature cleanup completed for company user', { userId }); logger.info('[ContractUploadController] signature cleanup completed for company user', { userId });
} }
await unitOfWork.commit();
logger.info(`[ContractUploadController] uploadCompanyContract success for userId: ${userId}`); logger.info(`[ContractUploadController] uploadCompanyContract success for userId: ${userId}`);
res.json({ success: true, uploads, skipped, downloadUrls: uploads.map(u => u.url || null) }); res.json({ success: true, uploads, skipped, downloadUrls: uploads.map(u => u.url || null) });
} catch (error) { } catch (error) {

View File

@ -5,6 +5,16 @@ class UserDocumentRepository {
this.unitOfWork = unitOfWork; this.unitOfWork = unitOfWork;
} }
_getConn() {
const conn = this.unitOfWork && this.unitOfWork.connection;
if (!conn) {
const err = new Error('UnitOfWork connection is not available (was commit()/rollback() already called?)');
err.code = 'UOW_CONNECTION_MISSING';
throw err;
}
return conn;
}
async insertDocument({ async insertDocument({
userId, userId,
documentType, documentType,
@ -16,7 +26,7 @@ class UserDocumentRepository {
mimeType mimeType
}) { }) {
logger.info('UserDocumentRepository.insertDocument:start', { userId, documentType, originalFilename }); logger.info('UserDocumentRepository.insertDocument:start', { userId, documentType, originalFilename });
const conn = this.unitOfWork.connection; const conn = this._getConn();
const query = ` const query = `
INSERT INTO user_documents ( INSERT INTO user_documents (
user_id, document_type, contract_type, object_storage_id, signatureBase64, user_id, document_type, contract_type, object_storage_id, signatureBase64,
@ -44,7 +54,7 @@ class UserDocumentRepository {
async insertIdMetadata({ userDocumentId, idType, idNumber, expiryDate }) { async insertIdMetadata({ userDocumentId, idType, idNumber, expiryDate }) {
logger.info('UserDocumentRepository.insertIdMetadata:start', { userDocumentId, idType, idNumber }); logger.info('UserDocumentRepository.insertIdMetadata:start', { userDocumentId, idType, idNumber });
const conn = this.unitOfWork.connection; const conn = this._getConn();
try { try {
await conn.query( await conn.query(
`INSERT INTO user_id_documents (user_document_id, id_type, id_number, expiry_date) `INSERT INTO user_id_documents (user_document_id, id_type, id_number, expiry_date)
@ -70,7 +80,7 @@ class UserDocumentRepository {
originalFilenameBack originalFilenameBack
}) { }) {
logger.info('UserDocumentRepository.insertIdDocument:start', { userId, documentType, idType, idNumber }); logger.info('UserDocumentRepository.insertIdDocument:start', { userId, documentType, idType, idNumber });
const conn = this.unitOfWork.connection; const conn = this._getConn();
try { try {
await conn.query( await conn.query(
`INSERT INTO user_id_documents ( `INSERT INTO user_id_documents (
@ -98,7 +108,7 @@ class UserDocumentRepository {
async getDocumentsForUser(userId) { async getDocumentsForUser(userId) {
logger.info('UserDocumentRepository.getDocumentsForUser:start', { userId }); logger.info('UserDocumentRepository.getDocumentsForUser:start', { userId });
const conn = this.unitOfWork.connection; const conn = this._getConn();
try { try {
const [rows] = await conn.query( const [rows] = await conn.query(
`SELECT * FROM user_documents WHERE user_id = ?`, `SELECT * FROM user_documents WHERE user_id = ?`,
@ -114,7 +124,7 @@ class UserDocumentRepository {
async getIdDocumentsForUser(userId) { async getIdDocumentsForUser(userId) {
logger.info('UserDocumentRepository.getIdDocumentsForUser:start', { userId }); logger.info('UserDocumentRepository.getIdDocumentsForUser:start', { userId });
const conn = this.unitOfWork.connection; const conn = this._getConn();
try { try {
const [rows] = await conn.query( const [rows] = await conn.query(
`SELECT * FROM user_id_documents WHERE user_id = ?`, `SELECT * FROM user_id_documents WHERE user_id = ?`,
@ -130,7 +140,7 @@ class UserDocumentRepository {
async getAllObjectStorageIdsForUser(userId) { async getAllObjectStorageIdsForUser(userId) {
logger.info('UserDocumentRepository.getAllObjectStorageIdsForUser:start', { userId }); logger.info('UserDocumentRepository.getAllObjectStorageIdsForUser:start', { userId });
const conn = this.unitOfWork.connection; const conn = this._getConn();
try { try {
// Get object_storage_id from user_documents // Get object_storage_id from user_documents
const [docRows] = await conn.query( const [docRows] = await conn.query(
@ -152,7 +162,7 @@ class UserDocumentRepository {
async deleteSignatureDocumentsForUser(userId) { async deleteSignatureDocumentsForUser(userId) {
logger.info('UserDocumentRepository.deleteSignatureDocumentsForUser:start', { userId }); logger.info('UserDocumentRepository.deleteSignatureDocumentsForUser:start', { userId });
const conn = this.unitOfWork.connection; const conn = this._getConn();
try { try {
const [result] = await conn.query( const [result] = await conn.query(
`DELETE FROM user_documents WHERE user_id = ? AND document_type = 'signature'`, `DELETE FROM user_documents WHERE user_id = ? AND document_type = 'signature'`,

View File

@ -157,5 +157,7 @@ router.get('/news/active', NewsController.listActive);
router.get('/invoices/mine', authMiddleware, InvoiceController.listMine); router.get('/invoices/mine', authMiddleware, InvoiceController.listMine);
router.get('/admin/invoices', authMiddleware, adminOnly, InvoiceController.adminList); router.get('/admin/invoices', authMiddleware, adminOnly, InvoiceController.adminList);
// NOTE: Contract signing uses UnitOfWork; any DB cleanup must happen before commit() closes the connection.
// export // export
module.exports = router; module.exports = router;

View File

@ -68,6 +68,7 @@ router.post('/document-templates/:id/revise', authMiddleware, upload.single('fil
router.post('/document-templates/:id/generate-pdf-with-signature', authMiddleware, DocumentTemplateController.generatePdfWithSignature); router.post('/document-templates/:id/generate-pdf-with-signature', authMiddleware, DocumentTemplateController.generatePdfWithSignature);
// Document uploads (moved from routes/documents.js) // Document uploads (moved from routes/documents.js)
// NOTE: contract upload flow stores a temporary signature row and must delete it BEFORE UnitOfWork.commit().
router.post('/upload/personal-id', authMiddleware, upload.fields([{ name: 'front', maxCount: 1 }, { name: 'back', maxCount: 1 }]), PersonalDocumentController.uploadPersonalId); router.post('/upload/personal-id', authMiddleware, upload.fields([{ name: 'front', maxCount: 1 }, { name: 'back', maxCount: 1 }]), PersonalDocumentController.uploadPersonalId);
router.post('/upload/company-id', authMiddleware, upload.fields([{ name: 'front', maxCount: 1 }, { name: 'back', maxCount: 1 }]), CompanyDocumentController.uploadCompanyId); router.post('/upload/company-id', authMiddleware, upload.fields([{ name: 'front', maxCount: 1 }, { name: 'back', maxCount: 1 }]), CompanyDocumentController.uploadCompanyId);
router.post('/upload/contract/personal', authMiddleware, upload.single('contract'), ContractUploadController.uploadPersonalContract); router.post('/upload/contract/personal', authMiddleware, upload.single('contract'), ContractUploadController.uploadPersonalContract);