- {{ audPreview.count }} destinataire(s) · {{ audPreview.dropped_no_email }} sans courriel écarté(s)
+ {{ audPreview.count }} destinataire(s) · {{ audPreview.dropped_no_email }} sans courriel écarté(s) · {{ audPreview.resiliated_excluded }} ex-client(s) résilié(s) exclus
ex. {{ audPreview.sample.slice(0, 4).map(s => s.firstname || s.email).join(', ') }}…
diff --git a/services/targo-hub/lib/events.js b/services/targo-hub/lib/events.js
index 48e35ed..ef2492c 100644
--- a/services/targo-hub/lib/events.js
+++ b/services/targo-hub/lib/events.js
@@ -239,6 +239,15 @@ function splitName (full) {
}
const firstEmail = (s) => String(s || '').split(/[;,]/)[0].trim()
+// Comptes F RÉSILIÉS (status 3/4/5) — à EXCLURE de l'audience (bug sync : services restent status=1 après
+// résiliation du compte → abonnements 'Actif' fantômes ; cf feedback_ghost_active_subscriptions). Caché 30 min.
+let _resilCache = null; let _resilTs = 0
+async function resiliatedSet () {
+ if (_resilCache && (Date.now() - _resilTs < 30 * 60 * 1000)) return _resilCache
+ try { const ids = await require('./legacy-sync').fResiliatedAccountIds(); _resilCache = new Set((ids || []).map(Number)); _resilTs = Date.now() } catch (e) { log('events resiliatedSet: ' + e.message); if (!_resilCache) _resilCache = new Set() }
+ return _resilCache
+}
+
// CSV : SEULE colonne obligatoire = email ; alias FR/EN acceptés ; en-tête requise.
function parseAudienceCsv (text) {
const lines = String(text || '').split(/\r?\n/).filter(l => l.trim())
@@ -264,6 +273,7 @@ function parseAudienceCsv (text) {
// Résout l'audience → destinataires [{email,firstname,lastname,language,customer_id}] (dédupliqués, courriel requis).
async function resolveAudience ({ mode = 'filter', filters = {}, csv = '' } = {}) {
let rows = []
+ let resiliatedExcluded = 0
if (mode === 'csv') {
const p = parseAudienceCsv(csv)
if (p.error) return { error: p.error }
@@ -274,7 +284,7 @@ async function resolveAudience ({ mode = 'filter', filters = {}, csv = '' } = {}
if (filters.statut === 'active') f.push(['disabled', '=', 0])
else if (filters.statut === 'disabled') f.push(['disabled', '=', 1])
if (filters.customer_type) f.push(['customer_type', '=', filters.customer_type]) // Individual = résidentiel · Company = commercial
- const CFIELDS = ['name', 'customer_name', 'email_id', 'language', 'customer_type']
+ const CFIELDS = ['name', 'customer_name', 'email_id', 'language', 'customer_type', 'legacy_account_id']
const minMonthly = Number(filters.min_monthly) || 0
let customers = []
if (filters.active_sub || minMonthly > 0) {
@@ -292,6 +302,10 @@ async function resolveAudience ({ mode = 'filter', filters = {}, csv = '' } = {}
let s = 0
for (let p = 0; p < 60; p++) { const b = await erp.list('Customer', { filters: f, fields: CFIELDS, limit: 500, start: s }); customers.push(...b); if (b.length < 500) break; s += 500 }
}
+ const resil = await resiliatedSet() // GARDE-FOU : jamais inviter un compte F résilié (abos fantômes, cf feedback_ghost_active_subscriptions)
+ const before = customers.length
+ customers = customers.filter(c => !resil.has(Number(c.legacy_account_id)))
+ resiliatedExcluded = before - customers.length
rows = customers.map(c => { const n = splitName(c.customer_name); return { email: firstEmail(c.email_id), firstname: n.firstname, lastname: n.lastname, language: normLang(c.language), customer_id: c.name, customer_type: c.customer_type || '', monthly: Math.round(c._monthly || 0) } })
}
const seen = new Set(); const recipients = []; const noEmail = []; let dropped = 0
@@ -304,7 +318,7 @@ async function resolveAudience ({ mode = 'filter', filters = {}, csv = '' } = {}
const k = r.email.toLowerCase(); if (seen.has(k)) continue; seen.add(k)
recipients.push(r)
}
- return { recipients, count: recipients.length, dropped_no_email: dropped, no_email: noEmail }
+ return { recipients, count: recipients.length, dropped_no_email: dropped, no_email: noEmail, resiliated_excluded: resiliatedExcluded }
}
// ── Page publique (festive, mobile-first, bilingue) ────────────────────────
@@ -569,7 +583,7 @@ async function handle (req, res, method, path, url) {
const b = await parseBody(req)
const r = await resolveAudience({ mode: b.mode, filters: b.filters || {}, csv: b.csv || '' })
if (r.error) return json(res, 400, { error: r.error })
- return json(res, 200, { count: r.count, dropped_no_email: r.dropped_no_email, sample: r.recipients.slice(0, 50), no_email: (r.no_email || []).slice(0, 300) })
+ return json(res, 200, { count: r.count, dropped_no_email: r.dropped_no_email, resiliated_excluded: r.resiliated_excluded || 0, sample: r.recipients.slice(0, 50), no_email: (r.no_email || []).slice(0, 300) })
}
return json(res, 404, { error: 'not found' })