@runnerpro/backend 1.21.14 → 1.21.16

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.
@@ -31,7 +31,7 @@ const sendMail = ({ subject, title, body, to, link, attachments, bcc }) => __awa
31
31
  return;
32
32
  const bodyHTML = getBodyHTML(title, body, link);
33
33
  const sendPromise = transporter.sendMail({
34
- from: process.env.GMAIL_EMAIL_USER,
34
+ from: `"RunnerPro" <${process.env.GMAIL_EMAIL_USER}>`,
35
35
  to: toMapped,
36
36
  bcc: process.env.NODE_ENV === 'PROD' && bcc ? bcc.map((correo) => correo).join(',') : '',
37
37
  subject,
@@ -1 +1 @@
1
- {"version":3,"file":"saveWorkoutAplication.d.ts","sourceRoot":"","sources":["../../../../src/workout/saveWorkoutAplication.ts"],"names":[],"mappings":"AAwRA;;;;;;;;;;GAUG;AACH,QAAA,MAAM,qBAAqB,SACnB,GAAG,UAED,GAAG,KACV,QAAQ,MAAM,GAAG,IAAI,CAsEvB,CAAC;AAuEF,OAAO,EAAE,qBAAqB,EAAE,CAAC"}
1
+ {"version":3,"file":"saveWorkoutAplication.d.ts","sourceRoot":"","sources":["../../../../src/workout/saveWorkoutAplication.ts"],"names":[],"mappings":"AA4SA;;;;;;;;;;GAUG;AACH,QAAA,MAAM,qBAAqB,SACnB,GAAG,UAED,GAAG,KACV,QAAQ,MAAM,GAAG,IAAI,CAsEvB,CAAC;AAuEF,OAAO,EAAE,qBAAqB,EAAE,CAAC"}
@@ -149,19 +149,25 @@ const similar = (a, b, tol) => {
149
149
  return Math.abs(na - nb) / Math.max(na, nb) <= tol;
150
150
  };
151
151
  // Ventana de solape de hora de inicio para el dedup cross-proveedor. Las
152
- // grabaciones de la MISMA actividad (típicamente un mismo reloj que sincroniza a
153
- // Strava/Apple/Garmin) empiezan en el mismo instante; 5 min absorben cualquier
154
- // desfase de redondeo/zona horaria sin abrir falsos positivos (no se inician dos
155
- // actividades del mismo grupo de tipo en 5 min).
156
- const VENTANA_INICIO_MS = 5 * 60 * 1000;
157
- const inicioSolapa = (a, b) => {
152
+ // grabaciones de la MISMA actividad (un mismo reloj que sincroniza a
153
+ // Strava/Apple/Garmin) empiezan casi en el mismo instante; 30 min absorben el
154
+ // desfase de relojes que arrancan la grabación tarde sin abrir falsos positivos
155
+ // (no se inician dos actividades del mismo grupo de tipo en 30 min).
156
+ const VENTANA_INICIO_MS = 30 * 60 * 1000;
157
+ // Ventana ampliada cuando una de las dos caras es un completado MANUAL: su
158
+ // "START AT" es una ESTIMACIÓN (hora de marcado − duración) que puede desviarse
159
+ // bastante de la hora real del reloj (el cliente marca hecho un rato después de
160
+ // terminar, o redondea la duración). 2h la absorben; si aun así no solapan, son
161
+ // dos entrenos distintos y se respetan como dobles.
162
+ const VENTANA_INICIO_MANUAL_MS = 2 * 60 * 60 * 1000;
163
+ const inicioSolapa = (a, b, ventanaMs = VENTANA_INICIO_MS) => {
158
164
  if (!a || !b)
159
165
  return false;
160
166
  const ta = new Date(a).getTime();
161
167
  const tb = new Date(b).getTime();
162
168
  if (Number.isNaN(ta) || Number.isNaN(tb))
163
169
  return false;
164
- return Math.abs(ta - tb) <= VENTANA_INICIO_MS;
170
+ return Math.abs(ta - tb) <= ventanaMs;
165
171
  };
166
172
  const buscarWorkoutSimilar = (q, opts, dateStr, pt) => __awaiter(void 0, void 0, void 0, function* () {
167
173
  // Pre-filtro por ventana de día ±1: el match real es por hora de inicio (no por
@@ -170,6 +176,10 @@ const buscarWorkoutSimilar = (q, opts, dateStr, pt) => __awaiter(void 0, void 0,
170
176
  // día cubre ese borde sin ensanchar el rango de más.
171
177
  const dayPrev = (0, moment_1.default)(dateStr, 'YYYY-MM-DD').subtract(1, 'day').format('YYYY-MM-DD');
172
178
  const dayNext = (0, moment_1.default)(dateStr, 'YYYY-MM-DD').add(1, 'day').format('YYYY-MM-DD');
179
+ // Candidatos: entrenos hechos ese día (±1) del mismo grupo de tipo con OTRA fuente
180
+ // — INCLUYE los MANUAL, para que un entreno sincronizado que llega pueda reclamar
181
+ // el que el cliente ya marcó a mano y fusionarse con él (los datos del reloj mandan,
182
+ // el título/estructura del plan se conservan).
173
183
  const rows = yield q(`SELECT "ID", "DURATION", "DISTANCE", "TIPO APLICATION", "START AT"
174
184
  FROM "WORKOUT"
175
185
  WHERE "ID CLIENTE" = ? AND "DATE" BETWEEN ? AND ? AND ${pt.sql}
@@ -177,12 +187,14 @@ const buscarWorkoutSimilar = (q, opts, dateStr, pt) => __awaiter(void 0, void 0,
177
187
  const cand = rows.filter((r) => {
178
188
  // Señal robusta: solape de hora de inicio. Inmune a las unidades de distancia
179
189
  // (Apple en millas) y a elapsed-vs-moving en la duración (webhook Strava).
190
+ // Ventana ampliada a 2h si el candidato es MANUAL (su "START AT" es estimado).
180
191
  // Sólo aplica si AMBAS filas tienen "START AT".
192
+ const ventana = String(r.tipoAplication) === 'MANUAL' ? VENTANA_INICIO_MANUAL_MS : VENTANA_INICIO_MS;
181
193
  if (opts.startAt && r.startAt)
182
- return inicioSolapa(r.startAt, opts.startAt);
183
- // Fallback legacy: filas sin "START AT" (anteriores a esta feature, sin
184
- // backfill) → se mantiene el heurístico distancia+duración para no regresar.
185
- return similar(r.duration, opts.duration, 0.1) && similar(r.distance, opts.distance, 0.1);
194
+ return inicioSolapa(r.startAt, opts.startAt, ventana);
195
+ // Fallback legacy: filas sin "START AT" (manuales antiguos o pre-feature)
196
+ // heurístico distancia+duración para no regresar.
197
+ return similar(r.duration, opts.duration, 0.25) && similar(r.distance, opts.distance, 0.25);
186
198
  });
187
199
  cand.sort((a, b) => prioridadFuente(b.tipoAplication) - prioridadFuente(a.tipoAplication));
188
200
  return cand[0] || null;
@@ -224,7 +236,7 @@ const registrarFuente = (q, idWorkout, tipo, idApp) => __awaiter(void 0, void 0,
224
236
  DO UPDATE SET "ID APLICATION" = EXCLUDED."ID APLICATION", "INGESTED AT" = NOW()`, [idWorkout, String(tipo), idApp !== null && idApp !== undefined ? String(idApp) : null]);
225
237
  });
226
238
  const aplicarMerge = (q, idWorkout, opts, esFilaNueva) => __awaiter(void 0, void 0, void 0, function* () {
227
- var _a, _b, _c;
239
+ var _a, _b, _c, _d;
228
240
  const [row] = yield q(`SELECT "DISTANCE", "DURATION", "POTENCIA", "DESNIVEL", "CALORIES", "FC MEDIA",
229
241
  "ZONE", "POLYLINE", "TIPO APLICATION", "ID APLICATION", "START AT"
230
242
  FROM "WORKOUT" WHERE "ID" = ? FOR UPDATE`, [idWorkout]);
@@ -241,14 +253,19 @@ const aplicarMerge = (q, idWorkout, opts, esFilaNueva) => __awaiter(void 0, void
241
253
  const zone = (0, paceZone_1.getZone)({ duration, distance, zones: cli });
242
254
  // POLYLINE: la primera fuente que la puso manda; nunca se pisa.
243
255
  const polyline = row && row.polyline ? row.polyline : getPolyline(opts.polyline);
244
- // START AT: la primera fuente que lo puso manda; nunca se pisa (igual que
245
- // POLYLINE). Es la clave del dedup cross-proveedor (solape de hora de inicio).
246
- const startAt = row && row.startAt ? row.startAt : (_c = opts.startAt) !== null && _c !== void 0 ? _c : null;
247
- // TIPO/ID APLICATION: sólo se setean en INSERT o si la fila no tenía fuente;
248
- // nunca se reemplazan (la primera fuente se queda).
256
+ // ¿La fila existente es un completado MANUAL? Su "START AT" es una estimación y su
257
+ // fuente es la de menor prioridad, así que una fuente REAL entrante debe pisarla.
258
+ const existingIsManual = String(row === null || row === void 0 ? void 0 : row.tipoAplication) === 'MANUAL';
259
+ // START AT: la primera fuente que lo puso manda y nunca se pisa (igual que
260
+ // POLYLINE) EXCEPTO si la existente es la estimación de un MANUAL y llega una
261
+ // fuente real: entonces la hora real del reloj pisa la estimada.
262
+ const startAt = row && row.startAt && !existingIsManual ? row.startAt : (_d = (_c = opts.startAt) !== null && _c !== void 0 ? _c : row === null || row === void 0 ? void 0 : row.startAt) !== null && _d !== void 0 ? _d : null;
263
+ // TIPO/ID APLICATION: se setean en INSERT, si la fila no tenía fuente, o si la fila
264
+ // era MANUAL y llega una fuente de más prioridad (reloj) que la reemplaza como
265
+ // principal. Entre dos fuentes reales, la primera se queda (no se reemplaza).
249
266
  let tipoFinal;
250
267
  let idFinal;
251
- if (esFilaNueva || !(row === null || row === void 0 ? void 0 : row.tipoAplication)) {
268
+ if (esFilaNueva || !(row === null || row === void 0 ? void 0 : row.tipoAplication) || (existingIsManual && prioNueva > prioActual)) {
252
269
  tipoFinal = opts.aplicationType;
253
270
  idFinal = opts.aplicationId;
254
271
  }
@@ -294,7 +311,7 @@ _data) => __awaiter(void 0, void 0, void 0, function* () {
294
311
  // extra para clientes no-ES con doble proveedor, a cambio de no bloquear el lock.
295
312
  const translated = yield (0, titleDescriptionTranslated_1.getTitleDescriptionTranslated)(opts.idCliente, opts.title, opts.description);
296
313
  const idWorkout = yield withTx((q) => __awaiter(void 0, void 0, void 0, function* () {
297
- var _d;
314
+ var _e;
298
315
  // Lock anti-carrera: serializa ingestas del mismo cliente/día/bucket-tipo
299
316
  // (Strava-webhook y Garmin-push pueden entrar casi a la vez).
300
317
  yield q('SELECT pg_advisory_xact_lock(?)', [hashInt(`${opts.idCliente}|${dateStr}|${bucketTipo(opts.type)}`)]);
@@ -327,7 +344,7 @@ _data) => __awaiter(void 0, void 0, void 0, function* () {
327
344
  const { title: titleNoTranslate, description: descriptionNoTranslate, titlePreferredLanguage, descriptionPreferredLanguage } = translated;
328
345
  const inserted = yield q(`INSERT INTO "WORKOUT"
329
346
  ("ID CLIENTE", "DATE", "START AT", "TITLE", "DESCRIPTION", "TITLE PREFERRED LANGUAGE", "DESCRIPTION PREFERRED LANGUAGE", "TYPE", "DONE", "SHOW CLIENT")
330
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, TRUE, TRUE) RETURNING "ID"`, [opts.idCliente, dateStr, (_d = opts.startAt) !== null && _d !== void 0 ? _d : null, titleNoTranslate, descriptionNoTranslate, titlePreferredLanguage, descriptionPreferredLanguage, opts.type]);
347
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, TRUE, TRUE) RETURNING "ID"`, [opts.idCliente, dateStr, (_e = opts.startAt) !== null && _e !== void 0 ? _e : null, titleNoTranslate, descriptionNoTranslate, titlePreferredLanguage, descriptionPreferredLanguage, opts.type]);
331
348
  if (!inserted || !inserted[0])
332
349
  return null;
333
350
  const id = inserted[0].id;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runnerpro/backend",
3
- "version": "1.21.14",
3
+ "version": "1.21.16",
4
4
  "description": "A collection of common backend functions",
5
5
  "exports": {
6
6
  ".": "./lib/cjs/index.js"
@@ -53,7 +53,7 @@
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@napi-rs/canvas": "^0.1.53",
56
- "@runnerpro/common": "^1.7.0",
56
+ "@runnerpro/common": "^1.7.3",
57
57
  "axios": "^1.6.7",
58
58
  "image-size": "^1.0.2",
59
59
  "jimp": "^0.22.10",
@@ -81,4 +81,4 @@
81
81
  "oauth-signature": "1.5.0",
82
82
  "socket.io": "^4.7.2"
83
83
  }
84
- }
84
+ }