This commit is contained in:
2026-06-18 22:11:30 +02:00
parent b2e73b4fe7
commit 0f3c27050b
4 changed files with 301 additions and 59 deletions
+136
View File
@@ -0,0 +1,136 @@
import db from '../db/index.js';
const JOB_NAME = 'auto_ticket_status';
function writeLog({ status, nbChanges, details, errorMsg }) {
try {
db.prepare(`
INSERT INTO job_logs (job_name, status, nb_changes, details, error_msg)
VALUES (?, ?, ?, ?, ?)
`).run(JOB_NAME, status, nbChanges ?? 0, details ?? null, errorMsg ?? null);
} catch (e) {
console.error('[autoTicketStatus] Impossible d\'écrire dans job_logs :', e.message);
}
}
/**
* Règles appliquées :
*
* 1. open → pending
* Ticket ouvert sans aucun message depuis plus de 2 jours.
* (updated_at sert d'horodatage de la dernière activité)
*
* 2. pending → resolved (clôture automatique)
* Ticket en attente où le créateur du ticket n'a pas posté de message
* depuis plus de 5 jours.
* On cherche le dernier message de l'émetteur (is_admin = 0 et user_id = ticket.user_id).
* Si ce message date de plus de 5 jours, le ticket est clôturé.
*/
export function checkTicketStatuses() {
let nbChanges = 0;
const details = [];
// ── Règle 1 : open → pending (pas de réponse depuis 2 jours) ─────────────
const toPending = db.prepare(`
SELECT id, ticket_number, subject, updated_at
FROM tickets
WHERE status = 'open'
AND datetime(updated_at) < datetime('now', '-2 days')
`).all();
const setPending = db.prepare(`
UPDATE tickets SET status = 'pending', updated_at = datetime('now') WHERE id = ?
`);
const notifyUserPending = db.prepare(`
INSERT INTO notifications (user_id, type, title, body, link)
SELECT user_id, 'ticket_reply',
'[' || ticket_number || '] Ticket mis en attente',
'Votre ticket "' || subject || '" est passé en attente faute de réponse depuis 2 jours.',
'/communication?ticket=' || id
FROM tickets WHERE id = ?
`);
for (const t of toPending) {
setPending.run(t.id);
try { notifyUserPending.run(t.id); } catch (_) { /* best-effort */ }
details.push(`[pending] ${t.ticket_number} (id=${t.id})`);
nbChanges++;
}
// ── Règle 2 : pending → resolved (pas de réponse créateur depuis 5 jours) ─
const pendingTickets = db.prepare(`
SELECT id, ticket_number, subject, user_id
FROM tickets
WHERE status = 'pending'
`).all();
const lastCreatorMsg = db.prepare(`
SELECT MAX(created_at) as last_at
FROM ticket_messages
WHERE ticket_id = ? AND user_id = ? AND is_admin = 0
`);
const setResolved = db.prepare(`
UPDATE tickets SET status = 'resolved', updated_at = datetime('now') WHERE id = ?
`);
const notifyUserResolved = db.prepare(`
INSERT INTO notifications (user_id, type, title, body, link)
SELECT user_id, 'ticket_reply',
'[' || ticket_number || '] Ticket clôturé automatiquement',
'Votre ticket "' || subject || '" a été clôturé automatiquement après 5 jours sans réponse.',
'/communication?ticket=' || id
FROM tickets WHERE id = ?
`);
for (const t of pendingTickets) {
const row = lastCreatorMsg.get(t.id, t.user_id);
// Si jamais de message créateur, on prend la date de création du ticket (updated_at)
const refDate = row?.last_at ?? null;
if (!refDate) continue; // pas de message créateur trouvé, on ne clôture pas
const ageDays = (Date.now() - new Date(refDate + 'Z').getTime()) / (1000 * 60 * 60 * 24);
if (ageDays > 5) {
setResolved.run(t.id);
try { notifyUserResolved.run(t.id); } catch (_) { /* best-effort */ }
details.push(`[resolved] ${t.ticket_number} (id=${t.id}, last_creator_msg=${refDate})`);
nbChanges++;
}
}
const summary = nbChanges === 0
? 'Aucun ticket modifié'
: details.join('; ');
writeLog({ status: 'ok', nbChanges, details: summary });
if (nbChanges > 0) {
console.log(`[autoTicketStatus] ${nbChanges} ticket(s) mis à jour : ${summary}`);
}
return nbChanges;
}
/**
* Démarre le job — exécution immédiate au démarrage, puis toutes les heures.
*/
export function startAutoTicketStatusJob() {
const INTERVAL_MS = 60 * 60 * 1000; // 1 heure
try {
checkTicketStatuses();
} catch (err) {
console.error('[autoTicketStatus] Erreur initiale :', err);
writeLog({ status: 'error', nbChanges: 0, errorMsg: err.message });
}
setInterval(() => {
try {
checkTicketStatuses();
} catch (err) {
console.error('[autoTicketStatus] Erreur :', err);
writeLog({ status: 'error', nbChanges: 0, errorMsg: err.message });
}
}, INTERVAL_MS);
console.log('[autoTicketStatus] Job démarré — vérification toutes les heures');
}
+2
View File
@@ -33,6 +33,7 @@ import { errorHandler } from './middleware/errorHandler.js';
import { requireAuth, requireAdmin } from './middleware/auth.js';
import { startAutoStatutJob } from './jobs/autoStatut.js';
import { startAutoExportJob } from './jobs/autoExport.js';
import { startAutoTicketStatusJob } from './jobs/autoTicketStatus.js';
import adminRouter from './routes/admin.js';
import invitationsRouter from './routes/invitations.js';
import auditLogsRouter from './routes/auditLogs.js';
@@ -141,4 +142,5 @@ app.listen(PORT, () => {
console.log(`Crowdlending API listening on http://localhost:${PORT}`);
startAutoStatutJob();
startAutoExportJob();
startAutoTicketStatusJob();
});