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
|
| 364 |
-
|
| 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: {
|