@blackcode_sa/metaestetics-api 1.13.4 → 1.13.5

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 (293) hide show
  1. package/dist/admin/index.d.mts +15 -28
  2. package/dist/admin/index.d.ts +15 -28
  3. package/dist/index.d.mts +16 -29
  4. package/dist/index.d.ts +16 -29
  5. package/dist/index.js +1 -0
  6. package/dist/index.mjs +1 -0
  7. package/package.json +121 -119
  8. package/src/__mocks__/firstore.ts +10 -10
  9. package/src/admin/aggregation/README.md +79 -79
  10. package/src/admin/aggregation/appointment/README.md +128 -128
  11. package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1984 -1984
  12. package/src/admin/aggregation/appointment/index.ts +1 -1
  13. package/src/admin/aggregation/clinic/README.md +52 -52
  14. package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +703 -703
  15. package/src/admin/aggregation/clinic/index.ts +1 -1
  16. package/src/admin/aggregation/forms/README.md +13 -13
  17. package/src/admin/aggregation/forms/filled-forms.aggregation.service.ts +322 -322
  18. package/src/admin/aggregation/forms/index.ts +1 -1
  19. package/src/admin/aggregation/index.ts +8 -8
  20. package/src/admin/aggregation/patient/README.md +27 -27
  21. package/src/admin/aggregation/patient/index.ts +1 -1
  22. package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -141
  23. package/src/admin/aggregation/practitioner/README.md +42 -42
  24. package/src/admin/aggregation/practitioner/index.ts +1 -1
  25. package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -433
  26. package/src/admin/aggregation/practitioner-invite/index.ts +1 -1
  27. package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts +961 -961
  28. package/src/admin/aggregation/procedure/README.md +43 -43
  29. package/src/admin/aggregation/procedure/index.ts +1 -1
  30. package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +702 -702
  31. package/src/admin/aggregation/reviews/index.ts +1 -1
  32. package/src/admin/aggregation/reviews/reviews.aggregation.service.ts +689 -689
  33. package/src/admin/analytics/analytics.admin.service.ts +278 -278
  34. package/src/admin/analytics/index.ts +2 -2
  35. package/src/admin/booking/README.md +125 -125
  36. package/src/admin/booking/booking.admin.ts +1037 -1037
  37. package/src/admin/booking/booking.calculator.ts +712 -712
  38. package/src/admin/booking/booking.types.ts +59 -59
  39. package/src/admin/booking/index.ts +3 -3
  40. package/src/admin/booking/timezones-problem.md +185 -185
  41. package/src/admin/calendar/README.md +7 -7
  42. package/src/admin/calendar/calendar.admin.service.ts +345 -345
  43. package/src/admin/calendar/index.ts +1 -1
  44. package/src/admin/documentation-templates/document-manager.admin.ts +260 -260
  45. package/src/admin/documentation-templates/index.ts +1 -1
  46. package/src/admin/free-consultation/free-consultation-utils.admin.ts +148 -148
  47. package/src/admin/free-consultation/index.ts +1 -1
  48. package/src/admin/index.ts +81 -81
  49. package/src/admin/logger/index.ts +78 -78
  50. package/src/admin/mailing/README.md +95 -95
  51. package/src/admin/mailing/appointment/appointment.mailing.service.ts +732 -732
  52. package/src/admin/mailing/appointment/index.ts +1 -1
  53. package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -40
  54. package/src/admin/mailing/base.mailing.service.ts +208 -208
  55. package/src/admin/mailing/index.ts +3 -3
  56. package/src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts +611 -611
  57. package/src/admin/mailing/practitionerInvite/index.ts +2 -2
  58. package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +395 -395
  59. package/src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts +155 -155
  60. package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -101
  61. package/src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts +228 -228
  62. package/src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts +242 -242
  63. package/src/admin/notifications/index.ts +1 -1
  64. package/src/admin/notifications/notifications.admin.ts +710 -710
  65. package/src/admin/requirements/README.md +128 -128
  66. package/src/admin/requirements/index.ts +1 -1
  67. package/src/admin/requirements/patient-requirements.admin.service.ts +475 -475
  68. package/src/admin/users/index.ts +1 -1
  69. package/src/admin/users/user-profile.admin.ts +405 -405
  70. package/src/backoffice/constants/certification.constants.ts +13 -13
  71. package/src/backoffice/constants/index.ts +1 -1
  72. package/src/backoffice/errors/backoffice.errors.ts +181 -181
  73. package/src/backoffice/errors/index.ts +1 -1
  74. package/src/backoffice/expo-safe/README.md +26 -26
  75. package/src/backoffice/expo-safe/index.ts +41 -41
  76. package/src/backoffice/index.ts +5 -5
  77. package/src/backoffice/services/FIXES_README.md +102 -102
  78. package/src/backoffice/services/README.md +57 -57
  79. package/src/backoffice/services/analytics.service.proposal.md +863 -863
  80. package/src/backoffice/services/analytics.service.summary.md +143 -143
  81. package/src/backoffice/services/brand.service.ts +256 -256
  82. package/src/backoffice/services/category.service.ts +384 -384
  83. package/src/backoffice/services/constants.service.ts +385 -385
  84. package/src/backoffice/services/documentation-template.service.ts +202 -202
  85. package/src/backoffice/services/index.ts +10 -10
  86. package/src/backoffice/services/migrate-products.ts +116 -116
  87. package/src/backoffice/services/product.service.ts +553 -553
  88. package/src/backoffice/services/requirement.service.ts +235 -235
  89. package/src/backoffice/services/subcategory.service.ts +461 -461
  90. package/src/backoffice/services/technology.service.ts +1151 -1151
  91. package/src/backoffice/types/README.md +12 -12
  92. package/src/backoffice/types/admin-constants.types.ts +69 -69
  93. package/src/backoffice/types/brand.types.ts +29 -29
  94. package/src/backoffice/types/category.types.ts +67 -67
  95. package/src/backoffice/types/documentation-templates.types.ts +28 -28
  96. package/src/backoffice/types/index.ts +10 -10
  97. package/src/backoffice/types/procedure-product.types.ts +38 -38
  98. package/src/backoffice/types/product.types.ts +240 -240
  99. package/src/backoffice/types/requirement.types.ts +63 -63
  100. package/src/backoffice/types/static/README.md +18 -18
  101. package/src/backoffice/types/static/blocking-condition.types.ts +21 -21
  102. package/src/backoffice/types/static/certification.types.ts +37 -37
  103. package/src/backoffice/types/static/contraindication.types.ts +19 -19
  104. package/src/backoffice/types/static/index.ts +6 -6
  105. package/src/backoffice/types/static/pricing.types.ts +16 -16
  106. package/src/backoffice/types/static/procedure-family.types.ts +14 -14
  107. package/src/backoffice/types/static/treatment-benefit.types.ts +22 -22
  108. package/src/backoffice/types/subcategory.types.ts +34 -34
  109. package/src/backoffice/types/technology.types.ts +168 -168
  110. package/src/backoffice/validations/index.ts +1 -1
  111. package/src/backoffice/validations/schemas.ts +164 -164
  112. package/src/config/__mocks__/firebase.ts +99 -99
  113. package/src/config/firebase.ts +78 -78
  114. package/src/config/index.ts +9 -9
  115. package/src/errors/auth.error.ts +6 -6
  116. package/src/errors/auth.errors.ts +200 -200
  117. package/src/errors/clinic.errors.ts +32 -32
  118. package/src/errors/firebase.errors.ts +47 -47
  119. package/src/errors/user.errors.ts +99 -99
  120. package/src/index.backup.ts +407 -407
  121. package/src/index.ts +6 -6
  122. package/src/locales/en.ts +31 -31
  123. package/src/recommender/admin/index.ts +1 -1
  124. package/src/recommender/admin/services/recommender.service.admin.ts +5 -5
  125. package/src/recommender/front/index.ts +1 -1
  126. package/src/recommender/front/services/onboarding.service.ts +5 -5
  127. package/src/recommender/front/services/recommender.service.ts +3 -3
  128. package/src/recommender/index.ts +1 -1
  129. package/src/services/PATIENTAUTH.MD +197 -197
  130. package/src/services/README.md +106 -106
  131. package/src/services/__tests__/auth/auth.mock.test.ts +17 -17
  132. package/src/services/__tests__/auth/auth.setup.ts +293 -293
  133. package/src/services/__tests__/auth.service.test.ts +346 -346
  134. package/src/services/__tests__/base.service.test.ts +77 -77
  135. package/src/services/__tests__/user.service.test.ts +528 -528
  136. package/src/services/analytics/ARCHITECTURE.md +199 -199
  137. package/src/services/analytics/CLOUD_FUNCTIONS.md +225 -225
  138. package/src/services/analytics/GROUPED_ANALYTICS.md +501 -501
  139. package/src/services/analytics/QUICK_START.md +393 -393
  140. package/src/services/analytics/README.md +304 -304
  141. package/src/services/analytics/SUMMARY.md +141 -141
  142. package/src/services/analytics/TRENDS.md +380 -380
  143. package/src/services/analytics/USAGE_GUIDE.md +518 -518
  144. package/src/services/analytics/analytics-cloud.service.ts +222 -222
  145. package/src/services/analytics/analytics.service.ts +2142 -2142
  146. package/src/services/analytics/index.ts +4 -4
  147. package/src/services/analytics/review-analytics.service.ts +941 -941
  148. package/src/services/analytics/utils/appointment-filtering.utils.ts +138 -138
  149. package/src/services/analytics/utils/cost-calculation.utils.ts +182 -182
  150. package/src/services/analytics/utils/grouping.utils.ts +434 -434
  151. package/src/services/analytics/utils/stored-analytics.utils.ts +347 -347
  152. package/src/services/analytics/utils/time-calculation.utils.ts +186 -186
  153. package/src/services/analytics/utils/trend-calculation.utils.ts +200 -200
  154. package/src/services/appointment/README.md +17 -17
  155. package/src/services/appointment/appointment.service.ts +2558 -2558
  156. package/src/services/appointment/index.ts +1 -1
  157. package/src/services/appointment/utils/appointment.utils.ts +552 -552
  158. package/src/services/appointment/utils/extended-procedure.utils.ts +314 -314
  159. package/src/services/appointment/utils/form-initialization.utils.ts +225 -225
  160. package/src/services/appointment/utils/recommended-procedure.utils.ts +195 -195
  161. package/src/services/appointment/utils/zone-management.utils.ts +353 -353
  162. package/src/services/appointment/utils/zone-photo.utils.ts +152 -152
  163. package/src/services/auth/auth.service.ts +989 -989
  164. package/src/services/auth/auth.v2.service.ts +961 -961
  165. package/src/services/auth/index.ts +7 -7
  166. package/src/services/auth/utils/error.utils.ts +90 -90
  167. package/src/services/auth/utils/firebase.utils.ts +49 -49
  168. package/src/services/auth/utils/index.ts +21 -21
  169. package/src/services/auth/utils/practitioner.utils.ts +125 -125
  170. package/src/services/base.service.ts +41 -41
  171. package/src/services/calendar/calendar.service.ts +1077 -1077
  172. package/src/services/calendar/calendar.v2.service.ts +1683 -1683
  173. package/src/services/calendar/calendar.v3.service.ts +313 -313
  174. package/src/services/calendar/externalCalendar.service.ts +178 -178
  175. package/src/services/calendar/index.ts +5 -5
  176. package/src/services/calendar/synced-calendars.service.ts +743 -743
  177. package/src/services/calendar/utils/appointment.utils.ts +265 -265
  178. package/src/services/calendar/utils/calendar-event.utils.ts +646 -646
  179. package/src/services/calendar/utils/clinic.utils.ts +237 -237
  180. package/src/services/calendar/utils/docs.utils.ts +157 -157
  181. package/src/services/calendar/utils/google-calendar.utils.ts +697 -697
  182. package/src/services/calendar/utils/index.ts +8 -8
  183. package/src/services/calendar/utils/patient.utils.ts +198 -198
  184. package/src/services/calendar/utils/practitioner.utils.ts +221 -221
  185. package/src/services/calendar/utils/synced-calendar.utils.ts +472 -472
  186. package/src/services/clinic/README.md +204 -204
  187. package/src/services/clinic/__tests__/clinic-admin.service.test.ts +287 -287
  188. package/src/services/clinic/__tests__/clinic-group.service.test.ts +352 -352
  189. package/src/services/clinic/__tests__/clinic.service.test.ts +354 -354
  190. package/src/services/clinic/billing-transactions.service.ts +217 -217
  191. package/src/services/clinic/clinic-admin.service.ts +202 -202
  192. package/src/services/clinic/clinic-group.service.ts +310 -310
  193. package/src/services/clinic/clinic.service.ts +708 -708
  194. package/src/services/clinic/index.ts +5 -5
  195. package/src/services/clinic/practitioner-invite.service.ts +519 -519
  196. package/src/services/clinic/utils/admin.utils.ts +551 -551
  197. package/src/services/clinic/utils/clinic-group.utils.ts +646 -646
  198. package/src/services/clinic/utils/clinic.utils.ts +949 -949
  199. package/src/services/clinic/utils/filter.utils.d.ts +23 -23
  200. package/src/services/clinic/utils/filter.utils.ts +446 -446
  201. package/src/services/clinic/utils/index.ts +11 -11
  202. package/src/services/clinic/utils/photos.utils.ts +188 -188
  203. package/src/services/clinic/utils/search.utils.ts +84 -84
  204. package/src/services/clinic/utils/tag.utils.ts +124 -124
  205. package/src/services/documentation-templates/documentation-template.service.ts +537 -537
  206. package/src/services/documentation-templates/filled-document.service.ts +587 -587
  207. package/src/services/documentation-templates/index.ts +2 -2
  208. package/src/services/index.ts +14 -14
  209. package/src/services/media/index.ts +1 -1
  210. package/src/services/media/media.service.ts +418 -418
  211. package/src/services/notifications/__tests__/notification.service.test.ts +242 -242
  212. package/src/services/notifications/index.ts +1 -1
  213. package/src/services/notifications/notification.service.ts +215 -215
  214. package/src/services/patient/README.md +48 -48
  215. package/src/services/patient/To-Do.md +43 -43
  216. package/src/services/patient/__tests__/patient.service.test.ts +294 -294
  217. package/src/services/patient/index.ts +2 -2
  218. package/src/services/patient/patient.service.ts +883 -883
  219. package/src/services/patient/patientRequirements.service.ts +285 -285
  220. package/src/services/patient/utils/aesthetic-analysis.utils.ts +176 -176
  221. package/src/services/patient/utils/clinic.utils.ts +80 -80
  222. package/src/services/patient/utils/docs.utils.ts +142 -142
  223. package/src/services/patient/utils/index.ts +9 -9
  224. package/src/services/patient/utils/location.utils.ts +126 -126
  225. package/src/services/patient/utils/medical-stuff.utils.ts +143 -143
  226. package/src/services/patient/utils/medical.utils.ts +458 -458
  227. package/src/services/patient/utils/practitioner.utils.ts +260 -260
  228. package/src/services/patient/utils/profile.utils.ts +510 -510
  229. package/src/services/patient/utils/sensitive.utils.ts +260 -260
  230. package/src/services/patient/utils/token.utils.ts +211 -211
  231. package/src/services/practitioner/README.md +145 -145
  232. package/src/services/practitioner/index.ts +1 -1
  233. package/src/services/practitioner/practitioner.service.ts +1742 -1742
  234. package/src/services/procedure/README.md +163 -163
  235. package/src/services/procedure/index.ts +1 -1
  236. package/src/services/procedure/procedure.service.ts +2200 -2200
  237. package/src/services/reviews/index.ts +1 -1
  238. package/src/services/reviews/reviews.service.ts +734 -734
  239. package/src/services/user/index.ts +1 -1
  240. package/src/services/user/user.service.ts +489 -489
  241. package/src/services/user/user.v2.service.ts +466 -466
  242. package/src/types/analytics/analytics.types.ts +597 -597
  243. package/src/types/analytics/grouped-analytics.types.ts +173 -173
  244. package/src/types/analytics/index.ts +4 -4
  245. package/src/types/analytics/stored-analytics.types.ts +137 -137
  246. package/src/types/appointment/index.ts +480 -480
  247. package/src/types/calendar/index.ts +258 -258
  248. package/src/types/calendar/synced-calendar.types.ts +66 -66
  249. package/src/types/clinic/index.ts +498 -489
  250. package/src/types/clinic/practitioner-invite.types.ts +91 -91
  251. package/src/types/clinic/preferences.types.ts +159 -159
  252. package/src/types/clinic/to-do +3 -3
  253. package/src/types/documentation-templates/index.ts +308 -308
  254. package/src/types/index.ts +47 -47
  255. package/src/types/notifications/README.md +77 -77
  256. package/src/types/notifications/index.ts +286 -286
  257. package/src/types/patient/aesthetic-analysis.types.ts +66 -66
  258. package/src/types/patient/allergies.ts +58 -58
  259. package/src/types/patient/index.ts +275 -275
  260. package/src/types/patient/medical-info.types.ts +152 -152
  261. package/src/types/patient/patient-requirements.ts +92 -92
  262. package/src/types/patient/token.types.ts +61 -61
  263. package/src/types/practitioner/index.ts +206 -206
  264. package/src/types/procedure/index.ts +181 -181
  265. package/src/types/profile/index.ts +39 -39
  266. package/src/types/reviews/index.ts +132 -132
  267. package/src/types/tz-lookup.d.ts +4 -4
  268. package/src/types/user/index.ts +38 -38
  269. package/src/utils/TIMESTAMPS.md +176 -176
  270. package/src/utils/TimestampUtils.ts +241 -241
  271. package/src/utils/index.ts +1 -1
  272. package/src/validations/appointment.schema.ts +574 -574
  273. package/src/validations/calendar.schema.ts +225 -225
  274. package/src/validations/clinic.schema.ts +494 -493
  275. package/src/validations/common.schema.ts +25 -25
  276. package/src/validations/documentation-templates/index.ts +1 -1
  277. package/src/validations/documentation-templates/template.schema.ts +220 -220
  278. package/src/validations/documentation-templates.schema.ts +10 -10
  279. package/src/validations/index.ts +20 -20
  280. package/src/validations/media.schema.ts +10 -10
  281. package/src/validations/notification.schema.ts +90 -90
  282. package/src/validations/patient/aesthetic-analysis.schema.ts +55 -55
  283. package/src/validations/patient/medical-info.schema.ts +125 -125
  284. package/src/validations/patient/patient-requirements.schema.ts +84 -84
  285. package/src/validations/patient/token.schema.ts +29 -29
  286. package/src/validations/patient.schema.ts +217 -217
  287. package/src/validations/practitioner.schema.ts +222 -222
  288. package/src/validations/procedure-product.schema.ts +41 -41
  289. package/src/validations/procedure.schema.ts +124 -124
  290. package/src/validations/profile-info.schema.ts +41 -41
  291. package/src/validations/reviews.schema.ts +195 -195
  292. package/src/validations/schemas.ts +104 -104
  293. 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
+ }