@blackcode_sa/metaestetics-api 1.4.8 → 1.4.9
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.d.mts +16 -16
- package/dist/index.d.ts +16 -16
- package/dist/index.js +736 -184
- package/dist/index.mjs +755 -196
- package/package.json +1 -1
- package/src/services/auth.service.ts +33 -11
- package/src/services/clinic/clinic-group.service.ts +63 -9
- package/src/services/clinic/clinic.service.ts +32 -9
- package/src/services/clinic/utils/admin.utils.ts +6 -51
- package/src/services/clinic/utils/clinic-group.utils.ts +178 -45
- package/src/services/clinic/utils/clinic.utils.ts +482 -39
- package/src/services/clinic/utils/photos.utils.ts +188 -0
- package/src/services/clinic/utils/review.utils.ts +24 -17
- package/src/services/clinic/utils/tag.utils.ts +33 -8
- package/src/types/clinic/index.ts +5 -5
- package/src/validations/clinic.schema.ts +3 -3
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getStorage,
|
|
3
|
+
ref,
|
|
4
|
+
uploadBytes,
|
|
5
|
+
getDownloadURL,
|
|
6
|
+
deleteObject,
|
|
7
|
+
} from "firebase/storage";
|
|
8
|
+
import { FirebaseApp } from "firebase/app";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Uploads a photo to Firebase Storage
|
|
12
|
+
* @param photo - The photo data URL or URL
|
|
13
|
+
* @param entityType - The type of entity (clinic-groups, clinics, etc.)
|
|
14
|
+
* @param entityId - The ID of the entity
|
|
15
|
+
* @param photoType - The type of photo (logo, featured, etc.)
|
|
16
|
+
* @param app - Firebase app instance
|
|
17
|
+
* @param fileName - Optional custom file name
|
|
18
|
+
* @returns The URL of the uploaded photo or null if upload failed
|
|
19
|
+
*/
|
|
20
|
+
export async function uploadPhoto(
|
|
21
|
+
photo: string | null,
|
|
22
|
+
entityType: string,
|
|
23
|
+
entityId: string,
|
|
24
|
+
photoType: string,
|
|
25
|
+
app: FirebaseApp,
|
|
26
|
+
fileName?: string
|
|
27
|
+
): Promise<string | null> {
|
|
28
|
+
// If photo is null or not a data URL, return it as is
|
|
29
|
+
if (!photo || typeof photo !== "string" || !photo.startsWith("data:")) {
|
|
30
|
+
return photo;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
console.log(
|
|
35
|
+
`[PHOTO_UTILS] Uploading ${photoType} for ${entityType}/${entityId}`
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// Get Firebase Storage instance
|
|
39
|
+
const storage = getStorage(app);
|
|
40
|
+
|
|
41
|
+
// Create a reference to the storage location
|
|
42
|
+
const storageFileName = fileName || `${photoType}-${Date.now()}`;
|
|
43
|
+
const storageRef = ref(
|
|
44
|
+
storage,
|
|
45
|
+
`${entityType}/${entityId}/${storageFileName}`
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// Extract base64 data from data URL
|
|
49
|
+
const base64Data = photo.split(",")[1];
|
|
50
|
+
const contentType = photo.split(";")[0].split(":")[1];
|
|
51
|
+
|
|
52
|
+
// Convert base64 to Blob
|
|
53
|
+
const byteCharacters = atob(base64Data);
|
|
54
|
+
const byteArrays = [];
|
|
55
|
+
for (let i = 0; i < byteCharacters.length; i++) {
|
|
56
|
+
byteArrays.push(byteCharacters.charCodeAt(i));
|
|
57
|
+
}
|
|
58
|
+
const blob = new Blob([new Uint8Array(byteArrays)], { type: contentType });
|
|
59
|
+
|
|
60
|
+
// Upload the file
|
|
61
|
+
await uploadBytes(storageRef, blob, { contentType });
|
|
62
|
+
|
|
63
|
+
// Get the download URL
|
|
64
|
+
const downloadUrl = await getDownloadURL(storageRef);
|
|
65
|
+
|
|
66
|
+
console.log(`[PHOTO_UTILS] ${photoType} uploaded successfully`, {
|
|
67
|
+
downloadUrl,
|
|
68
|
+
});
|
|
69
|
+
return downloadUrl;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error(`[PHOTO_UTILS] Error uploading ${photoType}:`, error);
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Deletes a photo from Firebase Storage
|
|
78
|
+
* @param photoUrl - The URL of the photo to delete
|
|
79
|
+
* @param app - Firebase app instance
|
|
80
|
+
* @returns Whether the deletion was successful
|
|
81
|
+
*/
|
|
82
|
+
export async function deletePhoto(
|
|
83
|
+
photoUrl: string,
|
|
84
|
+
app: FirebaseApp
|
|
85
|
+
): Promise<boolean> {
|
|
86
|
+
if (!photoUrl || !photoUrl.includes("firebasestorage.googleapis.com")) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
console.log(`[PHOTO_UTILS] Deleting photo`, { photoUrl });
|
|
92
|
+
|
|
93
|
+
// Get Firebase Storage instance
|
|
94
|
+
const storage = getStorage(app);
|
|
95
|
+
|
|
96
|
+
// Extract the storage path from the URL
|
|
97
|
+
const storageRef = ref(storage, photoUrl);
|
|
98
|
+
|
|
99
|
+
// Delete the file
|
|
100
|
+
await deleteObject(storageRef);
|
|
101
|
+
|
|
102
|
+
console.log(`[PHOTO_UTILS] Photo deleted successfully`);
|
|
103
|
+
return true;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error(`[PHOTO_UTILS] Error deleting photo:`, error);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Uploads multiple photos to Firebase Storage
|
|
112
|
+
* @param photos - Array of photo data URLs or URLs
|
|
113
|
+
* @param entityType - The type of entity (clinic-groups, clinics, etc.)
|
|
114
|
+
* @param entityId - The ID of the entity
|
|
115
|
+
* @param photoType - The type of photos (featured, gallery, etc.)
|
|
116
|
+
* @param app - Firebase app instance
|
|
117
|
+
* @returns Array of URLs of the uploaded photos
|
|
118
|
+
*/
|
|
119
|
+
export async function uploadMultiplePhotos(
|
|
120
|
+
photos: string[],
|
|
121
|
+
entityType: string,
|
|
122
|
+
entityId: string,
|
|
123
|
+
photoType: string,
|
|
124
|
+
app: FirebaseApp
|
|
125
|
+
): Promise<string[]> {
|
|
126
|
+
if (!photos || !Array.isArray(photos) || photos.length === 0) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const uploadPromises = photos.map((photo, index) =>
|
|
131
|
+
uploadPhoto(photo, entityType, entityId, `${photoType}-${index}`, app)
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const results = await Promise.all(uploadPromises);
|
|
136
|
+
return results.filter((url): url is string => url !== null);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error(`[PHOTO_UTILS] Error uploading multiple photos:`, error);
|
|
139
|
+
return photos.filter((photo) => !photo.startsWith("data:"));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Processes photos in an entity object, uploading any data URLs
|
|
145
|
+
* @param entity - The entity object containing photo fields
|
|
146
|
+
* @param entityType - The type of entity (clinic-groups, clinics, etc.)
|
|
147
|
+
* @param entityId - The ID of the entity
|
|
148
|
+
* @param photoFields - Array of field names that contain photos
|
|
149
|
+
* @param app - Firebase app instance
|
|
150
|
+
* @returns The entity with updated photo URLs
|
|
151
|
+
*/
|
|
152
|
+
export async function processEntityPhotos<T extends Record<string, any>>(
|
|
153
|
+
entity: T,
|
|
154
|
+
entityType: string,
|
|
155
|
+
entityId: string,
|
|
156
|
+
photoFields: { field: keyof T; type: string }[],
|
|
157
|
+
app: FirebaseApp
|
|
158
|
+
): Promise<T> {
|
|
159
|
+
const updatedEntity = { ...entity };
|
|
160
|
+
|
|
161
|
+
for (const { field, type } of photoFields) {
|
|
162
|
+
const value = entity[field];
|
|
163
|
+
|
|
164
|
+
if (!value) continue;
|
|
165
|
+
|
|
166
|
+
if (Array.isArray(value)) {
|
|
167
|
+
// Handle array of photos
|
|
168
|
+
updatedEntity[field] = (await uploadMultiplePhotos(
|
|
169
|
+
value,
|
|
170
|
+
entityType,
|
|
171
|
+
entityId,
|
|
172
|
+
type,
|
|
173
|
+
app
|
|
174
|
+
)) as any;
|
|
175
|
+
} else if (typeof value === "string") {
|
|
176
|
+
// Handle single photo
|
|
177
|
+
updatedEntity[field] = (await uploadPhoto(
|
|
178
|
+
value,
|
|
179
|
+
entityType,
|
|
180
|
+
entityId,
|
|
181
|
+
type,
|
|
182
|
+
app
|
|
183
|
+
)) as any;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return updatedEntity;
|
|
188
|
+
}
|
|
@@ -4,16 +4,20 @@ import {
|
|
|
4
4
|
setDoc,
|
|
5
5
|
Timestamp,
|
|
6
6
|
Firestore,
|
|
7
|
+
getDoc,
|
|
8
|
+
addDoc,
|
|
7
9
|
} from "firebase/firestore";
|
|
8
10
|
import { Clinic, ClinicReview } from "../../../types/clinic";
|
|
9
11
|
import { clinicReviewSchema } from "../../../validations/clinic.schema";
|
|
10
12
|
import { getClinic, updateClinic } from "./clinic.utils";
|
|
13
|
+
import { FirebaseApp } from "firebase/app";
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
16
|
* Adds a review to a clinic
|
|
14
17
|
* @param db - Firestore database instance
|
|
15
|
-
* @param clinicId - ID of the clinic to review
|
|
18
|
+
* @param clinicId - ID of the clinic to add the review to
|
|
16
19
|
* @param review - Review data
|
|
20
|
+
* @param app - Firebase app instance
|
|
17
21
|
* @returns The created review
|
|
18
22
|
*/
|
|
19
23
|
export async function addReview(
|
|
@@ -22,20 +26,25 @@ export async function addReview(
|
|
|
22
26
|
review: Omit<
|
|
23
27
|
ClinicReview,
|
|
24
28
|
"id" | "clinicId" | "createdAt" | "updatedAt" | "isVerified"
|
|
25
|
-
|
|
29
|
+
>,
|
|
30
|
+
app: FirebaseApp
|
|
26
31
|
): Promise<ClinicReview> {
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
// Proveravamo da li klinika postoji
|
|
33
|
+
const clinicRef = doc(db, "clinics", clinicId);
|
|
34
|
+
const clinicSnap = await getDoc(clinicRef);
|
|
35
|
+
|
|
36
|
+
if (!clinicSnap.exists()) {
|
|
29
37
|
throw new Error("Clinic not found");
|
|
30
38
|
}
|
|
31
39
|
|
|
40
|
+
const clinic = clinicSnap.data();
|
|
41
|
+
|
|
42
|
+
// Kreiramo recenziju
|
|
32
43
|
const now = Timestamp.now();
|
|
33
44
|
const reviewData: ClinicReview = {
|
|
45
|
+
...review,
|
|
34
46
|
id: doc(collection(db, "clinic_reviews")).id,
|
|
35
47
|
clinicId,
|
|
36
|
-
patientId: review.patientId,
|
|
37
|
-
rating: review.rating,
|
|
38
|
-
comment: review.comment,
|
|
39
48
|
createdAt: now,
|
|
40
49
|
updatedAt: now,
|
|
41
50
|
isVerified: false,
|
|
@@ -45,7 +54,7 @@ export async function addReview(
|
|
|
45
54
|
clinicReviewSchema.parse(reviewData);
|
|
46
55
|
|
|
47
56
|
// Čuvamo recenziju
|
|
48
|
-
await
|
|
57
|
+
await addDoc(collection(db, "clinic_reviews"), reviewData);
|
|
49
58
|
|
|
50
59
|
// Ažuriramo prosečnu ocenu klinike
|
|
51
60
|
const newRating = clinic.rating
|
|
@@ -68,18 +77,16 @@ export async function addReview(
|
|
|
68
77
|
...clinic.reviewsInfo,
|
|
69
78
|
{
|
|
70
79
|
id: reviewData.id,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
patientPhoto: "", // This should be fetched from patient service
|
|
76
|
-
createdAt: now,
|
|
77
|
-
updatedAt: now,
|
|
80
|
+
patientId: reviewData.patientId,
|
|
81
|
+
rating: reviewData.rating,
|
|
82
|
+
comment: reviewData.comment,
|
|
83
|
+
createdAt: reviewData.createdAt,
|
|
78
84
|
},
|
|
79
85
|
],
|
|
80
86
|
},
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
"system", // System update, no admin ID needed
|
|
88
|
+
null, // No clinic admin service needed for system updates
|
|
89
|
+
app
|
|
83
90
|
);
|
|
84
91
|
|
|
85
92
|
return reviewData;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Firestore } from "firebase/firestore";
|
|
2
2
|
import { Clinic, ClinicTag } from "../../../types/clinic";
|
|
3
3
|
import { getClinic, updateClinic } from "./clinic.utils";
|
|
4
|
+
import { FirebaseApp } from "firebase/app";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Adds tags to a clinic
|
|
@@ -9,6 +10,7 @@ import { getClinic, updateClinic } from "./clinic.utils";
|
|
|
9
10
|
* @param adminId - ID of the admin making the change
|
|
10
11
|
* @param newTags - Tags to add
|
|
11
12
|
* @param clinicAdminService - Service for clinic admin operations
|
|
13
|
+
* @param app - Firebase app instance
|
|
12
14
|
* @returns The updated clinic
|
|
13
15
|
*/
|
|
14
16
|
export async function addTags(
|
|
@@ -18,7 +20,8 @@ export async function addTags(
|
|
|
18
20
|
newTags: {
|
|
19
21
|
tags?: ClinicTag[];
|
|
20
22
|
},
|
|
21
|
-
clinicAdminService: any
|
|
23
|
+
clinicAdminService: any,
|
|
24
|
+
app: FirebaseApp
|
|
22
25
|
): Promise<Clinic> {
|
|
23
26
|
const clinic = await getClinic(db, clinicId);
|
|
24
27
|
if (!clinic) {
|
|
@@ -31,7 +34,16 @@ export async function addTags(
|
|
|
31
34
|
throw new Error("Admin not found");
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
if
|
|
37
|
+
// Check if admin is either:
|
|
38
|
+
// 1. The owner of the clinic group that this clinic belongs to, OR
|
|
39
|
+
// 2. Has this clinic in their managed clinics list AND is listed in the clinic's admins array
|
|
40
|
+
const hasPermission =
|
|
41
|
+
(admin.isGroupOwner && admin.clinicGroupId === clinic.clinicGroupId) ||
|
|
42
|
+
(admin.clinicsManaged.includes(clinicId) &&
|
|
43
|
+
clinic.admins &&
|
|
44
|
+
clinic.admins.includes(adminId));
|
|
45
|
+
|
|
46
|
+
if (!hasPermission) {
|
|
35
47
|
throw new Error("Admin does not have permission to update this clinic");
|
|
36
48
|
}
|
|
37
49
|
|
|
@@ -45,7 +57,8 @@ export async function addTags(
|
|
|
45
57
|
tags: updatedTags,
|
|
46
58
|
},
|
|
47
59
|
adminId,
|
|
48
|
-
clinicAdminService
|
|
60
|
+
clinicAdminService,
|
|
61
|
+
app
|
|
49
62
|
);
|
|
50
63
|
}
|
|
51
64
|
|
|
@@ -56,6 +69,7 @@ export async function addTags(
|
|
|
56
69
|
* @param adminId - ID of the admin making the change
|
|
57
70
|
* @param tagsToRemove - Tags to remove
|
|
58
71
|
* @param clinicAdminService - Service for clinic admin operations
|
|
72
|
+
* @param app - Firebase app instance
|
|
59
73
|
* @returns The updated clinic
|
|
60
74
|
*/
|
|
61
75
|
export async function removeTags(
|
|
@@ -65,7 +79,8 @@ export async function removeTags(
|
|
|
65
79
|
tagsToRemove: {
|
|
66
80
|
tags?: ClinicTag[];
|
|
67
81
|
},
|
|
68
|
-
clinicAdminService: any
|
|
82
|
+
clinicAdminService: any,
|
|
83
|
+
app: FirebaseApp
|
|
69
84
|
): Promise<Clinic> {
|
|
70
85
|
const clinic = await getClinic(db, clinicId);
|
|
71
86
|
if (!clinic) {
|
|
@@ -78,13 +93,22 @@ export async function removeTags(
|
|
|
78
93
|
throw new Error("Admin not found");
|
|
79
94
|
}
|
|
80
95
|
|
|
81
|
-
if
|
|
96
|
+
// Check if admin is either:
|
|
97
|
+
// 1. The owner of the clinic group that this clinic belongs to, OR
|
|
98
|
+
// 2. Has this clinic in their managed clinics list AND is listed in the clinic's admins array
|
|
99
|
+
const hasPermission =
|
|
100
|
+
(admin.isGroupOwner && admin.clinicGroupId === clinic.clinicGroupId) ||
|
|
101
|
+
(admin.clinicsManaged.includes(clinicId) &&
|
|
102
|
+
clinic.admins &&
|
|
103
|
+
clinic.admins.includes(adminId));
|
|
104
|
+
|
|
105
|
+
if (!hasPermission) {
|
|
82
106
|
throw new Error("Admin does not have permission to update this clinic");
|
|
83
107
|
}
|
|
84
108
|
|
|
85
|
-
//
|
|
109
|
+
// Remove specified tags
|
|
86
110
|
const updatedTags = clinic.tags.filter(
|
|
87
|
-
(tag) => !tagsToRemove.tags
|
|
111
|
+
(tag) => !tagsToRemove.tags || !tagsToRemove.tags.includes(tag)
|
|
88
112
|
);
|
|
89
113
|
|
|
90
114
|
return updateClinic(
|
|
@@ -94,6 +118,7 @@ export async function removeTags(
|
|
|
94
118
|
tags: updatedTags,
|
|
95
119
|
},
|
|
96
120
|
adminId,
|
|
97
|
-
clinicAdminService
|
|
121
|
+
clinicAdminService,
|
|
122
|
+
app
|
|
98
123
|
);
|
|
99
124
|
}
|
|
@@ -107,7 +107,7 @@ export interface ClinicInfo {
|
|
|
107
107
|
export interface ClinicAdmin {
|
|
108
108
|
id: string;
|
|
109
109
|
userRef: string;
|
|
110
|
-
clinicGroupId: string;
|
|
110
|
+
clinicGroupId: string; // Can be empty string initially for owners
|
|
111
111
|
isGroupOwner: boolean;
|
|
112
112
|
clinicsManaged: string[];
|
|
113
113
|
clinicsManagedInfo: ClinicInfo[];
|
|
@@ -195,7 +195,7 @@ export interface ClinicGroup {
|
|
|
195
195
|
admins: string[];
|
|
196
196
|
adminsInfo: AdminInfo[];
|
|
197
197
|
adminTokens: AdminToken[];
|
|
198
|
-
ownerId: string;
|
|
198
|
+
ownerId: string | null;
|
|
199
199
|
createdAt: Timestamp;
|
|
200
200
|
updatedAt: Timestamp;
|
|
201
201
|
isActive: boolean;
|
|
@@ -216,7 +216,7 @@ export interface CreateClinicGroupData {
|
|
|
216
216
|
hqLocation: ClinicLocation;
|
|
217
217
|
contactInfo: ClinicContactInfo;
|
|
218
218
|
contactPerson: ContactPerson;
|
|
219
|
-
ownerId: string;
|
|
219
|
+
ownerId: string | null;
|
|
220
220
|
isActive: boolean;
|
|
221
221
|
logo?: string | null;
|
|
222
222
|
practiceType?: PracticeType;
|
|
@@ -355,12 +355,12 @@ export interface UpdateClinicData extends Partial<CreateClinicData> {
|
|
|
355
355
|
*/
|
|
356
356
|
export interface CreateDefaultClinicGroupData {
|
|
357
357
|
name: string;
|
|
358
|
-
ownerId: string;
|
|
358
|
+
ownerId: string | null;
|
|
359
359
|
contactPerson: ContactPerson;
|
|
360
360
|
contactInfo: ClinicContactInfo;
|
|
361
361
|
hqLocation: ClinicLocation;
|
|
362
362
|
isActive: boolean;
|
|
363
|
-
logo?: string;
|
|
363
|
+
logo?: string | null;
|
|
364
364
|
practiceType?: PracticeType;
|
|
365
365
|
languages?: Language[];
|
|
366
366
|
subscriptionModel?: SubscriptionModel;
|
|
@@ -200,7 +200,7 @@ export const clinicGroupSchema = z.object({
|
|
|
200
200
|
admins: z.array(z.string()),
|
|
201
201
|
adminsInfo: z.array(adminInfoSchema),
|
|
202
202
|
adminTokens: z.array(adminTokenSchema),
|
|
203
|
-
ownerId: z.string(),
|
|
203
|
+
ownerId: z.string().nullable(),
|
|
204
204
|
createdAt: z.instanceof(Date).or(z.instanceof(Timestamp)), // Timestamp
|
|
205
205
|
updatedAt: z.instanceof(Date).or(z.instanceof(Timestamp)), // Timestamp
|
|
206
206
|
isActive: z.boolean(),
|
|
@@ -290,7 +290,7 @@ export const createClinicGroupSchema = z.object({
|
|
|
290
290
|
hqLocation: clinicLocationSchema,
|
|
291
291
|
contactInfo: clinicContactInfoSchema,
|
|
292
292
|
contactPerson: contactPersonSchema,
|
|
293
|
-
ownerId: z.string(),
|
|
293
|
+
ownerId: z.string().nullable(),
|
|
294
294
|
isActive: z.boolean(),
|
|
295
295
|
logo: z.string().optional().nullable(),
|
|
296
296
|
practiceType: z.nativeEnum(PracticeType).optional(),
|
|
@@ -337,7 +337,7 @@ export const createClinicSchema = z.object({
|
|
|
337
337
|
*/
|
|
338
338
|
export const createDefaultClinicGroupSchema = z.object({
|
|
339
339
|
name: z.string(),
|
|
340
|
-
ownerId: z.string(),
|
|
340
|
+
ownerId: z.string().nullable(),
|
|
341
341
|
contactPerson: contactPersonSchema,
|
|
342
342
|
contactInfo: clinicContactInfoSchema,
|
|
343
343
|
hqLocation: clinicLocationSchema,
|