@blackcode_sa/metaestetics-api 1.7.19 → 1.7.20

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.7.19",
4
+ "version": "1.7.20",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.mjs",
@@ -613,7 +613,10 @@ export class BookingAdmin {
613
613
  };
614
614
  const practitionerInfo: PractitionerProfileInfo = {
615
615
  id: practitionerSnap.id,
616
- practitionerPhoto: practitionerData.basicInfo.profileImageUrl || null,
616
+ practitionerPhoto:
617
+ typeof practitionerData.basicInfo.profileImageUrl === "string"
618
+ ? practitionerData.basicInfo.profileImageUrl
619
+ : null,
617
620
  name: `${practitionerData.basicInfo.firstName} ${practitionerData.basicInfo.lastName}`,
618
621
  email: practitionerData.basicInfo.email,
619
622
  phone: practitionerData.basicInfo.phoneNumber || null,
@@ -34,6 +34,11 @@ import {
34
34
  } from "../../types/practitioner";
35
35
  import { ProcedureSummaryInfo } from "../../types/procedure";
36
36
  import { ClinicService } from "../clinic/clinic.service";
37
+ import {
38
+ MediaService,
39
+ MediaAccessLevel,
40
+ MediaResource,
41
+ } from "../media/media.service";
37
42
  import {
38
43
  practitionerSchema,
39
44
  createPractitionerSchema,
@@ -53,6 +58,7 @@ import { ClinicInfo } from "../../types/profile";
53
58
 
54
59
  export class PractitionerService extends BaseService {
55
60
  private clinicService?: ClinicService;
61
+ private mediaService: MediaService;
56
62
 
57
63
  constructor(
58
64
  db: Firestore,
@@ -62,6 +68,7 @@ export class PractitionerService extends BaseService {
62
68
  ) {
63
69
  super(db, auth, app);
64
70
  this.clinicService = clinicService;
71
+ this.mediaService = new MediaService(db, auth, app);
65
72
  }
66
73
 
67
74
  private getClinicService(): ClinicService {
@@ -75,6 +82,71 @@ export class PractitionerService extends BaseService {
75
82
  this.clinicService = clinicService;
76
83
  }
77
84
 
85
+ /**
86
+ * Handles profile photo upload for practitioners
87
+ * @param profilePhoto - MediaResource (File, Blob, or URL string)
88
+ * @param practitionerId - ID of the practitioner
89
+ * @returns URL string of the uploaded or existing photo
90
+ */
91
+ private async handleProfilePhotoUpload(
92
+ profilePhoto: MediaResource | undefined,
93
+ practitionerId: string
94
+ ): Promise<string | undefined> {
95
+ if (!profilePhoto) {
96
+ return undefined;
97
+ }
98
+
99
+ // If it's already a URL string, return it as is
100
+ if (typeof profilePhoto === "string") {
101
+ return profilePhoto;
102
+ }
103
+
104
+ // If it's a File or Blob, upload it
105
+ if (profilePhoto instanceof File || profilePhoto instanceof Blob) {
106
+ console.log(
107
+ `[PractitionerService] Uploading profile photo for practitioner ${practitionerId}`
108
+ );
109
+
110
+ const mediaMetadata = await this.mediaService.uploadMedia(
111
+ profilePhoto,
112
+ practitionerId, // Using practitionerId as ownerId
113
+ MediaAccessLevel.PUBLIC, // Profile photos should be public
114
+ "practitioner_profile_photos",
115
+ profilePhoto instanceof File
116
+ ? profilePhoto.name
117
+ : `profile_photo_${practitionerId}`
118
+ );
119
+
120
+ return mediaMetadata.url;
121
+ }
122
+
123
+ return undefined;
124
+ }
125
+
126
+ /**
127
+ * Processes BasicPractitionerInfo to handle profile photo uploads
128
+ * @param basicInfo - The basic info containing potential MediaResource profile photo
129
+ * @param practitionerId - ID of the practitioner
130
+ * @returns Processed basic info with URL string for profileImageUrl
131
+ */
132
+ private async processBasicInfo(
133
+ basicInfo: PractitionerBasicInfo & { profileImageUrl?: MediaResource },
134
+ practitionerId: string
135
+ ): Promise<PractitionerBasicInfo> {
136
+ const processedBasicInfo = { ...basicInfo };
137
+
138
+ // Handle profile photo upload if needed
139
+ if (basicInfo.profileImageUrl) {
140
+ const uploadedUrl = await this.handleProfilePhotoUpload(
141
+ basicInfo.profileImageUrl,
142
+ practitionerId
143
+ );
144
+ processedBasicInfo.profileImageUrl = uploadedUrl;
145
+ }
146
+
147
+ return processedBasicInfo;
148
+ }
149
+
78
150
  /**
79
151
  * Creates a new practitioner
80
152
  */
@@ -104,7 +176,10 @@ export class PractitionerService extends BaseService {
104
176
  } = {
105
177
  id: practitionerId,
106
178
  userRef: validData.userRef,
107
- basicInfo: validData.basicInfo,
179
+ basicInfo: await this.processBasicInfo(
180
+ validData.basicInfo,
181
+ practitionerId
182
+ ),
108
183
  certification: validData.certification,
109
184
  clinics: validData.clinics || [],
110
185
  clinicWorkingHours: validData.clinicWorkingHours || [],
@@ -254,7 +329,10 @@ export class PractitionerService extends BaseService {
254
329
  } = {
255
330
  id: practitionerId,
256
331
  userRef: "", // Prazno - biće popunjeno kada korisnik kreira nalog
257
- basicInfo: validatedData.basicInfo,
332
+ basicInfo: await this.processBasicInfo(
333
+ validatedData.basicInfo,
334
+ practitionerId
335
+ ),
258
336
  certification: validatedData.certification,
259
337
  clinics: clinics,
260
338
  clinicWorkingHours: validatedData.clinicWorkingHours || [],
@@ -619,9 +697,20 @@ export class PractitionerService extends BaseService {
619
697
 
620
698
  const currentPractitioner = practitionerDoc.data() as Practitioner;
621
699
 
700
+ // Process basicInfo if it's being updated to handle profile photo uploads
701
+ let processedData = { ...validData };
702
+ if (validData.basicInfo) {
703
+ processedData.basicInfo = await this.processBasicInfo(
704
+ validData.basicInfo as PractitionerBasicInfo & {
705
+ profileImageUrl?: MediaResource;
706
+ },
707
+ practitionerId
708
+ );
709
+ }
710
+
622
711
  // Prepare update data
623
712
  const updateData = {
624
- ...validData,
713
+ ...processedData,
625
714
  updatedAt: serverTimestamp(),
626
715
  };
627
716
 
@@ -251,7 +251,10 @@ export class ProcedureService extends BaseService {
251
251
  id: practitionerSnapshot.id,
252
252
  name: `${practitioner.basicInfo.firstName} ${practitioner.basicInfo.lastName}`,
253
253
  description: practitioner.basicInfo.bio || "",
254
- photo: practitioner.basicInfo.profileImageUrl || "",
254
+ photo:
255
+ typeof practitioner.basicInfo.profileImageUrl === "string"
256
+ ? practitioner.basicInfo.profileImageUrl
257
+ : "", // Default to empty string if not a processed URL
255
258
  rating: practitioner.reviewInfo?.averageRating || 0,
256
259
  services: practitioner.procedures || [],
257
260
  };
@@ -412,7 +415,10 @@ export class ProcedureService extends BaseService {
412
415
  id: newPractitioner.id,
413
416
  name: `${newPractitioner.basicInfo.firstName} ${newPractitioner.basicInfo.lastName}`,
414
417
  description: newPractitioner.basicInfo.bio || "",
415
- photo: newPractitioner.basicInfo.profileImageUrl || "",
418
+ photo:
419
+ typeof newPractitioner.basicInfo.profileImageUrl === "string"
420
+ ? newPractitioner.basicInfo.profileImageUrl
421
+ : "", // Default to empty string if not a processed URL
416
422
  rating: newPractitioner.reviewInfo?.averageRating || 0,
417
423
  services: newPractitioner.procedures || [],
418
424
  };
@@ -11,6 +11,7 @@ import {
11
11
  PricingMeasure,
12
12
  } from "../../backoffice/types/static/pricing.types";
13
13
  import { ProcedureSummaryInfo } from "../procedure";
14
+ import type { MediaResource } from "../../services/media/media.service";
14
15
 
15
16
  export const PRACTITIONERS_COLLECTION = "practitioners";
16
17
  export const REGISTER_TOKENS_COLLECTION = "register_tokens";
@@ -26,7 +27,7 @@ export interface PractitionerBasicInfo {
26
27
  phoneNumber: string;
27
28
  dateOfBirth: Timestamp | Date;
28
29
  gender: "male" | "female" | "other";
29
- profileImageUrl?: string;
30
+ profileImageUrl?: MediaResource;
30
31
  bio?: string;
31
32
  languages: string[];
32
33
  }
@@ -4,7 +4,7 @@ import { z } from "zod";
4
4
  * Schema for validating both URL strings and File objects
5
5
  */
6
6
  export const mediaResourceSchema = z.union([
7
- z.string(),
7
+ z.string().url(),
8
8
  z.instanceof(File),
9
9
  z.instanceof(Blob),
10
10
  ]);
@@ -15,6 +15,7 @@ import {
15
15
  PricingMeasure,
16
16
  } from "../backoffice/types/static/pricing.types";
17
17
  import { clinicInfoSchema, procedureSummaryInfoSchema } from "./shared.schema";
18
+ import { mediaResourceSchema } from "./media.schema";
18
19
 
19
20
  /**
20
21
  * Šema za validaciju osnovnih informacija o zdravstvenom radniku
@@ -27,7 +28,7 @@ export const practitionerBasicInfoSchema = z.object({
27
28
  phoneNumber: z.string().regex(/^\+?[1-9]\d{1,14}$/, "Invalid phone number"),
28
29
  dateOfBirth: z.instanceof(Timestamp).or(z.date()),
29
30
  gender: z.enum(["male", "female", "other"]),
30
- profileImageUrl: z.string().url().optional(),
31
+ profileImageUrl: mediaResourceSchema.optional(),
31
32
  bio: z.string().max(1000).optional(),
32
33
  languages: z.array(z.string()).min(1),
33
34
  });
@@ -204,7 +205,7 @@ export const practitionerSignupSchema = z.object({
204
205
  basicInfo: z
205
206
  .object({
206
207
  phoneNumber: z.string().optional(),
207
- profileImageUrl: z.string().optional(),
208
+ profileImageUrl: mediaResourceSchema.optional(),
208
209
  gender: z.enum(["male", "female", "other"]).optional(),
209
210
  bio: z.string().optional(),
210
211
  })