Fixe
This commit is contained in:
@@ -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');
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user