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 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? // Business activity/sector
18
- createdAt DateTime @default(now())
19
- updatedAt DateTime @updatedAt
20
-
21
- currentStreak Int @default(0)
22
- longestStreak Int @default(0)
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 String @id @default(uuid())
35
- userId String @unique
36
- activityLabel String? // e.g. "Jus de Bissap"
37
- activityPhrase String? // Optimized elevator pitch
38
- activityType String? // e.g. "Production", "Service"
39
- locationCity String?
40
- mainCustomer String?
41
- mainProblem String?
42
- offerSimple String?
43
- promise String?
44
- marketData Json? // Stored Google Search results for TAM/SAM/SOM & Competition
45
- competitorList Json? // List of rivals found or declared
46
- financialProjections Json? // 3-year growth data
47
- teamMembers Json? // Array of { name, role, bio, photoUrl }
48
- fundingAsk String? // Amount and purpose
49
- lastUpdatedFromDay Int @default(0)
50
- createdAt DateTime @default(now())
51
- updatedAt DateTime @updatedAt
52
-
53
- user User @relation(fields: [userId], references: [id])
54
  }
55
 
56
  model Track {
57
- id String @id @default(uuid())
58
- title String
59
- description String?
60
- duration Int // Duration in days
61
- language Language @default(FR)
62
-
63
- // Payment Integration Fields
64
- isPremium Boolean @default(false)
65
- priceAmount Int? // Price in smallest currency unit (e.g., cents/XOF)
66
- stripePriceId String? // Stripe Price ID
67
-
68
- createdAt DateTime @default(now())
69
- updatedAt DateTime @updatedAt
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? // Array of strings: ["B_MODULE_1_OK"]
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 String @id @default(uuid())
102
- userId String
103
- trackId String
104
- score Int @default(0)
105
- lastInteraction DateTime @default(now())
106
- exerciseStatus ExerciseStatus @default(PENDING)
107
- badges Json? // Array of strings: ["CLARTE", "CONFIANCE"]
108
- behavioralScoring Json? // { discipline_financiere: 0, organisation: 0, ... }
109
- confidenceScore Float? // STT Whisper avg_logprob mapped to 0-100%
110
  adminTranscription String?
111
  overrideAudioUrl String?
112
  reviewedBy String?
113
- iterationCount Int @default(0) // Tracks Deep Dive loops
114
- aiSource String? // Provider used (GEMINI, OPENAI)
115
-
116
- // Removed Enriched Data for Pitch Deck (Moved to BusinessProfile - Sprint 38)
117
- createdAt DateTime @default(now())
118
- updatedAt DateTime @updatedAt
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 String @id @default(uuid())
128
- userId String
129
- trackId String
130
- status EnrollmentStatus @default(ACTIVE)
131
- currentDay Float @default(1)
132
- startedAt DateTime @default(now())
133
- completedAt DateTime?
134
- lastActivityAt DateTime @default(now())
135
-
136
- user User @relation(fields: [userId], references: [id])
137
- track Track @relation(fields: [trackId], references: [id])
138
- responses Response[]
139
  }
140
 
141
  model Response {
142
- id String @id @default(uuid())
143
  enrollmentId String
144
  userId String
145
  dayNumber Int
146
- content String? // Text response
147
- mediaUrl String? // Voice/Image response
148
- createdAt DateTime @default(now())
149
- aiSource String? // Provider used (GEMINI, OPENAI)
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 @id @default(uuid())
157
  userId String
158
- direction Direction // INBOUND, OUTBOUND
159
- channel String @default("WHATSAPP")
160
  content String?
161
  mediaUrl String?
162
- payload Json? // Raw payload from provider
163
- createdAt DateTime @default(now())
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.