@blackcode_sa/metaestetics-api 1.5.29 → 1.5.30
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/admin/index.d.mts +126 -1
- package/dist/admin/index.d.ts +126 -1
- package/dist/admin/index.js +347 -10
- package/dist/admin/index.mjs +345 -10
- package/dist/index.d.mts +64 -71
- package/dist/index.d.ts +64 -71
- package/dist/index.js +323 -688
- package/dist/index.mjs +359 -728
- package/package.json +2 -1
- package/src/admin/aggregation/README.md +79 -0
- package/src/admin/aggregation/clinic/README.md +52 -0
- package/src/admin/aggregation/patient/README.md +27 -0
- package/src/admin/aggregation/practitioner/README.md +42 -0
- package/src/admin/aggregation/procedure/README.md +43 -0
- package/src/admin/index.ts +9 -2
- package/src/admin/mailing/README.md +95 -0
- package/src/admin/mailing/base.mailing.service.ts +131 -0
- package/src/admin/mailing/index.ts +2 -0
- package/src/admin/mailing/practitionerInvite/index.ts +1 -0
- package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +256 -0
- package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -0
- package/src/services/README.md +106 -0
- package/src/services/clinic/README.md +87 -0
- package/src/services/clinic/clinic.service.ts +3 -126
- package/src/services/clinic/utils/clinic.utils.ts +2 -2
- package/src/services/practitioner/README.md +145 -0
- package/src/services/practitioner/practitioner.service.ts +119 -395
- package/src/services/procedure/README.md +88 -0
- package/src/services/procedure/procedure.service.ts +332 -369
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import * as admin from "firebase-admin";
|
|
2
|
+
import * as mailgun from "mailgun-js";
|
|
3
|
+
import { BaseMailingService } from "../base.mailing.service";
|
|
4
|
+
import { practitionerInvitationTemplate } from "./templates/invitation.template";
|
|
5
|
+
|
|
6
|
+
// Import specific types and collection constants
|
|
7
|
+
import {
|
|
8
|
+
Practitioner,
|
|
9
|
+
PractitionerToken,
|
|
10
|
+
PRACTITIONERS_COLLECTION,
|
|
11
|
+
} from "../../../types/practitioner";
|
|
12
|
+
import { Clinic, CLINICS_COLLECTION } from "../../../types/clinic";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Interface for the data required to send a practitioner invitation email
|
|
16
|
+
*/
|
|
17
|
+
export interface PractitionerInviteEmailData {
|
|
18
|
+
/** The token object from the practitioner service */
|
|
19
|
+
token: {
|
|
20
|
+
id: string;
|
|
21
|
+
token: string;
|
|
22
|
+
practitionerId: string;
|
|
23
|
+
email: string;
|
|
24
|
+
clinicId: string;
|
|
25
|
+
expiresAt: admin.firestore.Timestamp;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Practitioner basic info */
|
|
29
|
+
practitioner: {
|
|
30
|
+
firstName: string;
|
|
31
|
+
lastName: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Clinic info */
|
|
35
|
+
clinic: {
|
|
36
|
+
name: string;
|
|
37
|
+
contactEmail: string;
|
|
38
|
+
contactName?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Config options */
|
|
42
|
+
options?: {
|
|
43
|
+
registrationUrl?: string;
|
|
44
|
+
customSubject?: string;
|
|
45
|
+
fromAddress?: string;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Service for sending practitioner invitation emails
|
|
51
|
+
*/
|
|
52
|
+
export class PractitionerInviteMailingService extends BaseMailingService {
|
|
53
|
+
private readonly DEFAULT_REGISTRATION_URL =
|
|
54
|
+
"https://app.medclinic.com/register";
|
|
55
|
+
private readonly DEFAULT_SUBJECT =
|
|
56
|
+
"You've Been Invited to Join as a Practitioner";
|
|
57
|
+
private readonly DEFAULT_FROM_ADDRESS =
|
|
58
|
+
"MedClinic <no-reply@your-domain.com>";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Constructor for PractitionerInviteMailingService
|
|
62
|
+
* @param firestore Firestore instance provided by the caller
|
|
63
|
+
* @param mailgunClient Mailgun client instance provided by the caller
|
|
64
|
+
*/
|
|
65
|
+
constructor(
|
|
66
|
+
firestore: FirebaseFirestore.Firestore,
|
|
67
|
+
mailgunClient: mailgun.Mailgun
|
|
68
|
+
) {
|
|
69
|
+
super(firestore, mailgunClient);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Sends a practitioner invitation email
|
|
74
|
+
* @param data The practitioner invitation data
|
|
75
|
+
* @returns Promise resolved when email is sent
|
|
76
|
+
*/
|
|
77
|
+
async sendInvitationEmail(
|
|
78
|
+
data: PractitionerInviteEmailData
|
|
79
|
+
): Promise<mailgun.messages.SendResponse> {
|
|
80
|
+
try {
|
|
81
|
+
console.log(
|
|
82
|
+
"[PractitionerInviteMailingService] Sending invitation email to",
|
|
83
|
+
data.token.email
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// Format expiration date
|
|
87
|
+
const expirationDate = data.token.expiresAt
|
|
88
|
+
.toDate()
|
|
89
|
+
.toLocaleDateString("en-US", {
|
|
90
|
+
weekday: "long",
|
|
91
|
+
year: "numeric",
|
|
92
|
+
month: "long",
|
|
93
|
+
day: "numeric",
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Registration URL
|
|
97
|
+
const registrationUrl =
|
|
98
|
+
data.options?.registrationUrl || this.DEFAULT_REGISTRATION_URL;
|
|
99
|
+
|
|
100
|
+
// Contact information
|
|
101
|
+
const contactName = data.clinic.contactName || "Clinic Administrator";
|
|
102
|
+
const contactEmail = data.clinic.contactEmail;
|
|
103
|
+
|
|
104
|
+
// Subject line
|
|
105
|
+
const subject = data.options?.customSubject || this.DEFAULT_SUBJECT;
|
|
106
|
+
|
|
107
|
+
// Determine 'from' address
|
|
108
|
+
const fromAddress =
|
|
109
|
+
data.options?.fromAddress || this.DEFAULT_FROM_ADDRESS;
|
|
110
|
+
|
|
111
|
+
// Current year for copyright
|
|
112
|
+
const currentYear = new Date().getFullYear().toString();
|
|
113
|
+
|
|
114
|
+
// Practitioner full name
|
|
115
|
+
const practitionerName = `${data.practitioner.firstName} ${data.practitioner.lastName}`;
|
|
116
|
+
|
|
117
|
+
// Prepare template variables
|
|
118
|
+
const templateVariables = {
|
|
119
|
+
clinicName: data.clinic.name,
|
|
120
|
+
practitionerName,
|
|
121
|
+
inviteToken: data.token.token,
|
|
122
|
+
expirationDate,
|
|
123
|
+
registrationUrl,
|
|
124
|
+
contactName,
|
|
125
|
+
contactEmail,
|
|
126
|
+
currentYear,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Render HTML email
|
|
130
|
+
const html = this.renderTemplate(
|
|
131
|
+
practitionerInvitationTemplate,
|
|
132
|
+
templateVariables
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// Send email - ensure 'from' is included
|
|
136
|
+
const emailData: mailgun.messages.SendData = {
|
|
137
|
+
to: data.token.email,
|
|
138
|
+
from: fromAddress,
|
|
139
|
+
subject,
|
|
140
|
+
html,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const result = await this.sendEmail(emailData);
|
|
144
|
+
|
|
145
|
+
// Log success
|
|
146
|
+
await this.logEmailAttempt(
|
|
147
|
+
{
|
|
148
|
+
to: data.token.email,
|
|
149
|
+
subject,
|
|
150
|
+
templateName: "practitioner_invitation",
|
|
151
|
+
},
|
|
152
|
+
true
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return result;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.error(
|
|
158
|
+
"[PractitionerInviteMailingService] Error sending invitation email:",
|
|
159
|
+
error
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// Log failure
|
|
163
|
+
await this.logEmailAttempt(
|
|
164
|
+
{
|
|
165
|
+
to: data.token.email,
|
|
166
|
+
subject: data.options?.customSubject || this.DEFAULT_SUBJECT,
|
|
167
|
+
templateName: "practitioner_invitation",
|
|
168
|
+
},
|
|
169
|
+
false,
|
|
170
|
+
error
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Handles the practitioner token creation event from Cloud Functions
|
|
179
|
+
* Fetches necessary data using defined types and collection constants,
|
|
180
|
+
* and sends the invitation email.
|
|
181
|
+
* @param tokenData The fully typed token object including its id
|
|
182
|
+
* @param fromAddress The 'from' email address to use, obtained from config
|
|
183
|
+
* @returns Promise resolved when the email is sent
|
|
184
|
+
*/
|
|
185
|
+
async handleTokenCreationEvent(
|
|
186
|
+
tokenData: PractitionerToken,
|
|
187
|
+
fromAddress: string
|
|
188
|
+
): Promise<void> {
|
|
189
|
+
try {
|
|
190
|
+
console.log(
|
|
191
|
+
"[PractitionerInviteMailingService] Handling token creation event for token:",
|
|
192
|
+
tokenData.id
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// Get practitioner data using constant and type
|
|
196
|
+
const practitionerRef = this.db
|
|
197
|
+
.collection(PRACTITIONERS_COLLECTION)
|
|
198
|
+
.doc(tokenData.practitionerId);
|
|
199
|
+
const practitionerDoc = await practitionerRef.get();
|
|
200
|
+
|
|
201
|
+
if (!practitionerDoc.exists) {
|
|
202
|
+
throw new Error(`Practitioner ${tokenData.practitionerId} not found`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const practitionerData = practitionerDoc.data() as Practitioner;
|
|
206
|
+
|
|
207
|
+
// Get clinic data using constant and type
|
|
208
|
+
const clinicRef = this.db
|
|
209
|
+
.collection(CLINICS_COLLECTION)
|
|
210
|
+
.doc(tokenData.clinicId);
|
|
211
|
+
const clinicDoc = await clinicRef.get();
|
|
212
|
+
|
|
213
|
+
if (!clinicDoc.exists) {
|
|
214
|
+
throw new Error(`Clinic ${tokenData.clinicId} not found`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const clinicData = clinicDoc.data() as Clinic;
|
|
218
|
+
|
|
219
|
+
// Prepare email data using typed data
|
|
220
|
+
const emailData: PractitionerInviteEmailData = {
|
|
221
|
+
token: {
|
|
222
|
+
id: tokenData.id,
|
|
223
|
+
token: tokenData.token,
|
|
224
|
+
practitionerId: tokenData.practitionerId,
|
|
225
|
+
email: tokenData.email,
|
|
226
|
+
clinicId: tokenData.clinicId,
|
|
227
|
+
expiresAt: tokenData.expiresAt,
|
|
228
|
+
},
|
|
229
|
+
practitioner: {
|
|
230
|
+
firstName: practitionerData.basicInfo.firstName || "",
|
|
231
|
+
lastName: practitionerData.basicInfo.lastName || "",
|
|
232
|
+
},
|
|
233
|
+
clinic: {
|
|
234
|
+
name: clinicData.name || "Medical Clinic",
|
|
235
|
+
contactEmail: clinicData.contactInfo.email || "contact@medclinic.com",
|
|
236
|
+
},
|
|
237
|
+
options: {
|
|
238
|
+
fromAddress: fromAddress,
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// Send the invitation email
|
|
243
|
+
await this.sendInvitationEmail(emailData);
|
|
244
|
+
|
|
245
|
+
console.log(
|
|
246
|
+
"[PractitionerInviteMailingService] Invitation email sent successfully"
|
|
247
|
+
);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
console.error(
|
|
250
|
+
"[PractitionerInviteMailingService] Error handling token creation event:",
|
|
251
|
+
error
|
|
252
|
+
);
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML email template for practitioner invitation
|
|
3
|
+
*/
|
|
4
|
+
export const practitionerInvitationTemplate = `
|
|
5
|
+
<!DOCTYPE html>
|
|
6
|
+
<html>
|
|
7
|
+
<head>
|
|
8
|
+
<meta charset="UTF-8">
|
|
9
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
10
|
+
<title>Join {{clinicName}} as a Practitioner</title>
|
|
11
|
+
<style>
|
|
12
|
+
body {
|
|
13
|
+
font-family: Arial, sans-serif;
|
|
14
|
+
line-height: 1.6;
|
|
15
|
+
color: #333;
|
|
16
|
+
margin: 0;
|
|
17
|
+
padding: 0;
|
|
18
|
+
}
|
|
19
|
+
.container {
|
|
20
|
+
max-width: 600px;
|
|
21
|
+
margin: 0 auto;
|
|
22
|
+
padding: 20px;
|
|
23
|
+
}
|
|
24
|
+
.header {
|
|
25
|
+
background-color: #4A90E2;
|
|
26
|
+
padding: 20px;
|
|
27
|
+
text-align: center;
|
|
28
|
+
color: white;
|
|
29
|
+
}
|
|
30
|
+
.content {
|
|
31
|
+
padding: 20px;
|
|
32
|
+
background-color: #f9f9f9;
|
|
33
|
+
}
|
|
34
|
+
.footer {
|
|
35
|
+
padding: 20px;
|
|
36
|
+
text-align: center;
|
|
37
|
+
font-size: 12px;
|
|
38
|
+
color: #888;
|
|
39
|
+
}
|
|
40
|
+
.button {
|
|
41
|
+
display: inline-block;
|
|
42
|
+
background-color: #4A90E2;
|
|
43
|
+
color: white;
|
|
44
|
+
text-decoration: none;
|
|
45
|
+
padding: 12px 24px;
|
|
46
|
+
border-radius: 4px;
|
|
47
|
+
margin: 20px 0;
|
|
48
|
+
font-weight: bold;
|
|
49
|
+
}
|
|
50
|
+
.token {
|
|
51
|
+
font-size: 24px;
|
|
52
|
+
font-weight: bold;
|
|
53
|
+
color: #4A90E2;
|
|
54
|
+
padding: 10px;
|
|
55
|
+
background-color: #e9f0f9;
|
|
56
|
+
border-radius: 4px;
|
|
57
|
+
display: inline-block;
|
|
58
|
+
letter-spacing: 2px;
|
|
59
|
+
margin: 10px 0;
|
|
60
|
+
}
|
|
61
|
+
</style>
|
|
62
|
+
</head>
|
|
63
|
+
<body>
|
|
64
|
+
<div class="container">
|
|
65
|
+
<div class="header">
|
|
66
|
+
<h1>You've Been Invited</h1>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="content">
|
|
69
|
+
<p>Hello {{practitionerName}},</p>
|
|
70
|
+
|
|
71
|
+
<p>You have been invited to join <strong>{{clinicName}}</strong> as a healthcare practitioner.</p>
|
|
72
|
+
|
|
73
|
+
<p>Your profile has been created and is ready for you to claim. Please use the following token to register:</p>
|
|
74
|
+
|
|
75
|
+
<div style="text-align: center;">
|
|
76
|
+
<span class="token">{{inviteToken}}</span>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<p>This token will expire on <strong>{{expirationDate}}</strong>.</p>
|
|
80
|
+
|
|
81
|
+
<p>To create your account:</p>
|
|
82
|
+
<ol>
|
|
83
|
+
<li>Visit {{registrationUrl}}</li>
|
|
84
|
+
<li>Enter your email and create a password</li>
|
|
85
|
+
<li>When prompted, enter the token above</li>
|
|
86
|
+
</ol>
|
|
87
|
+
|
|
88
|
+
<div style="text-align: center;">
|
|
89
|
+
<a href="{{registrationUrl}}" class="button">Create Your Account</a>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<p>If you have any questions, please contact {{contactName}} at {{contactEmail}}.</p>
|
|
93
|
+
</div>
|
|
94
|
+
<div class="footer">
|
|
95
|
+
<p>This is an automated message from {{clinicName}}. Please do not reply to this email.</p>
|
|
96
|
+
<p>© {{currentYear}} {{clinicName}}. All rights reserved.</p>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</body>
|
|
100
|
+
</html>
|
|
101
|
+
`;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Services
|
|
2
|
+
|
|
3
|
+
This directory contains service modules that implement the business logic of the application. Services act as an intermediary layer between the API handlers and data models, encapsulating complex operations and enforcing business rules.
|
|
4
|
+
|
|
5
|
+
## Service Responsibilities
|
|
6
|
+
|
|
7
|
+
Services handle:
|
|
8
|
+
|
|
9
|
+
1. **Business Logic**: Implementing complex business rules and workflows
|
|
10
|
+
2. **Data Validation**: Ensuring data integrity before persistence
|
|
11
|
+
3. **Transaction Management**: Handling atomic operations across multiple entities
|
|
12
|
+
4. **Error Handling**: Providing consistent error responses for business logic failures
|
|
13
|
+
5. **Integration**: Coordinating interactions with external services and APIs
|
|
14
|
+
|
|
15
|
+
## Service Structure
|
|
16
|
+
|
|
17
|
+
Each service typically follows this pattern:
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// Service function signature
|
|
21
|
+
export const someServiceFunction = async (
|
|
22
|
+
params: SomeParamsType,
|
|
23
|
+
options?: SomeOptionsType
|
|
24
|
+
): Promise<SomeReturnType> => {
|
|
25
|
+
try {
|
|
26
|
+
// 1. Input validation
|
|
27
|
+
const validatedData = someSchema.parse(params);
|
|
28
|
+
|
|
29
|
+
// 2. Business logic implementation
|
|
30
|
+
// ...
|
|
31
|
+
|
|
32
|
+
// 3. Data persistence
|
|
33
|
+
const result = await saveToDatabase(processedData);
|
|
34
|
+
|
|
35
|
+
// 4. Return formatted response
|
|
36
|
+
return result;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
// Error handling and transformation
|
|
39
|
+
if (error instanceof z.ZodError) {
|
|
40
|
+
throw new ValidationError("Invalid input data", error);
|
|
41
|
+
}
|
|
42
|
+
// Other error handling...
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Core Services
|
|
49
|
+
|
|
50
|
+
The application includes the following service modules:
|
|
51
|
+
|
|
52
|
+
- **auth**: User authentication and authorization
|
|
53
|
+
- **user**: User profile management
|
|
54
|
+
- **practitioner**: Practitioner profile and availability management
|
|
55
|
+
- **clinic**: Clinic/facility management
|
|
56
|
+
- **procedure**: Medical procedure and service management
|
|
57
|
+
- **appointment**: Appointment scheduling and management
|
|
58
|
+
- **review**: Practitioner review and rating
|
|
59
|
+
- **search**: Search functionality across entities
|
|
60
|
+
- **notification**: User notification delivery
|
|
61
|
+
- **file**: File upload and management
|
|
62
|
+
|
|
63
|
+
## Error Handling
|
|
64
|
+
|
|
65
|
+
Services use a consistent error handling approach:
|
|
66
|
+
|
|
67
|
+
- Custom error types for different failure scenarios
|
|
68
|
+
- Error transformation to provide meaningful context
|
|
69
|
+
- Detailed error information for debugging while maintaining security
|
|
70
|
+
|
|
71
|
+
## Transaction Management
|
|
72
|
+
|
|
73
|
+
For operations affecting multiple entities, services implement transaction patterns to ensure data consistency:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// Example transaction pattern
|
|
77
|
+
export const complexOperation = async (data: SomeType): Promise<ResultType> => {
|
|
78
|
+
// Begin transaction context
|
|
79
|
+
try {
|
|
80
|
+
// Multiple database operations...
|
|
81
|
+
|
|
82
|
+
// If all succeed, return result
|
|
83
|
+
return result;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
// Handle error and ensure rollback if needed
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Service Dependencies
|
|
92
|
+
|
|
93
|
+
Services may depend on other services to complete their operations. Dependencies are typically:
|
|
94
|
+
|
|
95
|
+
- Explicitly imported at the module level
|
|
96
|
+
- Passed as parameters to functions when needed for testing
|
|
97
|
+
- Designed to avoid circular dependencies
|
|
98
|
+
|
|
99
|
+
## Testing
|
|
100
|
+
|
|
101
|
+
Services are designed to be easily testable:
|
|
102
|
+
|
|
103
|
+
- Pure functions where possible
|
|
104
|
+
- External dependencies injectable for mocking
|
|
105
|
+
- Clear input/output contracts
|
|
106
|
+
- Isolated business logic
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Clinic Service
|
|
2
|
+
|
|
3
|
+
This service manages clinic data within the Firestore database. It provides methods for creating, reading, updating, and managing clinics, their branches, and associated data like tags and administrators.
|
|
4
|
+
|
|
5
|
+
**Note:** This service relies on helper functions defined in the `./utils` directory for specific operations like photo uploads, geo-queries, and data fetching. Cloud Functions handle data aggregation into related entities (Practitioners, Procedures, ClinicGroups).
|
|
6
|
+
|
|
7
|
+
## `ClinicService` Class
|
|
8
|
+
|
|
9
|
+
Extends `BaseService`.
|
|
10
|
+
|
|
11
|
+
### Constructor
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
constructor(
|
|
15
|
+
db: Firestore,
|
|
16
|
+
auth: Auth,
|
|
17
|
+
app: FirebaseApp,
|
|
18
|
+
clinicGroupService: ClinicGroupService,
|
|
19
|
+
clinicAdminService: ClinicAdminService
|
|
20
|
+
)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Initializes the service with Firestore, Auth, App instances, and required dependency services (`ClinicGroupService`, `ClinicAdminService`).
|
|
24
|
+
|
|
25
|
+
### Core Methods
|
|
26
|
+
|
|
27
|
+
- **`createClinic(data: CreateClinicData, creatorAdminId: string): Promise<Clinic>`**
|
|
28
|
+
- Creates a new clinic document in Firestore.
|
|
29
|
+
- Validates input data using `createClinicSchema`.
|
|
30
|
+
- Verifies the `creatorAdminId` and their association with the specified `clinicGroupId`.
|
|
31
|
+
- Calculates the geohash for the clinic's location.
|
|
32
|
+
- Initializes default review information.
|
|
33
|
+
- Handles photo uploads for logo, cover photo, featured photos, and photos with tags using utility functions.
|
|
34
|
+
- Updates the creator admin's `clinicsManaged` list.
|
|
35
|
+
- **Aggregation Note:** Adding the clinic to the `ClinicGroup` is handled by Cloud Functions.
|
|
36
|
+
- **`updateClinic(clinicId: string, data: Partial<Omit<Clinic, \"id\" | \"createdAt\" | \"clinicGroupId\">>, adminId: string): Promise<Clinic>`**
|
|
37
|
+
- Updates an existing clinic document.
|
|
38
|
+
- Fetches the current clinic data.
|
|
39
|
+
- Validates the partial update data against the `clinicSchema` after merging with existing data.
|
|
40
|
+
- Updates the geohash if the location is changed.
|
|
41
|
+
- Handles photo updates/uploads if necessary.
|
|
42
|
+
- **Aggregation Note:** Aggregation updates in related entities (Practitioners, Procedures, ClinicGroup) are handled by Cloud Functions.
|
|
43
|
+
- **`deactivateClinic(clinicId: string, adminId: string): Promise<void>`**
|
|
44
|
+
- Sets the `isActive` flag of a clinic to `false`.
|
|
45
|
+
- Requires admin permission (verification logic might be in the calling function or assumed).
|
|
46
|
+
- **Aggregation Note:** Aggregation updates (e.g., removing from active lists, updating related entities) are handled by Cloud Functions.
|
|
47
|
+
- **`getClinic(clinicId: string): Promise<Clinic | null>`**
|
|
48
|
+
- Retrieves a single clinic document by its ID using `ClinicUtils.getClinic`.
|
|
49
|
+
- **`getClinicsByGroup(groupId: string): Promise<Clinic[]>`**
|
|
50
|
+
- Retrieves all clinic documents belonging to a specific `clinicGroupId` using `ClinicUtils.getClinicsByGroup`.
|
|
51
|
+
- **`findClinicsInRadius(center: { latitude: number; longitude: number }, radiusInKm: number, filters?: { procedures?: string[]; tags?: ClinicTag[] }): Promise<Clinic[]>`**
|
|
52
|
+
- Finds active clinics within a specified radius using geohash queries.
|
|
53
|
+
- Leverages `SearchUtils.findClinicsInRadius`.
|
|
54
|
+
- Optionally filters results by procedure IDs (services) and tags.
|
|
55
|
+
- **`addTags(clinicId: string, adminId: string, newTags: { tags?: ClinicTag[] }): Promise<Clinic>`**
|
|
56
|
+
- Adds specified tags to a clinic's `tags` array using `TagUtils.addTags`.
|
|
57
|
+
- Ensures the admin has permission. Prevents duplicates.
|
|
58
|
+
- **`removeTags(clinicId: string, adminId: string, tagsToRemove: { tags?: ClinicTag[] }): Promise<Clinic>`**
|
|
59
|
+
- Removes specified tags from a clinic's `tags` array using `TagUtils.removeTags`.
|
|
60
|
+
- Ensures the admin has permission.
|
|
61
|
+
- **`getClinicsByAdmin(adminId: string, options?: { isActive?: boolean; includeGroupClinics?: boolean }): Promise<Clinic[]>`**
|
|
62
|
+
- Retrieves clinics associated with a specific admin using `ClinicUtils.getClinicsByAdmin`.
|
|
63
|
+
- Handles options for filtering by `isActive` and including all group clinics for owners.
|
|
64
|
+
- **`getActiveClinicsByAdmin(adminId: string): Promise<Clinic[]>`**
|
|
65
|
+
- Retrieves only the active clinics associated with a specific admin using `ClinicUtils.getActiveClinicsByAdmin`.
|
|
66
|
+
- **`createClinicBranch(clinicGroupId: string, setupData: ClinicBranchSetupData, adminId: string): Promise<Clinic>`**
|
|
67
|
+
- Creates a new clinic (branch) within an existing `clinicGroupId`.
|
|
68
|
+
- Validates group existence. Uses `createClinic` internally.
|
|
69
|
+
- **`getClinicById(clinicId: string): Promise<Clinic | null>`**
|
|
70
|
+
- Retrieves a single clinic document by its ID using `ClinicUtils.getClinicById`.
|
|
71
|
+
- **`getAllClinics(pagination?: number, lastDoc?: any): Promise<{ clinics: Clinic[]; lastDoc: any }>`**
|
|
72
|
+
- Retrieves all clinics, ordered by ID, optionally with pagination, using `ClinicUtils.getAllClinics`.
|
|
73
|
+
- **`getAllClinicsInRange(center: { latitude: number; longitude: number }, rangeInKm: number, pagination?: number, lastDoc?: any): Promise<{ clinics: (Clinic & { distance: number })[]; lastDoc: any }>`**
|
|
74
|
+
- Retrieves all clinics within a range, sorted by distance, optionally with pagination, using `ClinicUtils.getAllClinicsInRange`.
|
|
75
|
+
- **`getClinicsByFilters(filters: { ... }): Promise<{ clinics: (Clinic & { distance?: number })[]; lastDoc: any }>`**
|
|
76
|
+
- Retrieves clinics based on complex filters (location, tags, procedures, rating, etc.).
|
|
77
|
+
- Uses `FilterUtils.getClinicsByFilters` for combined query and in-memory filtering.
|
|
78
|
+
|
|
79
|
+
### Utility Functions (`./utils`)
|
|
80
|
+
|
|
81
|
+
- **`clinic.utils.ts`**: Core fetching (`getClinic`, `getClinicsByGroup`, etc.), create/update/deactivate logic wrappers, admin checks.
|
|
82
|
+
- **`filter.utils.ts`**: Complex filtering logic (`getClinicsByFilters`), including geo-queries.
|
|
83
|
+
- **`clinic-group.utils.ts`**: Helpers for clinic group interactions (used by `ClinicGroupService`).
|
|
84
|
+
- **`tag.utils.ts`**: Logic for adding/removing tags (`addTags`, `removeTags`).
|
|
85
|
+
- **`photos.utils.ts`**: Firebase Storage interactions (`uploadPhoto`, `uploadMultiplePhotos`, `deletePhoto`).
|
|
86
|
+
- **`admin.utils.ts`**: Helpers for clinic admin interactions (used by `ClinicAdminService`).
|
|
87
|
+
- **`search.utils.ts`**: Geo-radius search logic (`findClinicsInRadius`).
|