@blackcode_sa/metaestetics-api 1.5.29 → 1.5.31
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 +327 -710
- package/dist/index.mjs +363 -750
- 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 +17 -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/calendar/utils/appointment.utils.ts +42 -91
- 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
|