From aa98d9ac37ab5502ef2d12c003630cc0c847d115 Mon Sep 17 00:00:00 2001 From: seaznCode Date: Wed, 14 Jan 2026 21:36:06 +0100 Subject: [PATCH] feat: implement deactivation of conflicting active contract templates during state updates --- .../template/DocumentTemplateRepository.js | 43 +++++++++++++++++++ services/template/DocumentTemplateService.js | 28 ++++++++++++ 2 files changed, 71 insertions(+) diff --git a/repositories/template/DocumentTemplateRepository.js b/repositories/template/DocumentTemplateRepository.js index e8ed559..7980df7 100644 --- a/repositories/template/DocumentTemplateRepository.js +++ b/repositories/template/DocumentTemplateRepository.js @@ -144,6 +144,49 @@ class DocumentTemplateRepository { } } + // Deactivate other active contract templates that would conflict with the one being activated. + // Conflict dimensions: + // - same type='contract' + // - same contract_type ('contract' | 'gdpr') + // - same lang + // - overlapping user_type (personal/company/both) + async deactivateOtherActiveContracts({ excludeId, contract_type, lang, user_type }, conn) { + logger.info('DocumentTemplateRepository.deactivateOtherActiveContracts:start', { excludeId, contract_type, lang, user_type }); + const safeContractType = (contract_type === 'gdpr' || contract_type === 'contract') ? contract_type : 'contract'; + const safeLang = (lang === 'en' || lang === 'de') ? lang : 'en'; + const safeUserType = (user_type === 'personal' || user_type === 'company' || user_type === 'both') ? user_type : 'both'; + + // user_type overlap rules + const overlap = safeUserType === 'both' + ? ['personal', 'company', 'both'] + : [safeUserType, 'both']; + + const query = ` + UPDATE document_templates + SET state = 'inactive', updatedAt = NOW() + WHERE id <> ? + AND type = 'contract' + AND contract_type = ? + AND lang = ? + AND state = 'active' + AND user_type IN (${overlap.map(() => '?').join(', ')}) + `; + const params = [excludeId, safeContractType, safeLang, ...overlap]; + try { + if (conn) { + const [res] = await conn.execute(query, params); + logger.info('DocumentTemplateRepository.deactivateOtherActiveContracts:success', { affected: res?.affectedRows }); + return res?.affectedRows || 0; + } + const res = await db.execute(query, params); + logger.info('DocumentTemplateRepository.deactivateOtherActiveContracts:success', { affected: res?.affectedRows }); + return res?.affectedRows || 0; + } catch (error) { + logger.error('DocumentTemplateRepository.deactivateOtherActiveContracts:error', { error: error.message }); + throw error; + } + } + async delete(id, conn) { logger.info('DocumentTemplateRepository.delete:start', { id }); const query = `DELETE FROM document_templates WHERE id = ?`; diff --git a/services/template/DocumentTemplateService.js b/services/template/DocumentTemplateService.js index 6b51d58..351066b 100644 --- a/services/template/DocumentTemplateService.js +++ b/services/template/DocumentTemplateService.js @@ -116,7 +116,25 @@ class DocumentTemplateService { 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 }); @@ -158,6 +176,16 @@ class DocumentTemplateService { 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);