feat: add template revision functionality to create new versions and deactivate previous templates
This commit is contained in:
parent
41b444a190
commit
deb94c028b
@ -448,6 +448,73 @@ exports.updateTemplate = async (req, res) => {
|
|||||||
res.json(enriched);
|
res.json(enriched);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// NEW: revise a template by creating a NEW template record (version bump) and deactivating the previous one.
|
||||||
|
// Route suggestion: POST /document-templates/:id/revise (multipart/form-data with 'file')
|
||||||
|
exports.reviseTemplate = async (req, res) => {
|
||||||
|
const previousId = req.params.id;
|
||||||
|
const { name, type, description, lang } = req.body;
|
||||||
|
const rawUserType = req.body.user_type || req.body.userType;
|
||||||
|
const rawContractType = req.body.contract_type || req.body.contractType;
|
||||||
|
const requestedState = req.body.state; // optional: 'active' | 'inactive'
|
||||||
|
|
||||||
|
const file = req.file;
|
||||||
|
if (!file) return res.status(400).json({ error: 'No file uploaded' });
|
||||||
|
|
||||||
|
const previous = await DocumentTemplateService.getTemplate(previousId);
|
||||||
|
if (!previous) return res.status(404).json({ error: 'Template not found' });
|
||||||
|
|
||||||
|
const nextType = type !== undefined ? type : previous.type;
|
||||||
|
const nextLang = lang !== undefined ? lang : previous.lang;
|
||||||
|
if (!nextLang || !['en', 'de'].includes(nextLang)) return res.status(400).json({ error: 'Invalid or missing language' });
|
||||||
|
|
||||||
|
const allowedUserTypes = ['personal', 'company', 'both'];
|
||||||
|
const user_type = allowedUserTypes.includes(rawUserType)
|
||||||
|
? rawUserType
|
||||||
|
: (previous.user_type || 'both');
|
||||||
|
|
||||||
|
const allowedContractTypes = ['contract', 'gdpr'];
|
||||||
|
let contract_type = null;
|
||||||
|
if (nextType === 'contract') {
|
||||||
|
const candidate = rawContractType !== undefined ? rawContractType : previous.contract_type;
|
||||||
|
contract_type = allowedContractTypes.includes(candidate) ? candidate : 'contract';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use "english" for en, "german" for de
|
||||||
|
const langFolder = nextLang === 'en' ? 'english' : 'german';
|
||||||
|
const storageKey = `DocumentTemplates/${langFolder}/${Date.now()}_${file.originalname}`;
|
||||||
|
|
||||||
|
const s3 = new S3Client({
|
||||||
|
region: process.env.EXOSCALE_REGION,
|
||||||
|
endpoint: process.env.EXOSCALE_ENDPOINT,
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
|
||||||
|
secretAccessKey: process.env.EXOSCALE_SECRET_KEY
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await s3.send(new PutObjectCommand({
|
||||||
|
Bucket: process.env.EXOSCALE_BUCKET,
|
||||||
|
Key: storageKey,
|
||||||
|
Body: file.buffer,
|
||||||
|
ContentType: file.mimetype
|
||||||
|
}));
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
name: name !== undefined ? name : previous.name,
|
||||||
|
type: nextType,
|
||||||
|
contract_type,
|
||||||
|
storageKey,
|
||||||
|
description: description !== undefined ? description : previous.description,
|
||||||
|
lang: nextLang,
|
||||||
|
user_type,
|
||||||
|
...(requestedState !== undefined ? { state: requestedState } : {})
|
||||||
|
};
|
||||||
|
|
||||||
|
const created = await DocumentTemplateService.reviseTemplate(previousId, payload);
|
||||||
|
if (!created) return res.status(404).json({ error: 'Template not found' });
|
||||||
|
const enriched = await enrichTemplate(created, s3);
|
||||||
|
res.status(201).json(enriched);
|
||||||
|
};
|
||||||
|
|
||||||
exports.updateTemplateState = async (req, res) => {
|
exports.updateTemplateState = async (req, res) => {
|
||||||
const id = req.params.id;
|
const id = req.params.id;
|
||||||
const { state } = req.body;
|
const { state } = req.body;
|
||||||
|
|||||||
@ -28,21 +28,24 @@ class DocumentTemplateRepository {
|
|||||||
: null;
|
: null;
|
||||||
const finalContractType = type === 'contract' ? (contract_type || 'contract') : null;
|
const finalContractType = type === 'contract' ? (contract_type || 'contract') : null;
|
||||||
|
|
||||||
|
const version = Number.isFinite(Number(data.version)) ? Math.max(1, Number(data.version)) : 1;
|
||||||
|
const state = (data.state === 'active' || data.state === 'inactive') ? data.state : 'inactive';
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
INSERT INTO document_templates (name, type, contract_type, storageKey, description, lang, user_type, version, state, createdAt, updatedAt)
|
INSERT INTO document_templates (name, type, contract_type, storageKey, description, lang, user_type, version, state, createdAt, updatedAt)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, 1, 'inactive', NOW(), NOW())
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
|
||||||
`;
|
`;
|
||||||
try {
|
try {
|
||||||
if (conn) {
|
if (conn) {
|
||||||
const [res] = await conn.execute(query, [name, type, finalContractType, storageKey, description, lang, user_type]);
|
const [res] = await conn.execute(query, [name, type, finalContractType, storageKey, description, lang, user_type, version, state]);
|
||||||
const insertId = res && (res.insertId || res[0]?.insertId);
|
const insertId = res && (res.insertId || res[0]?.insertId);
|
||||||
logger.info('DocumentTemplateRepository.create:success', { id: insertId || res.insertId });
|
logger.info('DocumentTemplateRepository.create:success', { id: insertId || res.insertId });
|
||||||
return { id: insertId || res.insertId, name, type, contract_type: finalContractType, storageKey, description, lang, user_type, version: 1, state: 'inactive' };
|
return { id: insertId || res.insertId, name, type, contract_type: finalContractType, storageKey, description, lang, user_type, version, state };
|
||||||
}
|
}
|
||||||
const result = await db.execute(query, [name, type, finalContractType, storageKey, description, lang, user_type]);
|
const result = await db.execute(query, [name, type, finalContractType, storageKey, description, lang, user_type, version, state]);
|
||||||
const insertId = result && result.insertId ? result.insertId : (Array.isArray(result) && result[0] && result[0].insertId ? result[0].insertId : undefined);
|
const insertId = result && result.insertId ? result.insertId : (Array.isArray(result) && result[0] && result[0].insertId ? result[0].insertId : undefined);
|
||||||
logger.info('DocumentTemplateRepository.create:success', { id: insertId });
|
logger.info('DocumentTemplateRepository.create:success', { id: insertId });
|
||||||
return { id: insertId, name, type, contract_type: finalContractType, storageKey, description, lang, user_type, version: 1, state: 'inactive' };
|
return { id: insertId, name, type, contract_type: finalContractType, storageKey, description, lang, user_type, version, state };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('DocumentTemplateRepository.create:error', { error: error.message });
|
logger.error('DocumentTemplateRepository.create:error', { error: error.message });
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@ -59,6 +59,7 @@ router.post('/permissions', authMiddleware, PermissionController.create);
|
|||||||
|
|
||||||
// Document templates upload & signature generation POSTs (moved)
|
// Document templates upload & signature generation POSTs (moved)
|
||||||
router.post('/document-templates', authMiddleware, upload.single('file'), DocumentTemplateController.uploadTemplate);
|
router.post('/document-templates', authMiddleware, upload.single('file'), DocumentTemplateController.uploadTemplate);
|
||||||
|
router.post('/document-templates/:id/revise', authMiddleware, upload.single('file'), DocumentTemplateController.reviseTemplate);
|
||||||
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)
|
||||||
|
|||||||
@ -128,6 +128,49 @@ class DocumentTemplateService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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) {
|
async getActiveTemplatesForUserType(userType, templateType = null, contractType = null) {
|
||||||
logger.info('DocumentTemplateService.getActiveTemplatesForUserType:start', { userType, templateType, contractType });
|
logger.info('DocumentTemplateService.getActiveTemplatesForUserType:start', { userType, templateType, contractType });
|
||||||
try {
|
try {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user