@runnerpro/backend 1.21.4 → 1.21.6

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.
@@ -73,7 +73,7 @@ const getConversation = (req, res, { isClient }) => __awaiter(void 0, void 0, vo
73
73
  "ENTRENADOR"."NAME" AS "ENTRENADOR NAME", "ENTRENADOR"."PHOTO" AS "ENTRENADOR PHOTO",
74
74
  (SELECT "NAME" FROM "ENTRENADOR" WHERE "ID" = "CHAT MESSAGE"."ID SENDER") AS "ENTRENADOR REAL NAME"
75
75
  FROM "CHAT MESSAGE"
76
- LEFT JOIN "WORKOUT" ON "WORKOUT"."ID" = "CHAT MESSAGE"."ID WORKOUT"
76
+ LEFT JOIN "WORKOUT" ON "WORKOUT"."ID" = "CHAT MESSAGE"."ID WORKOUT"::integer
77
77
  LEFT JOIN "CLIENTE" ON "CLIENTE"."ID" = "CHAT MESSAGE"."ID CLIENTE"
78
78
  LEFT JOIN "ENTRENADOR" ON "ENTRENADOR"."ID" = COALESCE("CHAT MESSAGE"."ID SENDER VIEW", "CLIENTE"."ID ENTRENADOR PRINCIPAL")
79
79
  AND "CHAT MESSAGE"."ID CLIENTE" != "CHAT MESSAGE"."ID SENDER"
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @param opts {{
3
3
  * date, type, title, description, distance, duration, power, desnivel, polyline,
4
+ * startAt, // ISO/Date de inicio (UTC) — clave del dedup cross-proveedor
4
5
  * idCliente, aplicationType, aplicationId,
5
6
  * queryConditionExist?, // LEGACY string SQL — deprecated, fallback
6
7
  * tiposEquivalentes?, // string[] de TYPEs equivalentes
@@ -1 +1 @@
1
- {"version":3,"file":"saveWorkoutAplication.d.ts","sourceRoot":"","sources":["../../../../src/workout/saveWorkoutAplication.ts"],"names":[],"mappings":"AAwPA;;;;;;;;;GASG;AACH,QAAA,MAAM,qBAAqB,SACnB,GAAG,UAED,GAAG,KACV,QAAQ,MAAM,GAAG,IAAI,CAsEvB,CAAC;AA+DF,OAAO,EAAE,qBAAqB,EAAE,CAAC"}
1
+ {"version":3,"file":"saveWorkoutAplication.d.ts","sourceRoot":"","sources":["../../../../src/workout/saveWorkoutAplication.ts"],"names":[],"mappings":"AAuRA;;;;;;;;;;GAUG;AACH,QAAA,MAAM,qBAAqB,SACnB,GAAG,UAED,GAAG,KACV,QAAQ,MAAM,GAAG,IAAI,CAsEvB,CAAC;AA+DF,OAAO,EAAE,qBAAqB,EAAE,CAAC"}
@@ -147,12 +147,42 @@ const similar = (a, b, tol) => {
147
147
  return na === nb;
148
148
  return Math.abs(na - nb) / Math.max(na, nb) <= tol;
149
149
  };
150
+ // Ventana de solape de hora de inicio para el dedup cross-proveedor. Las
151
+ // grabaciones de la MISMA actividad (típicamente un mismo reloj que sincroniza a
152
+ // Strava/Apple/Garmin) empiezan en el mismo instante; 5 min absorben cualquier
153
+ // desfase de redondeo/zona horaria sin abrir falsos positivos (no se inician dos
154
+ // actividades del mismo grupo de tipo en 5 min).
155
+ const VENTANA_INICIO_MS = 5 * 60 * 1000;
156
+ const inicioSolapa = (a, b) => {
157
+ if (!a || !b)
158
+ return false;
159
+ const ta = new Date(a).getTime();
160
+ const tb = new Date(b).getTime();
161
+ if (Number.isNaN(ta) || Number.isNaN(tb))
162
+ return false;
163
+ return Math.abs(ta - tb) <= VENTANA_INICIO_MS;
164
+ };
150
165
  const buscarWorkoutSimilar = (q, opts, dateStr, pt) => __awaiter(void 0, void 0, void 0, function* () {
151
- const rows = yield q(`SELECT "ID", "DURATION", "DISTANCE", "TIPO APLICATION"
166
+ // Pre-filtro por ventana de día ±1: el match real es por hora de inicio (no por
167
+ // DATE exacto). Strava deriva DATE de la hora LOCAL y Apple/Garmin de UTC, así
168
+ // que la misma actividad puede caer en días distintos cerca de medianoche; ±1
169
+ // día cubre ese borde sin ensanchar el rango de más.
170
+ const dayPrev = (0, moment_1.default)(dateStr, 'YYYY-MM-DD').subtract(1, 'day').format('YYYY-MM-DD');
171
+ const dayNext = (0, moment_1.default)(dateStr, 'YYYY-MM-DD').add(1, 'day').format('YYYY-MM-DD');
172
+ const rows = yield q(`SELECT "ID", "DURATION", "DISTANCE", "TIPO APLICATION", "START AT"
152
173
  FROM "WORKOUT"
153
- WHERE "ID CLIENTE" = ? AND "DATE" = ? AND ${pt.sql}
154
- AND "DONE" = TRUE AND "TIPO APLICATION" IS NOT NULL AND "TIPO APLICATION" <> ?`, [opts.idCliente, dateStr, ...pt.params, opts.aplicationType]);
155
- const cand = rows.filter((r) => similar(r.duration, opts.duration, 0.1) && similar(r.distance, opts.distance, 0.1));
174
+ WHERE "ID CLIENTE" = ? AND "DATE" BETWEEN ? AND ? AND ${pt.sql}
175
+ AND "DONE" = TRUE AND "TIPO APLICATION" IS NOT NULL AND "TIPO APLICATION" <> ?`, [opts.idCliente, dayPrev, dayNext, ...pt.params, opts.aplicationType]);
176
+ const cand = rows.filter((r) => {
177
+ // Señal robusta: solape de hora de inicio. Inmune a las unidades de distancia
178
+ // (Apple en millas) y a elapsed-vs-moving en la duración (webhook Strava).
179
+ // Sólo aplica si AMBAS filas tienen "START AT".
180
+ if (opts.startAt && r.startAt)
181
+ return inicioSolapa(r.startAt, opts.startAt);
182
+ // Fallback legacy: filas sin "START AT" (anteriores a esta feature, sin
183
+ // backfill) → se mantiene el heurístico distancia+duración para no regresar.
184
+ return similar(r.duration, opts.duration, 0.1) && similar(r.distance, opts.distance, 0.1);
185
+ });
156
186
  cand.sort((a, b) => prioridadFuente(b.tipoAplication) - prioridadFuente(a.tipoAplication));
157
187
  return cand[0] || null;
158
188
  });
@@ -193,9 +223,9 @@ const registrarFuente = (q, idWorkout, tipo, idApp) => __awaiter(void 0, void 0,
193
223
  DO UPDATE SET "ID APLICATION" = EXCLUDED."ID APLICATION", "INGESTED AT" = NOW()`, [idWorkout, String(tipo), idApp !== null && idApp !== undefined ? String(idApp) : null]);
194
224
  });
195
225
  const aplicarMerge = (q, idWorkout, opts, esFilaNueva) => __awaiter(void 0, void 0, void 0, function* () {
196
- var _a, _b;
226
+ var _a, _b, _c;
197
227
  const [row] = yield q(`SELECT "DISTANCE", "DURATION", "POTENCIA", "DESNIVEL", "CALORIES", "FC MEDIA",
198
- "ZONE", "POLYLINE", "TIPO APLICATION", "ID APLICATION"
228
+ "ZONE", "POLYLINE", "TIPO APLICATION", "ID APLICATION", "START AT"
199
229
  FROM "WORKOUT" WHERE "ID" = ? FOR UPDATE`, [idWorkout]);
200
230
  const prioNueva = prioridadFuente(opts.aplicationType);
201
231
  const prioActual = esFilaNueva ? -1 : prioridadFuente(row === null || row === void 0 ? void 0 : row.tipoAplication);
@@ -210,6 +240,9 @@ const aplicarMerge = (q, idWorkout, opts, esFilaNueva) => __awaiter(void 0, void
210
240
  const zone = (0, paceZone_1.getZone)({ duration, distance, zones: cli });
211
241
  // POLYLINE: la primera fuente que la puso manda; nunca se pisa.
212
242
  const polyline = row && row.polyline ? row.polyline : getPolyline(opts.polyline);
243
+ // START AT: la primera fuente que lo puso manda; nunca se pisa (igual que
244
+ // POLYLINE). Es la clave del dedup cross-proveedor (solape de hora de inicio).
245
+ const startAt = row && row.startAt ? row.startAt : (_c = opts.startAt) !== null && _c !== void 0 ? _c : null;
213
246
  // TIPO/ID APLICATION: sólo se setean en INSERT o si la fila no tenía fuente;
214
247
  // nunca se reemplazan (la primera fuente se queda).
215
248
  let tipoFinal;
@@ -225,9 +258,9 @@ const aplicarMerge = (q, idWorkout, opts, esFilaNueva) => __awaiter(void 0, void
225
258
  const feelingsAsked = (0, moment_1.default)(opts.date).format('YYYY-MM-DD') !== (0, moment_1.default)().format('YYYY-MM-DD');
226
259
  yield q(`UPDATE "WORKOUT" SET
227
260
  "DISTANCE" = ?, "DURATION" = ?, "POTENCIA" = ?, "DESNIVEL" = ?, "CALORIES" = ?,
228
- "FC MEDIA" = ?, "ZONE" = ?, "POLYLINE" = ?, "TIPO APLICATION" = ?, "ID APLICATION" = ?,
261
+ "FC MEDIA" = ?, "ZONE" = ?, "POLYLINE" = ?, "START AT" = ?, "TIPO APLICATION" = ?, "ID APLICATION" = ?,
229
262
  "FEELINGS ASKED" = ?, "DONE" = TRUE
230
- WHERE "ID" = ?`, [distance, duration, potencia, desnivel, calories, fcMedia, zone, polyline, tipoFinal, idFinal, feelingsAsked, idWorkout]);
263
+ WHERE "ID" = ?`, [distance, duration, potencia, desnivel, calories, fcMedia, zone, polyline, startAt, tipoFinal, idFinal, feelingsAsked, idWorkout]);
231
264
  yield mergeStreams(q, idWorkout, opts.streams);
232
265
  });
233
266
  // =============================================================================
@@ -236,6 +269,7 @@ const aplicarMerge = (q, idWorkout, opts, esFilaNueva) => __awaiter(void 0, void
236
269
  /**
237
270
  * @param opts {{
238
271
  * date, type, title, description, distance, duration, power, desnivel, polyline,
272
+ * startAt, // ISO/Date de inicio (UTC) — clave del dedup cross-proveedor
239
273
  * idCliente, aplicationType, aplicationId,
240
274
  * queryConditionExist?, // LEGACY string SQL — deprecated, fallback
241
275
  * tiposEquivalentes?, // string[] de TYPEs equivalentes
@@ -259,6 +293,7 @@ _data) => __awaiter(void 0, void 0, void 0, function* () {
259
293
  // extra para clientes no-ES con doble proveedor, a cambio de no bloquear el lock.
260
294
  const translated = yield (0, titleDescriptionTranslated_1.getTitleDescriptionTranslated)(opts.idCliente, opts.title, opts.description);
261
295
  const idWorkout = yield withTx((q) => __awaiter(void 0, void 0, void 0, function* () {
296
+ var _d;
262
297
  // Lock anti-carrera: serializa ingestas del mismo cliente/día/bucket-tipo
263
298
  // (Strava-webhook y Garmin-push pueden entrar casi a la vez).
264
299
  yield q('SELECT pg_advisory_xact_lock(?)', [hashInt(`${opts.idCliente}|${dateStr}|${bucketTipo(opts.type)}`)]);
@@ -290,8 +325,8 @@ _data) => __awaiter(void 0, void 0, void 0, function* () {
290
325
  // de abrir la tx (ver `translated` arriba) para no retener el advisory lock.
291
326
  const { title: titleNoTranslate, description: descriptionNoTranslate, titlePreferredLanguage, descriptionPreferredLanguage } = translated;
292
327
  const inserted = yield q(`INSERT INTO "WORKOUT"
293
- ("ID CLIENTE", "DATE", "TITLE", "DESCRIPTION", "TITLE PREFERRED LANGUAGE", "DESCRIPTION PREFERRED LANGUAGE", "TYPE", "DONE", "SHOW CLIENT")
294
- VALUES (?, ?, ?, ?, ?, ?, ?, TRUE, TRUE) RETURNING "ID"`, [opts.idCliente, dateStr, titleNoTranslate, descriptionNoTranslate, titlePreferredLanguage, descriptionPreferredLanguage, opts.type]);
328
+ ("ID CLIENTE", "DATE", "START AT", "TITLE", "DESCRIPTION", "TITLE PREFERRED LANGUAGE", "DESCRIPTION PREFERRED LANGUAGE", "TYPE", "DONE", "SHOW CLIENT")
329
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, TRUE, TRUE) RETURNING "ID"`, [opts.idCliente, dateStr, (_d = opts.startAt) !== null && _d !== void 0 ? _d : null, titleNoTranslate, descriptionNoTranslate, titlePreferredLanguage, descriptionPreferredLanguage, opts.type]);
295
330
  if (!inserted || !inserted[0])
296
331
  return null;
297
332
  const id = inserted[0].id;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runnerpro/backend",
3
- "version": "1.21.4",
3
+ "version": "1.21.6",
4
4
  "description": "A collection of common backend functions",
5
5
  "exports": {
6
6
  ".": "./lib/cjs/index.js"