CognxSafeTrack commited on
Commit ·
d9186b1
1
Parent(s): caa20c8
fix: Resolve Production Crash & Add UX Guidance for Completed Exercises
Browse files
apps/whatsapp-worker/src/services/whatsapp-logic.ts
CHANGED
|
@@ -307,7 +307,30 @@ export class WhatsAppLogic {
|
|
| 307 |
isDeepDive: isDeepDiveAction, iterationCount: currentIterationCount, imageUrl: imageUrl
|
| 308 |
}, { attempts: 3, backoff: { type: 'exponential', delay: 2000 } });
|
| 309 |
return;
|
|
|
|
|
|
|
| 310 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
}
|
| 312 |
}
|
| 313 |
}
|
|
|
|
| 307 |
isDeepDive: isDeepDiveAction, iterationCount: currentIterationCount, imageUrl: imageUrl
|
| 308 |
}, { attempts: 3, backoff: { type: 'exponential', delay: 2000 } });
|
| 309 |
return;
|
| 310 |
+
} else {
|
| 311 |
+
console.log(`[LOGIC] ⚠️ Fall-through: User ${user.id} in enrollment but no matching pendingProgress (Status likely not PENDING).`);
|
| 312 |
}
|
| 313 |
+
} else {
|
| 314 |
+
// Enrollment active but no trackDay found for currentDay?
|
| 315 |
+
console.warn(`[LOGIC] ⚠️ Active Enrollment for User ${user.id} but TrackDay ${activeEnrollment.currentDay} not found.`);
|
| 316 |
+
}
|
| 317 |
+
} else {
|
| 318 |
+
console.log(`[LOGIC] ℹ️ User ${user.id} has no active enrollment. Fall-through.`);
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
// 🌟 UX Guidance Fall-through 🌟
|
| 322 |
+
// If we reach here, it means we found a user with an active enrollment but no pending exercise (likely COMPLETED).
|
| 323 |
+
if (activeEnrollment) {
|
| 324 |
+
const userProgress = await prisma.userProgress.findUnique({
|
| 325 |
+
where: { userId_trackId: { userId: user.id, trackId: activeEnrollment.trackId } }
|
| 326 |
+
});
|
| 327 |
+
|
| 328 |
+
if (userProgress?.exerciseStatus === 'COMPLETED') {
|
| 329 |
+
console.log(`[LOGIC] 💡 User ${user.id} is COMPLETED. Sending navigation reminder.`);
|
| 330 |
+
const reminder = user.language === 'WOLOF'
|
| 331 |
+
? "Mat nga bés bi ba pare ! ✨\nBindal *2* wala *SUITE* ngir dem ci bés bi ci kanam.\n(Bindal *REPLAY* ngir dégtu mbind mi)."
|
| 332 |
+
: "Tu as déjà validé cette étape ! ✨\nEnvoie *2* ou *SUITE* pour passer à la suite.\n(Envoie *REPLAY* pour réécouter la leçon).";
|
| 333 |
+
await scheduleMessage(user.id, reminder);
|
| 334 |
}
|
| 335 |
}
|
| 336 |
}
|
packages/database/prisma/schema.prisma
CHANGED
|
@@ -8,70 +8,63 @@ datasource db {
|
|
| 8 |
}
|
| 9 |
|
| 10 |
model User {
|
| 11 |
-
id
|
| 12 |
-
phone
|
| 13 |
-
name
|
| 14 |
-
role
|
| 15 |
-
language
|
| 16 |
-
city
|
| 17 |
-
activity
|
| 18 |
-
createdAt
|
| 19 |
-
updatedAt
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
lastActivityAt DateTime?
|
| 24 |
-
|
| 25 |
-
enrollments Enrollment[]
|
| 26 |
-
responses Response[]
|
| 27 |
-
messages Message[]
|
| 28 |
-
payments Payment[]
|
| 29 |
-
progress UserProgress[]
|
| 30 |
businessProfile BusinessProfile?
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
}
|
| 32 |
|
| 33 |
model BusinessProfile {
|
| 34 |
-
id
|
| 35 |
-
userId
|
| 36 |
-
activityLabel
|
| 37 |
-
activityPhrase
|
| 38 |
-
activityType
|
| 39 |
-
locationCity
|
| 40 |
-
mainCustomer
|
| 41 |
-
mainProblem
|
| 42 |
-
offerSimple
|
| 43 |
-
promise
|
| 44 |
-
marketData
|
| 45 |
-
competitorList
|
| 46 |
-
financialProjections Json?
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
user User @relation(fields: [userId], references: [id])
|
| 54 |
}
|
| 55 |
|
| 56 |
model Track {
|
| 57 |
-
id
|
| 58 |
-
title
|
| 59 |
-
description
|
| 60 |
-
duration
|
| 61 |
-
language
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
days TrackDay[]
|
| 72 |
-
enrollments Enrollment[]
|
| 73 |
-
payments Payment[]
|
| 74 |
-
progress UserProgress[]
|
| 75 |
}
|
| 76 |
|
| 77 |
model TrackDay {
|
|
@@ -89,84 +82,103 @@ model TrackDay {
|
|
| 89 |
validationKeyword String?
|
| 90 |
buttonsJson Json?
|
| 91 |
exerciseCriteria Json?
|
| 92 |
-
badges Json?
|
| 93 |
unlockCondition String?
|
| 94 |
createdAt DateTime @default(now())
|
| 95 |
updatedAt DateTime @updatedAt
|
| 96 |
-
|
| 97 |
track Track @relation(fields: [trackId], references: [id])
|
| 98 |
}
|
| 99 |
|
| 100 |
model UserProgress {
|
| 101 |
-
id
|
| 102 |
-
userId
|
| 103 |
-
trackId
|
| 104 |
-
score
|
| 105 |
-
lastInteraction
|
| 106 |
-
exerciseStatus
|
| 107 |
-
badges
|
| 108 |
-
behavioralScoring
|
| 109 |
-
confidenceScore
|
| 110 |
adminTranscription String?
|
| 111 |
overrideAudioUrl String?
|
| 112 |
reviewedBy String?
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
user User @relation(fields: [userId], references: [id])
|
| 121 |
-
track Track @relation(fields: [trackId], references: [id])
|
| 122 |
|
| 123 |
@@unique([userId, trackId])
|
| 124 |
}
|
| 125 |
|
| 126 |
model Enrollment {
|
| 127 |
-
id
|
| 128 |
-
userId
|
| 129 |
-
trackId
|
| 130 |
-
status
|
| 131 |
-
currentDay
|
| 132 |
-
startedAt
|
| 133 |
-
completedAt
|
| 134 |
-
lastActivityAt DateTime
|
| 135 |
-
|
| 136 |
-
user
|
| 137 |
-
|
| 138 |
-
responses Response[]
|
| 139 |
}
|
| 140 |
|
| 141 |
model Response {
|
| 142 |
-
id String
|
| 143 |
enrollmentId String
|
| 144 |
userId String
|
| 145 |
dayNumber Int
|
| 146 |
-
content String?
|
| 147 |
-
mediaUrl String?
|
| 148 |
-
createdAt DateTime
|
| 149 |
-
aiSource String?
|
| 150 |
-
|
| 151 |
enrollment Enrollment @relation(fields: [enrollmentId], references: [id], onDelete: Cascade)
|
| 152 |
user User @relation(fields: [userId], references: [id])
|
| 153 |
}
|
| 154 |
|
| 155 |
model Message {
|
| 156 |
-
id String
|
| 157 |
userId String
|
| 158 |
-
direction Direction
|
| 159 |
-
channel String
|
| 160 |
content String?
|
| 161 |
mediaUrl String?
|
| 162 |
-
payload Json?
|
| 163 |
-
createdAt DateTime
|
| 164 |
-
|
| 165 |
-
user User @relation(fields: [userId], references: [id])
|
| 166 |
|
| 167 |
@@index([userId, createdAt])
|
| 168 |
}
|
| 169 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
enum Role {
|
| 171 |
STUDENT
|
| 172 |
ADMIN
|
|
@@ -195,21 +207,6 @@ enum Direction {
|
|
| 195 |
OUTBOUND
|
| 196 |
}
|
| 197 |
|
| 198 |
-
model Payment {
|
| 199 |
-
id String @id @default(uuid())
|
| 200 |
-
userId String
|
| 201 |
-
trackId String
|
| 202 |
-
amount Int
|
| 203 |
-
currency String @default("XOF")
|
| 204 |
-
status PaymentStatus @default(PENDING)
|
| 205 |
-
stripeSessionId String? @unique
|
| 206 |
-
createdAt DateTime @default(now())
|
| 207 |
-
updatedAt DateTime @updatedAt
|
| 208 |
-
|
| 209 |
-
user User @relation(fields: [userId], references: [id])
|
| 210 |
-
track Track @relation(fields: [trackId], references: [id])
|
| 211 |
-
}
|
| 212 |
-
|
| 213 |
enum PaymentStatus {
|
| 214 |
PENDING
|
| 215 |
COMPLETED
|
|
@@ -227,20 +224,8 @@ enum ExerciseStatus {
|
|
| 227 |
PENDING
|
| 228 |
PENDING_REMEDIATION
|
| 229 |
PENDING_REVIEW
|
| 230 |
-
PENDING_DEEPDIVE
|
| 231 |
COMPLETED
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
model TrainingData {
|
| 235 |
-
id String @id @default(uuid())
|
| 236 |
-
audioUrl String
|
| 237 |
-
transcription String @db.Text
|
| 238 |
-
manualCorrection String? @db.Text
|
| 239 |
-
rawWER Float?
|
| 240 |
-
normalizedWER Float?
|
| 241 |
-
status TrainingStatus @default(PENDING)
|
| 242 |
-
createdAt DateTime @default(now())
|
| 243 |
-
updatedAt DateTime @updatedAt
|
| 244 |
}
|
| 245 |
|
| 246 |
enum TrainingStatus {
|
|
|
|
| 8 |
}
|
| 9 |
|
| 10 |
model User {
|
| 11 |
+
id String @id @default(uuid())
|
| 12 |
+
phone String @unique
|
| 13 |
+
name String?
|
| 14 |
+
role Role @default(STUDENT)
|
| 15 |
+
language Language @default(FR)
|
| 16 |
+
city String?
|
| 17 |
+
activity String?
|
| 18 |
+
createdAt DateTime @default(now())
|
| 19 |
+
updatedAt DateTime @updatedAt
|
| 20 |
+
currentStreak Int @default(0)
|
| 21 |
+
longestStreak Int @default(0)
|
| 22 |
+
lastActivityAt DateTime?
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
businessProfile BusinessProfile?
|
| 24 |
+
enrollments Enrollment[]
|
| 25 |
+
messages Message[]
|
| 26 |
+
payments Payment[]
|
| 27 |
+
responses Response[]
|
| 28 |
+
progress UserProgress[]
|
| 29 |
}
|
| 30 |
|
| 31 |
model BusinessProfile {
|
| 32 |
+
id String @id @default(uuid())
|
| 33 |
+
userId String @unique
|
| 34 |
+
activityLabel String?
|
| 35 |
+
activityPhrase String?
|
| 36 |
+
activityType String?
|
| 37 |
+
locationCity String?
|
| 38 |
+
mainCustomer String?
|
| 39 |
+
mainProblem String?
|
| 40 |
+
offerSimple String?
|
| 41 |
+
promise String?
|
| 42 |
+
marketData Json?
|
| 43 |
+
competitorList Json?
|
| 44 |
+
financialProjections Json?
|
| 45 |
+
fundingAsk String?
|
| 46 |
+
lastUpdatedFromDay Int @default(0)
|
| 47 |
+
createdAt DateTime @default(now())
|
| 48 |
+
updatedAt DateTime @updatedAt
|
| 49 |
+
teamMembers Json?
|
| 50 |
+
user User @relation(fields: [userId], references: [id])
|
|
|
|
| 51 |
}
|
| 52 |
|
| 53 |
model Track {
|
| 54 |
+
id String @id @default(uuid())
|
| 55 |
+
title String
|
| 56 |
+
description String?
|
| 57 |
+
duration Int
|
| 58 |
+
language Language @default(FR)
|
| 59 |
+
isPremium Boolean @default(false)
|
| 60 |
+
priceAmount Int?
|
| 61 |
+
stripePriceId String?
|
| 62 |
+
createdAt DateTime @default(now())
|
| 63 |
+
updatedAt DateTime @updatedAt
|
| 64 |
+
enrollments Enrollment[]
|
| 65 |
+
payments Payment[]
|
| 66 |
+
days TrackDay[]
|
| 67 |
+
progress UserProgress[]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
}
|
| 69 |
|
| 70 |
model TrackDay {
|
|
|
|
| 82 |
validationKeyword String?
|
| 83 |
buttonsJson Json?
|
| 84 |
exerciseCriteria Json?
|
| 85 |
+
badges Json?
|
| 86 |
unlockCondition String?
|
| 87 |
createdAt DateTime @default(now())
|
| 88 |
updatedAt DateTime @updatedAt
|
|
|
|
| 89 |
track Track @relation(fields: [trackId], references: [id])
|
| 90 |
}
|
| 91 |
|
| 92 |
model UserProgress {
|
| 93 |
+
id String @id @default(uuid())
|
| 94 |
+
userId String
|
| 95 |
+
trackId String
|
| 96 |
+
score Int @default(0)
|
| 97 |
+
lastInteraction DateTime @default(now())
|
| 98 |
+
exerciseStatus ExerciseStatus @default(PENDING)
|
| 99 |
+
badges Json?
|
| 100 |
+
behavioralScoring Json?
|
| 101 |
+
confidenceScore Float?
|
| 102 |
adminTranscription String?
|
| 103 |
overrideAudioUrl String?
|
| 104 |
reviewedBy String?
|
| 105 |
+
createdAt DateTime @default(now())
|
| 106 |
+
updatedAt DateTime @updatedAt
|
| 107 |
+
iterationCount Int @default(0)
|
| 108 |
+
aiSource String?
|
| 109 |
+
track Track @relation(fields: [trackId], references: [id])
|
| 110 |
+
user User @relation(fields: [userId], references: [id])
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
@@unique([userId, trackId])
|
| 113 |
}
|
| 114 |
|
| 115 |
model Enrollment {
|
| 116 |
+
id String @id @default(uuid())
|
| 117 |
+
userId String
|
| 118 |
+
trackId String
|
| 119 |
+
status EnrollmentStatus @default(ACTIVE)
|
| 120 |
+
currentDay Float @default(1)
|
| 121 |
+
startedAt DateTime @default(now())
|
| 122 |
+
completedAt DateTime?
|
| 123 |
+
lastActivityAt DateTime @default(now())
|
| 124 |
+
track Track @relation(fields: [trackId], references: [id])
|
| 125 |
+
user User @relation(fields: [userId], references: [id])
|
| 126 |
+
responses Response[]
|
|
|
|
| 127 |
}
|
| 128 |
|
| 129 |
model Response {
|
| 130 |
+
id String @id @default(uuid())
|
| 131 |
enrollmentId String
|
| 132 |
userId String
|
| 133 |
dayNumber Int
|
| 134 |
+
content String?
|
| 135 |
+
mediaUrl String?
|
| 136 |
+
createdAt DateTime @default(now())
|
| 137 |
+
aiSource String?
|
|
|
|
| 138 |
enrollment Enrollment @relation(fields: [enrollmentId], references: [id], onDelete: Cascade)
|
| 139 |
user User @relation(fields: [userId], references: [id])
|
| 140 |
}
|
| 141 |
|
| 142 |
model Message {
|
| 143 |
+
id String @id @default(uuid())
|
| 144 |
userId String
|
| 145 |
+
direction Direction
|
| 146 |
+
channel String @default("WHATSAPP")
|
| 147 |
content String?
|
| 148 |
mediaUrl String?
|
| 149 |
+
payload Json?
|
| 150 |
+
createdAt DateTime @default(now())
|
| 151 |
+
user User @relation(fields: [userId], references: [id])
|
|
|
|
| 152 |
|
| 153 |
@@index([userId, createdAt])
|
| 154 |
}
|
| 155 |
|
| 156 |
+
model Payment {
|
| 157 |
+
id String @id @default(uuid())
|
| 158 |
+
userId String
|
| 159 |
+
trackId String
|
| 160 |
+
amount Int
|
| 161 |
+
currency String @default("XOF")
|
| 162 |
+
status PaymentStatus @default(PENDING)
|
| 163 |
+
stripeSessionId String? @unique
|
| 164 |
+
createdAt DateTime @default(now())
|
| 165 |
+
updatedAt DateTime @updatedAt
|
| 166 |
+
track Track @relation(fields: [trackId], references: [id])
|
| 167 |
+
user User @relation(fields: [userId], references: [id])
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
model TrainingData {
|
| 171 |
+
id String @id @default(uuid())
|
| 172 |
+
audioUrl String
|
| 173 |
+
transcription String
|
| 174 |
+
manualCorrection String?
|
| 175 |
+
rawWER Float?
|
| 176 |
+
normalizedWER Float?
|
| 177 |
+
status TrainingStatus @default(PENDING)
|
| 178 |
+
createdAt DateTime @default(now())
|
| 179 |
+
updatedAt DateTime @updatedAt
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
enum Role {
|
| 183 |
STUDENT
|
| 184 |
ADMIN
|
|
|
|
| 207 |
OUTBOUND
|
| 208 |
}
|
| 209 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
enum PaymentStatus {
|
| 211 |
PENDING
|
| 212 |
COMPLETED
|
|
|
|
| 224 |
PENDING
|
| 225 |
PENDING_REMEDIATION
|
| 226 |
PENDING_REVIEW
|
|
|
|
| 227 |
COMPLETED
|
| 228 |
+
PENDING_DEEPDIVE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
}
|
| 230 |
|
| 231 |
enum TrainingStatus {
|
tasks/lessons.md
CHANGED
|
@@ -12,6 +12,7 @@ Ce fichier archive les échecs et solutions liées à la **stabilité technique*
|
|
| 12 |
- **[AI] OpenAI Zod Schemas** : Pour les Structured Outputs, OpenAI exige que TOUS les champs d'un schéma soient obligatoires OU explicitement `.nullable().optional()`. Les `.optional()` seuls sur les sous-objets provoquent une erreur.
|
| 13 |
- **[QUEUES] Payload Sync** : S'assurer que les noms de champs dans `whatsappQueue.add` (producteur) matchent exactement le destructuring dans le worker (consommateur). Préférer `currentDay` partout pour la consistance.
|
| 14 |
- **[DB] Enrollment ID** : Toujours passer l'ID d'inscription (`enrollmentId`) au job de feedback pour permettre la persistance des réponses sans crash Prisma.
|
|
|
|
| 15 |
|
| 16 |
## 🛡️ Règle d'Or de l'Intégrité
|
| 17 |
**Les correctifs techniques ne doivent JAMAIS impacter la logique de personnalisation du prompt `generatePersonalizedLesson` ou des feedbacks.** Toute simplification technique qui réduit la spécificité de l'IA est un échec.
|
|
|
|
| 12 |
- **[AI] OpenAI Zod Schemas** : Pour les Structured Outputs, OpenAI exige que TOUS les champs d'un schéma soient obligatoires OU explicitement `.nullable().optional()`. Les `.optional()` seuls sur les sous-objets provoquent une erreur.
|
| 13 |
- **[QUEUES] Payload Sync** : S'assurer que les noms de champs dans `whatsappQueue.add` (producteur) matchent exactement le destructuring dans le worker (consommateur). Préférer `currentDay` partout pour la consistance.
|
| 14 |
- **[DB] Enrollment ID** : Toujours passer l'ID d'inscription (`enrollmentId`) au job de feedback pour permettre la persistance des réponses sans crash Prisma.
|
| 15 |
+
- **[UX] Guidance Post-Validation** : Si un utilisateur envoie un message alors qu'il est en statut `COMPLETED`, le système ne doit pas rester muet. Il doit lui renvoyer un message de navigation (ex: "Tape SUITE pour continuer").
|
| 16 |
|
| 17 |
## 🛡️ Règle d'Or de l'Intégrité
|
| 18 |
**Les correctifs techniques ne doivent JAMAIS impacter la logique de personnalisation du prompt `generatePersonalizedLesson` ou des feedbacks.** Toute simplification technique qui réduit la spécificité de l'IA est un échec.
|