@blackcode_sa/metaestetics-api 1.8.14 → 1.8.16

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.8.14",
4
+ "version": "1.8.16",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -14,7 +14,7 @@ import { Clinic, ClinicTag, CLINICS_COLLECTION } from "../../../types/clinic";
14
14
  import { geohashQueryBounds, distanceBetween } from "geofire-common";
15
15
 
16
16
  /**
17
- * Get clinics based on multiple filtering criteria
17
+ * Get clinics based on multiple filtering criteria with fallback strategies
18
18
  *
19
19
  * @param db - Firestore database instance
20
20
  * @param filters - Various filters to apply
@@ -41,79 +41,264 @@ export async function getClinicsByFilters(
41
41
  clinics: (Clinic & { distance?: number })[];
42
42
  lastDoc: any;
43
43
  }> {
44
- // 1. Prepare Firestore constraints
45
- const constraints: QueryConstraint[] = [];
46
- constraints.push(where("isActive", "==", filters.isActive ?? true));
44
+ try {
45
+ console.log("[CLINIC_SERVICE] Starting clinic filtering with multiple strategies");
46
+
47
+ // Geo query debug i validacija
48
+ if (filters.center && filters.radiusInKm) {
49
+ console.log('[CLINIC_SERVICE] Executing geo query:', {
50
+ center: filters.center,
51
+ radius: filters.radiusInKm,
52
+ serviceName: 'ClinicService'
53
+ });
54
+
55
+ // Validacija location podataka
56
+ if (!filters.center.latitude || !filters.center.longitude) {
57
+ console.warn('[CLINIC_SERVICE] Invalid location data:', filters.center);
58
+ filters.center = undefined;
59
+ filters.radiusInKm = undefined;
60
+ }
61
+ }
47
62
 
48
- // Tag (only first, Firestore limitation)
49
- if (filters.tags && filters.tags.length > 0) {
50
- constraints.push(where("tags", "array-contains", filters.tags[0]));
51
- }
63
+ // Base constraints function (used in all strategies)
64
+ const getBaseConstraints = () => {
65
+ const constraints: QueryConstraint[] = [];
66
+ constraints.push(where("isActive", "==", filters.isActive ?? true));
67
+
68
+ // Tag (only first, due to Firestore limitation)
69
+ if (filters.tags && filters.tags.length > 0) {
70
+ constraints.push(where("tags", "array-contains", filters.tags[0]));
71
+ }
72
+
73
+ // Procedure filters (most specific first)
74
+ if (filters.procedureTechnology) {
75
+ constraints.push(where("servicesInfo.technology", "==", filters.procedureTechnology));
76
+ } else if (filters.procedureSubcategory) {
77
+ constraints.push(where("servicesInfo.subCategory", "==", filters.procedureSubcategory));
78
+ } else if (filters.procedureCategory) {
79
+ constraints.push(where("servicesInfo.category", "==", filters.procedureCategory));
80
+ } else if (filters.procedureFamily) {
81
+ constraints.push(where("servicesInfo.procedureFamily", "==", filters.procedureFamily));
82
+ }
83
+
84
+ // Rating filters
85
+ if (filters.minRating !== undefined) {
86
+ constraints.push(where("reviewInfo.averageRating", ">=", filters.minRating));
87
+ }
88
+ if (filters.maxRating !== undefined) {
89
+ constraints.push(where("reviewInfo.averageRating", "<=", filters.maxRating));
90
+ }
91
+
92
+ return constraints;
93
+ };
52
94
 
53
- // Procedure filters (most specific)
54
- if (filters.procedureTechnology) {
55
- constraints.push(where("servicesInfo.technology", "==", filters.procedureTechnology));
56
- } else if (filters.procedureSubcategory) {
57
- constraints.push(where("servicesInfo.subCategory", "==", filters.procedureSubcategory));
58
- } else if (filters.procedureCategory) {
59
- constraints.push(where("servicesInfo.category", "==", filters.procedureCategory));
60
- } else if (filters.procedureFamily) {
61
- constraints.push(where("servicesInfo.procedureFamily", "==", filters.procedureFamily));
62
- }
95
+ // Strategy 1: Try nameLower search if nameSearch exists
96
+ if (filters.nameSearch && filters.nameSearch.trim()) {
97
+ try {
98
+ console.log("[CLINIC_SERVICE] Strategy 1: Trying nameLower search");
99
+ const searchTerm = filters.nameSearch.trim().toLowerCase();
100
+ const constraints = getBaseConstraints();
101
+ constraints.push(where("nameLower", ">=", searchTerm));
102
+ constraints.push(where("nameLower", "<=", searchTerm + "\uf8ff"));
103
+ constraints.push(orderBy("nameLower"));
63
104
 
64
- // Text search by nameLower
65
- let useNameLower = false;
66
- let searchTerm = "";
67
- if (filters.nameSearch && filters.nameSearch.trim()) {
68
- searchTerm = filters.nameSearch.trim().toLowerCase();
69
- constraints.push(where("nameLower", ">=", searchTerm));
70
- constraints.push(where("nameLower", "<=", searchTerm + "\uf8ff"));
71
- useNameLower = true;
72
- }
105
+ if (filters.lastDoc) {
106
+ if (typeof filters.lastDoc.data === "function") {
107
+ constraints.push(startAfter(filters.lastDoc));
108
+ } else if (Array.isArray(filters.lastDoc)) {
109
+ constraints.push(startAfter(...filters.lastDoc));
110
+ } else {
111
+ constraints.push(startAfter(filters.lastDoc));
112
+ }
113
+ }
114
+ constraints.push(limit(filters.pagination || 5));
73
115
 
74
- // Rating filters
75
- if (filters.minRating !== undefined) {
76
- constraints.push(where("reviewInfo.averageRating", ">=", filters.minRating));
77
- }
78
- if (filters.maxRating !== undefined) {
79
- constraints.push(where("reviewInfo.averageRating", "<=", filters.maxRating));
80
- }
116
+ const q = query(collection(db, CLINICS_COLLECTION), ...constraints);
117
+ const querySnapshot = await getDocs(q);
118
+ let clinics = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Clinic));
119
+
120
+ // Apply in-memory filters
121
+ clinics = applyInMemoryFilters(clinics, filters);
122
+
123
+ const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
124
+
125
+ console.log(`[CLINIC_SERVICE] Strategy 1 success: ${clinics.length} clinics`);
126
+
127
+ // Fix Load More - ako je broj rezultata manji od pagination, nema više
128
+ if (clinics.length < (filters.pagination || 5)) {
129
+ return { clinics, lastDoc: null };
130
+ }
131
+ return { clinics, lastDoc };
132
+ } catch (error) {
133
+ console.log("[CLINIC_SERVICE] Strategy 1 failed:", error);
134
+ }
135
+ }
81
136
 
82
- // Pagination and ordering
83
- constraints.push(orderBy("nameLower"));
84
- if (filters.lastDoc) {
85
- if (typeof filters.lastDoc.data === "function") {
86
- constraints.push(startAfter(filters.lastDoc));
87
- } else if (Array.isArray(filters.lastDoc)) {
88
- constraints.push(startAfter(...filters.lastDoc));
89
- } else {
90
- constraints.push(startAfter(filters.lastDoc));
137
+ // Strategy 2: Try name field search as fallback
138
+ if (filters.nameSearch && filters.nameSearch.trim()) {
139
+ try {
140
+ console.log("[CLINIC_SERVICE] Strategy 2: Trying name field search");
141
+ const searchTerm = filters.nameSearch.trim().toLowerCase();
142
+ const constraints = getBaseConstraints();
143
+ constraints.push(where("name", ">=", searchTerm));
144
+ constraints.push(where("name", "<=", searchTerm + "\uf8ff"));
145
+ constraints.push(orderBy("name"));
146
+
147
+ if (filters.lastDoc) {
148
+ if (typeof filters.lastDoc.data === "function") {
149
+ constraints.push(startAfter(filters.lastDoc));
150
+ } else if (Array.isArray(filters.lastDoc)) {
151
+ constraints.push(startAfter(...filters.lastDoc));
152
+ } else {
153
+ constraints.push(startAfter(filters.lastDoc));
154
+ }
155
+ }
156
+ constraints.push(limit(filters.pagination || 5));
157
+
158
+ const q = query(collection(db, CLINICS_COLLECTION), ...constraints);
159
+ const querySnapshot = await getDocs(q);
160
+ let clinics = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Clinic));
161
+
162
+ // Apply in-memory filters
163
+ clinics = applyInMemoryFilters(clinics, filters);
164
+
165
+ const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
166
+
167
+ console.log(`[CLINIC_SERVICE] Strategy 2 success: ${clinics.length} clinics`);
168
+
169
+ // Fix Load More - ako je broj rezultata manji od pagination, nema više
170
+ if (clinics.length < (filters.pagination || 5)) {
171
+ return { clinics, lastDoc: null };
172
+ }
173
+ return { clinics, lastDoc };
174
+ } catch (error) {
175
+ console.log("[CLINIC_SERVICE] Strategy 2 failed:", error);
176
+ }
91
177
  }
92
- }
93
- constraints.push(limit(filters.pagination || 5));
94
178
 
95
- // 2. Firestore query
96
- const q = query(collection(db, CLINICS_COLLECTION), ...constraints);
97
- const querySnapshot = await getDocs(q);
98
- let clinics = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Clinic));
179
+ // Strategy 3: createdAt ordering with client-side name filtering
180
+ try {
181
+ console.log("[CLINIC_SERVICE] Strategy 3: Using createdAt ordering with client-side filtering");
182
+ const constraints = getBaseConstraints();
183
+ constraints.push(orderBy("createdAt", "desc"));
184
+
185
+ if (filters.lastDoc) {
186
+ if (typeof filters.lastDoc.data === "function") {
187
+ constraints.push(startAfter(filters.lastDoc));
188
+ } else if (Array.isArray(filters.lastDoc)) {
189
+ constraints.push(startAfter(...filters.lastDoc));
190
+ } else {
191
+ constraints.push(startAfter(filters.lastDoc));
192
+ }
193
+ }
194
+ constraints.push(limit(filters.pagination || 5));
99
195
 
100
- // 3. In-memory filters for multi-tag and geo-radius
196
+ const q = query(collection(db, CLINICS_COLLECTION), ...constraints);
197
+ const querySnapshot = await getDocs(q);
198
+ let clinics = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Clinic));
199
+
200
+ // Client-side name filtering
201
+ if (filters.nameSearch && filters.nameSearch.trim()) {
202
+ const searchTerm = filters.nameSearch.trim().toLowerCase();
203
+ clinics = clinics.filter(clinic => {
204
+ const name = (clinic.name || '').toLowerCase();
205
+ const nameLower = clinic.nameLower || '';
206
+ return name.includes(searchTerm) || nameLower.includes(searchTerm);
207
+ });
208
+ console.log(`[CLINIC_SERVICE] Applied name filter, results: ${clinics.length}`);
209
+ }
210
+
211
+ // Apply in-memory filters
212
+ clinics = applyInMemoryFilters(clinics, filters);
213
+
214
+ const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
215
+
216
+ console.log(`[CLINIC_SERVICE] Strategy 3 success: ${clinics.length} clinics`);
217
+
218
+ // Fix Load More - ako je broj rezultata manji od pagination, nema više
219
+ if (clinics.length < (filters.pagination || 5)) {
220
+ return { clinics, lastDoc: null };
221
+ }
222
+ return { clinics, lastDoc };
223
+ } catch (error) {
224
+ console.log("[CLINIC_SERVICE] Strategy 3 failed:", error);
225
+ }
226
+
227
+ // Strategy 4: Minimal fallback query
228
+ try {
229
+ console.log("[CLINIC_SERVICE] Strategy 4: Minimal fallback");
230
+ const constraints: QueryConstraint[] = [
231
+ where("isActive", "==", true),
232
+ orderBy("createdAt", "desc"),
233
+ limit(filters.pagination || 5)
234
+ ];
235
+
236
+ const q = query(collection(db, CLINICS_COLLECTION), ...constraints);
237
+ const querySnapshot = await getDocs(q);
238
+ let clinics = querySnapshot.docs.map(doc => ({ ...doc.data(), id: doc.id } as Clinic));
239
+
240
+ // Client-side name filtering
241
+ if (filters.nameSearch && filters.nameSearch.trim()) {
242
+ const searchTerm = filters.nameSearch.trim().toLowerCase();
243
+ clinics = clinics.filter(clinic => {
244
+ const name = (clinic.name || '').toLowerCase();
245
+ const nameLower = clinic.nameLower || '';
246
+ return name.includes(searchTerm) || nameLower.includes(searchTerm);
247
+ });
248
+ console.log(`[CLINIC_SERVICE] Applied name filter, results: ${clinics.length}`);
249
+ }
250
+
251
+ // Apply in-memory filters
252
+ clinics = applyInMemoryFilters(clinics, filters);
253
+
254
+ const lastDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
255
+
256
+ console.log(`[CLINIC_SERVICE] Strategy 4 success: ${clinics.length} clinics`);
257
+
258
+ // Fix Load More - ako je broj rezultata manji od pagination, nema više
259
+ if (clinics.length < (filters.pagination || 5)) {
260
+ return { clinics, lastDoc: null };
261
+ }
262
+ return { clinics, lastDoc };
263
+ } catch (error) {
264
+ console.log("[CLINIC_SERVICE] Strategy 4 failed:", error);
265
+ }
266
+
267
+ // All strategies failed
268
+ console.log("[CLINIC_SERVICE] All strategies failed, returning empty result");
269
+ return { clinics: [], lastDoc: null };
270
+
271
+ } catch (error) {
272
+ console.error("[CLINIC_SERVICE] Error filtering clinics:", error);
273
+ return { clinics: [], lastDoc: null };
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Helper function to apply in-memory filters that Firestore doesn't support well
279
+ */
280
+ function applyInMemoryFilters(clinics: Clinic[], filters: any): (Clinic & { distance?: number })[] {
281
+ // Multi-tag filter (Firestore only supports single array-contains)
101
282
  if (filters.tags && filters.tags.length > 1) {
102
- clinics = clinics.filter(clinic => filters.tags!.every(tag => clinic.tags.includes(tag)));
283
+ clinics = clinics.filter(clinic => filters.tags!.every((tag: ClinicTag) => clinic.tags.includes(tag)));
103
284
  }
285
+
286
+ // Geo-radius filter
104
287
  if (filters.center && filters.radiusInKm) {
105
288
  clinics = clinics.filter(clinic => {
106
289
  const distance = distanceBetween(
107
290
  [filters.center!.latitude, filters.center!.longitude],
108
291
  [clinic.location.latitude, clinic.location.longitude]
109
- ) / 1000;
110
- // Optionally attach distance for frontend
292
+ ) / 1000; // Convert to km
293
+
294
+ // Attach distance for frontend sorting/display
111
295
  (clinic as any).distance = distance;
112
296
  return distance <= filters.radiusInKm!;
113
297
  });
298
+
299
+ // Sort by distance when geo filtering is applied
300
+ clinics.sort((a, b) => ((a as any).distance || 0) - ((b as any).distance || 0));
114
301
  }
115
-
116
- // 4. Return results and lastDoc
117
- const lastVisibleDoc = querySnapshot.docs.length > 0 ? querySnapshot.docs[querySnapshot.docs.length - 1] : null;
118
- return { clinics, lastDoc: lastVisibleDoc };
302
+
303
+ return clinics as (Clinic & { distance?: number })[];
119
304
  }