CognxSafeTrack commited on
Commit
cfe6413
·
1 Parent(s): 7b4936e

fix(whatsapp): idempotency lock and progression blockers

Browse files
apps/api/src/services/whatsapp.ts CHANGED
@@ -106,6 +106,7 @@ export class WhatsAppService {
106
  await prisma.enrollment.deleteMany({ where: { userId: user.id } });
107
  await prisma.userProgress.deleteMany({ where: { userId: user.id } });
108
  await prisma.response.deleteMany({ where: { userId: user.id } });
 
109
  // Also explicitly clear business AI profile to prevent context leak on restart
110
  await (prisma as any).businessProfile.deleteMany({ where: { userId: user.id } });
111
  user = await prisma.user.update({
@@ -360,9 +361,8 @@ export class WhatsAppService {
360
  where: { userId_trackId: { userId: user.id, trackId: activeEnrollment.trackId } }
361
  });
362
 
363
- // Strict check: you can only advance if the exercise is COMPLETED or if there's no mandatory exercise.
364
- // We err on the side of blocking if it's PENDING or null (unexpected state).
365
- if (userProgress?.exerciseStatus !== 'COMPLETED') {
366
  console.log(`[SUITE-BLOCKED] User ${user.id} tried SUITE but status is ${userProgress?.exerciseStatus || 'null'}`);
367
  await scheduleMessage(user.id, user.language === 'WOLOF'
368
  ? "Dafa laaj nga tontu laaj bi ci kaw dëbb (audio walla texte) balaa nga dem ci kanam ! 🎙️"
 
106
  await prisma.enrollment.deleteMany({ where: { userId: user.id } });
107
  await prisma.userProgress.deleteMany({ where: { userId: user.id } });
108
  await prisma.response.deleteMany({ where: { userId: user.id } });
109
+ await prisma.message.deleteMany({ where: { userId: user.id } }); // Purge totale historique des messages
110
  // Also explicitly clear business AI profile to prevent context leak on restart
111
  await (prisma as any).businessProfile.deleteMany({ where: { userId: user.id } });
112
  user = await prisma.user.update({
 
361
  where: { userId_trackId: { userId: user.id, trackId: activeEnrollment.trackId } }
362
  });
363
 
364
+ // Strict check: you can jump if completed OR if currently in a deep dive (user skips it).
365
+ if (userProgress?.exerciseStatus !== 'COMPLETED' && userProgress?.exerciseStatus !== 'PENDING_DEEPDIVE') {
 
366
  console.log(`[SUITE-BLOCKED] User ${user.id} tried SUITE but status is ${userProgress?.exerciseStatus || 'null'}`);
367
  await scheduleMessage(user.id, user.language === 'WOLOF'
368
  ? "Dafa laaj nga tontu laaj bi ci kaw dëbb (audio walla texte) balaa nga dem ci kanam ! 🎙️"
apps/whatsapp-worker/src/index.ts CHANGED
@@ -55,6 +55,24 @@ const worker = new Worker('whatsapp-queue', async (job: Job) => {
55
  }) as any;
56
  if (!user?.phone) return;
57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  const trackDay = await prisma.trackDay.findFirst({
59
  where: { trackId, dayNumber: currentDay }
60
  });
@@ -247,7 +265,7 @@ const worker = new Worker('whatsapp-queue', async (job: Job) => {
247
  nextDay = currentDay;
248
  }
249
 
250
- // 🚨 Hardening: If not qualified, we put the user in PENDING_REMEDIATION
251
  await prisma.userProgress.update({
252
  where: { userId_trackId: { userId, trackId } },
253
  data: {
 
55
  }) as any;
56
  if (!user?.phone) return;
57
 
58
+ // ─── 🚀 Idempotence Lock (Progression Bug Fix) ─────────
59
+ // Empêche de traiter le même feedback deux fois si le LLM prend > 20s.
60
+ const Redis = (await import('ioredis')).default;
61
+ const redis = process.env.REDIS_URL
62
+ ? new Redis(process.env.REDIS_URL, { maxRetriesPerRequest: null })
63
+ : new Redis({ host: process.env.REDIS_HOST || 'localhost', port: parseInt(process.env.REDIS_PORT || '6379'), maxRetriesPerRequest: null });
64
+
65
+ // On utilise un hash unique combinant l'utilisateur, le jour et un bout du texte.
66
+ const textHash = text ? text.substring(0, 10).replace(/[^a-z0-9]/gi, '') : '';
67
+ const lockKey = `lock:feedback:${userId}:${currentDay}:${textHash}`;
68
+
69
+ // EX 60 : Le verrou expire dans 60 secondes. S'il existe déjà, NX retourne null (ou 0).
70
+ const isLocked = await redis.set(lockKey, "1", "EX", 60, "NX");
71
+ if (!isLocked) {
72
+ console.log(`[WORKER] 🔒 Lock activé : ignorer ce job de feedback en double (User ${userId}, Day ${currentDay})`);
73
+ return;
74
+ }
75
+
76
  const trackDay = await prisma.trackDay.findFirst({
77
  where: { trackId, dayNumber: currentDay }
78
  });
 
265
  nextDay = currentDay;
266
  }
267
 
268
+ // 🚨 Hardening: If explicitly not qualified, we put the user in PENDING_REMEDIATION
269
  await prisma.userProgress.update({
270
  where: { userId_trackId: { userId, trackId } },
271
  data: {