hvp-shared 13.26.0 → 13.27.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.
@@ -166,6 +166,79 @@ export declare const SCHEDULER_WITH_DATE_REGEX: RegExp;
166
166
  * date.
167
167
  */
168
168
  export declare const SCHEDULER_DATE_FALLBACK_REGEX: RegExp;
169
+ /**
170
+ * Version tag stored in `extendedProperties.private.descriptionFormat` on
171
+ * events created with the labeled format. Lets analytics / future migration
172
+ * scripts distinguish labeled from positional events.
173
+ */
174
+ export declare const LABELED_DESCRIPTION_FORMAT_VERSION = "labeled-v1";
175
+ /**
176
+ * Per-field spec for the labeled description format.
177
+ *
178
+ * Each field has:
179
+ * - `label`: canonical label written when building (e.g., "Tutor").
180
+ * - `regex`: extraction regex with capturing groups. Group 1 = value.
181
+ * For `tutor`, group 2 captures the optional QVET id digits.
182
+ *
183
+ * Order-independent: labels anchor each field, so the format tolerates
184
+ * reordering or manual edits as long as the labels are present.
185
+ *
186
+ * Tolerates: `Tutor:`, `tutor :`, `TUTOR: `, trailing whitespace.
187
+ */
188
+ export declare const LABELED_DESCRIPTION_FIELDS: {
189
+ readonly tutor: {
190
+ readonly label: "Tutor";
191
+ readonly regex: RegExp;
192
+ };
193
+ readonly phones: {
194
+ readonly label: "Teléfonos";
195
+ readonly regex: RegExp;
196
+ };
197
+ readonly pet: {
198
+ readonly label: "Mascota";
199
+ readonly regex: RegExp;
200
+ };
201
+ readonly breed: {
202
+ readonly label: "Raza";
203
+ readonly regex: RegExp;
204
+ };
205
+ readonly age: {
206
+ readonly label: "Edad";
207
+ readonly regex: RegExp;
208
+ };
209
+ readonly motivo: {
210
+ readonly label: "Motivo";
211
+ readonly regex: RegExp;
212
+ };
213
+ readonly discount: {
214
+ readonly label: "Descuento";
215
+ readonly regex: RegExp;
216
+ };
217
+ };
218
+ export type LabeledDescriptionField = keyof typeof LABELED_DESCRIPTION_FIELDS;
219
+ /**
220
+ * Returns "labeled" if any known field label is present at the start of a line
221
+ * in the description; otherwise "positional" (legacy format).
222
+ */
223
+ export declare function detectDescriptionFormat(description: string): "labeled" | "positional";
224
+ /**
225
+ * Extract a single-line labeled field. Returns trimmed value or null.
226
+ *
227
+ * For `motivo` (which may span multiple lines), use {@link extractMotivoLabeled}.
228
+ * For the tutor's QVET id, use {@link extractTutorQvetIdLabeled}.
229
+ */
230
+ export declare function extractLabeledField(description: string, field: Exclude<LabeledDescriptionField, "motivo">): string | null;
231
+ /**
232
+ * Extract the QVET client id from the `Tutor: NAME (Q12345)` line. Returns the
233
+ * numeric portion as a string (e.g., "12345") or null.
234
+ */
235
+ export declare function extractTutorQvetIdLabeled(description: string): string | null;
236
+ /**
237
+ * Extract the motivo block which may span multiple lines. Starts at the
238
+ * `Motivo:` line and collects subsequent lines until another labeled field,
239
+ * an AGENDÓ/REAGENDÓ line, the HVP marker, or end of description.
240
+ */
241
+ export declare function extractMotivoLabeled(description: string): string | null;
169
242
  /**
170
243
  * Captures pet age expressions in Spanish from descriptions.
171
244
  *
@@ -197,4 +270,11 @@ export declare const HVP_EVENT_PROP_KEYS: {
197
270
  readonly isPreferredVet: "isPreferredVet";
198
271
  readonly source: "source";
199
272
  readonly standardVersion: "standardVersion";
273
+ /**
274
+ * Marks the description-builder format version used at create/update time
275
+ * (e.g., "labeled-v1"). Informational only — the parser detects format from
276
+ * content, not from this prop. Useful for analytics ("% of events on labeled
277
+ * format") and future migration scripts.
278
+ */
279
+ readonly descriptionFormat: "descriptionFormat";
200
280
  };
@@ -13,7 +13,11 @@
13
13
  * runs remain reproducible.
14
14
  */
15
15
  Object.defineProperty(exports, "__esModule", { value: true });
16
- exports.HVP_EVENT_PROP_KEYS = exports.KNOWN_BREEDS = exports.PET_AGE_REGEX = exports.SCHEDULER_DATE_FALLBACK_REGEX = exports.SCHEDULER_WITH_DATE_REGEX = exports.SCHEDULER_REGEX = exports.PHONE_REGEX = exports.QVET_ID_REGEX = exports.CANCELLED_TITLE_REGEX = exports.REMINDER_TITLE_REGEX = exports.PREFERRED_VET_REGEX = exports.EXCLUDED_TITLE_PREFIXES = exports.SERVICE_CODE_SYNONYMS = exports.CANONICAL_SERVICE_CODES = exports.EXCLUDED_COLOR_IDS = exports.NO_SHOW_COLOR_ID = exports.CANCELLED_COLOR_ID = exports.COLOR_ID_TO_BRANCH = exports.BRANCH_COLOR_IDS = exports.STANDARD_V1 = exports.STANDARD_V3 = exports.STANDARD_V2 = void 0;
16
+ exports.HVP_EVENT_PROP_KEYS = exports.KNOWN_BREEDS = exports.PET_AGE_REGEX = exports.LABELED_DESCRIPTION_FIELDS = exports.LABELED_DESCRIPTION_FORMAT_VERSION = exports.SCHEDULER_DATE_FALLBACK_REGEX = exports.SCHEDULER_WITH_DATE_REGEX = exports.SCHEDULER_REGEX = exports.PHONE_REGEX = exports.QVET_ID_REGEX = exports.CANCELLED_TITLE_REGEX = exports.REMINDER_TITLE_REGEX = exports.PREFERRED_VET_REGEX = exports.EXCLUDED_TITLE_PREFIXES = exports.SERVICE_CODE_SYNONYMS = exports.CANONICAL_SERVICE_CODES = exports.EXCLUDED_COLOR_IDS = exports.NO_SHOW_COLOR_ID = exports.CANCELLED_COLOR_ID = exports.COLOR_ID_TO_BRANCH = exports.BRANCH_COLOR_IDS = exports.STANDARD_V1 = exports.STANDARD_V3 = exports.STANDARD_V2 = void 0;
17
+ exports.detectDescriptionFormat = detectDescriptionFormat;
18
+ exports.extractLabeledField = extractLabeledField;
19
+ exports.extractTutorQvetIdLabeled = extractTutorQvetIdLabeled;
20
+ exports.extractMotivoLabeled = extractMotivoLabeled;
17
21
  // ─── Standard v1 ─────────────────────────────────────────────────────────────
18
22
  /**
19
23
  * Multi-factor appointment standard v2.
@@ -147,31 +151,31 @@ exports.STANDARD_V3 = {
147
151
  id: "context",
148
152
  weight: 8,
149
153
  label: "Motivo / contexto",
150
- description: 'La descripción tiene contexto clínico relevante (motivo de la visita, síntomas, antecedentes) más allá del nombre y teléfono. Mínimo 20 caracteres tras descontar nombre, teléfono y Q{id}.',
154
+ description: 'Eventos en formato labeled: cuenta el bloque "Motivo:" (>20 caracteres). Eventos legacy posicionales: la descripción tiene contexto clínico relevante más allá del nombre y teléfono mínimo 20 caracteres tras descontar nombre, teléfono y Q{id}.',
151
155
  },
152
156
  {
153
157
  id: "owner_name",
154
158
  weight: 8,
155
159
  label: "Nombre del tutor",
156
- description: 'Nombre del cliente/tutor. Auto-pasa si hay Q{id} en la descripción (cliente linkeado a QVET) o si el teléfono matchea un cliente QVET existente. Si no, busca nombre completo del tutor en la descripción (mínimo 2 palabras, mayúsculas/minúsculas con letras).',
160
+ description: 'Nombre del cliente/tutor. Auto-pasa si hay Q{id} en la descripción (cliente linkeado a QVET) o si el teléfono matchea un cliente QVET existente. Eventos labeled: lo lee de la línea "Tutor:". Eventos legacy: lo busca en la primera línea con 2+ palabras de letras (o el patrón "NAME | PETS | Q{id}").',
157
161
  },
158
162
  {
159
163
  id: "owner_phone",
160
164
  weight: 8,
161
165
  label: "Teléfono del tutor",
162
- description: 'Teléfono del tutor en cualquier formato común (con o sin +52, con o sin separadores). Auto-pasa si hay Q{id} (sabemos el teléfono del registro QVET). Necesario para confirmar la cita y avisar de cambios.',
166
+ description: 'Teléfono del tutor en cualquier formato común (con o sin +52, con o sin separadores). Eventos labeled: lo lee de la línea "Teléfonos:". Eventos legacy: lo busca en cualquier línea de la descripción. Auto-pasa si hay Q{id} (sabemos el teléfono del registro QVET).',
163
167
  },
164
168
  {
165
169
  id: "pet_age",
166
170
  weight: 4,
167
171
  label: "Edad de la mascota",
168
- description: 'Edad anotada en la descripción: "4 meses", "1 año y 4 meses", "8 semanas", "2 años". Importante para vacunación, dosis y diagnóstico.',
172
+ description: 'Edad anotada en la descripción. Eventos labeled: línea "Edad:". Eventos legacy: detección por patrón ("4 meses", "1 año y 4 meses", "8 semanas", "2 años"). Importante para vacunación, dosis y diagnóstico.',
169
173
  },
170
174
  {
171
175
  id: "pet_breed",
172
176
  weight: 4,
173
177
  label: "Raza de la mascota",
174
- description: 'Raza detectada en la descripción contra un diccionario de razas comunes: Yorkie, Shih Tzu, Cocker Spaniel, French Bulldog, Chihuahua, Schnauzer, Golden Retriever, Labrador, Persa, Siamés, mestizo, etc. Útil para protocolos clínicos y dosificación.',
178
+ description: 'Raza detectada en la descripción. Eventos labeled: línea "Raza:". Eventos legacy: match contra un diccionario de razas comunes (Yorkie, Shih Tzu, Cocker Spaniel, French Bulldog, Chihuahua, Schnauzer, Golden Retriever, Labrador, Persa, Siamés, mestizo, etc.).',
175
179
  },
176
180
  {
177
181
  id: "discount_format",
@@ -433,6 +437,122 @@ exports.SCHEDULER_WITH_DATE_REGEX = /\b(?:AG[.:]?|AGE[.:]|AGEND[OÓ][.:]?|AGENG[
433
437
  * date.
434
438
  */
435
439
  exports.SCHEDULER_DATE_FALLBACK_REGEX = /\b(\d{1,2}[./\-]\d{1,2}(?:[./\-]\d{2,4})?|\d{1,2}[./\-](?:ENE|FEB|MAR|ABR|MAY|JUN|JUL|AGO|SEP|OCT|NOV|DIC|ENERO|FEBRERO|MARZO|ABRIL|MAYO|JUNIO|JULIO|AGOSTO|SEPTIEMBRE|OCTUBRE|NOVIEMBRE|DICIEMBRE)(?:[./\-]\d{2,4})?)\s+([A-Z]{2,4})\b/i;
440
+ // ─── Labeled description format (06/2026) ───────────────────────────────────
441
+ /**
442
+ * Version tag stored in `extendedProperties.private.descriptionFormat` on
443
+ * events created with the labeled format. Lets analytics / future migration
444
+ * scripts distinguish labeled from positional events.
445
+ */
446
+ exports.LABELED_DESCRIPTION_FORMAT_VERSION = "labeled-v1";
447
+ /**
448
+ * Per-field spec for the labeled description format.
449
+ *
450
+ * Each field has:
451
+ * - `label`: canonical label written when building (e.g., "Tutor").
452
+ * - `regex`: extraction regex with capturing groups. Group 1 = value.
453
+ * For `tutor`, group 2 captures the optional QVET id digits.
454
+ *
455
+ * Order-independent: labels anchor each field, so the format tolerates
456
+ * reordering or manual edits as long as the labels are present.
457
+ *
458
+ * Tolerates: `Tutor:`, `tutor :`, `TUTOR: `, trailing whitespace.
459
+ */
460
+ exports.LABELED_DESCRIPTION_FIELDS = {
461
+ tutor: {
462
+ label: "Tutor",
463
+ regex: /^\s*tutor\s*:\s*(.+?)(?:\s*\(\s*Q(\d{4,7})\s*\))?\s*$/im,
464
+ },
465
+ phones: {
466
+ label: "Teléfonos",
467
+ regex: /^\s*tel[eé]fonos?\s*:\s*(.+?)\s*$/im,
468
+ },
469
+ pet: {
470
+ label: "Mascota",
471
+ regex: /^\s*mascota\s*:\s*(.+?)\s*$/im,
472
+ },
473
+ breed: {
474
+ label: "Raza",
475
+ regex: /^\s*raza\s*:\s*(.+?)\s*$/im,
476
+ },
477
+ age: {
478
+ label: "Edad",
479
+ regex: /^\s*edad\s*:\s*(.+?)\s*$/im,
480
+ },
481
+ motivo: {
482
+ label: "Motivo",
483
+ // Motivo is multi-line; extraction handled by extractMotivoLabeled().
484
+ // Regex below only detects presence of the label.
485
+ regex: /^\s*motivo\s*:/im,
486
+ },
487
+ discount: {
488
+ label: "Descuento",
489
+ regex: /^\s*descuento\s*:\s*(.+?)\s*$/im,
490
+ },
491
+ };
492
+ /**
493
+ * Matches a line that starts a new labeled field or the AGENDÓ/REAGENDÓ block
494
+ * or the HVP marker. Used to bound multi-line `Motivo:` capture.
495
+ */
496
+ const LABELED_BOUNDARY_REGEX = /^\s*(?:tutor|tel[eé]fonos?|mascota|raza|edad|descuento|motivo|agend|reagend|—)/i;
497
+ /**
498
+ * Returns "labeled" if any known field label is present at the start of a line
499
+ * in the description; otherwise "positional" (legacy format).
500
+ */
501
+ function detectDescriptionFormat(description) {
502
+ if (!description)
503
+ return "positional";
504
+ for (const field of Object.values(exports.LABELED_DESCRIPTION_FIELDS)) {
505
+ if (field.regex.test(description))
506
+ return "labeled";
507
+ }
508
+ return "positional";
509
+ }
510
+ /**
511
+ * Extract a single-line labeled field. Returns trimmed value or null.
512
+ *
513
+ * For `motivo` (which may span multiple lines), use {@link extractMotivoLabeled}.
514
+ * For the tutor's QVET id, use {@link extractTutorQvetIdLabeled}.
515
+ */
516
+ function extractLabeledField(description, field) {
517
+ if (!description)
518
+ return null;
519
+ const match = description.match(exports.LABELED_DESCRIPTION_FIELDS[field].regex);
520
+ const value = match?.[1]?.trim();
521
+ return value && value.length > 0 ? value : null;
522
+ }
523
+ /**
524
+ * Extract the QVET client id from the `Tutor: NAME (Q12345)` line. Returns the
525
+ * numeric portion as a string (e.g., "12345") or null.
526
+ */
527
+ function extractTutorQvetIdLabeled(description) {
528
+ if (!description)
529
+ return null;
530
+ const match = description.match(exports.LABELED_DESCRIPTION_FIELDS.tutor.regex);
531
+ const id = match?.[2]?.trim();
532
+ return id && id.length > 0 ? id : null;
533
+ }
534
+ /**
535
+ * Extract the motivo block which may span multiple lines. Starts at the
536
+ * `Motivo:` line and collects subsequent lines until another labeled field,
537
+ * an AGENDÓ/REAGENDÓ line, the HVP marker, or end of description.
538
+ */
539
+ function extractMotivoLabeled(description) {
540
+ if (!description)
541
+ return null;
542
+ const lines = description.split("\n");
543
+ const startIdx = lines.findIndex((l) => /^\s*motivo\s*:/i.test(l));
544
+ if (startIdx === -1)
545
+ return null;
546
+ const firstLine = lines[startIdx].replace(/^\s*motivo\s*:\s*/i, "");
547
+ const collected = [firstLine];
548
+ for (let i = startIdx + 1; i < lines.length; i++) {
549
+ if (LABELED_BOUNDARY_REGEX.test(lines[i]))
550
+ break;
551
+ collected.push(lines[i]);
552
+ }
553
+ const value = collected.join("\n").trim();
554
+ return value.length > 0 ? value : null;
555
+ }
436
556
  // ─── Pet age detection ───────────────────────────────────────────────────────
437
557
  /**
438
558
  * Captures pet age expressions in Spanish from descriptions.
@@ -541,4 +661,11 @@ exports.HVP_EVENT_PROP_KEYS = {
541
661
  isPreferredVet: "isPreferredVet",
542
662
  source: "source", // "hvp"
543
663
  standardVersion: "standardVersion",
664
+ /**
665
+ * Marks the description-builder format version used at create/update time
666
+ * (e.g., "labeled-v1"). Informational only — the parser detects format from
667
+ * content, not from this prop. Useful for analytics ("% of events on labeled
668
+ * format") and future migration scripts.
669
+ */
670
+ descriptionFormat: "descriptionFormat",
544
671
  };
@@ -207,6 +207,157 @@ describe("PET_AGE_REGEX", () => {
207
207
  expect("Cytopoint 30MG".match(google_calendar_constants_1.PET_AGE_REGEX)).toBeNull();
208
208
  });
209
209
  });
210
+ describe("Labeled description format (06/2026)", () => {
211
+ const labeledSample = [
212
+ "Tutor: OMAR GOMEZ (Q123456)",
213
+ "Teléfonos: 9992920692 / 9991234567",
214
+ "Mascota: MILO",
215
+ "Raza: Shih Tzu",
216
+ "Edad: 1 año",
217
+ "Motivo: Consulta de seguimiento por dermatitis",
218
+ "Descuento: 15% recordatorio",
219
+ "AGENDÓ JG 14/06/2026",
220
+ "— creada desde HVP app · [HVP]",
221
+ ].join("\n");
222
+ const positionalSample = [
223
+ "OMAR GOMEZ | MILO | Q123456",
224
+ "9992920692",
225
+ "Consulta de seguimiento por dermatitis",
226
+ "Shih Tzu, 1 año",
227
+ "15% recordatorio",
228
+ "AGENDÓ JG 14/06/2026",
229
+ "— creada desde HVP app · [HVP]",
230
+ ].join("\n");
231
+ describe("LABELED_DESCRIPTION_FORMAT_VERSION", () => {
232
+ it("is a stable string", () => {
233
+ expect(google_calendar_constants_1.LABELED_DESCRIPTION_FORMAT_VERSION).toBe("labeled-v1");
234
+ });
235
+ });
236
+ describe("LABELED_DESCRIPTION_FIELDS", () => {
237
+ it("has all expected fields with labels", () => {
238
+ const labels = Object.fromEntries(Object.entries(google_calendar_constants_1.LABELED_DESCRIPTION_FIELDS).map(([k, v]) => [k, v.label]));
239
+ expect(labels).toEqual({
240
+ tutor: "Tutor",
241
+ phones: "Teléfonos",
242
+ pet: "Mascota",
243
+ breed: "Raza",
244
+ age: "Edad",
245
+ motivo: "Motivo",
246
+ discount: "Descuento",
247
+ });
248
+ });
249
+ });
250
+ describe("detectDescriptionFormat", () => {
251
+ it("identifies a labeled description", () => {
252
+ expect((0, google_calendar_constants_1.detectDescriptionFormat)(labeledSample)).toBe("labeled");
253
+ });
254
+ it("identifies a positional (legacy) description", () => {
255
+ expect((0, google_calendar_constants_1.detectDescriptionFormat)(positionalSample)).toBe("positional");
256
+ });
257
+ it("treats empty / undefined as positional", () => {
258
+ expect((0, google_calendar_constants_1.detectDescriptionFormat)("")).toBe("positional");
259
+ expect((0, google_calendar_constants_1.detectDescriptionFormat)(undefined)).toBe("positional");
260
+ });
261
+ it("detects when only one label is present", () => {
262
+ expect((0, google_calendar_constants_1.detectDescriptionFormat)("Tutor: NORA\n9999999999")).toBe("labeled");
263
+ });
264
+ it("is case-insensitive on the label", () => {
265
+ expect((0, google_calendar_constants_1.detectDescriptionFormat)("tutor: NORA\n9999999999")).toBe("labeled");
266
+ expect((0, google_calendar_constants_1.detectDescriptionFormat)("TUTOR: NORA\n9999999999")).toBe("labeled");
267
+ });
268
+ });
269
+ describe("extractLabeledField", () => {
270
+ it("extracts the tutor name (without QVET id)", () => {
271
+ expect((0, google_calendar_constants_1.extractLabeledField)(labeledSample, "tutor")).toBe("OMAR GOMEZ");
272
+ });
273
+ it("extracts the tutor name when no QVET id is present", () => {
274
+ const desc = "Tutor: NORA DIRCIO\nMascota: KIRI";
275
+ expect((0, google_calendar_constants_1.extractLabeledField)(desc, "tutor")).toBe("NORA DIRCIO");
276
+ });
277
+ it("extracts phones, pet, breed, age, discount", () => {
278
+ expect((0, google_calendar_constants_1.extractLabeledField)(labeledSample, "phones")).toBe("9992920692 / 9991234567");
279
+ expect((0, google_calendar_constants_1.extractLabeledField)(labeledSample, "pet")).toBe("MILO");
280
+ expect((0, google_calendar_constants_1.extractLabeledField)(labeledSample, "breed")).toBe("Shih Tzu");
281
+ expect((0, google_calendar_constants_1.extractLabeledField)(labeledSample, "age")).toBe("1 año");
282
+ expect((0, google_calendar_constants_1.extractLabeledField)(labeledSample, "discount")).toBe("15% recordatorio");
283
+ });
284
+ it("returns null when the label is missing", () => {
285
+ const desc = "Tutor: OMAR\nMascota: MILO";
286
+ expect((0, google_calendar_constants_1.extractLabeledField)(desc, "discount")).toBeNull();
287
+ expect((0, google_calendar_constants_1.extractLabeledField)(desc, "breed")).toBeNull();
288
+ });
289
+ it("tolerates trailing whitespace and extra spaces around the colon", () => {
290
+ const desc = "Tutor : OMAR GOMEZ \nMascota:MILO";
291
+ expect((0, google_calendar_constants_1.extractLabeledField)(desc, "tutor")).toBe("OMAR GOMEZ");
292
+ expect((0, google_calendar_constants_1.extractLabeledField)(desc, "pet")).toBe("MILO");
293
+ });
294
+ it("is order-independent", () => {
295
+ const reordered = [
296
+ "Mascota: MILO",
297
+ "Tutor: OMAR GOMEZ (Q123456)",
298
+ "Edad: 1 año",
299
+ ].join("\n");
300
+ expect((0, google_calendar_constants_1.extractLabeledField)(reordered, "tutor")).toBe("OMAR GOMEZ");
301
+ expect((0, google_calendar_constants_1.extractLabeledField)(reordered, "pet")).toBe("MILO");
302
+ expect((0, google_calendar_constants_1.extractLabeledField)(reordered, "age")).toBe("1 año");
303
+ });
304
+ it("returns null for empty description", () => {
305
+ expect((0, google_calendar_constants_1.extractLabeledField)("", "tutor")).toBeNull();
306
+ });
307
+ });
308
+ describe("extractTutorQvetIdLabeled", () => {
309
+ it("extracts the QVET id from the tutor line", () => {
310
+ expect((0, google_calendar_constants_1.extractTutorQvetIdLabeled)(labeledSample)).toBe("123456");
311
+ });
312
+ it("returns null when no QVET id is present", () => {
313
+ const desc = "Tutor: NORA DIRCIO\nMascota: KIRI";
314
+ expect((0, google_calendar_constants_1.extractTutorQvetIdLabeled)(desc)).toBeNull();
315
+ });
316
+ it("tolerates extra whitespace inside the parens", () => {
317
+ const desc = "Tutor: NORA DIRCIO ( Q987654 )";
318
+ expect((0, google_calendar_constants_1.extractTutorQvetIdLabeled)(desc)).toBe("987654");
319
+ });
320
+ it("returns null when the tutor line is missing entirely", () => {
321
+ expect((0, google_calendar_constants_1.extractTutorQvetIdLabeled)("Mascota: KIRI")).toBeNull();
322
+ });
323
+ });
324
+ describe("extractMotivoLabeled", () => {
325
+ it("extracts a single-line motivo", () => {
326
+ expect((0, google_calendar_constants_1.extractMotivoLabeled)(labeledSample)).toBe("Consulta de seguimiento por dermatitis");
327
+ });
328
+ it("extracts a multi-line motivo", () => {
329
+ const desc = [
330
+ "Tutor: OMAR",
331
+ "Mascota: MILO",
332
+ "Motivo: Consulta inicial",
333
+ "Antecedentes de alergia",
334
+ "Sospecha de sarna",
335
+ "AGENDÓ JG 14/06/2026",
336
+ ].join("\n");
337
+ expect((0, google_calendar_constants_1.extractMotivoLabeled)(desc)).toBe("Consulta inicial\nAntecedentes de alergia\nSospecha de sarna");
338
+ });
339
+ it("stops at the next labeled field even when AGENDÓ is far below", () => {
340
+ const desc = [
341
+ "Tutor: OMAR",
342
+ "Motivo: Solo este motivo",
343
+ "Mascota: MILO",
344
+ "Raza: Shih Tzu",
345
+ "AGENDÓ JG 14/06/2026",
346
+ ].join("\n");
347
+ expect((0, google_calendar_constants_1.extractMotivoLabeled)(desc)).toBe("Solo este motivo");
348
+ });
349
+ it("stops at the HVP marker", () => {
350
+ const desc = [
351
+ "Motivo: Consulta",
352
+ "— creada desde HVP app · [HVP]",
353
+ ].join("\n");
354
+ expect((0, google_calendar_constants_1.extractMotivoLabeled)(desc)).toBe("Consulta");
355
+ });
356
+ it("returns null when no motivo label is present", () => {
357
+ expect((0, google_calendar_constants_1.extractMotivoLabeled)("Tutor: OMAR\nMascota: MILO")).toBeNull();
358
+ });
359
+ });
360
+ });
210
361
  describe("KNOWN_BREEDS", () => {
211
362
  it("includes common breeds observed in the discovery data", () => {
212
363
  const lower = google_calendar_constants_1.KNOWN_BREEDS.map((b) => b.toLowerCase());
@@ -85,4 +85,16 @@ export interface CreateAppointmentRequest {
85
85
  * collaborators list. If omitted, the auth user's col_code is used.
86
86
  */
87
87
  schedulerColCode?: string;
88
+ /**
89
+ * Optional manual override for the description body. When present, the
90
+ * backend uses this verbatim instead of building the description from the
91
+ * structured fields above. The backend still appends the AGENDÓ/REAGENDÓ
92
+ * scheduler line and the HVP marker so attribution and provenance are not
93
+ * lost.
94
+ *
95
+ * Used when the receptionist enables "Editar texto" in the form to fine-tune
96
+ * wording before saving. On PATCH, supplying this also short-circuits the
97
+ * labeled rebuild — useful for surgical edits.
98
+ */
99
+ descriptionOverride?: string;
88
100
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hvp-shared",
3
- "version": "13.26.0",
3
+ "version": "13.27.0",
4
4
  "description": "Shared types and utilities for HVP backend and frontend",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",