@runnerpro/backend 1.21.11 → 1.21.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/lib/cjs/achievements/catalog.js +204 -0
  2. package/lib/cjs/achievements/index.js +805 -0
  3. package/lib/cjs/achievements/streak.js +126 -0
  4. package/lib/cjs/chat/api/conversation.js +21 -29
  5. package/lib/cjs/chat/exposed/conversation.js +0 -7
  6. package/lib/cjs/chat/index.js +0 -7
  7. package/lib/cjs/index.js +34 -2
  8. package/lib/cjs/locale/en.js +0 -4
  9. package/lib/cjs/locale/es.js +0 -4
  10. package/lib/cjs/locale/fr.js +0 -4
  11. package/lib/cjs/locale/it.js +0 -4
  12. package/lib/cjs/sendNotification/index.js +4 -70
  13. package/lib/cjs/types/achievements/catalog.d.ts +104 -0
  14. package/lib/cjs/types/achievements/catalog.d.ts.map +1 -0
  15. package/lib/cjs/types/achievements/index.d.ts +41 -0
  16. package/lib/cjs/types/achievements/index.d.ts.map +1 -0
  17. package/lib/cjs/types/achievements/streak.d.ts +20 -0
  18. package/lib/cjs/types/achievements/streak.d.ts.map +1 -0
  19. package/lib/cjs/types/chat/api/conversation.d.ts.map +1 -1
  20. package/lib/cjs/types/chat/exposed/conversation.d.ts.map +1 -1
  21. package/lib/cjs/types/chat/index.d.ts.map +1 -1
  22. package/lib/cjs/types/index.d.ts +3 -1
  23. package/lib/cjs/types/index.d.ts.map +1 -1
  24. package/lib/cjs/types/locale/en.d.ts +0 -4
  25. package/lib/cjs/types/locale/en.d.ts.map +1 -1
  26. package/lib/cjs/types/locale/es.d.ts +0 -4
  27. package/lib/cjs/types/locale/es.d.ts.map +1 -1
  28. package/lib/cjs/types/locale/fr.d.ts +0 -4
  29. package/lib/cjs/types/locale/fr.d.ts.map +1 -1
  30. package/lib/cjs/types/locale/it.d.ts +0 -4
  31. package/lib/cjs/types/locale/it.d.ts.map +1 -1
  32. package/lib/cjs/types/sendNotification/index.d.ts +1 -20
  33. package/lib/cjs/types/sendNotification/index.d.ts.map +1 -1
  34. package/lib/cjs/types/workout/planificacionPrueba7dias/index.d.ts.map +1 -1
  35. package/lib/cjs/types/workout/saveWorkoutAplication.d.ts.map +1 -1
  36. package/lib/cjs/workout/planificacionPrueba7dias/index.js +7 -3
  37. package/lib/cjs/workout/saveWorkoutAplication.js +10 -0
  38. package/package.json +1 -1
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.computeStreakStats = exports.requiredFromPlanned = exports.STREAK_COMPLETION_THRESHOLD = void 0;
16
+ const db_1 = require("../db");
17
+ const moment_1 = __importDefault(require("moment"));
18
+ /**
19
+ * Regla de racha: una semana cuenta como completada si el usuario hace al menos
20
+ * el 80% de los entrenamientos planificados de esa semana (regla 80%). Tipos
21
+ * entrenables, semanas ISO. La semana actual incompleta no rompe la racha.
22
+ */
23
+ exports.STREAK_COMPLETION_THRESHOLD = 0.8;
24
+ const requiredFromPlanned = (plannedWorkouts) => {
25
+ const planned = Number(plannedWorkouts) || 0;
26
+ if (planned <= 0)
27
+ return 0;
28
+ return Math.max(1, Math.ceil(planned * exports.STREAK_COMPLETION_THRESHOLD));
29
+ };
30
+ exports.requiredFromPlanned = requiredFromPlanned;
31
+ /**
32
+ * Racha actual (current) y récord histórico (longest), en semanas.
33
+ *
34
+ * @param userId ID del cliente.
35
+ * @param sinceDate Si se pasa, solo cuentan workouts con "DATE" >= sinceDate
36
+ * (ancla "empezar de cero" para logros). Sin él, comportamiento sobre todo el
37
+ * historial.
38
+ */
39
+ const computeStreakStats = (userId, sinceDate = null) => __awaiter(void 0, void 0, void 0, function* () {
40
+ const params = [userId];
41
+ let sinceFilter = '';
42
+ if (sinceDate) {
43
+ sinceFilter = ' AND "DATE"::date >= ?::date';
44
+ params.push(sinceDate);
45
+ }
46
+ const rows = yield (0, db_1.query)(`SELECT date_trunc('week', "DATE"::date) AS "WEEK START",
47
+ COUNT(*) FILTER (WHERE "DONE" = true) AS completed,
48
+ COUNT(*) AS planned
49
+ FROM "WORKOUT"
50
+ WHERE "ID CLIENTE" = ?
51
+ AND "TYPE" IN ('CORRER','FUERZA','SPORT','BIKE','SWIM','TRAIL','WALK')
52
+ AND "SHOW CLIENT" IS TRUE${sinceFilter}
53
+ GROUP BY 1
54
+ ORDER BY 1 ASC`, params);
55
+ if (rows.length === 0)
56
+ return { current: 0, longest: 0 };
57
+ const byWeek = new Map();
58
+ for (const row of rows) {
59
+ const weekKey = (0, moment_1.default)(row.weekStart).startOf('isoWeek').format('YYYY-MM-DD');
60
+ byWeek.set(weekKey, {
61
+ completed: Number(row.completed) || 0,
62
+ planned: Number(row.planned) || 0,
63
+ });
64
+ }
65
+ const now = (0, moment_1.default)();
66
+ const currentWeek = now.clone().startOf('isoWeek');
67
+ // ── Récord histórico ───────────────────────────────────────────────
68
+ const firstWeek = (0, moment_1.default)(rows[0].weekStart).startOf('isoWeek');
69
+ const lastWeek = currentWeek.clone();
70
+ let longest = 0;
71
+ let segment = 0;
72
+ const fwd = firstWeek.clone();
73
+ const MAX_WEEKS_FORWARD = 520;
74
+ for (let i = 0; i < MAX_WEEKS_FORWARD && fwd.isSameOrBefore(lastWeek, 'day'); i++) {
75
+ const k = fwd.format('YYYY-MM-DD');
76
+ const data = byWeek.get(k);
77
+ const required = data ? (0, exports.requiredFromPlanned)(data.planned) : 0;
78
+ const isCurrent = fwd.isSame(currentWeek, 'day');
79
+ const cumple = !!data && required > 0 && data.completed >= required;
80
+ if (cumple) {
81
+ segment++;
82
+ if (segment > longest)
83
+ longest = segment;
84
+ }
85
+ else if (!isCurrent) {
86
+ segment = 0;
87
+ }
88
+ fwd.add(1, 'week');
89
+ }
90
+ // ── Racha actual ───────────────────────────────────────────────────
91
+ const cursor = currentWeek.clone();
92
+ const currentData = byWeek.get(cursor.format('YYYY-MM-DD'));
93
+ let current = 0;
94
+ if (currentData) {
95
+ const required = (0, exports.requiredFromPlanned)(currentData.planned);
96
+ if (required > 0 && currentData.completed >= required) {
97
+ current++;
98
+ }
99
+ }
100
+ cursor.subtract(1, 'week');
101
+ const MAX_LOOKBACK_WEEKS = 520;
102
+ let broken = false;
103
+ for (let i = 0; i < MAX_LOOKBACK_WEEKS && !broken; i++) {
104
+ const data = byWeek.get(cursor.format('YYYY-MM-DD'));
105
+ if (!data) {
106
+ broken = true;
107
+ break;
108
+ }
109
+ const required = (0, exports.requiredFromPlanned)(data.planned);
110
+ if (required === 0) {
111
+ cursor.subtract(1, 'week');
112
+ continue;
113
+ }
114
+ if (data.completed >= required) {
115
+ current++;
116
+ cursor.subtract(1, 'week');
117
+ }
118
+ else {
119
+ broken = true;
120
+ }
121
+ }
122
+ if (current > longest)
123
+ longest = current;
124
+ return { current, longest };
125
+ });
126
+ exports.computeStreakStats = computeStreakStats;
@@ -368,18 +368,22 @@ const sendMessage = (req, res, { sendNotification, firebaseMessaging, isClient }
368
368
  }
369
369
  }
370
370
  const [message] = yield (0, index_1.query)('INSERT INTO [CHAT MESSAGE] ([ID CLIENTE], [ID SENDER], [TEXT], [TEXT PREFERRED LANGUAGE], [PREFERRED LANGUAGE], [REPLY MESSAGE ID], [ID WORKOUT], [TYPE]) VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING [ID]', [isClient ? userid : idCliente, userid, textSpanish, textPreferredLanguage, preferredLanguage, replyMessageId, idWorkout, type]);
371
- res.send({ idMessage: message.id, text: textSpanish });
371
+ // Logros: mensaje enviado por el cliente → coach_1er_msg/coach_10_msg. Await
372
+ // acotado (1500 ms) para devolver los recién desbloqueados al FE (push). El
373
+ // bounded aísla fallo/lentitud: si vence, persiste en background y el FE lo
374
+ // recupera en el siguiente refetch.
375
+ let logrosDesbloqueados = [];
376
+ if (isClient) {
377
+ logrosDesbloqueados = yield (0, index_1.evaluateAchievementsBounded)(userid, 'chat', {}, 1500);
378
+ }
379
+ res.send({ idMessage: message.id, text: textSpanish, logrosDesbloqueados });
372
380
  if (!isClient) {
373
- // Recuperamos el nombre del entrenador para firmar el push con su nombre real (no genérico).
374
- const [entrenadorRow] = yield (0, index_1.query)('SELECT [NAME] FROM [ENTRENADOR] WHERE [ID] = ?', [userid]);
375
- const entrenador = ((entrenadorRow === null || entrenadorRow === void 0 ? void 0 : entrenadorRow.name) || 'Tu entrenador').split(' ')[0];
376
381
  sendNotification({
377
382
  firebaseMessaging,
378
383
  idCliente,
379
- title: (0, index_2.t)('{{ entrenador }} 💬', preferredLanguage || common_1.LANGUAGES.ES).replace('{{ entrenador }}', entrenador),
384
+ title: '💬 Nuevo mensaje de tu entrenador',
380
385
  body: textPreferredLanguage || textSpanish,
381
386
  screen: common_1.NOTIFICATION_SCREEN_TYPES.CHAT,
382
- channelId: common_1.NOTIFICATION_CHANNELS.COACH,
383
387
  });
384
388
  // Enviar a N8N lo que ha escrito el entrenador
385
389
  // const [lastSuggestionMsg] = await query(
@@ -431,20 +435,11 @@ const sendEmoji = (req, res, { sendNotification, firebaseMessaging, isClient })
431
435
  ]);
432
436
  res.send({ idReaction: reaction.id });
433
437
  if (!isClient) {
434
- const [[entrenadorRow], [clienteRow]] = yield Promise.all([
435
- (0, index_1.query)('SELECT [NAME] FROM [ENTRENADOR] WHERE [ID] = ?', [userid]),
436
- (0, index_1.query)('SELECT [PREFERRED LANGUAGE] FROM [CLIENTE] WHERE [ID] = ?', [idCliente]),
437
- ]);
438
- const entrenador = ((entrenadorRow === null || entrenadorRow === void 0 ? void 0 : entrenadorRow.name) || 'Tu entrenador').split(' ')[0];
439
- const lang = (clienteRow === null || clienteRow === void 0 ? void 0 : clienteRow.preferredLanguage) || common_1.LANGUAGES.ES;
440
438
  sendNotification({
441
439
  firebaseMessaging,
442
440
  idCliente,
443
- body: (0, index_2.t)('{{ entrenador }} reaccionó {{ emoji }} a tu mensaje', lang)
444
- .replace('{{ entrenador }}', entrenador)
445
- .replace('{{ emoji }}', emoji),
441
+ body: `El entrenador ha reaccionado a un mensaje '${emoji}'`,
446
442
  screen: common_1.NOTIFICATION_SCREEN_TYPES.CHAT,
447
- channelId: common_1.NOTIFICATION_CHANNELS.COACH,
448
443
  });
449
444
  }
450
445
  });
@@ -486,6 +481,12 @@ const sendFile = (req, res, { sendNotification, firebaseMessaging, isClient, buc
486
481
  thumbnail = yield getThumbnailFromVideo(filePath, duration);
487
482
  }
488
483
  const [{ id: idFile }] = yield (0, index_1.query)('INSERT INTO [CHAT MESSAGE] ([ID CLIENTE], [ID SENDER], [TEXT], [MIMETYPE], [DURATION], [TYPE]) VALUES (?, ?, ?, ?, ?, ?) RETURNING [ID]', [isClient ? userid : idCliente, userid, req.file.originalname, req.file.mimetype, duration || null, type || 2]);
484
+ // Logros: adjunto enviado por el cliente también cuenta como mensaje. Await
485
+ // acotado para devolver los recién desbloqueados al FE en la respuesta (push).
486
+ let logrosDesbloqueados = [];
487
+ if (isClient) {
488
+ logrosDesbloqueados = yield (0, index_1.evaluateAchievementsBounded)(userid, 'chat', {}, 1500);
489
+ }
489
490
  const fileData = fs_1.default.readFileSync(filePath);
490
491
  const files = [];
491
492
  if (req.file.mimetype.includes('image')) {
@@ -506,7 +507,7 @@ const sendFile = (req, res, { sendNotification, firebaseMessaging, isClient, buc
506
507
  else {
507
508
  files.push({ data: fileData, id: idFile });
508
509
  }
509
- res.send({ idFile });
510
+ res.send({ idFile, logrosDesbloqueados });
510
511
  for (const file of files) {
511
512
  yield bucket.file(`Chat/${file.id}`).save(file.data);
512
513
  }
@@ -519,22 +520,13 @@ const sendFile = (req, res, { sendNotification, firebaseMessaging, isClient, buc
519
520
  textFile = 'Vídeo';
520
521
  if (req.file.mimetype.includes('image'))
521
522
  textFile = 'Imagen';
522
- const [[entrenadorRow], [clienteRow]] = yield Promise.all([
523
- (0, index_1.query)('SELECT [NAME] FROM [ENTRENADOR] WHERE [ID] = ?', [userid]),
524
- (0, index_1.query)('SELECT [PREFERRED LANGUAGE] FROM [CLIENTE] WHERE [ID] = ?', [idCliente]),
525
- ]);
526
- const entrenador = ((entrenadorRow === null || entrenadorRow === void 0 ? void 0 : entrenadorRow.name) || 'Tu entrenador').split(' ')[0];
527
- const lang = (clienteRow === null || clienteRow === void 0 ? void 0 : clienteRow.preferredLanguage) || common_1.LANGUAGES.ES;
528
- const tipoArchivo = (0, index_2.t)(textFile, lang);
523
+ const [cliente] = yield (0, index_1.query)('SELECT [PREFERRED LANGUAGE] FROM [CLIENTE] WHERE [ID] = ?', [idCliente]);
529
524
  sendNotification({
530
525
  firebaseMessaging,
531
526
  idCliente,
532
- title: (0, index_2.t)('{{ entrenador }} te ha enviado {{ tipoArchivo }} 📎', lang)
533
- .replace('{{ entrenador }}', entrenador)
534
- .replace('{{ tipoArchivo }}', tipoArchivo),
535
- body: (0, index_2.t)('Ábrelo en el chat.', lang),
527
+ title: (0, index_2.t)('💬 Nuevo mensaje de tu entrenador', cliente.preferredLanguage),
528
+ body: (0, index_2.t)(textFile, cliente.preferredLanguage),
536
529
  screen: common_1.NOTIFICATION_SCREEN_TYPES.CHAT,
537
- channelId: common_1.NOTIFICATION_CHANNELS.COACH,
538
530
  });
539
531
  yield updateSenderView({ userid, idCliente, idMessage: idFile });
540
532
  }
@@ -30,13 +30,6 @@ exports.conversationRoute = conversationRoute;
30
30
  const socketConversation = (req, res, { isClient }) => __awaiter(void 0, void 0, void 0, function* () {
31
31
  const { idCliente } = req.body;
32
32
  const ioEmitter = req.app.get('socketIo');
33
- // El backend de la app ya no monta socket.io (la app recibe por push + recarga HTTP, no por
34
- // websocket). Sin emisor no hay a quién emitir: respondemos ok y salimos. Antes esto caía al
35
- // vacío igualmente porque la app no escuchaba el socket; ahora evitamos el 500 por undefined.
36
- if (!ioEmitter) {
37
- res.send({ status: 'ok' });
38
- return;
39
- }
40
33
  const room = (0, getRoom_1.getRoom)(isClient, idCliente);
41
34
  ioEmitter.sockets.in(room).emit('message', req.body);
42
35
  res.send({ status: 'ok' });
@@ -15,13 +15,6 @@ Object.defineProperty(exports, "chatExposed", { enumerable: true, get: function
15
15
  const getCountNotificaciones_1 = require("./utils/getCountNotificaciones");
16
16
  Object.defineProperty(exports, "getCountNotificaciones", { enumerable: true, get: function () { return getCountNotificaciones_1.getCountNotificaciones; } });
17
17
  const chat = ({ server, app, verify_access_token, tokenExposed, backendExtremeUrl, frontendUrl, isClient }) => {
18
- // El backend de la APP (isClient) NO usa websockets: la app recibe los mensajes por avisos
19
- // push (FCM) + recarga HTTP de la conversación, nunca por socket.io (no tiene cliente de
20
- // websocket). Montarlo aquí era código muerto — el emit caía al vacío — y además inflaba la
21
- // latencia p95 del servicio con conexiones de larga duración. El dashboard de coaches
22
- // (isClient=false) sí consume el socket, así que ahí se mantiene intacto.
23
- if (isClient)
24
- return;
25
18
  // @ts-ignore
26
19
  const io = (0, socket_io_1.default)(server, {
27
20
  cors: {
package/lib/cjs/index.js CHANGED
@@ -1,7 +1,30 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
2
25
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createGoogleModel = exports.generateText = exports.generateObject = exports.reprocessRecentMedia = exports.processMediaFile = exports.describeImage = exports.transcribeAudio = exports.updateSenderView = exports.saveResponseTime = exports.getZone = exports.saveDoneStructuraWorkout = exports.saveWorkoutLaps = exports.saveWorkoutAplication = exports.getPlanificacionPrueba7dias = exports.sendWorkoutToWatch = exports.getDefaultWorkoutImage = exports.generateShareMap = exports.reduceSizeImage = exports.getLetter = exports.getNumberByLetter = exports.appendSheet = exports.writeSheet = exports.findCellByValue = exports.readSheet = exports.NOTION_DATABASES_ID = exports.notionEditPage = exports.notionAddPage = exports.notionGetDatabase = exports.notionGetUsers = exports.getCountNotificaciones = exports.chatExposed = exports.chatApi = exports.chat = exports.getExerciseTranslatedDescription = exports.useTranslation = exports.LANGUAGES = exports.translate = exports.CHANNEL_SLACK = exports.notifySlack = exports.fetchIA = exports.err = exports.sendMail = exports.pool = exports.toPgArray = exports.batchQuery = exports.longRunningQuery = exports.queryWithClient = exports.query = exports.sleep = exports.sendNotification = void 0;
4
- exports.calculateCost = exports.MODEL_PRICING = exports.AISTUDIO_PREFIX = exports.BEDROCK_CLAUDE_HAIKU = exports.BEDROCK_CLAUDE_SONNET_4 = exports.BEDROCK_CLAUDE_SONNET_4_5 = exports.BEDROCK_CLAUDE_OPUS_4_1 = exports.BEDROCK_CLAUDE_OPUS_4_5 = exports.BEDROCK_CLAUDE_SONNET = exports.BEDROCK_CLAUDE_OPUS = exports.FALLBACK_MODELS = exports.PRODUCTION_MODELS = exports.AZURE_PRIMARY_MODELS = exports.AZURE_GPT_5_4_NANO = exports.AZURE_GPT_5_4_MINI = exports.AZURE_GPT_5_5 = exports.AZURE_GPT_5_4_NANO_DEPLOYMENT = exports.AZURE_GPT_5_4_MINI_DEPLOYMENT = exports.AZURE_GPT_5_5_DEPLOYMENT = exports.AZURE_PREFIX = exports.GOOGLE_MODELS = exports.AI_MODELS = exports.MODEL_TIER = exports.runWithModels = exports.runWithCostTracking = exports.createModelFromString = exports.createAzureModelFromString = exports.createBedrockModelFromString = exports.createGoogleModelFromString = void 0;
26
+ exports.describeImage = exports.transcribeAudio = exports.updateSenderView = exports.saveResponseTime = exports.getZone = exports.saveDoneStructuraWorkout = exports.computeStreakStats = exports.getUnlockedAchievements = exports.evaluateAchievementsBounded = exports.evaluateAchievements = exports.saveWorkoutLaps = exports.saveWorkoutAplication = exports.getPlanificacionPrueba7dias = exports.sendWorkoutToWatch = exports.getDefaultWorkoutImage = exports.generateShareMap = exports.reduceSizeImage = exports.getLetter = exports.getNumberByLetter = exports.appendSheet = exports.writeSheet = exports.findCellByValue = exports.readSheet = exports.NOTION_DATABASES_ID = exports.notionEditPage = exports.notionAddPage = exports.notionGetDatabase = exports.notionGetUsers = exports.getCountNotificaciones = exports.chatExposed = exports.chatApi = exports.chat = exports.getExerciseTranslatedDescription = exports.useTranslation = exports.LANGUAGES = exports.translate = exports.CHANNEL_SLACK = exports.notifySlack = exports.fetchIA = exports.err = exports.sendMail = exports.pool = exports.toPgArray = exports.batchQuery = exports.longRunningQuery = exports.queryWithClient = exports.query = exports.sleep = exports.sendNotification = exports.achievementsCatalog = void 0;
27
+ exports.calculateCost = exports.MODEL_PRICING = exports.AISTUDIO_PREFIX = exports.BEDROCK_CLAUDE_HAIKU = exports.BEDROCK_CLAUDE_SONNET_4 = exports.BEDROCK_CLAUDE_SONNET_4_5 = exports.BEDROCK_CLAUDE_OPUS_4_1 = exports.BEDROCK_CLAUDE_OPUS_4_5 = exports.BEDROCK_CLAUDE_SONNET = exports.BEDROCK_CLAUDE_OPUS = exports.FALLBACK_MODELS = exports.PRODUCTION_MODELS = exports.AZURE_PRIMARY_MODELS = exports.AZURE_GPT_5_4_NANO = exports.AZURE_GPT_5_4_MINI = exports.AZURE_GPT_5_5 = exports.AZURE_GPT_5_4_NANO_DEPLOYMENT = exports.AZURE_GPT_5_4_MINI_DEPLOYMENT = exports.AZURE_GPT_5_5_DEPLOYMENT = exports.AZURE_PREFIX = exports.GOOGLE_MODELS = exports.AI_MODELS = exports.MODEL_TIER = exports.runWithModels = exports.runWithCostTracking = exports.createModelFromString = exports.createAzureModelFromString = exports.createBedrockModelFromString = exports.createGoogleModelFromString = exports.createGoogleModel = exports.generateText = exports.generateObject = exports.reprocessRecentMedia = exports.processMediaFile = void 0;
5
28
  const sendNotification_1 = require("./sendNotification");
6
29
  Object.defineProperty(exports, "sendNotification", { enumerable: true, get: function () { return sendNotification_1.sendNotification; } });
7
30
  const sleep_1 = require("./sleep");
@@ -59,6 +82,15 @@ const saveWorkoutAplication_1 = require("./workout/saveWorkoutAplication");
59
82
  Object.defineProperty(exports, "saveWorkoutAplication", { enumerable: true, get: function () { return saveWorkoutAplication_1.saveWorkoutAplication; } });
60
83
  const saveWorkoutLaps_1 = require("./workout/saveWorkoutLaps");
61
84
  Object.defineProperty(exports, "saveWorkoutLaps", { enumerable: true, get: function () { return saveWorkoutLaps_1.saveWorkoutLaps; } });
85
+ const achievements_1 = require("./achievements");
86
+ Object.defineProperty(exports, "evaluateAchievements", { enumerable: true, get: function () { return achievements_1.evaluateAchievements; } });
87
+ Object.defineProperty(exports, "evaluateAchievementsBounded", { enumerable: true, get: function () { return achievements_1.evaluateAchievementsBounded; } });
88
+ Object.defineProperty(exports, "getUnlockedAchievements", { enumerable: true, get: function () { return achievements_1.getUnlockedAchievements; } });
89
+ Object.defineProperty(exports, "computeStreakStats", { enumerable: true, get: function () { return achievements_1.computeStreakStats; } });
90
+ // Catálogo de REGLAS de logros (umbrales + IDs). Fuente única de verdad para
91
+ // los consumidores del paquete (backfill, herramientas, tests). El catálogo de
92
+ // PRESENTACIÓN (labels/iconos) vive en el frontend.
93
+ exports.achievementsCatalog = __importStar(require("./achievements/catalog"));
62
94
  const estructuraWorkout_1 = require("./workout/estructuraWorkout");
63
95
  Object.defineProperty(exports, "saveDoneStructuraWorkout", { enumerable: true, get: function () { return estructuraWorkout_1.saveDoneStructuraWorkout; } });
64
96
  const paceZone_1 = require("./workout/paceZone");
@@ -7,9 +7,5 @@ const en = {
7
7
  Vídeo: 'Video',
8
8
  Imagen: 'Image',
9
9
  '💬 Nuevo mensaje de tu entrenador': '💬 New message from your trainer',
10
- '{{ entrenador }} 💬': '{{ entrenador }} 💬',
11
- '{{ entrenador }} te ha enviado {{ tipoArchivo }} 📎': '{{ entrenador }} sent you {{ tipoArchivo }} 📎',
12
- 'Ábrelo en el chat.': 'Open it in the chat.',
13
- '{{ entrenador }} reaccionó {{ emoji }} a tu mensaje': '{{ entrenador }} reacted {{ emoji }} to your message',
14
10
  };
15
11
  exports.en = en;
@@ -7,9 +7,5 @@ const es = {
7
7
  Vídeo: 'Vídeo',
8
8
  Imagen: 'Imagen',
9
9
  '💬 Nuevo mensaje de tu entrenador': '💬 Nuevo mensaje de tu entrenador',
10
- '{{ entrenador }} 💬': '{{ entrenador }} 💬',
11
- '{{ entrenador }} te ha enviado {{ tipoArchivo }} 📎': '{{ entrenador }} te ha enviado {{ tipoArchivo }} 📎',
12
- 'Ábrelo en el chat.': 'Ábrelo en el chat.',
13
- '{{ entrenador }} reaccionó {{ emoji }} a tu mensaje': '{{ entrenador }} reaccionó {{ emoji }} a tu mensaje',
14
10
  };
15
11
  exports.es = es;
@@ -7,9 +7,5 @@ const fr = {
7
7
  Vídeo: 'Vidéo',
8
8
  Imagen: 'Image',
9
9
  '💬 Nuevo mensaje de tu entrenador': '💬 Nouveau message de votre entraîneur',
10
- '{{ entrenador }} 💬': '{{ entrenador }} 💬',
11
- '{{ entrenador }} te ha enviado {{ tipoArchivo }} 📎': '{{ entrenador }} t’a envoyé {{ tipoArchivo }} 📎',
12
- 'Ábrelo en el chat.': 'Ouvre-le dans le chat.',
13
- '{{ entrenador }} reaccionó {{ emoji }} a tu mensaje': '{{ entrenador }} a réagi {{ emoji }} à ton message',
14
10
  };
15
11
  exports.fr = fr;
@@ -7,9 +7,5 @@ const it = {
7
7
  Vídeo: 'Video',
8
8
  Imagen: 'Immagine',
9
9
  '💬 Nuevo mensaje de tu entrenador': '💬 Nuovo messaggio da tuo allenatore',
10
- '{{ entrenador }} 💬': '{{ entrenador }} 💬',
11
- '{{ entrenador }} te ha enviado {{ tipoArchivo }} 📎': '{{ entrenador }} ti ha inviato {{ tipoArchivo }} 📎',
12
- 'Ábrelo en el chat.': 'Aprilo nella chat.',
13
- '{{ entrenador }} reaccionó {{ emoji }} a tu mensaje': '{{ entrenador }} ha reagito {{ emoji }} al tuo messaggio',
14
10
  };
15
11
  exports.it = it;
@@ -12,7 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.sendNotification = void 0;
13
13
  const index_1 = require("../db/index");
14
14
  const common_1 = require("@runnerpro/common");
15
- const sendNotification = ({ firebaseMessaging, idCliente, title, body, screen = common_1.NOTIFICATION_SCREEN_TYPES.HOME, screenParams = {}, imageUrl, channelId, largeIcon, bigText, actions, progress, sound, coachNombre, category, }) => __awaiter(void 0, void 0, void 0, function* () {
15
+ const sendNotification = ({ firebaseMessaging, idCliente, title, body, screen = common_1.NOTIFICATION_SCREEN_TYPES.HOME, screenParams = {}, }) => __awaiter(void 0, void 0, void 0, function* () {
16
16
  const devices = yield (0, index_1.query)('SELECT [SUBSCRIPTION], [TYPE] FROM [PUSH MANAGER] WHERE [ID CLIENTE] = ?', [idCliente]);
17
17
  const [{ id: idNotification }] = yield (0, index_1.query)('INSERT INTO "CLIENTE NOTIFICACION" ("ID CLIENTE", "TIMESTAMP", "TITLE", "BODY", "PARAMS") VALUES (?, NOW(), ?, ?, ?) RETURNING "ID"', [
18
18
  idCliente,
@@ -31,89 +31,23 @@ const sendNotification = ({ firebaseMessaging, idCliente, title, body, screen =
31
31
  idNotification: (idNotification || '0').toString(),
32
32
  screen,
33
33
  screenParams: screenParamsString,
34
- imageUrl,
35
- channelId,
36
- largeIcon,
37
- bigText,
38
- actions,
39
- progress,
40
- sound,
41
- coachNombre,
42
- category,
43
34
  }, device.subscription);
44
35
  }
45
36
  });
46
37
  exports.sendNotification = sendNotification;
47
38
  function notificationWEB(firebaseMessaging, msg, token) {
48
- var _a, _b;
49
39
  return __awaiter(this, void 0, void 0, function* () {
50
40
  if (!msg.title)
51
41
  msg.title = '';
52
42
  try {
53
- // FCM `data` solo acepta strings; serializamos los campos ricos para que la
54
- // app los reconstruya con Notifee al recibir el push.
55
- const data = {
56
- title: msg.title,
57
- body: msg.body,
58
- screen: msg.screen,
59
- screenParams: msg.screenParams,
60
- idNotification: String((_a = msg.idNotification) !== null && _a !== void 0 ? _a : ''),
61
- };
62
- if (msg.imageUrl)
63
- data.imageUrl = msg.imageUrl;
64
- if (msg.channelId)
65
- data.channelId = msg.channelId;
66
- if (msg.largeIcon)
67
- data.largeIcon = msg.largeIcon;
68
- if (msg.bigText)
69
- data.bigText = msg.bigText;
70
- if (msg.actions && msg.actions.length > 0)
71
- data.actions = JSON.stringify(msg.actions);
72
- if (msg.progress)
73
- data.progress = JSON.stringify(msg.progress);
74
- if (msg.sound)
75
- data.sound = msg.sound;
76
- if (msg.coachNombre)
77
- data.coachNombre = msg.coachNombre;
78
- const message = {
43
+ yield firebaseMessaging.send({
79
44
  token,
80
45
  notification: {
81
46
  title: msg.title,
82
47
  body: msg.body,
83
48
  },
84
- data,
85
- };
86
- // Bloques nativos solo cuando aportan, para no inflar el payload ni romper
87
- // pushes existentes que no usan estos campos.
88
- const channelIdResolved = (_b = msg.channelId) !== null && _b !== void 0 ? _b : common_1.NOTIFICATION_CHANNELS.DEFAULT;
89
- const androidNotification = {
90
- channelId: channelIdResolved,
91
- };
92
- if (msg.imageUrl)
93
- androidNotification.imageUrl = msg.imageUrl;
94
- if (msg.sound)
95
- androidNotification.sound = msg.sound;
96
- message.android = {
97
- priority: 'high',
98
- notification: androidNotification,
99
- };
100
- // iOS (APNs). mutable-content activa el Notification Service Extension, que
101
- // necesitamos cuando hay imagen rica O avatar del coach (para pintarlo como
102
- // communication notification con la cara del entrenador). category habilita
103
- // los botones de acción (UNNotificationCategory registrada en la app).
104
- const aps = {};
105
- if (msg.imageUrl || msg.largeIcon)
106
- aps['mutable-content'] = 1;
107
- if (msg.sound)
108
- aps.sound = msg.sound;
109
- if (msg.category)
110
- aps.category = msg.category;
111
- if (Object.keys(aps).length > 0) {
112
- message.apns = { payload: { aps } };
113
- if (msg.imageUrl)
114
- message.apns.fcm_options = { image: msg.imageUrl };
115
- }
116
- yield firebaseMessaging.send(message);
49
+ data: msg,
50
+ });
117
51
  }
118
52
  catch (error) {
119
53
  return error;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Catálogo de REGLAS de logros (umbrales). Vive en el paquete compartido para
3
+ * que lo usen tanto los hooks de evento (saveWorkoutAplication.postHooks y los
4
+ * eventos de Cliente-backend) como el backfill.
5
+ *
6
+ * Los `id` deben coincidir EXACTAMENTE con el catálogo de PRESENTACIÓN del
7
+ * frontend (Cliente-app/src/screens/PlanMap/utils/achievementCatalog.js).
8
+ * Frontend = labels/iconos/colores; aquí = umbrales/reglas.
9
+ */
10
+ export declare const CONSISTENCY: {
11
+ id: string;
12
+ weeks: number;
13
+ }[];
14
+ export declare const DISTANCE: {
15
+ id: string;
16
+ meters: number;
17
+ }[];
18
+ export declare const VOLUME: {
19
+ id: string;
20
+ km: number;
21
+ }[];
22
+ export declare const RUNNING_TYPES: string[];
23
+ export declare const PR_DISTANCES: number[];
24
+ export declare const PACE: {
25
+ id: string;
26
+ maxSecPerKm: number;
27
+ }[];
28
+ export declare const TIME_PR: {
29
+ id: string;
30
+ distance: number;
31
+ maxSeconds: number;
32
+ }[];
33
+ export declare const PR_NO_STREAM_MARGIN = 1.05;
34
+ export declare const PR_MANUAL_FLOOR: {
35
+ [official: number]: number;
36
+ };
37
+ export declare const PR_DISTANCE_BADGE: {
38
+ [meters: number]: string;
39
+ };
40
+ export declare const STRENGTH_COUNT: {
41
+ id: string;
42
+ count: number;
43
+ }[];
44
+ export declare const RACE_COUNT: {
45
+ id: string;
46
+ count: number;
47
+ }[];
48
+ export declare const CYCLING_DISTANCE: {
49
+ id: string;
50
+ meters: number;
51
+ }[];
52
+ export declare const CYCLING_VOLUME: {
53
+ id: string;
54
+ km: number;
55
+ }[];
56
+ export declare const SWIM_DISTANCE: {
57
+ id: string;
58
+ meters: number;
59
+ }[];
60
+ export declare const TRAIL_DISTANCE: {
61
+ id: string;
62
+ meters: number;
63
+ }[];
64
+ export declare const TRAIL_ELEVATION: {
65
+ id: string;
66
+ meters: number;
67
+ }[];
68
+ export declare const TRI_SWIM_DISTANCE: {
69
+ id: string;
70
+ meters: number;
71
+ }[];
72
+ export declare const CHAT_MSG: {
73
+ id: string;
74
+ count: number;
75
+ }[];
76
+ export declare const NUTRITION_MEALS: {
77
+ id: string;
78
+ count: number;
79
+ }[];
80
+ export declare const NUTRITION_DAYS_THRESHOLD = 7;
81
+ export declare const WEARABLE_APPS: {
82
+ id: string;
83
+ tipo: number;
84
+ }[];
85
+ export declare const PLAN_SESSION_TYPES: string[];
86
+ export declare const PLAN_ADHERENCE_82 = 0.82;
87
+ export declare const PLAN_BLOCK_ADHERENCE = 0.8;
88
+ export declare const PLAN_COMPLETE_ADHERENCE = 0.8;
89
+ export declare const PLAN_RACE_WEEK_DAYS = 6;
90
+ export declare const PLAN_RUN_TYPES: string[];
91
+ export declare const PLAN_LONG_RUN_MIN_DISTANCE = 14000;
92
+ export declare const PLAN_LONG_RUN_MIN_DURATION: number;
93
+ export declare const PLAN_RACE_PACE_TOLERANCE = 0.04;
94
+ export declare const PLAN_RACE_PACE_MIN_DISTANCE = 5000;
95
+ export declare const STRENGTH_WINDOWS: {
96
+ id: string;
97
+ sessions: number;
98
+ days: number;
99
+ }[];
100
+ export declare const STRENGTH_HABIT_WEEKS = 4;
101
+ export declare const RECOVERY_RUN_DAYS = 3;
102
+ export declare const RECOVERY_RUN_ZONE = 2;
103
+ export declare const BRICK_TYPES: string[];
104
+ //# sourceMappingURL=catalog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../../../src/achievements/catalog.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ export { computeStreakStats } from './streak';
2
+ /** Tiempos candidatos por distancia para un entreno (stream → best-effort; sin stream → híbrido). */
3
+ export declare const computeWorkoutEfforts: (workout: any, series: any) => any;
4
+ /**
5
+ * Evalúa los logros del usuario PARA UN EVENTO y persiste los desbloqueados
6
+ * (idempotente, no revoca). Se llama justo donde ocurre la condición:
7
+ * - 'workout' → al completar un entreno. opts.workoutId procesa SOLO ese
8
+ * entreno para los PRs (incremental); sin él, recalcula todos.
9
+ * - 'chat' | 'survey' | 'availability' | 'nutrition' | 'food_photo' | 'wearable'.
10
+ * - 'goal' → al añadir un objetivo (re-evalúa Plan, incl. pc_nuevo_capitulo).
11
+ * - 'all' → todo (backfill; recalcula los PRs desde cero).
12
+ * Nunca lanza por categorías aisladas (no debe romper el flujo que la invoca).
13
+ *
14
+ * Devuelve los logros RECIÉN desbloqueados en esta llamada: [{id, fechaDesbloqueo,
15
+ * params?}] (params solo en los dist_* con PR). Sirve para que el FE parchee su caché
16
+ * y suba el chip sin refetch. RETRO-COMPATIBLE: los callers que ignoran el retorno
17
+ * (backfill, postHooks de saveWorkoutAplication) siguen funcionando igual.
18
+ */
19
+ export declare const evaluateAchievements: (userId: any, event?: string, opts?: any) => Promise<Array<{
20
+ id: string;
21
+ fechaDesbloqueo: any;
22
+ params?: any;
23
+ }>>;
24
+ /**
25
+ * Variante acotada con timeout para el CAMINO CALIENTE (hooks de evento que
26
+ * responden HTTP). Devuelve los recién desbloqueados si el motor termina dentro
27
+ * de `timeoutMs`; si vence (o falla), devuelve [] sin romper el flujo que la invoca.
28
+ *
29
+ * La promesa del motor lleva su propio `.catch` ANTES del race, así que si vence el
30
+ * timeout sigue corriendo en background SIN unhandled rejection y persiste igual
31
+ * (el INSERT ocurre dentro de evaluateAchievements) → el FE lo recupera en el
32
+ * siguiente refetch. Nunca lanza.
33
+ */
34
+ export declare const evaluateAchievementsBounded: (userId: any, event?: string, opts?: any, timeoutMs?: number) => Promise<Array<{
35
+ id: string;
36
+ fechaDesbloqueo: any;
37
+ params?: any;
38
+ }>>;
39
+ /** Set completo de logros desbloqueados del usuario (para lecturas / verificación). */
40
+ export declare const getUnlockedAchievements: (userId: any) => Promise<string[]>;
41
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/achievements/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAiK9C,qGAAqG;AAErG,eAAO,MAAM,qBAAqB,YAAa,GAAG,UAAU,GAAG,KAAG,GAuBjE,CAAC;AAkjBF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,oBAAoB,WACvB,GAAG,UACJ,MAAM,SACP,GAAG,KACR,QAAQ,MAAM;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,GAAG,CAAC;IAAC,MAAM,CAAC,EAAE,GAAG,CAAA;CAAE,CAAC,CAyGnE,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,WAC9B,GAAG,UACJ,MAAM,SACP,GAAG,yBAER,QAAQ,MAAM;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,GAAG,CAAC;IAAC,MAAM,CAAC,EAAE,GAAG,CAAA;CAAE,CAAC,CAWnE,CAAC;AAEF,uFAAuF;AACvF,eAAO,MAAM,uBAAuB,WAAkB,GAAG,KAAG,QAAQ,MAAM,EAAE,CAO3E,CAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Regla de racha: una semana cuenta como completada si el usuario hace al menos
3
+ * el 80% de los entrenamientos planificados de esa semana (regla 80%). Tipos
4
+ * entrenables, semanas ISO. La semana actual incompleta no rompe la racha.
5
+ */
6
+ export declare const STREAK_COMPLETION_THRESHOLD = 0.8;
7
+ export declare const requiredFromPlanned: (plannedWorkouts: any) => number;
8
+ /**
9
+ * Racha actual (current) y récord histórico (longest), en semanas.
10
+ *
11
+ * @param userId ID del cliente.
12
+ * @param sinceDate Si se pasa, solo cuentan workouts con "DATE" >= sinceDate
13
+ * (ancla "empezar de cero" para logros). Sin él, comportamiento sobre todo el
14
+ * historial.
15
+ */
16
+ export declare const computeStreakStats: (userId: any, sinceDate?: any) => Promise<{
17
+ current: number;
18
+ longest: number;
19
+ }>;
20
+ //# sourceMappingURL=streak.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streak.d.ts","sourceRoot":"","sources":["../../../../src/achievements/streak.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAE/C,eAAO,MAAM,mBAAmB,oBAAqB,GAAG,KAAG,MAI1D,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,WACrB,GAAG,cACA,GAAG;aACM,MAAM;aAAW,MAAM;EA4F5C,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"conversation.d.ts","sourceRoot":"","sources":["../../../../../src/chat/api/conversation.ts"],"names":[],"mappings":"AAmBA,QAAA,MAAM,iBAAiB,0BAA2B,GAAG,SA0BpD,CAAC;AAoGF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,QAAA,MAAM,uBAAuB,qCAAkC,GAAG,iBAwCjE,CAAC;AAmGF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,QAAA,MAAM,WAAW,0EAAuE,GAAG,kBAsE1F,CAAC;AAEF,QAAA,MAAM,gBAAgB;;;;mBAqBrB,CAAC;AAqUF,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,WAAW,EAAE,uBAAuB,EAAE,CAAC"}
1
+ {"version":3,"file":"conversation.d.ts","sourceRoot":"","sources":["../../../../../src/chat/api/conversation.ts"],"names":[],"mappings":"AAmBA,QAAA,MAAM,iBAAiB,0BAA2B,GAAG,SA0BpD,CAAC;AAoGF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,QAAA,MAAM,uBAAuB,qCAAkC,GAAG,iBAwCjE,CAAC;AAmGF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,QAAA,MAAM,WAAW,0EAAuE,GAAG,kBA0E1F,CAAC;AAEF,QAAA,MAAM,gBAAgB;;;;mBAqBrB,CAAC;AAyTF,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,WAAW,EAAE,uBAAuB,EAAE,CAAC"}