'use strict' // ───────────────────────────────────────────────────────────────────────────── // coupon-triage.js — Levier 1 : qu'une demande de coupons hotspot ne « dorme » plus. // Courriel reçu → détecte l'intention → RÉSOUT la ferme par le COURRIEL de l'expéditeur // (pas le nom : ex. Chantaltrottier67@gmail.com → Jardins Lefort Inc) → crée un ticket // suivi (file support) → ALERTE (n8n → Google Chat). Zéro dépendance à F. // L'ÉMISSION des coupons (camping_customer_batch) reste manuelle/F pour l'instant (Levier 2). // ───────────────────────────────────────────────────────────────────────────── const https = require('https') const { json, log, parseBody, lookupCustomersByEmail } = require('./helpers') const COUPON_RX = /coupon|hotspot|code\s*d['e]?\s*acc[èe]s|acc[èe]s\s*wifi|wifi\s*(temporaire|invit)|forfait\s*camping|travailleur|prépay|prepaid|carte\s*temps/i const QTY_RX = /(\d{1,3})\s*(coupons?|codes?|acc[èe]s|travailleurs?|cartes?|forfaits?)/i // Détecte l'intention « coupons » + une quantité éventuelle dans le sujet/corps. function detectCoupon (subject, body) { const txt = `${subject || ''}\n${body || ''}` const m = txt.match(QTY_RX) return { isCoupon: COUPON_RX.test(txt), qty: m ? parseInt(m[1], 10) : null } } // Alerte vers n8n (qui poste dans Google Chat) ou un webhook Google Chat direct. // Configurable : N8N_COUPON_WEBHOOK ou GCHAT_WEBHOOK. Sans config → loggé + sauté (pas d'échec). async function alertWebhook (payload) { const url = process.env.N8N_COUPON_WEBHOOK || process.env.GCHAT_WEBHOOK || '' if (!url) { log('coupon-triage: aucun webhook (N8N_COUPON_WEBHOOK/GCHAT_WEBHOOK) — alerte sautée'); return { alerted: false, reason: 'no_webhook' } } const body = JSON.stringify({ text: payload.text, ...payload }) // Google Chat lit {text} ; n8n lit tout return new Promise((resolve) => { try { const u = new URL(url) const req = https.request({ hostname: u.hostname, path: u.pathname + u.search, method: 'POST', timeout: 8000, headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } }, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => resolve({ alerted: res.statusCode < 400, status: res.statusCode })) }) req.on('error', e => { log('coupon alert err: ' + e.message); resolve({ alerted: false, error: e.message }) }) req.on('timeout', () => req.destroy(new Error('timeout'))) req.write(body); req.end() } catch (e) { resolve({ alerted: false, error: e.message }) } }) } // Triage d'une demande. dryRun=true → détecte + résout, sans rien créer. async function triage ({ from = '', subject = '', body = '', agent = '', dryRun = false } = {}) { const det = detectCoupon(subject, body) const matches = from ? (await lookupCustomersByEmail(from).catch(() => [])) : [] const farm = (matches && matches.length) ? matches[0] : null const out = { detected: det.isCoupon, qty: det.qty, from, farm: farm ? { name: farm.name, customer_name: farm.customer_name } : null, matches: (matches || []).length, } if (!det.isCoupon) { out.note = 'non détecté comme demande de coupons'; return out } if (dryRun) { out.dry_run = true; return out } // 1) ticket suivi (file support ; signale si la ferme n'est pas résolue) const title = `Coupons hotspot${det.qty ? ` ×${det.qty}` : ''}${farm ? ` — ${farm.customer_name}` : ''}`.slice(0, 120) const desc = `Demande de coupons hotspot${det.qty ? ` (×${det.qty})` : ''} reçue de ${from}.\n` + (farm ? `Ferme facturable résolue par courriel : ${farm.customer_name} (${farm.name}).` : '⚠ Ferme NON résolue par le courriel expéditeur — à identifier (mentionnée dans le message ?).') + `\n\n--- message ---\n${String(body || '').slice(0, 800)}` let ticket = {} try { ticket = await require('./ticket-collab').createTicket({ title, category: 'Support', priority: 'Medium', status: 'Open', customer: farm ? farm.name : undefined, customer_name: farm ? farm.customer_name : undefined, queue: 'Supports', description: desc, agent }) } catch (e) { log('coupon-triage createTicket: ' + e.message); ticket = { ok: false, error: e.message } } // 2) alerte (n8n → Google Chat) const text = `🎟️ Demande de coupons${det.qty ? ` ×${det.qty}` : ''} — ${farm ? farm.customer_name : 'ferme à IDENTIFIER (' + from + ')'}` + `${ticket.name ? ` · ticket ${ticket.name}` : ''}${farm ? '' : ' · ⚠ courriel non résolu'}` const alert = await alertWebhook({ text, from, qty: det.qty, farm: out.farm, ticket: ticket.name || null }) return { ...out, ticket: ticket.name || null, ticket_ok: ticket.ok !== false, alert } } // Hook AUTO depuis l'inbox (ingestEmail) : la conversation est déjà le suivi (client résolu + file) → // on NE crée PAS de ticket séparé, on MARQUE la conv « coupons » + on ALERTE (Google Chat via n8n) pour ne pas la laisser dormir. async function onEmail ({ from = '', subject = '', body = '', conv = null } = {}) { const det = detectCoupon(subject, body) if (!det.isCoupon) return { detected: false } if (conv) { conv.couponRequest = { qty: det.qty, ts: new Date().toISOString() }; if (!conv.queue) conv.queue = 'Supports' } const farmName = (conv && (conv.customerName || conv.customer)) || from const link = (conv && conv.token) ? `\n${process.env.OPS_BASE_URL || 'https://ops.gigafibre.ca'}/#/conversations?c=${conv.token}` : '' const text = `🎟️ Demande de coupons${det.qty ? ` ×${det.qty}` : ''} — ${farmName}${(conv && conv.customer) ? '' : ' · ⚠ ferme à identifier'}${link}` const alert = await alertWebhook({ text, from, qty: det.qty, customer: (conv && conv.customer) || null, conv: (conv && conv.token) || null }) log(`coupon-triage onEmail: ${from} qty=${det.qty} alerted=${alert.alerted}`) return { detected: true, qty: det.qty, alerted: alert.alerted } } async function handle (req, res, method, p) { if (p === '/coupons/triage' && method === 'POST') { try { const b = await parseBody(req); return json(res, 200, await triage({ from: b.from, subject: b.subject, body: b.body, agent: req.headers['x-authentik-email'] || b.agent || '', dryRun: !!b.dryRun })) } catch (e) { return json(res, 500, { error: e.message }) } } return json(res, 404, { error: 'route coupons inconnue' }) } module.exports = { handle, triage, onEmail, detectCoupon, alertWebhook }