@blackcode_sa/metaestetics-api 1.12.59 → 1.12.61

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 (267) hide show
  1. package/dist/admin/index.d.mts +36 -4
  2. package/dist/admin/index.d.ts +36 -4
  3. package/dist/admin/index.js +147 -26
  4. package/dist/admin/index.mjs +147 -26
  5. package/dist/index.d.mts +9 -1
  6. package/dist/index.d.ts +9 -1
  7. package/package.json +119 -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 +1844 -1689
  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 +641 -641
  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 +75 -75
  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 +40 -40
  77. package/src/backoffice/services/brand.service.ts +256 -256
  78. package/src/backoffice/services/category.service.ts +318 -318
  79. package/src/backoffice/services/constants.service.ts +385 -385
  80. package/src/backoffice/services/documentation-template.service.ts +202 -202
  81. package/src/backoffice/services/index.ts +8 -8
  82. package/src/backoffice/services/migrate-products.ts +116 -116
  83. package/src/backoffice/services/product.service.ts +553 -553
  84. package/src/backoffice/services/requirement.service.ts +235 -235
  85. package/src/backoffice/services/subcategory.service.ts +395 -395
  86. package/src/backoffice/services/technology.service.ts +1070 -1070
  87. package/src/backoffice/types/README.md +12 -12
  88. package/src/backoffice/types/admin-constants.types.ts +69 -69
  89. package/src/backoffice/types/brand.types.ts +29 -29
  90. package/src/backoffice/types/category.types.ts +62 -62
  91. package/src/backoffice/types/documentation-templates.types.ts +28 -28
  92. package/src/backoffice/types/index.ts +10 -10
  93. package/src/backoffice/types/procedure-product.types.ts +38 -38
  94. package/src/backoffice/types/product.types.ts +240 -240
  95. package/src/backoffice/types/requirement.types.ts +63 -63
  96. package/src/backoffice/types/static/README.md +18 -18
  97. package/src/backoffice/types/static/blocking-condition.types.ts +21 -21
  98. package/src/backoffice/types/static/certification.types.ts +37 -37
  99. package/src/backoffice/types/static/contraindication.types.ts +19 -19
  100. package/src/backoffice/types/static/index.ts +6 -6
  101. package/src/backoffice/types/static/pricing.types.ts +16 -16
  102. package/src/backoffice/types/static/procedure-family.types.ts +14 -14
  103. package/src/backoffice/types/static/treatment-benefit.types.ts +22 -22
  104. package/src/backoffice/types/subcategory.types.ts +34 -34
  105. package/src/backoffice/types/technology.types.ts +161 -161
  106. package/src/backoffice/validations/index.ts +1 -1
  107. package/src/backoffice/validations/schemas.ts +163 -163
  108. package/src/config/__mocks__/firebase.ts +99 -99
  109. package/src/config/firebase.ts +78 -78
  110. package/src/config/index.ts +9 -9
  111. package/src/errors/auth.error.ts +6 -6
  112. package/src/errors/auth.errors.ts +200 -200
  113. package/src/errors/clinic.errors.ts +32 -32
  114. package/src/errors/firebase.errors.ts +47 -47
  115. package/src/errors/user.errors.ts +99 -99
  116. package/src/index.backup.ts +407 -407
  117. package/src/index.ts +6 -6
  118. package/src/locales/en.ts +31 -31
  119. package/src/recommender/admin/index.ts +1 -1
  120. package/src/recommender/admin/services/recommender.service.admin.ts +5 -5
  121. package/src/recommender/front/index.ts +1 -1
  122. package/src/recommender/front/services/onboarding.service.ts +5 -5
  123. package/src/recommender/front/services/recommender.service.ts +3 -3
  124. package/src/recommender/index.ts +1 -1
  125. package/src/services/PATIENTAUTH.MD +197 -197
  126. package/src/services/README.md +106 -106
  127. package/src/services/__tests__/auth/auth.mock.test.ts +17 -17
  128. package/src/services/__tests__/auth/auth.setup.ts +293 -293
  129. package/src/services/__tests__/auth.service.test.ts +346 -346
  130. package/src/services/__tests__/base.service.test.ts +77 -77
  131. package/src/services/__tests__/user.service.test.ts +528 -528
  132. package/src/services/appointment/README.md +17 -17
  133. package/src/services/appointment/appointment.service.ts +2082 -2082
  134. package/src/services/appointment/index.ts +1 -1
  135. package/src/services/appointment/utils/appointment.utils.ts +552 -552
  136. package/src/services/appointment/utils/extended-procedure.utils.ts +314 -314
  137. package/src/services/appointment/utils/form-initialization.utils.ts +225 -225
  138. package/src/services/appointment/utils/recommended-procedure.utils.ts +195 -195
  139. package/src/services/appointment/utils/zone-management.utils.ts +353 -353
  140. package/src/services/appointment/utils/zone-photo.utils.ts +152 -152
  141. package/src/services/auth/auth.service.ts +989 -989
  142. package/src/services/auth/auth.v2.service.ts +961 -961
  143. package/src/services/auth/index.ts +7 -7
  144. package/src/services/auth/utils/error.utils.ts +90 -90
  145. package/src/services/auth/utils/firebase.utils.ts +49 -49
  146. package/src/services/auth/utils/index.ts +21 -21
  147. package/src/services/auth/utils/practitioner.utils.ts +125 -125
  148. package/src/services/base.service.ts +41 -41
  149. package/src/services/calendar/calendar.service.ts +1077 -1077
  150. package/src/services/calendar/calendar.v2.service.ts +1683 -1683
  151. package/src/services/calendar/calendar.v3.service.ts +313 -313
  152. package/src/services/calendar/externalCalendar.service.ts +178 -178
  153. package/src/services/calendar/index.ts +5 -5
  154. package/src/services/calendar/synced-calendars.service.ts +743 -743
  155. package/src/services/calendar/utils/appointment.utils.ts +265 -265
  156. package/src/services/calendar/utils/calendar-event.utils.ts +646 -646
  157. package/src/services/calendar/utils/clinic.utils.ts +237 -237
  158. package/src/services/calendar/utils/docs.utils.ts +157 -157
  159. package/src/services/calendar/utils/google-calendar.utils.ts +697 -697
  160. package/src/services/calendar/utils/index.ts +8 -8
  161. package/src/services/calendar/utils/patient.utils.ts +198 -198
  162. package/src/services/calendar/utils/practitioner.utils.ts +221 -221
  163. package/src/services/calendar/utils/synced-calendar.utils.ts +472 -472
  164. package/src/services/clinic/README.md +204 -204
  165. package/src/services/clinic/__tests__/clinic-admin.service.test.ts +287 -287
  166. package/src/services/clinic/__tests__/clinic-group.service.test.ts +352 -352
  167. package/src/services/clinic/__tests__/clinic.service.test.ts +354 -354
  168. package/src/services/clinic/billing-transactions.service.ts +217 -217
  169. package/src/services/clinic/clinic-admin.service.ts +202 -202
  170. package/src/services/clinic/clinic-group.service.ts +310 -310
  171. package/src/services/clinic/clinic.service.ts +708 -708
  172. package/src/services/clinic/index.ts +5 -5
  173. package/src/services/clinic/practitioner-invite.service.ts +519 -519
  174. package/src/services/clinic/utils/admin.utils.ts +551 -551
  175. package/src/services/clinic/utils/clinic-group.utils.ts +646 -646
  176. package/src/services/clinic/utils/clinic.utils.ts +949 -949
  177. package/src/services/clinic/utils/filter.utils.d.ts +23 -23
  178. package/src/services/clinic/utils/filter.utils.ts +446 -446
  179. package/src/services/clinic/utils/index.ts +11 -11
  180. package/src/services/clinic/utils/photos.utils.ts +188 -188
  181. package/src/services/clinic/utils/search.utils.ts +84 -84
  182. package/src/services/clinic/utils/tag.utils.ts +124 -124
  183. package/src/services/documentation-templates/documentation-template.service.ts +537 -537
  184. package/src/services/documentation-templates/filled-document.service.ts +587 -587
  185. package/src/services/documentation-templates/index.ts +2 -2
  186. package/src/services/index.ts +13 -13
  187. package/src/services/media/index.ts +1 -1
  188. package/src/services/media/media.service.ts +418 -418
  189. package/src/services/notifications/__tests__/notification.service.test.ts +242 -242
  190. package/src/services/notifications/index.ts +1 -1
  191. package/src/services/notifications/notification.service.ts +215 -215
  192. package/src/services/patient/README.md +48 -48
  193. package/src/services/patient/To-Do.md +43 -43
  194. package/src/services/patient/__tests__/patient.service.test.ts +294 -294
  195. package/src/services/patient/index.ts +2 -2
  196. package/src/services/patient/patient.service.ts +883 -883
  197. package/src/services/patient/patientRequirements.service.ts +285 -285
  198. package/src/services/patient/utils/aesthetic-analysis.utils.ts +176 -176
  199. package/src/services/patient/utils/clinic.utils.ts +80 -80
  200. package/src/services/patient/utils/docs.utils.ts +142 -142
  201. package/src/services/patient/utils/index.ts +9 -9
  202. package/src/services/patient/utils/location.utils.ts +126 -126
  203. package/src/services/patient/utils/medical-stuff.utils.ts +143 -143
  204. package/src/services/patient/utils/medical.utils.ts +458 -458
  205. package/src/services/patient/utils/practitioner.utils.ts +260 -260
  206. package/src/services/patient/utils/profile.utils.ts +510 -510
  207. package/src/services/patient/utils/sensitive.utils.ts +260 -260
  208. package/src/services/patient/utils/token.utils.ts +211 -211
  209. package/src/services/practitioner/README.md +145 -145
  210. package/src/services/practitioner/index.ts +1 -1
  211. package/src/services/practitioner/practitioner.service.ts +1742 -1742
  212. package/src/services/procedure/README.md +163 -163
  213. package/src/services/procedure/index.ts +1 -1
  214. package/src/services/procedure/procedure.service.ts +1682 -1682
  215. package/src/services/reviews/index.ts +1 -1
  216. package/src/services/reviews/reviews.service.ts +636 -636
  217. package/src/services/user/index.ts +1 -1
  218. package/src/services/user/user.service.ts +489 -489
  219. package/src/services/user/user.v2.service.ts +466 -466
  220. package/src/types/appointment/index.ts +453 -453
  221. package/src/types/calendar/index.ts +258 -258
  222. package/src/types/calendar/synced-calendar.types.ts +66 -66
  223. package/src/types/clinic/index.ts +489 -489
  224. package/src/types/clinic/practitioner-invite.types.ts +91 -91
  225. package/src/types/clinic/preferences.types.ts +159 -159
  226. package/src/types/clinic/to-do +3 -3
  227. package/src/types/documentation-templates/index.ts +308 -308
  228. package/src/types/index.ts +44 -44
  229. package/src/types/notifications/README.md +77 -77
  230. package/src/types/notifications/index.ts +265 -265
  231. package/src/types/patient/aesthetic-analysis.types.ts +66 -66
  232. package/src/types/patient/allergies.ts +58 -58
  233. package/src/types/patient/index.ts +273 -273
  234. package/src/types/patient/medical-info.types.ts +152 -152
  235. package/src/types/patient/patient-requirements.ts +92 -81
  236. package/src/types/patient/token.types.ts +61 -61
  237. package/src/types/practitioner/index.ts +206 -206
  238. package/src/types/procedure/index.ts +181 -181
  239. package/src/types/profile/index.ts +39 -39
  240. package/src/types/reviews/index.ts +130 -130
  241. package/src/types/tz-lookup.d.ts +4 -4
  242. package/src/types/user/index.ts +38 -38
  243. package/src/utils/TIMESTAMPS.md +176 -176
  244. package/src/utils/TimestampUtils.ts +241 -241
  245. package/src/utils/index.ts +1 -1
  246. package/src/validations/appointment.schema.ts +574 -574
  247. package/src/validations/calendar.schema.ts +225 -225
  248. package/src/validations/clinic.schema.ts +493 -493
  249. package/src/validations/common.schema.ts +25 -25
  250. package/src/validations/documentation-templates/index.ts +1 -1
  251. package/src/validations/documentation-templates/template.schema.ts +220 -220
  252. package/src/validations/documentation-templates.schema.ts +10 -10
  253. package/src/validations/index.ts +20 -20
  254. package/src/validations/media.schema.ts +10 -10
  255. package/src/validations/notification.schema.ts +90 -90
  256. package/src/validations/patient/aesthetic-analysis.schema.ts +55 -55
  257. package/src/validations/patient/medical-info.schema.ts +125 -125
  258. package/src/validations/patient/patient-requirements.schema.ts +84 -75
  259. package/src/validations/patient/token.schema.ts +29 -29
  260. package/src/validations/patient.schema.ts +216 -216
  261. package/src/validations/practitioner.schema.ts +222 -222
  262. package/src/validations/procedure-product.schema.ts +41 -41
  263. package/src/validations/procedure.schema.ts +124 -124
  264. package/src/validations/profile-info.schema.ts +41 -41
  265. package/src/validations/reviews.schema.ts +189 -189
  266. package/src/validations/schemas.ts +104 -104
  267. 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
+ }