@runnerpro/backend 1.21.16 → 1.22.0

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.
@@ -24,12 +24,12 @@ const transporter = nodemailer_1.default.createTransport({
24
24
  greetingTimeout: 10000,
25
25
  socketTimeout: 20000,
26
26
  });
27
- const sendMail = ({ subject, title, body, to, link, attachments, bcc }) => __awaiter(void 0, void 0, void 0, function* () {
27
+ const sendMail = ({ subject, title, body, to, link, attachments, bcc, signoff }) => __awaiter(void 0, void 0, void 0, function* () {
28
28
  var _a;
29
29
  const toMapped = process.env.NODE_ENV === 'PROD' ? to.map((correo) => correo).join(',') : process.env.GMAIL_EMAIL_USER;
30
30
  if ((toMapped === null || toMapped === void 0 ? void 0 : toMapped.length) === 0)
31
31
  return;
32
- const bodyHTML = getBodyHTML(title, body, link);
32
+ const bodyHTML = getBodyHTML(title, body, link, signoff);
33
33
  const sendPromise = transporter.sendMail({
34
34
  from: `"RunnerPro" <${process.env.GMAIL_EMAIL_USER}>`,
35
35
  to: toMapped,
@@ -37,14 +37,7 @@ const sendMail = ({ subject, title, body, to, link, attachments, bcc }) => __awa
37
37
  subject,
38
38
  text: body + (link ? link.url : ''),
39
39
  html: bodyHTML,
40
- attachments: [
41
- ...(attachments || []),
42
- {
43
- filename: 'logo-large.png',
44
- path: 'https://storage.googleapis.com/assets_runnerpro/logo-large-nuevo.png',
45
- cid: 'logo',
46
- },
47
- ],
40
+ attachments: attachments || [],
48
41
  });
49
42
  const transport = (_a = transporter.transporter) !== null && _a !== void 0 ? _a : transporter;
50
43
  const errorPromise = new Promise((_, reject) => {
@@ -71,63 +64,47 @@ const sendMail = ({ subject, title, body, to, link, attachments, bcc }) => __awa
71
64
  return Promise.race([sendPromise, errorPromise, timeoutPromise]);
72
65
  });
73
66
  exports.sendMail = sendMail;
74
- function getBodyHTML(title, body, link) {
67
+ function getBodyHTML(title, body, link, signoff) {
68
+ // Estilo: correo personal, de founder a runner. Fondo blanco, sin cabecera
69
+ // negra ni tarjeta; Helvetica, texto a la izquierda, mucho aire, CTA como
70
+ // enlace de texto y firma personal al pie (como las newsletters de Cristóbal).
71
+ const FONT = 'Helvetica, Arial, sans-serif';
72
+ const INK = '#2c2c2b';
73
+ const MUTED = '#8a8a86';
74
+ const firma = signoff || '';
75
+ const greetingHTML = title
76
+ ? `<p style="margin:0 0 20px 0;font-family:${FONT};font-size:16px;line-height:26px;color:${INK};">${title}</p>`
77
+ : '';
78
+ const bodyHTML = `<div style="font-family:${FONT};font-size:16px;line-height:26px;color:${INK};">${body
79
+ .split('\n')
80
+ .join('<br>')}</div>`;
81
+ const linkHTML = link
82
+ ? `<p style="margin:26px 0 0 0;font-family:${FONT};font-size:16px;line-height:26px;">
83
+ <a href="${link.external ? '' : process.env.FRONTEND_URL}${link.url}" style="color:${INK};font-weight:bold;text-decoration:underline;">${link.text} &rarr;</a>
84
+ </p>`
85
+ : '';
86
+ const signoffHTML = firma
87
+ ? `<p style="margin:26px 0 0 0;font-family:${FONT};font-size:16px;line-height:26px;font-style:italic;color:${MUTED};">${firma}</p>`
88
+ : '';
75
89
  return `
76
90
  <html lang="es">
77
91
  <head>
78
92
  <meta charset="UTF-8">
79
93
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
80
- <title>Correo con estilo</title>
81
- <style>
82
- body {
83
- margin: 0;
84
- padding: 0;
85
- }
86
- .button {
87
- display: inline-block;
88
- padding: 12px 26px;
89
- font-size: 17px;
90
- text-align: center;
91
- text-decoration: none;
92
- background-color: #000000;
93
- color: #ffffff !important;
94
- margin: 40px 0 20px 0;
95
- font-family: 'Sofia Sans', 'Roboto', sans-serif;
96
- font-weight: bold;
97
- border-radius: 4px;
98
- border: 2px solid #000000;
99
- }
100
- </style>
94
+ <title>RunnerPro</title>
101
95
  </head>
102
- <body style="background: #f0f0f0; padding: 0 0 40px 0">
103
- <table role="presentation" style="width:100%;border-collapse:collapse;border:0;border-spacing:0;background:#f0f0f0">
96
+ <body style="margin:0;padding:0;background:#ffffff;">
97
+ <table role="presentation" width="100%" style="border-collapse:collapse;border:0;border-spacing:0;background:#ffffff;">
104
98
  <tr>
105
- <td align="center" style="padding:0;">
106
- <table role="presentation" style="width:100%;border-collapse:collapse;border-spacing:0;text-align:left;">
99
+ <td align="center" style="padding:32px 16px 48px 16px;">
100
+ <table role="presentation" width="560" style="width:560px;max-width:100%;border-collapse:collapse;border:0;border-spacing:0;text-align:left;">
107
101
  <tr>
108
- <td align="center" style="padding:5px 0 0 0;background:#000000;line-height:0">
109
- <img src="cid:logo" alt="" width="240" style="height:auto;display:block;">
110
- </td>
111
- </tr>
112
- <tr>
113
- <td style="padding:20px 24px 0px 24px; max-width: 600px" align="center">
114
- <h1 style="font-size:22px;text-align:center;margin:16px 0 6px 0;font-family:'Sofia Sans', 'Roboto', sans-serif;font-style: italic">
115
- ${title || ''}
116
- </h1>
117
- <p style="text-align:left;margin:0 0 12px 0;font-size:15px;line-height:24px;font-family:'Sofia Sans', 'Roboto', sans-serif;">
118
- ${body.split('\n').join('<br>')}
119
- </p>
120
- ${link
121
- ? `
122
- <table role="presentation" style="width:100%;border-collapse:collapse;border:0;border-spacing:0;background:#f0f0f0;">
123
- <tr>
124
- <td align="center" style="padding:0;">
125
- <a href="${link.external ? '' : process.env.FRONTEND_URL}${link.url}" class="button" style="color:white">${link.text}</a>
126
- </td>
127
- </tr>
128
- </table>
129
- `
130
- : ''}
102
+ <td style="padding:0;">
103
+ ${greetingHTML}
104
+ ${bodyHTML}
105
+ ${linkHTML}
106
+ ${signoffHTML}
107
+ <p style="margin:40px 0 0 0;font-family:${FONT};font-size:14px;color:${MUTED};">RunnerPro</p>
131
108
  </td>
132
109
  </tr>
133
110
  </table>
@@ -6,6 +6,7 @@ interface Mail {
6
6
  link?: {
7
7
  url: string;
8
8
  text: string;
9
+ external?: boolean;
9
10
  };
10
11
  attachments?: {
11
12
  filename: string;
@@ -13,7 +14,13 @@ interface Mail {
13
14
  cid?: string;
14
15
  }[];
15
16
  bcc?: string[];
17
+ /**
18
+ * Firma personal al pie (estilo newsletter de Cristóbal), p. ej.
19
+ * "Cristóbal, CEO de RunnerPro". Por defecto no se firma. Solo se muestra
20
+ * si el correo pasa explícitamente este campo (correos de cliente).
21
+ */
22
+ signoff?: string;
16
23
  }
17
- declare const sendMail: ({ subject, title, body, to, link, attachments, bcc }: Mail) => Promise<import("nodemailer/lib/smtp-transport").SentMessageInfo>;
24
+ declare const sendMail: ({ subject, title, body, to, link, attachments, bcc, signoff }: Mail) => Promise<import("nodemailer/lib/smtp-transport").SentMessageInfo>;
18
25
  export { sendMail };
19
26
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/sendMail/index.ts"],"names":[],"mappings":"AAaA,UAAU,IAAI;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,EAAE,CAAC;IACb,IAAI,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACrC,WAAW,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACjE,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,QAAA,MAAM,QAAQ,yDAAgE,IAAI,qEA6CjF,CAAC;AAuEF,OAAO,EAAE,QAAQ,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/sendMail/index.ts"],"names":[],"mappings":"AAaA,UAAU,IAAI;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,EAAE,CAAC;IACb,IAAI,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IACzD,WAAW,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACjE,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;IACf;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,QAAA,MAAM,QAAQ,kEAAyE,IAAI,qEAsC1F,CAAC;AAiEF,OAAO,EAAE,QAAQ,EAAE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"getPlanificacionPrueba7diasEmpezarCorrer.d.ts","sourceRoot":"","sources":["../../../../../../src/workout/planificacionPrueba7dias/empezarCorrer/getPlanificacionPrueba7diasEmpezarCorrer.ts"],"names":[],"mappings":"AAIA,QAAA,MAAM,wCAAwC;;;;;;;;;;;;;GAK7C,CAAC;AAEF,QAAA,MAAM,4CAA4C,kBAQjD,CAAC;AAEF,OAAO,EAAE,wCAAwC,EAAE,4CAA4C,EAAE,CAAC"}
1
+ {"version":3,"file":"getPlanificacionPrueba7diasEmpezarCorrer.d.ts","sourceRoot":"","sources":["../../../../../../src/workout/planificacionPrueba7dias/empezarCorrer/getPlanificacionPrueba7diasEmpezarCorrer.ts"],"names":[],"mappings":"AAIA,QAAA,MAAM,wCAAwC;;;;;;;;;;;;;GAK7C,CAAC;AAEF,QAAA,MAAM,4CAA4C,kBAgCjD,CAAC;AAEF,OAAO,EAAE,wCAAwC,EAAE,4CAA4C,EAAE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/workout/planificacionPrueba7dias/index.ts"],"names":[],"mappings":"AAaA;;;;;;;;;;;;;;;;;;GAkBG;AACH,QAAA,MAAM,2BAA2B,qBAAgB,GAAG,UAkEnD,CAAC;AAmIF,OAAO,EAAE,2BAA2B,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/workout/planificacionPrueba7dias/index.ts"],"names":[],"mappings":"AAaA;;;;;;;;;;;;;;;;;;GAkBG;AACH,QAAA,MAAM,2BAA2B,qBAAgB,GAAG,UA+EnD,CAAC;AAmIF,OAAO,EAAE,2BAA2B,EAAE,CAAC"}
@@ -15,14 +15,33 @@ const getPlanificacionPrueba7diasEmpezarCorrer = (c) => {
15
15
  };
16
16
  exports.getPlanificacionPrueba7diasEmpezarCorrer = getPlanificacionPrueba7diasEmpezarCorrer;
17
17
  const changeGoalDistanceByNivelActualEmpezarCorrer = (c) => {
18
- if (c.objetivoPrincipal === 'Correr después de lesión' || Number(c.goalDistance) === -2) {
19
- c.type = '3';
18
+ // A5 El mapeo estaba invertido y con un typo: la vuelta de lesión (el perfil más frágil)
19
+ // caía en el nivel MÁS duro (2x8'), porque se comparaba contra 'Correr después de lesión'
20
+ // (sin "una", valor que nunca casaba con el del formulario) y, vía -2, caía en type '3'.
21
+ // Además el nivel más suave (3x1') era inalcanzable.
22
+ //
23
+ // Capa 3 — Si el formulario web aportó el AGUANTE corriendo (F3), la progresión CaCos se
24
+ // gradúa con él; y el ESTADO de la lesión (F7) pone techo (con dolor nunca por encima del
25
+ // nivel suave). Sin esos datos (cohorte app): el frágil y el principiante absoluto arrancan
26
+ // en el nivel suave (type '1'); el resto en el intermedio (type '2').
27
+ const NIVEL_POR_AGUANTE = { lt5: 1, '5-20': 2, '20-45': 2, gt45: 3 };
28
+ const TECHO_POR_LESION = { duele: 1, leve: 2, prudencia: 2, recuperado: 3 };
29
+ let nivel;
30
+ if (NIVEL_POR_AGUANTE[c.aguanteCorriendo] !== undefined) {
31
+ nivel = NIVEL_POR_AGUANTE[c.aguanteCorriendo];
20
32
  }
21
- else if (Number(c.goalDistance) === -1) {
22
- c.type = '1';
33
+ else if (c.objetivoPrincipal === 'Correr después de una lesión' ||
34
+ c.objetivoPrincipal === 'Comenzar a correr' ||
35
+ Number(c.goalDistance) === -2) {
36
+ nivel = 1;
23
37
  }
24
38
  else {
25
- c.type = '2';
39
+ nivel = 2;
26
40
  }
41
+ if (c.objetivoPrincipal === 'Correr después de una lesión') {
42
+ const techo = TECHO_POR_LESION[c.estadoLesion] !== undefined ? TECHO_POR_LESION[c.estadoLesion] : 1;
43
+ nivel = Math.min(nivel, techo);
44
+ }
45
+ c.type = String(nivel);
27
46
  };
28
47
  exports.changeGoalDistanceByNivelActualEmpezarCorrer = changeGoalDistanceByNivelActualEmpezarCorrer;
@@ -40,14 +40,28 @@ const getPlanificacionPrueba7dias = (c, options = {}) => {
40
40
  // @ts-ignore
41
41
  workoutsBase = getWorkoutsByGoalDistance(c);
42
42
  if (!workoutsBase || workoutsBase.length === 0) {
43
- workoutsBase = [];
43
+ // El aviso se mantiene como señal de que un objetivo se ha quedado sin plantilla propia.
44
44
  (0, sendMail_1.sendMail)({
45
45
  to: ['david.jimenez@runnerpro.app'],
46
46
  subject: 'Planificación de prueba 7 días - No hay entrenamientos',
47
47
  body: JSON.stringify(c),
48
48
  title: '',
49
49
  });
50
+ // Fallback — nunca dejar al cliente sin plantilla base ('Correr una distancia/carrera'
51
+ // sin distancia elegida, 'Ciclismo', textos de objetivo futuros…): vuelta de lesión (o
52
+ // similar) → progresión CaCos; resto → plan 10k por nivel, el mismo default que
53
+ // 'Entrenamiento general'.
54
+ c.objetivoPrincipal = /lesi/i.test(String(c.objetivoPrincipal || '')) ? 'Correr después de una lesión' : 'Entrenamiento general';
55
+ c.goalDistance = -1;
56
+ changeGoalDistanceByNivelActual(c);
57
+ // @ts-ignore
58
+ workoutsBase = getWorkoutsByGoalDistance(c) || [];
50
59
  }
60
+ // Sin plantilla no hay nada que repartir: mejor semana vacía explícita que rellenar los
61
+ // días disponibles con stubs { date } sin contenido (aguas abajo acababan siendo filas
62
+ // WORKOUT sin título ni duración) o solo con la sesión de fuerza genérica.
63
+ if (workoutsBase.length === 0)
64
+ return [];
51
65
  if ((Number(c.nivelEntrenamientoFuerza) === 1 || Number(c.incluirEntrenamientoFuerza) === 1) && nDiasDisponibles >= 4) {
52
66
  // @ts-ignore
53
67
  workoutsBase.splice(1, 0, addWorkoutFuerza());
@@ -98,7 +112,7 @@ const getPlanificacionPrueba7dias = (c, options = {}) => {
98
112
  };
99
113
  exports.getPlanificacionPrueba7dias = getPlanificacionPrueba7dias;
100
114
  const changeGoalDistanceByNivelActual = (c) => {
101
- if (['Comenzar a correr', 'Correr después de lesión'].includes(c.objetivoPrincipal) || [-2].includes(Number(c.goalDistance)))
115
+ if (['Comenzar a correr', 'Correr después de una lesión'].includes(c.objetivoPrincipal) || [-2].includes(Number(c.goalDistance)))
102
116
  (0, getPlanificacionPrueba7diasEmpezarCorrer_1.changeGoalDistanceByNivelActualEmpezarCorrer)(c);
103
117
  else if (Number(c.goalDistance) > 1 && Number(c.goalDistance) < 8000)
104
118
  (0, getPlanificacionPrueba7dias5k_1.changeGoalDistanceByNivelActual5k)(c);
@@ -113,7 +127,7 @@ const changeGoalDistanceByNivelActual = (c) => {
113
127
  (0, getPlanificacionPrueba7dias42k_1.changeGoalDistanceByNivelActual42k)(c);
114
128
  };
115
129
  const getWorkoutsByGoalDistance = (c) => {
116
- if (['Comenzar a correr', 'Correr después de lesión'].includes(c.objetivoPrincipal) || Number(c.goalDistance) === -2)
130
+ if (['Comenzar a correr', 'Correr después de una lesión'].includes(c.objetivoPrincipal) || Number(c.goalDistance) === -2)
117
131
  return (0, getPlanificacionPrueba7diasEmpezarCorrer_1.getPlanificacionPrueba7diasEmpezarCorrer)(c);
118
132
  if (Number(c.goalDistance) === 5000)
119
133
  return (0, getPlanificacionPrueba7dias5k_1.getPlanificacionPrueba7dias5k)(c);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runnerpro/backend",
3
- "version": "1.21.16",
3
+ "version": "1.22.0",
4
4
  "description": "A collection of common backend functions",
5
5
  "exports": {
6
6
  ".": "./lib/cjs/index.js"