@blackcode_sa/metaestetics-api 1.12.64 → 1.12.66

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 (269) hide show
  1. package/dist/admin/index.d.mts +2 -0
  2. package/dist/admin/index.d.ts +2 -0
  3. package/dist/admin/index.js +45 -4
  4. package/dist/admin/index.mjs +45 -4
  5. package/dist/index.d.mts +2 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +80 -11
  8. package/dist/index.mjs +80 -11
  9. package/package.json +119 -119
  10. package/src/__mocks__/firstore.ts +10 -10
  11. package/src/admin/aggregation/README.md +79 -79
  12. package/src/admin/aggregation/appointment/README.md +128 -128
  13. package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1844 -1844
  14. package/src/admin/aggregation/appointment/index.ts +1 -1
  15. package/src/admin/aggregation/clinic/README.md +52 -52
  16. package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +703 -703
  17. package/src/admin/aggregation/clinic/index.ts +1 -1
  18. package/src/admin/aggregation/forms/README.md +13 -13
  19. package/src/admin/aggregation/forms/filled-forms.aggregation.service.ts +322 -322
  20. package/src/admin/aggregation/forms/index.ts +1 -1
  21. package/src/admin/aggregation/index.ts +8 -8
  22. package/src/admin/aggregation/patient/README.md +27 -27
  23. package/src/admin/aggregation/patient/index.ts +1 -1
  24. package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -141
  25. package/src/admin/aggregation/practitioner/README.md +42 -42
  26. package/src/admin/aggregation/practitioner/index.ts +1 -1
  27. package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -433
  28. package/src/admin/aggregation/practitioner-invite/index.ts +1 -1
  29. package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts +961 -961
  30. package/src/admin/aggregation/procedure/README.md +43 -43
  31. package/src/admin/aggregation/procedure/index.ts +1 -1
  32. package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +702 -702
  33. package/src/admin/aggregation/reviews/index.ts +1 -1
  34. package/src/admin/aggregation/reviews/reviews.aggregation.service.ts +689 -641
  35. package/src/admin/booking/README.md +125 -125
  36. package/src/admin/booking/booking.admin.ts +1037 -1037
  37. package/src/admin/booking/booking.calculator.ts +712 -712
  38. package/src/admin/booking/booking.types.ts +59 -59
  39. package/src/admin/booking/index.ts +3 -3
  40. package/src/admin/booking/timezones-problem.md +185 -185
  41. package/src/admin/calendar/README.md +7 -7
  42. package/src/admin/calendar/calendar.admin.service.ts +345 -345
  43. package/src/admin/calendar/index.ts +1 -1
  44. package/src/admin/documentation-templates/document-manager.admin.ts +260 -260
  45. package/src/admin/documentation-templates/index.ts +1 -1
  46. package/src/admin/free-consultation/free-consultation-utils.admin.ts +148 -148
  47. package/src/admin/free-consultation/index.ts +1 -1
  48. package/src/admin/index.ts +75 -75
  49. package/src/admin/logger/index.ts +78 -78
  50. package/src/admin/mailing/README.md +95 -95
  51. package/src/admin/mailing/appointment/appointment.mailing.service.ts +732 -732
  52. package/src/admin/mailing/appointment/index.ts +1 -1
  53. package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -40
  54. package/src/admin/mailing/base.mailing.service.ts +208 -208
  55. package/src/admin/mailing/index.ts +3 -3
  56. package/src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts +611 -611
  57. package/src/admin/mailing/practitionerInvite/index.ts +2 -2
  58. package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +395 -395
  59. package/src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts +155 -155
  60. package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -101
  61. package/src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts +228 -228
  62. package/src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts +242 -242
  63. package/src/admin/notifications/index.ts +1 -1
  64. package/src/admin/notifications/notifications.admin.ts +710 -710
  65. package/src/admin/requirements/README.md +128 -128
  66. package/src/admin/requirements/index.ts +1 -1
  67. package/src/admin/requirements/patient-requirements.admin.service.ts +475 -475
  68. package/src/admin/users/index.ts +1 -1
  69. package/src/admin/users/user-profile.admin.ts +405 -405
  70. package/src/backoffice/constants/certification.constants.ts +13 -13
  71. package/src/backoffice/constants/index.ts +1 -1
  72. package/src/backoffice/errors/backoffice.errors.ts +181 -181
  73. package/src/backoffice/errors/index.ts +1 -1
  74. package/src/backoffice/expo-safe/README.md +26 -26
  75. package/src/backoffice/expo-safe/index.ts +41 -41
  76. package/src/backoffice/index.ts +5 -5
  77. package/src/backoffice/services/FIXES_README.md +102 -102
  78. package/src/backoffice/services/README.md +40 -40
  79. package/src/backoffice/services/brand.service.ts +256 -256
  80. package/src/backoffice/services/category.service.ts +318 -318
  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 +395 -395
  88. package/src/backoffice/services/technology.service.ts +1083 -1083
  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 +62 -62
  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 +163 -163
  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/appointment/README.md +17 -17
  135. package/src/services/appointment/appointment.service.ts +2505 -2505
  136. package/src/services/appointment/index.ts +1 -1
  137. package/src/services/appointment/utils/appointment.utils.ts +552 -552
  138. package/src/services/appointment/utils/extended-procedure.utils.ts +314 -314
  139. package/src/services/appointment/utils/form-initialization.utils.ts +225 -225
  140. package/src/services/appointment/utils/recommended-procedure.utils.ts +195 -195
  141. package/src/services/appointment/utils/zone-management.utils.ts +353 -353
  142. package/src/services/appointment/utils/zone-photo.utils.ts +152 -152
  143. package/src/services/auth/auth.service.ts +989 -989
  144. package/src/services/auth/auth.v2.service.ts +961 -961
  145. package/src/services/auth/index.ts +7 -7
  146. package/src/services/auth/utils/error.utils.ts +90 -90
  147. package/src/services/auth/utils/firebase.utils.ts +49 -49
  148. package/src/services/auth/utils/index.ts +21 -21
  149. package/src/services/auth/utils/practitioner.utils.ts +125 -125
  150. package/src/services/base.service.ts +41 -41
  151. package/src/services/calendar/calendar.service.ts +1077 -1077
  152. package/src/services/calendar/calendar.v2.service.ts +1683 -1683
  153. package/src/services/calendar/calendar.v3.service.ts +313 -313
  154. package/src/services/calendar/externalCalendar.service.ts +178 -178
  155. package/src/services/calendar/index.ts +5 -5
  156. package/src/services/calendar/synced-calendars.service.ts +743 -743
  157. package/src/services/calendar/utils/appointment.utils.ts +265 -265
  158. package/src/services/calendar/utils/calendar-event.utils.ts +646 -646
  159. package/src/services/calendar/utils/clinic.utils.ts +237 -237
  160. package/src/services/calendar/utils/docs.utils.ts +157 -157
  161. package/src/services/calendar/utils/google-calendar.utils.ts +697 -697
  162. package/src/services/calendar/utils/index.ts +8 -8
  163. package/src/services/calendar/utils/patient.utils.ts +198 -198
  164. package/src/services/calendar/utils/practitioner.utils.ts +221 -221
  165. package/src/services/calendar/utils/synced-calendar.utils.ts +472 -472
  166. package/src/services/clinic/README.md +204 -204
  167. package/src/services/clinic/__tests__/clinic-admin.service.test.ts +287 -287
  168. package/src/services/clinic/__tests__/clinic-group.service.test.ts +352 -352
  169. package/src/services/clinic/__tests__/clinic.service.test.ts +354 -354
  170. package/src/services/clinic/billing-transactions.service.ts +217 -217
  171. package/src/services/clinic/clinic-admin.service.ts +202 -202
  172. package/src/services/clinic/clinic-group.service.ts +310 -310
  173. package/src/services/clinic/clinic.service.ts +708 -708
  174. package/src/services/clinic/index.ts +5 -5
  175. package/src/services/clinic/practitioner-invite.service.ts +519 -519
  176. package/src/services/clinic/utils/admin.utils.ts +551 -551
  177. package/src/services/clinic/utils/clinic-group.utils.ts +646 -646
  178. package/src/services/clinic/utils/clinic.utils.ts +949 -949
  179. package/src/services/clinic/utils/filter.utils.d.ts +23 -23
  180. package/src/services/clinic/utils/filter.utils.ts +446 -446
  181. package/src/services/clinic/utils/index.ts +11 -11
  182. package/src/services/clinic/utils/photos.utils.ts +188 -188
  183. package/src/services/clinic/utils/search.utils.ts +84 -84
  184. package/src/services/clinic/utils/tag.utils.ts +124 -124
  185. package/src/services/documentation-templates/documentation-template.service.ts +537 -537
  186. package/src/services/documentation-templates/filled-document.service.ts +587 -587
  187. package/src/services/documentation-templates/index.ts +2 -2
  188. package/src/services/index.ts +13 -13
  189. package/src/services/media/index.ts +1 -1
  190. package/src/services/media/media.service.ts +418 -418
  191. package/src/services/notifications/__tests__/notification.service.test.ts +242 -242
  192. package/src/services/notifications/index.ts +1 -1
  193. package/src/services/notifications/notification.service.ts +215 -215
  194. package/src/services/patient/README.md +48 -48
  195. package/src/services/patient/To-Do.md +43 -43
  196. package/src/services/patient/__tests__/patient.service.test.ts +294 -294
  197. package/src/services/patient/index.ts +2 -2
  198. package/src/services/patient/patient.service.ts +883 -883
  199. package/src/services/patient/patientRequirements.service.ts +285 -285
  200. package/src/services/patient/utils/aesthetic-analysis.utils.ts +176 -176
  201. package/src/services/patient/utils/clinic.utils.ts +80 -80
  202. package/src/services/patient/utils/docs.utils.ts +142 -142
  203. package/src/services/patient/utils/index.ts +9 -9
  204. package/src/services/patient/utils/location.utils.ts +126 -126
  205. package/src/services/patient/utils/medical-stuff.utils.ts +143 -143
  206. package/src/services/patient/utils/medical.utils.ts +458 -458
  207. package/src/services/patient/utils/practitioner.utils.ts +260 -260
  208. package/src/services/patient/utils/profile.utils.ts +510 -510
  209. package/src/services/patient/utils/sensitive.utils.ts +260 -260
  210. package/src/services/patient/utils/token.utils.ts +211 -211
  211. package/src/services/practitioner/README.md +145 -145
  212. package/src/services/practitioner/index.ts +1 -1
  213. package/src/services/practitioner/practitioner.service.ts +1742 -1742
  214. package/src/services/procedure/README.md +163 -163
  215. package/src/services/procedure/index.ts +1 -1
  216. package/src/services/procedure/procedure.service.ts +1715 -1682
  217. package/src/services/reviews/index.ts +1 -1
  218. package/src/services/reviews/reviews.service.ts +683 -636
  219. package/src/services/user/index.ts +1 -1
  220. package/src/services/user/user.service.ts +489 -489
  221. package/src/services/user/user.v2.service.ts +466 -466
  222. package/src/types/appointment/index.ts +480 -480
  223. package/src/types/calendar/index.ts +258 -258
  224. package/src/types/calendar/synced-calendar.types.ts +66 -66
  225. package/src/types/clinic/index.ts +489 -489
  226. package/src/types/clinic/practitioner-invite.types.ts +91 -91
  227. package/src/types/clinic/preferences.types.ts +159 -159
  228. package/src/types/clinic/to-do +3 -3
  229. package/src/types/documentation-templates/index.ts +308 -308
  230. package/src/types/index.ts +44 -44
  231. package/src/types/notifications/README.md +77 -77
  232. package/src/types/notifications/index.ts +265 -265
  233. package/src/types/patient/aesthetic-analysis.types.ts +66 -66
  234. package/src/types/patient/allergies.ts +58 -58
  235. package/src/types/patient/index.ts +275 -275
  236. package/src/types/patient/medical-info.types.ts +152 -152
  237. package/src/types/patient/patient-requirements.ts +92 -92
  238. package/src/types/patient/token.types.ts +61 -61
  239. package/src/types/practitioner/index.ts +206 -206
  240. package/src/types/procedure/index.ts +181 -181
  241. package/src/types/profile/index.ts +39 -39
  242. package/src/types/reviews/index.ts +132 -130
  243. package/src/types/tz-lookup.d.ts +4 -4
  244. package/src/types/user/index.ts +38 -38
  245. package/src/utils/TIMESTAMPS.md +176 -176
  246. package/src/utils/TimestampUtils.ts +241 -241
  247. package/src/utils/index.ts +1 -1
  248. package/src/validations/appointment.schema.ts +574 -574
  249. package/src/validations/calendar.schema.ts +225 -225
  250. package/src/validations/clinic.schema.ts +493 -493
  251. package/src/validations/common.schema.ts +25 -25
  252. package/src/validations/documentation-templates/index.ts +1 -1
  253. package/src/validations/documentation-templates/template.schema.ts +220 -220
  254. package/src/validations/documentation-templates.schema.ts +10 -10
  255. package/src/validations/index.ts +20 -20
  256. package/src/validations/media.schema.ts +10 -10
  257. package/src/validations/notification.schema.ts +90 -90
  258. package/src/validations/patient/aesthetic-analysis.schema.ts +55 -55
  259. package/src/validations/patient/medical-info.schema.ts +125 -125
  260. package/src/validations/patient/patient-requirements.schema.ts +84 -84
  261. package/src/validations/patient/token.schema.ts +29 -29
  262. package/src/validations/patient.schema.ts +217 -217
  263. package/src/validations/practitioner.schema.ts +222 -222
  264. package/src/validations/procedure-product.schema.ts +41 -41
  265. package/src/validations/procedure.schema.ts +124 -124
  266. package/src/validations/profile-info.schema.ts +41 -41
  267. package/src/validations/reviews.schema.ts +195 -189
  268. package/src/validations/schemas.ts +104 -104
  269. 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
+ }