File size: 35,190 Bytes
cc442ef
d978795
cc442ef
 
8927585
 
 
 
 
 
 
 
31e6d9a
 
 
 
 
 
 
 
 
 
2d900bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b073e6a
 
8927585
b073e6a
cc442ef
 
 
 
 
77c96f8
5443165
 
 
1dec751
 
42c5945
1dec751
 
 
 
 
cc442ef
 
5443165
 
 
 
 
 
 
cc442ef
 
 
 
abc4e24
 
 
 
 
b073e6a
abc4e24
 
 
 
d9879cf
 
abc4e24
 
cd75882
2d900bc
d978795
 
cd75882
 
854ab32
cfe6413
854ab32
 
cd75882
 
 
 
1dec751
 
 
 
 
 
 
cd75882
 
 
42c5945
 
 
 
 
 
 
 
 
19d10a4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77c96f8
 
 
e6d84cb
 
77c96f8
e6d84cb
 
77c96f8
 
 
2d900bc
fe2f79f
 
cd75882
84be762
 
fe2f79f
 
4967196
 
 
 
 
 
d9879cf
 
4967196
 
fe2f79f
4967196
 
fe2f79f
d9879cf
 
 
cd75882
 
 
 
9cc0a90
abc4e24
 
9cc0a90
 
 
8549297
d978795
9cc0a90
 
 
 
d978795
 
 
a4ce760
 
 
 
9cc0a90
 
 
a4ce760
 
 
9cc0a90
 
 
 
abc4e24
9cc0a90
 
 
abc4e24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0e83f45
 
 
4967196
0e83f45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9cc0a90
 
f0f0df4
 
 
 
 
 
 
 
 
 
 
 
 
9ca5873
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f0f0df4
 
9cc0a90
8cd83e2
4a79a6d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5746532
 
 
4a79a6d
22341b3
 
 
 
 
 
f41372b
 
 
 
 
22341b3
1dec751
 
4a79a6d
1dec751
3c6fc2a
 
1dec751
3c6fc2a
 
 
1dec751
 
3c6fc2a
 
 
567fb57
 
4a79a6d
3c6fc2a
 
 
4a79a6d
cc442ef
 
 
 
 
 
31e6d9a
2d900bc
 
4a79a6d
 
8549297
d978795
 
 
42c5945
 
 
 
c1cf9c6
 
 
 
 
 
 
 
42c5945
4a79a6d
42c5945
 
 
 
 
3c5c1e0
26c5d48
 
 
5746532
4a79a6d
42c5945
 
ab37938
 
 
 
42c5945
4a79a6d
5746532
 
 
ab37938
 
 
 
 
 
c1cf9c6
 
 
 
 
ab37938
c1cf9c6
 
 
 
 
 
 
 
 
 
 
 
 
 
ab37938
 
 
 
 
 
 
 
 
 
 
 
 
 
4a79a6d
 
 
 
 
 
 
 
 
dced83b
4a79a6d
 
 
 
 
 
dced83b
4a79a6d
77c96f8
 
 
 
 
 
 
 
d978795
 
 
 
 
 
 
 
a66a580
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4a79a6d
ab37938
31e6d9a
 
4a79a6d
ab37938
 
4a79a6d
d978795
31e6d9a
4a79a6d
 
e6d84cb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4a79a6d
3c5c1e0
ab37938
 
 
 
 
 
 
 
 
 
d978795
ab37938
 
 
 
d978795
ab37938
 
 
 
3c5c1e0
 
 
 
 
 
 
 
4a79a6d
 
 
 
d978795
 
3c5c1e0
 
 
ab37938
 
b073e6a
d978795
 
 
4a79a6d
 
 
 
 
aa52c48
d978795
aa52c48
 
 
 
d978795
aa52c48
 
 
4a79a6d
aa52c48
d978795
4a79a6d
aa52c48
2d900bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3c5c1e0
 
 
 
 
 
 
 
aa52c48
 
 
 
d978795
 
 
3c5c1e0
 
 
b073e6a
d978795
 
 
aa52c48
 
 
 
 
 
 
 
cc442ef
 
 
aa52c48
 
1a66683
31e6d9a
 
1a66683
cc442ef
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
import { prisma } from './prisma';
import { scheduleMessage, enrollUser, whatsappQueue, scheduleInteractiveButtons, scheduleInteractiveList, setTimeTravelContext, getTimeTravelContext, clearTimeTravelContext } from './queue';

export class WhatsAppService {
    private static normalizeCommand(text: string): string {
        return text
            .trim()
            .toLowerCase()
            .replace(/[.,!?;:]+$/, "") // Remove trailing punctuation
            .toUpperCase();
    }

    private static detectIntent(text: string): 'YES' | 'NO' | 'UNKNOWN' {
        const normalized = text.trim().toLowerCase().replace(/[.,!?;:]+$/, "");
        const yesWords = ['oui', 'ouais', 'wi', 'waaw', 'yes', 'yep', 'ok', 'd’accord', 'daccord', 'da’accord'];
        const noWords = ['non', 'déet', 'deet', 'no', 'nah', 'nein'];

        if (yesWords.some(w => normalized.includes(w))) return 'YES';
        if (noWords.some(w => normalized.includes(w))) return 'NO';
        return 'UNKNOWN';
    }

    private static levenshteinDistance(a: string, b: string): number {
        const matrix: number[][] = [];
        for (let i = 0; i <= b.length; i++) matrix[i] = [i];
        for (let j = 0; j <= a.length; j++) matrix[0][j] = j;

        for (let i = 1; i <= b.length; i++) {
            for (let j = 1; j <= a.length; j++) {
                if (b.charAt(i - 1) === a.charAt(j - 1)) {
                    matrix[i][j] = matrix[i - 1][j - 1];
                } else {
                    matrix[i][j] = Math.min(
                        matrix[i - 1][j - 1] + 1, // substitution
                        matrix[i][j - 1] + 1,     // insertion
                        matrix[i - 1][j] + 1      // deletion
                    );
                }
            }
        }
        return matrix[b.length][a.length];
    }

    private static isFuzzyMatch(text: string, target: string, threshold = 0.8): boolean {
        const normalized = text.trim().toUpperCase();
        const tar = target.toUpperCase();
        if (normalized === tar) return true;
        if (normalized.includes(tar) || tar.includes(normalized)) return true;

        const distance = this.levenshteinDistance(normalized, tar);
        const maxLength = Math.max(normalized.length, tar.length);
        const similarity = 1 - distance / maxLength;
        return similarity >= threshold;
    }

    static async handleIncomingMessage(phone: string, text: string, audioUrl?: string, imageUrl?: string) {
        const traceId = audioUrl ? `[STT-FLOW-${phone.slice(-4)}]` : imageUrl ? `[IMG-FLOW-${phone.slice(-4)}]` : `[TXT-FLOW-${phone.slice(-4)}]`;
        const normalizedText = this.normalizeCommand(text);
        console.log(`${traceId} Received: ${normalizedText} (Audio: ${audioUrl || 'N/A'}, Image: ${imageUrl || 'N/A'})`);

        // 1. Find or Create User
        let user = await prisma.user.findUnique({ where: { phone } });

        if (!user) {
            const isInscription = this.isFuzzyMatch(normalizedText, 'INSCRIPTION') || normalizedText.includes('INSCRI') || normalizedText.includes('INSCRI');

            if (isInscription) {
                console.log(`${traceId} New user registration triggered for ${phone}`);
                user = await prisma.user.create({ data: { phone } });
                await scheduleInteractiveButtons(user.id,
                    "Dalal jàmm! Xamle ngay tàmbali. ⏳ 30s.\n(FR) Ton cours se prépare (30s).",
                    [
                        { id: 'LANG_FR', title: 'Français 🇫🇷' },
                        { id: 'LANG_WO', title: 'Wolof 🇸🇳' }
                    ]
                );
                return;
            } else {
                console.log(`${traceId} Unregistered user ${phone} sent: "${normalizedText}". Sending instructions.`);
                // Anti-silence: Nudge them to register
                const { whatsappQueue } = await import('./queue');
                await whatsappQueue.add('send-message-direct', {
                    phone,
                    text: "🎓 Bienvenue chez XAMLÉ !\nPour commencer ta formation gratuite, envoie le mot : *INSCRIPTION*\n\n(WO) Dalal jàmm ! Ngir tàmbali sa njàng mburu, bindal : *INSCRIPTION*"
                });
                return;
            }
        }

        // 1.2 Log the incoming message in the DB
        try {
            await prisma.message.create({
                data: {
                    content: text,
                    mediaUrl: audioUrl || imageUrl,
                    direction: 'INBOUND',
                    userId: user.id
                }
            });
        } catch (err: unknown) {
            console.error('[WhatsAppService] Failed to log incoming message:', (err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err)));
        }

        // 1.5. Testing / Cheat Codes (Only for registered users)
        if (this.isFuzzyMatch(normalizedText, 'INSCRIPTION')) {
            // 🚨 COUPE-CIRCUIT #1: Kill any active Time-Travel context BEFORE resetting progression
            await clearTimeTravelContext(user.id);
            await prisma.enrollment.deleteMany({ where: { userId: user.id } });
            await prisma.userProgress.deleteMany({ where: { userId: user.id } });
            await prisma.response.deleteMany({ where: { userId: user.id } });
            await prisma.message.deleteMany({ where: { userId: user.id } }); // Purge totale historique des messages
            // Also explicitly clear business AI profile to prevent context leak on restart
            await (prisma as any).businessProfile.deleteMany({ where: { userId: user.id } });
            user = await prisma.user.update({
                where: { id: user.id },
                data: { city: null, activity: null }
            });
            await scheduleInteractiveButtons(user.id,
                "Réinitialisation réussie – choisissez votre langue / Tànnal sa làkk :",
                [
                    { id: 'LANG_FR', title: 'Français 🇫🇷' },
                    { id: 'LANG_WO', title: 'Wolof 🇸🇳' }
                ]
            );
            return;
        }

        if (normalizedText === 'TEST_IMAGE') {
            await whatsappQueue.add('send-image', {
                to: user.phone,
                imageUrl: 'https://r2.xamle.sn/branding/branding_xamle.png',
                caption: 'Branding XAMLÉ - Industrialisation 2026'
            });
            return;
        }

        if (normalizedText.startsWith('TEST_VIDEO')) {
            const parts = normalizedText.split(' ');
            if (parts.length < 3) {
                await scheduleMessage(user.id, "Usage: TEST_VIDEO <TrackId> <DayNumber>");
                return;
            }
            const trackId = parts[1];
            const dayNumber = parseFloat(parts[2]);

            await scheduleMessage(user.id, `🧪 Test Video pour ${trackId} J${dayNumber}...`);
            await whatsappQueue.add('send-content', {
                userId: user.id,
                trackId,
                dayNumber
            });
            return;
        }

        const systemCommands = ['1', '2', 'SUITE', 'APPROFONDIR', 'INSCRIPTION', 'SEED'];
        const isSystemCommand = systemCommands.some(cmd => this.isFuzzyMatch(normalizedText, cmd)) || normalizedText.includes('INSCRI');

        // 🚨 Guardrail "Gibberish" Lite (Global)
        if (text.length < 2 && !isSystemCommand) {
             await scheduleMessage(user.id, user.language === 'WOLOF'
                ? "Dama lay xaar nga wax ma lu gën a yaatu ci sa mbir. Waxtaanal ak man !"
                : "Je n'ai pas bien compris. Peux-tu me réexpliquer en quelques mots ?");
             return;
        }

        if (this.isFuzzyMatch(normalizedText, 'SEED')) {
            // Reply immediately so the webhook doesn't time out
            console.log(`[SEED] Triggered by user ${user.id}`);
            try {
                // @ts-ignore - dynamic import of sub-module
                const { seedDatabase } = await import('@repo/database/seed');
                const result = await seedDatabase(prisma);
                console.log('[SEED] Result:', result.message);

                // 🚨 COGNITIVE CACHE CLEAR: Delete old BusinessProfile contexts to prevent agricultural hallucinations
                try {
                    await (prisma as any).businessProfile.deleteMany({ where: { userId: user.id } });
                    await prisma.user.update({ where: { id: user.id }, data: { activity: null } });
                    console.log(`[SEED] Cleared cognitive cache for User ${user.id}`);
                } catch (cacheErr: unknown) {
                    console.error('[SEED] Failed to clear cognitive cache:', (cacheErr as Error).message);
                }

                await scheduleMessage(user.id, result.seeded
                    ? "✅ Seeding terminé ! Le Cache Cognitif a été réinitialisé.\nEnvoie INSCRIPTION pour commencer."
                    : "ℹ️ Les données existent déjà. Cache Cognitif purgé. Envoie INSCRIPTION."
                );
            } catch (err: unknown) {
                console.error('[SEED] Error:', (err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err)));
                await scheduleMessage(user.id, `❌ Erreur seed : ${(err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err))?.substring(0, 200)}`);
            }
            return;
        }

        // ─── Interactive LIST action IDs ──────────────────────────────────────
        // Format: DAY{N}_EXERCISE | DAY{N}_REPLAY | DAY{N}_CONTINUE | DAY{N}_PROMPT
        const dayActionMatch = normalizedText.match(/^DAY(\d+)_(EXERCISE|REPLAY|CONTINUE|PROMPT)$/);
        if (dayActionMatch) {
            const action = dayActionMatch[2];

            if (action === 'REPLAY') {
                const replayDay = parseFloat(dayActionMatch[1]);  // DAY11_REPLAY → 11
                const enrollment = await prisma.enrollment.findFirst({
                    where: { userId: user.id, status: 'ACTIVE' }
                });
                if (enrollment) {
                    // 🕰️ TIME-TRAVEL: Persist the replay context in Redis (30 min TTL)
                    await setTimeTravelContext(user.id, replayDay);
                    // ✅ UX: Confirmation FIRST, content delayed — message order guaranteed
                    await scheduleMessage(user.id, user.language === 'WOLOF'
                        ? `🔁 Dinanu la yëgël lexon Bés ${Math.floor(replayDay)} ci kanam...`
                        : `🔁 Je te renvoie la Leçon ${Math.floor(replayDay)} dans quelques secondes...`
                    );
                    await whatsappQueue.add('send-content', {
                        userId: user.id,
                        trackId: enrollment.trackId,
                        dayNumber: replayDay,
                        skipProgressUpdate: true
                    }, { delay: 2000 });
                }
                return;
            } else if (action === 'EXERCISE') {
                await scheduleMessage(user.id, user.language === 'WOLOF'
                    ? "🎙️ Yónnee sa tontu (audio walla bind) :"
                    : "🎙️ Envoie ta réponse (audio ou texte) :"
                );
                return;
            } else if (action === 'PROMPT') {
                const enrollment = await prisma.enrollment.findFirst({
                    where: { userId: user.id, status: 'ACTIVE' }
                });
                if (enrollment) {
                    const trackDay = await prisma.trackDay.findFirst({
                        where: { trackId: enrollment.trackId, dayNumber: enrollment.currentDay }
                    });
                    if (trackDay?.exercisePrompt) {
                        await scheduleMessage(user.id, trackDay.exercisePrompt);
                    } else {
                        await scheduleMessage(user.id, user.language === 'WOLOF' ? "Amul lëjj" : "Pas d'exercice pour ce jour");
                    }
                }
                return;
            } else if (action === 'CONTINUE') {
                // Determine if there is a pending exercise before advancing
                const pendingProgress = await prisma.userProgress.findFirst({
                    where: { userId: user.id, exerciseStatus: { in: ['PENDING', 'PENDING_REMEDIATION'] } }
                });
                if (pendingProgress) {
                    await scheduleMessage(user.id, user.language === 'WOLOF'
                        ? "Danga wara tontu lëjj bi balaa nga dem ci kanam. Tànnal 'Yónni tontu'."
                        : "Tu dois d'abord répondre à l'exercice avant de continuer. Choisis 'Faire l'exercice' ou 'Répondre'."
                    );
                } else {
                    // Safe to advance (either completed or dropped or already handled)
                    await scheduleMessage(user.id, user.language === 'WOLOF'
                        ? "Waaw, ñuy dem ci kanam !"
                        : "C'est noté, on avance !"
                    );
                    // To do: if advance needs to trigger scheduleTrackDay directly, it could be done here instead of tracking. 
                    // However, normally `SUITE` moves the day forward. 
                }
                return;
            }
        }

        // 1.7. Language Selection (Interactive Buttons)
        if (normalizedText === 'LANG_FR' || normalizedText === 'LANG_WO') {
            const newLang = normalizedText === 'LANG_FR' ? 'FR' : 'WOLOF';
            user = await prisma.user.update({
                where: { id: user.id },
                data: { language: newLang }
            });

            const promptText = newLang === 'FR'
                ? "Parfait, nous allons continuer en Français ! 🇫🇷\nDans quel domaine d'activité te trouves-tu ?"
                : "Baax na, dinanu wéy ci Wolof ! 🇸🇳\nCi ban mbir ngay yëngu ?";

            await scheduleInteractiveList(
                user.id,
                newLang === 'FR' ? "Ton secteur" : "Sa Mbir",
                promptText,
                newLang === 'FR' ? "Secteurs" : "Tànn",
                [{
                    title: newLang === 'FR' ? 'Liste' : 'Mbir',
                    rows: [
                        { id: 'SEC_COMMERCE', title: newLang === 'FR' ? 'Commerce / Vente' : 'Njaay' },
                        { id: 'SEC_AGRI', title: newLang === 'FR' ? 'Agri / Élevage' : 'Mbay / Samm' },
                        { id: 'SEC_FOOD', title: newLang === 'FR' ? 'Alimentation / Rest.' : 'Lekk / Restauration' },
                        { id: 'SEC_COUTURE', title: newLang === 'FR' ? 'Couture / Mode' : 'Couture' },
                        { id: 'SEC_BEAUTE', title: newLang === 'FR' ? 'Beauté / Bien-être' : 'Rafet' },
                        { id: 'SEC_TRANSPORT', title: newLang === 'FR' ? 'Transport / Livr.' : 'Transport / Yëgël' },
                        { id: 'SEC_TECH', title: newLang === 'FR' ? 'Tech / Digital' : 'Tech / Digital' },
                        { id: 'SEC_AUTRE', title: newLang === 'FR' ? 'Autre secteur' : 'Beneen mbir' }
                    ]
                }]
            );
            return;
        }

        // 2. Check Pending Exercise (User Progress)
        // 2. Resolve sector LIST reply IDs → human-readable label
        const SECTOR_LABELS: Record<string, { fr: string; wo: string }> = {
            SEC_COMMERCE: { fr: 'Commerce / Vente', wo: 'Njaay' },
            SEC_AGRI: { fr: 'Agriculture / Élevage', wo: 'Mbay' },
            SEC_FOOD: { fr: 'Alimentation / Restauration', wo: 'Lekk / Restauration' },
            SEC_TECH: { fr: 'Tech / Digital', wo: 'Tech / Digital' },
            SEC_BEAUTE: { fr: 'Beauté / Bien-être', wo: 'Rafet' },
            SEC_COUTURE: { fr: 'Couture / Mode', wo: 'Couture' },
            SEC_TRANSPORT: { fr: 'Transport / Livraison', wo: 'Transport / Yëgël' },
        };

        if (normalizedText === 'SEC_AUTRE') {
            await scheduleMessage(user.id, user.language === 'WOLOF'
                ? 'Waaw ! Wax ma ban mbir ngay def ci ab kàddu gatt :'
                : 'Parfait ! Décris ton activité en quelques mots :'
            );
            return;
        }

        const sectorLabel = SECTOR_LABELS[normalizedText];

        // 🚨 Brique 1 (Immuabilité) : Vérifier si l'utilisateur est déjà inscrit.
        const existingEnrollment = await prisma.enrollment.findFirst({
            where: { userId: user.id, status: 'ACTIVE' }
        });

        if (existingEnrollment && (sectorLabel || normalizedText.startsWith('SEC_'))) {
            console.log(`[IMMUTABILITY] User ${user.id} tried to change sector but is already enrolled.`);
            return; // Ignore and do not allow re-routing here
        }

        if (!existingEnrollment && (sectorLabel || (!user.activity && text.length > 2))) {
            const activity = sectorLabel
                ? (user.language === 'WOLOF' ? sectorLabel.wo : sectorLabel.fr)
                : text.trim();

            user = await prisma.user.update({
                where: { id: user.id },
                data: { activity }
            });

            const welcomeMsg = user.language === 'FR'
                ? `Parfait ! Secteur noté : *${activity}*.\nJe t'inscris à ta formation personnalisée !`
                : `Baax na ! Bind nanu la ci: *${activity}*.\nLéegi dinanu la dugal ci njàng mi !`;

            await scheduleMessage(user.id, welcomeMsg);

            const trackId = user.language === 'FR' ? "T1-FR" : "T1-WO";
            const defaultTrack = await prisma.track.findUnique({ where: { id: trackId } });
            if (defaultTrack) await enrollUser(user.id, defaultTrack.id);
            return;
        }

        // 3. Check Active Enrollment (Commands Priority)
        const activeEnrollment = await prisma.enrollment.findFirst({
            where: { userId: user.id, status: 'ACTIVE' },
            include: { track: true }
        });

        if (activeEnrollment) {
            const intent = this.detectIntent(text);
            const isSuite = this.isFuzzyMatch(normalizedText, 'SUITE') || normalizedText === '2';
            const isApprofondir = this.isFuzzyMatch(normalizedText, 'APPROFONDIR') || normalizedText === '1';

            // Handle SUITE Priority
            if (isSuite) {
                // 🚨 COUPE-CIRCUIT #2: Kill Time-Travel context BEFORE any progression logic
                await clearTimeTravelContext(user.id);

                const userProgress = await prisma.userProgress.findUnique({
                    where: { userId_trackId: { userId: user.id, trackId: activeEnrollment.trackId } }
                });

                // 🚨 UNBLOCKING GUARD: Allow SUITE if a response has been recorded for the current day OR if status is valid.
                const lastResponse = await prisma.response.findFirst({
                    where: { userId: user.id, dayNumber: activeEnrollment.currentDay },
                    orderBy: { createdAt: 'desc' }
                });

                if (userProgress?.exerciseStatus !== 'COMPLETED' && userProgress?.exerciseStatus !== 'PENDING_DEEPDIVE' && !lastResponse) {
                    console.log(`[SUITE-BLOCKED] User ${user.id} tried SUITE but status is ${userProgress?.exerciseStatus || 'null'} and no response found.`);
                    await scheduleMessage(user.id, user.language === 'WOLOF'
                        ? "Dafa laaj nga tontu laaj bi ci kaw dëbb (audio walla texte) balaa nga dem ci kanam ! 🎙️"
                        : "Tu dois d'abord répondre à l'exercice ci-dessus pour continuer ! 🎙️"
                    );
                    return;
                }

                console.log(`[SUITE-ALLOWED] User ${user.id} advancing from day ${activeEnrollment.currentDay}`);
                const nextDay = activeEnrollment.currentDay % 1 !== 0
                    ? Math.floor(activeEnrollment.currentDay) + 1
                    : activeEnrollment.currentDay + 1;

                await prisma.enrollment.update({ where: { id: activeEnrollment.id }, data: { currentDay: nextDay } });
                await prisma.userProgress.update({
                    where: { userId_trackId: { userId: user.id, trackId: activeEnrollment.trackId } },
                    data: {
                        exerciseStatus: 'PENDING',
                        iterationCount: 0 // Reset iteration count for the new day
                    }
                });
                await whatsappQueue.add('send-content', { userId: user.id, trackId: activeEnrollment.trackId, dayNumber: nextDay });
                return;
            }

            // Handle APPROFONDIR (Deep Dive Initiation)
            if (isApprofondir) {
                const userProgress = await prisma.userProgress.findUnique({
                    where: { userId_trackId: { userId: user.id, trackId: activeEnrollment.trackId } }
                });

                // 🚨 UNBLOCKING GUARD: Allow 1/APPROFONDIR if a response exists.
                const lastResponse = await prisma.response.findFirst({
                    where: { userId: user.id, dayNumber: activeEnrollment.currentDay },
                    orderBy: { createdAt: 'desc' }
                });

                if (userProgress?.exerciseStatus === 'COMPLETED' || (userProgress?.exerciseStatus === 'PENDING' && lastResponse)) {
                    // Force state transition if it was stuck
                    if (userProgress?.exerciseStatus === 'PENDING') {
                        await prisma.userProgress.update({
                            where: { id: userProgress.id },
                            data: { exerciseStatus: 'PENDING_DEEPDIVE' }
                        });
                    } else {
                        await prisma.userProgress.update({
                            where: { id: userProgress.id },
                            data: { exerciseStatus: 'PENDING_DEEPDIVE' }
                        });
                    }
                    
                    await scheduleMessage(user.id, user.language === 'WOLOF'
                        ? "Baax na ! Wax ma ndox mi nga yor ci sa mbir (njëg, jafe-jafe, njëgëndal, njàngat, etc.) ngir ma gën a deesi njàngat bi :"
                        : "Très bien ! Quelle information précise issue de ton terrain veux-tu ajouter ? (ex: un prix précis, un obstacle, un fournisseur, etc.) :"
                    );
                    return;
                } else {
                    await scheduleMessage(user.id, user.language === 'WOLOF'
                        ? "Dafa laaj nga tontu laaj bi ci kaw dëbb balaa nga natt nga tontu !"
                        : "Réponds d'abord à l'exercice principal avant d'approfondir !"
                    );
                    return;
                }
            }

            // Handle YES/NO Intents
            if (intent === 'YES' && normalizedText.length < 15) {
                const userProgress = await prisma.userProgress.findUnique({
                    where: { userId_trackId: { userId: user.id, trackId: activeEnrollment.trackId } }
                });
                if (userProgress?.exerciseStatus === 'COMPLETED') {
                    await scheduleMessage(user.id, user.language === 'WOLOF' ? "Waaw ! Lexon bi mu ngi ñëw..." : "C'est parti ! Voici la suite...");
                    await whatsappQueue.add('send-content', { userId: user.id, trackId: activeEnrollment.trackId, dayNumber: activeEnrollment.currentDay + 1 });
                    return;
                }
            }

            if (intent === 'NO' && normalizedText.length < 15) {
                await scheduleMessage(user.id, user.language === 'WOLOF' ? "Baax na, bu la neexee nga tontu laaj bi." : "Pas de souci, tu peux répondre à l'exercice quand tu es prêt.");
                return;
            }

            // Fallback to Exercise Response if nothing else matched
            // 🚨 COACHING GUARDRAIL: AI Coach only activated if profile (sector + language) is complete
            if (!user.activity) {
                await scheduleMessage(user.id, user.language === 'WOLOF'
                    ? "Danga wara tànn sa mbiru liggeey balaa ñuy tàmbali coaching bi."
                    : "Tu dois d'abord définir ton activité avant que le coach AI ne puisse t'aider.");
                return;
            }

            // 🕰️ TIME-TRAVEL: Compute effectiveDay — single source of truth for AI prompt + Prisma saves
            const timeTravelDay = await getTimeTravelContext(user.id);
            const effectiveDay = timeTravelDay ?? activeEnrollment.currentDay;
            const isTimeTravelMode = timeTravelDay !== null && timeTravelDay !== activeEnrollment.currentDay;
            if (isTimeTravelMode) {
                console.log(`[TIME-TRAVEL] 🕰️ User ${user.id} responding to replay Day ${effectiveDay} (real currentDay: ${activeEnrollment.currentDay})`);
            }

            // 🚨 TEXT RE-VALIDATION: Mirror the Worker-side `shouldForceRevalidation` logic.
            // Image/Audio flows reset PENDING via WhatsAppLogic. For text, we do it here.
            const userProgressState = await prisma.userProgress.findUnique({
                where: { userId_trackId: { userId: user.id, trackId: activeEnrollment.trackId } }
            });
            const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000);
            const isRecentlyCompleted = userProgressState?.exerciseStatus === 'COMPLETED'
                && userProgressState.updatedAt > tenMinutesAgo;

            if (isRecentlyCompleted && !audioUrl && !imageUrl && text.length > 5 && !isSystemCommand) {
                console.log(`[TXT-FLOW] 🔄 Re-validation User ${user.id} Day ${activeEnrollment.currentDay} (COMPLETED → PENDING)`);
                await prisma.userProgress.update({
                    where: { id: userProgressState!.id },
                    data: { exerciseStatus: 'PENDING' }
                });
            }

            const pendingProgress = await prisma.userProgress.findFirst({
                where: { userId: user.id, exerciseStatus: { in: ['PENDING', 'PENDING_REMEDIATION', 'PENDING_DEEPDIVE'] }, trackId: activeEnrollment.trackId },
            });

            if (pendingProgress) {
                const isDeepDiveAction = pendingProgress.exerciseStatus === 'PENDING_DEEPDIVE';

                const trackDay = await prisma.trackDay.findFirst({
                    where: { trackId: activeEnrollment.trackId, dayNumber: effectiveDay }
                });

                if (trackDay) {
                    // 🚨 Flexible Guardrail (Day 7 Fix)
                    const wordCount = (text || '').trim().split(/\s+/).length;
                    const validationKeyword = trackDay.validationKeyword || '';
                    const isOptionMatch = validationKeyword && this.isFuzzyMatch(normalizedText, validationKeyword);
                    
                    // Specific bypass for known short answers in modules (WhatsApp, Boutique, etc.)
                    const commonOptions = ['WHATSAPP', 'BOUTIQUE', 'APPEL', 'VENTE', 'SERVICE', 'PRODUCTION'];
                    const isCommonOption = commonOptions.some(opt => this.isFuzzyMatch(normalizedText, opt));

                    if (wordCount < 3 && !isOptionMatch && !isCommonOption) {
                        await scheduleMessage(user.id, user.language === 'WOLOF'
                            ? "Dama lay xaar nga wax ma lu gën a yaatu ci sa mbir (mbebetu 3 baat). Waxtaanal ak man !"
                            : "Ta réponse est un peu courte. Peux-tu m'en dire plus ? (Minimum 3 mots)");
                        return;
                    }

                    await scheduleMessage(user.id, user.language === 'WOLOF' ? "⏳ Defar ak sa tontu..." : "⏳ Analyse de votre réponse...");

                    // Update iteration count if it's a deep dive
                    let currentIterationCount = pendingProgress.iterationCount || 0;
                    if (isDeepDiveAction) {
                        currentIterationCount += 1;
                        await prisma.userProgress.update({
                            where: { id: pendingProgress.id },
                            data: { iterationCount: currentIterationCount } // Save the increment
                        });
                    }

                    // 🚨 Store response under effectiveDay (real day OR Time-Travel override day)
                    await prisma.response.create({
                        data: {
                            enrollmentId: activeEnrollment.id,
                            userId: user.id,
                            dayNumber: effectiveDay,
                            content: text
                        }
                    });

                    // Fetch previous responses to provide context to the AI Coach
                    const previousResponsesData = await prisma.response.findMany({
                        where: { userId: user.id, enrollmentId: activeEnrollment.id },
                        orderBy: { dayNumber: 'asc' },
                        take: 5 // Keep context size reasonable
                    });
                    const previousResponses = previousResponsesData.map(r => ({ day: r.dayNumber, response: r.content }));

                    await whatsappQueue.add('generate-feedback', {
                        userId: user.id, text, trackId: activeEnrollment.trackId, trackDayId: trackDay.id,
                        exercisePrompt: trackDay.exercisePrompt || '', lessonText: trackDay.lessonText || '',
                        exerciseCriteria: trackDay.exerciseCriteria, pendingProgressId: pendingProgress.id,
                        dayNumber: effectiveDay,          // ← effectiveDay: single source of truth
                        currentDay: effectiveDay,         // ← worker reads job.data.currentDay
                        totalDays: activeEnrollment.track.duration, language: user.language,
                        userActivity: user.activity,
                        userRegion: user.city,
                        previousResponses,
                        isDeepDive: isDeepDiveAction,
                        iterationCount: currentIterationCount,
                        imageUrl: imageUrl,
                        isTimeTravelMode,                  // ← Worker uses this to skip COMPLETED update
                        realCurrentDay: activeEnrollment.currentDay  // ← For logging only
                    }, { attempts: 3, backoff: { type: 'exponential', delay: 2000 } });
                    return;
                }
            }

            // Handle daily response (Fallback if no PENDING found earlier)
            console.log(`${traceId} User ${user.id} fallback daily response to effectiveDay ${effectiveDay}`);
            await prisma.response.create({
                data: {
                    enrollmentId: activeEnrollment.id,
                    userId: user.id,
                    dayNumber: effectiveDay,
                    content: text
                }
            });

            const trackDayFallback = await prisma.trackDay.findFirst({
                where: { trackId: activeEnrollment.trackId, dayNumber: effectiveDay }
            });

            if (trackDayFallback) {
                // 🚨 Guardrail: Contenu Vide / Gibberish 🚨
                const wordCount = text.trim().split(/\s+/).length;
                if (wordCount < 3 || text.length < 5) {
                    console.log(`${traceId} Guardrail: Input too short or potential gibberish: "${text}"`);
                    await scheduleMessage(user.id, user.language === 'WOLOF'
                        ? "Ma déggul li nga wax mbir mi... Mën nga ko gën a firi ci ab kàddu gatt (3-4 kàddu) ?"
                        : "Je n'ai pas bien compris ton activité. Peux-tu me réexpliquer en quelques mots ce que tu vends et à qui ?");
                    return;
                }

                // 🚨 Guardrail: Enrollment Priority 🚨
                if (!user.activity || !user.language) {
                    console.log(`${traceId} Blocking AI feedback: Enrollment incomplete for User ${user.id}`);
                    await scheduleMessage(user.id, user.language === 'WOLOF'
                        ? "Baax na, waaye laaj bi des na... Bindal 'INSCRIPTION' ngir tàmbali."
                        : "C'est noté, mais il faut d'abord terminer ton inscription. Envoie 'INSCRIPTION' pour commencer.");
                    return;
                }

                // Fetch previous responses to provide context to the AI Coach
                const previousResponsesData = await prisma.response.findMany({
                    where: { userId: user.id, enrollmentId: activeEnrollment.id },
                    orderBy: { dayNumber: 'asc' },
                    take: 5
                });
                const previousResponses = previousResponsesData.map(r => ({ day: r.dayNumber, response: r.content }));

                await whatsappQueue.add('generate-feedback', {
                    userId: user.id, text, trackId: activeEnrollment.trackId, trackDayId: trackDayFallback.id,
                    exercisePrompt: trackDayFallback.exercisePrompt || '', lessonText: trackDayFallback.lessonText || '',
                    exerciseCriteria: trackDayFallback.exerciseCriteria,
                    enrollmentId: activeEnrollment.id,
                    currentDay: effectiveDay,              // ← effectiveDay: single source of truth
                    dayNumber: effectiveDay,
                    totalDays: activeEnrollment.track.duration, language: user.language,
                    userActivity: user.activity,
                    userRegion: user.city,
                    previousResponses,
                    imageUrl: imageUrl,
                    isTimeTravelMode,
                    realCurrentDay: activeEnrollment.currentDay
                });
                return;
            }

            await scheduleMessage(user.id, user.language === 'WOLOF'
                ? "Baax na ! Yónnee *SUITE* ngir dem ci kanam walla tontul laaj bi ci kaw."
                : "✅ Message reçu ! Envoie *SUITE* pour avancer ou réponds à l'exercice ci-dessus."
            );
            return;
        }

        // 4. Default: fallback for generic unknown messages (not in onboarding, not in active enrollment)
        console.log(`${traceId} Unknown command from user ${user.id}: "${normalizedText}"`);
        await scheduleMessage(user.id, user.language === 'WOLOF'
            ? "Bañ ma dégg. Yónnee *INSCRIPTION* ngir tàmbalee ci kanam walla bind *SUITE*."
            : "Je n'ai pas compris. Envoie *INSCRIPTION* pour recommencer ou *SUITE* pour avancer."
        );
    }
}