@blackcode_sa/metaestetics-api 1.13.5 → 1.13.8

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.
Files changed (295) hide show
  1. package/dist/admin/index.d.mts +20 -1
  2. package/dist/admin/index.d.ts +20 -1
  3. package/dist/admin/index.js +217 -1
  4. package/dist/admin/index.mjs +217 -1
  5. package/dist/index.d.mts +26 -3
  6. package/dist/index.d.ts +26 -3
  7. package/dist/index.js +168 -6
  8. package/dist/index.mjs +168 -6
  9. package/package.json +121 -121
  10. package/src/__mocks__/firstore.ts +10 -10
  11. package/src/admin/aggregation/README.md +79 -79
  12. package/src/admin/aggregation/appointment/README.md +128 -128
  13. package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1984 -1984
  14. package/src/admin/aggregation/appointment/index.ts +1 -1
  15. package/src/admin/aggregation/clinic/README.md +52 -52
  16. package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +966 -703
  17. package/src/admin/aggregation/clinic/index.ts +1 -1
  18. package/src/admin/aggregation/forms/README.md +13 -13
  19. package/src/admin/aggregation/forms/filled-forms.aggregation.service.ts +322 -322
  20. package/src/admin/aggregation/forms/index.ts +1 -1
  21. package/src/admin/aggregation/index.ts +8 -8
  22. package/src/admin/aggregation/patient/README.md +27 -27
  23. package/src/admin/aggregation/patient/index.ts +1 -1
  24. package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -141
  25. package/src/admin/aggregation/practitioner/README.md +42 -42
  26. package/src/admin/aggregation/practitioner/index.ts +1 -1
  27. package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -433
  28. package/src/admin/aggregation/practitioner-invite/index.ts +1 -1
  29. package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts +961 -961
  30. package/src/admin/aggregation/procedure/README.md +43 -43
  31. package/src/admin/aggregation/procedure/index.ts +1 -1
  32. package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +702 -702
  33. package/src/admin/aggregation/reviews/index.ts +1 -1
  34. package/src/admin/aggregation/reviews/reviews.aggregation.service.ts +689 -689
  35. package/src/admin/analytics/analytics.admin.service.ts +278 -278
  36. package/src/admin/analytics/index.ts +2 -2
  37. package/src/admin/booking/README.md +125 -125
  38. package/src/admin/booking/booking.admin.ts +1037 -1037
  39. package/src/admin/booking/booking.calculator.ts +712 -712
  40. package/src/admin/booking/booking.types.ts +59 -59
  41. package/src/admin/booking/index.ts +3 -3
  42. package/src/admin/booking/timezones-problem.md +185 -185
  43. package/src/admin/calendar/README.md +7 -7
  44. package/src/admin/calendar/calendar.admin.service.ts +345 -345
  45. package/src/admin/calendar/index.ts +1 -1
  46. package/src/admin/documentation-templates/document-manager.admin.ts +260 -260
  47. package/src/admin/documentation-templates/index.ts +1 -1
  48. package/src/admin/free-consultation/free-consultation-utils.admin.ts +148 -148
  49. package/src/admin/free-consultation/index.ts +1 -1
  50. package/src/admin/index.ts +81 -81
  51. package/src/admin/logger/index.ts +78 -78
  52. package/src/admin/mailing/README.md +95 -95
  53. package/src/admin/mailing/appointment/appointment.mailing.service.ts +732 -732
  54. package/src/admin/mailing/appointment/index.ts +1 -1
  55. package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -40
  56. package/src/admin/mailing/base.mailing.service.ts +208 -208
  57. package/src/admin/mailing/index.ts +3 -3
  58. package/src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts +611 -611
  59. package/src/admin/mailing/practitionerInvite/index.ts +2 -2
  60. package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +395 -395
  61. package/src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts +155 -155
  62. package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -101
  63. package/src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts +228 -228
  64. package/src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts +242 -242
  65. package/src/admin/notifications/index.ts +1 -1
  66. package/src/admin/notifications/notifications.admin.ts +710 -710
  67. package/src/admin/requirements/README.md +128 -128
  68. package/src/admin/requirements/index.ts +1 -1
  69. package/src/admin/requirements/patient-requirements.admin.service.ts +475 -475
  70. package/src/admin/users/index.ts +1 -1
  71. package/src/admin/users/user-profile.admin.ts +405 -405
  72. package/src/backoffice/constants/certification.constants.ts +13 -13
  73. package/src/backoffice/constants/index.ts +1 -1
  74. package/src/backoffice/errors/backoffice.errors.ts +181 -181
  75. package/src/backoffice/errors/index.ts +1 -1
  76. package/src/backoffice/expo-safe/README.md +26 -26
  77. package/src/backoffice/expo-safe/index.ts +41 -41
  78. package/src/backoffice/index.ts +5 -5
  79. package/src/backoffice/services/FIXES_README.md +102 -102
  80. package/src/backoffice/services/README.md +57 -57
  81. package/src/backoffice/services/analytics.service.proposal.md +863 -863
  82. package/src/backoffice/services/analytics.service.summary.md +143 -143
  83. package/src/backoffice/services/brand.service.ts +256 -256
  84. package/src/backoffice/services/category.service.ts +384 -384
  85. package/src/backoffice/services/constants.service.ts +385 -385
  86. package/src/backoffice/services/documentation-template.service.ts +202 -202
  87. package/src/backoffice/services/index.ts +10 -10
  88. package/src/backoffice/services/migrate-products.ts +116 -116
  89. package/src/backoffice/services/product.service.ts +553 -553
  90. package/src/backoffice/services/requirement.service.ts +235 -235
  91. package/src/backoffice/services/subcategory.service.ts +461 -461
  92. package/src/backoffice/services/technology.service.ts +1151 -1151
  93. package/src/backoffice/types/README.md +12 -12
  94. package/src/backoffice/types/admin-constants.types.ts +69 -69
  95. package/src/backoffice/types/brand.types.ts +29 -29
  96. package/src/backoffice/types/category.types.ts +67 -67
  97. package/src/backoffice/types/documentation-templates.types.ts +28 -28
  98. package/src/backoffice/types/index.ts +10 -10
  99. package/src/backoffice/types/procedure-product.types.ts +38 -38
  100. package/src/backoffice/types/product.types.ts +240 -240
  101. package/src/backoffice/types/requirement.types.ts +63 -63
  102. package/src/backoffice/types/static/README.md +18 -18
  103. package/src/backoffice/types/static/blocking-condition.types.ts +21 -21
  104. package/src/backoffice/types/static/certification.types.ts +37 -37
  105. package/src/backoffice/types/static/contraindication.types.ts +19 -19
  106. package/src/backoffice/types/static/index.ts +6 -6
  107. package/src/backoffice/types/static/pricing.types.ts +16 -16
  108. package/src/backoffice/types/static/procedure-family.types.ts +14 -14
  109. package/src/backoffice/types/static/treatment-benefit.types.ts +22 -22
  110. package/src/backoffice/types/subcategory.types.ts +34 -34
  111. package/src/backoffice/types/technology.types.ts +168 -168
  112. package/src/backoffice/validations/index.ts +1 -1
  113. package/src/backoffice/validations/schemas.ts +164 -164
  114. package/src/config/__mocks__/firebase.ts +99 -99
  115. package/src/config/firebase.ts +78 -78
  116. package/src/config/index.ts +9 -9
  117. package/src/errors/auth.error.ts +6 -6
  118. package/src/errors/auth.errors.ts +211 -200
  119. package/src/errors/clinic.errors.ts +32 -32
  120. package/src/errors/firebase.errors.ts +47 -47
  121. package/src/errors/user.errors.ts +99 -99
  122. package/src/index.backup.ts +407 -407
  123. package/src/index.ts +6 -6
  124. package/src/locales/en.ts +31 -31
  125. package/src/recommender/admin/index.ts +1 -1
  126. package/src/recommender/admin/services/recommender.service.admin.ts +5 -5
  127. package/src/recommender/front/index.ts +1 -1
  128. package/src/recommender/front/services/onboarding.service.ts +5 -5
  129. package/src/recommender/front/services/recommender.service.ts +3 -3
  130. package/src/recommender/index.ts +1 -1
  131. package/src/services/PATIENTAUTH.MD +197 -197
  132. package/src/services/README.md +106 -106
  133. package/src/services/__tests__/auth/auth.mock.test.ts +17 -17
  134. package/src/services/__tests__/auth/auth.setup.ts +293 -293
  135. package/src/services/__tests__/auth.service.test.ts +346 -346
  136. package/src/services/__tests__/base.service.test.ts +77 -77
  137. package/src/services/__tests__/user.service.test.ts +528 -528
  138. package/src/services/analytics/ARCHITECTURE.md +199 -199
  139. package/src/services/analytics/CLOUD_FUNCTIONS.md +225 -225
  140. package/src/services/analytics/GROUPED_ANALYTICS.md +501 -501
  141. package/src/services/analytics/QUICK_START.md +393 -393
  142. package/src/services/analytics/README.md +304 -304
  143. package/src/services/analytics/SUMMARY.md +141 -141
  144. package/src/services/analytics/TRENDS.md +380 -380
  145. package/src/services/analytics/USAGE_GUIDE.md +518 -518
  146. package/src/services/analytics/analytics-cloud.service.ts +222 -222
  147. package/src/services/analytics/analytics.service.ts +2142 -2142
  148. package/src/services/analytics/index.ts +4 -4
  149. package/src/services/analytics/review-analytics.service.ts +941 -941
  150. package/src/services/analytics/utils/appointment-filtering.utils.ts +138 -138
  151. package/src/services/analytics/utils/cost-calculation.utils.ts +182 -182
  152. package/src/services/analytics/utils/grouping.utils.ts +434 -434
  153. package/src/services/analytics/utils/stored-analytics.utils.ts +347 -347
  154. package/src/services/analytics/utils/time-calculation.utils.ts +186 -186
  155. package/src/services/analytics/utils/trend-calculation.utils.ts +200 -200
  156. package/src/services/appointment/README.md +17 -17
  157. package/src/services/appointment/appointment.service.ts +2558 -2558
  158. package/src/services/appointment/index.ts +1 -1
  159. package/src/services/appointment/utils/appointment.utils.ts +552 -552
  160. package/src/services/appointment/utils/extended-procedure.utils.ts +314 -314
  161. package/src/services/appointment/utils/form-initialization.utils.ts +225 -225
  162. package/src/services/appointment/utils/recommended-procedure.utils.ts +195 -195
  163. package/src/services/appointment/utils/zone-management.utils.ts +353 -353
  164. package/src/services/appointment/utils/zone-photo.utils.ts +152 -152
  165. package/src/services/auth/auth.service.ts +1043 -989
  166. package/src/services/auth/auth.v2.service.ts +961 -961
  167. package/src/services/auth/index.ts +7 -7
  168. package/src/services/auth/utils/error.utils.ts +90 -90
  169. package/src/services/auth/utils/firebase.utils.ts +49 -49
  170. package/src/services/auth/utils/index.ts +21 -21
  171. package/src/services/auth/utils/practitioner.utils.ts +125 -125
  172. package/src/services/base.service.ts +41 -41
  173. package/src/services/calendar/calendar.service.ts +1077 -1077
  174. package/src/services/calendar/calendar.v2.service.ts +1683 -1683
  175. package/src/services/calendar/calendar.v3.service.ts +313 -313
  176. package/src/services/calendar/externalCalendar.service.ts +178 -178
  177. package/src/services/calendar/index.ts +5 -5
  178. package/src/services/calendar/synced-calendars.service.ts +743 -743
  179. package/src/services/calendar/utils/appointment.utils.ts +265 -265
  180. package/src/services/calendar/utils/calendar-event.utils.ts +646 -646
  181. package/src/services/calendar/utils/clinic.utils.ts +237 -237
  182. package/src/services/calendar/utils/docs.utils.ts +157 -157
  183. package/src/services/calendar/utils/google-calendar.utils.ts +697 -697
  184. package/src/services/calendar/utils/index.ts +8 -8
  185. package/src/services/calendar/utils/patient.utils.ts +198 -198
  186. package/src/services/calendar/utils/practitioner.utils.ts +221 -221
  187. package/src/services/calendar/utils/synced-calendar.utils.ts +472 -472
  188. package/src/services/clinic/README.md +204 -204
  189. package/src/services/clinic/__tests__/clinic-admin.service.test.ts +287 -287
  190. package/src/services/clinic/__tests__/clinic-group.service.test.ts +352 -352
  191. package/src/services/clinic/__tests__/clinic.service.test.ts +354 -354
  192. package/src/services/clinic/billing-transactions.service.ts +217 -217
  193. package/src/services/clinic/clinic-admin.service.ts +202 -202
  194. package/src/services/clinic/clinic-group.service.ts +310 -310
  195. package/src/services/clinic/clinic.service.ts +708 -708
  196. package/src/services/clinic/index.ts +5 -5
  197. package/src/services/clinic/practitioner-invite.service.ts +519 -519
  198. package/src/services/clinic/utils/admin.utils.ts +551 -551
  199. package/src/services/clinic/utils/clinic-group.utils.ts +646 -646
  200. package/src/services/clinic/utils/clinic.utils.ts +949 -949
  201. package/src/services/clinic/utils/filter.utils.d.ts +23 -23
  202. package/src/services/clinic/utils/filter.utils.ts +446 -446
  203. package/src/services/clinic/utils/index.ts +11 -11
  204. package/src/services/clinic/utils/photos.utils.ts +188 -188
  205. package/src/services/clinic/utils/search.utils.ts +84 -84
  206. package/src/services/clinic/utils/tag.utils.ts +124 -124
  207. package/src/services/documentation-templates/documentation-template.service.ts +537 -537
  208. package/src/services/documentation-templates/filled-document.service.ts +587 -587
  209. package/src/services/documentation-templates/index.ts +2 -2
  210. package/src/services/index.ts +14 -14
  211. package/src/services/media/index.ts +1 -1
  212. package/src/services/media/media.service.ts +418 -418
  213. package/src/services/notifications/__tests__/notification.service.test.ts +242 -242
  214. package/src/services/notifications/index.ts +1 -1
  215. package/src/services/notifications/notification.service.ts +215 -215
  216. package/src/services/patient/README.md +48 -48
  217. package/src/services/patient/To-Do.md +43 -43
  218. package/src/services/patient/__tests__/patient.service.test.ts +294 -294
  219. package/src/services/patient/index.ts +2 -2
  220. package/src/services/patient/patient.service.ts +883 -883
  221. package/src/services/patient/patientRequirements.service.ts +285 -285
  222. package/src/services/patient/utils/aesthetic-analysis.utils.ts +176 -176
  223. package/src/services/patient/utils/clinic.utils.ts +80 -80
  224. package/src/services/patient/utils/docs.utils.ts +142 -142
  225. package/src/services/patient/utils/index.ts +9 -9
  226. package/src/services/patient/utils/location.utils.ts +126 -126
  227. package/src/services/patient/utils/medical-stuff.utils.ts +143 -143
  228. package/src/services/patient/utils/medical.utils.ts +458 -458
  229. package/src/services/patient/utils/practitioner.utils.ts +260 -260
  230. package/src/services/patient/utils/profile.utils.ts +510 -510
  231. package/src/services/patient/utils/sensitive.utils.ts +260 -260
  232. package/src/services/patient/utils/token.utils.ts +211 -211
  233. package/src/services/practitioner/README.md +145 -145
  234. package/src/services/practitioner/index.ts +1 -1
  235. package/src/services/practitioner/practitioner.service.ts +1799 -1742
  236. package/src/services/procedure/README.md +163 -163
  237. package/src/services/procedure/index.ts +1 -1
  238. package/src/services/procedure/procedure.service.ts +2307 -2200
  239. package/src/services/reviews/index.ts +1 -1
  240. package/src/services/reviews/reviews.service.ts +734 -734
  241. package/src/services/user/index.ts +1 -1
  242. package/src/services/user/user.service.ts +489 -489
  243. package/src/services/user/user.v2.service.ts +466 -466
  244. package/src/types/analytics/analytics.types.ts +597 -597
  245. package/src/types/analytics/grouped-analytics.types.ts +173 -173
  246. package/src/types/analytics/index.ts +4 -4
  247. package/src/types/analytics/stored-analytics.types.ts +137 -137
  248. package/src/types/appointment/index.ts +480 -480
  249. package/src/types/calendar/index.ts +258 -258
  250. package/src/types/calendar/synced-calendar.types.ts +66 -66
  251. package/src/types/clinic/index.ts +498 -498
  252. package/src/types/clinic/practitioner-invite.types.ts +91 -91
  253. package/src/types/clinic/preferences.types.ts +159 -159
  254. package/src/types/clinic/to-do +3 -3
  255. package/src/types/documentation-templates/index.ts +308 -308
  256. package/src/types/index.ts +47 -47
  257. package/src/types/notifications/README.md +77 -77
  258. package/src/types/notifications/index.ts +286 -286
  259. package/src/types/patient/aesthetic-analysis.types.ts +66 -66
  260. package/src/types/patient/allergies.ts +58 -58
  261. package/src/types/patient/index.ts +275 -275
  262. package/src/types/patient/medical-info.types.ts +152 -152
  263. package/src/types/patient/patient-requirements.ts +92 -92
  264. package/src/types/patient/token.types.ts +61 -61
  265. package/src/types/practitioner/index.ts +206 -206
  266. package/src/types/procedure/index.ts +181 -181
  267. package/src/types/profile/index.ts +39 -39
  268. package/src/types/reviews/index.ts +132 -132
  269. package/src/types/tz-lookup.d.ts +4 -4
  270. package/src/types/user/index.ts +38 -38
  271. package/src/utils/TIMESTAMPS.md +176 -176
  272. package/src/utils/TimestampUtils.ts +241 -241
  273. package/src/utils/index.ts +1 -1
  274. package/src/validations/appointment.schema.ts +574 -574
  275. package/src/validations/calendar.schema.ts +225 -225
  276. package/src/validations/clinic.schema.ts +494 -494
  277. package/src/validations/common.schema.ts +25 -25
  278. package/src/validations/documentation-templates/index.ts +1 -1
  279. package/src/validations/documentation-templates/template.schema.ts +220 -220
  280. package/src/validations/documentation-templates.schema.ts +10 -10
  281. package/src/validations/index.ts +20 -20
  282. package/src/validations/media.schema.ts +10 -10
  283. package/src/validations/notification.schema.ts +90 -90
  284. package/src/validations/patient/aesthetic-analysis.schema.ts +55 -55
  285. package/src/validations/patient/medical-info.schema.ts +125 -125
  286. package/src/validations/patient/patient-requirements.schema.ts +84 -84
  287. package/src/validations/patient/token.schema.ts +29 -29
  288. package/src/validations/patient.schema.ts +217 -217
  289. package/src/validations/practitioner.schema.ts +222 -222
  290. package/src/validations/procedure-product.schema.ts +41 -41
  291. package/src/validations/procedure.schema.ts +124 -124
  292. package/src/validations/profile-info.schema.ts +41 -41
  293. package/src/validations/reviews.schema.ts +195 -195
  294. package/src/validations/schemas.ts +104 -104
  295. package/src/validations/shared.schema.ts +78 -78
@@ -1,697 +1,697 @@
1
- /// <reference types="axios" />
2
- import { Firestore, Timestamp } from "firebase/firestore";
3
- import axios from "axios";
4
- import {
5
- SyncedCalendar,
6
- SyncedCalendarProvider,
7
- UpdateSyncedCalendarData,
8
- } from "../../../types/calendar/synced-calendar.types";
9
- import {
10
- CalendarEvent,
11
- CalendarEventStatus,
12
- CalendarEventType,
13
- CalendarSyncStatus,
14
- } from "../../../types/calendar";
15
- import {
16
- updateLastSyncedTimestampUtil,
17
- updatePractitionerSyncedCalendarUtil,
18
- updatePatientSyncedCalendarUtil,
19
- updateClinicSyncedCalendarUtil,
20
- } from "./synced-calendar.utils";
21
-
22
- // API URL and client configuration - these should be environment variables in a real application
23
- export const GOOGLE_CALENDAR_API_URL = "https://www.googleapis.com/calendar/v3";
24
- const GOOGLE_OAUTH_URL = "https://oauth2.googleapis.com/token";
25
- const GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/auth";
26
- const CLIENT_ID = "your-client-id"; // Replace with your actual client ID
27
- const CLIENT_SECRET = "your-client-secret"; // Replace with your actual client secret
28
- const REDIRECT_URI = "your-redirect-uri"; // Replace with your actual redirect URI
29
-
30
- // Error interface for better error handling
31
- interface ApiError {
32
- message: string;
33
- status?: number;
34
- response?: {
35
- data?: any;
36
- status?: number;
37
- };
38
- }
39
-
40
- /**
41
- * Helper function for making API requests
42
- * @param method - HTTP method
43
- * @param url - Request URL
44
- * @param headers - Request headers
45
- * @param data - Request body data
46
- * @param params - URL query parameters
47
- * @returns Response data
48
- */
49
- export async function makeRequest(
50
- method: string,
51
- url: string,
52
- headers: Record<string, string>,
53
- data?: any,
54
- params?: Record<string, string>
55
- ): Promise<any> {
56
- // Construct URL with query parameters if provided
57
- const queryParams = params
58
- ? "?" +
59
- Object.entries(params)
60
- .map(
61
- ([key, value]) =>
62
- `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
63
- )
64
- .join("&")
65
- : "";
66
-
67
- const finalUrl = url + queryParams;
68
-
69
- // Example implementation with fetch
70
- const options: RequestInit = {
71
- method,
72
- headers,
73
- body: data ? JSON.stringify(data) : undefined,
74
- };
75
-
76
- const response = await fetch(finalUrl, options);
77
-
78
- if (!response.ok) {
79
- const error: any = new Error(
80
- `Request failed with status ${response.status}`
81
- );
82
- error.response = response;
83
- throw error;
84
- }
85
-
86
- return response.json();
87
- }
88
-
89
- /**
90
- * Authenticates with Google Calendar API
91
- * @param authCode - Authorization code from Google OAuth
92
- * @returns Access token and refresh token
93
- */
94
- export async function authenticateWithGoogleCalendarUtil(
95
- authCode: string
96
- ): Promise<{ accessToken: string; refreshToken: string; expiresIn: number }> {
97
- try {
98
- // Exchange authorization code for tokens
99
- const data = {
100
- code: authCode,
101
- client_id: CLIENT_ID,
102
- client_secret: CLIENT_SECRET,
103
- redirect_uri: REDIRECT_URI,
104
- grant_type: "authorization_code",
105
- };
106
-
107
- const response = await makeRequest(
108
- "post",
109
- GOOGLE_OAUTH_URL,
110
- { "Content-Type": "application/json" },
111
- data
112
- );
113
-
114
- return {
115
- accessToken: response.access_token,
116
- refreshToken: response.refresh_token,
117
- expiresIn: response.expires_in,
118
- };
119
- } catch (error) {
120
- const apiError = error as ApiError;
121
- console.error(
122
- "Error authenticating with Google Calendar:",
123
- apiError.message || "Unknown error"
124
- );
125
- throw new Error(
126
- `Failed to authenticate with Google Calendar: ${
127
- apiError.message || "Unknown error"
128
- }`
129
- );
130
- }
131
- }
132
-
133
- /**
134
- * Refreshes the Google Calendar API access token
135
- * @param refreshToken - Refresh token
136
- * @returns New access token and expiry
137
- */
138
- export async function refreshGoogleCalendarTokenUtil(
139
- refreshToken: string
140
- ): Promise<{ accessToken: string; expiresIn: number }> {
141
- try {
142
- // Use refresh token to get a new access token
143
- const data = {
144
- refresh_token: refreshToken,
145
- client_id: CLIENT_ID,
146
- client_secret: CLIENT_SECRET,
147
- grant_type: "refresh_token",
148
- };
149
-
150
- const response = await makeRequest(
151
- "post",
152
- GOOGLE_OAUTH_URL,
153
- { "Content-Type": "application/json" },
154
- data
155
- );
156
-
157
- return {
158
- accessToken: response.access_token,
159
- expiresIn: response.expires_in,
160
- };
161
- } catch (error) {
162
- const apiError = error as ApiError;
163
- console.error(
164
- "Error refreshing Google Calendar token:",
165
- apiError.message || "Unknown error"
166
- );
167
- throw new Error(
168
- `Failed to refresh Google Calendar token: ${
169
- apiError.message || "Unknown error"
170
- }`
171
- );
172
- }
173
- }
174
-
175
- /**
176
- * Lists available Google Calendars for a user
177
- * @param accessToken - Google API access token
178
- * @returns List of available calendars
179
- */
180
- export async function listGoogleCalendarsUtil(
181
- accessToken: string
182
- ): Promise<Array<{ id: string; name: string }>> {
183
- try {
184
- // Call Google Calendar API to list calendars
185
- const response = await makeRequest(
186
- "get",
187
- `${GOOGLE_CALENDAR_API_URL}/users/me/calendarList`,
188
- { Authorization: `Bearer ${accessToken}` }
189
- );
190
-
191
- // Map the response to our format
192
- return response.items.map((calendar: any) => ({
193
- id: calendar.id,
194
- name: calendar.summary,
195
- }));
196
- } catch (error) {
197
- const apiError = error as ApiError;
198
- console.error(
199
- "Error listing Google Calendars:",
200
- apiError.message || "Unknown error"
201
- );
202
- throw new Error(
203
- `Failed to list Google Calendars: ${apiError.message || "Unknown error"}`
204
- );
205
- }
206
- }
207
-
208
- /**
209
- * Ensures the synced calendar token is valid and refreshes if needed
210
- * @param db - Firestore instance
211
- * @param entityType - Type of entity (practitioner, patient, clinic)
212
- * @param entityId - ID of the entity
213
- * @param syncedCalendar - Synced calendar data
214
- * @returns Valid access token
215
- */
216
- async function ensureValidToken(
217
- db: Firestore,
218
- entityType: "practitioner" | "patient" | "clinic",
219
- entityId: string,
220
- syncedCalendar: SyncedCalendar
221
- ): Promise<string> {
222
- // Check if token is expired or will expire soon (within 5 minutes)
223
- const expiryTime = syncedCalendar.tokenExpiry.toDate();
224
- const now = new Date();
225
- const fiveMinutesFromNow = new Date(now.getTime() + 5 * 60 * 1000);
226
-
227
- if (expiryTime < fiveMinutesFromNow) {
228
- // Token is expired or will expire soon, refresh it
229
- const { accessToken, expiresIn } = await refreshGoogleCalendarTokenUtil(
230
- syncedCalendar.refreshToken
231
- );
232
-
233
- // Calculate new expiry time
234
- const tokenExpiry = new Date();
235
- tokenExpiry.setSeconds(tokenExpiry.getSeconds() + expiresIn);
236
-
237
- // Update the synced calendar with the new token
238
- const updateData: Omit<UpdateSyncedCalendarData, "updatedAt"> = {
239
- accessToken,
240
- tokenExpiry: Timestamp.fromDate(tokenExpiry),
241
- };
242
-
243
- // Update the synced calendar in Firestore
244
- switch (entityType) {
245
- case "practitioner":
246
- await updatePractitionerSyncedCalendarUtil(
247
- db,
248
- entityId,
249
- syncedCalendar.id,
250
- updateData
251
- );
252
- break;
253
- case "patient":
254
- await updatePatientSyncedCalendarUtil(
255
- db,
256
- entityId,
257
- syncedCalendar.id,
258
- updateData
259
- );
260
- break;
261
- case "clinic":
262
- await updateClinicSyncedCalendarUtil(
263
- db,
264
- entityId,
265
- syncedCalendar.id,
266
- updateData
267
- );
268
- break;
269
- }
270
-
271
- return accessToken;
272
- }
273
-
274
- // Token is still valid
275
- return syncedCalendar.accessToken;
276
- }
277
-
278
- /**
279
- * Syncs events from our system to Google Calendar
280
- * @param db - Firestore instance
281
- * @param entityType - Type of entity (practitioner, patient, clinic)
282
- * @param entityId - ID of the entity
283
- * @param syncedCalendar - Synced calendar to use
284
- * @param events - Events to sync
285
- * @param existingSyncId - Optional existing sync ID for updating an event
286
- * @returns Result of the sync operation
287
- */
288
- export async function syncEventsToGoogleCalendarUtil(
289
- db: Firestore,
290
- entityType: "practitioner" | "patient" | "clinic",
291
- entityId: string,
292
- syncedCalendar: SyncedCalendar,
293
- events: CalendarEvent[],
294
- existingSyncId?: string
295
- ): Promise<{
296
- success: boolean;
297
- syncedEvents: number;
298
- errors: any[];
299
- eventIds: string[];
300
- }> {
301
- try {
302
- // Refresh token if needed
303
- const { accessToken } = await refreshGoogleCalendarTokenUtil(
304
- syncedCalendar.refreshToken
305
- );
306
-
307
- let syncedCount = 0;
308
- const errors: any[] = [];
309
- const eventIds: string[] = [];
310
-
311
- // Process each event
312
- for (const event of events) {
313
- try {
314
- // For patients: Sync all INTERNAL events except CANCELED and REJECTED
315
- // For doctors: Only sync INTERNAL events with CONFIRMED status
316
- // For clinics: No longer syncing
317
-
318
- // Skip events that are external (we don't sync external events back)
319
- if (event.syncStatus === CalendarSyncStatus.EXTERNAL) {
320
- continue;
321
- }
322
-
323
- // Apply entity-specific sync rules
324
- if (
325
- entityType === "practitioner" &&
326
- event.status !== CalendarEventStatus.CONFIRMED
327
- ) {
328
- // For doctors, only sync CONFIRMED events
329
- continue;
330
- }
331
-
332
- if (
333
- entityType === "patient" &&
334
- (event.status === CalendarEventStatus.CANCELED ||
335
- event.status === CalendarEventStatus.REJECTED)
336
- ) {
337
- // For patients, don't sync CANCELED or REJECTED events
338
- continue;
339
- }
340
-
341
- if (entityType === "clinic") {
342
- // No longer syncing clinic events
343
- continue;
344
- }
345
-
346
- // Convert our event to Google Calendar format
347
- const googleEvent = convertCalendarEventToGoogleEventUtil(event);
348
- const headers = {
349
- Authorization: `Bearer ${accessToken}`,
350
- "Content-Type": "application/json",
351
- };
352
-
353
- let responseId = "";
354
-
355
- // Check if we have an existing sync ID to update
356
- if (existingSyncId) {
357
- // Update the existing event using the provided ID
358
- const response = await makeRequest(
359
- "put",
360
- `${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events/${existingSyncId}`,
361
- headers,
362
- googleEvent
363
- );
364
- responseId = response.id;
365
- } else {
366
- // Check if the event already has a synced calendar event ID for this calendar
367
- const existingSync = event.syncedCalendarEventId?.find(
368
- (sync) =>
369
- sync.syncedCalendarProvider === SyncedCalendarProvider.GOOGLE &&
370
- // We should check if this is the same calendar we're syncing with, but that information isn't stored
371
- // For now, we'll just use the first Google Calendar sync ID
372
- sync.syncedCalendarProvider === syncedCalendar.provider
373
- );
374
-
375
- if (existingSync) {
376
- // Update existing event
377
- const response = await makeRequest(
378
- "put",
379
- `${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events/${existingSync.eventId}`,
380
- headers,
381
- googleEvent
382
- );
383
- responseId = response.id;
384
- } else {
385
- // Create new event
386
- const response = await makeRequest(
387
- "post",
388
- `${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events`,
389
- headers,
390
- googleEvent
391
- );
392
- responseId = response.id;
393
- }
394
- }
395
-
396
- if (responseId) {
397
- eventIds.push(responseId);
398
- syncedCount++;
399
- }
400
- } catch (error) {
401
- const apiError = error as ApiError;
402
- errors.push({
403
- eventId: event.id,
404
- error: apiError.message || "Unknown error",
405
- status: apiError.response?.status,
406
- });
407
- }
408
- }
409
-
410
- // Update the last synced timestamp
411
- await updateLastSyncedTimestampUtil(
412
- db,
413
- entityType,
414
- entityId,
415
- syncedCalendar.id
416
- );
417
-
418
- return {
419
- success: errors.length === 0,
420
- syncedEvents: syncedCount,
421
- errors,
422
- eventIds,
423
- };
424
- } catch (error) {
425
- console.error("Error syncing with Google Calendar:", error);
426
- return {
427
- success: false,
428
- syncedEvents: 0,
429
- errors: [{ error: (error as Error).message || "Unknown error" }],
430
- eventIds: [],
431
- };
432
- }
433
- }
434
-
435
- /**
436
- * Fetches events from Google Calendar
437
- * @param db - Firestore instance
438
- * @param entityType - Type of entity (practitioner, patient, clinic)
439
- * @param entityId - ID of the entity
440
- * @param syncedCalendar - Synced calendar data
441
- * @param startDate - Start date for fetching events
442
- * @param endDate - End date for fetching events
443
- * @returns Events fetched from Google Calendar
444
- */
445
- export async function fetchEventsFromGoogleCalendarUtil(
446
- db: Firestore,
447
- entityType: "practitioner" | "patient" | "clinic",
448
- entityId: string,
449
- syncedCalendar: SyncedCalendar,
450
- startDate: Date,
451
- endDate: Date
452
- ): Promise<any[]> {
453
- try {
454
- // Ensure we have a valid token
455
- const accessToken = await ensureValidToken(
456
- db,
457
- entityType,
458
- entityId,
459
- syncedCalendar
460
- );
461
-
462
- // Format dates for Google Calendar API
463
- const timeMin = startDate.toISOString();
464
- const timeMax = endDate.toISOString();
465
-
466
- // Call Google Calendar API to fetch events
467
- const response = await makeRequest(
468
- "get",
469
- `${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events`,
470
- { Authorization: `Bearer ${accessToken}` },
471
- undefined,
472
- {
473
- timeMin,
474
- timeMax,
475
- singleEvents: "true",
476
- orderBy: "startTime",
477
- }
478
- );
479
-
480
- // Update the last synced timestamp
481
- await updateLastSyncedTimestampUtil(
482
- db,
483
- entityType,
484
- entityId,
485
- syncedCalendar.id
486
- );
487
-
488
- return response.items;
489
- } catch (error) {
490
- const apiError = error as ApiError;
491
- console.error(
492
- "Error fetching events from Google Calendar:",
493
- apiError.message || "Unknown error"
494
- );
495
- throw new Error(
496
- `Failed to fetch events from Google Calendar: ${
497
- apiError.message || "Unknown error"
498
- }`
499
- );
500
- }
501
- }
502
-
503
- /**
504
- * Converts a Google Calendar event to our system's format
505
- * @param googleEvent - Google Calendar event
506
- * @param entityId - ID of the entity (practitioner, patient, clinic)
507
- * @param entityType - Type of entity
508
- * @returns Converted calendar event
509
- */
510
- export function convertGoogleEventToCalendarEventUtil(
511
- googleEvent: any,
512
- entityId: string,
513
- entityType: "practitioner" | "patient" | "clinic"
514
- ): Partial<CalendarEvent> {
515
- // Extract start and end times
516
- const start = googleEvent.start.dateTime
517
- ? new Date(googleEvent.start.dateTime)
518
- : new Date(googleEvent.start.date);
519
- const end = googleEvent.end.dateTime
520
- ? new Date(googleEvent.end.dateTime)
521
- : new Date(googleEvent.end.date);
522
-
523
- // Create the calendar event
524
- const calendarEvent: Partial<CalendarEvent> = {
525
- eventName: googleEvent.summary || "External Event",
526
- eventLocation: googleEvent.location,
527
- eventTime: {
528
- start: Timestamp.fromDate(start),
529
- end: Timestamp.fromDate(end),
530
- },
531
- description: googleEvent.description || "",
532
- // External events are always set as CONFIRMED - status updates will happen externally
533
- status: CalendarEventStatus.CONFIRMED,
534
- // All external events are marked as EXTERNAL to indicate they originated outside our system
535
- syncStatus: CalendarSyncStatus.EXTERNAL,
536
- // All external events are treated as BLOCKING events
537
- eventType: CalendarEventType.BLOCKING,
538
- // Store the original Google Calendar event ID
539
- syncedCalendarEventId: [
540
- {
541
- eventId: googleEvent.id,
542
- syncedCalendarProvider: SyncedCalendarProvider.GOOGLE,
543
- syncedAt: Timestamp.now(),
544
- },
545
- ],
546
- };
547
-
548
- // Add entity-specific fields
549
- switch (entityType) {
550
- case "practitioner":
551
- calendarEvent.practitionerProfileId = entityId;
552
- break;
553
- case "patient":
554
- calendarEvent.patientProfileId = entityId;
555
- break;
556
- case "clinic":
557
- calendarEvent.clinicBranchId = entityId;
558
- break;
559
- }
560
-
561
- return calendarEvent;
562
- }
563
-
564
- /**
565
- * Converts our system's calendar event to Google Calendar format
566
- * @param calendarEvent - Our system's calendar event
567
- * @returns Google Calendar event
568
- */
569
- export function convertCalendarEventToGoogleEventUtil(
570
- calendarEvent: CalendarEvent
571
- ): any {
572
- // Create the Google Calendar event
573
- const googleEvent: any = {
574
- summary: calendarEvent.eventName,
575
- location: calendarEvent.eventLocation,
576
- description: calendarEvent.description,
577
- start: {
578
- dateTime: calendarEvent.eventTime.start.toDate().toISOString(),
579
- timeZone: "UTC",
580
- },
581
- end: {
582
- dateTime: calendarEvent.eventTime.end.toDate().toISOString(),
583
- timeZone: "UTC",
584
- },
585
- // Add reminders
586
- reminders: {
587
- useDefault: false,
588
- overrides: [
589
- { method: "email", minutes: 24 * 60 }, // 1 day before
590
- { method: "popup", minutes: 30 }, // 30 minutes before
591
- ],
592
- },
593
- };
594
-
595
- // Add status mapping
596
- switch (calendarEvent.status) {
597
- case CalendarEventStatus.CONFIRMED:
598
- googleEvent.status = "confirmed";
599
- break;
600
- case CalendarEventStatus.CANCELED:
601
- googleEvent.status = "cancelled";
602
- break;
603
- case CalendarEventStatus.PENDING:
604
- // Google Calendar doesn't have a direct equivalent for pending
605
- // We'll use tentative as the closest match
606
- googleEvent.status = "tentative";
607
- break;
608
- default:
609
- googleEvent.status = "confirmed";
610
- }
611
-
612
- // Add attendees if this is an appointment
613
- if (calendarEvent.eventType === CalendarEventType.APPOINTMENT) {
614
- googleEvent.attendees = [];
615
-
616
- // In a real implementation, you would fetch the email addresses
617
- // of the practitioner and patient from their profiles
618
- if (calendarEvent.practitionerProfileId) {
619
- googleEvent.attendees.push({
620
- email: "practitioner@example.com", // This would be fetched from the practitioner profile
621
- displayName: "Dr. Practitioner", // This would be fetched from the practitioner profile
622
- responseStatus: "accepted",
623
- });
624
- }
625
-
626
- if (calendarEvent.patientProfileId) {
627
- googleEvent.attendees.push({
628
- email: "patient@example.com", // This would be fetched from the patient profile
629
- displayName: "Patient", // This would be fetched from the patient profile
630
- responseStatus: "needsAction",
631
- });
632
- }
633
- }
634
-
635
- return googleEvent;
636
- }
637
-
638
- /**
639
- * Deletes an event from Google Calendar
640
- * @param db - Firestore instance
641
- * @param entityType - Type of entity (practitioner, patient, clinic)
642
- * @param entityId - ID of the entity
643
- * @param syncedCalendar - Synced calendar data
644
- * @param eventId - ID of the event in Google Calendar
645
- * @returns Success status
646
- */
647
- export async function deleteGoogleCalendarEventUtil(
648
- db: Firestore,
649
- entityType: "practitioner" | "patient" | "clinic",
650
- entityId: string,
651
- syncedCalendar: SyncedCalendar,
652
- eventId: string
653
- ): Promise<boolean> {
654
- try {
655
- // Ensure we have a valid token
656
- const accessToken = await ensureValidToken(
657
- db,
658
- entityType,
659
- entityId,
660
- syncedCalendar
661
- );
662
-
663
- // Delete the event from Google Calendar
664
- await makeRequest(
665
- "delete",
666
- `${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events/${eventId}`,
667
- { Authorization: `Bearer ${accessToken}` }
668
- );
669
-
670
- return true;
671
- } catch (error) {
672
- const apiError = error as ApiError;
673
- console.error(
674
- "Error deleting event from Google Calendar:",
675
- apiError.message || "Unknown error"
676
- );
677
- throw new Error(
678
- `Failed to delete event from Google Calendar: ${
679
- apiError.message || "Unknown error"
680
- }`
681
- );
682
- }
683
- }
684
-
685
- /**
686
- * Gets the OAuth URL for Google Calendar
687
- * @param scopes - OAuth scopes to request
688
- * @returns OAuth URL
689
- */
690
- export function getGoogleCalendarOAuthUrlUtil(
691
- scopes: string[] = ["https://www.googleapis.com/auth/calendar"]
692
- ): string {
693
- const scopeString = encodeURIComponent(scopes.join(" "));
694
- return `https://accounts.google.com/o/oauth2/v2/auth?client_id=${CLIENT_ID}&redirect_uri=${encodeURIComponent(
695
- REDIRECT_URI
696
- )}&response_type=code&scope=${scopeString}&access_type=offline&prompt=consent`;
697
- }
1
+ /// <reference types="axios" />
2
+ import { Firestore, Timestamp } from "firebase/firestore";
3
+ import axios from "axios";
4
+ import {
5
+ SyncedCalendar,
6
+ SyncedCalendarProvider,
7
+ UpdateSyncedCalendarData,
8
+ } from "../../../types/calendar/synced-calendar.types";
9
+ import {
10
+ CalendarEvent,
11
+ CalendarEventStatus,
12
+ CalendarEventType,
13
+ CalendarSyncStatus,
14
+ } from "../../../types/calendar";
15
+ import {
16
+ updateLastSyncedTimestampUtil,
17
+ updatePractitionerSyncedCalendarUtil,
18
+ updatePatientSyncedCalendarUtil,
19
+ updateClinicSyncedCalendarUtil,
20
+ } from "./synced-calendar.utils";
21
+
22
+ // API URL and client configuration - these should be environment variables in a real application
23
+ export const GOOGLE_CALENDAR_API_URL = "https://www.googleapis.com/calendar/v3";
24
+ const GOOGLE_OAUTH_URL = "https://oauth2.googleapis.com/token";
25
+ const GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/auth";
26
+ const CLIENT_ID = "your-client-id"; // Replace with your actual client ID
27
+ const CLIENT_SECRET = "your-client-secret"; // Replace with your actual client secret
28
+ const REDIRECT_URI = "your-redirect-uri"; // Replace with your actual redirect URI
29
+
30
+ // Error interface for better error handling
31
+ interface ApiError {
32
+ message: string;
33
+ status?: number;
34
+ response?: {
35
+ data?: any;
36
+ status?: number;
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Helper function for making API requests
42
+ * @param method - HTTP method
43
+ * @param url - Request URL
44
+ * @param headers - Request headers
45
+ * @param data - Request body data
46
+ * @param params - URL query parameters
47
+ * @returns Response data
48
+ */
49
+ export async function makeRequest(
50
+ method: string,
51
+ url: string,
52
+ headers: Record<string, string>,
53
+ data?: any,
54
+ params?: Record<string, string>
55
+ ): Promise<any> {
56
+ // Construct URL with query parameters if provided
57
+ const queryParams = params
58
+ ? "?" +
59
+ Object.entries(params)
60
+ .map(
61
+ ([key, value]) =>
62
+ `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
63
+ )
64
+ .join("&")
65
+ : "";
66
+
67
+ const finalUrl = url + queryParams;
68
+
69
+ // Example implementation with fetch
70
+ const options: RequestInit = {
71
+ method,
72
+ headers,
73
+ body: data ? JSON.stringify(data) : undefined,
74
+ };
75
+
76
+ const response = await fetch(finalUrl, options);
77
+
78
+ if (!response.ok) {
79
+ const error: any = new Error(
80
+ `Request failed with status ${response.status}`
81
+ );
82
+ error.response = response;
83
+ throw error;
84
+ }
85
+
86
+ return response.json();
87
+ }
88
+
89
+ /**
90
+ * Authenticates with Google Calendar API
91
+ * @param authCode - Authorization code from Google OAuth
92
+ * @returns Access token and refresh token
93
+ */
94
+ export async function authenticateWithGoogleCalendarUtil(
95
+ authCode: string
96
+ ): Promise<{ accessToken: string; refreshToken: string; expiresIn: number }> {
97
+ try {
98
+ // Exchange authorization code for tokens
99
+ const data = {
100
+ code: authCode,
101
+ client_id: CLIENT_ID,
102
+ client_secret: CLIENT_SECRET,
103
+ redirect_uri: REDIRECT_URI,
104
+ grant_type: "authorization_code",
105
+ };
106
+
107
+ const response = await makeRequest(
108
+ "post",
109
+ GOOGLE_OAUTH_URL,
110
+ { "Content-Type": "application/json" },
111
+ data
112
+ );
113
+
114
+ return {
115
+ accessToken: response.access_token,
116
+ refreshToken: response.refresh_token,
117
+ expiresIn: response.expires_in,
118
+ };
119
+ } catch (error) {
120
+ const apiError = error as ApiError;
121
+ console.error(
122
+ "Error authenticating with Google Calendar:",
123
+ apiError.message || "Unknown error"
124
+ );
125
+ throw new Error(
126
+ `Failed to authenticate with Google Calendar: ${
127
+ apiError.message || "Unknown error"
128
+ }`
129
+ );
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Refreshes the Google Calendar API access token
135
+ * @param refreshToken - Refresh token
136
+ * @returns New access token and expiry
137
+ */
138
+ export async function refreshGoogleCalendarTokenUtil(
139
+ refreshToken: string
140
+ ): Promise<{ accessToken: string; expiresIn: number }> {
141
+ try {
142
+ // Use refresh token to get a new access token
143
+ const data = {
144
+ refresh_token: refreshToken,
145
+ client_id: CLIENT_ID,
146
+ client_secret: CLIENT_SECRET,
147
+ grant_type: "refresh_token",
148
+ };
149
+
150
+ const response = await makeRequest(
151
+ "post",
152
+ GOOGLE_OAUTH_URL,
153
+ { "Content-Type": "application/json" },
154
+ data
155
+ );
156
+
157
+ return {
158
+ accessToken: response.access_token,
159
+ expiresIn: response.expires_in,
160
+ };
161
+ } catch (error) {
162
+ const apiError = error as ApiError;
163
+ console.error(
164
+ "Error refreshing Google Calendar token:",
165
+ apiError.message || "Unknown error"
166
+ );
167
+ throw new Error(
168
+ `Failed to refresh Google Calendar token: ${
169
+ apiError.message || "Unknown error"
170
+ }`
171
+ );
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Lists available Google Calendars for a user
177
+ * @param accessToken - Google API access token
178
+ * @returns List of available calendars
179
+ */
180
+ export async function listGoogleCalendarsUtil(
181
+ accessToken: string
182
+ ): Promise<Array<{ id: string; name: string }>> {
183
+ try {
184
+ // Call Google Calendar API to list calendars
185
+ const response = await makeRequest(
186
+ "get",
187
+ `${GOOGLE_CALENDAR_API_URL}/users/me/calendarList`,
188
+ { Authorization: `Bearer ${accessToken}` }
189
+ );
190
+
191
+ // Map the response to our format
192
+ return response.items.map((calendar: any) => ({
193
+ id: calendar.id,
194
+ name: calendar.summary,
195
+ }));
196
+ } catch (error) {
197
+ const apiError = error as ApiError;
198
+ console.error(
199
+ "Error listing Google Calendars:",
200
+ apiError.message || "Unknown error"
201
+ );
202
+ throw new Error(
203
+ `Failed to list Google Calendars: ${apiError.message || "Unknown error"}`
204
+ );
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Ensures the synced calendar token is valid and refreshes if needed
210
+ * @param db - Firestore instance
211
+ * @param entityType - Type of entity (practitioner, patient, clinic)
212
+ * @param entityId - ID of the entity
213
+ * @param syncedCalendar - Synced calendar data
214
+ * @returns Valid access token
215
+ */
216
+ async function ensureValidToken(
217
+ db: Firestore,
218
+ entityType: "practitioner" | "patient" | "clinic",
219
+ entityId: string,
220
+ syncedCalendar: SyncedCalendar
221
+ ): Promise<string> {
222
+ // Check if token is expired or will expire soon (within 5 minutes)
223
+ const expiryTime = syncedCalendar.tokenExpiry.toDate();
224
+ const now = new Date();
225
+ const fiveMinutesFromNow = new Date(now.getTime() + 5 * 60 * 1000);
226
+
227
+ if (expiryTime < fiveMinutesFromNow) {
228
+ // Token is expired or will expire soon, refresh it
229
+ const { accessToken, expiresIn } = await refreshGoogleCalendarTokenUtil(
230
+ syncedCalendar.refreshToken
231
+ );
232
+
233
+ // Calculate new expiry time
234
+ const tokenExpiry = new Date();
235
+ tokenExpiry.setSeconds(tokenExpiry.getSeconds() + expiresIn);
236
+
237
+ // Update the synced calendar with the new token
238
+ const updateData: Omit<UpdateSyncedCalendarData, "updatedAt"> = {
239
+ accessToken,
240
+ tokenExpiry: Timestamp.fromDate(tokenExpiry),
241
+ };
242
+
243
+ // Update the synced calendar in Firestore
244
+ switch (entityType) {
245
+ case "practitioner":
246
+ await updatePractitionerSyncedCalendarUtil(
247
+ db,
248
+ entityId,
249
+ syncedCalendar.id,
250
+ updateData
251
+ );
252
+ break;
253
+ case "patient":
254
+ await updatePatientSyncedCalendarUtil(
255
+ db,
256
+ entityId,
257
+ syncedCalendar.id,
258
+ updateData
259
+ );
260
+ break;
261
+ case "clinic":
262
+ await updateClinicSyncedCalendarUtil(
263
+ db,
264
+ entityId,
265
+ syncedCalendar.id,
266
+ updateData
267
+ );
268
+ break;
269
+ }
270
+
271
+ return accessToken;
272
+ }
273
+
274
+ // Token is still valid
275
+ return syncedCalendar.accessToken;
276
+ }
277
+
278
+ /**
279
+ * Syncs events from our system to Google Calendar
280
+ * @param db - Firestore instance
281
+ * @param entityType - Type of entity (practitioner, patient, clinic)
282
+ * @param entityId - ID of the entity
283
+ * @param syncedCalendar - Synced calendar to use
284
+ * @param events - Events to sync
285
+ * @param existingSyncId - Optional existing sync ID for updating an event
286
+ * @returns Result of the sync operation
287
+ */
288
+ export async function syncEventsToGoogleCalendarUtil(
289
+ db: Firestore,
290
+ entityType: "practitioner" | "patient" | "clinic",
291
+ entityId: string,
292
+ syncedCalendar: SyncedCalendar,
293
+ events: CalendarEvent[],
294
+ existingSyncId?: string
295
+ ): Promise<{
296
+ success: boolean;
297
+ syncedEvents: number;
298
+ errors: any[];
299
+ eventIds: string[];
300
+ }> {
301
+ try {
302
+ // Refresh token if needed
303
+ const { accessToken } = await refreshGoogleCalendarTokenUtil(
304
+ syncedCalendar.refreshToken
305
+ );
306
+
307
+ let syncedCount = 0;
308
+ const errors: any[] = [];
309
+ const eventIds: string[] = [];
310
+
311
+ // Process each event
312
+ for (const event of events) {
313
+ try {
314
+ // For patients: Sync all INTERNAL events except CANCELED and REJECTED
315
+ // For doctors: Only sync INTERNAL events with CONFIRMED status
316
+ // For clinics: No longer syncing
317
+
318
+ // Skip events that are external (we don't sync external events back)
319
+ if (event.syncStatus === CalendarSyncStatus.EXTERNAL) {
320
+ continue;
321
+ }
322
+
323
+ // Apply entity-specific sync rules
324
+ if (
325
+ entityType === "practitioner" &&
326
+ event.status !== CalendarEventStatus.CONFIRMED
327
+ ) {
328
+ // For doctors, only sync CONFIRMED events
329
+ continue;
330
+ }
331
+
332
+ if (
333
+ entityType === "patient" &&
334
+ (event.status === CalendarEventStatus.CANCELED ||
335
+ event.status === CalendarEventStatus.REJECTED)
336
+ ) {
337
+ // For patients, don't sync CANCELED or REJECTED events
338
+ continue;
339
+ }
340
+
341
+ if (entityType === "clinic") {
342
+ // No longer syncing clinic events
343
+ continue;
344
+ }
345
+
346
+ // Convert our event to Google Calendar format
347
+ const googleEvent = convertCalendarEventToGoogleEventUtil(event);
348
+ const headers = {
349
+ Authorization: `Bearer ${accessToken}`,
350
+ "Content-Type": "application/json",
351
+ };
352
+
353
+ let responseId = "";
354
+
355
+ // Check if we have an existing sync ID to update
356
+ if (existingSyncId) {
357
+ // Update the existing event using the provided ID
358
+ const response = await makeRequest(
359
+ "put",
360
+ `${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events/${existingSyncId}`,
361
+ headers,
362
+ googleEvent
363
+ );
364
+ responseId = response.id;
365
+ } else {
366
+ // Check if the event already has a synced calendar event ID for this calendar
367
+ const existingSync = event.syncedCalendarEventId?.find(
368
+ (sync) =>
369
+ sync.syncedCalendarProvider === SyncedCalendarProvider.GOOGLE &&
370
+ // We should check if this is the same calendar we're syncing with, but that information isn't stored
371
+ // For now, we'll just use the first Google Calendar sync ID
372
+ sync.syncedCalendarProvider === syncedCalendar.provider
373
+ );
374
+
375
+ if (existingSync) {
376
+ // Update existing event
377
+ const response = await makeRequest(
378
+ "put",
379
+ `${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events/${existingSync.eventId}`,
380
+ headers,
381
+ googleEvent
382
+ );
383
+ responseId = response.id;
384
+ } else {
385
+ // Create new event
386
+ const response = await makeRequest(
387
+ "post",
388
+ `${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events`,
389
+ headers,
390
+ googleEvent
391
+ );
392
+ responseId = response.id;
393
+ }
394
+ }
395
+
396
+ if (responseId) {
397
+ eventIds.push(responseId);
398
+ syncedCount++;
399
+ }
400
+ } catch (error) {
401
+ const apiError = error as ApiError;
402
+ errors.push({
403
+ eventId: event.id,
404
+ error: apiError.message || "Unknown error",
405
+ status: apiError.response?.status,
406
+ });
407
+ }
408
+ }
409
+
410
+ // Update the last synced timestamp
411
+ await updateLastSyncedTimestampUtil(
412
+ db,
413
+ entityType,
414
+ entityId,
415
+ syncedCalendar.id
416
+ );
417
+
418
+ return {
419
+ success: errors.length === 0,
420
+ syncedEvents: syncedCount,
421
+ errors,
422
+ eventIds,
423
+ };
424
+ } catch (error) {
425
+ console.error("Error syncing with Google Calendar:", error);
426
+ return {
427
+ success: false,
428
+ syncedEvents: 0,
429
+ errors: [{ error: (error as Error).message || "Unknown error" }],
430
+ eventIds: [],
431
+ };
432
+ }
433
+ }
434
+
435
+ /**
436
+ * Fetches events from Google Calendar
437
+ * @param db - Firestore instance
438
+ * @param entityType - Type of entity (practitioner, patient, clinic)
439
+ * @param entityId - ID of the entity
440
+ * @param syncedCalendar - Synced calendar data
441
+ * @param startDate - Start date for fetching events
442
+ * @param endDate - End date for fetching events
443
+ * @returns Events fetched from Google Calendar
444
+ */
445
+ export async function fetchEventsFromGoogleCalendarUtil(
446
+ db: Firestore,
447
+ entityType: "practitioner" | "patient" | "clinic",
448
+ entityId: string,
449
+ syncedCalendar: SyncedCalendar,
450
+ startDate: Date,
451
+ endDate: Date
452
+ ): Promise<any[]> {
453
+ try {
454
+ // Ensure we have a valid token
455
+ const accessToken = await ensureValidToken(
456
+ db,
457
+ entityType,
458
+ entityId,
459
+ syncedCalendar
460
+ );
461
+
462
+ // Format dates for Google Calendar API
463
+ const timeMin = startDate.toISOString();
464
+ const timeMax = endDate.toISOString();
465
+
466
+ // Call Google Calendar API to fetch events
467
+ const response = await makeRequest(
468
+ "get",
469
+ `${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events`,
470
+ { Authorization: `Bearer ${accessToken}` },
471
+ undefined,
472
+ {
473
+ timeMin,
474
+ timeMax,
475
+ singleEvents: "true",
476
+ orderBy: "startTime",
477
+ }
478
+ );
479
+
480
+ // Update the last synced timestamp
481
+ await updateLastSyncedTimestampUtil(
482
+ db,
483
+ entityType,
484
+ entityId,
485
+ syncedCalendar.id
486
+ );
487
+
488
+ return response.items;
489
+ } catch (error) {
490
+ const apiError = error as ApiError;
491
+ console.error(
492
+ "Error fetching events from Google Calendar:",
493
+ apiError.message || "Unknown error"
494
+ );
495
+ throw new Error(
496
+ `Failed to fetch events from Google Calendar: ${
497
+ apiError.message || "Unknown error"
498
+ }`
499
+ );
500
+ }
501
+ }
502
+
503
+ /**
504
+ * Converts a Google Calendar event to our system's format
505
+ * @param googleEvent - Google Calendar event
506
+ * @param entityId - ID of the entity (practitioner, patient, clinic)
507
+ * @param entityType - Type of entity
508
+ * @returns Converted calendar event
509
+ */
510
+ export function convertGoogleEventToCalendarEventUtil(
511
+ googleEvent: any,
512
+ entityId: string,
513
+ entityType: "practitioner" | "patient" | "clinic"
514
+ ): Partial<CalendarEvent> {
515
+ // Extract start and end times
516
+ const start = googleEvent.start.dateTime
517
+ ? new Date(googleEvent.start.dateTime)
518
+ : new Date(googleEvent.start.date);
519
+ const end = googleEvent.end.dateTime
520
+ ? new Date(googleEvent.end.dateTime)
521
+ : new Date(googleEvent.end.date);
522
+
523
+ // Create the calendar event
524
+ const calendarEvent: Partial<CalendarEvent> = {
525
+ eventName: googleEvent.summary || "External Event",
526
+ eventLocation: googleEvent.location,
527
+ eventTime: {
528
+ start: Timestamp.fromDate(start),
529
+ end: Timestamp.fromDate(end),
530
+ },
531
+ description: googleEvent.description || "",
532
+ // External events are always set as CONFIRMED - status updates will happen externally
533
+ status: CalendarEventStatus.CONFIRMED,
534
+ // All external events are marked as EXTERNAL to indicate they originated outside our system
535
+ syncStatus: CalendarSyncStatus.EXTERNAL,
536
+ // All external events are treated as BLOCKING events
537
+ eventType: CalendarEventType.BLOCKING,
538
+ // Store the original Google Calendar event ID
539
+ syncedCalendarEventId: [
540
+ {
541
+ eventId: googleEvent.id,
542
+ syncedCalendarProvider: SyncedCalendarProvider.GOOGLE,
543
+ syncedAt: Timestamp.now(),
544
+ },
545
+ ],
546
+ };
547
+
548
+ // Add entity-specific fields
549
+ switch (entityType) {
550
+ case "practitioner":
551
+ calendarEvent.practitionerProfileId = entityId;
552
+ break;
553
+ case "patient":
554
+ calendarEvent.patientProfileId = entityId;
555
+ break;
556
+ case "clinic":
557
+ calendarEvent.clinicBranchId = entityId;
558
+ break;
559
+ }
560
+
561
+ return calendarEvent;
562
+ }
563
+
564
+ /**
565
+ * Converts our system's calendar event to Google Calendar format
566
+ * @param calendarEvent - Our system's calendar event
567
+ * @returns Google Calendar event
568
+ */
569
+ export function convertCalendarEventToGoogleEventUtil(
570
+ calendarEvent: CalendarEvent
571
+ ): any {
572
+ // Create the Google Calendar event
573
+ const googleEvent: any = {
574
+ summary: calendarEvent.eventName,
575
+ location: calendarEvent.eventLocation,
576
+ description: calendarEvent.description,
577
+ start: {
578
+ dateTime: calendarEvent.eventTime.start.toDate().toISOString(),
579
+ timeZone: "UTC",
580
+ },
581
+ end: {
582
+ dateTime: calendarEvent.eventTime.end.toDate().toISOString(),
583
+ timeZone: "UTC",
584
+ },
585
+ // Add reminders
586
+ reminders: {
587
+ useDefault: false,
588
+ overrides: [
589
+ { method: "email", minutes: 24 * 60 }, // 1 day before
590
+ { method: "popup", minutes: 30 }, // 30 minutes before
591
+ ],
592
+ },
593
+ };
594
+
595
+ // Add status mapping
596
+ switch (calendarEvent.status) {
597
+ case CalendarEventStatus.CONFIRMED:
598
+ googleEvent.status = "confirmed";
599
+ break;
600
+ case CalendarEventStatus.CANCELED:
601
+ googleEvent.status = "cancelled";
602
+ break;
603
+ case CalendarEventStatus.PENDING:
604
+ // Google Calendar doesn't have a direct equivalent for pending
605
+ // We'll use tentative as the closest match
606
+ googleEvent.status = "tentative";
607
+ break;
608
+ default:
609
+ googleEvent.status = "confirmed";
610
+ }
611
+
612
+ // Add attendees if this is an appointment
613
+ if (calendarEvent.eventType === CalendarEventType.APPOINTMENT) {
614
+ googleEvent.attendees = [];
615
+
616
+ // In a real implementation, you would fetch the email addresses
617
+ // of the practitioner and patient from their profiles
618
+ if (calendarEvent.practitionerProfileId) {
619
+ googleEvent.attendees.push({
620
+ email: "practitioner@example.com", // This would be fetched from the practitioner profile
621
+ displayName: "Dr. Practitioner", // This would be fetched from the practitioner profile
622
+ responseStatus: "accepted",
623
+ });
624
+ }
625
+
626
+ if (calendarEvent.patientProfileId) {
627
+ googleEvent.attendees.push({
628
+ email: "patient@example.com", // This would be fetched from the patient profile
629
+ displayName: "Patient", // This would be fetched from the patient profile
630
+ responseStatus: "needsAction",
631
+ });
632
+ }
633
+ }
634
+
635
+ return googleEvent;
636
+ }
637
+
638
+ /**
639
+ * Deletes an event from Google Calendar
640
+ * @param db - Firestore instance
641
+ * @param entityType - Type of entity (practitioner, patient, clinic)
642
+ * @param entityId - ID of the entity
643
+ * @param syncedCalendar - Synced calendar data
644
+ * @param eventId - ID of the event in Google Calendar
645
+ * @returns Success status
646
+ */
647
+ export async function deleteGoogleCalendarEventUtil(
648
+ db: Firestore,
649
+ entityType: "practitioner" | "patient" | "clinic",
650
+ entityId: string,
651
+ syncedCalendar: SyncedCalendar,
652
+ eventId: string
653
+ ): Promise<boolean> {
654
+ try {
655
+ // Ensure we have a valid token
656
+ const accessToken = await ensureValidToken(
657
+ db,
658
+ entityType,
659
+ entityId,
660
+ syncedCalendar
661
+ );
662
+
663
+ // Delete the event from Google Calendar
664
+ await makeRequest(
665
+ "delete",
666
+ `${GOOGLE_CALENDAR_API_URL}/calendars/${syncedCalendar.calendarId}/events/${eventId}`,
667
+ { Authorization: `Bearer ${accessToken}` }
668
+ );
669
+
670
+ return true;
671
+ } catch (error) {
672
+ const apiError = error as ApiError;
673
+ console.error(
674
+ "Error deleting event from Google Calendar:",
675
+ apiError.message || "Unknown error"
676
+ );
677
+ throw new Error(
678
+ `Failed to delete event from Google Calendar: ${
679
+ apiError.message || "Unknown error"
680
+ }`
681
+ );
682
+ }
683
+ }
684
+
685
+ /**
686
+ * Gets the OAuth URL for Google Calendar
687
+ * @param scopes - OAuth scopes to request
688
+ * @returns OAuth URL
689
+ */
690
+ export function getGoogleCalendarOAuthUrlUtil(
691
+ scopes: string[] = ["https://www.googleapis.com/auth/calendar"]
692
+ ): string {
693
+ const scopeString = encodeURIComponent(scopes.join(" "));
694
+ return `https://accounts.google.com/o/oauth2/v2/auth?client_id=${CLIENT_ID}&redirect_uri=${encodeURIComponent(
695
+ REDIRECT_URI
696
+ )}&response_type=code&scope=${scopeString}&access_type=offline&prompt=consent`;
697
+ }