const DocumentTemplateRepository = require('../../repositories/template/DocumentTemplateRepository'); const UnitOfWork = require('../../database/UnitOfWork'); const { logger } = require('../../middleware/logger'); class DocumentTemplateService { async listTemplates() { logger.info('DocumentTemplateService.listTemplates:start'); try { const templates = await DocumentTemplateRepository.findAll(); logger.info('DocumentTemplateService.listTemplates:success', { count: templates.length }); return templates.map(t => ({ ...t, lang: t.lang })); } catch (error) { logger.error('DocumentTemplateService.listTemplates:error', { error: error.message }); throw error; } } async uploadTemplate(data) { logger.info('DocumentTemplateService.uploadTemplate:start', { name: data.name, type: data.type }); const uow = new UnitOfWork(); try { await uow.start(); const allowed = ['personal','company','both']; const user_type = allowed.includes(data.user_type || data.userType) ? (data.user_type || data.userType) : 'both'; 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(); logger.info('DocumentTemplateService.uploadTemplate:success', { id: created.id }); return created; } catch (err) { logger.error('DocumentTemplateService.uploadTemplate:error', { error: err.message }); await uow.rollback(err); throw err; } } async getTemplate(id) { logger.info('DocumentTemplateService.getTemplate:start', { id }); try { const template = await DocumentTemplateRepository.findById(id); if (!template) { logger.warn('DocumentTemplateService.getTemplate:not_found', { id }); return null; } logger.debug('DocumentTemplateService.getTemplate:meta', { id, storageKey: template.storageKey, lang: template.lang, version: template.version }); logger.info('DocumentTemplateService.getTemplate:success', { id }); return { ...template, lang: template.lang }; } catch (error) { logger.error('DocumentTemplateService.getTemplate:error', { id, error: error.message }); throw error; } } async deleteTemplate(id) { logger.info('DocumentTemplateService.deleteTemplate:start', { id }); const uow = new UnitOfWork(); try { await uow.start(); await DocumentTemplateRepository.delete(id, uow.connection); await uow.commit(); logger.info('DocumentTemplateService.deleteTemplate:success', { id }); return true; } catch (err) { logger.error('DocumentTemplateService.deleteTemplate:error', { id, error: err.message }); await uow.rollback(err); throw err; } } async updateTemplate(id, data) { logger.info('DocumentTemplateService.updateTemplate:start', { id }); const uow = new UnitOfWork(); try { await uow.start(); const current = await DocumentTemplateRepository.findById(id, uow.connection); if (!current) { logger.warn('DocumentTemplateService.updateTemplate:not_found', { id }); await uow.rollback(); return null; } const allowed = ['personal','company','both']; if (data.userType && !allowed.includes(data.userType)) delete data.userType; 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; await DocumentTemplateRepository.update( id, { ...data, version: newVersion, user_type: data.user_type || data.userType || current.user_type, contract_type }, uow.connection ); const updated = await DocumentTemplateRepository.findById(id, uow.connection); await uow.commit(); logger.info('DocumentTemplateService.updateTemplate:success', { id, version: newVersion }); return updated; } catch (err) { logger.error('DocumentTemplateService.updateTemplate:error', { id, error: err.message }); await uow.rollback(err); throw err; } } async updateTemplateState(id, state) { logger.info('DocumentTemplateService.updateTemplateState:start', { id, state }); const uow = new UnitOfWork(); try { await uow.start(); const current = await DocumentTemplateRepository.findById(id, uow.connection); if (!current) { logger.warn('DocumentTemplateService.updateTemplateState:not_found', { id }); await uow.rollback(); return null; } await DocumentTemplateRepository.updateState(id, state, uow.connection); // Enforce singleton active template per (contract_type + lang + overlapping user scope) if (state === 'active' && current.type === 'contract') { await DocumentTemplateRepository.deactivateOtherActiveContracts({ excludeId: id, contract_type: current.contract_type || 'contract', lang: current.lang, user_type: current.user_type || 'both' }, uow.connection); } const updated = await DocumentTemplateRepository.findById(id, uow.connection); await uow.commit(); logger.info('DocumentTemplateService.updateTemplateState:success', { id, state }); return updated; } catch (err) { logger.error('DocumentTemplateService.updateTemplateState:error', { id, state, error: err.message }); await uow.rollback(err); throw err; } } // NEW: Create a new template as a revision of an existing one. // - New record is created with version = previous.version + 1 // - Previous record is deactivated (state -> inactive) // - New record state defaults to previous state (active stays active, inactive stays inactive) async reviseTemplate(previousId, data) { logger.info('DocumentTemplateService.reviseTemplate:start', { previousId }); const uow = new UnitOfWork(); try { await uow.start(); const previous = await DocumentTemplateRepository.findById(previousId, uow.connection); if (!previous) { logger.warn('DocumentTemplateService.reviseTemplate:not_found', { previousId }); await uow.rollback(); return null; } const nextVersion = (previous.version || 1) + 1; const nextState = (data.state === 'active' || data.state === 'inactive') ? data.state : (previous.state === 'active' ? 'active' : 'inactive'); const created = await DocumentTemplateRepository.create( { ...data, version: nextVersion, state: nextState, }, uow.connection ); // If new template is active and is a contract template, deactivate other conflicting actives if (nextState === 'active' && (data.type === 'contract' || previous.type === 'contract')) { await DocumentTemplateRepository.deactivateOtherActiveContracts({ excludeId: created.id, contract_type: (data.contract_type || previous.contract_type || 'contract'), lang: (data.lang || previous.lang), user_type: (data.user_type || previous.user_type || 'both') }, uow.connection); } // Deactivate previous (requirement: deactive the previous one) await DocumentTemplateRepository.updateState(previousId, 'inactive', uow.connection); await uow.commit(); logger.info('DocumentTemplateService.reviseTemplate:success', { previousId, newId: created.id, version: nextVersion }); return created; } catch (err) { logger.error('DocumentTemplateService.reviseTemplate:error', { previousId, error: err.message }); await uow.rollback(err); throw err; } } async getActiveTemplatesForUserType(userType, templateType = null, contractType = null) { logger.info('DocumentTemplateService.getActiveTemplatesForUserType:start', { userType, templateType, contractType }); try { const rows = await DocumentTemplateRepository.findActiveByUserType(userType, templateType, contractType); logger.info('DocumentTemplateService.getActiveTemplatesForUserType:success', { count: rows.length }); return rows.map(t => ({ ...t, lang: t.lang })); } catch (error) { logger.error('DocumentTemplateService.getActiveTemplatesForUserType:error', { error: error.message }); throw error; } } // Convenience: return the most recent active template for a user type (by createdAt desc) async getLatestActiveForUserType(userType, templateType = 'contract', contractType = null) { logger.info('DocumentTemplateService.getLatestActiveForUserType:start', { userType, templateType, contractType }); try { const list = await DocumentTemplateRepository.findActiveByUserType(userType, templateType, contractType); const latest = Array.isArray(list) && list.length ? list[0] : null; logger.info('DocumentTemplateService.getLatestActiveForUserType:result', { found: !!latest, id: latest?.id }); return latest; } catch (error) { logger.error('DocumentTemplateService.getLatestActiveForUserType:error', { error: error.message }); throw error; } } // NEW: Retrieve Profit Planet signature (company stamp) as tag // Fallback order: // 1) Active stamp for provided companyId // 2) Any stamp for provided companyId // 3) Any active stamp globally // 4) Any stamp globally async getProfitPlanetSignatureTag({ companyId = null, maxW = 300, maxH = 300 } = {}) { const uow = new UnitOfWork(); const result = { tag: '', reason: 'not_started' }; logger.debug('DocumentTemplateService.getProfitPlanetSignatureTag:enter', { companyId, maxW, maxH }); try { await uow.start(); const conn = uow.connection; const safeRowMeta = (row) => { if (!row) return null; return { has_image: !!row.image_base64, mime: row.mime_type, image_len: row.image_base64 ? row.image_base64.length : 0, image_head: row.image_base64 ? row.image_base64.slice(0, 30) : '' }; }; // Helper to run single-row query safely const fetchOne = async (sql, params, reasonCode) => { logger.debug('DocumentTemplateService.getProfitPlanetSignatureTag:query:start', { reasonCode, sql, params }); try { const started = Date.now(); const [rows] = await conn.execute(sql, params); const ms = Date.now() - started; const rowCount = rows ? rows.length : 0; logger.debug('DocumentTemplateService.getProfitPlanetSignatureTag:query:result', { reasonCode, duration_ms: ms, rowCount, firstRow: safeRowMeta(rows && rows[0]) }); if (rows && rows[0] && rows[0].image_base64) { const mime = rows[0].mime_type || 'image/png'; const dataUri = `data:${mime};base64,${rows[0].image_base64}`; result.tag = ``; result.reason = reasonCode; logger.debug('DocumentTemplateService.getProfitPlanetSignatureTag:query:match', { reasonCode, mime, dataUri_len: dataUri.length, dataUri_head: dataUri.slice(0, 45) }); return true; } } catch (e) { logger.warn('DocumentTemplateService.getProfitPlanetSignatureTag:query_error', { reason: reasonCode, error: e.message }); } return false; }; if (companyId) { if (await fetchOne( 'SELECT mime_type,image_base64 FROM company_stamps WHERE company_id = ? AND is_active = 1 ORDER BY id DESC LIMIT 1', [companyId], 'company_active' )) { await uow.commit(); logger.debug('DocumentTemplateService.getProfitPlanetSignatureTag:exit', { reason: result.reason }); return result; } if (await fetchOne( 'SELECT mime_type,image_base64 FROM company_stamps WHERE company_id = ? ORDER BY is_active DESC, id DESC LIMIT 1', [companyId], 'company_any' )) { await uow.commit(); logger.debug('DocumentTemplateService.getProfitPlanetSignatureTag:exit', { reason: result.reason }); return result; } } if (await fetchOne( 'SELECT mime_type,image_base64 FROM company_stamps WHERE is_active = 1 ORDER BY id DESC LIMIT 1', [], 'global_active' )) { await uow.commit(); logger.debug('DocumentTemplateService.getProfitPlanetSignatureTag:exit', { reason: result.reason }); return result; } if (await fetchOne( 'SELECT mime_type,image_base64 FROM company_stamps ORDER BY is_active DESC, id DESC LIMIT 1', [], 'global_any' )) { await uow.commit(); logger.debug('DocumentTemplateService.getProfitPlanetSignatureTag:exit', { reason: result.reason }); return result; } result.reason = 'not_found'; await uow.commit(); logger.debug('DocumentTemplateService.getProfitPlanetSignatureTag:not_found', { companyId }); return result; } catch (err) { result.reason = 'error'; logger.error('DocumentTemplateService.getProfitPlanetSignatureTag:error', { error: err.message, companyId }); try { await uow.rollback(err); } catch (rbErr) { logger.error('DocumentTemplateService.getProfitPlanetSignatureTag:rollback_error', { error: rbErr.message }); } return result; } finally { logger.debug('DocumentTemplateService.getProfitPlanetSignatureTag:final', { reason: result.reason, hasTag: !!result.tag }); } } } module.exports = new DocumentTemplateService();