@blackcode_sa/metaestetics-api 1.7.26 → 1.7.27

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blackcode_sa/metaestetics-api",
3
3
  "private": false,
4
- "version": "1.7.26",
4
+ "version": "1.7.27",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.mjs",
package/src/index.ts CHANGED
@@ -31,6 +31,7 @@ export {
31
31
  FilledDocumentService,
32
32
  } from "./services/documentation-templates";
33
33
  export { CalendarServiceV2 } from "./services/calendar/calendar-refactored.service";
34
+ export { CalendarServiceV3 } from "./services/calendar/calendar.v3.service";
34
35
  export { SyncedCalendarsService } from "./services/calendar/synced-calendars.service";
35
36
  export { ReviewService } from "./services/reviews/reviews.service";
36
37
  export { AppointmentService } from "./services/appointment/appointment.service";
@@ -237,6 +238,8 @@ export type {
237
238
  SyncedCalendarEvent,
238
239
  CreateAppointmentParams,
239
240
  UpdateAppointmentParams,
241
+ CreateBlockingEventParams,
242
+ UpdateBlockingEventParams,
240
243
  } from "./types/calendar";
241
244
 
242
245
  export {
@@ -0,0 +1 @@
1
+ // Cloud functions recommender index file
@@ -0,0 +1,5 @@
1
+ // Here we will add cloud functions for recommender system
2
+
3
+ // Here we will do the calculation logic and cloud logic for all the recommendation calculations
4
+
5
+ // This is a main file, but I want it to use different calculations that will be placed in UTILS folder, not to contain all the logic for easier maintenance and readability
@@ -0,0 +1 @@
1
+ // Frontend recommender index file
@@ -0,0 +1,5 @@
1
+ // This service will be used for managing onboarding process for the patient, it will handle all data entry and retreive results
2
+
3
+ // This service will fill special fields and types that will be defined in types folder
4
+
5
+ // This service is not retreiving any recommendations in the UI and is only used by onboarding module (form and survey)
@@ -0,0 +1,3 @@
1
+ // This is a frontend UI implementation of recommender service, it will use cloud functions for calculations (HTTP callable), but it will wrap logic for getting results for the frontend
2
+
3
+ // This service should not be heavy, it should fetch recommendations, maybe even handle like/dislike for the results (for further refining if we implement that), but no more than that
@@ -0,0 +1 @@
1
+ // Recommender module re-exportindex file
@@ -0,0 +1,313 @@
1
+ import { Auth } from "firebase/auth";
2
+ import { Firestore, Timestamp, serverTimestamp } from "firebase/firestore";
3
+ import { FirebaseApp } from "firebase/app";
4
+ import { BaseService } from "../base.service";
5
+ import {
6
+ CalendarEvent,
7
+ CalendarEventStatus,
8
+ CalendarEventTime,
9
+ CalendarEventType,
10
+ CalendarSyncStatus,
11
+ CreateCalendarEventData,
12
+ UpdateCalendarEventData,
13
+ CALENDAR_COLLECTION,
14
+ SearchCalendarEventsParams,
15
+ SearchLocationEnum,
16
+ DateRange,
17
+ CreateBlockingEventParams,
18
+ UpdateBlockingEventParams,
19
+ } from "../../types/calendar";
20
+ import { PRACTITIONERS_COLLECTION } from "../../types/practitioner";
21
+ import { CLINICS_COLLECTION } from "../../types/clinic";
22
+ import { doc, getDoc, setDoc, updateDoc, deleteDoc } from "firebase/firestore";
23
+
24
+ // Import utility functions
25
+ import { searchCalendarEventsUtil } from "./utils/calendar-event.utils";
26
+
27
+ /**
28
+ * Calendar Service V3
29
+ * Focused on blocking event management and calendar event search
30
+ * Appointment logic is handled by AppointmentService and BookingAdmin
31
+ * External calendar sync is handled by dedicated cloud functions
32
+ */
33
+ export class CalendarServiceV3 extends BaseService {
34
+ /**
35
+ * Creates a new CalendarServiceV3 instance
36
+ * @param db - Firestore instance
37
+ * @param auth - Firebase Auth instance
38
+ * @param app - Firebase App instance
39
+ */
40
+ constructor(db: Firestore, auth: Auth, app: FirebaseApp) {
41
+ super(db, auth, app);
42
+ }
43
+
44
+ // #region Blocking Event CRUD Operations
45
+
46
+ /**
47
+ * Creates a blocking event for a practitioner or clinic
48
+ * @param params - Blocking event creation parameters
49
+ * @returns Created calendar event
50
+ */
51
+ async createBlockingEvent(
52
+ params: CreateBlockingEventParams
53
+ ): Promise<CalendarEvent> {
54
+ // Validate input parameters
55
+ this.validateBlockingEventParams(params);
56
+
57
+ // Generate a unique ID for the event
58
+ const eventId = this.generateId();
59
+
60
+ // Determine the collection path based on entity type
61
+ const collectionPath = this.getEntityCalendarPath(
62
+ params.entityType,
63
+ params.entityId
64
+ );
65
+
66
+ // Create the event document reference
67
+ const eventRef = doc(this.db, collectionPath, eventId);
68
+
69
+ // Prepare the event data
70
+ const eventData: CreateCalendarEventData = {
71
+ id: eventId,
72
+ eventName: params.eventName,
73
+ eventTime: params.eventTime,
74
+ eventType: params.eventType,
75
+ description: params.description || "",
76
+ status: CalendarEventStatus.CONFIRMED, // Blocking events are always confirmed
77
+ syncStatus: CalendarSyncStatus.INTERNAL,
78
+ createdAt: serverTimestamp(),
79
+ updatedAt: serverTimestamp(),
80
+ };
81
+
82
+ // Set entity-specific fields
83
+ if (params.entityType === "practitioner") {
84
+ eventData.practitionerProfileId = params.entityId;
85
+ } else {
86
+ eventData.clinicBranchId = params.entityId;
87
+ }
88
+
89
+ // Create the event
90
+ await setDoc(eventRef, eventData);
91
+
92
+ // Return the created event
93
+ return {
94
+ ...eventData,
95
+ createdAt: Timestamp.now(),
96
+ updatedAt: Timestamp.now(),
97
+ } as CalendarEvent;
98
+ }
99
+
100
+ /**
101
+ * Updates a blocking event
102
+ * @param params - Blocking event update parameters
103
+ * @returns Updated calendar event
104
+ */
105
+ async updateBlockingEvent(
106
+ params: UpdateBlockingEventParams
107
+ ): Promise<CalendarEvent> {
108
+ // Determine the collection path
109
+ const collectionPath = this.getEntityCalendarPath(
110
+ params.entityType,
111
+ params.entityId
112
+ );
113
+ const eventRef = doc(this.db, collectionPath, params.eventId);
114
+
115
+ // Check if event exists
116
+ const eventDoc = await getDoc(eventRef);
117
+ if (!eventDoc.exists()) {
118
+ throw new Error(`Blocking event with ID ${params.eventId} not found`);
119
+ }
120
+
121
+ // Prepare update data
122
+ const updateData: Partial<UpdateCalendarEventData> = {
123
+ updatedAt: serverTimestamp(),
124
+ };
125
+
126
+ if (params.eventName !== undefined) {
127
+ updateData.eventName = params.eventName;
128
+ }
129
+ if (params.eventTime !== undefined) {
130
+ updateData.eventTime = params.eventTime;
131
+ }
132
+ if (params.description !== undefined) {
133
+ updateData.description = params.description;
134
+ }
135
+ if (params.status !== undefined) {
136
+ updateData.status = params.status;
137
+ }
138
+
139
+ // Update the event
140
+ await updateDoc(eventRef, updateData);
141
+
142
+ // Get and return the updated event
143
+ const updatedEventDoc = await getDoc(eventRef);
144
+ return updatedEventDoc.data() as CalendarEvent;
145
+ }
146
+
147
+ /**
148
+ * Deletes a blocking event
149
+ * @param entityType - Type of entity (practitioner or clinic)
150
+ * @param entityId - ID of the entity
151
+ * @param eventId - ID of the event to delete
152
+ */
153
+ async deleteBlockingEvent(
154
+ entityType: "practitioner" | "clinic",
155
+ entityId: string,
156
+ eventId: string
157
+ ): Promise<void> {
158
+ // Determine the collection path
159
+ const collectionPath = this.getEntityCalendarPath(entityType, entityId);
160
+ const eventRef = doc(this.db, collectionPath, eventId);
161
+
162
+ // Check if event exists
163
+ const eventDoc = await getDoc(eventRef);
164
+ if (!eventDoc.exists()) {
165
+ throw new Error(`Blocking event with ID ${eventId} not found`);
166
+ }
167
+
168
+ // Delete the event
169
+ await deleteDoc(eventRef);
170
+ }
171
+
172
+ /**
173
+ * Gets a specific blocking event
174
+ * @param entityType - Type of entity (practitioner or clinic)
175
+ * @param entityId - ID of the entity
176
+ * @param eventId - ID of the event to retrieve
177
+ * @returns Calendar event or null if not found
178
+ */
179
+ async getBlockingEvent(
180
+ entityType: "practitioner" | "clinic",
181
+ entityId: string,
182
+ eventId: string
183
+ ): Promise<CalendarEvent | null> {
184
+ // Determine the collection path
185
+ const collectionPath = this.getEntityCalendarPath(entityType, entityId);
186
+ const eventRef = doc(this.db, collectionPath, eventId);
187
+
188
+ // Get the event
189
+ const eventDoc = await getDoc(eventRef);
190
+ if (!eventDoc.exists()) {
191
+ return null;
192
+ }
193
+
194
+ return eventDoc.data() as CalendarEvent;
195
+ }
196
+
197
+ /**
198
+ * Gets blocking events for a specific entity
199
+ * @param entityType - Type of entity (practitioner or clinic)
200
+ * @param entityId - ID of the entity
201
+ * @param dateRange - Optional date range filter
202
+ * @param eventType - Optional event type filter
203
+ * @returns Array of calendar events
204
+ */
205
+ async getEntityBlockingEvents(
206
+ entityType: "practitioner" | "clinic",
207
+ entityId: string,
208
+ dateRange?: DateRange,
209
+ eventType?: CalendarEventType
210
+ ): Promise<CalendarEvent[]> {
211
+ // Use the existing search functionality
212
+ const searchParams: SearchCalendarEventsParams = {
213
+ searchLocation:
214
+ entityType === "practitioner"
215
+ ? SearchLocationEnum.PRACTITIONER
216
+ : SearchLocationEnum.CLINIC,
217
+ entityId,
218
+ dateRange,
219
+ eventType,
220
+ };
221
+
222
+ // Filter to only blocking-type events if no specific eventType is provided
223
+ if (!eventType) {
224
+ // We'll need to filter the results to only include blocking-type events
225
+ const allEvents = await searchCalendarEventsUtil(this.db, searchParams);
226
+ return allEvents.filter(
227
+ (event) =>
228
+ event.eventType === CalendarEventType.BLOCKING ||
229
+ event.eventType === CalendarEventType.BREAK ||
230
+ event.eventType === CalendarEventType.FREE_DAY ||
231
+ event.eventType === CalendarEventType.OTHER
232
+ );
233
+ }
234
+
235
+ return searchCalendarEventsUtil(this.db, searchParams);
236
+ }
237
+
238
+ // #endregion
239
+
240
+ // #region Calendar Event Search
241
+
242
+ /**
243
+ * Searches for calendar events based on specified criteria.
244
+ * This method supports searching for ALL event types (appointments, blocking events, etc.)
245
+ *
246
+ * @param params - The search parameters
247
+ * @returns A promise that resolves to an array of matching calendar events
248
+ */
249
+ async searchCalendarEvents(
250
+ params: SearchCalendarEventsParams
251
+ ): Promise<CalendarEvent[]> {
252
+ // Use the utility function to perform the search
253
+ return searchCalendarEventsUtil(this.db, params);
254
+ }
255
+
256
+ // #endregion
257
+
258
+ // #region Private Helper Methods
259
+
260
+ /**
261
+ * Gets the calendar collection path for a specific entity
262
+ * @param entityType - Type of entity (practitioner or clinic)
263
+ * @param entityId - ID of the entity
264
+ * @returns Collection path string
265
+ */
266
+ private getEntityCalendarPath(
267
+ entityType: "practitioner" | "clinic",
268
+ entityId: string
269
+ ): string {
270
+ if (entityType === "practitioner") {
271
+ return `${PRACTITIONERS_COLLECTION}/${entityId}/${CALENDAR_COLLECTION}`;
272
+ } else {
273
+ return `${CLINICS_COLLECTION}/${entityId}/${CALENDAR_COLLECTION}`;
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Validates blocking event creation parameters
279
+ * @param params - Parameters to validate
280
+ * @throws Error if validation fails
281
+ */
282
+ private validateBlockingEventParams(params: CreateBlockingEventParams): void {
283
+ if (!params.entityType || !params.entityId) {
284
+ throw new Error("Entity type and ID are required");
285
+ }
286
+
287
+ if (!params.eventName || params.eventName.trim() === "") {
288
+ throw new Error("Event name is required");
289
+ }
290
+
291
+ if (!params.eventTime || !params.eventTime.start || !params.eventTime.end) {
292
+ throw new Error("Event time with start and end is required");
293
+ }
294
+
295
+ // Validate that end time is after start time
296
+ if (params.eventTime.end.toMillis() <= params.eventTime.start.toMillis()) {
297
+ throw new Error("Event end time must be after start time");
298
+ }
299
+
300
+ // Validate event type is one of the blocking types
301
+ const validTypes = [
302
+ CalendarEventType.BLOCKING,
303
+ CalendarEventType.BREAK,
304
+ CalendarEventType.FREE_DAY,
305
+ CalendarEventType.OTHER,
306
+ ];
307
+ if (!validTypes.includes(params.eventType)) {
308
+ throw new Error("Invalid event type for blocking events");
309
+ }
310
+ }
311
+
312
+ // #endregion
313
+ }
@@ -227,3 +227,32 @@ export interface SearchCalendarEventsParams {
227
227
  /** Optional filter for event type. */
228
228
  eventType?: CalendarEventType;
229
229
  }
230
+
231
+ /**
232
+ * Interface for creating blocking events
233
+ */
234
+ export interface CreateBlockingEventParams {
235
+ entityType: "practitioner" | "clinic";
236
+ entityId: string;
237
+ eventName: string;
238
+ eventTime: CalendarEventTime;
239
+ eventType:
240
+ | CalendarEventType.BLOCKING
241
+ | CalendarEventType.BREAK
242
+ | CalendarEventType.FREE_DAY
243
+ | CalendarEventType.OTHER;
244
+ description?: string;
245
+ }
246
+
247
+ /**
248
+ * Interface for updating blocking events
249
+ */
250
+ export interface UpdateBlockingEventParams {
251
+ entityType: "practitioner" | "clinic";
252
+ entityId: string;
253
+ eventId: string;
254
+ eventName?: string;
255
+ eventTime?: CalendarEventTime;
256
+ description?: string;
257
+ status?: CalendarEventStatus;
258
+ }
@@ -4,6 +4,8 @@ import {
4
4
  CalendarEventStatus,
5
5
  CalendarEventType,
6
6
  CalendarSyncStatus,
7
+ CreateBlockingEventParams,
8
+ UpdateBlockingEventParams,
7
9
  } from "../types/calendar";
8
10
  import { SyncedCalendarProvider } from "../types/calendar/synced-calendar.types";
9
11
  import {
@@ -221,3 +223,42 @@ export const calendarEventSchema = z.object({
221
223
  createdAt: z.instanceof(Date).or(z.instanceof(Timestamp)),
222
224
  updatedAt: z.instanceof(Date).or(z.instanceof(Timestamp)),
223
225
  });
226
+
227
+ /**
228
+ * Validation schema for creating blocking events
229
+ * Based on CreateBlockingEventParams interface
230
+ */
231
+ export const createBlockingEventSchema = z.object({
232
+ entityType: z.enum(["practitioner", "clinic"]),
233
+ entityId: z.string().min(1, "Entity ID is required"),
234
+ eventName: z
235
+ .string()
236
+ .min(1, "Event name is required")
237
+ .max(200, "Event name too long"),
238
+ eventTime: calendarEventTimeSchema,
239
+ eventType: z.enum([
240
+ CalendarEventType.BLOCKING,
241
+ CalendarEventType.BREAK,
242
+ CalendarEventType.FREE_DAY,
243
+ CalendarEventType.OTHER,
244
+ ]),
245
+ description: z.string().max(1000, "Description too long").optional(),
246
+ });
247
+
248
+ /**
249
+ * Validation schema for updating blocking events
250
+ * Based on UpdateBlockingEventParams interface
251
+ */
252
+ export const updateBlockingEventSchema = z.object({
253
+ entityType: z.enum(["practitioner", "clinic"]),
254
+ entityId: z.string().min(1, "Entity ID is required"),
255
+ eventId: z.string().min(1, "Event ID is required"),
256
+ eventName: z
257
+ .string()
258
+ .min(1, "Event name is required")
259
+ .max(200, "Event name too long")
260
+ .optional(),
261
+ eventTime: calendarEventTimeSchema.optional(),
262
+ description: z.string().max(1000, "Description too long").optional(),
263
+ status: z.nativeEnum(CalendarEventStatus).optional(),
264
+ });