@blackcode_sa/metaestetics-api 1.14.78 → 1.15.2

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 (42) hide show
  1. package/dist/admin/index.d.mts +5 -0
  2. package/dist/admin/index.d.ts +5 -0
  3. package/dist/admin/index.js +47 -5
  4. package/dist/admin/index.mjs +47 -5
  5. package/dist/index.d.mts +302 -1
  6. package/dist/index.d.ts +302 -1
  7. package/dist/index.js +2655 -1754
  8. package/dist/index.mjs +1880 -983
  9. package/package.json +1 -1
  10. package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +59 -6
  11. package/src/services/__tests__/auth/auth.mock.test.ts +2 -2
  12. package/src/services/__tests__/auth/auth.setup.ts +6 -1
  13. package/src/services/__tests__/auth.service.test.ts +8 -44
  14. package/src/services/__tests__/base.service.test.ts +4 -45
  15. package/src/services/__tests__/user.service.test.ts +6 -4
  16. package/src/services/appointment/utils/appointment.utils.ts +0 -3
  17. package/src/services/appointment/utils/extended-procedure.utils.ts +1 -0
  18. package/src/services/auth/auth.v2.service.ts +7 -7
  19. package/src/services/clinic/__tests__/clinic-admin.service.test.ts +11 -33
  20. package/src/services/clinic/__tests__/clinic-group.service.test.ts +21 -151
  21. package/src/services/clinic/__tests__/clinic.service.test.ts +17 -69
  22. package/src/services/clinic/utils/clinic-group.utils.ts +2 -2
  23. package/src/services/clinic/utils/clinic.utils.ts +28 -22
  24. package/src/services/clinic/utils/index.ts +0 -1
  25. package/src/services/notifications/__tests__/notification.service.test.ts +5 -5
  26. package/src/services/patient/__tests__/patient.service.test.ts +17 -25
  27. package/src/services/patient/patient.service.ts +136 -0
  28. package/src/services/patient/utils/body-assessment.utils.ts +159 -0
  29. package/src/services/patient/utils/docs.utils.ts +1 -1
  30. package/src/services/patient/utils/hair-scalp-assessment.utils.ts +158 -0
  31. package/src/services/patient/utils/pre-surgical-assessment.utils.ts +161 -0
  32. package/src/services/patient/utils/skin-quality-assessment.utils.ts +160 -0
  33. package/src/services/user/user.v2.service.ts +4 -3
  34. package/src/types/patient/body-assessment.types.ts +93 -0
  35. package/src/types/patient/hair-scalp-assessment.types.ts +98 -0
  36. package/src/types/patient/index.ts +4 -0
  37. package/src/types/patient/pre-surgical-assessment.types.ts +95 -0
  38. package/src/types/patient/skin-quality-assessment.types.ts +105 -0
  39. package/src/validations/patient/body-assessment.schema.ts +82 -0
  40. package/src/validations/patient/hair-scalp-assessment.schema.ts +70 -0
  41. package/src/validations/patient/pre-surgical-assessment.schema.ts +78 -0
  42. package/src/validations/patient/skin-quality-assessment.schema.ts +70 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blackcode_sa/metaestetics-api",
3
3
  "private": false,
4
- "version": "1.14.78",
4
+ "version": "1.15.2",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -13,7 +13,7 @@ import {
13
13
  } from '../../../types/patient/patient-requirements';
14
14
  import {
15
15
  Requirement as RequirementTemplate,
16
- // REQUIREMENTS_COLLECTION as REQUIREMENTS_TEMPLATES_COLLECTION, // Not used directly after refactor
16
+ REQUIREMENTS_COLLECTION,
17
17
  RequirementType,
18
18
  TimeUnit, // Added import
19
19
  } from '../../../backoffice/types/requirement.types';
@@ -671,12 +671,17 @@ export class AppointmentAggregationService {
671
671
  // Store created instances for fallback direct creation if needed
672
672
  let createdInstances = [];
673
673
 
674
+ // Resolve string IDs to full RequirementTemplate objects
675
+ const resolvedPreRequirements = await this.resolveRequirements(
676
+ appointment.preProcedureRequirements as any,
677
+ );
678
+
674
679
  // Log more details about the pre-requirements
675
680
  Logger.info(
676
681
  `[AggService] Found ${
677
- appointment.preProcedureRequirements.length
682
+ resolvedPreRequirements.length
678
683
  } pre-requirements to process: ${JSON.stringify(
679
- appointment.preProcedureRequirements.map(r => ({
684
+ resolvedPreRequirements.map(r => ({
680
685
  id: r.id,
681
686
  name: r.name,
682
687
  type: r.type,
@@ -687,7 +692,7 @@ export class AppointmentAggregationService {
687
692
  )}`,
688
693
  );
689
694
 
690
- for (const template of appointment.preProcedureRequirements) {
695
+ for (const template of resolvedPreRequirements) {
691
696
  if (!template) {
692
697
  Logger.warn(
693
698
  `[AggService] Found null/undefined template in preProcedureRequirements array`,
@@ -933,6 +938,51 @@ export class AppointmentAggregationService {
933
938
  }
934
939
  }
935
940
 
941
+ /**
942
+ * Resolves an array of requirement items that may be string IDs or full Requirement objects.
943
+ * String IDs are batch-fetched from the backoffice_requirements collection.
944
+ */
945
+ private async resolveRequirements(
946
+ items: (string | RequirementTemplate)[],
947
+ ): Promise<RequirementTemplate[]> {
948
+ if (items.length === 0) return [];
949
+
950
+ const resolved: RequirementTemplate[] = [];
951
+ const idsToFetch: string[] = [];
952
+
953
+ for (const item of items) {
954
+ if (typeof item === 'string') {
955
+ idsToFetch.push(item);
956
+ } else if (item && typeof item === 'object' && item.id) {
957
+ resolved.push(item);
958
+ }
959
+ }
960
+
961
+ if (idsToFetch.length > 0) {
962
+ Logger.info(
963
+ `[AggService] Resolving ${idsToFetch.length} requirement ID(s) from ${REQUIREMENTS_COLLECTION}`,
964
+ );
965
+
966
+ // Firestore getAll supports up to 500 docs per call
967
+ const refs = idsToFetch.map(id =>
968
+ this.db.collection(REQUIREMENTS_COLLECTION).doc(id),
969
+ );
970
+ const snapshots = await this.db.getAll(...refs);
971
+
972
+ for (const snap of snapshots) {
973
+ if (snap.exists) {
974
+ resolved.push({ id: snap.id, ...snap.data() } as RequirementTemplate);
975
+ } else {
976
+ Logger.warn(
977
+ `[AggService] Requirement template '${snap.id}' not found in ${REQUIREMENTS_COLLECTION}`,
978
+ );
979
+ }
980
+ }
981
+ }
982
+
983
+ return resolved;
984
+ }
985
+
936
986
  /**
937
987
  * Fetches post-requirements from a procedure document
938
988
  * @param procedureId - The procedure ID to fetch requirements from
@@ -949,12 +999,15 @@ export class AppointmentAggregationService {
949
999
  }
950
1000
 
951
1001
  const procedure = procedureDoc.data() as Procedure;
952
- const postRequirements = procedure.postRequirements || [];
1002
+ const rawPostRequirements = procedure.postRequirements || [];
953
1003
 
954
- if (postRequirements.length === 0) {
1004
+ if (rawPostRequirements.length === 0) {
955
1005
  return [];
956
1006
  }
957
1007
 
1008
+ // Resolve string IDs to full Requirement objects if needed
1009
+ const postRequirements = await this.resolveRequirements(rawPostRequirements as any);
1010
+
958
1011
  return postRequirements.map(req => ({
959
1012
  requirement: req,
960
1013
  sourceProcedures: [
@@ -1,8 +1,8 @@
1
- import { setupMocks, mockFirebaseUser } from "./auth.setup";
1
+ import { setupService, mockFirebaseUser } from "./auth.setup";
2
2
 
3
3
  describe("Firebase Mock Setup", () => {
4
4
  beforeEach(() => {
5
- setupMocks();
5
+ setupService();
6
6
  });
7
7
 
8
8
  it("should properly initialize mock Firebase instance", async () => {
@@ -288,6 +288,11 @@ export const getMockUser = () => ({
288
288
 
289
289
  // Setup service
290
290
  export const setupService = () => {
291
- const service = new AuthService();
291
+ const service = new AuthService(
292
+ mockDb as unknown as Firestore,
293
+ mockAuth as unknown as Auth,
294
+ {} as FirebaseApp,
295
+ mockUserService
296
+ );
292
297
  return service;
293
298
  };
@@ -121,6 +121,10 @@ jest.mock("../../config/firebase", () => ({
121
121
 
122
122
  import { AuthService } from "../auth/auth.service";
123
123
  import { User as FirebaseUser } from "firebase/auth";
124
+ import { Firestore } from "firebase/firestore";
125
+ import { Auth } from "firebase/auth";
126
+ import { FirebaseApp } from "firebase/app";
127
+ import { UserService } from "../user/user.service";
124
128
  import { UserRole } from "../../types";
125
129
 
126
130
  const mockFacebookUser = {
@@ -150,8 +154,10 @@ describe("AuthService", () => {
150
154
  let service: AuthService;
151
155
 
152
156
  beforeEach(async () => {
153
- service = new AuthService();
154
- await service["initialized"]; // Čekamo da se inicijalizacija završi
157
+ const mockDb = {} as Firestore;
158
+ const mockApp = {} as FirebaseApp;
159
+ const mockUserService = {} as UserService;
160
+ service = new AuthService(mockDb, mockAuth as unknown as Auth, mockApp, mockUserService);
155
161
  });
156
162
 
157
163
  afterEach(() => {
@@ -243,48 +249,6 @@ describe("AuthService", () => {
243
249
  });
244
250
  });
245
251
 
246
- describe("signInWithFacebook", () => {
247
- it("should sign in user with Facebook", async () => {
248
- const expectedUser = {
249
- uid: "facebook-uid",
250
- email: "facebook@example.com",
251
- roles: [UserRole.PATIENT],
252
- isAnonymous: false,
253
- createdAt: expect.any(Date),
254
- updatedAt: expect.any(Date),
255
- lastLoginAt: expect.any(Date),
256
- patientProfile: null,
257
- practitionerProfile: null,
258
- adminProfile: null,
259
- };
260
-
261
- const result = await service.signInWithFacebook();
262
-
263
- expect(result).toEqual(expectedUser);
264
- });
265
- });
266
-
267
- describe("signInWithApple", () => {
268
- it("should sign in user with Apple", async () => {
269
- const expectedUser = {
270
- uid: "apple-uid",
271
- email: "apple@example.com",
272
- roles: [UserRole.PATIENT],
273
- isAnonymous: false,
274
- createdAt: expect.any(Date),
275
- updatedAt: expect.any(Date),
276
- lastLoginAt: expect.any(Date),
277
- patientProfile: null,
278
- practitionerProfile: null,
279
- adminProfile: null,
280
- };
281
-
282
- const result = await service.signInWithApple();
283
-
284
- expect(result).toEqual(expectedUser);
285
- });
286
- });
287
-
288
252
  describe("signInAnonymously", () => {
289
253
  it("should sign in user anonymously", async () => {
290
254
  const expectedUser = {
@@ -1,10 +1,7 @@
1
1
  import { BaseService } from "../base.service";
2
- import { getFirebaseInstance } from "../../config/firebase";
3
2
  import { Auth } from "firebase/auth";
4
3
  import { Firestore } from "firebase/firestore";
5
-
6
- // Mock Firebase config
7
- jest.mock("../../config/firebase");
4
+ import { FirebaseApp } from "firebase/app";
8
5
 
9
6
  // Test implementacija BaseService klase
10
7
  class TestService extends BaseService {
@@ -16,62 +13,24 @@ class TestService extends BaseService {
16
13
  public getAuth(): Auth {
17
14
  return this.auth;
18
15
  }
19
-
20
- // Expose initialize method for testing
21
- public async testEnsureInitialized() {
22
- await this.ensureInitialized();
23
- }
24
16
  }
25
17
 
26
18
  describe("BaseService", () => {
27
19
  let service: TestService;
28
20
  const mockDb = {} as Firestore;
29
21
  const mockAuth = {} as Auth;
22
+ const mockApp = {} as FirebaseApp;
30
23
 
31
24
  beforeEach(() => {
32
25
  jest.clearAllMocks();
33
- (getFirebaseInstance as jest.Mock).mockResolvedValue({
34
- db: mockDb,
35
- auth: mockAuth,
36
- });
37
26
  });
38
27
 
39
28
  describe("initialization", () => {
40
- it("treba da inicijalizuje Firebase servise", async () => {
41
- service = new TestService();
42
- await service.testEnsureInitialized();
29
+ it("treba da inicijalizuje Firebase servise", () => {
30
+ service = new TestService(mockDb, mockAuth, mockApp);
43
31
 
44
- expect(getFirebaseInstance).toHaveBeenCalled();
45
32
  expect(service.getDb()).toBe(mockDb);
46
33
  expect(service.getAuth()).toBe(mockAuth);
47
34
  });
48
-
49
- it("treba da hendla grešku pri inicijalizaciji", async () => {
50
- const mockError = new Error("Firebase init error");
51
- (getFirebaseInstance as jest.Mock).mockRejectedValue(mockError);
52
-
53
- service = new TestService();
54
-
55
- await expect(service.testEnsureInitialized()).rejects.toThrow(mockError);
56
- });
57
-
58
- it("treba da zadrži grešku inicijalizacije i baca je pri svakom pozivu", async () => {
59
- const mockError = new Error("Firebase init error");
60
- (getFirebaseInstance as jest.Mock).mockRejectedValue(mockError);
61
-
62
- service = new TestService();
63
-
64
- // Prvi pokušaj
65
- await expect(service.testEnsureInitialized()).rejects.toThrow(mockError);
66
-
67
- // Firebase je sada uspešno inicijalizovan
68
- (getFirebaseInstance as jest.Mock).mockResolvedValue({
69
- db: mockDb,
70
- auth: mockAuth,
71
- });
72
-
73
- // Ali i dalje treba da dobijemo originalnu grešku
74
- await expect(service.testEnsureInitialized()).rejects.toThrow(mockError);
75
- });
76
35
  });
77
36
  });
@@ -131,9 +131,11 @@ describe("UserService", () => {
131
131
  });
132
132
 
133
133
  userService = new UserService(
134
+ mockDb,
134
135
  mockAuth,
135
- new PatientService(),
136
- new ClinicAdminService()
136
+ {} as any,
137
+ new PatientService(mockDb, mockAuth, {} as any),
138
+ new ClinicAdminService(mockDb, mockAuth, {} as any)
137
139
  );
138
140
  });
139
141
 
@@ -460,7 +462,7 @@ describe("UserService", () => {
460
462
  });
461
463
  });
462
464
 
463
- describe("removeRole", () => {
465
+ describe("removeRoleAndProfile", () => {
464
466
  it("treba da ukloni ulogu korisniku", async () => {
465
467
  const userWithMultipleRoles = {
466
468
  ...mockUser,
@@ -487,7 +489,7 @@ describe("UserService", () => {
487
489
  data: () => updatedUser,
488
490
  });
489
491
 
490
- await userService.removeRole("test-uid", UserRole.PATIENT);
492
+ await userService.removeRoleAndProfile("test-uid", UserRole.PATIENT);
491
493
 
492
494
  // Provera da li je pozvan updateDoc sa ispravnim parametrima
493
495
  expect(updateDoc).toHaveBeenCalledWith(
@@ -33,9 +33,6 @@ import { PATIENTS_COLLECTION } from '../../../types/patient';
33
33
  import { CLINICS_COLLECTION } from '../../../types/clinic';
34
34
  import { BlockingCondition } from '../../../backoffice/types/static/blocking-condition.types';
35
35
  import { Requirement } from '../../../backoffice/types/requirement.types';
36
- import { PRACTITIONERS_COLLECTION } from '../../../types/practitioner';
37
- import { CLINICS_COLLECTION } from '../../../types/clinic';
38
- import { PATIENTS_COLLECTION } from '../../../types/patient';
39
36
  import { PROCEDURES_COLLECTION } from '../../../types/procedure';
40
37
  import { Technology, TECHNOLOGIES_COLLECTION } from '../../../backoffice/types/technology.types';
41
38
  import type { ContraindicationDynamic } from '../../../backoffice';
@@ -4,6 +4,7 @@ import {
4
4
  ExtendedProcedureInfo,
5
5
  AppointmentProductMetadata,
6
6
  APPOINTMENTS_COLLECTION,
7
+ LinkedFormInfo,
7
8
  } from '../../../types/appointment';
8
9
  import { getAppointmentOrThrow, initializeMetadata } from './zone-management.utils';
9
10
  import { PROCEDURES_COLLECTION } from '../../../types/procedure';
@@ -424,7 +424,7 @@ export class AuthServiceV2 extends BaseService {
424
424
  >(functions, "createAnonymousPatientProfile");
425
425
 
426
426
  const result = await createAnonymousPatientProfile({});
427
- return result.data.user;
427
+ return (result.data as any).user;
428
428
  }
429
429
  }
430
430
 
@@ -450,7 +450,7 @@ export class AuthServiceV2 extends BaseService {
450
450
  );
451
451
 
452
452
  const result = await createAnonymousPatientProfile({});
453
- return result.data.user;
453
+ return (result.data as any).user;
454
454
  }
455
455
  }
456
456
 
@@ -468,7 +468,7 @@ export class AuthServiceV2 extends BaseService {
468
468
  );
469
469
 
470
470
  const result = await createAnonymousPatientProfile({});
471
- return result.data.user;
471
+ return (result.data as any).user;
472
472
  }
473
473
 
474
474
  /**
@@ -545,7 +545,7 @@ export class AuthServiceV2 extends BaseService {
545
545
  },
546
546
  });
547
547
 
548
- return result.data.user;
548
+ return (result.data as any).user;
549
549
  } catch (error) {
550
550
  if (error instanceof z.ZodError) {
551
551
  throw AUTH_ERRORS.VALIDATION_ERROR;
@@ -598,7 +598,7 @@ export class AuthServiceV2 extends BaseService {
598
598
  },
599
599
  });
600
600
 
601
- return result.data.user;
601
+ return (result.data as any).user;
602
602
  } catch (error: unknown) {
603
603
  const firebaseError = error as FirebaseError;
604
604
  if (firebaseError.code === FirebaseErrorCode.POPUP_CLOSED_BY_USER) {
@@ -645,7 +645,7 @@ export class AuthServiceV2 extends BaseService {
645
645
  },
646
646
  });
647
647
 
648
- return result.data.user;
648
+ return (result.data as any).user;
649
649
  } catch (error: unknown) {
650
650
  const firebaseError = error as FirebaseError;
651
651
  if (firebaseError.code === FirebaseErrorCode.POPUP_CLOSED_BY_USER) {
@@ -693,7 +693,7 @@ export class AuthServiceV2 extends BaseService {
693
693
  },
694
694
  });
695
695
 
696
- return result.data.user;
696
+ return (result.data as any).user;
697
697
  } catch (error: unknown) {
698
698
  const firebaseError = error as FirebaseError;
699
699
  if (firebaseError.code === FirebaseErrorCode.POPUP_CLOSED_BY_USER) {
@@ -14,7 +14,7 @@ import {
14
14
  import {
15
15
  CreateClinicAdminData,
16
16
  ClinicAdmin,
17
- } from "../../../types/clinic-admin";
17
+ } from "../../../types/clinic";
18
18
  import { CLINIC_ADMIN_ERRORS } from "../../../errors/clinic.errors";
19
19
 
20
20
  // Mock Firebase
@@ -30,6 +30,7 @@ describe("ClinicAdminService", () => {
30
30
  clinicGroupId: "test-group-id",
31
31
  isGroupOwner: false,
32
32
  clinicsManaged: ["clinic-1", "clinic-2"],
33
+ clinicsManagedInfo: [],
33
34
  contactInfo: {
34
35
  firstName: "John",
35
36
  lastName: "Doe",
@@ -39,8 +40,8 @@ describe("ClinicAdminService", () => {
39
40
  },
40
41
  roleTitle: "Clinic Administrator",
41
42
  isActive: true,
42
- createdAt: mockTimestamp.now().toDate(),
43
- updatedAt: mockTimestamp.now().toDate(),
43
+ createdAt: mockTimestamp.now(),
44
+ updatedAt: mockTimestamp.now(),
44
45
  };
45
46
 
46
47
  beforeEach(() => {
@@ -53,7 +54,11 @@ describe("ClinicAdminService", () => {
53
54
  analytics: null,
54
55
  });
55
56
 
56
- clinicAdminService = new ClinicAdminService();
57
+ clinicAdminService = new ClinicAdminService(
58
+ {} as any,
59
+ {} as any,
60
+ {} as any
61
+ );
57
62
 
58
63
  // Mock Firestore functions
59
64
  (doc as jest.Mock).mockReturnValue("doc-ref");
@@ -218,41 +223,15 @@ describe("ClinicAdminService", () => {
218
223
  });
219
224
  });
220
225
 
221
- describe("setActive", () => {
222
- it("treba da aktivira/deaktivira clinic admina", async () => {
223
- await clinicAdminService.setActive(mockClinicAdmin.id, false);
224
-
225
- expect(updateDoc).toHaveBeenCalled();
226
- expect(updateDoc).toHaveBeenCalledWith(
227
- "doc-ref",
228
- expect.objectContaining({
229
- isActive: false,
230
- updatedAt: expect.any(Date),
231
- })
232
- );
233
- });
234
-
235
- it("treba da baci grešku ako admin ne postoji", async () => {
236
- (getDoc as jest.Mock).mockResolvedValueOnce({
237
- exists: () => false,
238
- });
239
-
240
- await expect(
241
- clinicAdminService.setActive("non-existent", false)
242
- ).rejects.toThrow(CLINIC_ADMIN_ERRORS.NOT_FOUND);
243
- });
244
- });
245
-
246
226
  describe("addClinicToManaged", () => {
247
227
  it("treba da doda kliniku admin-u", async () => {
248
228
  const clinicId = "new-clinic-id";
249
- const result = await clinicAdminService.addClinicToManaged(
229
+ await clinicAdminService.addClinicToManaged(
250
230
  mockClinicAdmin.id,
251
231
  clinicId
252
232
  );
253
233
 
254
234
  expect(updateDoc).toHaveBeenCalled();
255
- expect(result.clinicsManaged).toContain(clinicId);
256
235
  });
257
236
 
258
237
  it("treba da baci grešku ako klinika već postoji", async () => {
@@ -267,13 +246,12 @@ describe("ClinicAdminService", () => {
267
246
  describe("removeClinicFromManaged", () => {
268
247
  it("treba da ukloni kliniku iz admin-a", async () => {
269
248
  const clinicId = mockClinicAdmin.clinicsManaged[0];
270
- const result = await clinicAdminService.removeClinicFromManaged(
249
+ await clinicAdminService.removeClinicFromManaged(
271
250
  mockClinicAdmin.id,
272
251
  clinicId
273
252
  );
274
253
 
275
254
  expect(updateDoc).toHaveBeenCalled();
276
- expect(result.clinicsManaged).not.toContain(clinicId);
277
255
  });
278
256
 
279
257
  it("treba da baci grešku ako klinika ne postoji", async () => {