@blackcode_sa/metaestetics-api 1.5.0 → 1.5.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.
package/dist/index.mjs CHANGED
@@ -3053,6 +3053,19 @@ import {
3053
3053
 
3054
3054
  // src/types/practitioner/index.ts
3055
3055
  var PRACTITIONERS_COLLECTION = "practitioners";
3056
+ var REGISTER_TOKENS_COLLECTION = "register_tokens";
3057
+ var PractitionerStatus = /* @__PURE__ */ ((PractitionerStatus2) => {
3058
+ PractitionerStatus2["DRAFT"] = "draft";
3059
+ PractitionerStatus2["ACTIVE"] = "active";
3060
+ return PractitionerStatus2;
3061
+ })(PractitionerStatus || {});
3062
+ var PractitionerTokenStatus = /* @__PURE__ */ ((PractitionerTokenStatus2) => {
3063
+ PractitionerTokenStatus2["ACTIVE"] = "active";
3064
+ PractitionerTokenStatus2["USED"] = "used";
3065
+ PractitionerTokenStatus2["EXPIRED"] = "expired";
3066
+ PractitionerTokenStatus2["REVOKED"] = "revoked";
3067
+ return PractitionerTokenStatus2;
3068
+ })(PractitionerTokenStatus || {});
3056
3069
 
3057
3070
  // src/validations/practitioner.schema.ts
3058
3071
  import { z as z10 } from "zod";
@@ -3164,6 +3177,7 @@ var practitionerSchema = z10.object({
3164
3177
  clinicWorkingHours: z10.array(practitionerClinicWorkingHoursSchema),
3165
3178
  isActive: z10.boolean(),
3166
3179
  isVerified: z10.boolean(),
3180
+ status: z10.nativeEnum(PractitionerStatus),
3167
3181
  createdAt: z10.instanceof(Timestamp7),
3168
3182
  updatedAt: z10.instanceof(Timestamp7)
3169
3183
  });
@@ -3174,7 +3188,35 @@ var createPractitionerSchema = z10.object({
3174
3188
  clinics: z10.array(z10.string()).optional(),
3175
3189
  clinicWorkingHours: z10.array(practitionerClinicWorkingHoursSchema).optional(),
3176
3190
  isActive: z10.boolean(),
3177
- isVerified: z10.boolean()
3191
+ isVerified: z10.boolean(),
3192
+ status: z10.nativeEnum(PractitionerStatus).optional()
3193
+ });
3194
+ var createDraftPractitionerSchema = z10.object({
3195
+ basicInfo: practitionerBasicInfoSchema,
3196
+ certification: practitionerCertificationSchema,
3197
+ clinics: z10.array(z10.string()).optional(),
3198
+ clinicWorkingHours: z10.array(practitionerClinicWorkingHoursSchema).optional(),
3199
+ isActive: z10.boolean().optional().default(false),
3200
+ isVerified: z10.boolean().optional().default(false)
3201
+ });
3202
+ var practitionerTokenSchema = z10.object({
3203
+ id: z10.string().min(1),
3204
+ token: z10.string().min(6),
3205
+ practitionerId: z10.string().min(1),
3206
+ email: z10.string().email(),
3207
+ clinicId: z10.string().min(1),
3208
+ status: z10.nativeEnum(PractitionerTokenStatus),
3209
+ createdBy: z10.string().min(1),
3210
+ createdAt: z10.instanceof(Timestamp7),
3211
+ expiresAt: z10.instanceof(Timestamp7),
3212
+ usedBy: z10.string().optional(),
3213
+ usedAt: z10.instanceof(Timestamp7).optional()
3214
+ });
3215
+ var createPractitionerTokenSchema = z10.object({
3216
+ practitionerId: z10.string().min(1),
3217
+ email: z10.string().email(),
3218
+ clinicId: z10.string().min(1),
3219
+ expiresAt: z10.date().optional()
3178
3220
  });
3179
3221
 
3180
3222
  // src/services/practitioner/practitioner.service.ts
@@ -3225,6 +3267,7 @@ var PractitionerService = class extends BaseService {
3225
3267
  clinicWorkingHours: validatedData.clinicWorkingHours || [],
3226
3268
  isActive: validatedData.isActive,
3227
3269
  isVerified: validatedData.isVerified,
3270
+ status: validatedData.status || "active" /* ACTIVE */,
3228
3271
  createdAt: serverTimestamp9(),
3229
3272
  updatedAt: serverTimestamp9()
3230
3273
  };
@@ -3249,6 +3292,200 @@ var PractitionerService = class extends BaseService {
3249
3292
  throw error;
3250
3293
  }
3251
3294
  }
3295
+ /**
3296
+ * Kreira novi draft profil zdravstvenog radnika bez povezanog korisnika
3297
+ * Koristi se od strane administratora klinike za kreiranje profila i kasnije pozivanje
3298
+ * @param data Podaci za kreiranje draft profila
3299
+ * @param createdBy ID administratora koji kreira profil
3300
+ * @param clinicId ID klinike za koju se kreira profil
3301
+ * @returns Objekt koji sadrži kreirani draft profil i token za registraciju
3302
+ */
3303
+ async createDraftPractitioner(data, createdBy, clinicId) {
3304
+ try {
3305
+ const validatedData = createDraftPractitionerSchema.parse(data);
3306
+ const clinic = await this.getClinicService().getClinic(clinicId);
3307
+ if (!clinic) {
3308
+ throw new Error(`Clinic ${clinicId} not found`);
3309
+ }
3310
+ const clinics = data.clinics || [clinicId];
3311
+ if (data.clinics) {
3312
+ for (const cId of data.clinics) {
3313
+ if (cId !== clinicId) {
3314
+ const otherClinic = await this.getClinicService().getClinic(cId);
3315
+ if (!otherClinic) {
3316
+ throw new Error(`Clinic ${cId} not found`);
3317
+ }
3318
+ }
3319
+ }
3320
+ }
3321
+ const practitionerId = this.generateId();
3322
+ const practitionerData = {
3323
+ id: practitionerId,
3324
+ userRef: "",
3325
+ // Prazno - biće popunjeno kada korisnik kreira nalog
3326
+ basicInfo: validatedData.basicInfo,
3327
+ certification: validatedData.certification,
3328
+ clinics,
3329
+ clinicWorkingHours: validatedData.clinicWorkingHours || [],
3330
+ isActive: validatedData.isActive !== void 0 ? validatedData.isActive : false,
3331
+ isVerified: validatedData.isVerified !== void 0 ? validatedData.isVerified : false,
3332
+ status: "draft" /* DRAFT */,
3333
+ createdAt: serverTimestamp9(),
3334
+ updatedAt: serverTimestamp9()
3335
+ };
3336
+ practitionerSchema.parse({
3337
+ ...practitionerData,
3338
+ userRef: "temp-for-validation",
3339
+ createdAt: Timestamp8.now(),
3340
+ updatedAt: Timestamp8.now()
3341
+ });
3342
+ await setDoc7(
3343
+ doc5(this.db, PRACTITIONERS_COLLECTION, practitionerData.id),
3344
+ practitionerData
3345
+ );
3346
+ const savedPractitioner = await this.getPractitioner(practitionerData.id);
3347
+ if (!savedPractitioner) {
3348
+ throw new Error("Failed to create draft practitioner profile");
3349
+ }
3350
+ const tokenString = this.generateId().slice(0, 6).toUpperCase();
3351
+ const expiration = new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3);
3352
+ const token = {
3353
+ id: this.generateId(),
3354
+ token: tokenString,
3355
+ practitionerId,
3356
+ email: practitionerData.basicInfo.email,
3357
+ clinicId,
3358
+ status: "active" /* ACTIVE */,
3359
+ createdBy,
3360
+ createdAt: Timestamp8.now(),
3361
+ expiresAt: Timestamp8.fromDate(expiration)
3362
+ };
3363
+ practitionerTokenSchema.parse(token);
3364
+ const tokenPath = `${PRACTITIONERS_COLLECTION}/${practitionerId}/${REGISTER_TOKENS_COLLECTION}/${token.id}`;
3365
+ await setDoc7(doc5(this.db, tokenPath), token);
3366
+ return { practitioner: savedPractitioner, token };
3367
+ } catch (error) {
3368
+ if (error instanceof z11.ZodError) {
3369
+ throw new Error("Invalid practitioner data: " + error.message);
3370
+ }
3371
+ throw error;
3372
+ }
3373
+ }
3374
+ /**
3375
+ * Creates a token for inviting practitioner to claim their profile
3376
+ * @param data Data for creating token
3377
+ * @param createdBy ID of the user creating the token
3378
+ * @returns Created token
3379
+ */
3380
+ async createPractitionerToken(data, createdBy) {
3381
+ try {
3382
+ const validatedData = createPractitionerTokenSchema.parse(data);
3383
+ const practitioner = await this.getPractitioner(
3384
+ validatedData.practitionerId
3385
+ );
3386
+ if (!practitioner) {
3387
+ throw new Error("Practitioner not found");
3388
+ }
3389
+ if (practitioner.status !== "draft" /* DRAFT */) {
3390
+ throw new Error(
3391
+ "Can only create tokens for practitioners in DRAFT status"
3392
+ );
3393
+ }
3394
+ const clinic = await this.getClinicService().getClinic(
3395
+ validatedData.clinicId
3396
+ );
3397
+ if (!clinic) {
3398
+ throw new Error(`Clinic ${validatedData.clinicId} not found`);
3399
+ }
3400
+ if (!practitioner.clinics.includes(validatedData.clinicId)) {
3401
+ throw new Error("Practitioner is not associated with this clinic");
3402
+ }
3403
+ const expiration = validatedData.expiresAt || new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3);
3404
+ const tokenString = this.generateId().slice(0, 6).toUpperCase();
3405
+ const token = {
3406
+ id: this.generateId(),
3407
+ token: tokenString,
3408
+ practitionerId: validatedData.practitionerId,
3409
+ email: validatedData.email,
3410
+ clinicId: validatedData.clinicId,
3411
+ status: "active" /* ACTIVE */,
3412
+ createdBy,
3413
+ createdAt: Timestamp8.now(),
3414
+ expiresAt: Timestamp8.fromDate(expiration)
3415
+ };
3416
+ practitionerTokenSchema.parse(token);
3417
+ const tokenPath = `${PRACTITIONERS_COLLECTION}/${validatedData.practitionerId}/${REGISTER_TOKENS_COLLECTION}/${token.id}`;
3418
+ await setDoc7(doc5(this.db, tokenPath), token);
3419
+ return token;
3420
+ } catch (error) {
3421
+ if (error instanceof z11.ZodError) {
3422
+ throw new Error("Invalid token data: " + error.message);
3423
+ }
3424
+ throw error;
3425
+ }
3426
+ }
3427
+ /**
3428
+ * Gets active tokens for a practitioner
3429
+ * @param practitionerId ID of the practitioner
3430
+ * @returns Array of active tokens
3431
+ */
3432
+ async getPractitionerActiveTokens(practitionerId) {
3433
+ const tokensRef = collection3(
3434
+ this.db,
3435
+ `${PRACTITIONERS_COLLECTION}/${practitionerId}/${REGISTER_TOKENS_COLLECTION}`
3436
+ );
3437
+ const q = query3(
3438
+ tokensRef,
3439
+ where3("status", "==", "active" /* ACTIVE */),
3440
+ where3("expiresAt", ">", Timestamp8.now())
3441
+ );
3442
+ const querySnapshot = await getDocs3(q);
3443
+ return querySnapshot.docs.map((doc20) => doc20.data());
3444
+ }
3445
+ /**
3446
+ * Gets a token by its string value and validates it
3447
+ * @param tokenString The token string to find
3448
+ * @returns The token if found and valid, null otherwise
3449
+ */
3450
+ async validateToken(tokenString) {
3451
+ const practitionersRef = collection3(this.db, PRACTITIONERS_COLLECTION);
3452
+ const practitionersSnapshot = await getDocs3(practitionersRef);
3453
+ for (const practitionerDoc of practitionersSnapshot.docs) {
3454
+ const practitionerId = practitionerDoc.id;
3455
+ const tokensRef = collection3(
3456
+ this.db,
3457
+ `${PRACTITIONERS_COLLECTION}/${practitionerId}/${REGISTER_TOKENS_COLLECTION}`
3458
+ );
3459
+ const q = query3(
3460
+ tokensRef,
3461
+ where3("token", "==", tokenString),
3462
+ where3("status", "==", "active" /* ACTIVE */),
3463
+ where3("expiresAt", ">", Timestamp8.now())
3464
+ );
3465
+ const tokenSnapshot = await getDocs3(q);
3466
+ if (!tokenSnapshot.empty) {
3467
+ return tokenSnapshot.docs[0].data();
3468
+ }
3469
+ }
3470
+ return null;
3471
+ }
3472
+ /**
3473
+ * Marks a token as used
3474
+ * @param tokenId ID of the token
3475
+ * @param practitionerId ID of the practitioner
3476
+ * @param userId ID of the user using the token
3477
+ */
3478
+ async markTokenAsUsed(tokenId, practitionerId, userId) {
3479
+ const tokenRef = doc5(
3480
+ this.db,
3481
+ `${PRACTITIONERS_COLLECTION}/${practitionerId}/${REGISTER_TOKENS_COLLECTION}/${tokenId}`
3482
+ );
3483
+ await updateDoc8(tokenRef, {
3484
+ status: "used" /* USED */,
3485
+ usedBy: userId,
3486
+ usedAt: Timestamp8.now()
3487
+ });
3488
+ }
3252
3489
  /**
3253
3490
  * Dohvata zdravstvenog radnika po ID-u
3254
3491
  */
@@ -3282,7 +3519,20 @@ var PractitionerService = class extends BaseService {
3282
3519
  const q = query3(
3283
3520
  collection3(this.db, PRACTITIONERS_COLLECTION),
3284
3521
  where3("clinics", "array-contains", clinicId),
3285
- where3("isActive", "==", true)
3522
+ where3("isActive", "==", true),
3523
+ where3("status", "==", "active" /* ACTIVE */)
3524
+ );
3525
+ const querySnapshot = await getDocs3(q);
3526
+ return querySnapshot.docs.map((doc20) => doc20.data());
3527
+ }
3528
+ /**
3529
+ * Dohvata sve draft zdravstvene radnike za određenu kliniku
3530
+ */
3531
+ async getDraftPractitionersByClinic(clinicId) {
3532
+ const q = query3(
3533
+ collection3(this.db, PRACTITIONERS_COLLECTION),
3534
+ where3("clinics", "array-contains", clinicId),
3535
+ where3("status", "==", "draft" /* DRAFT */)
3286
3536
  );
3287
3537
  const querySnapshot = await getDocs3(q);
3288
3538
  return querySnapshot.docs.map((doc20) => doc20.data());
@@ -3391,6 +3641,35 @@ var PractitionerService = class extends BaseService {
3391
3641
  }
3392
3642
  await deleteDoc2(doc5(this.db, PRACTITIONERS_COLLECTION, practitionerId));
3393
3643
  }
3644
+ /**
3645
+ * Validates a registration token and claims the associated draft practitioner profile
3646
+ * @param tokenString The token provided by the practitioner
3647
+ * @param userId The ID of the user claiming the profile
3648
+ * @returns The claimed practitioner profile or null if token is invalid
3649
+ */
3650
+ async validateTokenAndClaimProfile(tokenString, userId) {
3651
+ const token = await this.validateToken(tokenString);
3652
+ if (!token) {
3653
+ return null;
3654
+ }
3655
+ const practitioner = await this.getPractitioner(token.practitionerId);
3656
+ if (!practitioner) {
3657
+ return null;
3658
+ }
3659
+ if (practitioner.status !== "draft" /* DRAFT */) {
3660
+ throw new Error("This practitioner profile has already been claimed");
3661
+ }
3662
+ const existingPractitioner = await this.getPractitionerByUserRef(userId);
3663
+ if (existingPractitioner) {
3664
+ throw new Error("User already has a practitioner profile");
3665
+ }
3666
+ const updatedPractitioner = await this.updatePractitioner(practitioner.id, {
3667
+ userRef: userId,
3668
+ status: "active" /* ACTIVE */
3669
+ });
3670
+ await this.markTokenAsUsed(token.id, token.practitionerId, userId);
3671
+ return updatedPractitioner;
3672
+ }
3394
3673
  };
3395
3674
 
3396
3675
  // src/services/user.service.ts
@@ -3609,25 +3888,6 @@ var UserService = class extends BaseService {
3609
3888
  ...updates,
3610
3889
  updatedAt: serverTimestamp10()
3611
3890
  };
3612
- updatedUser.roles.forEach((role) => {
3613
- switch (role) {
3614
- case "patient" /* PATIENT */:
3615
- if (!updatedUser.patientProfile) {
3616
- throw new Error(`Missing patient profile for role ${role}`);
3617
- }
3618
- break;
3619
- case "practitioner" /* PRACTITIONER */:
3620
- if (!updatedUser.practitionerProfile) {
3621
- throw new Error(`Missing practitioner profile for role ${role}`);
3622
- }
3623
- break;
3624
- case "clinic_admin" /* CLINIC_ADMIN */:
3625
- if (!updatedUser.adminProfile) {
3626
- throw new Error(`Missing admin profile for role ${role}`);
3627
- }
3628
- break;
3629
- }
3630
- });
3631
3891
  userSchema.parse(updatedUser);
3632
3892
  await updateDoc9(userRef, {
3633
3893
  ...updates,
@@ -9129,8 +9389,11 @@ export {
9129
9389
  PatientService,
9130
9390
  PracticeType,
9131
9391
  PractitionerService,
9392
+ PractitionerStatus,
9393
+ PractitionerTokenStatus,
9132
9394
  PricingMeasure,
9133
9395
  ProcedureFamily,
9396
+ REGISTER_TOKENS_COLLECTION,
9134
9397
  SYNCED_CALENDARS_COLLECTION,
9135
9398
  SubscriptionModel,
9136
9399
  SyncedCalendarProvider,
@@ -9175,11 +9438,13 @@ export {
9175
9438
  createClinicSchema,
9176
9439
  createDefaultClinicGroupSchema,
9177
9440
  createDocumentTemplateSchema,
9441
+ createDraftPractitionerSchema,
9178
9442
  createPatientLocationInfoSchema,
9179
9443
  createPatientMedicalInfoSchema,
9180
9444
  createPatientProfileSchema,
9181
9445
  createPatientSensitiveInfoSchema,
9182
9446
  createPractitionerSchema,
9447
+ createPractitionerTokenSchema,
9183
9448
  createUserOptionsSchema,
9184
9449
  doctorInfoSchema,
9185
9450
  documentElementSchema,
@@ -9212,6 +9477,7 @@ export {
9212
9477
  practitionerProfileInfoSchema,
9213
9478
  practitionerReviewSchema,
9214
9479
  practitionerSchema,
9480
+ practitionerTokenSchema,
9215
9481
  practitionerWorkingHoursSchema,
9216
9482
  preRequirementNotificationSchema,
9217
9483
  procedureCategorizationSchema,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blackcode_sa/metaestetics-api",
3
3
  "private": false,
4
- "version": "1.5.0",
4
+ "version": "1.5.2",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.mjs",
package/src/index.ts CHANGED
@@ -123,8 +123,16 @@ export type {
123
123
  PractitionerReview,
124
124
  PractitionerWorkingHours,
125
125
  PractitionerClinicWorkingHours,
126
+ CreateDraftPractitionerData,
127
+ PractitionerToken,
128
+ CreatePractitionerTokenData,
129
+ } from "./types/practitioner";
130
+ export {
131
+ PRACTITIONERS_COLLECTION,
132
+ REGISTER_TOKENS_COLLECTION,
133
+ PractitionerStatus,
134
+ PractitionerTokenStatus,
126
135
  } from "./types/practitioner";
127
- export { PRACTITIONERS_COLLECTION } from "./types/practitioner";
128
136
 
129
137
  // Clinic types
130
138
  export type {