feat: implement deactivation of conflicting active contract templates during state updates

This commit is contained in:
seaznCode 2026-01-14 21:36:06 +01:00
parent deb94c028b
commit aa98d9ac37
2 changed files with 71 additions and 0 deletions

View File

@ -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 = ?`;

View File

@ -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);