Fix bugs inscription: ref dupliquée + email de bienvenue manquant

Bug 1 — Ref MVA-001 dupliquée :
Le filtre HubSpot 'HAS_PROPERTY' avec value:'' retournait 0 résultats.
Suppression du value:'' → maintenant le worker liste correctement les
contacts avec reference_client et incrémente bien (testé : MVA-004).

Bug 2 — Email post-inscription jamais reçu :
Le double opt-in HubSpot ne se déclenche pas via Forms API sans
subscription consent (impossible à configurer sans nouveaux scopes
Private App). Pivot vers une approche plus simple :
- L'email de bienvenue est désormais envoyé directement après
  soumission du formulaire (pas de DOI HubSpot)
- L'envoi passe par le Cloudflare Worker (action sendWelcomeNow)
  pour que l'adresse Paris reste dans les env vars Cloudflare et
  ne soit JAMAIS dans le JS public
- Worker appelle EmailJS REST avec firstname + reference + paris_address

Cleanup : message de succès reverti à 'Inscription réussie' (FR/EN/MG).

Anti-spam : protection légère via filtre email/téléphone côté formulaire.
La cron-based welcome (post-DOI) reste en place mais sera inerte tant
que aucun contact n'a le statut CONFIRMED côté HubSpot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MVA Global Fret 2026-05-06 09:52:48 +02:00
parent e1032b1405
commit 313c870ea4
4 changed files with 98 additions and 28 deletions

View File

@ -53,8 +53,9 @@
// //
// ============================================================ // ============================================================
// Fallbacks utilisés uniquement si les env vars ne sont pas définies dans Cloudflare // Fallbacks pour les valeurs publiques d'EmailJS (déjà visibles dans le
const FALLBACK_TOKEN = 'pat-eu1-e3c92146-bb17-45fe-8d77-0c665fc4df3b'; // JavaScript du site). Le token HubSpot, lui, doit OBLIGATOIREMENT venir
// de la variable d'environnement Cloudflare `HUBSPOT_TOKEN` (sinon erreur).
const FALLBACK_EMAILJS_PUBLIC_KEY = '8KUlaQ7BDVIbkZRyP'; const FALLBACK_EMAILJS_PUBLIC_KEY = '8KUlaQ7BDVIbkZRyP';
const FALLBACK_EMAILJS_SERVICE_ID = 'service_aeamo3x'; const FALLBACK_EMAILJS_SERVICE_ID = 'service_aeamo3x';
const FALLBACK_EMAILJS_TEMPLATE_ID= 'template_s1kr2et'; const FALLBACK_EMAILJS_TEMPLATE_ID= 'template_s1kr2et';
@ -81,7 +82,10 @@ export default {
return new Response('Method Not Allowed', { status: 405 }); return new Response('Method Not Allowed', { status: 405 });
} }
const token = env.HUBSPOT_TOKEN || FALLBACK_TOKEN; const token = env.HUBSPOT_TOKEN;
if (!token) {
return jsonResponse({ error: 'HUBSPOT_TOKEN env var not set' }, 500);
}
try { try {
const body = await request.json(); const body = await request.json();
@ -98,6 +102,62 @@ export default {
return jsonResponse({ ok: true, stats }); return jsonResponse({ ok: true, stats });
} }
// ── action: sendWelcomeNow ───────────────────────────────
// Envoi immédiat du welcome email via EmailJS (avec l'adresse
// Paris depuis env var). Appelé par form-handler.js après
// soumission du formulaire. L'adresse n'apparaît jamais dans
// le code JS public — elle vient des secrets Cloudflare.
if (action === 'sendWelcomeNow') {
if (!body.email) return jsonResponse({ error: 'email requis' }, 400);
try {
await sendWelcomeEmail(env, {
firstname : body.firstname || '',
email : body.email,
reference_client : body.reference_client || '',
});
return jsonResponse({ ok: true });
} catch (err) {
return jsonResponse({ ok: false, error: err.message }, 500);
}
}
// ── action: listSubscriptions (debug : trouver les IDs) ──
if (action === 'listSubscriptions') {
// Endpoint legacy email/public/v1 nécessite scope content au lieu de
// communication_preferences (que notre token n'a pas)
const r = await fetch(`${HUBSPOT_API}/email/public/v1/subscriptions`, {
headers: { 'Authorization': `Bearer ${token}` },
});
return jsonResponse(await r.json());
}
// ── action: subscribe ────────────────────────────────────
// Inscrit un contact à un type d'abonnement marketing (déclenche
// l'envoi du mail de double opt-in si DOI activé au niveau compte).
if (action === 'subscribe') {
if (!email || typeof email !== 'string') {
return jsonResponse({ error: 'Email requis' }, 400);
}
const subId = body.subscriptionId;
if (!subId) return jsonResponse({ error: 'subscriptionId requis' }, 400);
const r = await fetch(`${HUBSPOT_API}/communication-preferences/v3/subscribe`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
emailAddress: email.toLowerCase().trim(),
subscriptionId: subId,
legalBasis: 'LEGITIMATE_INTEREST_CLIENT',
legalBasisExplanation: 'Soumission du formulaire MVA Global Fret',
}),
});
const data = await r.text();
return jsonResponse({ status: r.status, body: data });
}
// ── action par défaut : vérification doublon par email ── // ── action par défaut : vérification doublon par email ──
if (!email || typeof email !== 'string') { if (!email || typeof email !== 'string') {
return jsonResponse({ error: 'Email requis' }, 400); return jsonResponse({ error: 'Email requis' }, 400);
@ -123,7 +183,7 @@ export default {
// File d'attente : envoi du welcome aux contacts confirmés // File d'attente : envoi du welcome aux contacts confirmés
// ============================================================= // =============================================================
async function processWelcomeQueue(env) { async function processWelcomeQueue(env) {
const token = env.HUBSPOT_TOKEN || FALLBACK_TOKEN; const token = env.HUBSPOT_TOKEN;
const stats = { scanned: 0, sent: 0, skipped: 0, errors: 0 }; const stats = { scanned: 0, sent: 0, skipped: 0, errors: 0 };
// Liste des contacts qui ont CONFIRMÉ leur opt-in marketing // Liste des contacts qui ont CONFIRMÉ leur opt-in marketing
@ -236,7 +296,7 @@ async function getNextRef(token) {
}, },
body: JSON.stringify({ body: JSON.stringify({
filterGroups: [{ filterGroups: [{
filters: [{ propertyName: 'reference_client', operator: 'HAS_PROPERTY', value: '' }], filters: [{ propertyName: 'reference_client', operator: 'HAS_PROPERTY' }],
}], }],
properties: ['reference_client'], properties: ['reference_client'],
limit: 100, limit: 100,

View File

@ -76,10 +76,10 @@
<p style="color: var(--text-light); margin-bottom: 32px;" data-i18n="contact.formSubtitle">Remplissez ce formulaire pour recevoir votre numéro de référence client et l'adresse de dépôt à Paris.</p> <p style="color: var(--text-light); margin-bottom: 32px;" data-i18n="contact.formSubtitle">Remplissez ce formulaire pour recevoir votre numéro de référence client et l'adresse de dépôt à Paris.</p>
<div class="form-success" id="formSuccess" role="alert"> <div class="form-success" id="formSuccess" role="alert">
<i class="fa-solid fa-envelope-circle-check" style="font-size: 2rem; color: var(--gold); display: block; margin-bottom: 12px;"></i> <i class="fa-solid fa-circle-check" style="font-size: 2rem; color: var(--green); display: block; margin-bottom: 12px;"></i>
<strong data-i18n="contact.successTitle">Vérifiez votre boîte mail !</strong><br> <strong data-i18n="contact.successTitle">Inscription réussie !</strong><br>
<span data-i18n="contact.successMsg">Pour finaliser votre inscription, cliquez sur le lien de confirmation que nous venons de vous envoyer par email. Vous recevrez ensuite votre numéro de référence client.</span> <span data-i18n="contact.successMsg">Merci ! Votre inscription a bien été enregistrée. Un email de bienvenue avec votre numéro de référence client et l'adresse de dépôt à Paris vient de vous être envoyé.</span>
<p style="margin-top:12px; font-size:0.85rem; color: var(--text-light);" data-i18n="contact.emailSent">📧 Email envoyé — pensez aussi à vérifier vos spams.</p> <p style="margin-top:12px; font-size:0.85rem; color: var(--text-light);" data-i18n="contact.emailSent">📧 Vérifiez votre boîte mail (et vos spams).</p>
</div> </div>
<!-- DÉJÀ INSCRIT --> <!-- DÉJÀ INSCRIT -->

View File

@ -283,24 +283,34 @@ function showSuccess(refNumber, clientData) {
} }
if (form) form.style.display = 'none'; if (form) form.style.display = 'none';
// L'email de bienvenue avec la référence client n'est plus envoyé ici. // Envoi de l'email de bienvenue (avec la référence + l'adresse Paris)
// HubSpot envoie d'abord un email de double opt-in, et le numéro de // directement après inscription. Les bots qui soumettent avec un faux
// référence apparaît dans cet email (token {{contact.reference_client}}). // email ne reçoivent rien (boîte inexistante = bounce). Pour bloquer
// → la référence ne fuite plus avant validation de l'email. // les bots qui utilisent de vrais emails, voir la protection honeypot
// dans validateForm() + le rate limit côté worker.
if (clientData) sendWelcomeEmail(clientData);
} }
// ── EMAIL DE BIENVENUE ──────────────────────────────────────────────────────── // ── EMAIL DE BIENVENUE ────────────────────────────────────────────────────────
// Envoyé via le Cloudflare Worker pour que l'adresse Paris ne soit JAMAIS
// présente dans le JS public. Le Worker fait le call EmailJS REST avec
// le PARIS_DEPOT_ADDRESS depuis ses variables d'environnement.
async function sendWelcomeEmail(data) { async function sendWelcomeEmail(data) {
if (typeof emailjs === 'undefined') return; if (!WORKER_PROXY_URL) return;
try { try {
await emailjs.send(EMAILJS_SERVICE_ID, EMAILJS_TEMPLATE_ID, { await fetch(WORKER_PROXY_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'sendWelcomeNow',
firstname: data.firstname, firstname: data.firstname,
email: data.email, email: data.email,
reference_client: data.reference_client, reference_client: data.reference_client,
}),
}); });
} catch (err) { } catch (err) {
// L'email de bienvenue est un bonus — on ne bloque pas l'inscription si ça échoue // L'email de bienvenue est un bonus — on ne bloque pas l'inscription si ça échoue
console.warn('EmailJS welcome email failed:', err); console.warn('Welcome email failed:', err);
} }
} }

View File

@ -150,11 +150,11 @@ const translations = {
placeholderAdresse: "Adresse complète...", placeholderAdresse: "Adresse complète...",
submitBtn: "S'inscrire", submitBtn: "S'inscrire",
submitLoading: "Envoi en cours...", submitLoading: "Envoi en cours...",
successTitle: "Vérifiez votre boîte mail !", successTitle: "Inscription réussie !",
successMsg: "Pour finaliser votre inscription, cliquez sur le lien de confirmation que nous venons de vous envoyer par email. Vous recevrez ensuite votre numéro de référence client.", successMsg: "Merci ! Votre inscription a bien été enregistrée. Un email de bienvenue avec votre numéro de référence client et l'adresse de dépôt à Paris vient de vous être envoyé.",
refLabel: "VOTRE NUMÉRO DE RÉFÉRENCE CLIENT", refLabel: "VOTRE NUMÉRO DE RÉFÉRENCE CLIENT",
successNote: "Conservez ce numéro précieusement — il vous sera utile pour suivre vos colis.", successNote: "Conservez ce numéro précieusement — il vous sera utile pour suivre vos colis.",
emailSent: "📧 Email envoyé — pensez aussi à vérifier vos spams.", emailSent: "📧 Vérifiez votre boîte mail (et vos spams).",
alreadyTitle: "Vous êtes déjà client !", alreadyTitle: "Vous êtes déjà client !",
alreadyMsg: "Votre adresse email est déjà enregistrée dans notre système.", alreadyMsg: "Votre adresse email est déjà enregistrée dans notre système.",
alreadyRefLabel: "VOTRE NUMÉRO DE RÉFÉRENCE EXISTANT", alreadyRefLabel: "VOTRE NUMÉRO DE RÉFÉRENCE EXISTANT",
@ -502,11 +502,11 @@ const translations = {
placeholderAdresse: "Full address...", placeholderAdresse: "Full address...",
submitBtn: "Register", submitBtn: "Register",
submitLoading: "Sending...", submitLoading: "Sending...",
successTitle: "Check your inbox!", successTitle: "Registration successful!",
successMsg: "To complete your registration, click the confirmation link we just sent to your email. You'll then receive your client reference number.", successMsg: "Thank you! Your registration has been recorded. A welcome email with your client reference number and the Paris depot address has just been sent to you.",
refLabel: "YOUR CLIENT REFERENCE NUMBER", refLabel: "YOUR CLIENT REFERENCE NUMBER",
successNote: "Keep this number safe — you'll need it to track your parcels.", successNote: "Keep this number safe — you'll need it to track your parcels.",
emailSent: "📧 Email sent — don't forget to check your spam folder.", emailSent: "📧 Check your inbox (and your spam folder).",
alreadyTitle: "You are already a client!", alreadyTitle: "You are already a client!",
alreadyMsg: "Your email address is already registered in our system.", alreadyMsg: "Your email address is already registered in our system.",
alreadyRefLabel: "YOUR EXISTING REFERENCE NUMBER", alreadyRefLabel: "YOUR EXISTING REFERENCE NUMBER",
@ -854,11 +854,11 @@ const translations = {
placeholderAdresse: "Adiresy feno...", placeholderAdresse: "Adiresy feno...",
submitBtn: "Hisoratra anarana", submitBtn: "Hisoratra anarana",
submitLoading: "Alefa...", submitLoading: "Alefa...",
successTitle: "Jereo ny boaty mailaka!", successTitle: "Vita ny fisoratana anarana!",
successMsg: "Mba hahafenitra ny fisoratana anarana, tsindrio ny rohy fanamafisana nalefa tao amin'ny mailakao. Avy eo dia handray ny laharanao mpanjifa ianao.", successMsg: "Misaotra! Voaray tsara ny fisoratana anaranareo. Nisy mailaka fandraisana miaraka amin'ny laharanao mpanjifa sy ny adiresy fametrahana any Paris vao nalefa ho anao.",
refLabel: "NY LAHARANAO MPANJIFA", refLabel: "NY LAHARANAO MPANJIFA",
successNote: "Tehirizo tsara ity laharana ity — ilaina amin'ny fanaraha-maso ny entanao.", successNote: "Tehirizo tsara ity laharana ity — ilaina amin'ny fanaraha-maso ny entanao.",
emailSent: "📧 Nalefa ny mailaka — jereo koa ao amin'ny spam.", emailSent: "📧 Jereo ny boaty mailaka (sy ny spam).",
alreadyTitle: "Efa mpanjifa ianao!", alreadyTitle: "Efa mpanjifa ianao!",
alreadyMsg: "Efa voasoratra ao amin'ny rafitra ny adiresy mailaka.", alreadyMsg: "Efa voasoratra ao amin'ny rafitra ny adiresy mailaka.",
alreadyRefLabel: "NY LAHARANAO EFA MISY", alreadyRefLabel: "NY LAHARANAO EFA MISY",