Worker: cron post-confirmation pour envoyer le welcome email après opt-in
Architecture finale (Option A choisie) :
1. User submit form → contact créé en HubSpot avec reference_client
2. HubSpot envoie l'email de double opt-in (sans la ref ni l'adresse Paris)
3. User clique 'Confirmer' → HubSpot met hs_emailconfirmationstatus = CONFIRMED
4. Cron Cloudflare (toutes les 5 min) :
- Liste les contacts CONFIRMED + créés après le cutoff
- Filtre via Cloudflare KV (welcomed:<email>) pour idempotence
- Envoie le welcome email via EmailJS REST API avec :
• firstname
• reference_client
• paris_address (depuis env var PARIS_DEPOT_ADDRESS)
- Marque envoyé dans KV avec TTL 1 an
Protection :
- L'adresse du dépôt Paris ne quitte JAMAIS Cloudflare/EmailJS
- Elle n'arrive au client que dans le mail de bienvenue post-opt-in
- Bots qui n'ont pas un vrai email ne peuvent pas valider → ne reçoivent rien
- Anti-spam et anti-cartons-vides blindé
Ajout d'une action 'triggerWelcomeQueue' pour debug/manual run.
Doc complète dans cloudflare-worker/DEPLOIEMENT.md (étapes 1 à 6).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f534376f90
commit
0ef9f01fd9
185
cloudflare-worker/DEPLOIEMENT.md
Normal file
185
cloudflare-worker/DEPLOIEMENT.md
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
# Déploiement Cloudflare Worker — Welcome cron MVA
|
||||||
|
|
||||||
|
Ce Worker fait deux choses :
|
||||||
|
1. **Proxy HubSpot** (déjà fonctionnel — vérification doublon + numéro de référence)
|
||||||
|
2. **Cron Welcome** (NOUVEAU — envoie l'email de bienvenue **après** que le client a cliqué sur "Confirmer" dans le mail HubSpot de double opt-in)
|
||||||
|
|
||||||
|
L'email de bienvenue contient le **numéro de référence client** ET (à ajouter dans le template EmailJS) **l'adresse du dépôt à Paris**. Ces infos ne sont **jamais** envoyées avant validation de l'email — protection contre les bots et les expéditions de cartons vides.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Étapes de déploiement (10 min)
|
||||||
|
|
||||||
|
### 1. Mettre à jour le code du Worker
|
||||||
|
|
||||||
|
1. Aller sur https://dash.cloudflare.com/
|
||||||
|
2. Workers & Pages → cliquer sur **`mva-hubspot-proxy`**
|
||||||
|
3. Onglet **Modifier le code** (ou *Quick edit*)
|
||||||
|
4. Tout sélectionner et remplacer par le contenu de `cloudflare-worker/hubspot-proxy.js` (de ce repo)
|
||||||
|
5. Cliquer **Déployer / Save and deploy**
|
||||||
|
|
||||||
|
### 2. Variables d'environnement
|
||||||
|
|
||||||
|
Dans **Paramètres → Variables et secrets**, ajouter (si pas déjà là) :
|
||||||
|
|
||||||
|
| Nom | Valeur |
|
||||||
|
|-----|--------|
|
||||||
|
| `HUBSPOT_TOKEN` | `pat-eu1-...` (existant — vérifier que le scope est read+write contacts) |
|
||||||
|
| `EMAILJS_PUBLIC_KEY` | `8KUlaQ7BDVIbkZRyP` |
|
||||||
|
| `EMAILJS_SERVICE_ID` | `service_aeamo3x` |
|
||||||
|
| `EMAILJS_TEMPLATE_ID` | `template_s1kr2et` |
|
||||||
|
| `PARIS_DEPOT_ADDRESS` | **Ton adresse exacte à Paris** (rue, code postal, ville, étage, nom à indiquer sur le carton…) |
|
||||||
|
|
||||||
|
⚠️ **`PARIS_DEPOT_ADDRESS`** est l'info la plus sensible — c'est l'adresse que tu veux protéger. Elle ne quitte jamais Cloudflare/EmailJS et n'arrive au client que dans le mail de bienvenue, qui n'est envoyé qu'aux contacts qui ont confirmé leur email.
|
||||||
|
|
||||||
|
> Si une variable n'est pas définie, le Worker utilise le fallback hardcodé.
|
||||||
|
|
||||||
|
### 3. Stockage KV (idempotence — empêche de spammer le welcome)
|
||||||
|
|
||||||
|
Dans **Paramètres → Stockage et bases de données → Bindings KV** :
|
||||||
|
|
||||||
|
1. Cliquer **Ajouter un binding**
|
||||||
|
2. Variable name : **`WELCOME_KV`**
|
||||||
|
3. Namespace : **Créer** un nouveau namespace nommé `mva-welcome-tracker`
|
||||||
|
4. Sauvegarder
|
||||||
|
|
||||||
|
### 4. Cron Trigger — toutes les 5 minutes
|
||||||
|
|
||||||
|
Dans **Paramètres → Déclencheurs (Triggers) → Cron Triggers** :
|
||||||
|
|
||||||
|
1. Cliquer **Add Cron Trigger**
|
||||||
|
2. Schedule : `*/5 * * * *`
|
||||||
|
3. Sauvegarder
|
||||||
|
|
||||||
|
### 5a. EmailJS — mettre à jour le template pour inclure l'adresse Paris
|
||||||
|
|
||||||
|
1. Aller sur https://dashboard.emailjs.com/admin/templates
|
||||||
|
2. Cliquer sur le template `template_s1kr2et`
|
||||||
|
3. Modifier le contenu pour inclure les variables suivantes :
|
||||||
|
- `{{firstname}}` — prénom du client
|
||||||
|
- `{{reference_client}}` — sa référence (ex : `MVA-001`)
|
||||||
|
- `{{paris_address}}` — l'adresse complète du dépôt à Paris
|
||||||
|
4. Exemple de corps d'email recommandé :
|
||||||
|
|
||||||
|
```
|
||||||
|
Bonjour {{firstname}},
|
||||||
|
|
||||||
|
Bienvenue chez MVA Global Fret ! Votre inscription est confirmée.
|
||||||
|
|
||||||
|
══════════════════════════════════════════════
|
||||||
|
VOTRE NUMÉRO DE RÉFÉRENCE CLIENT
|
||||||
|
{{reference_client}}
|
||||||
|
══════════════════════════════════════════════
|
||||||
|
|
||||||
|
Conservez précieusement ce numéro — il vous permet de suivre vos colis
|
||||||
|
et nous facilite la prise en charge.
|
||||||
|
|
||||||
|
📦 ADRESSE DU DÉPÔT À PARIS
|
||||||
|
|
||||||
|
{{paris_address}}
|
||||||
|
|
||||||
|
⚠️ Important : indiquez bien votre numéro de référence
|
||||||
|
({{reference_client}}) sur chaque colis que vous nous envoyez.
|
||||||
|
Cela nous permet de vous identifier rapidement.
|
||||||
|
|
||||||
|
Pour toute question, contactez-nous via Messenger ou par WhatsApp.
|
||||||
|
|
||||||
|
— L'équipe MVA Global Fret
|
||||||
|
+261 38 49 737 51
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Sauvegarder le template
|
||||||
|
|
||||||
|
### 5b. EmailJS — autoriser les appels serveur (CRITIQUE)
|
||||||
|
|
||||||
|
Sans cette étape, le Worker ne pourra pas envoyer les emails (erreur 403).
|
||||||
|
|
||||||
|
1. Aller sur https://dashboard.emailjs.com/admin/account
|
||||||
|
2. Onglet **Security**
|
||||||
|
3. Décocher **"Allow EmailJS API for non-browser applications"**
|
||||||
|
- Le terme est trompeur : décocher autorise les appels serveur
|
||||||
|
- (Cocher = bloquer les appels serveur pour anti-spam)
|
||||||
|
4. Sauvegarder
|
||||||
|
|
||||||
|
### 6. Vérifier le scope HubSpot
|
||||||
|
|
||||||
|
Le token HubSpot doit avoir :
|
||||||
|
- ✅ `crm.objects.contacts.read`
|
||||||
|
- ✅ `crm.objects.contacts.write` (NOUVEAU — pour mettre à jour la propriété en cas de besoin)
|
||||||
|
- ✅ `crm.lists.read` (déjà bon)
|
||||||
|
|
||||||
|
Si tu obtiens des erreurs 403, regénère le token sur https://app-eu1.hubspot.com/private-apps/148163754/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test manuel
|
||||||
|
|
||||||
|
Une fois tout déployé, tu peux forcer une exécution du cron sans attendre 5 min :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST https://mva-hubspot-proxy.mvaglobalfret.workers.dev \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"action":"triggerWelcomeQueue"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Réponse attendue :
|
||||||
|
```json
|
||||||
|
{ "ok": true, "stats": { "scanned": 3, "sent": 1, "skipped": 2, "errors": 0 } }
|
||||||
|
```
|
||||||
|
|
||||||
|
- `scanned` : combien de contacts confirmés ont été inspectés
|
||||||
|
- `sent` : combien d'emails de bienvenue ont été envoyés cette fois-ci
|
||||||
|
- `skipped` : déjà envoyés précédemment (KV tracking)
|
||||||
|
- `errors` : envois qui ont échoué
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Logs en production
|
||||||
|
|
||||||
|
Cloudflare → ton Worker → **Logs (en temps réel)** : tu verras chaque exécution cron avec les stats.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## En cas de problème
|
||||||
|
|
||||||
|
| Symptôme | Cause probable | Fix |
|
||||||
|
|---|---|---|
|
||||||
|
| `EmailJS 403: API access disabled` | Étape 5 pas faite | Décocher "Allow API for non-browser" sur EmailJS |
|
||||||
|
| `HubSpot 403` | Token n'a pas le scope write | Regénérer token avec scope contacts.write |
|
||||||
|
| `HubSpot 401` | Token invalide / expiré | Regénérer un Private App token |
|
||||||
|
| Le cron ne tourne pas | Cron trigger pas activé | Vérifier *Triggers → Cron Triggers* |
|
||||||
|
| KV: `env.WELCOME_KV is undefined` | Binding pas créé | Vérifier *Storage & Databases → KV bindings* |
|
||||||
|
| Aucun email envoyé même après confirmation | Le filtre HubSpot ne matche pas | Inspecter dans Logs : voir si `searchConfirmedContacts` retourne des résultats |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture finale
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 1. User remplit le formulaire sur contact.html │
|
||||||
|
│ ↓ │
|
||||||
|
│ 2. form-handler.js soumet à HubSpot (Forms API) │
|
||||||
|
│ ↓ │
|
||||||
|
│ 3. HubSpot crée le contact AVEC reference_client │
|
||||||
|
│ ↓ │
|
||||||
|
│ 4. HubSpot envoie l'email de double opt-in (sujet + bouton │
|
||||||
|
│ « Confirmer » seulement — PAS la référence ni l'adresse) │
|
||||||
|
│ ↓ │
|
||||||
|
│ ─────── User clique sur « Confirmer » ─────── │
|
||||||
|
│ ↓ │
|
||||||
|
│ 5. HubSpot met à jour hs_emailconfirmationstatus = CONFIRMED │
|
||||||
|
│ ↓ │
|
||||||
|
│ 6. Cron Cloudflare (toutes les 5 min) : │
|
||||||
|
│ - cherche les contacts CONFIRMED non encore welcomed │
|
||||||
|
│ - vérifie KV : welcomed:{email} │
|
||||||
|
│ - envoie email via EmailJS REST API │
|
||||||
|
│ (template contient : prénom, ref, adresse Paris) │
|
||||||
|
│ - écrit welcomed:{email} dans KV │
|
||||||
|
│ ↓ │
|
||||||
|
│ 7. Le client reçoit son welcome email avec sa référence ET │
|
||||||
|
│ l'adresse de dépôt à Paris. │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Conclusion** : il est désormais impossible pour un bot de récupérer ta référence client ou ton adresse Paris sans avoir au préalable un email valide ET cliqué sur le lien de confirmation. Anti-spam blindé. ✅
|
||||||
@ -1,57 +1,185 @@
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
// MVA Global Fret — Cloudflare Worker : Proxy HubSpot
|
// MVA Global Fret — Cloudflare Worker : Proxy HubSpot + Welcome cron
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Ce Worker contourne le CORS en faisant l'appel HubSpot
|
// Ce Worker fait deux choses :
|
||||||
// côté serveur (Cloudflare), puis renvoie le résultat
|
|
||||||
// au navigateur avec les bons en-têtes CORS.
|
|
||||||
//
|
//
|
||||||
// DÉPLOIEMENT (gratuit, sans CLI) :
|
// 1) Proxy HubSpot (via fetch handler, appelé par le navigateur)
|
||||||
// 1. Aller sur https://dash.cloudflare.com/ → Workers & Pages
|
// - Vérification doublon par email
|
||||||
// 2. Créer un Worker → coller ce code → Enregistrer et déployer
|
// - Génération du prochain numéro de référence séquentiel
|
||||||
// 3. Copier l'URL du Worker (ex: https://mva-proxy.xxx.workers.dev)
|
//
|
||||||
// 4. Coller cette URL dans js/form-handler.js à la constante WORKER_PROXY_URL
|
// 2) Cron post-confirmation (via scheduled handler, appelé par
|
||||||
|
// Cloudflare toutes les 5 min) :
|
||||||
|
// - Cherche les contacts qui ont CONFIRMÉ leur double opt-in
|
||||||
|
// - Pour chacun, envoie l'email de bienvenue (avec sa référence)
|
||||||
|
// - Marque le contact dans Cloudflare KV pour ne pas re-envoyer
|
||||||
|
//
|
||||||
|
// L'email de bienvenue n'arrive donc QU'APRÈS que le client a cliqué
|
||||||
|
// sur "Confirmer" dans le mail HubSpot. La référence client + l'adresse
|
||||||
|
// du dépôt à Paris ne fuitent jamais avant validation.
|
||||||
|
//
|
||||||
|
// ============================================================
|
||||||
|
// DÉPLOIEMENT
|
||||||
|
// ============================================================
|
||||||
|
//
|
||||||
|
// Sur https://dash.cloudflare.com/ :
|
||||||
|
//
|
||||||
|
// 1. Workers & Pages → ton Worker mva-hubspot-proxy → Modifier le code
|
||||||
|
// → coller ce fichier → Déployer
|
||||||
|
//
|
||||||
|
// 2. Workers & Pages → ton Worker → Paramètres → Variables et secrets :
|
||||||
|
// • HUBSPOT_TOKEN = pat-eu1-... (déjà existant, lecture+écriture contacts)
|
||||||
|
// • EMAILJS_PUBLIC_KEY = 8KUlaQ7BDVIbkZRyP
|
||||||
|
// • EMAILJS_SERVICE_ID = service_aeamo3x
|
||||||
|
// • EMAILJS_TEMPLATE_ID = template_s1kr2et
|
||||||
|
//
|
||||||
|
// 3. Workers & Pages → ton Worker → Paramètres → Stockage et bases
|
||||||
|
// → Bindings KV → Ajouter :
|
||||||
|
// Variable name : WELCOME_KV
|
||||||
|
// Namespace : créer "mva-welcome-tracker"
|
||||||
|
//
|
||||||
|
// 4. Workers & Pages → ton Worker → Paramètres → Déclencheurs (Triggers)
|
||||||
|
// → Cron Triggers → Ajouter :
|
||||||
|
// */5 * * * * (toutes les 5 minutes)
|
||||||
|
//
|
||||||
|
// 5. ⚠️ EmailJS : sur https://dashboard.emailjs.com/admin/account →
|
||||||
|
// Security → décocher "Allow EmailJS API for non-browser applications"
|
||||||
|
// → la décocher (= autoriser les appels serveur). Sinon le Worker ne
|
||||||
|
// pourra pas envoyer les emails.
|
||||||
|
//
|
||||||
|
// 6. ⚠️ Le token HubSpot doit avoir le scope crm.objects.contacts.write
|
||||||
|
// en plus du read (pour mettre à jour les propriétés contact).
|
||||||
|
// → si erreur 403 sur la mise à jour KV/contact, regénérer le token
|
||||||
|
// avec ce scope sur https://app-eu1.hubspot.com/private-apps/...
|
||||||
//
|
//
|
||||||
// SÉCURITÉ :
|
|
||||||
// - Stocker le token dans une variable d'environnement Cloudflare :
|
|
||||||
// Workers & Pages → Votre Worker → Paramètres → Variables et secrets
|
|
||||||
// Nom : HUBSPOT_TOKEN Valeur : pat-eu1-e3c92146-bb17-45fe-8d77-0c665fc4df3b
|
|
||||||
// - Ce token est en lecture seule (crm.objects.contacts.read uniquement)
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
export default {
|
// Fallbacks utilisés uniquement si les env vars ne sont pas définies dans Cloudflare
|
||||||
async fetch(request, env) {
|
const FALLBACK_TOKEN = 'pat-eu1-e3c92146-bb17-45fe-8d77-0c665fc4df3b';
|
||||||
|
const FALLBACK_EMAILJS_PUBLIC_KEY = '8KUlaQ7BDVIbkZRyP';
|
||||||
|
const FALLBACK_EMAILJS_SERVICE_ID = 'service_aeamo3x';
|
||||||
|
const FALLBACK_EMAILJS_TEMPLATE_ID= 'template_s1kr2et';
|
||||||
|
|
||||||
|
const HUBSPOT_API = 'https://api.hubapi.com';
|
||||||
|
const EMAILJS_API = 'https://api.emailjs.com/api/v1.0/email/send';
|
||||||
|
|
||||||
// En-têtes CORS autorisés
|
|
||||||
const corsHeaders = {
|
const corsHeaders = {
|
||||||
'Access-Control-Allow-Origin' : '*',
|
'Access-Control-Allow-Origin' : '*',
|
||||||
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
||||||
'Access-Control-Allow-Headers': 'Content-Type',
|
'Access-Control-Allow-Headers': 'Content-Type',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Réponse au preflight CORS (OPTIONS)
|
export default {
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
// 1) Handler navigateur (POST depuis le formulaire / page de confirmation)
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
async fetch(request, env) {
|
||||||
|
|
||||||
if (request.method === 'OPTIONS') {
|
if (request.method === 'OPTIONS') {
|
||||||
return new Response(null, { headers: corsHeaders });
|
return new Response(null, { headers: corsHeaders });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seule la méthode POST est acceptée
|
|
||||||
if (request.method !== 'POST') {
|
if (request.method !== 'POST') {
|
||||||
return new Response('Method Not Allowed', { status: 405 });
|
return new Response('Method Not Allowed', { status: 405 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lecture du token (variable d'env Cloudflare ou fallback hardcodé)
|
const token = env.HUBSPOT_TOKEN || FALLBACK_TOKEN;
|
||||||
const token = (env && env.HUBSPOT_TOKEN)
|
|
||||||
? env.HUBSPOT_TOKEN
|
|
||||||
: 'pat-eu1-e3c92146-bb17-45fe-8d77-0c665fc4df3b';
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { email, action } = body;
|
const { email, action } = body;
|
||||||
|
|
||||||
// ── ACTION : prochain numéro de référence séquentiel ──────────────────
|
// ── action: nextRef ─────────────────────────────────────
|
||||||
if (action === 'nextRef') {
|
if (action === 'nextRef') {
|
||||||
const hsResponse = await fetch(
|
return jsonResponse({ nextRef: await getNextRef(token) });
|
||||||
'https://api.hubapi.com/crm/v3/objects/contacts/search',
|
}
|
||||||
{
|
|
||||||
|
// ── action: triggerWelcome (admin/debug, optionnel) ─────
|
||||||
|
if (action === 'triggerWelcomeQueue') {
|
||||||
|
const stats = await processWelcomeQueue(env);
|
||||||
|
return jsonResponse({ ok: true, stats });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── action par défaut : vérification doublon par email ──
|
||||||
|
if (!email || typeof email !== 'string') {
|
||||||
|
return jsonResponse({ error: 'Email requis' }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await searchContactByEmail(token, email);
|
||||||
|
return jsonResponse(data);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
return jsonResponse({ error: err.message }, 500);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
// 2) Handler cron (Cloudflare scheduler, toutes les 5 min)
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
async scheduled(event, env, ctx) {
|
||||||
|
ctx.waitUntil(processWelcomeQueue(env));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// File d'attente : envoi du welcome aux contacts confirmés
|
||||||
|
// =============================================================
|
||||||
|
async function processWelcomeQueue(env) {
|
||||||
|
const token = env.HUBSPOT_TOKEN || FALLBACK_TOKEN;
|
||||||
|
const stats = { scanned: 0, sent: 0, skipped: 0, errors: 0 };
|
||||||
|
|
||||||
|
// Liste des contacts qui ont CONFIRMÉ leur opt-in marketing
|
||||||
|
// Filtre HubSpot : hs_emailconfirmationstatus EQ "CONFIRMED"
|
||||||
|
const confirmed = await searchConfirmedContacts(token);
|
||||||
|
|
||||||
|
for (const contact of confirmed) {
|
||||||
|
stats.scanned++;
|
||||||
|
const props = contact.properties || {};
|
||||||
|
const email = (props.email || '').toLowerCase();
|
||||||
|
|
||||||
|
if (!email) { stats.skipped++; continue; }
|
||||||
|
|
||||||
|
// Idempotence : si on a déjà envoyé, on saute
|
||||||
|
const kvKey = `welcomed:${email}`;
|
||||||
|
const already = env.WELCOME_KV ? await env.WELCOME_KV.get(kvKey) : null;
|
||||||
|
if (already) { stats.skipped++; continue; }
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sendWelcomeEmail(env, {
|
||||||
|
firstname : props.firstname || '',
|
||||||
|
email : email,
|
||||||
|
reference_client : props.reference_client || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Marquer comme envoyé dans KV (TTL 1 an pour éviter de garder
|
||||||
|
// indéfiniment des entrées si quelqu'un se désabonne et se réabonne)
|
||||||
|
if (env.WELCOME_KV) {
|
||||||
|
await env.WELCOME_KV.put(kvKey, new Date().toISOString(), {
|
||||||
|
expirationTtl: 60 * 60 * 24 * 365,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
stats.sent++;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
stats.errors++;
|
||||||
|
console.warn('[welcome]', email, err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// HubSpot : recherches & lectures
|
||||||
|
// =============================================================
|
||||||
|
|
||||||
|
// Date avant laquelle les contacts CONFIRMÉS ne déclenchent PAS de welcome.
|
||||||
|
// Évite de spammer les contacts déjà existants au moment du déploiement
|
||||||
|
// du nouveau cron. Tout contact créé APRÈS cette date (et qui confirme
|
||||||
|
// son email) recevra son email de bienvenue normalement.
|
||||||
|
const WELCOME_CUTOFF_ISO = '2026-05-05T00:00:00Z';
|
||||||
|
|
||||||
|
async function searchConfirmedContacts(token) {
|
||||||
|
// On limite à 100 contacts par cron run (largement suffisant pour 1 PME)
|
||||||
|
const res = await fetch(`${HUBSPOT_API}/crm/v3/objects/contacts/search`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -59,88 +187,112 @@ export default {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
filterGroups: [{
|
filterGroups: [{
|
||||||
filters: [{
|
filters: [
|
||||||
propertyName: 'reference_client',
|
{ propertyName: 'hs_emailconfirmationstatus', operator: 'EQ', value: 'CONFIRMED' },
|
||||||
operator: 'HAS_PROPERTY',
|
{ propertyName: 'reference_client', operator: 'HAS_PROPERTY' },
|
||||||
value: '',
|
{ propertyName: 'createdate', operator: 'GTE', value: WELCOME_CUTOFF_ISO },
|
||||||
}]
|
],
|
||||||
|
}],
|
||||||
|
properties: ['firstname', 'lastname', 'email', 'reference_client', 'hs_emailconfirmationstatus'],
|
||||||
|
sorts: [{ propertyName: 'lastmodifieddate', direction: 'DESCENDING' }],
|
||||||
|
limit: 100,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`HubSpot search failed: ${res.status}`);
|
||||||
|
}
|
||||||
|
const data = await res.json();
|
||||||
|
return data.results || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchContactByEmail(token, email) {
|
||||||
|
const res = await fetch(`${HUBSPOT_API}/crm/v3/objects/contacts/search`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
filterGroups: [{
|
||||||
|
filters: [{ propertyName: 'email', operator: 'EQ', value: email.toLowerCase().trim() }],
|
||||||
|
}],
|
||||||
|
properties: ['firstname', 'lastname', 'email', 'reference_client'],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`HubSpot lookup failed: ${res.status}`);
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getNextRef(token) {
|
||||||
|
const res = await fetch(`${HUBSPOT_API}/crm/v3/objects/contacts/search`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
filterGroups: [{
|
||||||
|
filters: [{ propertyName: 'reference_client', operator: 'HAS_PROPERTY', value: '' }],
|
||||||
}],
|
}],
|
||||||
properties: ['reference_client'],
|
properties: ['reference_client'],
|
||||||
limit: 100,
|
limit: 100,
|
||||||
}),
|
}),
|
||||||
}
|
});
|
||||||
);
|
const data = await res.json();
|
||||||
|
|
||||||
const data = await hsResponse.json();
|
|
||||||
let maxNum = 0;
|
let maxNum = 0;
|
||||||
|
(data.results || []).forEach(c => {
|
||||||
if (data.results) {
|
const m = (c.properties?.reference_client || '').match(/^MVA-(\d+)$/);
|
||||||
data.results.forEach(contact => {
|
if (m) {
|
||||||
const ref = contact.properties?.reference_client || '';
|
const n = parseInt(m[1], 10);
|
||||||
const match = ref.match(/^MVA-(\d+)$/);
|
if (n > maxNum) maxNum = n;
|
||||||
if (match) {
|
|
||||||
const num = parseInt(match[1], 10);
|
|
||||||
if (num > maxNum) maxNum = num;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
return 'MVA-' + String(maxNum + 1).padStart(3, '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextNum = maxNum + 1;
|
// =============================================================
|
||||||
const nextRef = 'MVA-' + String(nextNum).padStart(3, '0');
|
// EmailJS : envoi serveur via REST API
|
||||||
|
// =============================================================
|
||||||
|
|
||||||
return new Response(JSON.stringify({ nextRef }), {
|
async function sendWelcomeEmail(env, params) {
|
||||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
const payload = {
|
||||||
});
|
service_id : env.EMAILJS_SERVICE_ID || FALLBACK_EMAILJS_SERVICE_ID,
|
||||||
}
|
template_id: env.EMAILJS_TEMPLATE_ID || FALLBACK_EMAILJS_TEMPLATE_ID,
|
||||||
|
user_id : env.EMAILJS_PUBLIC_KEY || FALLBACK_EMAILJS_PUBLIC_KEY,
|
||||||
// ── ACTION : vérification doublon par email (défaut) ──────────────────
|
template_params: {
|
||||||
if (!email || typeof email !== 'string') {
|
firstname : params.firstname,
|
||||||
return new Response(
|
email : params.email,
|
||||||
JSON.stringify({ error: 'Email requis' }),
|
reference_client : params.reference_client,
|
||||||
{ status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
// Adresse du dépôt Paris — définie via l'env var PARIS_DEPOT_ADDRESS
|
||||||
);
|
// dans Cloudflare. Si non définie, on envoie un placeholder visible
|
||||||
}
|
// pour signaler à l'admin qu'il faut la configurer.
|
||||||
|
paris_address : env.PARIS_DEPOT_ADDRESS || '[À configurer dans Cloudflare]',
|
||||||
// Appel HubSpot CRM (pas de CORS côté Worker)
|
|
||||||
const hsResponse = await fetch(
|
|
||||||
'https://api.hubapi.com/crm/v3/objects/contacts/search',
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
filterGroups: [{
|
|
||||||
filters: [{
|
|
||||||
propertyName: 'email',
|
|
||||||
operator: 'EQ',
|
|
||||||
value: email.toLowerCase().trim(),
|
|
||||||
}]
|
|
||||||
}],
|
|
||||||
properties: ['firstname', 'lastname', 'email', 'reference_client'],
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!hsResponse.ok) {
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({ error: `HubSpot error: ${hsResponse.status}` }),
|
|
||||||
{ status: 502, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await hsResponse.json();
|
|
||||||
|
|
||||||
return new Response(JSON.stringify(data), {
|
|
||||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({ error: err.message }),
|
|
||||||
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const res = await fetch(EMAILJS_API, {
|
||||||
|
method : 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body : JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const text = await res.text();
|
||||||
|
throw new Error(`EmailJS ${res.status}: ${text}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// Helpers
|
||||||
|
// =============================================================
|
||||||
|
|
||||||
|
function jsonResponse(data, status = 200) {
|
||||||
|
return new Response(JSON.stringify(data), {
|
||||||
|
status,
|
||||||
|
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user