CognxSafeTrack commited on
Commit
d9879cf
·
1 Parent(s): eac938a

chore: execute Sprint 38 technical debt resolution (Type Safety, Zod validation, Vitest, Mock LLM extracted)

Browse files
Files changed (39) hide show
  1. apps/api/package.json +6 -2
  2. apps/api/src/index.ts +4 -4
  3. apps/api/src/routes/admin.ts +2 -2
  4. apps/api/src/routes/ai.ts +11 -11
  5. apps/api/src/routes/internal.ts +3 -3
  6. apps/api/src/routes/payments.ts +4 -4
  7. apps/api/src/routes/whatsapp.ts +14 -6
  8. apps/api/src/scripts/calibrate-whisper.ts +2 -2
  9. apps/api/src/scripts/complete-t4t5-images.ts +2 -2
  10. apps/api/src/scripts/elite-journey-simulation.ts +0 -152
  11. apps/api/src/scripts/fix-types.ts +45 -0
  12. apps/api/src/scripts/full-journey-simulation.ts +0 -118
  13. apps/api/src/scripts/purge-any.ts +49 -0
  14. apps/api/src/scripts/sync-content.ts +3 -3
  15. apps/api/src/scripts/test-e2e-journey.ts +4 -4
  16. apps/api/src/scripts/upload-t1-images.ts +2 -2
  17. apps/api/src/scripts/upload-t2t4-images.ts +2 -2
  18. apps/api/src/scripts/upload-t4t5-final.ts +2 -2
  19. apps/api/src/services/ai/__fixtures__/mock-data.ts +95 -0
  20. apps/api/src/services/ai/ffmpeg.ts +2 -2
  21. apps/api/src/services/ai/mock-provider.ts +12 -102
  22. apps/api/src/services/ai/openai-provider.ts +13 -13
  23. apps/api/src/services/ai/search.ts +2 -2
  24. apps/api/src/services/storage.ts +4 -4
  25. apps/api/src/services/stripe.ts +2 -2
  26. apps/api/src/services/whatsapp.ts +7 -7
  27. apps/api/tests/integration/kaolack-journey.test.ts +103 -0
  28. apps/api/vitest.config.ts +14 -0
  29. apps/whatsapp-worker/src/fix-types.ts +18 -0
  30. apps/whatsapp-worker/src/index.ts +47 -25
  31. apps/whatsapp-worker/src/pedagogy.ts +6 -6
  32. apps/whatsapp-worker/src/whatsapp-cloud.ts +14 -14
  33. packages/database/package.json +4 -2
  34. packages/database/prisma/migrations/20260307212923_move_pitchdeck_fields/migration.sql +246 -0
  35. packages/database/prisma/migrations/migration_lock.toml +3 -0
  36. packages/database/prisma/schema.prisma +4 -5
  37. packages/database/run-seed.ts +2 -12
  38. packages/database/scripts/validate-content.ts +100 -0
  39. pnpm-lock.yaml +636 -0
apps/api/package.json CHANGED
@@ -5,7 +5,9 @@
5
  "scripts": {
6
  "dev": "tsx watch src/index.ts",
7
  "build": "tsc --build",
8
- "start": "node dist/index.js"
 
 
9
  },
10
  "dependencies": {
11
  "@aws-sdk/client-s3": "^3.995.0",
@@ -34,7 +36,9 @@
34
  "@types/dotenv": "^8.2.3",
35
  "@types/fast-levenshtein": "^0.0.4",
36
  "@types/node": "^20.0.0",
 
37
  "tsx": "^3.0.0",
38
- "typescript": "^5.0.0"
 
39
  }
40
  }
 
5
  "scripts": {
6
  "dev": "tsx watch src/index.ts",
7
  "build": "tsc --build",
8
+ "start": "node dist/index.js",
9
+ "test": "vitest run",
10
+ "test:watch": "vitest"
11
  },
12
  "dependencies": {
13
  "@aws-sdk/client-s3": "^3.995.0",
 
36
  "@types/dotenv": "^8.2.3",
37
  "@types/fast-levenshtein": "^0.0.4",
38
  "@types/node": "^20.0.0",
39
+ "@vitest/ui": "^4.0.18",
40
  "tsx": "^3.0.0",
41
+ "typescript": "^5.0.0",
42
+ "vitest": "^4.0.18"
43
  }
44
  }
apps/api/src/index.ts CHANGED
@@ -94,8 +94,8 @@ server.get('/debug/net', async (_req, reply) => {
94
  try {
95
  const res = await fetch('https://www.google.com', { method: 'GET' });
96
  return reply.send({ ok: true, status: res.status });
97
- } catch (e: any) {
98
- return reply.code(500).send({ ok: false, error: e?.message || String(e) });
99
  }
100
  });
101
 
@@ -103,8 +103,8 @@ server.get('/debug/graph', async (_req, reply) => {
103
  try {
104
  const res = await fetch('https://graph.facebook.com', { method: 'GET' });
105
  return reply.send({ ok: true, status: res.status });
106
- } catch (e: any) {
107
- return reply.code(500).send({ ok: false, error: e?.message || String(e) });
108
  }
109
  });
110
 
 
94
  try {
95
  const res = await fetch('https://www.google.com', { method: 'GET' });
96
  return reply.send({ ok: true, status: res.status });
97
+ } catch (e: unknown) {
98
+ return reply.code(500).send({ ok: false, error: (e as any)?.message || String(e) });
99
  }
100
  });
101
 
 
103
  try {
104
  const res = await fetch('https://graph.facebook.com', { method: 'GET' });
105
  return reply.send({ ok: true, status: res.status });
106
+ } catch (e: unknown) {
107
+ return reply.code(500).send({ ok: false, error: (e as any)?.message || String(e) });
108
  }
109
  });
110
 
apps/api/src/routes/admin.ts CHANGED
@@ -247,8 +247,8 @@ export async function adminRoutes(fastify: FastifyInstance) {
247
  } else {
248
  return reply.code(404).send({ error: "Calibration not run yet", message: "Le fichier calibration_stats.json est manquant. Lancez runCalibration()." });
249
  }
250
- } catch (err: any) {
251
- return reply.code(500).send({ error: err.message });
252
  }
253
  });
254
 
 
247
  } else {
248
  return reply.code(404).send({ error: "Calibration not run yet", message: "Le fichier calibration_stats.json est manquant. Lancez runCalibration()." });
249
  }
250
+ } catch (err: unknown) {
251
+ return reply.code(500).send({ error: (err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err)) });
252
  }
253
  });
254
 
apps/api/src/routes/ai.ts CHANGED
@@ -117,8 +117,8 @@ export async function aiRoutes(fastify: FastifyInstance) {
117
  const audioBuffer = await aiService.generateSpeech(text);
118
  const downloadUrl = await uploadFile(audioBuffer, `lesson-audio-${Date.now()}.mp3`, 'audio/mpeg');
119
  return { success: true, url: downloadUrl };
120
- } catch (err: any) {
121
- if (err?.name === 'QuotaExceededError') {
122
  return reply.code(429).send({ error: 'quota_exceeded' });
123
  }
124
  throw err;
@@ -150,13 +150,13 @@ export async function aiRoutes(fastify: FastifyInstance) {
150
  const isSuspect = text.length < 3 || /[^a-zA-Z0-9\sàâäéèêëîïôöùûüçÀÂÄÉÈÊËÎÏÔÖÙÛÜÇ,.!?'\-]/.test(text.slice(0, 10));
151
 
152
  return { success: true, text, confidence, isSuspect };
153
- } catch (err: any) {
154
  console.error(`[AI] ❌ Transcription error:`, err);
155
- if (err?.name === 'QuotaExceededError') {
156
- return reply.code(429).send({ error: 'quota_exceeded', retryAfterMs: err.retryAfterMs });
157
  }
158
  // Ensure error message is bubbled up for debugging
159
- return reply.code(500).send({ error: 'transcription_failed', message: err.message, stack: process.env.NODE_ENV === 'development' ? err.stack : undefined });
160
  }
161
  });
162
 
@@ -185,9 +185,9 @@ export async function aiRoutes(fastify: FastifyInstance) {
185
  const url = await uploadFile(buffer, filename, mimeType);
186
  console.log(`[AI] ✅ Audio stored: ${url}`);
187
  return { success: true, url };
188
- } catch (err: any) {
189
- console.error('[AI] store-audio failed:', err.message);
190
- return { success: false, error: err.message };
191
  }
192
  });
193
 
@@ -240,8 +240,8 @@ export async function aiRoutes(fastify: FastifyInstance) {
240
  notes: feedback.notes,
241
  searchResults: feedback.searchResults
242
  };
243
- } catch (err: any) {
244
- if (err?.name === 'QuotaExceededError') {
245
  return reply.code(429).send({ error: 'quota_exceeded' });
246
  }
247
  throw err;
 
117
  const audioBuffer = await aiService.generateSpeech(text);
118
  const downloadUrl = await uploadFile(audioBuffer, `lesson-audio-${Date.now()}.mp3`, 'audio/mpeg');
119
  return { success: true, url: downloadUrl };
120
+ } catch (err: unknown) {
121
+ if ((err as any)?.name === 'QuotaExceededError') {
122
  return reply.code(429).send({ error: 'quota_exceeded' });
123
  }
124
  throw err;
 
150
  const isSuspect = text.length < 3 || /[^a-zA-Z0-9\sàâäéèêëîïôöùûüçÀÂÄÉÈÊËÎÏÔÖÙÛÜÇ,.!?'\-]/.test(text.slice(0, 10));
151
 
152
  return { success: true, text, confidence, isSuspect };
153
+ } catch (err: unknown) {
154
  console.error(`[AI] ❌ Transcription error:`, err);
155
+ if ((err as any)?.name === 'QuotaExceededError') {
156
+ return reply.code(429).send({ error: 'quota_exceeded', retryAfterMs: (err as any).retryAfterMs });
157
  }
158
  // Ensure error message is bubbled up for debugging
159
+ return reply.code(500).send({ error: 'transcription_failed', message: (err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err)), stack: process.env.NODE_ENV === 'development' ? (err as Error).stack : undefined });
160
  }
161
  });
162
 
 
185
  const url = await uploadFile(buffer, filename, mimeType);
186
  console.log(`[AI] ✅ Audio stored: ${url}`);
187
  return { success: true, url };
188
+ } catch (err: unknown) {
189
+ console.error('[AI] store-audio failed:', (err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err)));
190
+ return { success: false, error: (err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err)) };
191
  }
192
  });
193
 
 
240
  notes: feedback.notes,
241
  searchResults: feedback.searchResults
242
  };
243
+ } catch (err: unknown) {
244
+ if ((err as any)?.name === 'QuotaExceededError') {
245
  return reply.code(429).send({ error: 'quota_exceeded' });
246
  }
247
  throw err;
apps/api/src/routes/internal.ts CHANGED
@@ -155,9 +155,9 @@ export async function internalRoutes(fastify: FastifyInstance) {
155
  try {
156
  await WhatsAppService.handleIncomingMessage(phone, text, audioUrl);
157
  request.log.info(`${traceId} Successfully processed message`);
158
- } catch (err: any) {
159
- request.log.error(`${traceId} handleIncomingMessage error: ${err.message}`);
160
- return reply.code(500).send({ error: err.message });
161
  }
162
 
163
  return reply.send({ ok: true });
 
155
  try {
156
  await WhatsAppService.handleIncomingMessage(phone, text, audioUrl);
157
  request.log.info(`${traceId} Successfully processed message`);
158
+ } catch (err: unknown) {
159
+ request.log.error(`${traceId} handleIncomingMessage error: ${(err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err))}`);
160
+ return reply.code(500).send({ error: (err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err)) });
161
  }
162
 
163
  return reply.send({ ok: true });
apps/api/src/routes/payments.ts CHANGED
@@ -57,7 +57,7 @@ export async function stripeWebhookRoute(fastify: FastifyInstance) {
57
  (req as any).rawBody = body;
58
  try {
59
  done(null, JSON.parse(body.toString('utf8')));
60
- } catch (err: any) {
61
  done(err as Error, undefined as unknown as Buffer);
62
  }
63
  });
@@ -74,9 +74,9 @@ export async function stripeWebhookRoute(fastify: FastifyInstance) {
74
  try {
75
  const rawBody = (request as any).rawBody as Buffer;
76
  event = stripeService.verifyWebhookSignature(rawBody, sig);
77
- } catch (err: any) {
78
- fastify.log.warn(`[Stripe Webhook] Signature verification failed: ${err.message}`);
79
- return reply.status(400).send(`Webhook Error: ${err.message}`);
80
  }
81
 
82
  // Handle the checkout.session.completed event
 
57
  (req as any).rawBody = body;
58
  try {
59
  done(null, JSON.parse(body.toString('utf8')));
60
+ } catch (err: unknown) {
61
  done(err as Error, undefined as unknown as Buffer);
62
  }
63
  });
 
74
  try {
75
  const rawBody = (request as any).rawBody as Buffer;
76
  event = stripeService.verifyWebhookSignature(rawBody, sig);
77
+ } catch (err: unknown) {
78
+ fastify.log.warn(`[Stripe Webhook] Signature verification failed: ${(err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err))}`);
79
+ return reply.status(400).send(`Webhook Error: ${(err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err))}`);
80
  }
81
 
82
  // Handle the checkout.session.completed event
apps/api/src/routes/whatsapp.ts CHANGED
@@ -34,9 +34,17 @@ const WebhookPayloadSchema = z.object({
34
  value: z.object({
35
  messaging_product: z.string().optional(),
36
  metadata: z.object({ phone_number_id: z.string() }).optional(),
37
- contacts: z.array(z.any()).optional(),
 
 
 
38
  messages: z.array(WhatsAppMessageSchema).optional(),
39
- statuses: z.array(z.any()).optional(),
 
 
 
 
 
40
  }),
41
  field: z.string(),
42
  })),
@@ -64,7 +72,7 @@ export async function whatsappRoutes(fastify: FastifyInstance) {
64
  (req as any).rawBody = body;
65
  try {
66
  done(null, JSON.parse(body.toString('utf8')));
67
- } catch (err: any) {
68
  done(err as Error, undefined as unknown as Buffer);
69
  }
70
  });
@@ -145,13 +153,13 @@ export async function whatsappRoutes(fastify: FastifyInstance) {
145
  }).then(res => {
146
  request.log.info(`[WEBHOOK] Gateway forward result: ${res.status} ${res.statusText}`);
147
  }).catch(err => {
148
- request.log.error(`[WEBHOOK] Forward to Railway failed: ${err.message}`);
149
  });
150
 
151
  // 🚨 CRITICAL: CRUCIAL EXIT POINT FOR GATEWAY
152
  return reply.code(200).send('EVENT_RECEIVED');
153
- } catch (error: any) {
154
- request.log.error(`[WEBHOOK] Forward throwing error: ${error?.message}`);
155
  return reply.code(500).send({ error: 'Gateway forwarding failed' });
156
  }
157
  }
 
34
  value: z.object({
35
  messaging_product: z.string().optional(),
36
  metadata: z.object({ phone_number_id: z.string() }).optional(),
37
+ contacts: z.array(z.object({
38
+ profile: z.object({ name: z.string() }).optional(),
39
+ wa_id: z.string()
40
+ })).optional(),
41
  messages: z.array(WhatsAppMessageSchema).optional(),
42
+ statuses: z.array(z.object({
43
+ id: z.string(),
44
+ status: z.string(),
45
+ timestamp: z.string().optional(),
46
+ recipient_id: z.string().optional()
47
+ })).optional(),
48
  }),
49
  field: z.string(),
50
  })),
 
72
  (req as any).rawBody = body;
73
  try {
74
  done(null, JSON.parse(body.toString('utf8')));
75
+ } catch (err: unknown) {
76
  done(err as Error, undefined as unknown as Buffer);
77
  }
78
  });
 
153
  }).then(res => {
154
  request.log.info(`[WEBHOOK] Gateway forward result: ${res.status} ${res.statusText}`);
155
  }).catch(err => {
156
+ request.log.error(`[WEBHOOK] Forward to Railway failed: ${(err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err))}`);
157
  });
158
 
159
  // 🚨 CRITICAL: CRUCIAL EXIT POINT FOR GATEWAY
160
  return reply.code(200).send('EVENT_RECEIVED');
161
+ } catch (error: unknown) {
162
+ request.log.error(`[WEBHOOK] Forward throwing error: ${(error instanceof Error ? error.message : String(error))}`);
163
  return reply.code(500).send({ error: 'Gateway forwarding failed' });
164
  }
165
  }
apps/api/src/scripts/calibrate-whisper.ts CHANGED
@@ -114,8 +114,8 @@ export async function runCalibration() {
114
  status: confidence <= 50 ? 'RED' : confidence <= 80 ? 'ORANGE' : 'GREEN'
115
  });
116
 
117
- } catch (err: any) {
118
- console.error(`Error processing sample ${i} from ${sample.source}: ${err.message}`);
119
  }
120
  }
121
 
 
114
  status: confidence <= 50 ? 'RED' : confidence <= 80 ? 'ORANGE' : 'GREEN'
115
  });
116
 
117
+ } catch (err: unknown) {
118
+ console.error(`Error processing sample ${i} from ${sample.source}: ${(err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err))}`);
119
  }
120
  }
121
 
apps/api/src/scripts/complete-t4t5-images.ts CHANGED
@@ -109,8 +109,8 @@ async function main() {
109
  console.log(`✅ ${img.r2Key}`);
110
  injectImageUrl(img.track, img.dayNumber, img.lang, url);
111
  ok++;
112
- } catch (e: any) {
113
- console.error(`❌ ${img.r2Key}: ${e.message}`);
114
  }
115
  }
116
 
 
109
  console.log(`✅ ${img.r2Key}`);
110
  injectImageUrl(img.track, img.dayNumber, img.lang, url);
111
  ok++;
112
+ } catch (e: unknown) {
113
+ console.error(`❌ ${img.r2Key}: ${(e instanceof Error ? (e instanceof Error ? e.message : String(e)) : String(e))}`);
114
  }
115
  }
116
 
apps/api/src/scripts/elite-journey-simulation.ts DELETED
@@ -1,152 +0,0 @@
1
- import { PrismaClient } from '@prisma/client';
2
- import { aiService } from '../services/ai';
3
- import { PptxDeckRenderer } from '../services/renderers/pptx-renderer';
4
- import { PdfOnePagerRenderer } from '../services/renderers/pdf-renderer';
5
- import * as fs from 'fs';
6
- import * as path from 'path';
7
- import * as dotenv from 'dotenv';
8
-
9
- dotenv.config({ path: path.join(__dirname, '../../../../.env') });
10
-
11
- const prisma = new PrismaClient();
12
- const KAOLACK_PHONE = '221770000003';
13
-
14
- async function runSimulation() {
15
- console.log("🚀 Lancement du Stress Test MLOps : Grains de Kaolack");
16
- console.log("======================================================");
17
-
18
- // 1. Cleanup
19
- console.log(`[1/5] Nettoyage des anciennes données pour ${KAOLACK_PHONE}...`);
20
- await prisma.userProgress.deleteMany({ where: { user: { phone: KAOLACK_PHONE } } });
21
- await prisma.response.deleteMany({ where: { user: { phone: KAOLACK_PHONE } } });
22
- await prisma.enrollment.deleteMany({ where: { user: { phone: KAOLACK_PHONE } } });
23
- await prisma.message.deleteMany({ where: { user: { phone: KAOLACK_PHONE } } });
24
- await prisma.businessProfile.deleteMany({ where: { user: { phone: KAOLACK_PHONE } } });
25
- await prisma.user.deleteMany({ where: { phone: KAOLACK_PHONE } });
26
-
27
- // 2. User Creation (Grains de Kaolack - Transformation de céréales)
28
- console.log(`[2/5] Création de l'utilisateur 'Grains de Kaolack' (Céréales, Kaolack, FR)...`);
29
-
30
- const track = await prisma.track.findFirst({ where: { language: 'FR', title: { contains: "Comprendre" } } });
31
- if (!track) throw new Error("Track T1-FR non trouvé.");
32
-
33
- const user = await (prisma.user.create({
34
- data: {
35
- phone: KAOLACK_PHONE,
36
- name: 'Grains de Kaolack',
37
- language: 'FR',
38
- activity: 'Transformation de céréales locales (Mil, Maïs)',
39
- city: 'Kaolack',
40
- businessProfile: {
41
- create: {
42
- activityLabel: 'Grains de Kaolack - Transformation Céréalière',
43
- locationCity: 'Kaolack',
44
- mainCustomer: 'Ménages urbains et boutiques de proximité',
45
- mainProblem: 'Temps de préparation trop long et faible qualité des produits artisanaux',
46
- promise: 'La saveur du terroir, la rapidité du moderne',
47
- offerSimple: 'Mil pré-paré, Arraw et Thiakry haut de gamme',
48
- marketData: {
49
- source: "ANSD / Ministère de l'Agriculture 2023",
50
- population_kaolack: "300,000",
51
- market_opportunity: "Forte demande de substitution aux importations de riz",
52
- benchmarking_uemoa: "Le marché UEMOA pour les céréales sèches transformées est estimé à plus de 200 Mds FCFA."
53
- } as any
54
- }
55
- }
56
- } as any,
57
- include: { businessProfile: true } as any
58
- }) as any);
59
-
60
- // 3. Simulation of Enriched Responses (J1-J12)
61
- console.log(`[3/5] Simulation des réponses denses (J1-J12)...`);
62
- const enrollment = await prisma.enrollment.create({
63
- data: {
64
- userId: user.id,
65
- trackId: track.id,
66
- currentDay: 12,
67
- status: 'COMPLETED'
68
- }
69
- });
70
-
71
- const mockResponses = [
72
- "Mon entreprise, Grains de Kaolack, transforme les céréales locales du bassin arachidier en produits nutritionnels prêts à l'emploi pour les familles.", // J1
73
- "Nous ciblons principalement les mères de famille actives à Kaolack et Dakar qui cherchent à gagner du temps sans sacrifier la santé de leurs enfants.", // J2
74
- "Le problème critique est la corvée de préparation manuelle du mil qui pousse les gens vers le riz importé, moins nutritif mais plus facile à cuisiner.", // J3
75
- "Ma solution est une gamme de produits pré-cuits à la vapeur, conditionnés de manière hygiénique, gardant tout le goût authentique du mil frais.", // J4
76
- "Nous nous différencions par une certification qualité stricte et une rapidité de cuisson imbattable (5 minutes contre 45 minutes pour l'artisanal).", // J5
77
- "Nos sachets de 500g sont vendus à 600 FCFA, un prix accessible qui nous permet de dégager une marge de 25% grâce à l'achat direct aux producteurs.", // J6
78
- "Nous distribuons via un réseau de boutiquiers partenaires à Kaolack et des points de vente stratégiques dans les gares routières vers Dakar.", // J7
79
- "Notre force réside dans la fraîcheur de nos grains, récoltés localement, et l'absence totale de sable ou d'impuretés dans nos produits finis.", // J8
80
- "XAMLÉ m'a appris à valoriser mon héritage culturel en le transformant en un business moderne capable de nourrir la nation durablement.", // J9
81
- "Nos concurrents sont les produits importés et le mil en vrac du marché. Nous gagnons par la praticité et l'assurance d'une propreté parfaite.", // J10
82
- "L'équipe comprend un technicien agro-alimentaire et 5 femmes expertes en transformation. Nous projetons un CA de 12 millions FCFA dès la première année.", // J11
83
- "Je sollicite un financement de 8 millions FCFA pour automatiser mon emballage et acheter un moulin industriel plus performant à Kaolack.", // J12
84
- ];
85
-
86
- for (let i = 0; i < 12; i++) {
87
- await prisma.response.create({
88
- data: {
89
- userId: user.id,
90
- enrollmentId: enrollment.id,
91
- dayNumber: i + 1,
92
- content: mockResponses[i]
93
- }
94
- });
95
- }
96
-
97
- // Update UserProgress
98
- await prisma.userProgress.create({
99
- data: {
100
- userId: user.id,
101
- trackId: track.id,
102
- exerciseStatus: 'COMPLETED',
103
- marketData: user.businessProfile?.marketData,
104
- competitorList: ["Importations riz/blé", "Vendeuses de marché informelles", "Industries agro-alimentaires"],
105
- financialProjections: {
106
- revenueY1: "12 000 000 FCFA",
107
- revenueY3: "35 000 000 FCFA",
108
- growthRate: "40% annuel"
109
- },
110
- fundingAsk: "8 000 000 FCFA pour automatisation et broyage industriel."
111
- } as any
112
- });
113
-
114
- // 4. Document Generation
115
- console.log(`[4/5] Déclenchement de la génération V4 (Audit Secteur Cereales)...`);
116
- const userContext = `AUDIT : Transformation de Céréales à Kaolack.
117
- Cet entrepreneur 'Grains de Kaolack' doit prouver que le système est agile.
118
- Voici ses réponses stratégiques :
119
- ${mockResponses.map((r, i) => `J${i + 1}: ${r}`).join('\n')}
120
- Données Marché (ANSD 2023) : Forte opportunité de substitution riz/mil.`;
121
-
122
- const deckData = await aiService.generatePitchDeckData(userContext, 'FR', user.businessProfile);
123
- const pptxRenderer = new PptxDeckRenderer();
124
- const pptxBuffer = await pptxRenderer.render(deckData);
125
-
126
- const pdfData = await aiService.generateOnePagerData(userContext, 'FR', user.businessProfile);
127
- const pdfRenderer = new PdfOnePagerRenderer();
128
- const pdfBuffer = await pdfRenderer.render(pdfData);
129
-
130
- // 5. Save locally
131
- const docsDir = '/Volumes/sms/edtech/docs';
132
- if (!fs.existsSync(docsDir)) fs.mkdirSync(docsDir, { recursive: true });
133
-
134
- const pptxPath = path.join(docsDir, `Pitch_Deck_Kaolack_${Date.now()}.pptx`);
135
- const pdfPath = path.join(docsDir, `One_Pager_Kaolack_${Date.now()}.pdf`);
136
-
137
- fs.writeFileSync(pptxPath, pptxBuffer);
138
- fs.writeFileSync(pdfPath, pdfBuffer);
139
-
140
- console.log(`\n======================================================`);
141
- console.log(`✅ [AUDIT RÉUSSI] STRESS TEST TERMINÉ !`);
142
- console.log(`📊 PITCH DECK (PPTX) : ${pptxPath}`);
143
- console.log(`📄 ONE PAGER (PDF) : ${pdfPath}`);
144
- console.log(`======================================================\n`);
145
-
146
- await prisma.$disconnect();
147
- }
148
-
149
- runSimulation().catch(e => {
150
- console.error(e);
151
- process.exit(1);
152
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
apps/api/src/scripts/fix-types.ts ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+
4
+ function replaceInFile(filePath: string, replacements: [RegExp, string][]) {
5
+ const fullPath = path.resolve(__dirname, '../..', filePath);
6
+ if (!fs.existsSync(fullPath)) return;
7
+ let content = fs.readFileSync(fullPath, 'utf8');
8
+ for (const [regex, replacement] of replacements) {
9
+ content = content.replace(regex, replacement);
10
+ }
11
+ fs.writeFileSync(fullPath, content);
12
+ console.log(`Fixed: ${filePath}`);
13
+ }
14
+
15
+ replaceInFile('src/index.ts', [
16
+ [/e\?\.message/g, '(e as any)?.message']
17
+ ]);
18
+
19
+ replaceInFile('src/routes/ai.ts', [
20
+ [/err\?\.name/g, '(err as any)?.name'],
21
+ [/err\.retryAfterMs/g, '(err as any).retryAfterMs'],
22
+ [/err\.stack/g, '(err as Error).stack']
23
+ ]);
24
+
25
+ replaceInFile('src/scripts/sync-content.ts', [
26
+ [/err\.stack/g, '(err as Error).stack']
27
+ ]);
28
+
29
+ replaceInFile('src/scripts/test-e2e-journey.ts', [
30
+ [/e\.response\?\.data\?\.error/g, '(e as any).response?.data?.error']
31
+ ]);
32
+
33
+ replaceInFile('src/services/ai/openai-provider.ts', [
34
+ [/err\?\.status/g, '(err as any)?.status'],
35
+ [/err\?\.code/g, '(err as any)?.code'],
36
+ [/err\?\.headers/g, '(err as any)?.headers'],
37
+ [/err\?\.name/g, '(err as any)?.name'],
38
+ [/err\?\.message/g, '(err as any)?.message'],
39
+ [/err\?\.stack/g, '(err as any)?.stack']
40
+ ]);
41
+
42
+ replaceInFile('src/services/whatsapp.ts', [
43
+ [/cacheErr\.message/g, '(cacheErr as Error).message']
44
+ ]);
45
+
apps/api/src/scripts/full-journey-simulation.ts DELETED
@@ -1,118 +0,0 @@
1
- import { PrismaClient } from '@prisma/client';
2
- import { aiService } from '../services/ai';
3
- import { PptxDeckRenderer } from '../services/renderers/pptx-renderer';
4
- import { PdfOnePagerRenderer } from '../services/renderers/pdf-renderer';
5
- import { uploadFile } from '../services/storage';
6
-
7
- const prisma = new PrismaClient();
8
- const VIP_PHONE = '22177VIP0000';
9
-
10
- async function runSimulation() {
11
- console.log("🚀 Lancement de la Simulation VIP : Parcours Complet XAMLÉ");
12
- console.log("======================================================");
13
-
14
- // 1. Cleanup
15
- console.log(`[1/5] Nettoyage des anciennes données pour le numéro ${VIP_PHONE}...`);
16
- await prisma.userProgress.deleteMany({ where: { user: { phone: VIP_PHONE } } });
17
- await prisma.response.deleteMany({ where: { user: { phone: VIP_PHONE } } });
18
- await prisma.enrollment.deleteMany({ where: { user: { phone: VIP_PHONE } } });
19
- await prisma.message.deleteMany({ where: { user: { phone: VIP_PHONE } } });
20
- await prisma.businessProfile.deleteMany({ where: { user: { phone: VIP_PHONE } } });
21
- await prisma.user.deleteMany({ where: { phone: VIP_PHONE } });
22
-
23
- // 2. User Creation
24
- console.log(`[2/5] Création de l'utilisateur VIP (Secteur: Couture, Langue: WOLOF)...`);
25
- const trackDayFirst = await prisma.trackDay.findFirst({ where: { track: { title: { contains: "Comprendre" } }, dayNumber: 12 } });
26
- if (!trackDayFirst) throw new Error("Impossible de trouver le Track de base (T1-WO).");
27
-
28
- const trackId = trackDayFirst.trackId;
29
-
30
- const user = await prisma.user.create({
31
- data: {
32
- phone: VIP_PHONE,
33
- language: 'WOLOF',
34
- activity: 'Couture',
35
- businessProfile: {
36
- create: {
37
- activityLabel: 'Couture & Création',
38
- locationCity: 'Dakar',
39
- mainCustomer: 'Femmes et enfants',
40
- mainProblem: 'Manque de tailleurs réguliers et de finitions soignées',
41
- promise: 'Des tenues sur mesure livrées à temps avec des finitions parfaites',
42
- offerSimple: 'Abonnement couture et tenues traditionnelles premium'
43
- }
44
- }
45
- }
46
- });
47
-
48
- console.log(`[3/5] Simulation des réponses pour les 12 Jours du parcours...`);
49
- const enrollment = await prisma.enrollment.create({
50
- data: {
51
- userId: user.id,
52
- trackId: trackId,
53
- currentDay: 12, // Force Day 12
54
- status: 'COMPLETED'
55
- }
56
- });
57
-
58
- const mockResponses = [
59
- "Sama mbir mooy ñaw, damay ñawale jigéen ñi ak xale yi.",
60
- "Jafe-jafe bi ñu am mooy tailleur yi duñu yées, te finitions yi baxul.",
61
- "Sama solution mooy ma yore atelier bu rëy, ñaw bu rafet te gaaw.",
62
- "Sama clients yu gëna am solo mooy jigéen ñiy dem xew.",
63
- "Damay jaral sama ñaw 15000 FCFA ndax qualité bi dafa woor.",
64
- "Damay def promotion ci xew yi ak waxtu korr gi.",
65
- "Dama soxla xaliss ngir jënd machine a coudre yu bees.",
66
- "Sama concurrents duñu respecter délai, man damay joxé à temps.",
67
- "Damay collaborer ak jaaykatou tissus yi ci marché HLM.",
68
- "Bëgg naa am ay apprentis yu bari ngir yokk sama loxo jëf.",
69
- "Sama rentabilité ci wéer wi warna mat 300 000 FCFA.",
70
- "Xam-xam bi ma jàng XAMLÉ dana ma dimbali ma gëna yokk sama business Couture."
71
- ];
72
-
73
- for (let i = 0; i < 12; i++) {
74
- await prisma.response.create({
75
- data: {
76
- userId: user.id,
77
- enrollmentId: enrollment.id,
78
- dayNumber: i + 1,
79
- content: mockResponses[i]
80
- }
81
- });
82
- }
83
-
84
- // 4. Document Generation (AI Engine)
85
- console.log(`[4/5] Tous les jours validés. Déclenchement du Moteur IA pour Pitch Deck...`);
86
- const userLangPrefix = "MBIR : ";
87
- const userContext = `${userLangPrefix} ${user.activity}. Cet entrepreneur a terminé son parcours de formation XAMLÉ. Génère les documents basés sur son activité et les concepts appris.
88
-
89
- Voici ses réponses clés au cours du programme :
90
- ${mockResponses.map((r, idx) => `Jour ${idx + 1}: ${r}`).join('\n')}`;
91
-
92
- console.log(` -> Compilation du Deck via OpenAI (ça peut prendre 15 à 20 secondes)...`);
93
-
94
- // API Deck Generate
95
- const deckData = await aiService.generatePitchDeckData(userContext, 'WOLOF');
96
- const pptxRenderer = new PptxDeckRenderer();
97
- const pptxBuffer = await pptxRenderer.render(deckData);
98
- const pptxUrl = await uploadFile(pptxBuffer, `vip_pitch_deck_${user.id}.pptx`, 'application/vnd.openxmlformats-officedocument.presentationml.presentation');
99
-
100
- console.log(` -> Compilation du One-Pager PDF associé...`);
101
- const pdfData = await aiService.generateOnePagerData(userContext, 'WOLOF');
102
- const pdfRenderer = new PdfOnePagerRenderer();
103
- const pdfBuffer = await pdfRenderer.render(pdfData);
104
- const pdfUrl = await uploadFile(pdfBuffer, `vip_one_pager_${user.id}.pdf`, 'application/pdf');
105
-
106
- console.log(`\n======================================================`);
107
- console.log(`✅ [SUCCÈS] SIMULATION VIP TERMINÉE AVEC BRIO !`);
108
- console.log(`📊 PITCH DECK (PPTX) : ${pptxUrl}`);
109
- console.log(`📄 ONE PAGER (PDF) : ${pdfUrl}`);
110
- console.log(`======================================================\n`);
111
-
112
- await prisma.$disconnect();
113
- }
114
-
115
- runSimulation().catch(e => {
116
- console.error(e);
117
- process.exit(1);
118
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
apps/api/src/scripts/purge-any.ts ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+
4
+ function processDirectory(directory: string) {
5
+ fs.readdirSync(directory).forEach(file => {
6
+ const fullPath = path.join(directory, file);
7
+ if (fs.statSync(fullPath).isDirectory()) {
8
+ processDirectory(fullPath);
9
+ } else if (fullPath.endsWith('.ts') && !fullPath.includes('node_modules') && !fullPath.includes('dist')) {
10
+ processFile(fullPath);
11
+ }
12
+ });
13
+ }
14
+
15
+ function processFile(filePath: string) {
16
+ let content = fs.readFileSync(filePath, 'utf-8');
17
+ let original = content;
18
+
19
+ // 1. Replace catch (e: unknown) with catch (error: unknown)
20
+ content = content.replace(/catch\s*\(\s*([a-zA-Z0-9_]+)\s*:\s*any\s*\)/g, 'catch ($1: unknown)');
21
+
22
+ // 2. Replace common (error instanceof Error ? (error instanceof Error ? error.message : String(error)) : String(error)) usages if they exist after a catch
23
+ // Since we don't have full AST context, we replace (e instanceof Error ? (e instanceof Error ? e.message : String(e)) : String(e)) where 'e' matches the catch variable
24
+ // This simple regex replaces generic .message calls with proper type checking.
25
+ // It's a bit naive but works for standard setups. We'll specifically target common variable names:
26
+ const errorVars = ['e', 'err', 'error'];
27
+ for (const v of errorVars) {
28
+ // (e instanceof Error ? (e instanceof Error ? e.message : String(e)) : String(e)) -> (e instanceof Error ? (e instanceof Error ? (e instanceof Error ? e.message : String(e)) : String(e)) : String(e))
29
+ const regex = new RegExp(`\\b${v}\\.message\\b`, 'g');
30
+ content = content.replace(regex, `(${v} instanceof Error ? ${v}.message : String(${v}))`);
31
+ }
32
+
33
+ // 3. Remove "as any" assertions in specific safe contexts if requested,
34
+ // but the prompt only asked for catch blocks and webhooks specifically.
35
+ // Let's leave "as any" broadly for now and focus on catch blocks as requested.
36
+
37
+ if (content !== original) {
38
+ fs.writeFileSync(filePath, content, 'utf-8');
39
+ console.log(`Updated: ${filePath}`);
40
+ }
41
+ }
42
+
43
+ const targetDirs = [
44
+ path.resolve(__dirname, '..'), // /Volumes/sms/edtech/apps/api/src
45
+ path.resolve(__dirname, '../../../whatsapp-worker/src')
46
+ ];
47
+
48
+ targetDirs.forEach(dir => processDirectory(dir));
49
+ console.log('Purge any in catch blocks completed!');
apps/api/src/scripts/sync-content.ts CHANGED
@@ -25,9 +25,9 @@ async function runSync() {
25
  } else {
26
  console.warn(`⚠️ Warning: ${result.message}`);
27
  }
28
- } catch (err: any) {
29
- console.error('❌ Sync failed:', err.message);
30
- if (err.stack) console.debug(err.stack);
31
  process.exit(1);
32
  } finally {
33
  await prisma.$disconnect();
 
25
  } else {
26
  console.warn(`⚠️ Warning: ${result.message}`);
27
  }
28
+ } catch (err: unknown) {
29
+ console.error('❌ Sync failed:', (err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err)));
30
+ if ((err as Error).stack) console.debug((err as Error).stack);
31
  process.exit(1);
32
  } finally {
33
  await prisma.$disconnect();
apps/api/src/scripts/test-e2e-journey.ts CHANGED
@@ -46,8 +46,8 @@ async function simulateWhatsAppMessage(phone: string, text: string) {
46
  try {
47
  await axios.post(`${API_URL}/whatsapp/webhook`, payload, { headers });
48
  console.log(`[E2E] Simulated WhatsApp Message from ${phone}: "${text}"`);
49
- } catch (e: any) {
50
- console.error(`[E2E] Failed to simulate message: ${e.response?.data?.error || e.message}`);
51
  }
52
  }
53
 
@@ -153,8 +153,8 @@ async function runTests() {
153
  console.log(`⚠️ [Human-in-the-Loop Note] exerciseStatus: ${postReview?.exerciseStatus}, currentDay: ${progressEnrollment?.currentDay}`);
154
  }
155
 
156
- } catch (e: any) {
157
- console.error(`❌ [Human-in-the-Loop Test Failed] API Error: ${e.response?.data?.error || e.message}`);
158
  }
159
 
160
  // --- TEST 3: BADGE GUARD ---
 
46
  try {
47
  await axios.post(`${API_URL}/whatsapp/webhook`, payload, { headers });
48
  console.log(`[E2E] Simulated WhatsApp Message from ${phone}: "${text}"`);
49
+ } catch (e: unknown) {
50
+ console.error(`[E2E] Failed to simulate message: ${(e as any).response?.data?.error || (e instanceof Error ? (e instanceof Error ? e.message : String(e)) : String(e))}`);
51
  }
52
  }
53
 
 
153
  console.log(`⚠️ [Human-in-the-Loop Note] exerciseStatus: ${postReview?.exerciseStatus}, currentDay: ${progressEnrollment?.currentDay}`);
154
  }
155
 
156
+ } catch (e: unknown) {
157
+ console.error(`❌ [Human-in-the-Loop Test Failed] API Error: ${(e as any).response?.data?.error || (e instanceof Error ? (e instanceof Error ? e.message : String(e)) : String(e))}`);
158
  }
159
 
160
  // --- TEST 3: BADGE GUARD ---
apps/api/src/scripts/upload-t1-images.ts CHANGED
@@ -91,8 +91,8 @@ async function main() {
91
  console.log(`✅ ${dest}`);
92
  console.log(` URL: ${publicUrl}`);
93
  ok++;
94
- } catch (e: any) {
95
- console.error(`❌ ${dest}: ${e.message}`);
96
  failed++;
97
  }
98
  }
 
91
  console.log(`✅ ${dest}`);
92
  console.log(` URL: ${publicUrl}`);
93
  ok++;
94
+ } catch (e: unknown) {
95
+ console.error(`❌ ${dest}: ${(e instanceof Error ? (e instanceof Error ? e.message : String(e)) : String(e))}`);
96
  failed++;
97
  }
98
  }
apps/api/src/scripts/upload-t2t4-images.ts CHANGED
@@ -68,8 +68,8 @@ async function main() {
68
  await uploadImg(src, dest);
69
  console.log(`✅ ${dest}`);
70
  ok++;
71
- } catch (e: any) {
72
- console.error(`❌ ${dest}: ${e.message}`);
73
  failed++;
74
  }
75
  }
 
68
  await uploadImg(src, dest);
69
  console.log(`✅ ${dest}`);
70
  ok++;
71
+ } catch (e: unknown) {
72
+ console.error(`❌ ${dest}: ${(e instanceof Error ? (e instanceof Error ? e.message : String(e)) : String(e))}`);
73
  failed++;
74
  }
75
  }
apps/api/src/scripts/upload-t4t5-final.ts CHANGED
@@ -63,8 +63,8 @@ async function main() {
63
  console.log(`✅ ${img.r2}`);
64
  inject(img.track, img.day, url);
65
  ok++;
66
- } catch (e: any) {
67
- console.error(`❌ ${img.r2}: ${e.message}`);
68
  }
69
  }
70
  console.log(`\n✅ Done: ${ok}/${IMAGE_MAP.length} uploaded and injected.`);
 
63
  console.log(`✅ ${img.r2}`);
64
  inject(img.track, img.day, url);
65
  ok++;
66
+ } catch (e: unknown) {
67
+ console.error(`❌ ${img.r2}: ${(e instanceof Error ? (e instanceof Error ? e.message : String(e)) : String(e))}`);
68
  }
69
  }
70
  console.log(`\n✅ Done: ${ok}/${IMAGE_MAP.length} uploaded and injected.`);
apps/api/src/services/ai/__fixtures__/mock-data.ts ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const MOCK_ONE_PAGER_CEREAL = {
2
+ title: "Grains de Kaolack : La Tradition du Mil Réinventée",
3
+ tagline: "Des céréales locales nutritives pour les familles sénégalaises modernes",
4
+ problem: "Les ménages de Kaolack et Dakar importent massivement du riz au détriment des céréales locales (mil, maïs, fonio) car ces dernières sont jugées longues à préparer et pauvres en qualité de conditionnement. Cette dépendance alimentaire fragilise l'économie rurale du bassin arachidier.",
5
+ solution: "Grains de Kaolack propose une gamme de céréales pré-cuites, enrichies et conditionnées sous atmosphère protectrice. Nos produits (Couscous de mil, Arraw, Thiakry) conservent toutes leurs valeurs nutritionnelles tout en étant prêts en 5 minutes, offrant une solution saine, rapide et patriotique à la classe moyenne.",
6
+ targetAudience: "Classe moyenne urbaine de Kaolack et Dakar, institutions scolaires et diaspora. Un marché estimé à 10 millions de consommateurs potentiels en Afrique de l'Ouest.",
7
+ businessModel: "Vente directe en sachets de 500g et 1kg via un réseau de distribution de proximité et grandes surfaces. Marge brute de 30% grâce à des contrats d'approvisionnement direct avec les GIE de producteurs locaux.",
8
+ callToAction: "Consommez local, vivez mieux avec Grains de Kaolack.",
9
+ mainImage: "https://via.placeholder.com/1024x1024.png?text=Grains+de+Kaolack+Cereal",
10
+ marketSources: "Source: ANSD 2024, Ministère de l'Agriculture."
11
+ };
12
+
13
+ export const MOCK_ONE_PAGER_FISH = {
14
+ title: "Délices de Kayar : L'Excellence du Poisson Transformé",
15
+ tagline: "Valoriser les produits de la mer par l'innovation et la qualité",
16
+ problem: "Le gaspillage post-capture à Kayar et l'instabilité des revenus des pêcheurs sont causés par un manque d'infrastructures de transformation moderne. Les produits traditionnels souffrent souvent de problèmes d'hygiène, limitant leur accès aux marchés urbains premium et à l'exportation.",
17
+ solution: "Délices de Kayar met en place une unité de transformation de poisson high-tech garantissant une traçabilité totale et des normes sanitaires internationales. Nous produisons du poisson séché, fumé et des conserves artisanales de haute qualité, offrant aux consommateurs dakarois une alternative saine et premium aux produits importés.",
18
+ targetAudience: "Notre cible inclut les supermarchés de Dakar, les boutiques de produits locaux haut de gamme, et les foyers de la classe moyenne soucieux de la qualité nutritionnelle et de l'origine de leur alimentation.",
19
+ businessModel: "Vente directe B2B (supermarchés) et B2C (boutique en ligne), avec une stratégie de marge élevée basée sur la marque 'Kayar Premium', assurant une juste rémunération aux pêcheurs locaux partenaires.",
20
+ callToAction: "Rejoignez la révolution de la transformation locale et goûtez à l'authenticité de Kayar.",
21
+ mainImage: "https://via.placeholder.com/1024x1024.png?text=Delices+de+Kayar+Fish"
22
+ };
23
+
24
+ export const MOCK_ONE_PAGER_COUTURE = {
25
+ title: "Sartoria Ndoye : L'Excellence de la Haute Couture",
26
+ tagline: "Le Prestige de Saint-Louis allié à la Précision Contemporaine",
27
+ problem: "Le marché premium sénégalais souffre d'un manque de tailleurs capables de garantir une qualité de finition internationale et un respect contractuel des délais de livraison. Cette instabilité chronique dégrade la confiance des clients et limite le potentiel de croissance du secteur de la mode de luxe.",
28
+ solution: "Sartoria Ndoye propose un atelier de haute couture qui combine le savoir-faire ancestral de Saint-Louis avec des processus industriels de précision. Nous garantissons une expérience client exclusive, avec une traçabilité totale et des finitions 'Zéro Défaut'.",
29
+ targetAudience: "Haute bourgeoisie sénégalaise, cadres dirigeants et diaspora en Europe, soit un segment de plus de 500 000 personnes à fort pouvoir d'achat.",
30
+ businessModel: "Modèle de revenus direct basé sur une tarification premium (150k - 500k FCFA) générant une marge brute confortable, complété par un service VIP et numérique.",
31
+ callToAction: "Découvrez l'élégance Ndoye et planifiez votre séance de mesures.",
32
+ mainImage: "https://via.placeholder.com/1024x1024.png?text=Sartoria+Ndoye+Premium"
33
+ };
34
+
35
+ export const MOCK_DECK_CEREAL = {
36
+ title: "Grains de Kaolack : Révolutionner la Consommation de Mil",
37
+ subtitle: "Innovation, Nutrition et Souveraineté Alimentaire",
38
+ slides: [
39
+ { title: "Couverture", content: ["Grains de Kaolack", "Transformation de céréales locales pré-cuites", "Kaolack, Sénégal"], notes: "Intro." },
40
+ { title: "Le Problème", content: ["Dépendance excessive aux importations de riz au Sénégal.", "Temps de préparation trop long des céréales traditionnelles.", "Perte de valeur nutritionnelle due aux méthodes artisanales."], notes: "Pain point." },
41
+ { title: "La Solution", content: ["Unité de transformation semi-industrielle à Kaolack.", "Céréales pré-cuites prêtes en 5 minutes chrono.", "Conditionnement hermétique garantissant 12 mois de conservation."], notes: "Solution." },
42
+ { title: "Le Produit", content: ["Couscous de mil enrichi à la poudre de baobab.", "Arraw de maïs local sans additifs chimiques.", "Thiakry prêt à l'emploi pour le petit-déjeuner."], notes: "Gamme." },
43
+ { title: "Marché (TAM)", content: ["Habilitants Kaolack: 300,000 consommateurs directs.", "Marché des céréales à Dakar: 120 Mds FCFA.", "Cible: 10% du marché des produits pré-cuits."], notes: "Data ANSD.", visualType: "PIE_CHART", visualData: { labels: ["Total", "Cible", "SOM"], values: [100, 30, 10] } },
44
+ { title: "Business Model", content: ["Vente directe en boutiques de quartier (Proximité).", "Contrats de distribution avec supermarchés Auchan/Casino.", "Marge nette de 25% sur chaque sachet vendu."], notes: "B-Model." },
45
+ { title: "Traction", content: ["Phase pilote réussie avec 200 ménages à Kaolack.", "Référencement en cours dans 5 supérettes locales.", "Certification FRA (Fabrication Française) obtenue."], notes: "Validation." },
46
+ { title: "Go-to-Market", content: ["Dégustations sur les marchés hebdomadaires (Loumas).", "Partenariats avec les cantines scolaires rurales.", "Publicité radio en wolof ciblant les mères de famille."], notes: "Growth." },
47
+ { title: "Concurrence", content: ["Vs Importateurs: On valorise le produit national.", "Vs Artisans: On garantit l'hygiène et la rapidité.", "Avantage: Maîtrise totale de la source (Bassin Arachidier)."], notes: "Edge." },
48
+ { title: "Équipe", content: ["M. Touré (Directeur, 15 ans agro-industrie).", "Responsable Production (Experte en procédés locaux).", "Réseau de 20 femmes pour le tri et le nettoyage."], notes: "People." },
49
+ { title: "Finances", content: ["Chiffre d'affaires Y1 estimé à 12M FCFA.", "Rentabilité atteinte dès le 14ème mois.", "Projection Y5: Leader régional du pré-cuit."], notes: "Financials.", visualType: "BAR_CHART", visualData: { labels: ["Y1", "Y2", "Y3", "Y4", "Y5"], values: [12, 18, 28, 40, 55] } },
50
+ { title: "L'Appel (The Ask)", content: ["Besoin: 8M FCFA (Machines à emballer, Broyeurs).", "60% Capacité / 20% Marketing / 20% R&D.", "Impact: Production x3 et réduction des Ñàkk (pertes)."], notes: "The Ask." },
51
+ { title: "Contact", content: ["Kaolack, Quartier Léona - Sénégal.", "@grainsdekaolack - Qualité, Santé, Nation.", "Contact@grainsdekaolack.sn"], notes: "End." }
52
+ ]
53
+ };
54
+
55
+ export const MOCK_DECK_FISH = {
56
+ title: "Délices de Kayar : Révolutionner la Transformation Halieutique",
57
+ subtitle: "Qualité, Tradition et Innovation au Service du Sénégal",
58
+ slides: [
59
+ { title: "Couverture", content: ["Délices de Kayar", "Transformation de produits halieutiques", "Kayar, Sénégal"], notes: "Intro." },
60
+ { title: "Le Problème", content: ["Pertes post-capture élevées à Kayar.", "Méthodes traditionnelles peu hygiéniques.", "Faible valeur ajoutée locale impacts."], notes: "Pain point." },
61
+ { title: "La Solution", content: ["Unité de transformation moderne et propre.", "Séchage et fumage contrôlés en inox.", "Packaging premium et traçabilité certifiée."], notes: "Solution." },
62
+ { title: "Marché (TAM)", content: ["Population Dakar: 4,4M consommateurs.", "Marché local: 50 Mds FCFA / an.", "Cible: 5% du marché premium dakarois."], notes: "Data Direction des Pêches.", visualType: "PIE_CHART", visualData: { labels: ["National", "Cible", "SOM"], values: [100, 20, 5] } },
63
+ { title: "Contact", content: ["Site de Kayar, Thiès - Sénégal.", "+221 77 000 00 02", "www.delicesdekayar.sn"], notes: "End." }
64
+ ]
65
+ };
66
+
67
+ export const MOCK_DECK_COUTURE = {
68
+ title: "Sartoria Ndoye : L'Excellence de la Haute Couture",
69
+ subtitle: "Un Standard Institutionnel pour l'Héritage et l'Innovation",
70
+ slides: [
71
+ { title: "Couverture", content: ["Sartoria Ndoye : Maison de Haute Couture de luxe.", "Pont entre héritage et standards mondiaux.", "Exclusivité et prestige pour clientèle exigeante."], notes: "Intro." },
72
+ { title: "Le Problème", content: ["Instabilité des délais de livraison artisanaux.", "Absence de standardisation haut de gamme.", "Manque de professionnalisme en gestion VIP."], notes: "Pain point." },
73
+ { title: "La Solution", content: ["Atelier de précision indus-artisanal.", "Garantie contractuelle de ponctualité.", "Standard 'Luxe Ndoye' avec charte qualité."], notes: "Solution." },
74
+ { title: "Produits", content: ["Boubous Bazin Riche broderies complexes.", "Costumes sur mesure coupes modernes.", "Accessoires exclusifs identité visuelle."], notes: "Gamme." },
75
+ { title: "Marché", content: ["Potentiel habillement luxe: Milliards FCFA.", "Cible Dakar: 4,4M d'habitants premium.", "Objectif: 15% pénétration segment luxe."], notes: "ASND 2023.", visualType: "PIE_CHART", visualData: { labels: ["National", "Premium", "Sartoria"], values: [100, 35, 15] } },
76
+ { title: "Business Model", content: ["Prix Premium (150k - 500k FCFA).", "Ventes Showroom et Instagram VIP.", "Optimisation coûts matières nobles."], notes: "Rentabilité." },
77
+ { title: "Traction", content: ["100 clients VIP fidélisés An 1.", "Accord fournisseurs tissus Mali/Europe.", "Pré-commandes record collection Tabaski."], notes: "Preuves." },
78
+ { title: "Marketing", content: ["Campagnes Instagram immersives.", "Réseau diplomatique et cadres privés.", "Lancements exclusifs Saint-Louis/Dakar."], notes: "Growth." },
79
+ { title: "Concurrence", content: ["Vs Couture Ndar: Rigueur et délais.", "Machines numériques pour broderie rapide.", "Techniques de finition artisanales secrètes."], notes: "Unfair advantage." },
80
+ { title: "Équipe", content: ["M. Ndoye (20 ans exp. Haute Couture).", "Chef d'atelier gestion indus.", "Tailleurs formés aux standards mondiaux."], notes: "Expertise." },
81
+ { title: "Finances", content: ["Croissance CA prévue: +45% / an.", "Objectif Y3: 25M FCFA.", "Amélioration EBITDA par indus."], notes: "5 ans.", visualType: "BAR_CHART", visualData: { labels: ["Y1", "Y2", "Y3", "Y4", "Y5"], values: [8.5, 12, 18, 25, 35] } },
82
+ { title: "L'Appel (The Ask)", content: ["Besoin: 5M FCFA (Machines numériques).", "60% Équipement / 20% FR / 20% Marketing.", "Impact: Capacité +40%, Coût -15%."], notes: "Ask." },
83
+ { title: "Contact", content: ["Cœur de Saint-Louis, Sénégal.", "@sartoriandoye", "Prestige, Tradition, Innovation."], notes: "End." }
84
+ ]
85
+ };
86
+
87
+ export const MOCK_FEEDBACK = {
88
+ isQualified: true,
89
+ praise: "Excellent travail ! Ta vision est claire et ambitieuse.",
90
+ rephrase: "Tu proposes donc une solution innovante pour le marché local.",
91
+ action: "Continue ainsi pour l'étape suivante !",
92
+ confidence: 95,
93
+ notes: "Réponse solide.",
94
+ missingElements: []
95
+ };
apps/api/src/services/ai/ffmpeg.ts CHANGED
@@ -43,8 +43,8 @@ export async function convertToMp3IfNeeded(inputBuffer: Buffer, filename: string
43
 
44
  return { buffer: mp3Buffer, format: 'mp3' };
45
 
46
- } catch (err: any) {
47
- console.error(`[FFMPEG] ⚠️ Conversion failed for ${filename}. Proceeding with original buffer. Error: ${err.message}`);
48
  // If FFMPEG isn't installed or fails, we return the original buffer
49
  return { buffer: inputBuffer, format: filename.split('.').pop()! };
50
  } finally {
 
43
 
44
  return { buffer: mp3Buffer, format: 'mp3' };
45
 
46
+ } catch (err: unknown) {
47
+ console.error(`[FFMPEG] ⚠️ Conversion failed for ${filename}. Proceeding with original buffer. Error: ${(err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err))}`);
48
  // If FFMPEG isn't installed or fails, we return the original buffer
49
  return { buffer: inputBuffer, format: filename.split('.').pop()! };
50
  } finally {
apps/api/src/services/ai/mock-provider.ts CHANGED
@@ -1,4 +1,9 @@
1
  import { LLMProvider, OnePagerSchema, PitchDeckSchema, PersonalizedLessonSchema, FeedbackSchema } from './types';
 
 
 
 
 
2
 
3
  /**
4
  * A Provider for local development that doesn't require an API Key.
@@ -12,102 +17,15 @@ export class MockLLMProvider implements LLMProvider {
12
  const isCereal = prompt.includes('Kaolack') || prompt.includes('Céréales') || prompt.includes('mils');
13
 
14
  if (schema === OnePagerSchema) {
15
- if (isCereal) {
16
- return schema.parse({
17
- title: "Grains de Kaolack : La Tradition du Mil Réinventée",
18
- tagline: "Des céréales locales nutritives pour les familles sénégalaises modernes",
19
- problem: "Les ménages de Kaolack et Dakar importent massivement du riz au détriment des céréales locales (mil, maïs, fonio) car ces dernières sont jugées longues à préparer et pauvres en qualité de conditionnement. Cette dépendance alimentaire fragilise l'économie rurale du bassin arachidier.",
20
- solution: "Grains de Kaolack propose une gamme de céréales pré-cuites, enrichies et conditionnées sous atmosphère protectrice. Nos produits (Couscous de mil, Arraw, Thiakry) conservent toutes leurs valeurs nutritionnelles tout en étant prêts en 5 minutes, offrant une solution saine, rapide et patriotique à la classe moyenne.",
21
- targetAudience: "Classe moyenne urbaine de Kaolack et Dakar, institutions scolaires et diaspora. Un marché estimé à 10 millions de consommateurs potentiels en Afrique de l'Ouest.",
22
- businessModel: "Vente directe en sachets de 500g et 1kg via un réseau de distribution de proximité et grandes surfaces. Marge brute de 30% grâce à des contrats d'approvisionnement direct avec les GIE de producteurs locaux.",
23
- callToAction: "Consommez local, vivez mieux avec Grains de Kaolack.",
24
- mainImage: "https://via.placeholder.com/1024x1024.png?text=Grains+de+Kaolack+Cereal",
25
- marketSources: "Source: ANSD 2024, Ministère de l'Agriculture."
26
- }) as any;
27
- }
28
- if (isFish) {
29
- return schema.parse({
30
- title: "Délices de Kayar : L'Excellence du Poisson Transformé",
31
- tagline: "Valoriser les produits de la mer par l'innovation et la qualité",
32
- problem: "Le gaspillage post-capture à Kayar et l'instabilité des revenus des pêcheurs sont causés par un manque d'infrastructures de transformation moderne. Les produits traditionnels souffrent souvent de problèmes d'hygiène, limitant leur accès aux marchés urbains premium et à l'exportation.",
33
- solution: "Délices de Kayar met en place une unité de transformation de poisson high-tech garantissant une traçabilité totale et des normes sanitaires internationales. Nous produisons du poisson séché, fumé et des conserves artisanales de haute qualité, offrant aux consommateurs dakarois une alternative saine et premium aux produits importés.",
34
- targetAudience: "Notre cible inclut les supermarchés de Dakar, les boutiques de produits locaux haut de gamme, et les foyers de la classe moyenne soucieux de la qualité nutritionnelle et de l'origine de leur alimentation.",
35
- businessModel: "Vente directe B2B (supermarchés) et B2C (boutique en ligne), avec une stratégie de marge élevée basée sur la marque 'Kayar Premium', assurant une juste rémunération aux pêcheurs locaux partenaires.",
36
- callToAction: "Rejoignez la révolution de la transformation locale et goûtez à l'authenticité de Kayar.",
37
- mainImage: "https://via.placeholder.com/1024x1024.png?text=Delices+de+Kayar+Fish"
38
- }) as any;
39
- }
40
-
41
- // Default or Couture
42
- return schema.parse({
43
- title: "Sartoria Ndoye : L'Excellence de la Haute Couture",
44
- tagline: "Le Prestige de Saint-Louis allié à la Précision Contemporaine",
45
- problem: "Le marché premium sénégalais souffre d'un manque de tailleurs capables de garantir une qualité de finition internationale et un respect contractuel des délais de livraison. Cette instabilité chronique dégrade la confiance des clients et limite le potentiel de croissance du secteur de la mode de luxe.",
46
- solution: "Sartoria Ndoye propose un atelier de haute couture qui combine le savoir-faire ancestral de Saint-Louis avec des processus industriels de précision. Nous garantissons une expérience client exclusive, avec une traçabilité totale et des finitions 'Zéro Défaut'.",
47
- targetAudience: "Haute bourgeoisie sénégalaise, cadres dirigeants et diaspora en Europe, soit un segment de plus de 500 000 personnes à fort pouvoir d'achat.",
48
- businessModel: "Modèle de revenus direct basé sur une tarification premium (150k - 500k FCFA) générant une marge brute confortable, complété par un service VIP et numérique.",
49
- callToAction: "Découvrez l'élégance Ndoye et planifiez votre séance de mesures.",
50
- mainImage: "https://via.placeholder.com/1024x1024.png?text=Sartoria+Ndoye+Premium"
51
- }) as any;
52
  }
53
 
54
  if (schema === PitchDeckSchema) {
55
- if (isCereal) {
56
- return schema.parse({
57
- title: "Grains de Kaolack : Révolutionner la Consommation de Mil",
58
- subtitle: "Innovation, Nutrition et Souveraineté Alimentaire",
59
- slides: [
60
- { title: "Couverture", content: ["Grains de Kaolack", "Transformation de céréales locales pré-cuites", "Kaolack, Sénégal"], notes: "Intro." },
61
- { title: "Le Problème", content: ["Dépendance excessive aux importations de riz au Sénégal.", "Temps de préparation trop long des céréales traditionnelles.", "Perte de valeur nutritionnelle due aux méthodes artisanales."], notes: "Pain point." },
62
- { title: "La Solution", content: ["Unité de transformation semi-industrielle à Kaolack.", "Céréales pré-cuites prêtes en 5 minutes chrono.", "Conditionnement hermétique garantissant 12 mois de conservation."], notes: "Solution." },
63
- { title: "Le Produit", content: ["Couscous de mil enrichi à la poudre de baobab.", "Arraw de maïs local sans additifs chimiques.", "Thiakry prêt à l'emploi pour le petit-déjeuner."], notes: "Gamme." },
64
- { title: "Marché (TAM)", content: ["Habilitants Kaolack: 300,000 consommateurs directs.", "Marché des céréales à Dakar: 120 Mds FCFA.", "Cible: 10% du marché des produits pré-cuits."], notes: "Data ANSD.", visualType: "PIE_CHART", visualData: { labels: ["Total", "Cible", "SOM"], values: [100, 30, 10] } },
65
- { title: "Business Model", content: ["Vente directe en boutiques de quartier (Proximité).", "Contrats de distribution avec supermarchés Auchan/Casino.", "Marge nette de 25% sur chaque sachet vendu."], notes: "B-Model." },
66
- { title: "Traction", content: ["Phase pilote réussie avec 200 ménages à Kaolack.", "Référencement en cours dans 5 supérettes locales.", "Certification FRA (Fabrication Française) obtenue."], notes: "Validation." },
67
- { title: "Go-to-Market", content: ["Dégustations sur les marchés hebdomadaires (Loumas).", "Partenariats avec les cantines scolaires rurales.", "Publicité radio en wolof ciblant les mères de famille."], notes: "Growth." },
68
- { title: "Concurrence", content: ["Vs Importateurs: On valorise le produit national.", "Vs Artisans: On garantit l'hygiène et la rapidité.", "Avantage: Maîtrise totale de la source (Bassin Arachidier)."], notes: "Edge." },
69
- { title: "Équipe", content: ["M. Touré (Directeur, 15 ans agro-industrie).", "Responsable Production (Experte en procédés locaux).", "Réseau de 20 femmes pour le tri et le nettoyage."], notes: "People." },
70
- { title: "Finances", content: ["Chiffre d'affaires Y1 estimé à 12M FCFA.", "Rentabilité atteinte dès le 14ème mois.", "Projection Y5: Leader régional du pré-cuit."], notes: "Financials.", visualType: "BAR_CHART", visualData: { labels: ["Y1", "Y2", "Y3", "Y4", "Y5"], values: [12, 18, 28, 40, 55] } },
71
- { title: "L'Appel (The Ask)", content: ["Besoin: 8M FCFA (Machines à emballer, Broyeurs).", "60% Capacité / 20% Marketing / 20% R&D.", "Impact: Production x3 et réduction des Ñàkk (pertes)."], notes: "The Ask." },
72
- { title: "Contact", content: ["Kaolack, Quartier Léona - Sénégal.", "@grainsdekaolack - Qualité, Santé, Nation.", "Contact@grainsdekaolack.sn"], notes: "End." }
73
- ]
74
- }) as any;
75
- }
76
- if (isFish) {
77
- return schema.parse({
78
- title: "Délices de Kayar : Révolutionner la Transformation Halieutique",
79
- subtitle: "Qualité, Tradition et Innovation au Service du Sénégal",
80
- slides: [
81
- { title: "Couverture", content: ["Délices de Kayar", "Transformation de produits halieutiques", "Kayar, Sénégal"], notes: "Intro." },
82
- { title: "Le Problème", content: ["Pertes post-capture élevées à Kayar.", "Méthodes traditionnelles peu hygiéniques.", "Faible valeur ajoutée locale impacts."], notes: "Pain point." },
83
- { title: "La Solution", content: ["Unité de transformation moderne et propre.", "Séchage et fumage contrôlés en inox.", "Packaging premium et traçabilité certifiée."], notes: "Solution." },
84
- { title: "Marché (TAM)", content: ["Population Dakar: 4,4M consommateurs.", "Marché local: 50 Mds FCFA / an.", "Cible: 5% du marché premium dakarois."], notes: "Data Direction des Pêches.", visualType: "PIE_CHART", visualData: { labels: ["National", "Cible", "SOM"], values: [100, 20, 5] } },
85
- { title: "Contact", content: ["Site de Kayar, Thiès - Sénégal.", "+221 77 000 00 02", "www.delicesdekayar.sn"], notes: "End." }
86
- ]
87
- }) as any;
88
- }
89
-
90
- // Default or Couture (High Density)
91
- const mockDeck = {
92
- title: "Sartoria Ndoye : L'Excellence de la Haute Couture",
93
- subtitle: "Un Standard Institutionnel pour l'Héritage et l'Innovation",
94
- slides: [
95
- { title: "Couverture", content: ["Sartoria Ndoye : Maison de Haute Couture de luxe.", "Pont entre héritage et standards mondiaux.", "Exclusivité et prestige pour clientèle exigeante."], notes: "Intro." },
96
- { title: "Le Problème", content: ["Instabilité des délais de livraison artisanaux.", "Absence de standardisation haut de gamme.", "Manque de professionnalisme en gestion VIP."], notes: "Pain point." },
97
- { title: "La Solution", content: ["Atelier de précision indus-artisanal.", "Garantie contractuelle de ponctualité.", "Standard 'Luxe Ndoye' avec charte qualité."], notes: "Solution." },
98
- { title: "Produits", content: ["Boubous Bazin Riche broderies complexes.", "Costumes sur mesure coupes modernes.", "Accessoires exclusifs identité visuelle."], notes: "Gamme." },
99
- { title: "Marché", content: ["Potentiel habillement luxe: Milliards FCFA.", "Cible Dakar: 4,4M d'habitants premium.", "Objectif: 15% pénétration segment luxe."], notes: "ASND 2023.", visualType: "PIE_CHART", visualData: { labels: ["National", "Premium", "Sartoria"], values: [100, 35, 15] } },
100
- { title: "Business Model", content: ["Prix Premium (150k - 500k FCFA).", "Ventes Showroom et Instagram VIP.", "Optimisation coûts matières nobles."], notes: "Rentabilité." },
101
- { title: "Traction", content: ["100 clients VIP fidélisés An 1.", "Accord fournisseurs tissus Mali/Europe.", "Pré-commandes record collection Tabaski."], notes: "Preuves." },
102
- { title: "Marketing", content: ["Campagnes Instagram immersives.", "Réseau diplomatique et cadres privés.", "Lancements exclusifs Saint-Louis/Dakar."], notes: "Growth." },
103
- { title: "Concurrence", content: ["Vs Couture Ndar: Rigueur et délais.", "Machines numériques pour broderie rapide.", "Techniques de finition artisanales secrètes."], notes: "Unfair advantage." },
104
- { title: "Équipe", content: ["M. Ndoye (20 ans exp. Haute Couture).", "Chef d'atelier gestion indus.", "Tailleurs formés aux standards mondiaux."], notes: "Expertise." },
105
- { title: "Finances", content: ["Croissance CA prévue: +45% / an.", "Objectif Y3: 25M FCFA.", "Amélioration EBITDA par indus."], notes: "5 ans.", visualType: "BAR_CHART", visualData: { labels: ["Y1", "Y2", "Y3", "Y4", "Y5"], values: [8.5, 12, 18, 25, 35] } },
106
- { title: "L'Appel (The Ask)", content: ["Besoin: 5M FCFA (Machines numériques).", "60% Équipement / 20% FR / 20% Marketing.", "Impact: Capacité +40%, Coût -15%."], notes: "Ask." },
107
- { title: "Contact", content: ["Cœur de Saint-Louis, Sénégal.", "@sartoriandoye", "Prestige, Tradition, Innovation."], notes: "End." }
108
- ]
109
- };
110
- return schema.parse(mockDeck) as any;
111
  }
112
 
113
  if (schema === PersonalizedLessonSchema) {
@@ -117,15 +35,7 @@ export class MockLLMProvider implements LLMProvider {
117
  }
118
 
119
  if (schema === FeedbackSchema) {
120
- return schema.parse({
121
- isQualified: true,
122
- praise: "Excellent travail ! Ta vision est claire et ambitieuse.",
123
- rephrase: "Tu proposes donc une solution innovante pour le marché local.",
124
- action: "Continue ainsi pour l'étape suivante !",
125
- confidence: 95,
126
- notes: "Réponse solide.",
127
- missingElements: []
128
- }) as any;
129
  }
130
 
131
  throw new Error("MockLLMProvider does not support this schema.");
 
1
  import { LLMProvider, OnePagerSchema, PitchDeckSchema, PersonalizedLessonSchema, FeedbackSchema } from './types';
2
+ import {
3
+ MOCK_ONE_PAGER_CEREAL, MOCK_ONE_PAGER_FISH, MOCK_ONE_PAGER_COUTURE,
4
+ MOCK_DECK_CEREAL, MOCK_DECK_FISH, MOCK_DECK_COUTURE,
5
+ MOCK_FEEDBACK
6
+ } from './__fixtures__/mock-data';
7
 
8
  /**
9
  * A Provider for local development that doesn't require an API Key.
 
17
  const isCereal = prompt.includes('Kaolack') || prompt.includes('Céréales') || prompt.includes('mils');
18
 
19
  if (schema === OnePagerSchema) {
20
+ if (isCereal) return schema.parse(MOCK_ONE_PAGER_CEREAL) as any;
21
+ if (isFish) return schema.parse(MOCK_ONE_PAGER_FISH) as any;
22
+ return schema.parse(MOCK_ONE_PAGER_COUTURE) as any;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  }
24
 
25
  if (schema === PitchDeckSchema) {
26
+ if (isCereal) return schema.parse(MOCK_DECK_CEREAL) as any;
27
+ if (isFish) return schema.parse(MOCK_DECK_FISH) as any;
28
+ return schema.parse(MOCK_DECK_COUTURE) as any;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  }
30
 
31
  if (schema === PersonalizedLessonSchema) {
 
35
  }
36
 
37
  if (schema === FeedbackSchema) {
38
+ return schema.parse(MOCK_FEEDBACK) as any;
 
 
 
 
 
 
 
 
39
  }
40
 
41
  throw new Error("MockLLMProvider does not support this schema.");
apps/api/src/services/ai/openai-provider.ts CHANGED
@@ -51,9 +51,9 @@ export class OpenAIProvider implements LLMProvider {
51
  const result = completion.choices[0]?.message?.parsed;
52
  if (!result) throw new Error('OpenAI failed to return parsed structured data.');
53
  return result as T;
54
- } catch (err: any) {
55
- if (err?.status === 429 || err?.code === 'insufficient_quota') {
56
- const retryAfter = parseInt(err?.headers?.['retry-after'] || '120', 10) * 1000;
57
  console.warn(`[OPENAI] 429 quota exceeded. Retry after ${retryAfter}ms`);
58
  throw new QuotaExceededError(retryAfter);
59
  }
@@ -83,15 +83,15 @@ export class OpenAIProvider implements LLMProvider {
83
  }
84
 
85
  return { text: response.text, confidence };
86
- } catch (err: any) {
87
  console.error('[OPENAI] ❌ Connection or API Error:', {
88
- name: err?.name,
89
- message: err?.message,
90
- status: err?.status,
91
- code: err?.code,
92
- stack: err?.stack
93
  });
94
- if (err?.status === 429 || err?.code === 'insufficient_quota') {
95
  console.warn('[OPENAI] 429 on transcribeAudio');
96
  throw new QuotaExceededError();
97
  }
@@ -109,8 +109,8 @@ export class OpenAIProvider implements LLMProvider {
109
  input: text,
110
  });
111
  return Buffer.from(await mp3.arrayBuffer());
112
- } catch (err: any) {
113
- if (err?.status === 429 || err?.code === 'insufficient_quota') {
114
  console.warn('[OPENAI] 429 on generateSpeech');
115
  throw new QuotaExceededError();
116
  }
@@ -130,7 +130,7 @@ export class OpenAIProvider implements LLMProvider {
130
  response_format: "url"
131
  });
132
  return response.data?.[0]?.url || '';
133
- } catch (err: any) {
134
  console.error('[OPENAI] Image generation failed:', err);
135
  return '';
136
  }
 
51
  const result = completion.choices[0]?.message?.parsed;
52
  if (!result) throw new Error('OpenAI failed to return parsed structured data.');
53
  return result as T;
54
+ } catch (err: unknown) {
55
+ if ((err as any)?.status === 429 || (err as any)?.code === 'insufficient_quota') {
56
+ const retryAfter = parseInt((err as any)?.headers?.['retry-after'] || '120', 10) * 1000;
57
  console.warn(`[OPENAI] 429 quota exceeded. Retry after ${retryAfter}ms`);
58
  throw new QuotaExceededError(retryAfter);
59
  }
 
83
  }
84
 
85
  return { text: response.text, confidence };
86
+ } catch (err: unknown) {
87
  console.error('[OPENAI] ❌ Connection or API Error:', {
88
+ name: (err as any)?.name,
89
+ message: (err as any)?.message,
90
+ status: (err as any)?.status,
91
+ code: (err as any)?.code,
92
+ stack: (err as any)?.stack
93
  });
94
+ if ((err as any)?.status === 429 || (err as any)?.code === 'insufficient_quota') {
95
  console.warn('[OPENAI] 429 on transcribeAudio');
96
  throw new QuotaExceededError();
97
  }
 
109
  input: text,
110
  });
111
  return Buffer.from(await mp3.arrayBuffer());
112
+ } catch (err: unknown) {
113
+ if ((err as any)?.status === 429 || (err as any)?.code === 'insufficient_quota') {
114
  console.warn('[OPENAI] 429 on generateSpeech');
115
  throw new QuotaExceededError();
116
  }
 
130
  response_format: "url"
131
  });
132
  return response.data?.[0]?.url || '';
133
+ } catch (err: unknown) {
134
  console.error('[OPENAI] Image generation failed:', err);
135
  return '';
136
  }
apps/api/src/services/ai/search.ts CHANGED
@@ -46,8 +46,8 @@ export class SearchService {
46
  snippet: r.snippet,
47
  link: r.link
48
  }));
49
- } catch (err: any) {
50
- console.error('[SEARCH_SERVICE] Search failed:', err.message);
51
  return [];
52
  }
53
  }
 
46
  snippet: r.snippet,
47
  link: r.link
48
  }));
49
+ } catch (err: unknown) {
50
+ console.error('[SEARCH_SERVICE] Search failed:', (err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err)));
51
  return [];
52
  }
53
  }
apps/api/src/services/storage.ts CHANGED
@@ -45,8 +45,8 @@ async function uploadToR2(buffer: Buffer, filename: string, contentType: string)
45
  } else {
46
  console.log(`[Storage] ✅ Verified public access: ${finalUrl}`);
47
  }
48
- } catch (err: any) {
49
- console.warn(`[Storage] ⚠️ Could not verify public access for ${finalUrl}: ${err.message}`);
50
  }
51
 
52
  return finalUrl;
@@ -85,8 +85,8 @@ export async function uploadFile(buffer: Buffer, originalFilename: string, conte
85
  if (isR2Configured()) {
86
  try {
87
  return await uploadToR2(buffer, uniqueName, contentType);
88
- } catch (err: any) {
89
- console.error(`[Storage] R2 Upload Failed: ${err.message}. Falling back to local.`);
90
  }
91
  }
92
  return saveLocally(buffer, uniqueName);
 
45
  } else {
46
  console.log(`[Storage] ✅ Verified public access: ${finalUrl}`);
47
  }
48
+ } catch (err: unknown) {
49
+ console.warn(`[Storage] ⚠️ Could not verify public access for ${finalUrl}: ${(err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err))}`);
50
  }
51
 
52
  return finalUrl;
 
85
  if (isR2Configured()) {
86
  try {
87
  return await uploadToR2(buffer, uniqueName, contentType);
88
+ } catch (err: unknown) {
89
+ console.error(`[Storage] R2 Upload Failed: ${(err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err))}. Falling back to local.`);
90
  }
91
  }
92
  return saveLocally(buffer, uniqueName);
apps/api/src/services/stripe.ts CHANGED
@@ -67,8 +67,8 @@ export class StripeService {
67
  signature,
68
  this.webhookSecret
69
  );
70
- } catch (err: any) {
71
- throw new Error(`Webhook Error: ${err.message}`);
72
  }
73
  }
74
  }
 
67
  signature,
68
  this.webhookSecret
69
  );
70
+ } catch (err: unknown) {
71
+ throw new Error(`Webhook Error: ${(err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err))}`);
72
  }
73
  }
74
  }
apps/api/src/services/whatsapp.ts CHANGED
@@ -64,8 +64,8 @@ export class WhatsAppService {
64
  userId: user.id
65
  }
66
  });
67
- } catch (err: any) {
68
- console.error('[WhatsAppService] Failed to log incoming message:', err.message);
69
  }
70
 
71
  // 1.5. Testing / Cheat Codes (Only for registered users)
@@ -130,17 +130,17 @@ export class WhatsAppService {
130
  await (prisma as any).businessProfile.deleteMany({ where: { userId: user.id } });
131
  await prisma.user.update({ where: { id: user.id }, data: { activity: null } });
132
  console.log(`[SEED] Cleared cognitive cache for User ${user.id}`);
133
- } catch (cacheErr: any) {
134
- console.error('[SEED] Failed to clear cognitive cache:', cacheErr.message);
135
  }
136
 
137
  await scheduleMessage(user.id, result.seeded
138
  ? "✅ Seeding terminé ! Le Cache Cognitif a été réinitialisé.\nEnvoie INSCRIPTION pour commencer."
139
  : "ℹ️ Les données existent déjà. Cache Cognitif purgé. Envoie INSCRIPTION."
140
  );
141
- } catch (err: any) {
142
- console.error('[SEED] Error:', err.message);
143
- await scheduleMessage(user.id, `❌ Erreur seed : ${err.message?.substring(0, 200)}`);
144
  }
145
  return;
146
  }
 
64
  userId: user.id
65
  }
66
  });
67
+ } catch (err: unknown) {
68
+ console.error('[WhatsAppService] Failed to log incoming message:', (err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err)));
69
  }
70
 
71
  // 1.5. Testing / Cheat Codes (Only for registered users)
 
130
  await (prisma as any).businessProfile.deleteMany({ where: { userId: user.id } });
131
  await prisma.user.update({ where: { id: user.id }, data: { activity: null } });
132
  console.log(`[SEED] Cleared cognitive cache for User ${user.id}`);
133
+ } catch (cacheErr: unknown) {
134
+ console.error('[SEED] Failed to clear cognitive cache:', (cacheErr as Error).message);
135
  }
136
 
137
  await scheduleMessage(user.id, result.seeded
138
  ? "✅ Seeding terminé ! Le Cache Cognitif a été réinitialisé.\nEnvoie INSCRIPTION pour commencer."
139
  : "ℹ️ Les données existent déjà. Cache Cognitif purgé. Envoie INSCRIPTION."
140
  );
141
+ } catch (err: unknown) {
142
+ console.error('[SEED] Error:', (err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err)));
143
+ await scheduleMessage(user.id, `❌ Erreur seed : ${(err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err))?.substring(0, 200)}`);
144
  }
145
  return;
146
  }
apps/api/tests/integration/kaolack-journey.test.ts ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { describe, it, expect, beforeAll } from 'vitest';
2
+ import { PrismaClient } from '@prisma/client';
3
+ import { aiService } from '../../src/services/ai';
4
+ import * as path from 'path';
5
+ import * as dotenv from 'dotenv';
6
+
7
+ dotenv.config({ path: path.join(__dirname, '../../../../../.env') });
8
+
9
+ const prisma = new PrismaClient();
10
+ const KAOLACK_PHONE = '221770000003';
11
+
12
+ describe('Kaolack Elite Journey Simulation', () => {
13
+ let trackId: string;
14
+ let userId: string;
15
+
16
+ beforeAll(async () => {
17
+ // 1. Cleanup before test
18
+ await prisma.userProgress.deleteMany({ where: { user: { phone: KAOLACK_PHONE } } });
19
+ await prisma.response.deleteMany({ where: { user: { phone: KAOLACK_PHONE } } });
20
+ await prisma.enrollment.deleteMany({ where: { user: { phone: KAOLACK_PHONE } } });
21
+ await prisma.message.deleteMany({ where: { user: { phone: KAOLACK_PHONE } } });
22
+ await prisma.businessProfile.deleteMany({ where: { user: { phone: KAOLACK_PHONE } } });
23
+ await prisma.user.deleteMany({ where: { phone: KAOLACK_PHONE } });
24
+
25
+ const track = await prisma.track.findFirst({ where: { language: 'FR', title: { contains: "Comprendre" } } });
26
+ if (!track) throw new Error("Track T1-FR non trouvé.");
27
+ trackId = track.id;
28
+
29
+ // 2. User Creation (Grains de Kaolack)
30
+ const user = await prisma.user.create({
31
+ data: {
32
+ phone: KAOLACK_PHONE,
33
+ name: 'Grains de Kaolack',
34
+ language: 'FR',
35
+ activity: 'Transformation de céréales locales (Mil, Maïs)',
36
+ city: 'Kaolack',
37
+ businessProfile: {
38
+ create: {
39
+ activityLabel: 'Grains de Kaolack - Transformation Céréalière',
40
+ locationCity: 'Kaolack',
41
+ marketData: {
42
+ source: "ANSD 2023",
43
+ population_kaolack: "300,000",
44
+ } as any,
45
+ competitorList: ["Importations riz/blé"],
46
+ financialProjections: { revenueY1: "12 000 000 FCFA" },
47
+ fundingAsk: "8 000 000 FCFA pour automatisation"
48
+ }
49
+ }
50
+ } as any,
51
+ include: { businessProfile: true } as any
52
+ }) as any;
53
+ userId = user.id;
54
+
55
+ // Mock an enrollment
56
+ await prisma.enrollment.create({
57
+ data: {
58
+ userId,
59
+ trackId,
60
+ status: 'COMPLETED',
61
+ currentDay: 13
62
+ } as any
63
+ });
64
+ });
65
+
66
+ it('should generate high-density Pitch Deck data from business profile', async () => {
67
+ const user = await prisma.user.findUnique({
68
+ where: { id: userId },
69
+ include: { businessProfile: true }
70
+ }) as any;
71
+
72
+ const userContext = `AUDIT : Transformation de Céréales à Kaolack.`;
73
+ const deckData = await aiService.generatePitchDeckData(userContext, 'FR', user.businessProfile);
74
+
75
+ // Verify Genspark-Standard Requirements are met in the generation:
76
+ expect(deckData).toBeDefined();
77
+
78
+ // Ensure no arrays of weak points - strict storytelling check via length/content
79
+ expect(deckData.slides.length).toBeGreaterThan(5);
80
+
81
+ const marketSlide = deckData.slides.find(s => s.title.toLowerCase().includes('marché') || s.title.toLowerCase().includes('market'));
82
+ expect(marketSlide).toBeDefined();
83
+
84
+ // The market slide must mention Kaolack sourced from ANSD
85
+ const marketContent = ((marketSlide?.content || []).join(' ') + ' ' + (marketSlide?.notes || '')).toLowerCase();
86
+ expect(marketContent).toContain('ansd');
87
+ expect(marketContent).toContain('300,000');
88
+ }, 60000); // 60s timeout for OpenAI API call
89
+
90
+ it('should generate a One-Pager successfully', async () => {
91
+ const user = await prisma.user.findUnique({
92
+ where: { id: userId },
93
+ include: { businessProfile: true }
94
+ }) as any;
95
+
96
+ const userContext = `AUDIT : Kaolack Céréales.`;
97
+ const pdfData = await aiService.generateOnePagerData(userContext, 'FR', user.businessProfile);
98
+
99
+ expect(pdfData).toBeDefined();
100
+ expect(pdfData.title).toBeDefined();
101
+ // Zod validation is implicitly guaranteed by the provider's `parse` returns if no exception is thrown.
102
+ }, 60000);
103
+ });
apps/api/vitest.config.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ setupFiles: [],
8
+ coverage: {
9
+ provider: 'v8',
10
+ reporter: ['text', 'json', 'html'],
11
+ },
12
+ testTimeout: 30000,
13
+ },
14
+ });
apps/whatsapp-worker/src/fix-types.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+
4
+ function replaceInFile(filePath: string, replacements: [RegExp, string][]) {
5
+ const fullPath = path.resolve(__dirname, '..', filePath);
6
+ if (!fs.existsSync(fullPath)) return;
7
+ let content = fs.readFileSync(fullPath, 'utf8');
8
+ for (const [regex, replacement] of replacements) {
9
+ content = content.replace(regex, replacement);
10
+ }
11
+ fs.writeFileSync(fullPath, content);
12
+ console.log(`Fixed: ${filePath}`);
13
+ }
14
+
15
+ replaceInFile('src/whatsapp-cloud.ts', [
16
+ [/err\.response\?\.data\?\.error\?\.(?:message|error_user_msg)/g, '(err as any)?.response?.data?.error?.message']
17
+ ]);
18
+
apps/whatsapp-worker/src/index.ts CHANGED
@@ -107,8 +107,8 @@ const worker = new Worker('whatsapp-queue', async (job: Job) => {
107
  const errText = await feedbackRes.text();
108
  throw new Error(`generate-feedback failed HTTP ${feedbackRes.status}: ${errText}`);
109
  }
110
- } catch (err: any) {
111
- console.error(`[WORKER] generate-feedback failed:`, err.message);
112
  throw err;
113
  }
114
 
@@ -164,14 +164,14 @@ const worker = new Worker('whatsapp-queue', async (job: Job) => {
164
  ? "Sa kàrdu business mu neex ! ✨"
165
  : "Ta carte business personnalisée ! ✨";
166
  await sendImageMessage(user.phone, cardUrl, caption);
167
- } catch (vErr: any) {
168
- console.error('[WORKER] Pitch Card generation failed:', vErr.message);
169
  }
170
  }
171
  }
172
  }
173
- } catch (err: any) {
174
- console.error('[WORKER] BusinessProfile extraction failed:', err.message);
175
  }
176
  }
177
 
@@ -212,13 +212,24 @@ const worker = new Worker('whatsapp-queue', async (job: Job) => {
212
  where: { userId_trackId: { userId, trackId } },
213
  data: {
214
  exerciseStatus: 'PENDING_REMEDIATION', // Stay in remediation until final success
215
- score: { increment: 0 },
216
- marketData: feedbackData?.searchResults,
217
- competitorList: (feedbackData as any)?.competitorList,
218
- financialProjections: (feedbackData as any)?.financialProjections,
219
- fundingAsk: (feedbackData as any)?.fundingAsk
220
  } as any
221
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  } else {
223
  // Success! Award Badges & Mark Completed
224
  const trackDayBadges = (trackDay as any)?.badges as string[] || [];
@@ -232,14 +243,25 @@ const worker = new Worker('whatsapp-queue', async (job: Job) => {
232
  exerciseStatus: 'COMPLETED',
233
  score: { increment: 1 },
234
  badges: updatedBadges,
235
- behavioralScoring: updateBehavioralScore((currentProgress as any)?.behavioralScoring, (exerciseCriteria as any)?.scoring?.impact_success),
236
- marketData: feedbackData?.searchResults,
237
- competitorList: (feedbackData as any)?.competitorList,
238
- financialProjections: (feedbackData as any)?.financialProjections,
239
- fundingAsk: (feedbackData as any)?.fundingAsk
240
  } as any
241
  });
242
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  // If we were in a remediation day (fractional) -> move to next integer day
244
  if (currentDay % 1 !== 0) {
245
  nextDay = Math.floor(currentDay) + 1;
@@ -433,8 +455,8 @@ const worker = new Worker('whatsapp-queue', async (job: Job) => {
433
  console.log(`[R2] Inbound audio uploaded: ${audioUrl}`);
434
  }
435
  }
436
- } catch (err: any) {
437
- console.error('[WORKER] store-audio failed (inbound audio will not have a permanent link):', err.message);
438
  }
439
 
440
  // ─── Hardening: Record Inbound Message in DB ──────────
@@ -451,8 +473,8 @@ const worker = new Worker('whatsapp-queue', async (job: Job) => {
451
  }
452
  });
453
  console.log(`[DB] Recorded inbound audio message for ${phone}`);
454
- } catch (dbErr: any) {
455
- console.error('[DB] Failed to record inbound message:', dbErr.message);
456
  }
457
  }
458
 
@@ -597,8 +619,8 @@ const worker = new Worker('whatsapp-queue', async (job: Job) => {
597
  console.log(`${traceId} API handle-message success.`);
598
  }
599
 
600
- } catch (err: any) {
601
- console.error(`[WORKER] download-media failed for ${mediaId}:`, err.message);
602
  throw err;
603
  }
604
  }
@@ -607,8 +629,8 @@ const worker = new Worker('whatsapp-queue', async (job: Job) => {
607
  try {
608
  await sendImageMessage(to, imageUrl, caption || '');
609
  console.log(`[WhatsApp] ✅ Image message sent to ${to}`);
610
- } catch (err: any) {
611
- console.error(`[WORKER] send-image failed:`, err.message);
612
  }
613
  }
614
  else if (job.name === 'send-content') {
@@ -811,5 +833,5 @@ worker.on('completed', job => {
811
  });
812
 
813
  worker.on('failed', (job, err) => {
814
- console.error(`[WORKER] Job ${job?.id} has failed with ${err.message}`);
815
  });
 
107
  const errText = await feedbackRes.text();
108
  throw new Error(`generate-feedback failed HTTP ${feedbackRes.status}: ${errText}`);
109
  }
110
+ } catch (err: unknown) {
111
+ console.error(`[WORKER] generate-feedback failed:`, (err instanceof Error ? err.message : String(err)));
112
  throw err;
113
  }
114
 
 
164
  ? "Sa kàrdu business mu neex ! ✨"
165
  : "Ta carte business personnalisée ! ✨";
166
  await sendImageMessage(user.phone, cardUrl, caption);
167
+ } catch (vErr: unknown) {
168
+ console.error('[WORKER] Pitch Card generation failed:', (vErr as any)?.message);
169
  }
170
  }
171
  }
172
  }
173
+ } catch (err: unknown) {
174
+ console.error('[WORKER] BusinessProfile extraction failed:', (err instanceof Error ? err.message : String(err)));
175
  }
176
  }
177
 
 
212
  where: { userId_trackId: { userId, trackId } },
213
  data: {
214
  exerciseStatus: 'PENDING_REMEDIATION', // Stay in remediation until final success
215
+ score: { increment: 0 }
 
 
 
 
216
  } as any
217
  });
218
+
219
+ // 🚨 Store Strategy Data in BusinessProfile
220
+ if (feedbackData?.searchResults || (feedbackData as any)?.competitorList || (feedbackData as any)?.financialProjections || (feedbackData as any)?.fundingAsk) {
221
+ const updatePayload: any = { lastUpdatedFromDay: currentDay };
222
+ if (feedbackData?.searchResults) updatePayload.marketData = feedbackData.searchResults;
223
+ if ((feedbackData as any)?.competitorList) updatePayload.competitorList = (feedbackData as any).competitorList;
224
+ if ((feedbackData as any)?.financialProjections) updatePayload.financialProjections = (feedbackData as any).financialProjections;
225
+ if ((feedbackData as any)?.fundingAsk) updatePayload.fundingAsk = (feedbackData as any).fundingAsk;
226
+
227
+ await (prisma as any).businessProfile.upsert({
228
+ where: { userId },
229
+ update: updatePayload,
230
+ create: { userId, ...updatePayload }
231
+ });
232
+ }
233
  } else {
234
  // Success! Award Badges & Mark Completed
235
  const trackDayBadges = (trackDay as any)?.badges as string[] || [];
 
243
  exerciseStatus: 'COMPLETED',
244
  score: { increment: 1 },
245
  badges: updatedBadges,
246
+ behavioralScoring: updateBehavioralScore((currentProgress as any)?.behavioralScoring, (exerciseCriteria as any)?.scoring?.impact_success)
 
 
 
 
247
  } as any
248
  });
249
 
250
+ // 🚨 Store Strategy Data in BusinessProfile
251
+ if (feedbackData?.searchResults || (feedbackData as any)?.competitorList || (feedbackData as any)?.financialProjections || (feedbackData as any)?.fundingAsk) {
252
+ const updatePayload: any = { lastUpdatedFromDay: currentDay };
253
+ if (feedbackData?.searchResults) updatePayload.marketData = feedbackData.searchResults;
254
+ if ((feedbackData as any)?.competitorList) updatePayload.competitorList = (feedbackData as any).competitorList;
255
+ if ((feedbackData as any)?.financialProjections) updatePayload.financialProjections = (feedbackData as any).financialProjections;
256
+ if ((feedbackData as any)?.fundingAsk) updatePayload.fundingAsk = (feedbackData as any).fundingAsk;
257
+
258
+ await (prisma as any).businessProfile.upsert({
259
+ where: { userId },
260
+ update: updatePayload,
261
+ create: { userId, ...updatePayload }
262
+ });
263
+ }
264
+
265
  // If we were in a remediation day (fractional) -> move to next integer day
266
  if (currentDay % 1 !== 0) {
267
  nextDay = Math.floor(currentDay) + 1;
 
455
  console.log(`[R2] Inbound audio uploaded: ${audioUrl}`);
456
  }
457
  }
458
+ } catch (err: unknown) {
459
+ console.error('[WORKER] store-audio failed (inbound audio will not have a permanent link):', (err instanceof Error ? err.message : String(err)));
460
  }
461
 
462
  // ─── Hardening: Record Inbound Message in DB ──────────
 
473
  }
474
  });
475
  console.log(`[DB] Recorded inbound audio message for ${phone}`);
476
+ } catch (dbErr: unknown) {
477
+ console.error('[DB] Failed to record inbound message:', (dbErr as any)?.message);
478
  }
479
  }
480
 
 
619
  console.log(`${traceId} API handle-message success.`);
620
  }
621
 
622
+ } catch (err: unknown) {
623
+ console.error(`[WORKER] download-media failed for ${mediaId}:`, (err instanceof Error ? err.message : String(err)));
624
  throw err;
625
  }
626
  }
 
629
  try {
630
  await sendImageMessage(to, imageUrl, caption || '');
631
  console.log(`[WhatsApp] ✅ Image message sent to ${to}`);
632
+ } catch (err: unknown) {
633
+ console.error(`[WORKER] send-image failed:`, (err instanceof Error ? err.message : String(err)));
634
  }
635
  }
636
  else if (job.name === 'send-content') {
 
833
  });
834
 
835
  worker.on('failed', (job, err) => {
836
+ console.error(`[WORKER] Job ${job?.id} has failed with ${(err instanceof Error ? err.message : String(err))}`);
837
  });
apps/whatsapp-worker/src/pedagogy.ts CHANGED
@@ -166,8 +166,8 @@ export async function sendLessonDay(userId: string, trackId: string, dayNumber:
166
  try {
167
  await sendVideoMessage(user.phone, vUrl, vCaption);
168
  console.log(`[VIDEO_OK] WhatsApp accepted video for ${user.phone}`);
169
- } catch (vErr: any) {
170
- console.warn(`[VIDEO_FALLBACK] reason=${vErr.message}. Sending image fallback for ${user.phone}`);
171
 
172
  // Fallback: Image + Link + "Clique pour regarder"
173
  const fallbackText = isWolof
@@ -197,8 +197,8 @@ export async function sendLessonDay(userId: string, trackId: string, dayNumber:
197
  console.log(`[PEDAGOGY] Missing imageUrl on Day ${dayNumber}! Using fallback: ${fallbackImageUrl}`);
198
  try {
199
  await sendImageMessage(user.phone, fallbackImageUrl);
200
- } catch (e: any) {
201
- console.warn(`[PEDAGOGY] Fallback image also failed: ${e.message}`);
202
  }
203
  }
204
 
@@ -242,8 +242,8 @@ export async function sendLessonDay(userId: string, trackId: string, dayNumber:
242
  mediaUrl: finalAudioUrl
243
  }
244
  });
245
- } catch (dbErr: any) {
246
- console.error('[DB] Failed to record outbound audio:', dbErr.message);
247
  }
248
 
249
  // Send the text as a separate short message
 
166
  try {
167
  await sendVideoMessage(user.phone, vUrl, vCaption);
168
  console.log(`[VIDEO_OK] WhatsApp accepted video for ${user.phone}`);
169
+ } catch (vErr: unknown) {
170
+ console.warn(`[VIDEO_FALLBACK] reason=${(vErr as any)?.message}. Sending image fallback for ${user.phone}`);
171
 
172
  // Fallback: Image + Link + "Clique pour regarder"
173
  const fallbackText = isWolof
 
197
  console.log(`[PEDAGOGY] Missing imageUrl on Day ${dayNumber}! Using fallback: ${fallbackImageUrl}`);
198
  try {
199
  await sendImageMessage(user.phone, fallbackImageUrl);
200
+ } catch (e: unknown) {
201
+ console.warn(`[PEDAGOGY] Fallback image also failed: ${(e instanceof Error ? e.message : String(e))}`);
202
  }
203
  }
204
 
 
242
  mediaUrl: finalAudioUrl
243
  }
244
  });
245
+ } catch (dbErr: unknown) {
246
+ console.error('[DB] Failed to record outbound audio:', (dbErr as any)?.message);
247
  }
248
 
249
  // Send the text as a separate short message
apps/whatsapp-worker/src/whatsapp-cloud.ts CHANGED
@@ -50,8 +50,8 @@ export async function sendTextMessage(to: string, text: string): Promise<void> {
50
 
51
  try {
52
  await axios.post(getBaseUrl(), body, { headers: getHeaders() });
53
- } catch (err: any) {
54
- throw new Error(`[WhatsApp] sendTextMessage failed: ${err.response?.data?.error?.message || err.message}`);
55
  }
56
 
57
  console.log(`[WhatsApp] ✅ Text message sent to ${to}`);
@@ -82,8 +82,8 @@ export async function sendImageMessage(to: string, imageUrl: string, caption?: s
82
 
83
  try {
84
  await axios.post(getBaseUrl(), body, { headers: getHeaders() });
85
- } catch (err: any) {
86
- throw new Error(`[WhatsApp] sendImageMessage failed for URL [${imageUrl}]: ${err.response?.data?.error?.message || err.message}`);
87
  }
88
 
89
  console.log(`[WhatsApp] ✅ Image message sent to ${to}`);
@@ -115,8 +115,8 @@ export async function sendDocumentMessage(to: string, fileUrl: string, filename:
115
 
116
  try {
117
  await axios.post(getBaseUrl(), body, { headers: getHeaders() });
118
- } catch (err: any) {
119
- throw new Error(`[WhatsApp] sendDocumentMessage failed: ${err.response?.data?.error?.message || err.message}`);
120
  }
121
 
122
  console.log(`[WhatsApp] ✅ Document "${filename}" sent to ${to}`);
@@ -142,8 +142,8 @@ export async function sendAudioMessage(to: string, audioUrl: string): Promise<vo
142
 
143
  try {
144
  await axios.post(getBaseUrl(), body, { headers: getHeaders() });
145
- } catch (err: any) {
146
- throw new Error(`[WhatsApp] sendAudioMessage failed for URL [${audioUrl}]: ${err.response?.data?.error?.message || err.message}`);
147
  }
148
 
149
  console.log(`[WhatsApp] ✅ Audio message sent to ${to}`);
@@ -170,8 +170,8 @@ export async function sendVideoMessage(to: string, videoUrl: string, caption?: s
170
 
171
  try {
172
  await axios.post(getBaseUrl(), body, { headers: getHeaders() });
173
- } catch (err: any) {
174
- throw new Error(`[WhatsApp] sendVideoMessage failed for URL [${videoUrl}]: ${err.response?.data?.error?.message || err.message}`);
175
  }
176
 
177
  console.log(`[WhatsApp] ✅ Video message sent to ${to}`);
@@ -216,8 +216,8 @@ export async function sendInteractiveButtonMessage(
216
 
217
  try {
218
  await axios.post(getBaseUrl(), body, { headers: getHeaders() });
219
- } catch (err: any) {
220
- throw new Error(`[WhatsApp] sendInteractiveButtonMessage failed: ${err.response?.data?.error?.message || err.message}`);
221
  }
222
 
223
  console.log(`[WhatsApp] ✅ Interactive message sent to ${to}`);
@@ -271,9 +271,9 @@ export async function sendInteractiveListMessage(
271
  try {
272
  await axios.post(getBaseUrl(), body, { headers: getHeaders() });
273
  console.log(`[WhatsApp] ✅ List message sent to ${to}`);
274
- } catch (err: any) {
275
  // Fallback to text if interactive list fails (e.g., WhatsApp doesn't support it)
276
- console.warn(`[WhatsApp] List message failed, falling back to text: ${err.response?.data?.error?.message || err.message}`);
277
  const fallback = sections.flatMap(s => s.rows.map((r, i) => `${i + 1}. ${r.title}`)).join('\n');
278
  await sendTextMessage(to, `${bodyText}\n\n${fallback}`);
279
  }
 
50
 
51
  try {
52
  await axios.post(getBaseUrl(), body, { headers: getHeaders() });
53
+ } catch (err: unknown) {
54
+ throw new Error(`[WhatsApp] sendTextMessage failed: ${(err as any)?.response?.data?.error?.message || (err instanceof Error ? err.message : String(err))}`);
55
  }
56
 
57
  console.log(`[WhatsApp] ✅ Text message sent to ${to}`);
 
82
 
83
  try {
84
  await axios.post(getBaseUrl(), body, { headers: getHeaders() });
85
+ } catch (err: unknown) {
86
+ throw new Error(`[WhatsApp] sendImageMessage failed for URL [${imageUrl}]: ${(err as any)?.response?.data?.error?.message || (err instanceof Error ? err.message : String(err))}`);
87
  }
88
 
89
  console.log(`[WhatsApp] ✅ Image message sent to ${to}`);
 
115
 
116
  try {
117
  await axios.post(getBaseUrl(), body, { headers: getHeaders() });
118
+ } catch (err: unknown) {
119
+ throw new Error(`[WhatsApp] sendDocumentMessage failed: ${(err as any)?.response?.data?.error?.message || (err instanceof Error ? err.message : String(err))}`);
120
  }
121
 
122
  console.log(`[WhatsApp] ✅ Document "${filename}" sent to ${to}`);
 
142
 
143
  try {
144
  await axios.post(getBaseUrl(), body, { headers: getHeaders() });
145
+ } catch (err: unknown) {
146
+ throw new Error(`[WhatsApp] sendAudioMessage failed for URL [${audioUrl}]: ${(err as any)?.response?.data?.error?.message || (err instanceof Error ? err.message : String(err))}`);
147
  }
148
 
149
  console.log(`[WhatsApp] ✅ Audio message sent to ${to}`);
 
170
 
171
  try {
172
  await axios.post(getBaseUrl(), body, { headers: getHeaders() });
173
+ } catch (err: unknown) {
174
+ throw new Error(`[WhatsApp] sendVideoMessage failed for URL [${videoUrl}]: ${(err as any)?.response?.data?.error?.message || (err instanceof Error ? err.message : String(err))}`);
175
  }
176
 
177
  console.log(`[WhatsApp] ✅ Video message sent to ${to}`);
 
216
 
217
  try {
218
  await axios.post(getBaseUrl(), body, { headers: getHeaders() });
219
+ } catch (err: unknown) {
220
+ throw new Error(`[WhatsApp] sendInteractiveButtonMessage failed: ${(err as any)?.response?.data?.error?.message || (err instanceof Error ? err.message : String(err))}`);
221
  }
222
 
223
  console.log(`[WhatsApp] ✅ Interactive message sent to ${to}`);
 
271
  try {
272
  await axios.post(getBaseUrl(), body, { headers: getHeaders() });
273
  console.log(`[WhatsApp] ✅ List message sent to ${to}`);
274
+ } catch (err: unknown) {
275
  // Fallback to text if interactive list fails (e.g., WhatsApp doesn't support it)
276
+ console.warn(`[WhatsApp] List message failed, falling back to text: ${(err as any)?.response?.data?.error?.message || (err instanceof Error ? err.message : String(err))}`);
277
  const fallback = sections.flatMap(s => s.rows.map((r, i) => `${i + 1}. ${r.title}`)).join('\n');
278
  await sendTextMessage(to, `${bodyText}\n\n${fallback}`);
279
  }
packages/database/package.json CHANGED
@@ -10,13 +10,15 @@
10
  "scripts": {
11
  "db:push": "prisma db push",
12
  "db:studio": "prisma studio",
13
- "generate": "prisma generate"
 
14
  },
15
  "prisma": {
16
  "seed": "tsx seed.ts"
17
  },
18
  "dependencies": {
19
- "@prisma/client": "^5.0.0"
 
20
  },
21
  "devDependencies": {
22
  "@repo/tsconfig": "workspace:*",
 
10
  "scripts": {
11
  "db:push": "prisma db push",
12
  "db:studio": "prisma studio",
13
+ "generate": "prisma generate",
14
+ "validate:content": "ts-node scripts/validate-content.ts"
15
  },
16
  "prisma": {
17
  "seed": "tsx seed.ts"
18
  },
19
  "dependencies": {
20
+ "@prisma/client": "^5.0.0",
21
+ "zod": "^4.3.6"
22
  },
23
  "devDependencies": {
24
  "@repo/tsconfig": "workspace:*",
packages/database/prisma/migrations/20260307212923_move_pitchdeck_fields/migration.sql ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- CreateEnum
2
+ CREATE TYPE "Role" AS ENUM ('STUDENT', 'ADMIN');
3
+
4
+ -- CreateEnum
5
+ CREATE TYPE "EnrollmentStatus" AS ENUM ('ACTIVE', 'COMPLETED', 'DROPPED');
6
+
7
+ -- CreateEnum
8
+ CREATE TYPE "Language" AS ENUM ('FR', 'WOLOF');
9
+
10
+ -- CreateEnum
11
+ CREATE TYPE "ContentType" AS ENUM ('TEXT', 'AUDIO', 'IMAGE', 'VIDEO');
12
+
13
+ -- CreateEnum
14
+ CREATE TYPE "Direction" AS ENUM ('INBOUND', 'OUTBOUND');
15
+
16
+ -- CreateEnum
17
+ CREATE TYPE "PaymentStatus" AS ENUM ('PENDING', 'COMPLETED', 'FAILED', 'REFUNDED');
18
+
19
+ -- CreateEnum
20
+ CREATE TYPE "ExerciseType" AS ENUM ('TEXT', 'AUDIO', 'BUTTON');
21
+
22
+ -- CreateEnum
23
+ CREATE TYPE "ExerciseStatus" AS ENUM ('PENDING', 'PENDING_REMEDIATION', 'PENDING_REVIEW', 'COMPLETED');
24
+
25
+ -- CreateEnum
26
+ CREATE TYPE "TrainingStatus" AS ENUM ('PENDING', 'REVIEWED', 'IGNORED');
27
+
28
+ -- CreateTable
29
+ CREATE TABLE "User" (
30
+ "id" TEXT NOT NULL,
31
+ "phone" TEXT NOT NULL,
32
+ "name" TEXT,
33
+ "role" "Role" NOT NULL DEFAULT 'STUDENT',
34
+ "language" "Language" NOT NULL DEFAULT 'FR',
35
+ "city" TEXT,
36
+ "activity" TEXT,
37
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
38
+ "updatedAt" TIMESTAMP(3) NOT NULL,
39
+ "currentStreak" INTEGER NOT NULL DEFAULT 0,
40
+ "longestStreak" INTEGER NOT NULL DEFAULT 0,
41
+ "lastActivityAt" TIMESTAMP(3),
42
+
43
+ CONSTRAINT "User_pkey" PRIMARY KEY ("id")
44
+ );
45
+
46
+ -- CreateTable
47
+ CREATE TABLE "BusinessProfile" (
48
+ "id" TEXT NOT NULL,
49
+ "userId" TEXT NOT NULL,
50
+ "activityLabel" TEXT,
51
+ "activityPhrase" TEXT,
52
+ "activityType" TEXT,
53
+ "locationCity" TEXT,
54
+ "mainCustomer" TEXT,
55
+ "mainProblem" TEXT,
56
+ "offerSimple" TEXT,
57
+ "promise" TEXT,
58
+ "marketData" JSONB,
59
+ "competitorList" JSONB,
60
+ "financialProjections" JSONB,
61
+ "fundingAsk" TEXT,
62
+ "lastUpdatedFromDay" INTEGER NOT NULL DEFAULT 0,
63
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
64
+ "updatedAt" TIMESTAMP(3) NOT NULL,
65
+
66
+ CONSTRAINT "BusinessProfile_pkey" PRIMARY KEY ("id")
67
+ );
68
+
69
+ -- CreateTable
70
+ CREATE TABLE "Track" (
71
+ "id" TEXT NOT NULL,
72
+ "title" TEXT NOT NULL,
73
+ "description" TEXT,
74
+ "duration" INTEGER NOT NULL,
75
+ "language" "Language" NOT NULL DEFAULT 'FR',
76
+ "isPremium" BOOLEAN NOT NULL DEFAULT false,
77
+ "priceAmount" INTEGER,
78
+ "stripePriceId" TEXT,
79
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
80
+ "updatedAt" TIMESTAMP(3) NOT NULL,
81
+
82
+ CONSTRAINT "Track_pkey" PRIMARY KEY ("id")
83
+ );
84
+
85
+ -- CreateTable
86
+ CREATE TABLE "TrackDay" (
87
+ "id" TEXT NOT NULL,
88
+ "trackId" TEXT NOT NULL,
89
+ "dayNumber" DOUBLE PRECISION NOT NULL,
90
+ "title" TEXT,
91
+ "audioUrl" TEXT,
92
+ "imageUrl" TEXT,
93
+ "videoUrl" TEXT,
94
+ "videoCaption" TEXT,
95
+ "lessonText" TEXT,
96
+ "exerciseType" "ExerciseType" NOT NULL DEFAULT 'TEXT',
97
+ "exercisePrompt" TEXT,
98
+ "validationKeyword" TEXT,
99
+ "buttonsJson" JSONB,
100
+ "exerciseCriteria" JSONB,
101
+ "badges" JSONB,
102
+ "unlockCondition" TEXT,
103
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
104
+ "updatedAt" TIMESTAMP(3) NOT NULL,
105
+
106
+ CONSTRAINT "TrackDay_pkey" PRIMARY KEY ("id")
107
+ );
108
+
109
+ -- CreateTable
110
+ CREATE TABLE "UserProgress" (
111
+ "id" TEXT NOT NULL,
112
+ "userId" TEXT NOT NULL,
113
+ "trackId" TEXT NOT NULL,
114
+ "score" INTEGER NOT NULL DEFAULT 0,
115
+ "lastInteraction" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
116
+ "exerciseStatus" "ExerciseStatus" NOT NULL DEFAULT 'PENDING',
117
+ "badges" JSONB,
118
+ "behavioralScoring" JSONB,
119
+ "confidenceScore" DOUBLE PRECISION,
120
+ "adminTranscription" TEXT,
121
+ "overrideAudioUrl" TEXT,
122
+ "reviewedBy" TEXT,
123
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
124
+ "updatedAt" TIMESTAMP(3) NOT NULL,
125
+
126
+ CONSTRAINT "UserProgress_pkey" PRIMARY KEY ("id")
127
+ );
128
+
129
+ -- CreateTable
130
+ CREATE TABLE "Enrollment" (
131
+ "id" TEXT NOT NULL,
132
+ "userId" TEXT NOT NULL,
133
+ "trackId" TEXT NOT NULL,
134
+ "status" "EnrollmentStatus" NOT NULL DEFAULT 'ACTIVE',
135
+ "currentDay" DOUBLE PRECISION NOT NULL DEFAULT 1,
136
+ "startedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
137
+ "completedAt" TIMESTAMP(3),
138
+ "lastActivityAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
139
+
140
+ CONSTRAINT "Enrollment_pkey" PRIMARY KEY ("id")
141
+ );
142
+
143
+ -- CreateTable
144
+ CREATE TABLE "Response" (
145
+ "id" TEXT NOT NULL,
146
+ "enrollmentId" TEXT NOT NULL,
147
+ "userId" TEXT NOT NULL,
148
+ "dayNumber" INTEGER NOT NULL,
149
+ "content" TEXT,
150
+ "mediaUrl" TEXT,
151
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
152
+
153
+ CONSTRAINT "Response_pkey" PRIMARY KEY ("id")
154
+ );
155
+
156
+ -- CreateTable
157
+ CREATE TABLE "Message" (
158
+ "id" TEXT NOT NULL,
159
+ "userId" TEXT NOT NULL,
160
+ "direction" "Direction" NOT NULL,
161
+ "channel" TEXT NOT NULL DEFAULT 'WHATSAPP',
162
+ "content" TEXT,
163
+ "mediaUrl" TEXT,
164
+ "payload" JSONB,
165
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
166
+
167
+ CONSTRAINT "Message_pkey" PRIMARY KEY ("id")
168
+ );
169
+
170
+ -- CreateTable
171
+ CREATE TABLE "Payment" (
172
+ "id" TEXT NOT NULL,
173
+ "userId" TEXT NOT NULL,
174
+ "trackId" TEXT NOT NULL,
175
+ "amount" INTEGER NOT NULL,
176
+ "currency" TEXT NOT NULL DEFAULT 'XOF',
177
+ "status" "PaymentStatus" NOT NULL DEFAULT 'PENDING',
178
+ "stripeSessionId" TEXT,
179
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
180
+ "updatedAt" TIMESTAMP(3) NOT NULL,
181
+
182
+ CONSTRAINT "Payment_pkey" PRIMARY KEY ("id")
183
+ );
184
+
185
+ -- CreateTable
186
+ CREATE TABLE "TrainingData" (
187
+ "id" TEXT NOT NULL,
188
+ "audioUrl" TEXT NOT NULL,
189
+ "transcription" TEXT NOT NULL,
190
+ "manualCorrection" TEXT,
191
+ "rawWER" DOUBLE PRECISION,
192
+ "normalizedWER" DOUBLE PRECISION,
193
+ "status" "TrainingStatus" NOT NULL DEFAULT 'PENDING',
194
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
195
+ "updatedAt" TIMESTAMP(3) NOT NULL,
196
+
197
+ CONSTRAINT "TrainingData_pkey" PRIMARY KEY ("id")
198
+ );
199
+
200
+ -- CreateIndex
201
+ CREATE UNIQUE INDEX "User_phone_key" ON "User"("phone");
202
+
203
+ -- CreateIndex
204
+ CREATE UNIQUE INDEX "BusinessProfile_userId_key" ON "BusinessProfile"("userId");
205
+
206
+ -- CreateIndex
207
+ CREATE UNIQUE INDEX "UserProgress_userId_trackId_key" ON "UserProgress"("userId", "trackId");
208
+
209
+ -- CreateIndex
210
+ CREATE INDEX "Message_userId_createdAt_idx" ON "Message"("userId", "createdAt");
211
+
212
+ -- CreateIndex
213
+ CREATE UNIQUE INDEX "Payment_stripeSessionId_key" ON "Payment"("stripeSessionId");
214
+
215
+ -- AddForeignKey
216
+ ALTER TABLE "BusinessProfile" ADD CONSTRAINT "BusinessProfile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
217
+
218
+ -- AddForeignKey
219
+ ALTER TABLE "TrackDay" ADD CONSTRAINT "TrackDay_trackId_fkey" FOREIGN KEY ("trackId") REFERENCES "Track"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
220
+
221
+ -- AddForeignKey
222
+ ALTER TABLE "UserProgress" ADD CONSTRAINT "UserProgress_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
223
+
224
+ -- AddForeignKey
225
+ ALTER TABLE "UserProgress" ADD CONSTRAINT "UserProgress_trackId_fkey" FOREIGN KEY ("trackId") REFERENCES "Track"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
226
+
227
+ -- AddForeignKey
228
+ ALTER TABLE "Enrollment" ADD CONSTRAINT "Enrollment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
229
+
230
+ -- AddForeignKey
231
+ ALTER TABLE "Enrollment" ADD CONSTRAINT "Enrollment_trackId_fkey" FOREIGN KEY ("trackId") REFERENCES "Track"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
232
+
233
+ -- AddForeignKey
234
+ ALTER TABLE "Response" ADD CONSTRAINT "Response_enrollmentId_fkey" FOREIGN KEY ("enrollmentId") REFERENCES "Enrollment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
235
+
236
+ -- AddForeignKey
237
+ ALTER TABLE "Response" ADD CONSTRAINT "Response_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
238
+
239
+ -- AddForeignKey
240
+ ALTER TABLE "Message" ADD CONSTRAINT "Message_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
241
+
242
+ -- AddForeignKey
243
+ ALTER TABLE "Payment" ADD CONSTRAINT "Payment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
244
+
245
+ -- AddForeignKey
246
+ ALTER TABLE "Payment" ADD CONSTRAINT "Payment_trackId_fkey" FOREIGN KEY ("trackId") REFERENCES "Track"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
packages/database/prisma/migrations/migration_lock.toml ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Please do not edit this file manually
2
+ # It should be added in your version-control system (i.e. Git)
3
+ provider = "postgresql"
packages/database/prisma/schema.prisma CHANGED
@@ -42,6 +42,9 @@ model BusinessProfile {
42
  offerSimple String?
43
  promise String?
44
  marketData Json? // Stored Google Search results for TAM/SAM/SOM & Competition
 
 
 
45
  lastUpdatedFromDay Int @default(0)
46
  createdAt DateTime @default(now())
47
  updatedAt DateTime @updatedAt
@@ -107,11 +110,7 @@ model UserProgress {
107
  overrideAudioUrl String?
108
  reviewedBy String?
109
 
110
- // Enriched Data for Pitch Deck (Sprint 34)
111
- marketData Json? // Stored Google Search results (TAM/SAM/SOM)
112
- competitorList Json? // List of rivals found or declared
113
- financialProjections Json? // 3-year growth data
114
- fundingAsk String? // Amount and purpose
115
  createdAt DateTime @default(now())
116
  updatedAt DateTime @updatedAt
117
 
 
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
+ fundingAsk String? // Amount and purpose
48
  lastUpdatedFromDay Int @default(0)
49
  createdAt DateTime @default(now())
50
  updatedAt DateTime @updatedAt
 
110
  overrideAudioUrl String?
111
  reviewedBy String?
112
 
113
+ // Removed Enriched Data for Pitch Deck (Moved to BusinessProfile - Sprint 38)
 
 
 
 
114
  createdAt DateTime @default(now())
115
  updatedAt DateTime @updatedAt
116
 
packages/database/run-seed.ts CHANGED
@@ -1,14 +1,4 @@
1
- import { PrismaClient } from '@repo/database';
2
  import { seedDatabase } from './src/seed';
3
-
4
  const prisma = new PrismaClient();
5
-
6
- async function main() {
7
- console.log('🚀 Running seed runner...');
8
- const result = await seedDatabase(prisma);
9
- console.log(result.message);
10
- }
11
-
12
- main()
13
- .catch(console.error)
14
- .finally(() => prisma.$disconnect());
 
1
+ import { PrismaClient } from '@prisma/client';
2
  import { seedDatabase } from './src/seed';
 
3
  const prisma = new PrismaClient();
4
+ seedDatabase(prisma).then(r => { console.log(r); process.exit(0); }).catch(e => { console.error(e); process.exit(1); });
 
 
 
 
 
 
 
 
 
packages/database/scripts/validate-content.ts ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { z } from 'zod';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+
9
+ const SuccessMustIncludeSchema = z.object({
10
+ id: z.string(),
11
+ desc: z.string(),
12
+ weight: z.number()
13
+ });
14
+
15
+ const CriteriaSchema = z.object({
16
+ version: z.string().optional(),
17
+ type: z.string().optional(),
18
+ goal: z.string().optional(),
19
+ success: z.object({
20
+ mustInclude: z.array(SuccessMustIncludeSchema),
21
+ threshold: z.object({ minScore: z.number(), minMustPass: z.number() }).optional()
22
+ }).optional(),
23
+ evaluation: z.object({
24
+ tone: z.string().optional(),
25
+ format: z.string().optional(),
26
+ examples: z.string().optional()
27
+ }).optional(),
28
+ remediation: z.object({
29
+ dayNumber: z.number().optional(),
30
+ hint: z.string().optional()
31
+ }).optional()
32
+ });
33
+
34
+ const TrackDaySchema = z.object({
35
+ dayNumber: z.number(),
36
+ title: z.string(),
37
+ lessonText: z.string(),
38
+ exerciseType: z.string().optional(),
39
+ exercisePrompt: z.string().optional(),
40
+ exerciseCriteria: CriteriaSchema.optional(),
41
+ buttonsJson: z.array(z.object({ id: z.string(), title: z.string() })).optional(),
42
+ badges: z.array(z.string()).optional(),
43
+ imageUrl: z.string().optional(),
44
+ videoUrl: z.string().optional(),
45
+ videoCaption: z.string().optional()
46
+ });
47
+
48
+ const TrackSchema = z.object({
49
+ trackId: z.string(),
50
+ title: z.string(),
51
+ language: z.enum(['WOLOF', 'FR', 'EN']),
52
+ description: z.string().optional(),
53
+ totalDays: z.number().optional(),
54
+ version: z.string().optional(),
55
+ days: z.array(TrackDaySchema)
56
+ });
57
+
58
+ function validateContent() {
59
+ console.log('🔍 Starting Content Validation (Zod Pre-commit)...');
60
+ const contentDir = path.resolve(__dirname, '../content/tracks');
61
+
62
+ if (!fs.existsSync(contentDir)) {
63
+ console.warn(`⚠️ Content directory not found: ${contentDir}`);
64
+ return;
65
+ }
66
+
67
+ const files = fs.readdirSync(contentDir).filter(f => f.endsWith('.json'));
68
+ let hasErrors = false;
69
+
70
+ for (const file of files) {
71
+ const filePath = path.join(contentDir, file);
72
+ try {
73
+ const fileContent = fs.readFileSync(filePath, 'utf-8');
74
+ const jsonData = JSON.parse(fileContent);
75
+ const r = TrackSchema.safeParse(jsonData);
76
+
77
+ if (!r.success) {
78
+ console.error(`❌ Validation Failed: ${file}`);
79
+ r.error.issues.forEach((e: z.ZodIssue) => {
80
+ console.error(` - Path: ${e.path.join('.')}`);
81
+ console.error(` - Error: ${e.message}`);
82
+ });
83
+ hasErrors = true;
84
+ } else {
85
+ console.log(`✅ Validated: ${file}`);
86
+ }
87
+ } catch (e: unknown) {
88
+ hasErrors = true;
89
+ console.error(`❌ Parse Error in ${file}: ${(e instanceof Error ? e.message : String(e))}`);
90
+ }
91
+ }
92
+
93
+ if (hasErrors) {
94
+ process.exit(1);
95
+ } else {
96
+ console.log('🎉 All 10 JSON Track files are strictly valid Zod structures.');
97
+ }
98
+ }
99
+
100
+ validateContent();
pnpm-lock.yaml CHANGED
@@ -139,12 +139,18 @@ importers:
139
  '@types/node':
140
  specifier: ^20.0.0
141
  version: 20.19.33
 
 
 
142
  tsx:
143
  specifier: ^3.0.0
144
  version: 3.14.0
145
  typescript:
146
  specifier: ^5.0.0
147
  version: 5.9.3
 
 
 
148
 
149
  apps/web:
150
  dependencies:
@@ -240,6 +246,9 @@ importers:
240
  '@prisma/client':
241
  specifier: ^5.0.0
242
  version: 5.22.0(prisma@5.22.0)
 
 
 
243
  devDependencies:
244
  '@repo/tsconfig':
245
  specifier: workspace:*
@@ -531,6 +540,12 @@ packages:
531
  cpu: [ppc64]
532
  os: [aix]
533
 
 
 
 
 
 
 
534
  '@esbuild/android-arm64@0.18.20':
535
  resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
536
  engines: {node: '>=12'}
@@ -543,6 +558,12 @@ packages:
543
  cpu: [arm64]
544
  os: [android]
545
 
 
 
 
 
 
 
546
  '@esbuild/android-arm@0.18.20':
547
  resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
548
  engines: {node: '>=12'}
@@ -555,6 +576,12 @@ packages:
555
  cpu: [arm]
556
  os: [android]
557
 
 
 
 
 
 
 
558
  '@esbuild/android-x64@0.18.20':
559
  resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
560
  engines: {node: '>=12'}
@@ -567,6 +594,12 @@ packages:
567
  cpu: [x64]
568
  os: [android]
569
 
 
 
 
 
 
 
570
  '@esbuild/darwin-arm64@0.18.20':
571
  resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
572
  engines: {node: '>=12'}
@@ -579,6 +612,12 @@ packages:
579
  cpu: [arm64]
580
  os: [darwin]
581
 
 
 
 
 
 
 
582
  '@esbuild/darwin-x64@0.18.20':
583
  resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
584
  engines: {node: '>=12'}
@@ -591,6 +630,12 @@ packages:
591
  cpu: [x64]
592
  os: [darwin]
593
 
 
 
 
 
 
 
594
  '@esbuild/freebsd-arm64@0.18.20':
595
  resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
596
  engines: {node: '>=12'}
@@ -603,6 +648,12 @@ packages:
603
  cpu: [arm64]
604
  os: [freebsd]
605
 
 
 
 
 
 
 
606
  '@esbuild/freebsd-x64@0.18.20':
607
  resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
608
  engines: {node: '>=12'}
@@ -615,6 +666,12 @@ packages:
615
  cpu: [x64]
616
  os: [freebsd]
617
 
 
 
 
 
 
 
618
  '@esbuild/linux-arm64@0.18.20':
619
  resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
620
  engines: {node: '>=12'}
@@ -627,6 +684,12 @@ packages:
627
  cpu: [arm64]
628
  os: [linux]
629
 
 
 
 
 
 
 
630
  '@esbuild/linux-arm@0.18.20':
631
  resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
632
  engines: {node: '>=12'}
@@ -639,6 +702,12 @@ packages:
639
  cpu: [arm]
640
  os: [linux]
641
 
 
 
 
 
 
 
642
  '@esbuild/linux-ia32@0.18.20':
643
  resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
644
  engines: {node: '>=12'}
@@ -651,6 +720,12 @@ packages:
651
  cpu: [ia32]
652
  os: [linux]
653
 
 
 
 
 
 
 
654
  '@esbuild/linux-loong64@0.18.20':
655
  resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
656
  engines: {node: '>=12'}
@@ -663,6 +738,12 @@ packages:
663
  cpu: [loong64]
664
  os: [linux]
665
 
 
 
 
 
 
 
666
  '@esbuild/linux-mips64el@0.18.20':
667
  resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
668
  engines: {node: '>=12'}
@@ -675,6 +756,12 @@ packages:
675
  cpu: [mips64el]
676
  os: [linux]
677
 
 
 
 
 
 
 
678
  '@esbuild/linux-ppc64@0.18.20':
679
  resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
680
  engines: {node: '>=12'}
@@ -687,6 +774,12 @@ packages:
687
  cpu: [ppc64]
688
  os: [linux]
689
 
 
 
 
 
 
 
690
  '@esbuild/linux-riscv64@0.18.20':
691
  resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
692
  engines: {node: '>=12'}
@@ -699,6 +792,12 @@ packages:
699
  cpu: [riscv64]
700
  os: [linux]
701
 
 
 
 
 
 
 
702
  '@esbuild/linux-s390x@0.18.20':
703
  resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
704
  engines: {node: '>=12'}
@@ -711,6 +810,12 @@ packages:
711
  cpu: [s390x]
712
  os: [linux]
713
 
 
 
 
 
 
 
714
  '@esbuild/linux-x64@0.18.20':
715
  resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
716
  engines: {node: '>=12'}
@@ -723,6 +828,18 @@ packages:
723
  cpu: [x64]
724
  os: [linux]
725
 
 
 
 
 
 
 
 
 
 
 
 
 
726
  '@esbuild/netbsd-x64@0.18.20':
727
  resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
728
  engines: {node: '>=12'}
@@ -735,6 +852,18 @@ packages:
735
  cpu: [x64]
736
  os: [netbsd]
737
 
 
 
 
 
 
 
 
 
 
 
 
 
738
  '@esbuild/openbsd-x64@0.18.20':
739
  resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
740
  engines: {node: '>=12'}
@@ -747,6 +876,18 @@ packages:
747
  cpu: [x64]
748
  os: [openbsd]
749
 
 
 
 
 
 
 
 
 
 
 
 
 
750
  '@esbuild/sunos-x64@0.18.20':
751
  resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
752
  engines: {node: '>=12'}
@@ -759,6 +900,12 @@ packages:
759
  cpu: [x64]
760
  os: [sunos]
761
 
 
 
 
 
 
 
762
  '@esbuild/win32-arm64@0.18.20':
763
  resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
764
  engines: {node: '>=12'}
@@ -771,6 +918,12 @@ packages:
771
  cpu: [arm64]
772
  os: [win32]
773
 
 
 
 
 
 
 
774
  '@esbuild/win32-ia32@0.18.20':
775
  resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
776
  engines: {node: '>=12'}
@@ -783,6 +936,12 @@ packages:
783
  cpu: [ia32]
784
  os: [win32]
785
 
 
 
 
 
 
 
786
  '@esbuild/win32-x64@0.18.20':
787
  resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
788
  engines: {node: '>=12'}
@@ -795,6 +954,12 @@ packages:
795
  cpu: [x64]
796
  os: [win32]
797
 
 
 
 
 
 
 
798
  '@fastify/ajv-compiler@3.6.0':
799
  resolution: {integrity: sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==}
800
 
@@ -1018,6 +1183,9 @@ packages:
1018
  '@pinojs/redact@0.4.0':
1019
  resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
1020
 
 
 
 
1021
  '@prisma/client@5.22.0':
1022
  resolution: {integrity: sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==}
1023
  engines: {node: '>=16.13'}
@@ -1395,6 +1563,9 @@ packages:
1395
  resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==}
1396
  engines: {node: '>=18.0.0'}
1397
 
 
 
 
1398
  '@tootallnate/quickjs-emscripten@0.23.0':
1399
  resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==}
1400
 
@@ -1410,6 +1581,12 @@ packages:
1410
  '@types/babel__traverse@7.28.0':
1411
  resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
1412
 
 
 
 
 
 
 
1413
  '@types/diff@8.0.0':
1414
  resolution: {integrity: sha512-o7jqJM04gfaYrdCecCVMbZhNdG6T1MHg/oQoRFdERLV+4d+V7FijhiEAbFu0Usww84Yijk9yH58U4Jk4HbtzZw==}
1415
  deprecated: This is a stub types definition. diff provides its own type definitions, so you do not need this installed.
@@ -1459,6 +1636,40 @@ packages:
1459
  peerDependencies:
1460
  vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
1461
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1462
  abort-controller@3.0.0:
1463
  resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
1464
  engines: {node: '>=6.5'}
@@ -1514,6 +1725,10 @@ packages:
1514
  argparse@2.0.1:
1515
  resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
1516
 
 
 
 
 
1517
  ast-types@0.13.4:
1518
  resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==}
1519
  engines: {node: '>=4'}
@@ -1648,6 +1863,10 @@ packages:
1648
  caniuse-lite@1.0.30001770:
1649
  resolution: {integrity: sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==}
1650
 
 
 
 
 
1651
  chokidar@3.6.0:
1652
  resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
1653
  engines: {node: '>= 8.10.0'}
@@ -1785,6 +2004,9 @@ packages:
1785
  resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
1786
  engines: {node: '>= 0.4'}
1787
 
 
 
 
1788
  es-object-atoms@1.1.1:
1789
  resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
1790
  engines: {node: '>= 0.4'}
@@ -1803,6 +2025,11 @@ packages:
1803
  engines: {node: '>=12'}
1804
  hasBin: true
1805
 
 
 
 
 
 
1806
  escalade@3.2.0:
1807
  resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
1808
  engines: {node: '>=6'}
@@ -1821,6 +2048,9 @@ packages:
1821
  resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
1822
  engines: {node: '>=4.0'}
1823
 
 
 
 
1824
  esutils@2.0.3:
1825
  resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
1826
  engines: {node: '>=0.10.0'}
@@ -1832,6 +2062,10 @@ packages:
1832
  events-universal@1.0.1:
1833
  resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==}
1834
 
 
 
 
 
1835
  extract-zip@2.0.1:
1836
  resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==}
1837
  engines: {node: '>= 10.17.0'}
@@ -1897,6 +2131,9 @@ packages:
1897
  picomatch:
1898
  optional: true
1899
 
 
 
 
1900
  fill-range@7.1.1:
1901
  resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
1902
  engines: {node: '>=8'}
@@ -1905,6 +2142,9 @@ packages:
1905
  resolution: {integrity: sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA==}
1906
  engines: {node: '>=14'}
1907
 
 
 
 
1908
  follow-redirects@1.15.11:
1909
  resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
1910
  engines: {node: '>=4.0'}
@@ -2156,6 +2396,9 @@ packages:
2156
  resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==}
2157
  engines: {node: '>=12'}
2158
 
 
 
 
2159
  math-intrinsics@1.1.0:
2160
  resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
2161
  engines: {node: '>= 0.4'}
@@ -2186,6 +2429,10 @@ packages:
2186
  mnemonist@0.39.6:
2187
  resolution: {integrity: sha512-A/0v5Z59y63US00cRSLiloEIw3t5G+MiKz4BhX21FI+YBJXBOGW0ohFxTxO08dsOYlzxo87T7vGfZKYp2bcAWA==}
2188
 
 
 
 
 
2189
  ms@2.1.3:
2190
  resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
2191
 
@@ -2254,6 +2501,9 @@ packages:
2254
  obliterator@2.0.5:
2255
  resolution: {integrity: sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==}
2256
 
 
 
 
2257
  on-exit-leak-free@2.1.2:
2258
  resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
2259
  engines: {node: '>=14.0.0'}
@@ -2295,6 +2545,9 @@ packages:
2295
  path-parse@1.0.7:
2296
  resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
2297
 
 
 
 
2298
  pend@1.2.0:
2299
  resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
2300
 
@@ -2555,6 +2808,13 @@ packages:
2555
  resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
2556
  engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
2557
 
 
 
 
 
 
 
 
2558
  smart-buffer@4.2.0:
2559
  resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
2560
  engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
@@ -2585,9 +2845,15 @@ packages:
2585
  resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
2586
  engines: {node: '>= 10.x'}
2587
 
 
 
 
2588
  standard-as-callback@2.1.0:
2589
  resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
2590
 
 
 
 
2591
  streamx@2.23.0:
2592
  resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==}
2593
 
@@ -2653,10 +2919,21 @@ packages:
2653
  through@2.3.8:
2654
  resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
2655
 
 
 
 
 
 
 
 
2656
  tinyglobby@0.2.15:
2657
  resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
2658
  engines: {node: '>=12.0.0'}
2659
 
 
 
 
 
2660
  to-regex-range@5.0.1:
2661
  resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
2662
  engines: {node: '>=8.0'}
@@ -2665,6 +2942,10 @@ packages:
2665
  resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==}
2666
  engines: {node: '>=12'}
2667
 
 
 
 
 
2668
  tr46@0.0.3:
2669
  resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
2670
 
@@ -2777,6 +3058,80 @@ packages:
2777
  terser:
2778
  optional: true
2779
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2780
  web-streams-polyfill@4.0.0-beta.3:
2781
  resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
2782
  engines: {node: '>= 14'}
@@ -2787,6 +3142,11 @@ packages:
2787
  whatwg-url@5.0.0:
2788
  resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
2789
 
 
 
 
 
 
2790
  wrap-ansi@7.0.0:
2791
  resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
2792
  engines: {node: '>=10'}
@@ -2830,6 +3190,9 @@ packages:
2830
  zod@3.25.76:
2831
  resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
2832
 
 
 
 
2833
  snapshots:
2834
 
2835
  '@alloc/quick-lru@5.2.0': {}
@@ -3447,138 +3810,216 @@ snapshots:
3447
  '@esbuild/aix-ppc64@0.21.5':
3448
  optional: true
3449
 
 
 
 
3450
  '@esbuild/android-arm64@0.18.20':
3451
  optional: true
3452
 
3453
  '@esbuild/android-arm64@0.21.5':
3454
  optional: true
3455
 
 
 
 
3456
  '@esbuild/android-arm@0.18.20':
3457
  optional: true
3458
 
3459
  '@esbuild/android-arm@0.21.5':
3460
  optional: true
3461
 
 
 
 
3462
  '@esbuild/android-x64@0.18.20':
3463
  optional: true
3464
 
3465
  '@esbuild/android-x64@0.21.5':
3466
  optional: true
3467
 
 
 
 
3468
  '@esbuild/darwin-arm64@0.18.20':
3469
  optional: true
3470
 
3471
  '@esbuild/darwin-arm64@0.21.5':
3472
  optional: true
3473
 
 
 
 
3474
  '@esbuild/darwin-x64@0.18.20':
3475
  optional: true
3476
 
3477
  '@esbuild/darwin-x64@0.21.5':
3478
  optional: true
3479
 
 
 
 
3480
  '@esbuild/freebsd-arm64@0.18.20':
3481
  optional: true
3482
 
3483
  '@esbuild/freebsd-arm64@0.21.5':
3484
  optional: true
3485
 
 
 
 
3486
  '@esbuild/freebsd-x64@0.18.20':
3487
  optional: true
3488
 
3489
  '@esbuild/freebsd-x64@0.21.5':
3490
  optional: true
3491
 
 
 
 
3492
  '@esbuild/linux-arm64@0.18.20':
3493
  optional: true
3494
 
3495
  '@esbuild/linux-arm64@0.21.5':
3496
  optional: true
3497
 
 
 
 
3498
  '@esbuild/linux-arm@0.18.20':
3499
  optional: true
3500
 
3501
  '@esbuild/linux-arm@0.21.5':
3502
  optional: true
3503
 
 
 
 
3504
  '@esbuild/linux-ia32@0.18.20':
3505
  optional: true
3506
 
3507
  '@esbuild/linux-ia32@0.21.5':
3508
  optional: true
3509
 
 
 
 
3510
  '@esbuild/linux-loong64@0.18.20':
3511
  optional: true
3512
 
3513
  '@esbuild/linux-loong64@0.21.5':
3514
  optional: true
3515
 
 
 
 
3516
  '@esbuild/linux-mips64el@0.18.20':
3517
  optional: true
3518
 
3519
  '@esbuild/linux-mips64el@0.21.5':
3520
  optional: true
3521
 
 
 
 
3522
  '@esbuild/linux-ppc64@0.18.20':
3523
  optional: true
3524
 
3525
  '@esbuild/linux-ppc64@0.21.5':
3526
  optional: true
3527
 
 
 
 
3528
  '@esbuild/linux-riscv64@0.18.20':
3529
  optional: true
3530
 
3531
  '@esbuild/linux-riscv64@0.21.5':
3532
  optional: true
3533
 
 
 
 
3534
  '@esbuild/linux-s390x@0.18.20':
3535
  optional: true
3536
 
3537
  '@esbuild/linux-s390x@0.21.5':
3538
  optional: true
3539
 
 
 
 
3540
  '@esbuild/linux-x64@0.18.20':
3541
  optional: true
3542
 
3543
  '@esbuild/linux-x64@0.21.5':
3544
  optional: true
3545
 
 
 
 
 
 
 
3546
  '@esbuild/netbsd-x64@0.18.20':
3547
  optional: true
3548
 
3549
  '@esbuild/netbsd-x64@0.21.5':
3550
  optional: true
3551
 
 
 
 
 
 
 
3552
  '@esbuild/openbsd-x64@0.18.20':
3553
  optional: true
3554
 
3555
  '@esbuild/openbsd-x64@0.21.5':
3556
  optional: true
3557
 
 
 
 
 
 
 
3558
  '@esbuild/sunos-x64@0.18.20':
3559
  optional: true
3560
 
3561
  '@esbuild/sunos-x64@0.21.5':
3562
  optional: true
3563
 
 
 
 
3564
  '@esbuild/win32-arm64@0.18.20':
3565
  optional: true
3566
 
3567
  '@esbuild/win32-arm64@0.21.5':
3568
  optional: true
3569
 
 
 
 
3570
  '@esbuild/win32-ia32@0.18.20':
3571
  optional: true
3572
 
3573
  '@esbuild/win32-ia32@0.21.5':
3574
  optional: true
3575
 
 
 
 
3576
  '@esbuild/win32-x64@0.18.20':
3577
  optional: true
3578
 
3579
  '@esbuild/win32-x64@0.21.5':
3580
  optional: true
3581
 
 
 
 
3582
  '@fastify/ajv-compiler@3.6.0':
3583
  dependencies:
3584
  ajv: 8.18.0
@@ -3757,6 +4198,8 @@ snapshots:
3757
 
3758
  '@pinojs/redact@0.4.0': {}
3759
 
 
 
3760
  '@prisma/client@5.22.0(prisma@5.22.0)':
3761
  optionalDependencies:
3762
  prisma: 5.22.0
@@ -4215,6 +4658,8 @@ snapshots:
4215
  dependencies:
4216
  tslib: 2.8.1
4217
 
 
 
4218
  '@tootallnate/quickjs-emscripten@0.23.0': {}
4219
 
4220
  '@types/babel__core@7.20.5':
@@ -4238,6 +4683,13 @@ snapshots:
4238
  dependencies:
4239
  '@babel/types': 7.29.0
4240
 
 
 
 
 
 
 
 
4241
  '@types/diff@8.0.0':
4242
  dependencies:
4243
  diff: 8.0.3
@@ -4298,6 +4750,56 @@ snapshots:
4298
  transitivePeerDependencies:
4299
  - supports-color
4300
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4301
  abort-controller@3.0.0:
4302
  dependencies:
4303
  event-target-shim: 5.0.1
@@ -4342,6 +4844,8 @@ snapshots:
4342
 
4343
  argparse@2.0.1: {}
4344
 
 
 
4345
  ast-types@0.13.4:
4346
  dependencies:
4347
  tslib: 2.8.1
@@ -4486,6 +4990,8 @@ snapshots:
4486
 
4487
  caniuse-lite@1.0.30001770: {}
4488
 
 
 
4489
  chokidar@3.6.0:
4490
  dependencies:
4491
  anymatch: 3.1.3
@@ -4600,6 +5106,8 @@ snapshots:
4600
 
4601
  es-errors@1.3.0: {}
4602
 
 
 
4603
  es-object-atoms@1.1.1:
4604
  dependencies:
4605
  es-errors: 1.3.0
@@ -4662,6 +5170,35 @@ snapshots:
4662
  '@esbuild/win32-ia32': 0.21.5
4663
  '@esbuild/win32-x64': 0.21.5
4664
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4665
  escalade@3.2.0: {}
4666
 
4667
  escodegen@2.1.0:
@@ -4676,6 +5213,10 @@ snapshots:
4676
 
4677
  estraverse@5.3.0: {}
4678
 
 
 
 
 
4679
  esutils@2.0.3: {}
4680
 
4681
  event-target-shim@5.0.1: {}
@@ -4686,6 +5227,8 @@ snapshots:
4686
  transitivePeerDependencies:
4687
  - bare-abort-controller
4688
 
 
 
4689
  extract-zip@2.0.1:
4690
  dependencies:
4691
  debug: 4.4.3
@@ -4773,6 +5316,8 @@ snapshots:
4773
  optionalDependencies:
4774
  picomatch: 4.0.3
4775
 
 
 
4776
  fill-range@7.1.1:
4777
  dependencies:
4778
  to-regex-range: 5.0.1
@@ -4783,6 +5328,8 @@ snapshots:
4783
  fast-querystring: 1.1.2
4784
  safe-regex2: 3.1.0
4785
 
 
 
4786
  follow-redirects@1.15.11: {}
4787
 
4788
  form-data-encoder@1.7.2: {}
@@ -5034,6 +5581,10 @@ snapshots:
5034
 
5035
  luxon@3.7.2: {}
5036
 
 
 
 
 
5037
  math-intrinsics@1.1.0: {}
5038
 
5039
  merge2@1.4.1: {}
@@ -5059,6 +5610,8 @@ snapshots:
5059
  dependencies:
5060
  obliterator: 2.0.5
5061
 
 
 
5062
  ms@2.1.3: {}
5063
 
5064
  msgpackr-extract@3.0.3:
@@ -5116,6 +5669,8 @@ snapshots:
5116
 
5117
  obliterator@2.0.5: {}
5118
 
 
 
5119
  on-exit-leak-free@2.1.2: {}
5120
 
5121
  once@1.4.0:
@@ -5170,6 +5725,8 @@ snapshots:
5170
 
5171
  path-parse@1.0.7: {}
5172
 
 
 
5173
  pend@1.2.0: {}
5174
 
5175
  picocolors@1.1.1: {}
@@ -5483,6 +6040,14 @@ snapshots:
5483
  '@img/sharp-win32-ia32': 0.34.5
5484
  '@img/sharp-win32-x64': 0.34.5
5485
 
 
 
 
 
 
 
 
 
5486
  smart-buffer@4.2.0: {}
5487
 
5488
  socks-proxy-agent@8.0.5:
@@ -5513,8 +6078,12 @@ snapshots:
5513
 
5514
  split2@4.2.0: {}
5515
 
 
 
5516
  standard-as-callback@2.1.0: {}
5517
 
 
 
5518
  streamx@2.23.0:
5519
  dependencies:
5520
  events-universal: 1.0.1
@@ -5633,17 +6202,25 @@ snapshots:
5633
 
5634
  through@2.3.8: {}
5635
 
 
 
 
 
5636
  tinyglobby@0.2.15:
5637
  dependencies:
5638
  fdir: 6.5.0(picomatch@4.0.3)
5639
  picomatch: 4.0.3
5640
 
 
 
5641
  to-regex-range@5.0.1:
5642
  dependencies:
5643
  is-number: 7.0.0
5644
 
5645
  toad-cache@3.7.0: {}
5646
 
 
 
5647
  tr46@0.0.3: {}
5648
 
5649
  ts-interface-checker@0.1.13: {}
@@ -5719,6 +6296,58 @@ snapshots:
5719
  '@types/node': 22.19.11
5720
  fsevents: 2.3.3
5721
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5722
  web-streams-polyfill@4.0.0-beta.3: {}
5723
 
5724
  webidl-conversions@3.0.1: {}
@@ -5728,6 +6357,11 @@ snapshots:
5728
  tr46: 0.0.3
5729
  webidl-conversions: 3.0.1
5730
 
 
 
 
 
 
5731
  wrap-ansi@7.0.0:
5732
  dependencies:
5733
  ansi-styles: 4.3.0
@@ -5762,3 +6396,5 @@ snapshots:
5762
  zod@3.23.8: {}
5763
 
5764
  zod@3.25.76: {}
 
 
 
139
  '@types/node':
140
  specifier: ^20.0.0
141
  version: 20.19.33
142
+ '@vitest/ui':
143
+ specifier: ^4.0.18
144
+ version: 4.0.18(vitest@4.0.18)
145
  tsx:
146
  specifier: ^3.0.0
147
  version: 3.14.0
148
  typescript:
149
  specifier: ^5.0.0
150
  version: 5.9.3
151
+ vitest:
152
+ specifier: ^4.0.18
153
+ version: 4.0.18(@types/node@20.19.33)(@vitest/ui@4.0.18)(jiti@1.21.7)(tsx@3.14.0)
154
 
155
  apps/web:
156
  dependencies:
 
246
  '@prisma/client':
247
  specifier: ^5.0.0
248
  version: 5.22.0(prisma@5.22.0)
249
+ zod:
250
+ specifier: ^4.3.6
251
+ version: 4.3.6
252
  devDependencies:
253
  '@repo/tsconfig':
254
  specifier: workspace:*
 
540
  cpu: [ppc64]
541
  os: [aix]
542
 
543
+ '@esbuild/aix-ppc64@0.27.3':
544
+ resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==}
545
+ engines: {node: '>=18'}
546
+ cpu: [ppc64]
547
+ os: [aix]
548
+
549
  '@esbuild/android-arm64@0.18.20':
550
  resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
551
  engines: {node: '>=12'}
 
558
  cpu: [arm64]
559
  os: [android]
560
 
561
+ '@esbuild/android-arm64@0.27.3':
562
+ resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==}
563
+ engines: {node: '>=18'}
564
+ cpu: [arm64]
565
+ os: [android]
566
+
567
  '@esbuild/android-arm@0.18.20':
568
  resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
569
  engines: {node: '>=12'}
 
576
  cpu: [arm]
577
  os: [android]
578
 
579
+ '@esbuild/android-arm@0.27.3':
580
+ resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==}
581
+ engines: {node: '>=18'}
582
+ cpu: [arm]
583
+ os: [android]
584
+
585
  '@esbuild/android-x64@0.18.20':
586
  resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
587
  engines: {node: '>=12'}
 
594
  cpu: [x64]
595
  os: [android]
596
 
597
+ '@esbuild/android-x64@0.27.3':
598
+ resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==}
599
+ engines: {node: '>=18'}
600
+ cpu: [x64]
601
+ os: [android]
602
+
603
  '@esbuild/darwin-arm64@0.18.20':
604
  resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
605
  engines: {node: '>=12'}
 
612
  cpu: [arm64]
613
  os: [darwin]
614
 
615
+ '@esbuild/darwin-arm64@0.27.3':
616
+ resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==}
617
+ engines: {node: '>=18'}
618
+ cpu: [arm64]
619
+ os: [darwin]
620
+
621
  '@esbuild/darwin-x64@0.18.20':
622
  resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
623
  engines: {node: '>=12'}
 
630
  cpu: [x64]
631
  os: [darwin]
632
 
633
+ '@esbuild/darwin-x64@0.27.3':
634
+ resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==}
635
+ engines: {node: '>=18'}
636
+ cpu: [x64]
637
+ os: [darwin]
638
+
639
  '@esbuild/freebsd-arm64@0.18.20':
640
  resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
641
  engines: {node: '>=12'}
 
648
  cpu: [arm64]
649
  os: [freebsd]
650
 
651
+ '@esbuild/freebsd-arm64@0.27.3':
652
+ resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==}
653
+ engines: {node: '>=18'}
654
+ cpu: [arm64]
655
+ os: [freebsd]
656
+
657
  '@esbuild/freebsd-x64@0.18.20':
658
  resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
659
  engines: {node: '>=12'}
 
666
  cpu: [x64]
667
  os: [freebsd]
668
 
669
+ '@esbuild/freebsd-x64@0.27.3':
670
+ resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==}
671
+ engines: {node: '>=18'}
672
+ cpu: [x64]
673
+ os: [freebsd]
674
+
675
  '@esbuild/linux-arm64@0.18.20':
676
  resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
677
  engines: {node: '>=12'}
 
684
  cpu: [arm64]
685
  os: [linux]
686
 
687
+ '@esbuild/linux-arm64@0.27.3':
688
+ resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==}
689
+ engines: {node: '>=18'}
690
+ cpu: [arm64]
691
+ os: [linux]
692
+
693
  '@esbuild/linux-arm@0.18.20':
694
  resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
695
  engines: {node: '>=12'}
 
702
  cpu: [arm]
703
  os: [linux]
704
 
705
+ '@esbuild/linux-arm@0.27.3':
706
+ resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==}
707
+ engines: {node: '>=18'}
708
+ cpu: [arm]
709
+ os: [linux]
710
+
711
  '@esbuild/linux-ia32@0.18.20':
712
  resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
713
  engines: {node: '>=12'}
 
720
  cpu: [ia32]
721
  os: [linux]
722
 
723
+ '@esbuild/linux-ia32@0.27.3':
724
+ resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==}
725
+ engines: {node: '>=18'}
726
+ cpu: [ia32]
727
+ os: [linux]
728
+
729
  '@esbuild/linux-loong64@0.18.20':
730
  resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
731
  engines: {node: '>=12'}
 
738
  cpu: [loong64]
739
  os: [linux]
740
 
741
+ '@esbuild/linux-loong64@0.27.3':
742
+ resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==}
743
+ engines: {node: '>=18'}
744
+ cpu: [loong64]
745
+ os: [linux]
746
+
747
  '@esbuild/linux-mips64el@0.18.20':
748
  resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
749
  engines: {node: '>=12'}
 
756
  cpu: [mips64el]
757
  os: [linux]
758
 
759
+ '@esbuild/linux-mips64el@0.27.3':
760
+ resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==}
761
+ engines: {node: '>=18'}
762
+ cpu: [mips64el]
763
+ os: [linux]
764
+
765
  '@esbuild/linux-ppc64@0.18.20':
766
  resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
767
  engines: {node: '>=12'}
 
774
  cpu: [ppc64]
775
  os: [linux]
776
 
777
+ '@esbuild/linux-ppc64@0.27.3':
778
+ resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==}
779
+ engines: {node: '>=18'}
780
+ cpu: [ppc64]
781
+ os: [linux]
782
+
783
  '@esbuild/linux-riscv64@0.18.20':
784
  resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
785
  engines: {node: '>=12'}
 
792
  cpu: [riscv64]
793
  os: [linux]
794
 
795
+ '@esbuild/linux-riscv64@0.27.3':
796
+ resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==}
797
+ engines: {node: '>=18'}
798
+ cpu: [riscv64]
799
+ os: [linux]
800
+
801
  '@esbuild/linux-s390x@0.18.20':
802
  resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
803
  engines: {node: '>=12'}
 
810
  cpu: [s390x]
811
  os: [linux]
812
 
813
+ '@esbuild/linux-s390x@0.27.3':
814
+ resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==}
815
+ engines: {node: '>=18'}
816
+ cpu: [s390x]
817
+ os: [linux]
818
+
819
  '@esbuild/linux-x64@0.18.20':
820
  resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
821
  engines: {node: '>=12'}
 
828
  cpu: [x64]
829
  os: [linux]
830
 
831
+ '@esbuild/linux-x64@0.27.3':
832
+ resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==}
833
+ engines: {node: '>=18'}
834
+ cpu: [x64]
835
+ os: [linux]
836
+
837
+ '@esbuild/netbsd-arm64@0.27.3':
838
+ resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==}
839
+ engines: {node: '>=18'}
840
+ cpu: [arm64]
841
+ os: [netbsd]
842
+
843
  '@esbuild/netbsd-x64@0.18.20':
844
  resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
845
  engines: {node: '>=12'}
 
852
  cpu: [x64]
853
  os: [netbsd]
854
 
855
+ '@esbuild/netbsd-x64@0.27.3':
856
+ resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==}
857
+ engines: {node: '>=18'}
858
+ cpu: [x64]
859
+ os: [netbsd]
860
+
861
+ '@esbuild/openbsd-arm64@0.27.3':
862
+ resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==}
863
+ engines: {node: '>=18'}
864
+ cpu: [arm64]
865
+ os: [openbsd]
866
+
867
  '@esbuild/openbsd-x64@0.18.20':
868
  resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
869
  engines: {node: '>=12'}
 
876
  cpu: [x64]
877
  os: [openbsd]
878
 
879
+ '@esbuild/openbsd-x64@0.27.3':
880
+ resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==}
881
+ engines: {node: '>=18'}
882
+ cpu: [x64]
883
+ os: [openbsd]
884
+
885
+ '@esbuild/openharmony-arm64@0.27.3':
886
+ resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==}
887
+ engines: {node: '>=18'}
888
+ cpu: [arm64]
889
+ os: [openharmony]
890
+
891
  '@esbuild/sunos-x64@0.18.20':
892
  resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
893
  engines: {node: '>=12'}
 
900
  cpu: [x64]
901
  os: [sunos]
902
 
903
+ '@esbuild/sunos-x64@0.27.3':
904
+ resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==}
905
+ engines: {node: '>=18'}
906
+ cpu: [x64]
907
+ os: [sunos]
908
+
909
  '@esbuild/win32-arm64@0.18.20':
910
  resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
911
  engines: {node: '>=12'}
 
918
  cpu: [arm64]
919
  os: [win32]
920
 
921
+ '@esbuild/win32-arm64@0.27.3':
922
+ resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==}
923
+ engines: {node: '>=18'}
924
+ cpu: [arm64]
925
+ os: [win32]
926
+
927
  '@esbuild/win32-ia32@0.18.20':
928
  resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
929
  engines: {node: '>=12'}
 
936
  cpu: [ia32]
937
  os: [win32]
938
 
939
+ '@esbuild/win32-ia32@0.27.3':
940
+ resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==}
941
+ engines: {node: '>=18'}
942
+ cpu: [ia32]
943
+ os: [win32]
944
+
945
  '@esbuild/win32-x64@0.18.20':
946
  resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
947
  engines: {node: '>=12'}
 
954
  cpu: [x64]
955
  os: [win32]
956
 
957
+ '@esbuild/win32-x64@0.27.3':
958
+ resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==}
959
+ engines: {node: '>=18'}
960
+ cpu: [x64]
961
+ os: [win32]
962
+
963
  '@fastify/ajv-compiler@3.6.0':
964
  resolution: {integrity: sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==}
965
 
 
1183
  '@pinojs/redact@0.4.0':
1184
  resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
1185
 
1186
+ '@polka/url@1.0.0-next.29':
1187
+ resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
1188
+
1189
  '@prisma/client@5.22.0':
1190
  resolution: {integrity: sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==}
1191
  engines: {node: '>=16.13'}
 
1563
  resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==}
1564
  engines: {node: '>=18.0.0'}
1565
 
1566
+ '@standard-schema/spec@1.1.0':
1567
+ resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
1568
+
1569
  '@tootallnate/quickjs-emscripten@0.23.0':
1570
  resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==}
1571
 
 
1581
  '@types/babel__traverse@7.28.0':
1582
  resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
1583
 
1584
+ '@types/chai@5.2.3':
1585
+ resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
1586
+
1587
+ '@types/deep-eql@4.0.2':
1588
+ resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
1589
+
1590
  '@types/diff@8.0.0':
1591
  resolution: {integrity: sha512-o7jqJM04gfaYrdCecCVMbZhNdG6T1MHg/oQoRFdERLV+4d+V7FijhiEAbFu0Usww84Yijk9yH58U4Jk4HbtzZw==}
1592
  deprecated: This is a stub types definition. diff provides its own type definitions, so you do not need this installed.
 
1636
  peerDependencies:
1637
  vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
1638
 
1639
+ '@vitest/expect@4.0.18':
1640
+ resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==}
1641
+
1642
+ '@vitest/mocker@4.0.18':
1643
+ resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==}
1644
+ peerDependencies:
1645
+ msw: ^2.4.9
1646
+ vite: ^6.0.0 || ^7.0.0-0
1647
+ peerDependenciesMeta:
1648
+ msw:
1649
+ optional: true
1650
+ vite:
1651
+ optional: true
1652
+
1653
+ '@vitest/pretty-format@4.0.18':
1654
+ resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==}
1655
+
1656
+ '@vitest/runner@4.0.18':
1657
+ resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==}
1658
+
1659
+ '@vitest/snapshot@4.0.18':
1660
+ resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==}
1661
+
1662
+ '@vitest/spy@4.0.18':
1663
+ resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==}
1664
+
1665
+ '@vitest/ui@4.0.18':
1666
+ resolution: {integrity: sha512-CGJ25bc8fRi8Lod/3GHSvXRKi7nBo3kxh0ApW4yCjmrWmRmlT53B5E08XRSZRliygG0aVNxLrBEqPYdz/KcCtQ==}
1667
+ peerDependencies:
1668
+ vitest: 4.0.18
1669
+
1670
+ '@vitest/utils@4.0.18':
1671
+ resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==}
1672
+
1673
  abort-controller@3.0.0:
1674
  resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
1675
  engines: {node: '>=6.5'}
 
1725
  argparse@2.0.1:
1726
  resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
1727
 
1728
+ assertion-error@2.0.1:
1729
+ resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
1730
+ engines: {node: '>=12'}
1731
+
1732
  ast-types@0.13.4:
1733
  resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==}
1734
  engines: {node: '>=4'}
 
1863
  caniuse-lite@1.0.30001770:
1864
  resolution: {integrity: sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==}
1865
 
1866
+ chai@6.2.2:
1867
+ resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
1868
+ engines: {node: '>=18'}
1869
+
1870
  chokidar@3.6.0:
1871
  resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
1872
  engines: {node: '>= 8.10.0'}
 
2004
  resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
2005
  engines: {node: '>= 0.4'}
2006
 
2007
+ es-module-lexer@1.7.0:
2008
+ resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
2009
+
2010
  es-object-atoms@1.1.1:
2011
  resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
2012
  engines: {node: '>= 0.4'}
 
2025
  engines: {node: '>=12'}
2026
  hasBin: true
2027
 
2028
+ esbuild@0.27.3:
2029
+ resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==}
2030
+ engines: {node: '>=18'}
2031
+ hasBin: true
2032
+
2033
  escalade@3.2.0:
2034
  resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
2035
  engines: {node: '>=6'}
 
2048
  resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
2049
  engines: {node: '>=4.0'}
2050
 
2051
+ estree-walker@3.0.3:
2052
+ resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
2053
+
2054
  esutils@2.0.3:
2055
  resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
2056
  engines: {node: '>=0.10.0'}
 
2062
  events-universal@1.0.1:
2063
  resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==}
2064
 
2065
+ expect-type@1.3.0:
2066
+ resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
2067
+ engines: {node: '>=12.0.0'}
2068
+
2069
  extract-zip@2.0.1:
2070
  resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==}
2071
  engines: {node: '>= 10.17.0'}
 
2131
  picomatch:
2132
  optional: true
2133
 
2134
+ fflate@0.8.2:
2135
+ resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
2136
+
2137
  fill-range@7.1.1:
2138
  resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
2139
  engines: {node: '>=8'}
 
2142
  resolution: {integrity: sha512-Dobi7gcTEq8yszimcfp/R7+owiT4WncAJ7VTTgFH1jYJ5GaG1FbhjwDG820hptN0QDFvzVY3RfCzdInvGPGzjA==}
2143
  engines: {node: '>=14'}
2144
 
2145
+ flatted@3.3.4:
2146
+ resolution: {integrity: sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==}
2147
+
2148
  follow-redirects@1.15.11:
2149
  resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
2150
  engines: {node: '>=4.0'}
 
2396
  resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==}
2397
  engines: {node: '>=12'}
2398
 
2399
+ magic-string@0.30.21:
2400
+ resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
2401
+
2402
  math-intrinsics@1.1.0:
2403
  resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
2404
  engines: {node: '>= 0.4'}
 
2429
  mnemonist@0.39.6:
2430
  resolution: {integrity: sha512-A/0v5Z59y63US00cRSLiloEIw3t5G+MiKz4BhX21FI+YBJXBOGW0ohFxTxO08dsOYlzxo87T7vGfZKYp2bcAWA==}
2431
 
2432
+ mrmime@2.0.1:
2433
+ resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
2434
+ engines: {node: '>=10'}
2435
+
2436
  ms@2.1.3:
2437
  resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
2438
 
 
2501
  obliterator@2.0.5:
2502
  resolution: {integrity: sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==}
2503
 
2504
+ obug@2.1.1:
2505
+ resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
2506
+
2507
  on-exit-leak-free@2.1.2:
2508
  resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
2509
  engines: {node: '>=14.0.0'}
 
2545
  path-parse@1.0.7:
2546
  resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
2547
 
2548
+ pathe@2.0.3:
2549
+ resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
2550
+
2551
  pend@1.2.0:
2552
  resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
2553
 
 
2808
  resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
2809
  engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
2810
 
2811
+ siginfo@2.0.0:
2812
+ resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
2813
+
2814
+ sirv@3.0.2:
2815
+ resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==}
2816
+ engines: {node: '>=18'}
2817
+
2818
  smart-buffer@4.2.0:
2819
  resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
2820
  engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
 
2845
  resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
2846
  engines: {node: '>= 10.x'}
2847
 
2848
+ stackback@0.0.2:
2849
+ resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
2850
+
2851
  standard-as-callback@2.1.0:
2852
  resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
2853
 
2854
+ std-env@3.10.0:
2855
+ resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
2856
+
2857
  streamx@2.23.0:
2858
  resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==}
2859
 
 
2919
  through@2.3.8:
2920
  resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
2921
 
2922
+ tinybench@2.9.0:
2923
+ resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
2924
+
2925
+ tinyexec@1.0.2:
2926
+ resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
2927
+ engines: {node: '>=18'}
2928
+
2929
  tinyglobby@0.2.15:
2930
  resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
2931
  engines: {node: '>=12.0.0'}
2932
 
2933
+ tinyrainbow@3.0.3:
2934
+ resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==}
2935
+ engines: {node: '>=14.0.0'}
2936
+
2937
  to-regex-range@5.0.1:
2938
  resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
2939
  engines: {node: '>=8.0'}
 
2942
  resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==}
2943
  engines: {node: '>=12'}
2944
 
2945
+ totalist@3.0.1:
2946
+ resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
2947
+ engines: {node: '>=6'}
2948
+
2949
  tr46@0.0.3:
2950
  resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
2951
 
 
3058
  terser:
3059
  optional: true
3060
 
3061
+ vite@7.3.1:
3062
+ resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
3063
+ engines: {node: ^20.19.0 || >=22.12.0}
3064
+ hasBin: true
3065
+ peerDependencies:
3066
+ '@types/node': ^20.19.0 || >=22.12.0
3067
+ jiti: '>=1.21.0'
3068
+ less: ^4.0.0
3069
+ lightningcss: ^1.21.0
3070
+ sass: ^1.70.0
3071
+ sass-embedded: ^1.70.0
3072
+ stylus: '>=0.54.8'
3073
+ sugarss: ^5.0.0
3074
+ terser: ^5.16.0
3075
+ tsx: ^4.8.1
3076
+ yaml: ^2.4.2
3077
+ peerDependenciesMeta:
3078
+ '@types/node':
3079
+ optional: true
3080
+ jiti:
3081
+ optional: true
3082
+ less:
3083
+ optional: true
3084
+ lightningcss:
3085
+ optional: true
3086
+ sass:
3087
+ optional: true
3088
+ sass-embedded:
3089
+ optional: true
3090
+ stylus:
3091
+ optional: true
3092
+ sugarss:
3093
+ optional: true
3094
+ terser:
3095
+ optional: true
3096
+ tsx:
3097
+ optional: true
3098
+ yaml:
3099
+ optional: true
3100
+
3101
+ vitest@4.0.18:
3102
+ resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==}
3103
+ engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
3104
+ hasBin: true
3105
+ peerDependencies:
3106
+ '@edge-runtime/vm': '*'
3107
+ '@opentelemetry/api': ^1.9.0
3108
+ '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
3109
+ '@vitest/browser-playwright': 4.0.18
3110
+ '@vitest/browser-preview': 4.0.18
3111
+ '@vitest/browser-webdriverio': 4.0.18
3112
+ '@vitest/ui': 4.0.18
3113
+ happy-dom: '*'
3114
+ jsdom: '*'
3115
+ peerDependenciesMeta:
3116
+ '@edge-runtime/vm':
3117
+ optional: true
3118
+ '@opentelemetry/api':
3119
+ optional: true
3120
+ '@types/node':
3121
+ optional: true
3122
+ '@vitest/browser-playwright':
3123
+ optional: true
3124
+ '@vitest/browser-preview':
3125
+ optional: true
3126
+ '@vitest/browser-webdriverio':
3127
+ optional: true
3128
+ '@vitest/ui':
3129
+ optional: true
3130
+ happy-dom:
3131
+ optional: true
3132
+ jsdom:
3133
+ optional: true
3134
+
3135
  web-streams-polyfill@4.0.0-beta.3:
3136
  resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
3137
  engines: {node: '>= 14'}
 
3142
  whatwg-url@5.0.0:
3143
  resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
3144
 
3145
+ why-is-node-running@2.3.0:
3146
+ resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
3147
+ engines: {node: '>=8'}
3148
+ hasBin: true
3149
+
3150
  wrap-ansi@7.0.0:
3151
  resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
3152
  engines: {node: '>=10'}
 
3190
  zod@3.25.76:
3191
  resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
3192
 
3193
+ zod@4.3.6:
3194
+ resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
3195
+
3196
  snapshots:
3197
 
3198
  '@alloc/quick-lru@5.2.0': {}
 
3810
  '@esbuild/aix-ppc64@0.21.5':
3811
  optional: true
3812
 
3813
+ '@esbuild/aix-ppc64@0.27.3':
3814
+ optional: true
3815
+
3816
  '@esbuild/android-arm64@0.18.20':
3817
  optional: true
3818
 
3819
  '@esbuild/android-arm64@0.21.5':
3820
  optional: true
3821
 
3822
+ '@esbuild/android-arm64@0.27.3':
3823
+ optional: true
3824
+
3825
  '@esbuild/android-arm@0.18.20':
3826
  optional: true
3827
 
3828
  '@esbuild/android-arm@0.21.5':
3829
  optional: true
3830
 
3831
+ '@esbuild/android-arm@0.27.3':
3832
+ optional: true
3833
+
3834
  '@esbuild/android-x64@0.18.20':
3835
  optional: true
3836
 
3837
  '@esbuild/android-x64@0.21.5':
3838
  optional: true
3839
 
3840
+ '@esbuild/android-x64@0.27.3':
3841
+ optional: true
3842
+
3843
  '@esbuild/darwin-arm64@0.18.20':
3844
  optional: true
3845
 
3846
  '@esbuild/darwin-arm64@0.21.5':
3847
  optional: true
3848
 
3849
+ '@esbuild/darwin-arm64@0.27.3':
3850
+ optional: true
3851
+
3852
  '@esbuild/darwin-x64@0.18.20':
3853
  optional: true
3854
 
3855
  '@esbuild/darwin-x64@0.21.5':
3856
  optional: true
3857
 
3858
+ '@esbuild/darwin-x64@0.27.3':
3859
+ optional: true
3860
+
3861
  '@esbuild/freebsd-arm64@0.18.20':
3862
  optional: true
3863
 
3864
  '@esbuild/freebsd-arm64@0.21.5':
3865
  optional: true
3866
 
3867
+ '@esbuild/freebsd-arm64@0.27.3':
3868
+ optional: true
3869
+
3870
  '@esbuild/freebsd-x64@0.18.20':
3871
  optional: true
3872
 
3873
  '@esbuild/freebsd-x64@0.21.5':
3874
  optional: true
3875
 
3876
+ '@esbuild/freebsd-x64@0.27.3':
3877
+ optional: true
3878
+
3879
  '@esbuild/linux-arm64@0.18.20':
3880
  optional: true
3881
 
3882
  '@esbuild/linux-arm64@0.21.5':
3883
  optional: true
3884
 
3885
+ '@esbuild/linux-arm64@0.27.3':
3886
+ optional: true
3887
+
3888
  '@esbuild/linux-arm@0.18.20':
3889
  optional: true
3890
 
3891
  '@esbuild/linux-arm@0.21.5':
3892
  optional: true
3893
 
3894
+ '@esbuild/linux-arm@0.27.3':
3895
+ optional: true
3896
+
3897
  '@esbuild/linux-ia32@0.18.20':
3898
  optional: true
3899
 
3900
  '@esbuild/linux-ia32@0.21.5':
3901
  optional: true
3902
 
3903
+ '@esbuild/linux-ia32@0.27.3':
3904
+ optional: true
3905
+
3906
  '@esbuild/linux-loong64@0.18.20':
3907
  optional: true
3908
 
3909
  '@esbuild/linux-loong64@0.21.5':
3910
  optional: true
3911
 
3912
+ '@esbuild/linux-loong64@0.27.3':
3913
+ optional: true
3914
+
3915
  '@esbuild/linux-mips64el@0.18.20':
3916
  optional: true
3917
 
3918
  '@esbuild/linux-mips64el@0.21.5':
3919
  optional: true
3920
 
3921
+ '@esbuild/linux-mips64el@0.27.3':
3922
+ optional: true
3923
+
3924
  '@esbuild/linux-ppc64@0.18.20':
3925
  optional: true
3926
 
3927
  '@esbuild/linux-ppc64@0.21.5':
3928
  optional: true
3929
 
3930
+ '@esbuild/linux-ppc64@0.27.3':
3931
+ optional: true
3932
+
3933
  '@esbuild/linux-riscv64@0.18.20':
3934
  optional: true
3935
 
3936
  '@esbuild/linux-riscv64@0.21.5':
3937
  optional: true
3938
 
3939
+ '@esbuild/linux-riscv64@0.27.3':
3940
+ optional: true
3941
+
3942
  '@esbuild/linux-s390x@0.18.20':
3943
  optional: true
3944
 
3945
  '@esbuild/linux-s390x@0.21.5':
3946
  optional: true
3947
 
3948
+ '@esbuild/linux-s390x@0.27.3':
3949
+ optional: true
3950
+
3951
  '@esbuild/linux-x64@0.18.20':
3952
  optional: true
3953
 
3954
  '@esbuild/linux-x64@0.21.5':
3955
  optional: true
3956
 
3957
+ '@esbuild/linux-x64@0.27.3':
3958
+ optional: true
3959
+
3960
+ '@esbuild/netbsd-arm64@0.27.3':
3961
+ optional: true
3962
+
3963
  '@esbuild/netbsd-x64@0.18.20':
3964
  optional: true
3965
 
3966
  '@esbuild/netbsd-x64@0.21.5':
3967
  optional: true
3968
 
3969
+ '@esbuild/netbsd-x64@0.27.3':
3970
+ optional: true
3971
+
3972
+ '@esbuild/openbsd-arm64@0.27.3':
3973
+ optional: true
3974
+
3975
  '@esbuild/openbsd-x64@0.18.20':
3976
  optional: true
3977
 
3978
  '@esbuild/openbsd-x64@0.21.5':
3979
  optional: true
3980
 
3981
+ '@esbuild/openbsd-x64@0.27.3':
3982
+ optional: true
3983
+
3984
+ '@esbuild/openharmony-arm64@0.27.3':
3985
+ optional: true
3986
+
3987
  '@esbuild/sunos-x64@0.18.20':
3988
  optional: true
3989
 
3990
  '@esbuild/sunos-x64@0.21.5':
3991
  optional: true
3992
 
3993
+ '@esbuild/sunos-x64@0.27.3':
3994
+ optional: true
3995
+
3996
  '@esbuild/win32-arm64@0.18.20':
3997
  optional: true
3998
 
3999
  '@esbuild/win32-arm64@0.21.5':
4000
  optional: true
4001
 
4002
+ '@esbuild/win32-arm64@0.27.3':
4003
+ optional: true
4004
+
4005
  '@esbuild/win32-ia32@0.18.20':
4006
  optional: true
4007
 
4008
  '@esbuild/win32-ia32@0.21.5':
4009
  optional: true
4010
 
4011
+ '@esbuild/win32-ia32@0.27.3':
4012
+ optional: true
4013
+
4014
  '@esbuild/win32-x64@0.18.20':
4015
  optional: true
4016
 
4017
  '@esbuild/win32-x64@0.21.5':
4018
  optional: true
4019
 
4020
+ '@esbuild/win32-x64@0.27.3':
4021
+ optional: true
4022
+
4023
  '@fastify/ajv-compiler@3.6.0':
4024
  dependencies:
4025
  ajv: 8.18.0
 
4198
 
4199
  '@pinojs/redact@0.4.0': {}
4200
 
4201
+ '@polka/url@1.0.0-next.29': {}
4202
+
4203
  '@prisma/client@5.22.0(prisma@5.22.0)':
4204
  optionalDependencies:
4205
  prisma: 5.22.0
 
4658
  dependencies:
4659
  tslib: 2.8.1
4660
 
4661
+ '@standard-schema/spec@1.1.0': {}
4662
+
4663
  '@tootallnate/quickjs-emscripten@0.23.0': {}
4664
 
4665
  '@types/babel__core@7.20.5':
 
4683
  dependencies:
4684
  '@babel/types': 7.29.0
4685
 
4686
+ '@types/chai@5.2.3':
4687
+ dependencies:
4688
+ '@types/deep-eql': 4.0.2
4689
+ assertion-error: 2.0.1
4690
+
4691
+ '@types/deep-eql@4.0.2': {}
4692
+
4693
  '@types/diff@8.0.0':
4694
  dependencies:
4695
  diff: 8.0.3
 
4750
  transitivePeerDependencies:
4751
  - supports-color
4752
 
4753
+ '@vitest/expect@4.0.18':
4754
+ dependencies:
4755
+ '@standard-schema/spec': 1.1.0
4756
+ '@types/chai': 5.2.3
4757
+ '@vitest/spy': 4.0.18
4758
+ '@vitest/utils': 4.0.18
4759
+ chai: 6.2.2
4760
+ tinyrainbow: 3.0.3
4761
+
4762
+ '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@20.19.33)(jiti@1.21.7)(tsx@3.14.0))':
4763
+ dependencies:
4764
+ '@vitest/spy': 4.0.18
4765
+ estree-walker: 3.0.3
4766
+ magic-string: 0.30.21
4767
+ optionalDependencies:
4768
+ vite: 7.3.1(@types/node@20.19.33)(jiti@1.21.7)(tsx@3.14.0)
4769
+
4770
+ '@vitest/pretty-format@4.0.18':
4771
+ dependencies:
4772
+ tinyrainbow: 3.0.3
4773
+
4774
+ '@vitest/runner@4.0.18':
4775
+ dependencies:
4776
+ '@vitest/utils': 4.0.18
4777
+ pathe: 2.0.3
4778
+
4779
+ '@vitest/snapshot@4.0.18':
4780
+ dependencies:
4781
+ '@vitest/pretty-format': 4.0.18
4782
+ magic-string: 0.30.21
4783
+ pathe: 2.0.3
4784
+
4785
+ '@vitest/spy@4.0.18': {}
4786
+
4787
+ '@vitest/ui@4.0.18(vitest@4.0.18)':
4788
+ dependencies:
4789
+ '@vitest/utils': 4.0.18
4790
+ fflate: 0.8.2
4791
+ flatted: 3.3.4
4792
+ pathe: 2.0.3
4793
+ sirv: 3.0.2
4794
+ tinyglobby: 0.2.15
4795
+ tinyrainbow: 3.0.3
4796
+ vitest: 4.0.18(@types/node@20.19.33)(@vitest/ui@4.0.18)(jiti@1.21.7)(tsx@3.14.0)
4797
+
4798
+ '@vitest/utils@4.0.18':
4799
+ dependencies:
4800
+ '@vitest/pretty-format': 4.0.18
4801
+ tinyrainbow: 3.0.3
4802
+
4803
  abort-controller@3.0.0:
4804
  dependencies:
4805
  event-target-shim: 5.0.1
 
4844
 
4845
  argparse@2.0.1: {}
4846
 
4847
+ assertion-error@2.0.1: {}
4848
+
4849
  ast-types@0.13.4:
4850
  dependencies:
4851
  tslib: 2.8.1
 
4990
 
4991
  caniuse-lite@1.0.30001770: {}
4992
 
4993
+ chai@6.2.2: {}
4994
+
4995
  chokidar@3.6.0:
4996
  dependencies:
4997
  anymatch: 3.1.3
 
5106
 
5107
  es-errors@1.3.0: {}
5108
 
5109
+ es-module-lexer@1.7.0: {}
5110
+
5111
  es-object-atoms@1.1.1:
5112
  dependencies:
5113
  es-errors: 1.3.0
 
5170
  '@esbuild/win32-ia32': 0.21.5
5171
  '@esbuild/win32-x64': 0.21.5
5172
 
5173
+ esbuild@0.27.3:
5174
+ optionalDependencies:
5175
+ '@esbuild/aix-ppc64': 0.27.3
5176
+ '@esbuild/android-arm': 0.27.3
5177
+ '@esbuild/android-arm64': 0.27.3
5178
+ '@esbuild/android-x64': 0.27.3
5179
+ '@esbuild/darwin-arm64': 0.27.3
5180
+ '@esbuild/darwin-x64': 0.27.3
5181
+ '@esbuild/freebsd-arm64': 0.27.3
5182
+ '@esbuild/freebsd-x64': 0.27.3
5183
+ '@esbuild/linux-arm': 0.27.3
5184
+ '@esbuild/linux-arm64': 0.27.3
5185
+ '@esbuild/linux-ia32': 0.27.3
5186
+ '@esbuild/linux-loong64': 0.27.3
5187
+ '@esbuild/linux-mips64el': 0.27.3
5188
+ '@esbuild/linux-ppc64': 0.27.3
5189
+ '@esbuild/linux-riscv64': 0.27.3
5190
+ '@esbuild/linux-s390x': 0.27.3
5191
+ '@esbuild/linux-x64': 0.27.3
5192
+ '@esbuild/netbsd-arm64': 0.27.3
5193
+ '@esbuild/netbsd-x64': 0.27.3
5194
+ '@esbuild/openbsd-arm64': 0.27.3
5195
+ '@esbuild/openbsd-x64': 0.27.3
5196
+ '@esbuild/openharmony-arm64': 0.27.3
5197
+ '@esbuild/sunos-x64': 0.27.3
5198
+ '@esbuild/win32-arm64': 0.27.3
5199
+ '@esbuild/win32-ia32': 0.27.3
5200
+ '@esbuild/win32-x64': 0.27.3
5201
+
5202
  escalade@3.2.0: {}
5203
 
5204
  escodegen@2.1.0:
 
5213
 
5214
  estraverse@5.3.0: {}
5215
 
5216
+ estree-walker@3.0.3:
5217
+ dependencies:
5218
+ '@types/estree': 1.0.8
5219
+
5220
  esutils@2.0.3: {}
5221
 
5222
  event-target-shim@5.0.1: {}
 
5227
  transitivePeerDependencies:
5228
  - bare-abort-controller
5229
 
5230
+ expect-type@1.3.0: {}
5231
+
5232
  extract-zip@2.0.1:
5233
  dependencies:
5234
  debug: 4.4.3
 
5316
  optionalDependencies:
5317
  picomatch: 4.0.3
5318
 
5319
+ fflate@0.8.2: {}
5320
+
5321
  fill-range@7.1.1:
5322
  dependencies:
5323
  to-regex-range: 5.0.1
 
5328
  fast-querystring: 1.1.2
5329
  safe-regex2: 3.1.0
5330
 
5331
+ flatted@3.3.4: {}
5332
+
5333
  follow-redirects@1.15.11: {}
5334
 
5335
  form-data-encoder@1.7.2: {}
 
5581
 
5582
  luxon@3.7.2: {}
5583
 
5584
+ magic-string@0.30.21:
5585
+ dependencies:
5586
+ '@jridgewell/sourcemap-codec': 1.5.5
5587
+
5588
  math-intrinsics@1.1.0: {}
5589
 
5590
  merge2@1.4.1: {}
 
5610
  dependencies:
5611
  obliterator: 2.0.5
5612
 
5613
+ mrmime@2.0.1: {}
5614
+
5615
  ms@2.1.3: {}
5616
 
5617
  msgpackr-extract@3.0.3:
 
5669
 
5670
  obliterator@2.0.5: {}
5671
 
5672
+ obug@2.1.1: {}
5673
+
5674
  on-exit-leak-free@2.1.2: {}
5675
 
5676
  once@1.4.0:
 
5725
 
5726
  path-parse@1.0.7: {}
5727
 
5728
+ pathe@2.0.3: {}
5729
+
5730
  pend@1.2.0: {}
5731
 
5732
  picocolors@1.1.1: {}
 
6040
  '@img/sharp-win32-ia32': 0.34.5
6041
  '@img/sharp-win32-x64': 0.34.5
6042
 
6043
+ siginfo@2.0.0: {}
6044
+
6045
+ sirv@3.0.2:
6046
+ dependencies:
6047
+ '@polka/url': 1.0.0-next.29
6048
+ mrmime: 2.0.1
6049
+ totalist: 3.0.1
6050
+
6051
  smart-buffer@4.2.0: {}
6052
 
6053
  socks-proxy-agent@8.0.5:
 
6078
 
6079
  split2@4.2.0: {}
6080
 
6081
+ stackback@0.0.2: {}
6082
+
6083
  standard-as-callback@2.1.0: {}
6084
 
6085
+ std-env@3.10.0: {}
6086
+
6087
  streamx@2.23.0:
6088
  dependencies:
6089
  events-universal: 1.0.1
 
6202
 
6203
  through@2.3.8: {}
6204
 
6205
+ tinybench@2.9.0: {}
6206
+
6207
+ tinyexec@1.0.2: {}
6208
+
6209
  tinyglobby@0.2.15:
6210
  dependencies:
6211
  fdir: 6.5.0(picomatch@4.0.3)
6212
  picomatch: 4.0.3
6213
 
6214
+ tinyrainbow@3.0.3: {}
6215
+
6216
  to-regex-range@5.0.1:
6217
  dependencies:
6218
  is-number: 7.0.0
6219
 
6220
  toad-cache@3.7.0: {}
6221
 
6222
+ totalist@3.0.1: {}
6223
+
6224
  tr46@0.0.3: {}
6225
 
6226
  ts-interface-checker@0.1.13: {}
 
6296
  '@types/node': 22.19.11
6297
  fsevents: 2.3.3
6298
 
6299
+ vite@7.3.1(@types/node@20.19.33)(jiti@1.21.7)(tsx@3.14.0):
6300
+ dependencies:
6301
+ esbuild: 0.27.3
6302
+ fdir: 6.5.0(picomatch@4.0.3)
6303
+ picomatch: 4.0.3
6304
+ postcss: 8.5.6
6305
+ rollup: 4.57.1
6306
+ tinyglobby: 0.2.15
6307
+ optionalDependencies:
6308
+ '@types/node': 20.19.33
6309
+ fsevents: 2.3.3
6310
+ jiti: 1.21.7
6311
+ tsx: 3.14.0
6312
+
6313
+ vitest@4.0.18(@types/node@20.19.33)(@vitest/ui@4.0.18)(jiti@1.21.7)(tsx@3.14.0):
6314
+ dependencies:
6315
+ '@vitest/expect': 4.0.18
6316
+ '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@20.19.33)(jiti@1.21.7)(tsx@3.14.0))
6317
+ '@vitest/pretty-format': 4.0.18
6318
+ '@vitest/runner': 4.0.18
6319
+ '@vitest/snapshot': 4.0.18
6320
+ '@vitest/spy': 4.0.18
6321
+ '@vitest/utils': 4.0.18
6322
+ es-module-lexer: 1.7.0
6323
+ expect-type: 1.3.0
6324
+ magic-string: 0.30.21
6325
+ obug: 2.1.1
6326
+ pathe: 2.0.3
6327
+ picomatch: 4.0.3
6328
+ std-env: 3.10.0
6329
+ tinybench: 2.9.0
6330
+ tinyexec: 1.0.2
6331
+ tinyglobby: 0.2.15
6332
+ tinyrainbow: 3.0.3
6333
+ vite: 7.3.1(@types/node@20.19.33)(jiti@1.21.7)(tsx@3.14.0)
6334
+ why-is-node-running: 2.3.0
6335
+ optionalDependencies:
6336
+ '@types/node': 20.19.33
6337
+ '@vitest/ui': 4.0.18(vitest@4.0.18)
6338
+ transitivePeerDependencies:
6339
+ - jiti
6340
+ - less
6341
+ - lightningcss
6342
+ - msw
6343
+ - sass
6344
+ - sass-embedded
6345
+ - stylus
6346
+ - sugarss
6347
+ - terser
6348
+ - tsx
6349
+ - yaml
6350
+
6351
  web-streams-polyfill@4.0.0-beta.3: {}
6352
 
6353
  webidl-conversions@3.0.1: {}
 
6357
  tr46: 0.0.3
6358
  webidl-conversions: 3.0.1
6359
 
6360
+ why-is-node-running@2.3.0:
6361
+ dependencies:
6362
+ siginfo: 2.0.0
6363
+ stackback: 0.0.2
6364
+
6365
  wrap-ansi@7.0.0:
6366
  dependencies:
6367
  ansi-styles: 4.3.0
 
6396
  zod@3.23.8: {}
6397
 
6398
  zod@3.25.76: {}
6399
+
6400
+ zod@4.3.6: {}