@blackcode_sa/metaestetics-api 1.4.2 → 1.4.4

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.4.2",
4
+ "version": "1.4.4",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.mjs",
@@ -45,6 +45,21 @@ import { FirebaseError } from "../errors/firebase.errors";
45
45
  import { BaseService } from "./base.service";
46
46
  import { UserService } from "./user.service";
47
47
  import { throws } from "assert";
48
+ import {
49
+ ClinicGroup,
50
+ AdminToken,
51
+ AdminTokenStatus,
52
+ CreateClinicGroupData,
53
+ CreateClinicAdminData,
54
+ ContactPerson,
55
+ ClinicAdminSignupData,
56
+ SubscriptionModel,
57
+ CLINIC_GROUPS_COLLECTION,
58
+ } from "../types/clinic";
59
+ import { clinicAdminSignupSchema } from "../validations/clinic.schema";
60
+ import { ClinicGroupService } from "./clinic/clinic-group.service";
61
+ import { ClinicAdminService } from "./clinic/clinic-admin.service";
62
+ import { ClinicService } from "./clinic/clinic.service";
48
63
 
49
64
  export class AuthService extends BaseService {
50
65
  private googleProvider = new GoogleAuthProvider();
@@ -84,6 +99,168 @@ export class AuthService extends BaseService {
84
99
  return this.userService.createUser(firebaseUser, [initialRole]);
85
100
  }
86
101
 
102
+ /**
103
+ * Registers a new clinic admin user with email and password
104
+ * Can either create a new clinic group or join an existing one with a token
105
+ *
106
+ * @param data - Clinic admin signup data
107
+ * @returns The created user
108
+ */
109
+ async signUpClinicAdmin(data: ClinicAdminSignupData): Promise<User> {
110
+ try {
111
+ // Validate data
112
+ await clinicAdminSignupSchema.parseAsync(data);
113
+
114
+ // Create Firebase user
115
+ const { user: firebaseUser } = await createUserWithEmailAndPassword(
116
+ this.auth,
117
+ data.email,
118
+ data.password
119
+ );
120
+
121
+ // Create user with CLINIC_ADMIN role
122
+ const user = await this.userService.createUser(
123
+ firebaseUser,
124
+ [UserRole.CLINIC_ADMIN],
125
+ {
126
+ skipProfileCreation: true,
127
+ }
128
+ );
129
+
130
+ // Create contact person object
131
+ const contactPerson: ContactPerson = {
132
+ firstName: data.firstName,
133
+ lastName: data.lastName,
134
+ title: data.title,
135
+ email: data.email,
136
+ phoneNumber: data.phoneNumber,
137
+ };
138
+
139
+ // Initialize services
140
+ const clinicAdminService = new ClinicAdminService(
141
+ this.db,
142
+ this.auth,
143
+ this.app
144
+ );
145
+ const clinicGroupService = new ClinicGroupService(
146
+ this.db,
147
+ this.auth,
148
+ this.app,
149
+ clinicAdminService
150
+ );
151
+ const clinicService = new ClinicService(
152
+ this.db,
153
+ this.auth,
154
+ this.app,
155
+ clinicGroupService,
156
+ clinicAdminService
157
+ );
158
+
159
+ // Set services to resolve circular dependencies
160
+ clinicAdminService.setServices(clinicGroupService, clinicService);
161
+
162
+ if (data.isCreatingNewGroup) {
163
+ // Create new clinic group
164
+ if (!data.clinicGroupData) {
165
+ throw new Error(
166
+ "Clinic group data is required when creating a new group"
167
+ );
168
+ }
169
+
170
+ // Create clinic group
171
+ const createClinicGroupData: CreateClinicGroupData = {
172
+ name: data.clinicGroupData.name,
173
+ hqLocation: data.clinicGroupData.hqLocation,
174
+ contactInfo: data.clinicGroupData.contactInfo,
175
+ contactPerson: contactPerson,
176
+ ownerId: firebaseUser.uid,
177
+ isActive: true,
178
+ logo: data.clinicGroupData.logo,
179
+ subscriptionModel:
180
+ data.clinicGroupData.subscriptionModel ||
181
+ SubscriptionModel.NO_SUBSCRIPTION,
182
+ };
183
+
184
+ // Create clinic group
185
+ await clinicGroupService.createClinicGroup(
186
+ createClinicGroupData,
187
+ firebaseUser.uid,
188
+ true
189
+ );
190
+ } else {
191
+ // Join existing clinic group with token
192
+ if (!data.inviteToken) {
193
+ throw new Error(
194
+ "Invite token is required when joining an existing group"
195
+ );
196
+ }
197
+
198
+ // Find the token in the database
199
+ const groupsRef = collection(this.db, CLINIC_GROUPS_COLLECTION);
200
+ const q = query(groupsRef);
201
+ const querySnapshot = await getDocs(q);
202
+
203
+ let foundGroup: ClinicGroup | null = null;
204
+ let foundToken: AdminToken | null = null;
205
+
206
+ for (const docSnapshot of querySnapshot.docs) {
207
+ const group = docSnapshot.data() as ClinicGroup;
208
+
209
+ // Find the token in the group's tokens
210
+ const token = group.adminTokens.find(
211
+ (t) =>
212
+ t.token === data.inviteToken &&
213
+ t.status === AdminTokenStatus.ACTIVE &&
214
+ new Date(t.expiresAt.toDate()) > new Date()
215
+ );
216
+
217
+ if (token) {
218
+ foundGroup = group;
219
+ foundToken = token;
220
+ break;
221
+ }
222
+ }
223
+
224
+ if (!foundGroup || !foundToken) {
225
+ throw new Error("Invalid or expired invite token");
226
+ }
227
+
228
+ // Create clinic admin
229
+ const createClinicAdminData: CreateClinicAdminData = {
230
+ userRef: firebaseUser.uid,
231
+ clinicGroupId: foundGroup.id,
232
+ isGroupOwner: false,
233
+ clinicsManaged: [],
234
+ contactInfo: contactPerson,
235
+ roleTitle: data.title,
236
+ isActive: true,
237
+ };
238
+
239
+ await clinicAdminService.createClinicAdmin(createClinicAdminData);
240
+
241
+ // Mark token as used
242
+ await clinicGroupService.verifyAndUseAdminToken(
243
+ foundGroup.id,
244
+ data.inviteToken,
245
+ firebaseUser.uid
246
+ );
247
+ }
248
+
249
+ return user;
250
+ } catch (error) {
251
+ if (error instanceof z.ZodError) {
252
+ throw AUTH_ERRORS.VALIDATION_ERROR;
253
+ }
254
+
255
+ const firebaseError = error as FirebaseError;
256
+ if (firebaseError.code === FirebaseErrorCode.EMAIL_ALREADY_IN_USE) {
257
+ throw AUTH_ERRORS.EMAIL_ALREADY_EXISTS;
258
+ }
259
+
260
+ throw error;
261
+ }
262
+ }
263
+
87
264
  /**
88
265
  * Prijavljuje korisnika sa email-om i lozinkom
89
266
  */
@@ -10,6 +10,7 @@ import {
10
10
  deleteDoc,
11
11
  Timestamp,
12
12
  serverTimestamp,
13
+ FieldValue,
13
14
  } from "firebase/firestore";
14
15
  import { BaseService } from "../base.service";
15
16
  import {
@@ -19,6 +20,8 @@ import {
19
20
  AdminToken,
20
21
  AdminTokenStatus,
21
22
  CreateAdminTokenData,
23
+ ClinicGroupSetupData,
24
+ UpdateClinicGroupData,
22
25
  } from "../../types/clinic";
23
26
  import { ClinicAdminService } from "./clinic-admin.service";
24
27
  import { geohashForLocation } from "geofire-common";
@@ -107,6 +110,37 @@ export class ClinicGroupService extends BaseService {
107
110
  return ClinicGroupUtils.deactivateClinicGroup(this.db, groupId);
108
111
  }
109
112
 
113
+ /**
114
+ * Sets up additional clinic group information after initial creation
115
+ *
116
+ * @param groupId - The ID of the clinic group to set up
117
+ * @param setupData - The setup data for the clinic group
118
+ * @returns The updated clinic group
119
+ */
120
+ async setupClinicGroup(
121
+ groupId: string,
122
+ setupData: ClinicGroupSetupData
123
+ ): Promise<ClinicGroup> {
124
+ // Get the clinic group
125
+ const clinicGroup = await this.getClinicGroup(groupId);
126
+ if (!clinicGroup) {
127
+ throw new Error(`Clinic group with ID ${groupId} not found`);
128
+ }
129
+
130
+ // Update the clinic group with the setup data
131
+ const updateData = {
132
+ languages: setupData.languages,
133
+ practiceType: setupData.practiceType,
134
+ description: setupData.description,
135
+ logo: setupData.logo,
136
+ calendarSyncEnabled: setupData.calendarSyncEnabled,
137
+ autoConfirmAppointments: setupData.autoConfirmAppointments,
138
+ };
139
+
140
+ // Update the clinic group
141
+ return this.updateClinicGroup(groupId, updateData);
142
+ }
143
+
110
144
  /**
111
145
  * Kreira admin token za grupaciju
112
146
  */
@@ -12,6 +12,7 @@ import {
12
12
  serverTimestamp,
13
13
  GeoPoint,
14
14
  QueryConstraint,
15
+ FieldValue,
15
16
  } from "firebase/firestore";
16
17
  import { BaseService } from "../base.service";
17
18
  import {
@@ -22,6 +23,8 @@ import {
22
23
  ClinicTag,
23
24
  ClinicTags,
24
25
  ClinicGroup,
26
+ ClinicBranchSetupData,
27
+ CLINIC_ADMINS_COLLECTION,
25
28
  } from "../../types/clinic";
26
29
  import { ClinicGroupService } from "./clinic-group.service";
27
30
  import { ClinicAdminService } from "./clinic-admin.service";
@@ -43,6 +46,7 @@ import * as ClinicUtils from "./utils/clinic.utils";
43
46
  import * as ReviewUtils from "./utils/review.utils";
44
47
  import * as TagUtils from "./utils/tag.utils";
45
48
  import * as SearchUtils from "./utils/search.utils";
49
+ import * as AdminUtils from "./utils/admin.utils";
46
50
 
47
51
  export class ClinicService extends BaseService {
48
52
  private clinicGroupService: ClinicGroupService;
@@ -219,4 +223,53 @@ export class ClinicService extends BaseService {
219
223
  this.clinicGroupService
220
224
  );
221
225
  }
226
+
227
+ /**
228
+ * Creates a new clinic branch for a clinic group
229
+ *
230
+ * @param clinicGroupId - The ID of the clinic group
231
+ * @param setupData - The setup data for the clinic branch
232
+ * @param adminId - The ID of the admin creating the branch
233
+ * @returns The created clinic
234
+ */
235
+ async createClinicBranch(
236
+ clinicGroupId: string,
237
+ setupData: ClinicBranchSetupData,
238
+ adminId: string
239
+ ): Promise<Clinic> {
240
+ // Validate that the clinic group exists
241
+ const clinicGroup = await this.clinicGroupService.getClinicGroup(
242
+ clinicGroupId
243
+ );
244
+ if (!clinicGroup) {
245
+ throw new Error(`Clinic group with ID ${clinicGroupId} not found`);
246
+ }
247
+
248
+ // Create the clinic data
249
+ const createClinicData: CreateClinicData = {
250
+ clinicGroupId,
251
+ name: setupData.name,
252
+ description: setupData.description,
253
+ location: setupData.location,
254
+ contactInfo: setupData.contactInfo,
255
+ workingHours: setupData.workingHours,
256
+ tags: setupData.tags,
257
+ photos: setupData.photos,
258
+ photosWithTags: setupData.photosWithTags,
259
+ doctors: [],
260
+ services: [],
261
+ admins: [adminId],
262
+ isActive: true,
263
+ isVerified: false,
264
+ logo: setupData.logo,
265
+ featuredPhotos: setupData.featuredPhotos || [],
266
+ };
267
+
268
+ // Create the clinic
269
+ const clinic = await this.createClinic(createClinicData, adminId);
270
+
271
+ // Note: The createClinic method already adds the clinic to the admin's managed clinics
272
+
273
+ return clinic;
274
+ }
222
275
  }
@@ -86,6 +86,7 @@ export class UserService extends BaseService {
86
86
  groupToken?: string;
87
87
  groupId?: string;
88
88
  };
89
+ skipProfileCreation?: boolean;
89
90
  }
90
91
  ): Promise<User> {
91
92
  const userData: CreateUserData = {
@@ -142,6 +143,7 @@ export class UserService extends BaseService {
142
143
  groupToken?: string;
143
144
  groupId?: string;
144
145
  };
146
+ skipProfileCreation?: boolean;
145
147
  }
146
148
  ): Promise<{
147
149
  patientProfile?: string;
@@ -172,6 +174,12 @@ export class UserService extends BaseService {
172
174
  profiles.patientProfile = patientProfile.id;
173
175
  break;
174
176
  case UserRole.CLINIC_ADMIN:
177
+ // Skip profile creation if explicitly requested
178
+ // This is used when we know the profile will be created elsewhere (e.g. in signUpClinicAdmin)
179
+ if (options?.skipProfileCreation) {
180
+ break;
181
+ }
182
+
175
183
  // Ako imamo token, verifikujemo ga i dodajemo admina u postojeću grupu
176
184
  if (
177
185
  options?.clinicAdminData?.groupToken &&