CognxSafeTrack Claude Sonnet 4.6 commited on
Commit ·
71969b1
1
Parent(s): 3d9b9c9
feat(bot): add EN/ES/PT language support in onboarding flow
Browse filesReplace 2-button interactive picker with 5-language interactive list
(FR, Wolof, EN, ES, PT). WhatsApp buttons are capped at 3, so the
list format is now used throughout the language selection step.
Adds getDefaultTrack() and getSectorConfirmMsg() helpers to map all
5 languages to their respective track IDs and localised confirmations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
apps/whatsapp-worker/src/handlers/OnboardingHandler.ts
CHANGED
|
@@ -5,6 +5,14 @@ import { logger } from '../logger';
|
|
| 5 |
import { clearTimeTravelContext } from '../timeTravelContext';
|
| 6 |
import { FlowConfigSchema } from '@repo/shared-types';
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
const LEGACY_SECTORS: Array<{ id: string; label: string }> = [
|
| 9 |
{ id: 'SEC_COMMERCE', label: 'Commerce / Vente' },
|
| 10 |
{ id: 'SEC_AGRI', label: 'Agriculture / Élevage' },
|
|
@@ -25,6 +33,22 @@ function getValidatedConfig(ctx: MessageContext) {
|
|
| 25 |
return parsed.data;
|
| 26 |
}
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
function getSectors(ctx: MessageContext): Array<{ id: string; label: string }> {
|
| 29 |
const config = getValidatedConfig(ctx);
|
| 30 |
return config?.sectors || LEGACY_SECTORS;
|
|
@@ -38,7 +62,7 @@ export class OnboardingHandler implements MessageHandler {
|
|
| 38 |
if (isFuzzyMatch(normalizedText, 'INSCRIPTION') || normalizedText.includes('INSCRI')) return true;
|
| 39 |
|
| 40 |
// 2. Language Selection
|
| 41 |
-
if (
|
| 42 |
|
| 43 |
// 3. Sector Selection (only if not active enrollment)
|
| 44 |
const sectors = getSectors(ctx);
|
|
@@ -62,16 +86,14 @@ export class OnboardingHandler implements MessageHandler {
|
|
| 62 |
if (user) {
|
| 63 |
// Guard: if user hasn't completed onboarding yet (no activity), skip hard reset
|
| 64 |
if (!user.activity) {
|
| 65 |
-
logger.info({ traceId, userId: user.id }, "INSCRIPTION during onboarding — resending language
|
| 66 |
-
|
| 67 |
-
await whatsappQueue.add('send-interactive-buttons', {
|
| 68 |
userId: user.id,
|
| 69 |
organizationId: ctx.organizationId,
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
]
|
| 75 |
});
|
| 76 |
return true;
|
| 77 |
}
|
|
@@ -96,14 +118,13 @@ export class OnboardingHandler implements MessageHandler {
|
|
| 96 |
bodyText = "Bienvenue ! Choisissez votre langue :";
|
| 97 |
}
|
| 98 |
|
| 99 |
-
await whatsappQueue.add('send-interactive-
|
| 100 |
userId: user.id,
|
| 101 |
organizationId: ctx.organizationId,
|
|
|
|
| 102 |
bodyText,
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
{ id: 'LANG_WO', title: 'Wolof 🇸🇳' }
|
| 106 |
-
]
|
| 107 |
});
|
| 108 |
return true;
|
| 109 |
}
|
|
@@ -111,32 +132,31 @@ export class OnboardingHandler implements MessageHandler {
|
|
| 111 |
if (!user) return false;
|
| 112 |
|
| 113 |
// --- 2. Language selection ---
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
const sectors = getSectors(ctx);
|
| 120 |
-
const rows = sectors.map((s) => ({
|
| 121 |
-
id: s.id,
|
| 122 |
-
title: s.label
|
| 123 |
-
}));
|
| 124 |
-
|
| 125 |
-
// Add fallback "Other" if less than 10 sectors (Meta limit is 10 rows per section)
|
| 126 |
if (rows.length < 10 && !rows.some(r => r.id === 'SEC_AUTRE')) {
|
| 127 |
-
rows.push({ id: 'SEC_AUTRE', title:
|
| 128 |
}
|
| 129 |
|
| 130 |
await whatsappQueue.add('send-interactive-list', {
|
| 131 |
userId: user.id,
|
| 132 |
organizationId: ctx.organizationId,
|
| 133 |
-
headerText:
|
| 134 |
-
bodyText:
|
| 135 |
-
buttonLabel:
|
| 136 |
-
sections: [{
|
| 137 |
-
title: newLang === 'FR' ? 'Liste' : 'Mbir',
|
| 138 |
-
rows
|
| 139 |
-
}]
|
| 140 |
});
|
| 141 |
return true;
|
| 142 |
}
|
|
@@ -147,11 +167,11 @@ export class OnboardingHandler implements MessageHandler {
|
|
| 147 |
if (sector || normalizedText.startsWith('SEC_')) {
|
| 148 |
const activity = sector ? sector.label : text.trim();
|
| 149 |
user = await prisma.user.update({ where: { id: user.id }, data: { activity } });
|
| 150 |
-
const msg = user.language
|
| 151 |
await whatsappQueue.add('send-message', { userId: user.id, organizationId: ctx.organizationId, text: msg });
|
| 152 |
|
| 153 |
const config = getValidatedConfig(ctx);
|
| 154 |
-
const trackId = config?.defaultTrackId || (user.language
|
| 155 |
|
| 156 |
await whatsappQueue.add('enroll-user', { userId: user.id, trackId, organizationId: ctx.organizationId });
|
| 157 |
return true;
|
|
@@ -160,12 +180,12 @@ export class OnboardingHandler implements MessageHandler {
|
|
| 160 |
// --- 4. Free-text Activity detection ---
|
| 161 |
if (!user.activity && text.length > 2) {
|
| 162 |
user = await prisma.user.update({ where: { id: user.id }, data: { activity: text.trim() } });
|
| 163 |
-
const msg = user.language
|
| 164 |
await whatsappQueue.add('send-message', { userId: user.id, organizationId: ctx.organizationId, text: msg });
|
| 165 |
-
|
| 166 |
const config = getValidatedConfig(ctx);
|
| 167 |
-
const trackId = config?.defaultTrackId || (user.language
|
| 168 |
-
|
| 169 |
await whatsappQueue.add('enroll-user', { userId: user.id, trackId, organizationId: ctx.organizationId });
|
| 170 |
return true;
|
| 171 |
}
|
|
|
|
| 5 |
import { clearTimeTravelContext } from '../timeTravelContext';
|
| 6 |
import { FlowConfigSchema } from '@repo/shared-types';
|
| 7 |
|
| 8 |
+
const LANGUAGE_ROWS = [
|
| 9 |
+
{ id: 'LANG_FR', title: 'Français 🇫🇷' },
|
| 10 |
+
{ id: 'LANG_WO', title: 'Wolof 🇸🇳' },
|
| 11 |
+
{ id: 'LANG_EN', title: 'English 🇬🇧' },
|
| 12 |
+
{ id: 'LANG_ES', title: 'Español 🇪🇸' },
|
| 13 |
+
{ id: 'LANG_PT', title: 'Português 🇧🇷' },
|
| 14 |
+
];
|
| 15 |
+
|
| 16 |
const LEGACY_SECTORS: Array<{ id: string; label: string }> = [
|
| 17 |
{ id: 'SEC_COMMERCE', label: 'Commerce / Vente' },
|
| 18 |
{ id: 'SEC_AGRI', label: 'Agriculture / Élevage' },
|
|
|
|
| 33 |
return parsed.data;
|
| 34 |
}
|
| 35 |
|
| 36 |
+
function getDefaultTrack(language: string): string {
|
| 37 |
+
const map: Record<string, string> = { FR: 'T1-FR', WOLOF: 'T1-WO', EN: 'T1-EN', ES: 'T1-ES', PT: 'T1-PT' };
|
| 38 |
+
return map[language] ?? 'T1-FR';
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
function getSectorConfirmMsg(language: string, activity: string): string {
|
| 42 |
+
const map: Record<string, string> = {
|
| 43 |
+
FR: `Secteur noté : *${activity}*`,
|
| 44 |
+
WOLOF: `Bind nanu la ci: *${activity}*`,
|
| 45 |
+
EN: `Sector noted: *${activity}*`,
|
| 46 |
+
ES: `Sector registrado: *${activity}*`,
|
| 47 |
+
PT: `Setor registrado: *${activity}*`,
|
| 48 |
+
};
|
| 49 |
+
return map[language] ?? `Secteur noté : *${activity}*`;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
function getSectors(ctx: MessageContext): Array<{ id: string; label: string }> {
|
| 53 |
const config = getValidatedConfig(ctx);
|
| 54 |
return config?.sectors || LEGACY_SECTORS;
|
|
|
|
| 62 |
if (isFuzzyMatch(normalizedText, 'INSCRIPTION') || normalizedText.includes('INSCRI')) return true;
|
| 63 |
|
| 64 |
// 2. Language Selection
|
| 65 |
+
if (['LANG_FR', 'LANG_WO', 'LANG_EN', 'LANG_ES', 'LANG_PT'].includes(normalizedText)) return true;
|
| 66 |
|
| 67 |
// 3. Sector Selection (only if not active enrollment)
|
| 68 |
const sectors = getSectors(ctx);
|
|
|
|
| 86 |
if (user) {
|
| 87 |
// Guard: if user hasn't completed onboarding yet (no activity), skip hard reset
|
| 88 |
if (!user.activity) {
|
| 89 |
+
logger.info({ traceId, userId: user.id }, "INSCRIPTION during onboarding — resending language list");
|
| 90 |
+
await whatsappQueue.add('send-interactive-list', {
|
|
|
|
| 91 |
userId: user.id,
|
| 92 |
organizationId: ctx.organizationId,
|
| 93 |
+
headerText: "XAMLÉ",
|
| 94 |
+
bodyText: "Choose your language / Choisissez votre langue / Tànnal sa làkk",
|
| 95 |
+
buttonLabel: "Language",
|
| 96 |
+
sections: [{ title: "Languages", rows: LANGUAGE_ROWS }]
|
|
|
|
| 97 |
});
|
| 98 |
return true;
|
| 99 |
}
|
|
|
|
| 118 |
bodyText = "Bienvenue ! Choisissez votre langue :";
|
| 119 |
}
|
| 120 |
|
| 121 |
+
await whatsappQueue.add('send-interactive-list', {
|
| 122 |
userId: user.id,
|
| 123 |
organizationId: ctx.organizationId,
|
| 124 |
+
headerText: "XAMLÉ",
|
| 125 |
bodyText,
|
| 126 |
+
buttonLabel: "Language",
|
| 127 |
+
sections: [{ title: "Languages", rows: LANGUAGE_ROWS }]
|
|
|
|
|
|
|
| 128 |
});
|
| 129 |
return true;
|
| 130 |
}
|
|
|
|
| 132 |
if (!user) return false;
|
| 133 |
|
| 134 |
// --- 2. Language selection ---
|
| 135 |
+
const LANG_MAP: Record<string, { lang: 'FR' | 'EN' | 'ES' | 'PT' | 'WOLOF'; sectorPrompt: string; sectorHeader: string; sectorButton: string; sectorOther: string; sectorSection: string }> = {
|
| 136 |
+
LANG_FR: { lang: 'FR', sectorPrompt: "Parfait ! Dans quel domaine travailles-tu ?", sectorHeader: "Ton secteur", sectorButton: "Secteurs", sectorOther: "Autre secteur", sectorSection: "Liste" },
|
| 137 |
+
LANG_WO: { lang: 'WOLOF', sectorPrompt: "Baax na ! Ci ban mbir ngay yëngu ?", sectorHeader: "Sa Mbir", sectorButton: "Tànn", sectorOther: "Beneen mbir", sectorSection: "Mbir" },
|
| 138 |
+
LANG_EN: { lang: 'EN', sectorPrompt: "Great! What sector do you work in?", sectorHeader: "Your sector", sectorButton: "Sectors", sectorOther: "Other sector", sectorSection: "List" },
|
| 139 |
+
LANG_ES: { lang: 'ES', sectorPrompt: "¡Perfecto! ¿En qué sector trabajas?", sectorHeader: "Tu sector", sectorButton: "Sectores", sectorOther: "Otro sector", sectorSection: "Lista" },
|
| 140 |
+
LANG_PT: { lang: 'PT', sectorPrompt: "Ótimo! Em que setor você trabalha?", sectorHeader: "Seu setor", sectorButton: "Setores", sectorOther: "Outro setor", sectorSection: "Lista" },
|
| 141 |
+
};
|
| 142 |
+
|
| 143 |
+
if (normalizedText in LANG_MAP) {
|
| 144 |
+
const { lang, sectorPrompt, sectorHeader, sectorButton, sectorOther, sectorSection } = LANG_MAP[normalizedText];
|
| 145 |
+
user = await prisma.user.update({ where: { id: user.id }, data: { language: lang } });
|
| 146 |
+
|
| 147 |
const sectors = getSectors(ctx);
|
| 148 |
+
const rows = sectors.map((s) => ({ id: s.id, title: s.label }));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
if (rows.length < 10 && !rows.some(r => r.id === 'SEC_AUTRE')) {
|
| 150 |
+
rows.push({ id: 'SEC_AUTRE', title: sectorOther });
|
| 151 |
}
|
| 152 |
|
| 153 |
await whatsappQueue.add('send-interactive-list', {
|
| 154 |
userId: user.id,
|
| 155 |
organizationId: ctx.organizationId,
|
| 156 |
+
headerText: sectorHeader,
|
| 157 |
+
bodyText: sectorPrompt,
|
| 158 |
+
buttonLabel: sectorButton,
|
| 159 |
+
sections: [{ title: sectorSection, rows }]
|
|
|
|
|
|
|
|
|
|
| 160 |
});
|
| 161 |
return true;
|
| 162 |
}
|
|
|
|
| 167 |
if (sector || normalizedText.startsWith('SEC_')) {
|
| 168 |
const activity = sector ? sector.label : text.trim();
|
| 169 |
user = await prisma.user.update({ where: { id: user.id }, data: { activity } });
|
| 170 |
+
const msg = getSectorConfirmMsg(user.language, activity);
|
| 171 |
await whatsappQueue.add('send-message', { userId: user.id, organizationId: ctx.organizationId, text: msg });
|
| 172 |
|
| 173 |
const config = getValidatedConfig(ctx);
|
| 174 |
+
const trackId = config?.defaultTrackId || getDefaultTrack(user.language);
|
| 175 |
|
| 176 |
await whatsappQueue.add('enroll-user', { userId: user.id, trackId, organizationId: ctx.organizationId });
|
| 177 |
return true;
|
|
|
|
| 180 |
// --- 4. Free-text Activity detection ---
|
| 181 |
if (!user.activity && text.length > 2) {
|
| 182 |
user = await prisma.user.update({ where: { id: user.id }, data: { activity: text.trim() } });
|
| 183 |
+
const msg = getSectorConfirmMsg(user.language, text.trim());
|
| 184 |
await whatsappQueue.add('send-message', { userId: user.id, organizationId: ctx.organizationId, text: msg });
|
| 185 |
+
|
| 186 |
const config = getValidatedConfig(ctx);
|
| 187 |
+
const trackId = config?.defaultTrackId || getDefaultTrack(user.language);
|
| 188 |
+
|
| 189 |
await whatsappQueue.add('enroll-user', { userId: user.id, trackId, organizationId: ctx.organizationId });
|
| 190 |
return true;
|
| 191 |
}
|