@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.
Files changed (30) hide show
  1. package/dist/admin/index.d.mts +126 -1
  2. package/dist/admin/index.d.ts +126 -1
  3. package/dist/admin/index.js +347 -10
  4. package/dist/admin/index.mjs +345 -10
  5. package/dist/index.d.mts +64 -71
  6. package/dist/index.d.ts +64 -71
  7. package/dist/index.js +327 -710
  8. package/dist/index.mjs +363 -750
  9. package/package.json +2 -1
  10. package/src/admin/aggregation/README.md +79 -0
  11. package/src/admin/aggregation/clinic/README.md +52 -0
  12. package/src/admin/aggregation/patient/README.md +27 -0
  13. package/src/admin/aggregation/practitioner/README.md +42 -0
  14. package/src/admin/aggregation/procedure/README.md +43 -0
  15. package/src/admin/index.ts +17 -2
  16. package/src/admin/mailing/README.md +95 -0
  17. package/src/admin/mailing/base.mailing.service.ts +131 -0
  18. package/src/admin/mailing/index.ts +2 -0
  19. package/src/admin/mailing/practitionerInvite/index.ts +1 -0
  20. package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +256 -0
  21. package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -0
  22. package/src/services/README.md +106 -0
  23. package/src/services/calendar/utils/appointment.utils.ts +42 -91
  24. package/src/services/clinic/README.md +87 -0
  25. package/src/services/clinic/clinic.service.ts +3 -126
  26. package/src/services/clinic/utils/clinic.utils.ts +2 -2
  27. package/src/services/practitioner/README.md +145 -0
  28. package/src/services/practitioner/practitioner.service.ts +119 -395
  29. package/src/services/procedure/README.md +88 -0
  30. 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>&copy; {{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