@blackcode_sa/metaestetics-api 1.12.66 → 1.12.68

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.
@@ -23,6 +23,12 @@ import {
23
23
  import { BaseService } from "../../services/base.service";
24
24
  import { CATEGORIES_COLLECTION } from "../types/category.types";
25
25
 
26
+ /**
27
+ * ID of the free-consultation subcategory that should be hidden from admin backoffice.
28
+ * This subcategory is used internally for free consultation procedures.
29
+ */
30
+ const EXCLUDED_SUBCATEGORY_ID = 'free-consultation';
31
+
26
32
  /**
27
33
  * Servis za upravljanje podkategorijama procedura.
28
34
  * Podkategorije su drugi nivo organizacije i pripadaju određenoj kategoriji.
@@ -37,6 +43,14 @@ import { CATEGORIES_COLLECTION } from "../types/category.types";
37
43
  * });
38
44
  */
39
45
  export class SubcategoryService extends BaseService {
46
+ /**
47
+ * Filters out excluded subcategories from a list.
48
+ * @param subcategories - List of subcategories to filter
49
+ * @returns Filtered list without excluded subcategories
50
+ */
51
+ private filterExcludedSubcategories(subcategories: Subcategory[]): Subcategory[] {
52
+ return subcategories.filter(sub => sub.id !== EXCLUDED_SUBCATEGORY_ID);
53
+ }
40
54
  /**
41
55
  * Vraća referencu na Firestore kolekciju podkategorija za određenu kategoriju
42
56
  * @param categoryId - ID roditeljske kategorije
@@ -90,8 +104,10 @@ export class SubcategoryService extends BaseService {
90
104
  const categoryId = categoryDoc.id;
91
105
  const subcategoriesRef = this.getSubcategoriesRef(categoryId);
92
106
  const q = query(subcategoriesRef, where("isActive", "==", active));
93
- const snapshot = await getCountFromServer(q);
94
- counts[categoryId] = snapshot.data().count;
107
+ const snapshot = await getDocs(q);
108
+ // Filter out excluded subcategory and count
109
+ const filteredDocs = snapshot.docs.filter(doc => doc.id !== EXCLUDED_SUBCATEGORY_ID);
110
+ counts[categoryId] = filteredDocs.length;
95
111
  }
96
112
 
97
113
  return counts;
@@ -129,8 +145,9 @@ export class SubcategoryService extends BaseService {
129
145
  ...doc.data(),
130
146
  } as Subcategory)
131
147
  );
148
+ const filteredSubcategories = this.filterExcludedSubcategories(subcategories);
132
149
  const newLastVisible = querySnapshot.docs[querySnapshot.docs.length - 1];
133
- return { subcategories, lastVisible: newLastVisible };
150
+ return { subcategories: filteredSubcategories, lastVisible: newLastVisible };
134
151
  }
135
152
 
136
153
  /**
@@ -169,8 +186,9 @@ export class SubcategoryService extends BaseService {
169
186
  ...doc.data(),
170
187
  } as Subcategory)
171
188
  );
189
+ const filteredSubcategories = this.filterExcludedSubcategories(subcategories);
172
190
  const newLastVisible = querySnapshot.docs[querySnapshot.docs.length - 1];
173
- return { subcategories, lastVisible: newLastVisible };
191
+ return { subcategories: filteredSubcategories, lastVisible: newLastVisible };
174
192
  }
175
193
 
176
194
  /**
@@ -184,13 +202,14 @@ export class SubcategoryService extends BaseService {
184
202
  where("isActive", "==", true)
185
203
  );
186
204
  const querySnapshot = await getDocs(q);
187
- return querySnapshot.docs.map(
205
+ const subcategories = querySnapshot.docs.map(
188
206
  (doc) =>
189
207
  ({
190
208
  id: doc.id,
191
209
  ...doc.data(),
192
210
  } as Subcategory)
193
211
  );
212
+ return this.filterExcludedSubcategories(subcategories);
194
213
  }
195
214
 
196
215
  /**
@@ -203,13 +222,14 @@ export class SubcategoryService extends BaseService {
203
222
  where("isActive", "==", true)
204
223
  );
205
224
  const querySnapshot = await getDocs(q);
206
- return querySnapshot.docs.map(
225
+ const subcategories = querySnapshot.docs.map(
207
226
  (doc) =>
208
227
  ({
209
228
  id: doc.id,
210
229
  ...doc.data(),
211
230
  } as Subcategory)
212
231
  );
232
+ return this.filterExcludedSubcategories(subcategories);
213
233
  }
214
234
 
215
235
  /**
@@ -297,6 +317,26 @@ export class SubcategoryService extends BaseService {
297
317
  * @returns Podkategorija ili null ako ne postoji
298
318
  */
299
319
  async getById(categoryId: string, subcategoryId: string) {
320
+ // Prevent access to excluded subcategory
321
+ if (subcategoryId === EXCLUDED_SUBCATEGORY_ID) return null;
322
+
323
+ const docRef = doc(this.getSubcategoriesRef(categoryId), subcategoryId);
324
+ const docSnap = await getDoc(docRef);
325
+ if (!docSnap.exists()) return null;
326
+ return {
327
+ id: docSnap.id,
328
+ ...docSnap.data(),
329
+ } as Subcategory;
330
+ }
331
+
332
+ /**
333
+ * Internal method to get subcategory by ID without filtering.
334
+ * Used internally for consultation procedures.
335
+ * @param categoryId - ID of the category
336
+ * @param subcategoryId - ID of the subcategory to get
337
+ * @returns Subcategory or null if not found
338
+ */
339
+ async getByIdInternal(categoryId: string, subcategoryId: string): Promise<Subcategory | null> {
300
340
  const docRef = doc(this.getSubcategoriesRef(categoryId), subcategoryId);
301
341
  const docSnap = await getDoc(docRef);
302
342
  if (!docSnap.exists()) return null;
@@ -306,6 +346,30 @@ export class SubcategoryService extends BaseService {
306
346
  } as Subcategory;
307
347
  }
308
348
 
349
+ /**
350
+ * Finds a subcategory by exact name match within a specific category.
351
+ * Used for CSV import matching.
352
+ * @param name - Exact name of the subcategory to find
353
+ * @param categoryId - ID of the category to search within
354
+ * @returns Subcategory if found, null otherwise
355
+ */
356
+ async findByNameAndCategory(name: string, categoryId: string): Promise<Subcategory | null> {
357
+ const q = query(
358
+ this.getSubcategoriesRef(categoryId),
359
+ where('name', '==', name),
360
+ where('isActive', '==', true),
361
+ );
362
+ const querySnapshot = await getDocs(q);
363
+ if (querySnapshot.empty) return null;
364
+ const doc = querySnapshot.docs[0];
365
+ // Exclude free-consultation subcategory
366
+ if (doc.id === EXCLUDED_SUBCATEGORY_ID) return null;
367
+ return {
368
+ id: doc.id,
369
+ ...doc.data(),
370
+ } as Subcategory;
371
+ }
372
+
309
373
  /**
310
374
  * Exports subcategories to CSV string, suitable for Excel/Sheets.
311
375
  * Includes headers and optional UTF-8 BOM.
@@ -353,6 +417,8 @@ export class SubcategoryService extends BaseService {
353
417
  if (snapshot.empty) break;
354
418
 
355
419
  for (const d of snapshot.docs) {
420
+ // Exclude free-consultation subcategory from CSV export
421
+ if (d.id === EXCLUDED_SUBCATEGORY_ID) continue;
356
422
  const subcategory = ({ id: d.id, ...d.data() } as unknown) as Subcategory;
357
423
  rows.push(this.subcategoryToCsvRow(subcategory));
358
424
  }
@@ -33,6 +33,12 @@ import { ProcedureFamily } from '../types/static/procedure-family.types';
33
33
  import { Practitioner, PractitionerCertification } from '../../types/practitioner';
34
34
  import { Product, PRODUCTS_COLLECTION } from '../types/product.types';
35
35
 
36
+ /**
37
+ * ID of the free-consultation-tech technology that should be hidden from admin backoffice.
38
+ * This technology is used internally for free consultation procedures.
39
+ */
40
+ const EXCLUDED_TECHNOLOGY_ID = 'free-consultation-tech';
41
+
36
42
  /**
37
43
  * Default vrednosti za sertifikaciju
38
44
  */
@@ -45,6 +51,14 @@ const DEFAULT_CERTIFICATION_REQUIREMENT: CertificationRequirement = {
45
51
  * Service for managing technologies.
46
52
  */
47
53
  export class TechnologyService extends BaseService implements ITechnologyService {
54
+ /**
55
+ * Filters out excluded technologies from a list.
56
+ * @param technologies - List of technologies to filter
57
+ * @returns Filtered list without excluded technologies
58
+ */
59
+ private filterExcludedTechnologies(technologies: Technology[]): Technology[] {
60
+ return technologies.filter(tech => tech.id !== EXCLUDED_TECHNOLOGY_ID);
61
+ }
48
62
  /**
49
63
  * Reference to the Firestore collection of technologies.
50
64
  */
@@ -100,6 +114,8 @@ export class TechnologyService extends BaseService implements ITechnologyService
100
114
  const snapshot = await getDocs(q);
101
115
  const counts: Record<string, number> = {};
102
116
  snapshot.docs.forEach(doc => {
117
+ // Exclude free-consultation-tech from counts
118
+ if (doc.id === EXCLUDED_TECHNOLOGY_ID) return;
103
119
  const tech = doc.data() as Technology;
104
120
  counts[tech.subcategoryId] = (counts[tech.subcategoryId] || 0) + 1;
105
121
  });
@@ -116,6 +132,8 @@ export class TechnologyService extends BaseService implements ITechnologyService
116
132
  const snapshot = await getDocs(q);
117
133
  const counts: Record<string, number> = {};
118
134
  snapshot.docs.forEach(doc => {
135
+ // Exclude free-consultation-tech from counts
136
+ if (doc.id === EXCLUDED_TECHNOLOGY_ID) return;
119
137
  const tech = doc.data() as Technology;
120
138
  counts[tech.categoryId] = (counts[tech.categoryId] || 0) + 1;
121
139
  });
@@ -151,8 +169,9 @@ export class TechnologyService extends BaseService implements ITechnologyService
151
169
  ...doc.data(),
152
170
  } as Technology),
153
171
  );
172
+ const filteredTechnologies = this.filterExcludedTechnologies(technologies);
154
173
  const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
155
- return { technologies, lastVisible: newLastVisible };
174
+ return { technologies: filteredTechnologies, lastVisible: newLastVisible };
156
175
  }
157
176
 
158
177
  /**
@@ -187,8 +206,9 @@ export class TechnologyService extends BaseService implements ITechnologyService
187
206
  ...doc.data(),
188
207
  } as Technology),
189
208
  );
209
+ const filteredTechnologies = this.filterExcludedTechnologies(technologies);
190
210
  const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
191
- return { technologies, lastVisible: newLastVisible };
211
+ return { technologies: filteredTechnologies, lastVisible: newLastVisible };
192
212
  }
193
213
 
194
214
  /**
@@ -223,8 +243,9 @@ export class TechnologyService extends BaseService implements ITechnologyService
223
243
  ...doc.data(),
224
244
  } as Technology),
225
245
  );
246
+ const filteredTechnologies = this.filterExcludedTechnologies(technologies);
226
247
  const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
227
- return { technologies, lastVisible: newLastVisible };
248
+ return { technologies: filteredTechnologies, lastVisible: newLastVisible };
228
249
  }
229
250
 
230
251
  /**
@@ -300,6 +321,25 @@ export class TechnologyService extends BaseService implements ITechnologyService
300
321
  * @returns The technology or null if it doesn't exist.
301
322
  */
302
323
  async getById(id: string): Promise<Technology | null> {
324
+ // Prevent access to excluded technology
325
+ if (id === EXCLUDED_TECHNOLOGY_ID) return null;
326
+
327
+ const docRef = doc(this.technologiesRef, id);
328
+ const docSnap = await getDoc(docRef);
329
+ if (!docSnap.exists()) return null;
330
+ return {
331
+ id: docSnap.id,
332
+ ...docSnap.data(),
333
+ } as Technology;
334
+ }
335
+
336
+ /**
337
+ * Internal method to get technology by ID without filtering.
338
+ * Used internally for consultation procedures.
339
+ * @param id - The ID of the requested technology
340
+ * @returns The technology or null if it doesn't exist
341
+ */
342
+ async getByIdInternal(id: string): Promise<Technology | null> {
303
343
  const docRef = doc(this.technologiesRef, id);
304
344
  const docSnap = await getDoc(docRef);
305
345
  if (!docSnap.exists()) return null;
@@ -309,6 +349,29 @@ export class TechnologyService extends BaseService implements ITechnologyService
309
349
  } as Technology;
310
350
  }
311
351
 
352
+ /**
353
+ * Finds a technology by exact name match.
354
+ * Used for CSV import duplicate detection.
355
+ * @param name - Exact name of the technology to find
356
+ * @returns Technology if found, null otherwise
357
+ */
358
+ async findByName(name: string): Promise<Technology | null> {
359
+ const q = query(
360
+ this.technologiesRef,
361
+ where('name', '==', name),
362
+ where('isActive', '==', true),
363
+ );
364
+ const snapshot = await getDocs(q);
365
+ if (snapshot.empty) return null;
366
+ const doc = snapshot.docs[0];
367
+ // Exclude free-consultation-tech
368
+ if (doc.id === EXCLUDED_TECHNOLOGY_ID) return null;
369
+ return {
370
+ id: doc.id,
371
+ ...doc.data(),
372
+ } as Technology;
373
+ }
374
+
312
375
  /**
313
376
  * Dodaje novi zahtev tehnologiji
314
377
  * @param technologyId - ID tehnologije
@@ -759,13 +822,14 @@ export class TechnologyService extends BaseService implements ITechnologyService
759
822
  orderBy('name'),
760
823
  );
761
824
  const snapshot = await getDocs(q);
762
- return snapshot.docs.map(
825
+ const technologies = snapshot.docs.map(
763
826
  doc =>
764
827
  ({
765
828
  id: doc.id,
766
829
  ...doc.data(),
767
830
  } as Technology),
768
831
  );
832
+ return this.filterExcludedTechnologies(technologies);
769
833
  }
770
834
 
771
835
  /**
@@ -785,13 +849,14 @@ export class TechnologyService extends BaseService implements ITechnologyService
785
849
  orderBy('name'),
786
850
  );
787
851
  const snapshot = await getDocs(q);
788
- return snapshot.docs.map(
852
+ const technologies = snapshot.docs.map(
789
853
  doc =>
790
854
  ({
791
855
  id: doc.id,
792
856
  ...doc.data(),
793
857
  } as Technology),
794
858
  );
859
+ return this.filterExcludedTechnologies(technologies);
795
860
  }
796
861
 
797
862
  /**
@@ -804,13 +869,14 @@ export class TechnologyService extends BaseService implements ITechnologyService
804
869
  orderBy('name'),
805
870
  );
806
871
  const snapshot = await getDocs(q);
807
- return snapshot.docs.map(
872
+ const technologies = snapshot.docs.map(
808
873
  doc =>
809
874
  ({
810
875
  id: doc.id,
811
876
  ...doc.data(),
812
877
  } as Technology),
813
878
  );
879
+ return this.filterExcludedTechnologies(technologies);
814
880
  }
815
881
 
816
882
  // ==========================================
@@ -1019,6 +1085,8 @@ export class TechnologyService extends BaseService implements ITechnologyService
1019
1085
  if (snapshot.empty) break;
1020
1086
 
1021
1087
  for (const d of snapshot.docs) {
1088
+ // Exclude free-consultation-tech from CSV export
1089
+ if (d.id === EXCLUDED_TECHNOLOGY_ID) continue;
1022
1090
  const technology = ({ id: d.id, ...d.data() } as unknown) as Technology;
1023
1091
  // Fetch products for this technology
1024
1092
  const productNames = await this.getProductNamesForTechnology(technology.id!);
@@ -59,4 +59,9 @@ export interface ICategoryService {
59
59
  delete(id: string): Promise<void>;
60
60
  reactivate(id: string): Promise<void>;
61
61
  getById(id: string): Promise<Category | null>;
62
+ findByNameAndFamily(name: string, family: ProcedureFamily): Promise<Category | null>;
63
+ exportToCsv(options?: {
64
+ includeInactive?: boolean;
65
+ includeBom?: boolean;
66
+ }): Promise<string>;
62
67
  }
@@ -160,4 +160,9 @@ export interface ITechnologyService {
160
160
  getAllForFilterBySubcategory(subcategoryId: string): Promise<Technology[]>;
161
161
  getAllForFilterBySubcategoryId(categoryId: string, subcategoryId: string): Promise<Technology[]>;
162
162
  getAllForFilter(): Promise<Technology[]>;
163
+ findByName(name: string): Promise<Technology | null>;
164
+ exportToCsv(options?: {
165
+ includeInactive?: boolean;
166
+ includeBom?: boolean;
167
+ }): Promise<string>;
163
168
  }
@@ -1497,9 +1497,9 @@ export class ProcedureService extends BaseService {
1497
1497
  // Get references to related entities (Category, Subcategory, Technology)
1498
1498
  // For consultation, we don't need a product
1499
1499
  const [category, subcategory, technology] = await Promise.all([
1500
- this.categoryService.getById(data.categoryId),
1501
- this.subcategoryService.getById(data.categoryId, data.subcategoryId),
1502
- this.technologyService.getById(data.technologyId),
1500
+ this.categoryService.getByIdInternal(data.categoryId),
1501
+ this.subcategoryService.getByIdInternal(data.categoryId, data.subcategoryId),
1502
+ this.technologyService.getByIdInternal(data.technologyId),
1503
1503
  ]);
1504
1504
 
1505
1505
  if (!category || !subcategory || !technology) {