@blackcode_sa/metaestetics-api 1.7.26 → 1.7.28

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.
@@ -55,19 +55,29 @@ import { distanceBetween } from "geofire-common";
55
55
  import { CertificationSpecialty } from "../../backoffice/types/static/certification.types";
56
56
  import { Clinic, DoctorInfo, CLINICS_COLLECTION } from "../../types/clinic";
57
57
  import { ClinicInfo } from "../../types/profile";
58
+ import { ProcedureService } from "../procedure/procedure.service";
59
+ import { ProcedureFamily } from "../../backoffice/types/static/procedure-family.types";
60
+ import {
61
+ Currency,
62
+ PricingMeasure,
63
+ } from "../../backoffice/types/static/pricing.types";
64
+ import { CreateProcedureData } from "../../types/procedure";
58
65
 
59
66
  export class PractitionerService extends BaseService {
60
67
  private clinicService?: ClinicService;
61
68
  private mediaService: MediaService;
69
+ private procedureService?: ProcedureService;
62
70
 
63
71
  constructor(
64
72
  db: Firestore,
65
73
  auth: Auth,
66
74
  app: FirebaseApp,
67
- clinicService?: ClinicService
75
+ clinicService?: ClinicService,
76
+ procedureService?: ProcedureService
68
77
  ) {
69
78
  super(db, auth, app);
70
79
  this.clinicService = clinicService;
80
+ this.procedureService = procedureService;
71
81
  this.mediaService = new MediaService(db, auth, app);
72
82
  }
73
83
 
@@ -78,10 +88,21 @@ export class PractitionerService extends BaseService {
78
88
  return this.clinicService;
79
89
  }
80
90
 
91
+ private getProcedureService(): ProcedureService {
92
+ if (!this.procedureService) {
93
+ throw new Error("Procedure service not initialized!");
94
+ }
95
+ return this.procedureService;
96
+ }
97
+
81
98
  setClinicService(clinicService: ClinicService): void {
82
99
  this.clinicService = clinicService;
83
100
  }
84
101
 
102
+ setProcedureService(procedureService: ProcedureService): void {
103
+ this.procedureService = procedureService;
104
+ }
105
+
85
106
  /**
86
107
  * Handles profile photo upload for practitioners
87
108
  * @param profilePhoto - MediaResource (File, Blob, or URL string)
@@ -1183,4 +1204,160 @@ export class PractitionerService extends BaseService {
1183
1204
  throw error;
1184
1205
  }
1185
1206
  }
1207
+
1208
+ /**
1209
+ * Enables free consultation for a practitioner in a specific clinic
1210
+ * Creates a free consultation procedure with hardcoded parameters
1211
+ * @param practitionerId - ID of the practitioner
1212
+ * @param clinicId - ID of the clinic
1213
+ * @returns The created consultation procedure
1214
+ */
1215
+ async EnableFreeConsultation(
1216
+ practitionerId: string,
1217
+ clinicId: string
1218
+ ): Promise<void> {
1219
+ try {
1220
+ // Validate that practitioner exists and is active
1221
+ const practitioner = await this.getPractitioner(practitionerId);
1222
+ if (!practitioner) {
1223
+ throw new Error(`Practitioner ${practitionerId} not found`);
1224
+ }
1225
+
1226
+ if (
1227
+ !practitioner.isActive ||
1228
+ practitioner.status !== PractitionerStatus.ACTIVE
1229
+ ) {
1230
+ throw new Error(`Practitioner ${practitionerId} is not active`);
1231
+ }
1232
+
1233
+ // Validate that clinic exists
1234
+ const clinic = await this.getClinicService().getClinic(clinicId);
1235
+ if (!clinic) {
1236
+ throw new Error(`Clinic ${clinicId} not found`);
1237
+ }
1238
+
1239
+ // Check if practitioner is associated with this clinic
1240
+ if (!practitioner.clinics.includes(clinicId)) {
1241
+ throw new Error(
1242
+ `Practitioner ${practitionerId} is not associated with clinic ${clinicId}`
1243
+ );
1244
+ }
1245
+
1246
+ // Check if free consultation already exists for this practitioner in this clinic
1247
+ const existingProcedures =
1248
+ await this.getProcedureService().getProceduresByPractitioner(
1249
+ practitionerId
1250
+ );
1251
+ const existingConsultation = existingProcedures.find(
1252
+ (procedure) =>
1253
+ procedure.name === "Free Consultation" &&
1254
+ procedure.clinicBranchId === clinicId &&
1255
+ procedure.isActive
1256
+ );
1257
+
1258
+ if (existingConsultation) {
1259
+ console.log(
1260
+ `Free consultation already exists for practitioner ${practitionerId} in clinic ${clinicId}`
1261
+ );
1262
+ return;
1263
+ }
1264
+
1265
+ // Create procedure data for free consultation (without productId)
1266
+ const consultationData: Omit<CreateProcedureData, "productId"> = {
1267
+ name: "Free Consultation",
1268
+ description:
1269
+ "Free initial consultation to discuss treatment options and assess patient needs.",
1270
+ family: ProcedureFamily.AESTHETICS,
1271
+ categoryId: "consultation",
1272
+ subcategoryId: "free-consultation",
1273
+ technologyId: "free-consultation-tech",
1274
+ price: 0,
1275
+ currency: Currency.EUR,
1276
+ pricingMeasure: PricingMeasure.PER_SESSION,
1277
+ duration: 30, // 30 minutes consultation
1278
+ practitionerId: practitionerId,
1279
+ clinicBranchId: clinicId,
1280
+ photos: [], // No photos for consultation
1281
+ };
1282
+
1283
+ // Create the consultation procedure using the special method
1284
+ await this.getProcedureService().createConsultationProcedure(
1285
+ consultationData
1286
+ );
1287
+
1288
+ console.log(
1289
+ `Free consultation enabled for practitioner ${practitionerId} in clinic ${clinicId}`
1290
+ );
1291
+ } catch (error) {
1292
+ console.error(
1293
+ `Error enabling free consultation for practitioner ${practitionerId} in clinic ${clinicId}:`,
1294
+ error
1295
+ );
1296
+ throw error;
1297
+ }
1298
+ }
1299
+
1300
+ /**
1301
+ * Disables free consultation for a practitioner in a specific clinic
1302
+ * Finds and deactivates the existing free consultation procedure
1303
+ * @param practitionerId - ID of the practitioner
1304
+ * @param clinicId - ID of the clinic
1305
+ */
1306
+ async DisableFreeConsultation(
1307
+ practitionerId: string,
1308
+ clinicId: string
1309
+ ): Promise<void> {
1310
+ try {
1311
+ // Validate that practitioner exists
1312
+ const practitioner = await this.getPractitioner(practitionerId);
1313
+ if (!practitioner) {
1314
+ throw new Error(`Practitioner ${practitionerId} not found`);
1315
+ }
1316
+
1317
+ // Validate that clinic exists
1318
+ const clinic = await this.getClinicService().getClinic(clinicId);
1319
+ if (!clinic) {
1320
+ throw new Error(`Clinic ${clinicId} not found`);
1321
+ }
1322
+
1323
+ // Check if practitioner is associated with this clinic
1324
+ if (!practitioner.clinics.includes(clinicId)) {
1325
+ throw new Error(
1326
+ `Practitioner ${practitionerId} is not associated with clinic ${clinicId}`
1327
+ );
1328
+ }
1329
+
1330
+ // Find the free consultation procedure for this practitioner in this clinic
1331
+ const existingProcedures =
1332
+ await this.getProcedureService().getProceduresByPractitioner(
1333
+ practitionerId
1334
+ );
1335
+ const freeConsultation = existingProcedures.find(
1336
+ (procedure) =>
1337
+ procedure.name === "Free Consultation" &&
1338
+ procedure.clinicBranchId === clinicId &&
1339
+ procedure.isActive
1340
+ );
1341
+
1342
+ if (!freeConsultation) {
1343
+ console.log(
1344
+ `No active free consultation found for practitioner ${practitionerId} in clinic ${clinicId}`
1345
+ );
1346
+ return;
1347
+ }
1348
+
1349
+ // Deactivate the consultation procedure
1350
+ await this.getProcedureService().deactivateProcedure(freeConsultation.id);
1351
+
1352
+ console.log(
1353
+ `Free consultation disabled for practitioner ${practitionerId} in clinic ${clinicId}`
1354
+ );
1355
+ } catch (error) {
1356
+ console.error(
1357
+ `Error disabling free consultation for practitioner ${practitionerId} in clinic ${clinicId}:`,
1358
+ error
1359
+ );
1360
+ throw error;
1361
+ }
1362
+ }
1186
1363
  }
@@ -1001,4 +1001,145 @@ export class ProcedureService extends BaseService {
1001
1001
 
1002
1002
  return filteredProcedures;
1003
1003
  }
1004
+
1005
+ /**
1006
+ * Creates a consultation procedure without requiring a product
1007
+ * This is a special method for consultation procedures that don't use products
1008
+ * @param data - The data for creating a consultation procedure (without productId)
1009
+ * @returns The created procedure
1010
+ */
1011
+ async createConsultationProcedure(
1012
+ data: Omit<CreateProcedureData, "productId">
1013
+ ): Promise<Procedure> {
1014
+ // Generate procedure ID first so we can use it for media uploads
1015
+ const procedureId = this.generateId();
1016
+
1017
+ // Get references to related entities (Category, Subcategory, Technology)
1018
+ // For consultation, we don't need a product
1019
+ const [category, subcategory, technology] = await Promise.all([
1020
+ this.categoryService.getById(data.categoryId),
1021
+ this.subcategoryService.getById(data.categoryId, data.subcategoryId),
1022
+ this.technologyService.getById(data.technologyId),
1023
+ ]);
1024
+
1025
+ if (!category || !subcategory || !technology) {
1026
+ throw new Error("One or more required base entities not found");
1027
+ }
1028
+
1029
+ // Get clinic and practitioner information for aggregation
1030
+ const clinicRef = doc(this.db, CLINICS_COLLECTION, data.clinicBranchId);
1031
+ const clinicSnapshot = await getDoc(clinicRef);
1032
+ if (!clinicSnapshot.exists()) {
1033
+ throw new Error(`Clinic with ID ${data.clinicBranchId} not found`);
1034
+ }
1035
+ const clinic = clinicSnapshot.data() as Clinic;
1036
+
1037
+ const practitionerRef = doc(
1038
+ this.db,
1039
+ PRACTITIONERS_COLLECTION,
1040
+ data.practitionerId
1041
+ );
1042
+ const practitionerSnapshot = await getDoc(practitionerRef);
1043
+ if (!practitionerSnapshot.exists()) {
1044
+ throw new Error(`Practitioner with ID ${data.practitionerId} not found`);
1045
+ }
1046
+ const practitioner = practitionerSnapshot.data() as Practitioner;
1047
+
1048
+ // Process photos if provided
1049
+ let processedPhotos: string[] = [];
1050
+ if (data.photos && data.photos.length > 0) {
1051
+ processedPhotos = await this.processMediaArray(
1052
+ data.photos,
1053
+ procedureId,
1054
+ "procedure-photos"
1055
+ );
1056
+ }
1057
+
1058
+ // Create aggregated clinic info for the procedure document
1059
+ const clinicInfo = {
1060
+ id: clinicSnapshot.id,
1061
+ name: clinic.name,
1062
+ description: clinic.description || "",
1063
+ featuredPhoto:
1064
+ clinic.featuredPhotos && clinic.featuredPhotos.length > 0
1065
+ ? typeof clinic.featuredPhotos[0] === "string"
1066
+ ? clinic.featuredPhotos[0]
1067
+ : ""
1068
+ : typeof clinic.coverPhoto === "string"
1069
+ ? clinic.coverPhoto
1070
+ : "",
1071
+ location: clinic.location,
1072
+ contactInfo: clinic.contactInfo,
1073
+ };
1074
+
1075
+ // Create aggregated doctor info for the procedure document
1076
+ const doctorInfo = {
1077
+ id: practitionerSnapshot.id,
1078
+ name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
1079
+ description: practitioner.basicInfo.bio || "",
1080
+ photo:
1081
+ typeof practitioner.basicInfo.profileImageUrl === "string"
1082
+ ? practitioner.basicInfo.profileImageUrl
1083
+ : "",
1084
+ rating: practitioner.reviewInfo?.averageRating || 0,
1085
+ services: practitioner.procedures || [],
1086
+ };
1087
+
1088
+ // Create a placeholder product for consultation procedures
1089
+ const consultationProduct: Product = {
1090
+ id: "consultation-no-product",
1091
+ name: "No Product Required",
1092
+ description: "Consultation procedures do not require specific products",
1093
+ brandId: "consultation-brand",
1094
+ brandName: "Consultation",
1095
+ technologyId: data.technologyId,
1096
+ technologyName: technology.name,
1097
+ isActive: true,
1098
+ createdAt: new Date(),
1099
+ updatedAt: new Date(),
1100
+ };
1101
+
1102
+ // Create the procedure object
1103
+ const newProcedure: Omit<Procedure, "createdAt" | "updatedAt"> = {
1104
+ id: procedureId,
1105
+ ...data,
1106
+ photos: processedPhotos,
1107
+ category,
1108
+ subcategory,
1109
+ technology,
1110
+ product: consultationProduct, // Use placeholder product
1111
+ blockingConditions: technology.blockingConditions,
1112
+ contraindications: technology.contraindications || [],
1113
+ treatmentBenefits: technology.benefits,
1114
+ preRequirements: technology.requirements.pre,
1115
+ postRequirements: technology.requirements.post,
1116
+ certificationRequirement: technology.certificationRequirement,
1117
+ documentationTemplates: technology?.documentationTemplates || [],
1118
+ clinicInfo,
1119
+ doctorInfo,
1120
+ reviewInfo: {
1121
+ totalReviews: 0,
1122
+ averageRating: 0,
1123
+ effectivenessOfTreatment: 0,
1124
+ outcomeExplanation: 0,
1125
+ painManagement: 0,
1126
+ followUpCare: 0,
1127
+ valueForMoney: 0,
1128
+ recommendationPercentage: 0,
1129
+ },
1130
+ isActive: true,
1131
+ };
1132
+
1133
+ // Create the procedure document
1134
+ const procedureRef = doc(this.db, PROCEDURES_COLLECTION, procedureId);
1135
+ await setDoc(procedureRef, {
1136
+ ...newProcedure,
1137
+ createdAt: serverTimestamp(),
1138
+ updatedAt: serverTimestamp(),
1139
+ });
1140
+
1141
+ // Return the created procedure (fetch again to get server timestamps)
1142
+ const savedDoc = await getDoc(procedureRef);
1143
+ return savedDoc.data() as Procedure;
1144
+ }
1004
1145
  }
@@ -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
+ }
@@ -94,6 +94,7 @@ export interface Practitioner {
94
94
  clinicWorkingHours: PractitionerClinicWorkingHours[]; // Radno vreme za svaku kliniku
95
95
  clinicsInfo: ClinicInfo[]; // Aggregated clinic information
96
96
  procedures: string[]; // Reference na procedure koje izvodi
97
+ freeConsultations?: Record<string, string> | null; // Map of clinic IDs to procedure ID for free consultations (one per clinic)
97
98
  proceduresInfo: ProcedureSummaryInfo[]; // Aggregated procedure information
98
99
  reviewInfo: PractitionerReviewInfo; // Aggregated review information
99
100
  isActive: boolean;
@@ -113,6 +114,7 @@ export interface CreatePractitionerData {
113
114
  clinics?: string[];
114
115
  clinicWorkingHours?: PractitionerClinicWorkingHours[];
115
116
  clinicsInfo?: ClinicInfo[];
117
+ freeConsultations?: Record<string, string> | null;
116
118
  isActive: boolean;
117
119
  isVerified: boolean;
118
120
  status?: PractitionerStatus;
@@ -127,6 +129,7 @@ export interface CreateDraftPractitionerData {
127
129
  clinics?: string[];
128
130
  clinicWorkingHours?: PractitionerClinicWorkingHours[];
129
131
  clinicsInfo?: ClinicInfo[];
132
+ freeConsultations?: Record<string, string> | null;
130
133
  isActive?: boolean;
131
134
  isVerified?: boolean;
132
135
  }
@@ -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
+ });
@@ -125,6 +125,7 @@ export const practitionerSchema = z.object({
125
125
  clinicWorkingHours: z.array(practitionerClinicWorkingHoursSchema),
126
126
  clinicsInfo: z.array(clinicInfoSchema),
127
127
  procedures: z.array(z.string()),
128
+ freeConsultations: z.record(z.string(), z.string()).optional().nullable(),
128
129
  proceduresInfo: z.array(procedureSummaryInfoSchema),
129
130
  reviewInfo: practitionerReviewInfoSchema,
130
131
  isActive: z.boolean(),
@@ -144,6 +145,7 @@ export const createPractitionerSchema = z.object({
144
145
  clinics: z.array(z.string()).optional(),
145
146
  clinicWorkingHours: z.array(practitionerClinicWorkingHoursSchema).optional(),
146
147
  clinicsInfo: z.array(clinicInfoSchema).optional(),
148
+ freeConsultations: z.record(z.string(), z.string()).optional().nullable(),
147
149
  proceduresInfo: z.array(procedureSummaryInfoSchema).optional(),
148
150
  isActive: z.boolean(),
149
151
  isVerified: z.boolean(),
@@ -159,6 +161,7 @@ export const createDraftPractitionerSchema = z.object({
159
161
  clinics: z.array(z.string()).optional(),
160
162
  clinicWorkingHours: z.array(practitionerClinicWorkingHoursSchema).optional(),
161
163
  clinicsInfo: z.array(clinicInfoSchema).optional(),
164
+ freeConsultations: z.record(z.string(), z.string()).optional().nullable(),
162
165
  proceduresInfo: z.array(procedureSummaryInfoSchema).optional(),
163
166
  isActive: z.boolean().optional().default(false),
164
167
  isVerified: z.boolean().optional().default(false),