hvp-shared 13.1.0 → 13.3.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.
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Google Calendar Integration — Constants
3
+ *
4
+ * Implements the appointment standard documented in
5
+ * `resources/notes/google-calendar/standard-v1.md`.
6
+ *
7
+ * Consumed by:
8
+ * - Backend diagnostic engine (parser + scorer)
9
+ * - Frontend dashboard (dimension labels, weights)
10
+ *
11
+ * Changes to scoring rules require a new version (STANDARD_V2) so previous
12
+ * runs remain reproducible.
13
+ */
14
+ import type { AppointmentBranch, AppointmentStandard } from "../contracts/google-calendar/responses";
15
+ /**
16
+ * Multi-factor appointment standard v1.
17
+ *
18
+ * Weights sum to 100. An event is "compliant" when its score >= 80.
19
+ */
20
+ export declare const STANDARD_V1: AppointmentStandard;
21
+ /**
22
+ * Google Calendar colorId values mapped to HVP branches.
23
+ *
24
+ * Confirmed via Phase 0 discovery (2026-05-18):
25
+ * - 11 (Tomato) → Urban
26
+ * - 9 (Blueberry) → Harbor
27
+ * - 3 (Grape) → Montejo (citas) + recordatorios operacionales
28
+ */
29
+ export declare const BRANCH_COLOR_IDS: Readonly<Record<AppointmentBranch, string>>;
30
+ /**
31
+ * Reverse lookup: colorId → branch.
32
+ */
33
+ export declare const COLOR_ID_TO_BRANCH: Readonly<Record<string, AppointmentBranch>>;
34
+ /** colorId 8 (Graphite) = cancelled. Tracked as a separate metric. */
35
+ export declare const CANCELLED_COLOR_ID = "8";
36
+ /**
37
+ * colorIds reserved for special events (NOT patient appointments).
38
+ *
39
+ * - 2 (Sage) — aniversario HVP, "no agendar nada", recordatorios admin
40
+ * - 4 (Flamingo) — cumpleaños colaboradores
41
+ * - 5 (Banana) — bloqueos (vacaciones, cursos, NO AGENDAR)
42
+ * - 10 (Basil) — reuniones, eventos importantes
43
+ */
44
+ export declare const EXCLUDED_COLOR_IDS: ReadonlySet<string>;
45
+ /**
46
+ * Canonical service codes — the diagnostic engine's source of truth.
47
+ *
48
+ * The parser maps free-form prefixes (via SERVICE_CODE_SYNONYMS) to one of these.
49
+ */
50
+ export declare const CANONICAL_SERVICE_CODES: readonly ["CONSULTA", "SIN_CITA", "CONSULTA_SEGUIMIENTO", "VACUNA", "CIRUGIA", "CERTIFICADO", "ULTRASONIDO", "RADIOGRAFIA", "ECOCARDIO", "OVH", "PROFILAXIS_DENTAL", "ANESTESIA", "EUTANASIA", "RETIRO_PUNTOS", "REVISION", "DESPARASITACION", "APLICACION", "TOMA_MUESTRAS", "TERAPIA_MEDICA", "INYECCION"];
51
+ export type CanonicalServiceCode = (typeof CANONICAL_SERVICE_CODES)[number];
52
+ /**
53
+ * Maps free-form prefixes (uppercase, trimmed, period-stripped) to canonical codes.
54
+ *
55
+ * Example: title "C. OFT LUKE RGL*" → first token "C" → CONSULTA.
56
+ */
57
+ export declare const SERVICE_CODE_SYNONYMS: Readonly<Record<string, CanonicalServiceCode>>;
58
+ /**
59
+ * Title prefixes (uppercase, first token) that mark NON-clinical events.
60
+ *
61
+ * Diagnostic engine excludes these — they are not patient appointments.
62
+ */
63
+ export declare const EXCLUDED_TITLE_PREFIXES: ReadonlySet<string>;
64
+ /**
65
+ * Detects an asterisk at the end of the title (after optional whitespace).
66
+ * `*` at the end = "médico preferido" = the vet earns a commission.
67
+ *
68
+ * Examples that match:
69
+ * "C. ARCHIE AAT*"
70
+ * "C. ARCHIE AAT *"
71
+ * "AM CAMILA APL*"
72
+ */
73
+ export declare const PREFERRED_VET_REGEX: RegExp;
74
+ /**
75
+ * Detects "ENVIAR RECORDATORIOS *" titles (Montejo recordatorios). Even though
76
+ * they live in colorId 3 (Montejo), they are NOT patient appointments.
77
+ */
78
+ export declare const REMINDER_TITLE_REGEX: RegExp;
79
+ /**
80
+ * Detects cancelled events by title content. Used in addition to colorId 8.
81
+ */
82
+ export declare const CANCELLED_TITLE_REGEX: RegExp;
83
+ /**
84
+ * Matches a QVET client id ("Q123456").
85
+ * Range chosen wide enough to cover historical and future ids.
86
+ */
87
+ export declare const QVET_ID_REGEX: RegExp;
88
+ /**
89
+ * Captures Mexican phone numbers in any of the common written formats.
90
+ * Returns the full match; caller normalizes to last 10 digits.
91
+ *
92
+ * Tolerates:
93
+ * +52 1 999 442 9488
94
+ * +52 999 442 9488
95
+ * 999 442 9488
96
+ * 999-442-9488
97
+ * 9994429488
98
+ */
99
+ export declare const PHONE_REGEX: RegExp;
100
+ /**
101
+ * Captures the "agendado por" col_code from a description.
102
+ *
103
+ * Variants observed:
104
+ * "AGENDO XZA 16 MAYO"
105
+ * "AG YMP 14.05.26"
106
+ * "AGENDADA POR SLR 02/12/25"
107
+ * "AGENDADO POR SLR 02/12/25"
108
+ */
109
+ export declare const SCHEDULER_REGEX: RegExp;
110
+ /**
111
+ * Keys used in Google Calendar `extendedProperties.private` for events created
112
+ * from HVP. Phase 2 writes these; Phase 1 reads them for full attribution.
113
+ */
114
+ export declare const HVP_EVENT_PROP_KEYS: {
115
+ readonly qvetClientId: "qvetClientId";
116
+ readonly qvetPetId: "qvetPetId";
117
+ readonly serviceCode: "serviceCode";
118
+ readonly branchId: "branchId";
119
+ readonly vetColCode: "vetColCode";
120
+ readonly scheduledByColCode: "scheduledByColCode";
121
+ readonly isPreferredVet: "isPreferredVet";
122
+ readonly source: "source";
123
+ readonly standardVersion: "standardVersion";
124
+ };
@@ -0,0 +1,261 @@
1
+ "use strict";
2
+ /**
3
+ * Google Calendar Integration — Constants
4
+ *
5
+ * Implements the appointment standard documented in
6
+ * `resources/notes/google-calendar/standard-v1.md`.
7
+ *
8
+ * Consumed by:
9
+ * - Backend diagnostic engine (parser + scorer)
10
+ * - Frontend dashboard (dimension labels, weights)
11
+ *
12
+ * Changes to scoring rules require a new version (STANDARD_V2) so previous
13
+ * runs remain reproducible.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.HVP_EVENT_PROP_KEYS = 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.CANCELLED_COLOR_ID = exports.COLOR_ID_TO_BRANCH = exports.BRANCH_COLOR_IDS = exports.STANDARD_V1 = void 0;
17
+ // ─── Standard v1 ─────────────────────────────────────────────────────────────
18
+ /**
19
+ * Multi-factor appointment standard v1.
20
+ *
21
+ * Weights sum to 100. An event is "compliant" when its score >= 80.
22
+ */
23
+ exports.STANDARD_V1 = {
24
+ version: "1.0",
25
+ minimumCompliantScore: 80,
26
+ dimensions: [
27
+ { id: "branch", weight: 15, label: "Sucursal" },
28
+ { id: "service", weight: 20, label: "Servicio" },
29
+ { id: "pet", weight: 15, label: "Paciente" },
30
+ { id: "client", weight: 25, label: "Cliente (linkable a QVET)" },
31
+ { id: "context", weight: 10, label: "Motivo / contexto" },
32
+ { id: "vet", weight: 15, label: "Atribución del vet" },
33
+ ],
34
+ };
35
+ // ─── ColorId → Branch mapping ────────────────────────────────────────────────
36
+ /**
37
+ * Google Calendar colorId values mapped to HVP branches.
38
+ *
39
+ * Confirmed via Phase 0 discovery (2026-05-18):
40
+ * - 11 (Tomato) → Urban
41
+ * - 9 (Blueberry) → Harbor
42
+ * - 3 (Grape) → Montejo (citas) + recordatorios operacionales
43
+ */
44
+ exports.BRANCH_COLOR_IDS = {
45
+ urban: "11",
46
+ harbor: "9",
47
+ montejo: "3",
48
+ };
49
+ /**
50
+ * Reverse lookup: colorId → branch.
51
+ */
52
+ exports.COLOR_ID_TO_BRANCH = {
53
+ "11": "urban",
54
+ "9": "harbor",
55
+ "3": "montejo",
56
+ };
57
+ // ─── Excluded colorIds ───────────────────────────────────────────────────────
58
+ /** colorId 8 (Graphite) = cancelled. Tracked as a separate metric. */
59
+ exports.CANCELLED_COLOR_ID = "8";
60
+ /**
61
+ * colorIds reserved for special events (NOT patient appointments).
62
+ *
63
+ * - 2 (Sage) — aniversario HVP, "no agendar nada", recordatorios admin
64
+ * - 4 (Flamingo) — cumpleaños colaboradores
65
+ * - 5 (Banana) — bloqueos (vacaciones, cursos, NO AGENDAR)
66
+ * - 10 (Basil) — reuniones, eventos importantes
67
+ */
68
+ exports.EXCLUDED_COLOR_IDS = new Set([
69
+ "2",
70
+ "4",
71
+ "5",
72
+ "10",
73
+ ]);
74
+ // ─── Service codes ───────────────────────────────────────────────────────────
75
+ /**
76
+ * Canonical service codes — the diagnostic engine's source of truth.
77
+ *
78
+ * The parser maps free-form prefixes (via SERVICE_CODE_SYNONYMS) to one of these.
79
+ */
80
+ exports.CANONICAL_SERVICE_CODES = [
81
+ "CONSULTA",
82
+ "SIN_CITA",
83
+ "CONSULTA_SEGUIMIENTO",
84
+ "VACUNA",
85
+ "CIRUGIA",
86
+ "CERTIFICADO",
87
+ "ULTRASONIDO",
88
+ "RADIOGRAFIA",
89
+ "ECOCARDIO",
90
+ "OVH",
91
+ "PROFILAXIS_DENTAL",
92
+ "ANESTESIA",
93
+ "EUTANASIA",
94
+ "RETIRO_PUNTOS",
95
+ "REVISION",
96
+ "DESPARASITACION",
97
+ "APLICACION",
98
+ "TOMA_MUESTRAS",
99
+ "TERAPIA_MEDICA",
100
+ "INYECCION",
101
+ ];
102
+ /**
103
+ * Maps free-form prefixes (uppercase, trimmed, period-stripped) to canonical codes.
104
+ *
105
+ * Example: title "C. OFT LUKE RGL*" → first token "C" → CONSULTA.
106
+ */
107
+ exports.SERVICE_CODE_SYNONYMS = {
108
+ // Consulta
109
+ C: "CONSULTA",
110
+ CONSULTA: "CONSULTA",
111
+ // Sin Cita (walk-in)
112
+ SC: "SIN_CITA",
113
+ // Consulta Seguimiento
114
+ CS: "CONSULTA_SEGUIMIENTO",
115
+ // Vacuna
116
+ V: "VACUNA",
117
+ VAC: "VACUNA",
118
+ VACUNA: "VACUNA",
119
+ // Cirugía
120
+ CX: "CIRUGIA",
121
+ CIRUGIA: "CIRUGIA",
122
+ CIRUGÍA: "CIRUGIA",
123
+ // Certificado
124
+ CERT: "CERTIFICADO",
125
+ CERTIF: "CERTIFICADO",
126
+ CERTIFICADO: "CERTIFICADO",
127
+ // Ultrasonido
128
+ USG: "ULTRASONIDO",
129
+ ULTRASONIDO: "ULTRASONIDO",
130
+ // Radiografía
131
+ RX: "RADIOGRAFIA",
132
+ RADIOGRAFIA: "RADIOGRAFIA",
133
+ RADIOGRAFÍA: "RADIOGRAFIA",
134
+ // Ecocardio
135
+ ECO: "ECOCARDIO",
136
+ ECOCARDIOGRAMA: "ECOCARDIO",
137
+ // OVH (ovariohisterectomía)
138
+ OVH: "OVH",
139
+ // Profilaxis dental
140
+ PROFILAXIS: "PROFILAXIS_DENTAL",
141
+ LIMPIEZA: "PROFILAXIS_DENTAL",
142
+ // Anestesia
143
+ ANESTESIA: "ANESTESIA",
144
+ // Eutanasia
145
+ EUTANASIA: "EUTANASIA",
146
+ // Retiro de puntos / vendaje
147
+ RETIRO: "RETIRO_PUNTOS",
148
+ RET: "RETIRO_PUNTOS",
149
+ // Revisión
150
+ REV: "REVISION",
151
+ REVISION: "REVISION",
152
+ REVISIÓN: "REVISION",
153
+ // Desparasitación
154
+ D: "DESPARASITACION",
155
+ DES: "DESPARASITACION",
156
+ DESPARASITACION: "DESPARASITACION",
157
+ DESPARASITACIÓN: "DESPARASITACION",
158
+ // Aplicación de medicamento / microchip / etc.
159
+ AP: "APLICACION",
160
+ APL: "APLICACION",
161
+ AM: "APLICACION",
162
+ APLI: "APLICACION",
163
+ APLICACION: "APLICACION",
164
+ APLICACIÓN: "APLICACION",
165
+ // Toma de muestras
166
+ TM: "TOMA_MUESTRAS",
167
+ // Terapia médica (visto como "T.M." → TM)
168
+ T: "TERAPIA_MEDICA",
169
+ // Inyección
170
+ INYECCION: "INYECCION",
171
+ INYECCIÓN: "INYECCION",
172
+ };
173
+ /**
174
+ * Title prefixes (uppercase, first token) that mark NON-clinical events.
175
+ *
176
+ * Diagnostic engine excludes these — they are not patient appointments.
177
+ */
178
+ exports.EXCLUDED_TITLE_PREFIXES = new Set([
179
+ "RECIBIR",
180
+ "ENTREGAR",
181
+ "DESPACHAR",
182
+ "DESP",
183
+ "ENVIAR",
184
+ "RECEPCION",
185
+ "RECEPCIÓN",
186
+ "RECORDATORIO",
187
+ "RECORDATORIOS",
188
+ "ENTREVISTA",
189
+ "REUNION",
190
+ "REUNIÓN",
191
+ "CUMPLE",
192
+ "CUMPLEAÑOS",
193
+ "ANIVERSARIO",
194
+ "VACACIONES",
195
+ "NO",
196
+ "IMPORTANTE",
197
+ ]);
198
+ // ─── Title patterns ──────────────────────────────────────────────────────────
199
+ /**
200
+ * Detects an asterisk at the end of the title (after optional whitespace).
201
+ * `*` at the end = "médico preferido" = the vet earns a commission.
202
+ *
203
+ * Examples that match:
204
+ * "C. ARCHIE AAT*"
205
+ * "C. ARCHIE AAT *"
206
+ * "AM CAMILA APL*"
207
+ */
208
+ exports.PREFERRED_VET_REGEX = /\*\s*$/;
209
+ /**
210
+ * Detects "ENVIAR RECORDATORIOS *" titles (Montejo recordatorios). Even though
211
+ * they live in colorId 3 (Montejo), they are NOT patient appointments.
212
+ */
213
+ exports.REMINDER_TITLE_REGEX = /^ENVIAR\s+RECORDATORIOS\b/i;
214
+ /**
215
+ * Detects cancelled events by title content. Used in addition to colorId 8.
216
+ */
217
+ exports.CANCELLED_TITLE_REGEX = /\b(?:CANCEL(?:ADA|ADO))\b|\bse\s+cancela\b/i;
218
+ // ─── Description patterns ────────────────────────────────────────────────────
219
+ /**
220
+ * Matches a QVET client id ("Q123456").
221
+ * Range chosen wide enough to cover historical and future ids.
222
+ */
223
+ exports.QVET_ID_REGEX = /Q\d{4,7}/;
224
+ /**
225
+ * Captures Mexican phone numbers in any of the common written formats.
226
+ * Returns the full match; caller normalizes to last 10 digits.
227
+ *
228
+ * Tolerates:
229
+ * +52 1 999 442 9488
230
+ * +52 999 442 9488
231
+ * 999 442 9488
232
+ * 999-442-9488
233
+ * 9994429488
234
+ */
235
+ exports.PHONE_REGEX = /(?:\+?52\s?1?\s?)?(?:\d{3}[\s-]?\d{3}[\s-]?\d{4}|\d{10})/;
236
+ /**
237
+ * Captures the "agendado por" col_code from a description.
238
+ *
239
+ * Variants observed:
240
+ * "AGENDO XZA 16 MAYO"
241
+ * "AG YMP 14.05.26"
242
+ * "AGENDADA POR SLR 02/12/25"
243
+ * "AGENDADO POR SLR 02/12/25"
244
+ */
245
+ exports.SCHEDULER_REGEX = /\b(?:AG|AGENDO|AGENDAD[OA]\s+POR)\s+([A-Z]{2,4})\b/i;
246
+ // ─── HVP-created extendedProperties keys ─────────────────────────────────────
247
+ /**
248
+ * Keys used in Google Calendar `extendedProperties.private` for events created
249
+ * from HVP. Phase 2 writes these; Phase 1 reads them for full attribution.
250
+ */
251
+ exports.HVP_EVENT_PROP_KEYS = {
252
+ qvetClientId: "qvetClientId",
253
+ qvetPetId: "qvetPetId",
254
+ serviceCode: "serviceCode",
255
+ branchId: "branchId",
256
+ vetColCode: "vetColCode",
257
+ scheduledByColCode: "scheduledByColCode",
258
+ isPreferredVet: "isPreferredVet",
259
+ source: "source", // "hvp"
260
+ standardVersion: "standardVersion",
261
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const google_calendar_constants_1 = require("./google-calendar.constants");
4
+ describe("STANDARD_V1", () => {
5
+ it("has weights that sum to 100", () => {
6
+ const total = google_calendar_constants_1.STANDARD_V1.dimensions.reduce((sum, d) => sum + d.weight, 0);
7
+ expect(total).toBe(100);
8
+ });
9
+ it("declares exactly the six expected dimensions", () => {
10
+ const ids = google_calendar_constants_1.STANDARD_V1.dimensions.map((d) => d.id).sort();
11
+ expect(ids).toEqual(["branch", "client", "context", "pet", "service", "vet"]);
12
+ });
13
+ it("has a sensible compliance threshold", () => {
14
+ expect(google_calendar_constants_1.STANDARD_V1.minimumCompliantScore).toBeGreaterThanOrEqual(50);
15
+ expect(google_calendar_constants_1.STANDARD_V1.minimumCompliantScore).toBeLessThanOrEqual(100);
16
+ });
17
+ it("has a non-empty version string", () => {
18
+ expect(google_calendar_constants_1.STANDARD_V1.version).toMatch(/^\d+\.\d+$/);
19
+ });
20
+ });
21
+ describe("Branch ↔ colorId mapping", () => {
22
+ it("is a round-trip", () => {
23
+ for (const [branch, colorId] of Object.entries(google_calendar_constants_1.BRANCH_COLOR_IDS)) {
24
+ expect(google_calendar_constants_1.COLOR_ID_TO_BRANCH[colorId]).toBe(branch);
25
+ }
26
+ });
27
+ it("uses the discovery-confirmed colorIds", () => {
28
+ expect(google_calendar_constants_1.BRANCH_COLOR_IDS.urban).toBe("11");
29
+ expect(google_calendar_constants_1.BRANCH_COLOR_IDS.harbor).toBe("9");
30
+ expect(google_calendar_constants_1.BRANCH_COLOR_IDS.montejo).toBe("3");
31
+ });
32
+ it("does not collide with excluded or cancelled colorIds", () => {
33
+ for (const colorId of Object.values(google_calendar_constants_1.BRANCH_COLOR_IDS)) {
34
+ expect(google_calendar_constants_1.EXCLUDED_COLOR_IDS.has(colorId)).toBe(false);
35
+ expect(colorId).not.toBe(google_calendar_constants_1.CANCELLED_COLOR_ID);
36
+ }
37
+ });
38
+ });
39
+ describe("Service code synonyms", () => {
40
+ it("only maps to canonical codes", () => {
41
+ const canonicalSet = new Set(google_calendar_constants_1.CANONICAL_SERVICE_CODES);
42
+ for (const [synonym, canonical] of Object.entries(google_calendar_constants_1.SERVICE_CODE_SYNONYMS)) {
43
+ expect(canonicalSet.has(canonical)).toBe(true);
44
+ // synonyms should be uppercase (we normalize before lookup)
45
+ expect(synonym).toBe(synonym.toUpperCase());
46
+ }
47
+ });
48
+ it("covers every canonical code with at least one synonym", () => {
49
+ const reverseIndex = new Map();
50
+ for (const [syn, canonical] of Object.entries(google_calendar_constants_1.SERVICE_CODE_SYNONYMS)) {
51
+ const arr = reverseIndex.get(canonical) ?? [];
52
+ arr.push(syn);
53
+ reverseIndex.set(canonical, arr);
54
+ }
55
+ for (const canonical of google_calendar_constants_1.CANONICAL_SERVICE_CODES) {
56
+ expect(reverseIndex.get(canonical)?.length ?? 0).toBeGreaterThan(0);
57
+ }
58
+ });
59
+ it("collapses VACUNA variants", () => {
60
+ expect(google_calendar_constants_1.SERVICE_CODE_SYNONYMS.V).toBe("VACUNA");
61
+ expect(google_calendar_constants_1.SERVICE_CODE_SYNONYMS.VAC).toBe("VACUNA");
62
+ expect(google_calendar_constants_1.SERVICE_CODE_SYNONYMS.VACUNA).toBe("VACUNA");
63
+ });
64
+ it("collapses APLICACION variants (AP/APL/AM)", () => {
65
+ expect(google_calendar_constants_1.SERVICE_CODE_SYNONYMS.AP).toBe("APLICACION");
66
+ expect(google_calendar_constants_1.SERVICE_CODE_SYNONYMS.APL).toBe("APLICACION");
67
+ expect(google_calendar_constants_1.SERVICE_CODE_SYNONYMS.AM).toBe("APLICACION");
68
+ });
69
+ });
70
+ describe("Title regexes", () => {
71
+ it("detects the preferred-vet asterisk at the end of titles", () => {
72
+ expect(google_calendar_constants_1.PREFERRED_VET_REGEX.test("C. ARCHIE AAT*")).toBe(true);
73
+ expect(google_calendar_constants_1.PREFERRED_VET_REGEX.test("AM CAMILA APL* ")).toBe(true);
74
+ expect(google_calendar_constants_1.PREFERRED_VET_REGEX.test("CX. DANNA MAT*")).toBe(true);
75
+ });
76
+ it("does not flag asterisk in the middle of a title", () => {
77
+ expect(google_calendar_constants_1.PREFERRED_VET_REGEX.test("C. MILO *fpo IS A BUG")).toBe(false);
78
+ });
79
+ it("identifies Montejo reminder events", () => {
80
+ expect(google_calendar_constants_1.REMINDER_TITLE_REGEX.test("ENVIAR RECORDATORIOS MONT")).toBe(true);
81
+ expect(google_calendar_constants_1.REMINDER_TITLE_REGEX.test("enviar recordatorios mont")).toBe(true);
82
+ expect(google_calendar_constants_1.REMINDER_TITLE_REGEX.test("D. MILO")).toBe(false);
83
+ });
84
+ it("flags operational prefixes as excluded", () => {
85
+ expect(google_calendar_constants_1.EXCLUDED_TITLE_PREFIXES.has("RECIBIR")).toBe(true);
86
+ expect(google_calendar_constants_1.EXCLUDED_TITLE_PREFIXES.has("ENTREGAR")).toBe(true);
87
+ expect(google_calendar_constants_1.EXCLUDED_TITLE_PREFIXES.has("ENTREVISTA")).toBe(true);
88
+ expect(google_calendar_constants_1.EXCLUDED_TITLE_PREFIXES.has("C")).toBe(false);
89
+ });
90
+ });
91
+ describe("Description regexes", () => {
92
+ it("captures Q{id} client ids", () => {
93
+ expect("DIANA LAURA | KIRI | Q913562".match(google_calendar_constants_1.QVET_ID_REGEX)?.[0]).toBe("Q913562");
94
+ expect("Q966648".match(google_calendar_constants_1.QVET_ID_REGEX)?.[0]).toBe("Q966648");
95
+ expect("no qvet here".match(google_calendar_constants_1.QVET_ID_REGEX)).toBeNull();
96
+ });
97
+ it("captures phones in common Mexican formats", () => {
98
+ expect("+52 1 999 442 9488".match(google_calendar_constants_1.PHONE_REGEX)?.[0]).toBe("+52 1 999 442 9488");
99
+ expect("999 442 9488".match(google_calendar_constants_1.PHONE_REGEX)?.[0]).toBe("999 442 9488");
100
+ expect("9994429488".match(google_calendar_constants_1.PHONE_REGEX)?.[0]).toBe("9994429488");
101
+ expect("999-442-9488".match(google_calendar_constants_1.PHONE_REGEX)?.[0]).toBe("999-442-9488");
102
+ });
103
+ it("captures scheduler col_code from descriptions", () => {
104
+ const cases = [
105
+ ["AGENDO XZA 16 MAYO", "XZA"],
106
+ ["AG YMP 14.05.26", "YMP"],
107
+ ["AGENDADA POR SLR 02/12/25", "SLR"],
108
+ ["AGENDADO POR SCP", "SCP"],
109
+ ];
110
+ for (const [text, expected] of cases) {
111
+ const match = text.match(google_calendar_constants_1.SCHEDULER_REGEX);
112
+ expect(match?.[1]?.toUpperCase()).toBe(expected);
113
+ }
114
+ });
115
+ });
@@ -28,3 +28,4 @@ export * from './settlement.enums';
28
28
  export * from './client-billing.enums';
29
29
  export * from './sat-income-invoice';
30
30
  export * from './global-invoice.enums';
31
+ export * from './google-calendar.constants';
@@ -44,3 +44,4 @@ __exportStar(require("./settlement.enums"), exports);
44
44
  __exportStar(require("./client-billing.enums"), exports);
45
45
  __exportStar(require("./sat-income-invoice"), exports);
46
46
  __exportStar(require("./global-invoice.enums"), exports);
47
+ __exportStar(require("./google-calendar.constants"), exports);
@@ -240,3 +240,26 @@ export interface SearchDocumentsQuery {
240
240
  q: string;
241
241
  limit?: number;
242
242
  }
243
+ /**
244
+ * Compliance Matrix Query
245
+ *
246
+ * Filters for the admin/manager `GET /api/documents/acknowledgments/matrix`
247
+ * endpoint. All optional. The endpoint always restricts to docs that
248
+ * `requiresAcknowledgment === true` AND `status === "current"` — those are
249
+ * the only docs an ack is meaningful on.
250
+ *
251
+ * @example GET /api/documents/acknowledgments/matrix?category=operational
252
+ */
253
+ export interface DocumentComplianceMatrixQuery {
254
+ /** Restrict rows to these doc IDs. */
255
+ documentIds?: string[];
256
+ /** Restrict columns to these collaborator IDs. */
257
+ collaboratorIds?: string[];
258
+ /** Filter rows by document category. */
259
+ category?: Category;
260
+ /**
261
+ * Filter rows whose audience includes this role (or `"all"`). Lets admin
262
+ * scope the matrix to docs everyone has to read, or only managers, etc.
263
+ */
264
+ audienceRole?: DocumentAudienceEntry;
265
+ }
@@ -6,6 +6,7 @@
6
6
  * - Use Public → View → Admin inheritance where it adds value.
7
7
  */
8
8
  import { Category, ChangeType, ContentType, Criticality, DocumentStatus, DocumentType, ExternalLinkProvider } from "../../constants/document.enums";
9
+ import { WebAppRole } from "../../constants/collaborator.constants";
9
10
  import { DocumentAudienceEntry } from "./requests";
10
11
  /**
11
12
  * Compact reference to a document's current version, embedded in
@@ -160,3 +161,74 @@ export interface PendingAcknowledgmentResponse {
160
161
  pendingDays: number;
161
162
  isFromOnboarding: boolean;
162
163
  }
164
+ /**
165
+ * Compliance Matrix Cell
166
+ *
167
+ * Per `(document, collaborator)` pair, the read state.
168
+ *
169
+ * - `acked` — collaborator has a valid ack on the doc's current version
170
+ * (covers patch chains; major/minor invalidates).
171
+ * - `pending` — collaborator is in the doc's audience and has NOT acked.
172
+ * - `not_applicable` — collaborator's role is outside the doc's audience.
173
+ */
174
+ export type DocumentComplianceCellStatus = "acked" | "pending" | "not_applicable";
175
+ export interface DocumentComplianceCell {
176
+ collaboratorId: string;
177
+ status: DocumentComplianceCellStatus;
178
+ /** Present when `status === "acked"`. ISO 8601. */
179
+ acknowledgedAt?: string;
180
+ }
181
+ /**
182
+ * Compliance Matrix Collaborator (column header)
183
+ *
184
+ * Compact info needed to render a column header. Order of `collaborators[]`
185
+ * in the response matches order of `cells[]` on each row.
186
+ */
187
+ export interface DocumentComplianceCollaborator {
188
+ id: string;
189
+ col_code: string;
190
+ fullName: string;
191
+ role: WebAppRole;
192
+ }
193
+ /**
194
+ * Compliance Matrix Row (one document)
195
+ */
196
+ export interface DocumentComplianceRow {
197
+ documentId: string;
198
+ documentSlug: string;
199
+ documentTitle: string;
200
+ category: Category;
201
+ criticality: Criticality;
202
+ audience: DocumentAudienceEntry[];
203
+ currentVersionId: string | null;
204
+ /** Semver string of the current version, e.g. "1.2.0". */
205
+ versionNumber: string | null;
206
+ /** Number of collaborators in the matrix that the audience applies to. */
207
+ totalApplicable: number;
208
+ /** Of those, how many have a valid ack on the current version. */
209
+ totalAcked: number;
210
+ /** `totalAcked / totalApplicable * 100`, rounded to 0 decimals. 0 when N/A. */
211
+ pctRead: number;
212
+ /** Index-aligned with the response's `collaborators[]`. */
213
+ cells: DocumentComplianceCell[];
214
+ }
215
+ /**
216
+ * Document Compliance Matrix Response
217
+ *
218
+ * Admin/manager view: matrix of "who has read what". Rows are documents,
219
+ * columns are collaborators.
220
+ *
221
+ * Used for: `GET /api/documents/acknowledgments/matrix`
222
+ */
223
+ export interface DocumentComplianceMatrixResponse {
224
+ collaborators: DocumentComplianceCollaborator[];
225
+ rows: DocumentComplianceRow[];
226
+ /** Total docs in the response (rows.length, surfaced for KPI). */
227
+ totalDocs: number;
228
+ /** Total applicable doc-collaborator pairs across all rows. */
229
+ totalApplicablePairs: number;
230
+ /** Of those, how many are acked. */
231
+ totalAckedPairs: number;
232
+ /** Average of pctRead across rows (or `totalAckedPairs / totalApplicablePairs * 100`). */
233
+ pctReadOverall: number;
234
+ }
@@ -0,0 +1,2 @@
1
+ export * from './requests';
2
+ export * from './responses';
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./requests"), exports);
18
+ __exportStar(require("./responses"), exports);
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Google Calendar Integration - Request Types
3
+ *
4
+ * Request contracts for the Google Calendar diagnostic + appointment management feature.
5
+ * See: resources/notes/google-calendar/standard-v1.md
6
+ */
7
+ /**
8
+ * Diagnose request — score appointments against the standard for a date window.
9
+ *
10
+ * @example GET /api/google-calendar/diagnose?from=2026-04-01&to=2026-05-01
11
+ */
12
+ export interface DiagnoseAppointmentsRequest {
13
+ /** ISO date (inclusive). Start of the window in Mexico_City timezone. */
14
+ from: string;
15
+ /** ISO date (inclusive). End of the window in Mexico_City timezone. */
16
+ to: string;
17
+ /** Filter results to a single branch. Optional. */
18
+ branchId?: 'urban' | 'harbor' | 'montejo';
19
+ /** Filter to events where this col_code is detected as the attending vet. Optional. */
20
+ vetColCode?: string;
21
+ }
22
+ /**
23
+ * Single-event diagnose request.
24
+ *
25
+ * @example GET /api/google-calendar/diagnose/event/:eventId
26
+ */
27
+ export interface DiagnoseEventRequest {
28
+ eventId: string;
29
+ }
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ /**
3
+ * Google Calendar Integration - Request Types
4
+ *
5
+ * Request contracts for the Google Calendar diagnostic + appointment management feature.
6
+ * See: resources/notes/google-calendar/standard-v1.md
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Google Calendar Integration - Response Types
3
+ *
4
+ * Response contracts for the Google Calendar diagnostic feature.
5
+ * See: resources/notes/google-calendar/standard-v1.md
6
+ */
7
+ /**
8
+ * One of the six dimensions of the appointment standard.
9
+ */
10
+ export type AppointmentStandardDimensionId = 'branch' | 'service' | 'pet' | 'client' | 'context' | 'vet';
11
+ /**
12
+ * One of the three branches.
13
+ */
14
+ export type AppointmentBranch = 'urban' | 'harbor' | 'montejo';
15
+ /**
16
+ * Definition of the multi-factor scoring standard used by the diagnostic engine.
17
+ *
18
+ * Defined in code as a constant (see constants/google-calendar.constants.ts).
19
+ * Sent to the frontend so the dashboard can render dimension names/weights without
20
+ * hardcoding.
21
+ */
22
+ export interface AppointmentStandard {
23
+ /** Semver-like version identifier (e.g. "1.0"). */
24
+ version: string;
25
+ /** Dimensions and their weights (must sum to 100). */
26
+ dimensions: ReadonlyArray<{
27
+ id: AppointmentStandardDimensionId;
28
+ weight: number;
29
+ /** Display label in Spanish. */
30
+ label: string;
31
+ }>;
32
+ /** Threshold above which an event is considered "compliant". */
33
+ minimumCompliantScore: number;
34
+ }
35
+ /**
36
+ * A single dimension result for one event.
37
+ */
38
+ export interface DiagnosticDimensionResult {
39
+ id: AppointmentStandardDimensionId;
40
+ passed: boolean;
41
+ weight: number;
42
+ /** Human-readable evidence ("Q966648 in description", "phone 999... matched", etc.) or null when missing. */
43
+ evidence: string | null;
44
+ }
45
+ /**
46
+ * Reason an event was excluded from scoring.
47
+ */
48
+ export type DiagnosticExclusionReason = 'cancelled' | 'blocker' | 'special' | 'operational' | 'interview' | 'reminder';
49
+ /**
50
+ * Full diagnostic result for one event.
51
+ */
52
+ export interface DiagnosticResult {
53
+ eventId: string;
54
+ /** ISO datetime in Mexico_City. */
55
+ eventStart: string;
56
+ /** Duration in minutes. */
57
+ durationMinutes: number;
58
+ /** Title as it appears in GCal. */
59
+ title: string;
60
+ /** htmlLink to the event in Google Calendar. */
61
+ htmlLink: string | null;
62
+ /** Score 0-100. */
63
+ score: number;
64
+ /** Score >= minimumCompliantScore. */
65
+ isCompliant: boolean;
66
+ /** Per-dimension results. */
67
+ dimensions: DiagnosticDimensionResult[];
68
+ /** Inferred branch from colorId or extendedProperties. Null if undetected. */
69
+ inferredBranch: AppointmentBranch | null;
70
+ /** Inferred canonical service code (e.g. "CONSULTA"). Null if undetected. */
71
+ inferredService: string | null;
72
+ /** Inferred pet name from the title. */
73
+ inferredPetName: string | null;
74
+ /** col_code of the attending vet, when detectable. */
75
+ inferredVetColCode: string | null;
76
+ /** col_code of the person who scheduled, when detectable in description. */
77
+ inferredSchedulerColCode: string | null;
78
+ /** Q{id} or null. */
79
+ inferredQvetClientId: string | null;
80
+ /** Last 10 digits of phone, normalized. Null if not found. */
81
+ inferredPhone: string | null;
82
+ /** Asterisk at end of title => preferred vet => generates commission. */
83
+ isPreferredVet: boolean;
84
+ /** True iff event has extendedProperties.private (HVP-created). */
85
+ isHvpCreated: boolean;
86
+ }
87
+ /**
88
+ * Excluded event — does not have a score.
89
+ */
90
+ export interface DiagnosticExcludedEvent {
91
+ eventId: string;
92
+ eventStart: string;
93
+ title: string;
94
+ reason: DiagnosticExclusionReason;
95
+ }
96
+ /**
97
+ * Aggregated summary of diagnostic results for a window.
98
+ *
99
+ * @example GET /api/google-calendar/diagnose
100
+ */
101
+ export interface DiagnoseAppointmentsResponse {
102
+ /** Window inputs echoed back. */
103
+ window: {
104
+ from: string;
105
+ to: string;
106
+ };
107
+ /** Standard used for scoring (so frontend can render labels/weights). */
108
+ standard: AppointmentStandard;
109
+ /** Counts. */
110
+ totalFetched: number;
111
+ totalEvaluated: number;
112
+ totalExcluded: number;
113
+ totalCancelled: number;
114
+ /** totalEvaluated where score >= minimumCompliantScore. */
115
+ compliantCount: number;
116
+ /** Average score across evaluated events (0-100). */
117
+ averageScore: number;
118
+ /** Per-dimension breakdown: how many evaluated events passed each dimension. */
119
+ byDimension: Record<AppointmentStandardDimensionId, {
120
+ passed: number;
121
+ total: number;
122
+ }>;
123
+ /** Per-branch breakdown. */
124
+ byBranch: Record<AppointmentBranch | 'unknown', {
125
+ count: number;
126
+ avgScore: number;
127
+ }>;
128
+ /** Per-vet breakdown (top 20 by count). col_code => { totalCount, preferredCount }. */
129
+ byVet: Record<string, {
130
+ count: number;
131
+ preferredCount: number;
132
+ avgScore: number;
133
+ }>;
134
+ /** Detailed results per evaluated event (paginated by caller — capped at 500 in response for now). */
135
+ results: DiagnosticResult[];
136
+ /** Detailed excluded events (capped at 100). */
137
+ excluded: DiagnosticExcludedEvent[];
138
+ }
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ /**
3
+ * Google Calendar Integration - Response Types
4
+ *
5
+ * Response contracts for the Google Calendar diagnostic feature.
6
+ * See: resources/notes/google-calendar/standard-v1.md
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -16,6 +16,7 @@ export * from './client-billing';
16
16
  export * from './global-invoice';
17
17
  export * from './inventory-report';
18
18
  export * from './google-contacts';
19
+ export * from './google-calendar';
19
20
  export * from './study-type-catalog';
20
21
  export * from './supplier-overlay';
21
22
  export * from './external-study';
@@ -32,6 +32,7 @@ __exportStar(require("./client-billing"), exports);
32
32
  __exportStar(require("./global-invoice"), exports);
33
33
  __exportStar(require("./inventory-report"), exports);
34
34
  __exportStar(require("./google-contacts"), exports);
35
+ __exportStar(require("./google-calendar"), exports);
35
36
  __exportStar(require("./study-type-catalog"), exports);
36
37
  __exportStar(require("./supplier-overlay"), exports);
37
38
  __exportStar(require("./external-study"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hvp-shared",
3
- "version": "13.1.0",
3
+ "version": "13.3.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",