@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":"
|
|
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 (
|
|
153
|
-
// Strava/Apple/Garmin) empiezan en el mismo instante;
|
|
154
|
-
// desfase de
|
|
155
|
-
// actividades del mismo grupo de tipo en
|
|
156
|
-
const VENTANA_INICIO_MS =
|
|
157
|
-
|
|
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) <=
|
|
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" (
|
|
184
|
-
//
|
|
185
|
-
return similar(r.duration, opts.duration, 0.
|
|
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
|
-
//
|
|
245
|
-
//
|
|
246
|
-
const
|
|
247
|
-
//
|
|
248
|
-
//
|
|
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
|
|
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, (
|
|
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.
|
|
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.
|
|
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
|
+
}
|