@blackcode_sa/metaestetics-api 1.5.27 → 1.5.29

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 (35) hide show
  1. package/dist/admin/index.d.mts +1199 -1
  2. package/dist/admin/index.d.ts +1199 -1
  3. package/dist/admin/index.js +1337 -2
  4. package/dist/admin/index.mjs +1333 -2
  5. package/dist/backoffice/index.d.mts +99 -7
  6. package/dist/backoffice/index.d.ts +99 -7
  7. package/dist/index.d.mts +4184 -2426
  8. package/dist/index.d.ts +4184 -2426
  9. package/dist/index.js +2692 -1546
  10. package/dist/index.mjs +2663 -1502
  11. package/package.json +1 -1
  12. package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +642 -0
  13. package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -0
  14. package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -0
  15. package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +508 -0
  16. package/src/admin/index.ts +53 -4
  17. package/src/index.ts +28 -4
  18. package/src/services/calendar/calendar-refactored.service.ts +1 -1
  19. package/src/services/clinic/clinic.service.ts +344 -77
  20. package/src/services/clinic/utils/clinic.utils.ts +187 -8
  21. package/src/services/clinic/utils/filter.utils.d.ts +23 -0
  22. package/src/services/clinic/utils/filter.utils.ts +264 -0
  23. package/src/services/practitioner/practitioner.service.ts +616 -5
  24. package/src/services/procedure/procedure.service.ts +678 -52
  25. package/src/services/reviews/reviews.service.ts +842 -0
  26. package/src/types/clinic/index.ts +24 -56
  27. package/src/types/practitioner/index.ts +34 -33
  28. package/src/types/procedure/index.ts +39 -0
  29. package/src/types/profile/index.ts +1 -1
  30. package/src/types/reviews/index.ts +126 -0
  31. package/src/validations/clinic.schema.ts +37 -64
  32. package/src/validations/practitioner.schema.ts +42 -32
  33. package/src/validations/procedure.schema.ts +14 -3
  34. package/src/validations/reviews.schema.ts +189 -0
  35. package/src/services/clinic/utils/review.utils.ts +0 -93
@@ -18,17 +18,19 @@ import {
18
18
  Clinic,
19
19
  CreateClinicData,
20
20
  CLINICS_COLLECTION,
21
- ClinicReview,
22
21
  ClinicTag,
23
22
  ClinicGroup,
24
23
  ClinicBranchSetupData,
25
24
  ClinicLocation,
26
25
  } from "../../../types/clinic";
27
- import { geohashForLocation } from "geofire-common";
26
+ import {
27
+ geohashForLocation,
28
+ distanceBetween,
29
+ geohashQueryBounds,
30
+ } from "geofire-common";
28
31
  import {
29
32
  clinicSchema,
30
33
  createClinicSchema,
31
- clinicReviewSchema,
32
34
  } from "../../../validations/clinic.schema";
33
35
  import { z } from "zod";
34
36
  import { FirebaseApp } from "firebase/app";
@@ -273,11 +275,18 @@ export async function createClinic(
273
275
  photosWithTags: processedPhotosWithTags,
274
276
  doctors: [],
275
277
  doctorsInfo: [],
276
- services: [],
277
- servicesInfo: [],
278
- reviews: [],
279
- reviewsInfo: [],
280
- rating: { average: 0, count: 0 },
278
+ procedures: [],
279
+ proceduresInfo: [],
280
+ reviewInfo: {
281
+ totalReviews: 0,
282
+ averageRating: 0,
283
+ cleanliness: 0,
284
+ facilities: 0,
285
+ staffFriendliness: 0,
286
+ waitingTime: 0,
287
+ accessibility: 0,
288
+ recommendationPercentage: 0,
289
+ },
281
290
  admins: [creatorAdminId],
282
291
  createdAt: now,
283
292
  updatedAt: now,
@@ -768,3 +777,173 @@ export async function getActiveClinicsByAdmin(
768
777
  clinicGroupService
769
778
  );
770
779
  }
780
+
781
+ /**
782
+ * Retrieves a clinic by its ID
783
+ *
784
+ * @param db - Firestore database instance
785
+ * @param clinicId - ID of the clinic to retrieve
786
+ * @returns The clinic if found, null otherwise
787
+ */
788
+ export async function getClinicById(
789
+ db: Firestore,
790
+ clinicId: string
791
+ ): Promise<Clinic | null> {
792
+ try {
793
+ const clinicRef = doc(db, CLINICS_COLLECTION, clinicId);
794
+ const clinicSnapshot = await getDoc(clinicRef);
795
+
796
+ if (!clinicSnapshot.exists()) {
797
+ return null;
798
+ }
799
+
800
+ const clinicData = clinicSnapshot.data() as Clinic;
801
+ return {
802
+ ...clinicData,
803
+ id: clinicSnapshot.id,
804
+ };
805
+ } catch (error) {
806
+ console.error("[CLINIC_UTILS] Error getting clinic by ID:", error);
807
+ throw error;
808
+ }
809
+ }
810
+
811
+ /**
812
+ * Retrieves all clinics with optional pagination
813
+ *
814
+ * @param db - Firestore database instance
815
+ * @param pagination - Optional number of clinics per page (0 or undefined returns all)
816
+ * @param lastDoc - Optional last document for pagination (if continuing from a previous page)
817
+ * @returns Array of clinics and the last document for pagination
818
+ */
819
+ export async function getAllClinics(
820
+ db: Firestore,
821
+ pagination?: number,
822
+ lastDoc?: any
823
+ ): Promise<{ clinics: Clinic[]; lastDoc: any }> {
824
+ try {
825
+ const clinicsCollection = collection(db, CLINICS_COLLECTION);
826
+ let clinicsQuery = query(clinicsCollection);
827
+
828
+ // If pagination is specified and greater than 0, limit the query
829
+ if (pagination && pagination > 0) {
830
+ const { limit, startAfter } = require("firebase/firestore");
831
+
832
+ if (lastDoc) {
833
+ clinicsQuery = query(
834
+ clinicsCollection,
835
+ startAfter(lastDoc),
836
+ limit(pagination)
837
+ );
838
+ } else {
839
+ clinicsQuery = query(clinicsCollection, limit(pagination));
840
+ }
841
+ }
842
+
843
+ const clinicsSnapshot = await getDocs(clinicsQuery);
844
+ const lastVisible = clinicsSnapshot.docs[clinicsSnapshot.docs.length - 1];
845
+
846
+ const clinics = clinicsSnapshot.docs.map((doc) => {
847
+ const data = doc.data() as Clinic;
848
+ return {
849
+ ...data,
850
+ id: doc.id,
851
+ };
852
+ });
853
+
854
+ return {
855
+ clinics,
856
+ lastDoc: lastVisible,
857
+ };
858
+ } catch (error) {
859
+ console.error("[CLINIC_UTILS] Error getting all clinics:", error);
860
+ throw error;
861
+ }
862
+ }
863
+
864
+ /**
865
+ * Retrieves all clinics within a specified range from a location with optional pagination
866
+ *
867
+ * @param db - Firestore database instance
868
+ * @param center - The center location coordinates {latitude, longitude}
869
+ * @param rangeInKm - The range in kilometers to search within
870
+ * @param pagination - Optional number of clinics per page (0 or undefined returns all)
871
+ * @param lastDoc - Optional last document for pagination (if continuing from a previous page)
872
+ * @returns Array of clinics with distance information and the last document for pagination
873
+ */
874
+ export async function getAllClinicsInRange(
875
+ db: Firestore,
876
+ center: { latitude: number; longitude: number },
877
+ rangeInKm: number,
878
+ pagination?: number,
879
+ lastDoc?: any
880
+ ): Promise<{ clinics: (Clinic & { distance: number })[]; lastDoc: any }> {
881
+ const bounds = geohashQueryBounds(
882
+ [center.latitude, center.longitude],
883
+ rangeInKm * 1000
884
+ );
885
+
886
+ const matchingClinics: (Clinic & { distance: number })[] = [];
887
+ let lastDocSnapshot = null;
888
+
889
+ for (const b of bounds) {
890
+ const constraints: QueryConstraint[] = [
891
+ where("location.geohash", ">=", b[0]),
892
+ where("location.geohash", "<=", b[1]),
893
+ where("isActive", "==", true),
894
+ ];
895
+
896
+ const q = query(collection(db, CLINICS_COLLECTION), ...constraints);
897
+ const querySnapshot = await getDocs(q);
898
+
899
+ for (const doc of querySnapshot.docs) {
900
+ const clinic = doc.data() as Clinic;
901
+ const distance = distanceBetween(
902
+ [center.latitude, center.longitude],
903
+ [clinic.location.latitude, clinic.location.longitude]
904
+ );
905
+
906
+ // Convert to kilometers
907
+ const distanceInKm = distance / 1000;
908
+
909
+ if (distanceInKm <= rangeInKm) {
910
+ matchingClinics.push({
911
+ ...clinic,
912
+ distance: distanceInKm,
913
+ });
914
+ }
915
+ }
916
+ }
917
+
918
+ // Sort by distance
919
+ matchingClinics.sort((a, b) => a.distance - b.distance);
920
+
921
+ if (pagination && pagination > 0) {
922
+ // Paginate results
923
+ let result = matchingClinics;
924
+ if (lastDoc && matchingClinics.length > 0) {
925
+ const lastIndex = matchingClinics.findIndex(
926
+ (clinic) => clinic.id === lastDoc.id
927
+ );
928
+ if (lastIndex !== -1) {
929
+ result = matchingClinics.slice(lastIndex + 1);
930
+ }
931
+ }
932
+
933
+ const paginatedClinics = result.slice(0, pagination);
934
+ const newLastDoc =
935
+ paginatedClinics.length > 0
936
+ ? paginatedClinics[paginatedClinics.length - 1]
937
+ : null;
938
+
939
+ return {
940
+ clinics: paginatedClinics,
941
+ lastDoc: newLastDoc,
942
+ };
943
+ }
944
+
945
+ return {
946
+ clinics: matchingClinics,
947
+ lastDoc: null,
948
+ };
949
+ }
@@ -0,0 +1,23 @@
1
+ import { Firestore } from "firebase/firestore";
2
+ import { Clinic, ClinicTag } from "../../../types/clinic";
3
+
4
+ export function getClinicsByFilters(
5
+ db: Firestore,
6
+ filters: {
7
+ center?: { latitude: number; longitude: number };
8
+ radiusInKm?: number;
9
+ tags?: ClinicTag[];
10
+ procedureFamily?: string;
11
+ procedureCategory?: string;
12
+ procedureSubcategory?: string;
13
+ procedureTechnology?: string;
14
+ minRating?: number;
15
+ maxRating?: number;
16
+ pagination?: number;
17
+ lastDoc?: any;
18
+ isActive?: boolean;
19
+ }
20
+ ): Promise<{
21
+ clinics: (Clinic & { distance?: number })[];
22
+ lastDoc: any;
23
+ }>;
@@ -0,0 +1,264 @@
1
+ import {
2
+ collection,
3
+ query,
4
+ where,
5
+ getDocs,
6
+ Firestore,
7
+ QueryConstraint,
8
+ startAfter,
9
+ limit,
10
+ documentId,
11
+ orderBy,
12
+ } from "firebase/firestore";
13
+ import { Clinic, ClinicTag, CLINICS_COLLECTION } from "../../../types/clinic";
14
+ import { geohashQueryBounds, distanceBetween } from "geofire-common";
15
+
16
+ /**
17
+ * Get clinics based on multiple filtering criteria
18
+ *
19
+ * @param db - Firestore database instance
20
+ * @param filters - Various filters to apply
21
+ * @returns Filtered clinics and the last document for pagination
22
+ */
23
+ export async function getClinicsByFilters(
24
+ db: Firestore,
25
+ filters: {
26
+ center?: { latitude: number; longitude: number };
27
+ radiusInKm?: number;
28
+ tags?: ClinicTag[];
29
+ procedureFamily?: string;
30
+ procedureCategory?: string;
31
+ procedureSubcategory?: string;
32
+ procedureTechnology?: string;
33
+ minRating?: number;
34
+ maxRating?: number;
35
+ pagination?: number;
36
+ lastDoc?: any;
37
+ isActive?: boolean;
38
+ }
39
+ ): Promise<{
40
+ clinics: (Clinic & { distance?: number })[];
41
+ lastDoc: any;
42
+ }> {
43
+ console.log(
44
+ "[FILTER_UTILS] Starting clinic filtering with criteria:",
45
+ filters
46
+ );
47
+
48
+ // Determine if we're doing a geo query or a regular query
49
+ const isGeoQuery =
50
+ filters.center && filters.radiusInKm && filters.radiusInKm > 0;
51
+
52
+ // Initialize base constraints
53
+ const constraints: QueryConstraint[] = [];
54
+
55
+ // Add active status filter (default to active if not specified)
56
+ if (filters.isActive !== undefined) {
57
+ constraints.push(where("isActive", "==", filters.isActive));
58
+ } else {
59
+ constraints.push(where("isActive", "==", true));
60
+ }
61
+
62
+ // Add tag filtering if specified
63
+ if (filters.tags && filters.tags.length > 0) {
64
+ // Note: We can only use array-contains-any once per query, so we're selecting one tag
65
+ // for the query and we'll filter the remaining tags in memory
66
+ constraints.push(where("tags", "array-contains", filters.tags[0]));
67
+ }
68
+
69
+ // Hierarchical procedure filter - only apply the most specific one provided
70
+ // Order of specificity: technology > subcategory > category > family
71
+ if (filters.procedureTechnology) {
72
+ constraints.push(
73
+ where("servicesInfo.technology", "==", filters.procedureTechnology)
74
+ );
75
+ } else if (filters.procedureSubcategory) {
76
+ constraints.push(
77
+ where("servicesInfo.subCategory", "==", filters.procedureSubcategory)
78
+ );
79
+ } else if (filters.procedureCategory) {
80
+ constraints.push(
81
+ where("servicesInfo.category", "==", filters.procedureCategory)
82
+ );
83
+ } else if (filters.procedureFamily) {
84
+ constraints.push(
85
+ where("servicesInfo.procedureFamily", "==", filters.procedureFamily)
86
+ );
87
+ }
88
+
89
+ // Add pagination if specified
90
+ if (filters.pagination && filters.pagination > 0 && filters.lastDoc) {
91
+ constraints.push(startAfter(filters.lastDoc));
92
+ constraints.push(limit(filters.pagination));
93
+ } else if (filters.pagination && filters.pagination > 0) {
94
+ constraints.push(limit(filters.pagination));
95
+ }
96
+
97
+ // Add ordering to make pagination consistent
98
+ constraints.push(orderBy(documentId()));
99
+
100
+ let clinicsResult: (Clinic & { distance?: number })[] = [];
101
+ let lastVisibleDoc = null;
102
+
103
+ // For geo queries, we need a different approach
104
+ if (isGeoQuery) {
105
+ const center = filters.center!;
106
+ const radiusInKm = filters.radiusInKm!;
107
+
108
+ // Get the geohash query bounds
109
+ const bounds = geohashQueryBounds(
110
+ [center.latitude, center.longitude],
111
+ radiusInKm * 1000 // Convert to meters
112
+ );
113
+
114
+ // Collect matching clinics from all bounds
115
+ const matchingClinics: (Clinic & { distance: number })[] = [];
116
+
117
+ // Execute queries for each bound
118
+ for (const bound of bounds) {
119
+ // Create a geo query for this bound
120
+ const geoConstraints = [
121
+ ...constraints,
122
+ where("location.geohash", ">=", bound[0]),
123
+ where("location.geohash", "<=", bound[1]),
124
+ ];
125
+
126
+ const q = query(collection(db, CLINICS_COLLECTION), ...geoConstraints);
127
+ const querySnapshot = await getDocs(q);
128
+
129
+ console.log(
130
+ `[FILTER_UTILS] Found ${querySnapshot.docs.length} clinics in geo bound`
131
+ );
132
+
133
+ // Process results and filter by actual distance
134
+ for (const doc of querySnapshot.docs) {
135
+ const clinic = { ...doc.data(), id: doc.id } as Clinic;
136
+
137
+ // Calculate actual distance
138
+ const distance = distanceBetween(
139
+ [center.latitude, center.longitude],
140
+ [clinic.location.latitude, clinic.location.longitude]
141
+ );
142
+
143
+ // Convert to kilometers
144
+ const distanceInKm = distance / 1000;
145
+
146
+ // Check if within radius
147
+ if (distanceInKm <= radiusInKm) {
148
+ // Add distance to clinic object
149
+ matchingClinics.push({
150
+ ...clinic,
151
+ distance: distanceInKm,
152
+ });
153
+ }
154
+ }
155
+ }
156
+
157
+ // Apply additional filters that couldn't be applied in the query
158
+ let filteredClinics = matchingClinics;
159
+
160
+ // Filter by multiple tags if more than one tag was specified
161
+ if (filters.tags && filters.tags.length > 1) {
162
+ filteredClinics = filteredClinics.filter((clinic) => {
163
+ // Check if clinic has all specified tags
164
+ return filters.tags!.every((tag) => clinic.tags.includes(tag));
165
+ });
166
+ }
167
+
168
+ // Filter by rating
169
+ if (filters.minRating !== undefined) {
170
+ filteredClinics = filteredClinics.filter(
171
+ (clinic) => clinic.reviewInfo.averageRating >= filters.minRating!
172
+ );
173
+ }
174
+
175
+ if (filters.maxRating !== undefined) {
176
+ filteredClinics = filteredClinics.filter(
177
+ (clinic) => clinic.reviewInfo.averageRating <= filters.maxRating!
178
+ );
179
+ }
180
+
181
+ // Sort by distance
182
+ filteredClinics.sort((a, b) => a.distance - b.distance);
183
+
184
+ // Apply pagination after all filters have been applied
185
+ if (filters.pagination && filters.pagination > 0) {
186
+ // If we have a lastDoc, find its index
187
+ let startIndex = 0;
188
+ if (filters.lastDoc) {
189
+ const lastDocIndex = filteredClinics.findIndex(
190
+ (clinic) => clinic.id === filters.lastDoc.id
191
+ );
192
+ if (lastDocIndex !== -1) {
193
+ startIndex = lastDocIndex + 1;
194
+ }
195
+ }
196
+
197
+ // Get paginated subset
198
+ const paginatedClinics = filteredClinics.slice(
199
+ startIndex,
200
+ startIndex + filters.pagination
201
+ );
202
+
203
+ // Set last document for next pagination
204
+ lastVisibleDoc =
205
+ paginatedClinics.length > 0
206
+ ? paginatedClinics[paginatedClinics.length - 1]
207
+ : null;
208
+
209
+ clinicsResult = paginatedClinics;
210
+ } else {
211
+ clinicsResult = filteredClinics;
212
+ }
213
+ } else {
214
+ // For non-geo queries, execute a single query with all constraints
215
+ const q = query(collection(db, CLINICS_COLLECTION), ...constraints);
216
+ const querySnapshot = await getDocs(q);
217
+
218
+ console.log(
219
+ `[FILTER_UTILS] Found ${querySnapshot.docs.length} clinics with regular query`
220
+ );
221
+
222
+ // Convert docs to clinics
223
+ const clinics = querySnapshot.docs.map((doc) => {
224
+ return { ...doc.data(), id: doc.id } as Clinic;
225
+ });
226
+
227
+ // Apply filters that couldn't be applied in the query
228
+ let filteredClinics = clinics;
229
+
230
+ // Filter by multiple tags if more than one tag was specified
231
+ if (filters.tags && filters.tags.length > 1) {
232
+ filteredClinics = filteredClinics.filter((clinic) => {
233
+ // Check if clinic has all specified tags
234
+ return filters.tags!.every((tag) => clinic.tags.includes(tag));
235
+ });
236
+ }
237
+
238
+ // Filter by rating
239
+ if (filters.minRating !== undefined) {
240
+ filteredClinics = filteredClinics.filter(
241
+ (clinic) => clinic.reviewInfo.averageRating >= filters.minRating!
242
+ );
243
+ }
244
+
245
+ if (filters.maxRating !== undefined) {
246
+ filteredClinics = filteredClinics.filter(
247
+ (clinic) => clinic.reviewInfo.averageRating <= filters.maxRating!
248
+ );
249
+ }
250
+
251
+ // Set last document for pagination
252
+ lastVisibleDoc =
253
+ querySnapshot.docs.length > 0
254
+ ? querySnapshot.docs[querySnapshot.docs.length - 1]
255
+ : null;
256
+
257
+ clinicsResult = filteredClinics;
258
+ }
259
+
260
+ return {
261
+ clinics: clinicsResult,
262
+ lastDoc: lastVisibleDoc,
263
+ };
264
+ }