@blackcode_sa/metaestetics-api 1.13.5 → 1.13.6

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