@blackcode_sa/metaestetics-api 1.14.65 → 1.14.69

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.
@@ -0,0 +1,292 @@
1
+ import { BaseMailingService } from "../base.mailing.service";
2
+ import { clinicWelcomeTemplate } from "./templates/welcome.template";
3
+ import { Logger } from "../../logger";
4
+ import { ClinicGroup, CLINIC_GROUPS_COLLECTION } from "../../../types/clinic";
5
+
6
+ /**
7
+ * Minimal interface for the mailgun.js client
8
+ */
9
+ interface NewMailgunMessagesAPI {
10
+ create(domain: string, data: any): Promise<any>;
11
+ }
12
+ interface NewMailgunClient {
13
+ messages: NewMailgunMessagesAPI;
14
+ }
15
+
16
+ /**
17
+ * Interface for clinic welcome email data
18
+ */
19
+ export interface ClinicWelcomeEmailData {
20
+ /** Admin information */
21
+ admin: {
22
+ name: string;
23
+ email: string;
24
+ };
25
+
26
+ /** Clinic group information */
27
+ clinicGroup: {
28
+ id: string;
29
+ name: string;
30
+ };
31
+
32
+ /** Config options */
33
+ options?: {
34
+ dashboardUrl?: string;
35
+ supportEmail?: string;
36
+ customSubject?: string;
37
+ fromAddress?: string;
38
+ mailgunDomain?: string;
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Service for sending clinic welcome/confirmation emails
44
+ * Sent to clinic admins after successful signup
45
+ */
46
+ export class ClinicWelcomeMailingService extends BaseMailingService {
47
+ private readonly DEFAULT_DASHBOARD_URL =
48
+ "https://clinic.metaesthetics.net/dashboard";
49
+ private readonly DEFAULT_SUPPORT_EMAIL = "support@metaesthetics.net";
50
+ private readonly DEFAULT_SUBJECT =
51
+ "Welcome to MetaEsthetics - Your Clinic Registration is Complete";
52
+ private readonly DEFAULT_MAILGUN_DOMAIN = "mg.metaesthetics.net";
53
+
54
+ /**
55
+ * Constructor for ClinicWelcomeMailingService
56
+ * @param firestore - Firestore instance provided by the caller
57
+ * @param mailgunClient - Mailgun client instance (mailgun.js v10+) provided by the caller
58
+ */
59
+ constructor(
60
+ firestore: FirebaseFirestore.Firestore,
61
+ mailgunClient: NewMailgunClient
62
+ ) {
63
+ super(firestore, mailgunClient);
64
+ }
65
+
66
+ /**
67
+ * Sends a clinic welcome email
68
+ * @param data - The clinic welcome email data
69
+ * @returns Promise resolved when email is sent
70
+ */
71
+ async sendWelcomeEmail(data: ClinicWelcomeEmailData): Promise<any> {
72
+ try {
73
+ Logger.info(
74
+ "[ClinicWelcomeMailingService] Sending welcome email to",
75
+ data.admin.email
76
+ );
77
+
78
+ // Get current date for registration date
79
+ const registrationDate = new Date().toLocaleDateString("en-US", {
80
+ weekday: "long",
81
+ year: "numeric",
82
+ month: "long",
83
+ day: "numeric",
84
+ });
85
+
86
+ // Dashboard URL
87
+ const dashboardUrl =
88
+ data.options?.dashboardUrl || this.DEFAULT_DASHBOARD_URL;
89
+
90
+ // Support email
91
+ const supportEmail =
92
+ data.options?.supportEmail || this.DEFAULT_SUPPORT_EMAIL;
93
+
94
+ // Subject line
95
+ const subject = data.options?.customSubject || this.DEFAULT_SUBJECT;
96
+
97
+ // Determine 'from' address
98
+ const fromAddress =
99
+ data.options?.fromAddress ||
100
+ `MetaEsthetics <no-reply@${
101
+ data.options?.mailgunDomain || this.DEFAULT_MAILGUN_DOMAIN
102
+ }>`;
103
+
104
+ // Current year for copyright
105
+ const currentYear = new Date().getFullYear().toString();
106
+
107
+ // Prepare template variables
108
+ const templateVariables = {
109
+ adminName: data.admin.name,
110
+ adminEmail: data.admin.email,
111
+ clinicGroupName: data.clinicGroup.name,
112
+ registrationDate,
113
+ dashboardUrl,
114
+ supportEmail,
115
+ currentYear,
116
+ };
117
+
118
+ // Debug log for template variables
119
+ Logger.info("[ClinicWelcomeMailingService] Template variables:", {
120
+ adminName: templateVariables.adminName,
121
+ adminEmail: templateVariables.adminEmail,
122
+ clinicGroupName: templateVariables.clinicGroupName,
123
+ dashboardUrl: templateVariables.dashboardUrl,
124
+ });
125
+
126
+ // Render HTML email
127
+ const html = this.renderTemplate(clinicWelcomeTemplate, templateVariables);
128
+
129
+ // Data for the sendEmail method
130
+ const mailgunSendData = {
131
+ to: data.admin.email,
132
+ from: fromAddress,
133
+ subject,
134
+ html,
135
+ };
136
+
137
+ // Determine the domain to use for sending
138
+ const domainToSendFrom =
139
+ data.options?.mailgunDomain || this.DEFAULT_MAILGUN_DOMAIN;
140
+
141
+ Logger.info("[ClinicWelcomeMailingService] Sending email with data:", {
142
+ domain: domainToSendFrom,
143
+ to: mailgunSendData.to,
144
+ from: mailgunSendData.from,
145
+ subject: mailgunSendData.subject,
146
+ hasHtml: !!mailgunSendData.html,
147
+ });
148
+
149
+ // Call the sendEmail method from BaseMailingService
150
+ const result = await this.sendEmail(domainToSendFrom, mailgunSendData);
151
+
152
+ // Log success
153
+ await this.logEmailAttempt(
154
+ {
155
+ to: data.admin.email,
156
+ subject,
157
+ templateName: "clinic_welcome",
158
+ },
159
+ true
160
+ );
161
+
162
+ return result;
163
+ } catch (error: any) {
164
+ Logger.error(
165
+ "[ClinicWelcomeMailingService] Error sending welcome email:",
166
+ {
167
+ errorMessage: error.message,
168
+ errorDetails: error.details,
169
+ errorStatus: error.status,
170
+ stack: error.stack,
171
+ }
172
+ );
173
+
174
+ // Log failure
175
+ await this.logEmailAttempt(
176
+ {
177
+ to: data.admin.email,
178
+ subject: data.options?.customSubject || this.DEFAULT_SUBJECT,
179
+ templateName: "clinic_welcome",
180
+ },
181
+ false,
182
+ error
183
+ );
184
+
185
+ throw error;
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Handles the clinic group creation event from Cloud Functions.
191
+ * Fetches necessary data and sends the welcome email to the admin.
192
+ * @param clinicGroupData - The clinic group data including id
193
+ * @param mailgunConfig - Mailgun configuration (from, domain)
194
+ * @returns Promise resolved when the email is sent
195
+ */
196
+ async handleClinicGroupCreationEvent(
197
+ clinicGroupData: ClinicGroup & { id: string },
198
+ mailgunConfig: {
199
+ fromAddress: string;
200
+ domain: string;
201
+ dashboardUrl?: string;
202
+ supportEmail?: string;
203
+ }
204
+ ): Promise<void> {
205
+ try {
206
+ Logger.info(
207
+ "[ClinicWelcomeMailingService] Handling clinic group creation event for:",
208
+ clinicGroupData.id
209
+ );
210
+
211
+ // Validate clinic group data
212
+ if (!clinicGroupData || !clinicGroupData.id) {
213
+ throw new Error(
214
+ `Invalid clinic group data: Missing required properties.`
215
+ );
216
+ }
217
+
218
+ // Validate contact person
219
+ if (!clinicGroupData.contactPerson) {
220
+ throw new Error(
221
+ `Clinic group ${clinicGroupData.id} is missing contactPerson`
222
+ );
223
+ }
224
+
225
+ if (!clinicGroupData.contactPerson.email) {
226
+ throw new Error(
227
+ `Clinic group ${clinicGroupData.id} is missing admin email`
228
+ );
229
+ }
230
+
231
+ // Get admin name from contact person
232
+ const adminName = `${clinicGroupData.contactPerson.firstName || ""} ${
233
+ clinicGroupData.contactPerson.lastName || ""
234
+ }`.trim() || "Clinic Admin";
235
+
236
+ const adminEmail = clinicGroupData.contactPerson.email;
237
+
238
+ Logger.info(
239
+ `[ClinicWelcomeMailingService] Sending welcome email to admin: ${adminName} (${adminEmail})`
240
+ );
241
+
242
+ // Validate fromAddress
243
+ if (!mailgunConfig.fromAddress) {
244
+ Logger.warn(
245
+ "[ClinicWelcomeMailingService] No fromAddress provided, using default"
246
+ );
247
+ mailgunConfig.fromAddress = `MetaEsthetics <no-reply@${this.DEFAULT_MAILGUN_DOMAIN}>`;
248
+ }
249
+
250
+ // Prepare email data
251
+ const emailData: ClinicWelcomeEmailData = {
252
+ admin: {
253
+ name: adminName,
254
+ email: adminEmail,
255
+ },
256
+ clinicGroup: {
257
+ id: clinicGroupData.id,
258
+ name: clinicGroupData.name || "Your Clinic",
259
+ },
260
+ options: {
261
+ fromAddress: mailgunConfig.fromAddress,
262
+ mailgunDomain: mailgunConfig.domain,
263
+ dashboardUrl: mailgunConfig.dashboardUrl,
264
+ supportEmail: mailgunConfig.supportEmail,
265
+ },
266
+ };
267
+
268
+ Logger.info(
269
+ "[ClinicWelcomeMailingService] Email data prepared, sending welcome email"
270
+ );
271
+
272
+ // Send the welcome email
273
+ await this.sendWelcomeEmail(emailData);
274
+
275
+ Logger.info(
276
+ "[ClinicWelcomeMailingService] Welcome email sent successfully"
277
+ );
278
+ } catch (error: any) {
279
+ Logger.error(
280
+ "[ClinicWelcomeMailingService] Error handling clinic group creation event:",
281
+ {
282
+ errorMessage: error.message,
283
+ errorDetails: error.details,
284
+ errorStatus: error.status,
285
+ stack: error.stack,
286
+ clinicGroupId: clinicGroupData?.id,
287
+ }
288
+ );
289
+ throw error;
290
+ }
291
+ }
292
+ }
@@ -0,0 +1 @@
1
+ export * from "./clinicWelcome.mailing";
@@ -0,0 +1,252 @@
1
+ /**
2
+ * HTML email template for clinic welcome/confirmation email
3
+ * Sent to clinic admins after successful signup
4
+ */
5
+ export const clinicWelcomeTemplate = `
6
+ <!DOCTYPE html>
7
+ <html>
8
+ <head>
9
+ <meta charset="UTF-8">
10
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
11
+ <title>Welcome to MetaEsthetics</title>
12
+ <style>
13
+ body {
14
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
15
+ line-height: 1.6;
16
+ color: #333;
17
+ margin: 0;
18
+ padding: 0;
19
+ background-color: #f5f5f5;
20
+ }
21
+ .container {
22
+ max-width: 600px;
23
+ margin: 0 auto;
24
+ padding: 20px;
25
+ }
26
+ .header {
27
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
28
+ padding: 40px 20px;
29
+ text-align: center;
30
+ border-radius: 8px 8px 0 0;
31
+ }
32
+ .header h1 {
33
+ color: white;
34
+ margin: 0;
35
+ font-size: 28px;
36
+ font-weight: 600;
37
+ }
38
+ .header p {
39
+ color: rgba(255, 255, 255, 0.9);
40
+ margin: 10px 0 0 0;
41
+ font-size: 16px;
42
+ }
43
+ .content {
44
+ padding: 40px 30px;
45
+ background-color: #ffffff;
46
+ }
47
+ .greeting {
48
+ font-size: 18px;
49
+ color: #333;
50
+ margin-bottom: 20px;
51
+ }
52
+ .message {
53
+ font-size: 16px;
54
+ color: #555;
55
+ margin-bottom: 25px;
56
+ }
57
+ .highlight-box {
58
+ background: linear-gradient(135deg, #f6f9fc 0%, #eef2f7 100%);
59
+ border-left: 4px solid #667eea;
60
+ padding: 20px;
61
+ margin: 25px 0;
62
+ border-radius: 0 8px 8px 0;
63
+ }
64
+ .highlight-box h3 {
65
+ margin: 0 0 10px 0;
66
+ color: #333;
67
+ font-size: 16px;
68
+ }
69
+ .highlight-box p {
70
+ margin: 0;
71
+ color: #555;
72
+ }
73
+ .next-steps {
74
+ margin: 30px 0;
75
+ }
76
+ .next-steps h3 {
77
+ color: #333;
78
+ font-size: 18px;
79
+ margin-bottom: 15px;
80
+ }
81
+ .step {
82
+ display: flex;
83
+ align-items: flex-start;
84
+ margin-bottom: 15px;
85
+ }
86
+ .step-number {
87
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
88
+ color: white;
89
+ width: 28px;
90
+ height: 28px;
91
+ border-radius: 50%;
92
+ display: flex;
93
+ align-items: center;
94
+ justify-content: center;
95
+ font-weight: 600;
96
+ font-size: 14px;
97
+ margin-right: 12px;
98
+ flex-shrink: 0;
99
+ }
100
+ .step-content {
101
+ flex: 1;
102
+ padding-top: 3px;
103
+ }
104
+ .step-title {
105
+ font-weight: 600;
106
+ color: #333;
107
+ margin-bottom: 3px;
108
+ }
109
+ .step-description {
110
+ color: #666;
111
+ font-size: 14px;
112
+ }
113
+ .button-container {
114
+ text-align: center;
115
+ margin: 35px 0;
116
+ }
117
+ .button {
118
+ display: inline-block;
119
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
120
+ color: white;
121
+ text-decoration: none;
122
+ padding: 14px 32px;
123
+ border-radius: 6px;
124
+ font-weight: 600;
125
+ font-size: 16px;
126
+ transition: transform 0.2s;
127
+ }
128
+ .button:hover {
129
+ transform: translateY(-2px);
130
+ }
131
+ .support-box {
132
+ background-color: #f8f9fa;
133
+ padding: 20px;
134
+ border-radius: 8px;
135
+ margin-top: 30px;
136
+ text-align: center;
137
+ }
138
+ .support-box p {
139
+ margin: 0 0 10px 0;
140
+ color: #555;
141
+ }
142
+ .support-box a {
143
+ color: #667eea;
144
+ text-decoration: none;
145
+ font-weight: 500;
146
+ }
147
+ .footer {
148
+ padding: 30px;
149
+ text-align: center;
150
+ background-color: #f8f9fa;
151
+ border-radius: 0 0 8px 8px;
152
+ }
153
+ .footer p {
154
+ margin: 5px 0;
155
+ font-size: 13px;
156
+ color: #888;
157
+ }
158
+ .footer a {
159
+ color: #667eea;
160
+ text-decoration: none;
161
+ }
162
+ .social-links {
163
+ margin: 15px 0;
164
+ }
165
+ .social-links a {
166
+ display: inline-block;
167
+ margin: 0 8px;
168
+ color: #888;
169
+ text-decoration: none;
170
+ }
171
+ </style>
172
+ </head>
173
+ <body>
174
+ <div class="container">
175
+ <div class="header">
176
+ <h1>Welcome to MetaEsthetics!</h1>
177
+ <p>Your clinic registration is complete</p>
178
+ </div>
179
+ <div class="content">
180
+ <p class="greeting">Hello {{adminName}},</p>
181
+
182
+ <p class="message">
183
+ Thank you for registering <strong>{{clinicGroupName}}</strong> with MetaEsthetics.
184
+ Your account has been successfully created and you're now ready to start managing
185
+ your aesthetic practice with our comprehensive platform.
186
+ </p>
187
+
188
+ <div class="highlight-box">
189
+ <h3>Account Details</h3>
190
+ <p><strong>Clinic Group:</strong> {{clinicGroupName}}</p>
191
+ <p><strong>Admin Email:</strong> {{adminEmail}}</p>
192
+ <p><strong>Registration Date:</strong> {{registrationDate}}</p>
193
+ </div>
194
+
195
+ <div class="next-steps">
196
+ <h3>Get Started with MetaEsthetics</h3>
197
+
198
+ <div class="step">
199
+ <div class="step-number">1</div>
200
+ <div class="step-content">
201
+ <div class="step-title">Complete Your Profile</div>
202
+ <div class="step-description">Add your clinic details, working hours, and upload photos to attract patients.</div>
203
+ </div>
204
+ </div>
205
+
206
+ <div class="step">
207
+ <div class="step-number">2</div>
208
+ <div class="step-content">
209
+ <div class="step-title">Add Your Team</div>
210
+ <div class="step-description">Invite practitioners and staff members to join your clinic.</div>
211
+ </div>
212
+ </div>
213
+
214
+ <div class="step">
215
+ <div class="step-number">3</div>
216
+ <div class="step-content">
217
+ <div class="step-title">Set Up Services</div>
218
+ <div class="step-description">Configure your procedures, pricing, and availability for bookings.</div>
219
+ </div>
220
+ </div>
221
+
222
+ <div class="step">
223
+ <div class="step-number">4</div>
224
+ <div class="step-content">
225
+ <div class="step-title">Start Accepting Patients</div>
226
+ <div class="step-description">Begin managing appointments and growing your practice.</div>
227
+ </div>
228
+ </div>
229
+ </div>
230
+
231
+ <div class="button-container">
232
+ <a href="{{dashboardUrl}}" class="button">Go to Dashboard</a>
233
+ </div>
234
+
235
+ <div class="support-box">
236
+ <p>Need help getting started?</p>
237
+ <p>Contact our support team at <a href="mailto:{{supportEmail}}">{{supportEmail}}</a></p>
238
+ </div>
239
+ </div>
240
+ <div class="footer">
241
+ <p>This is an automated message from MetaEsthetics.</p>
242
+ <p>&copy; {{currentYear}} MetaEsthetics. All rights reserved.</p>
243
+ <div class="social-links">
244
+ <a href="https://metaesthetics.net">Website</a> |
245
+ <a href="https://metaesthetics.net/privacy">Privacy Policy</a> |
246
+ <a href="https://metaesthetics.net/terms">Terms of Service</a>
247
+ </div>
248
+ </div>
249
+ </div>
250
+ </body>
251
+ </html>
252
+ `;
@@ -2,3 +2,4 @@ export * from "./base.mailing.service";
2
2
  export * from "./practitionerInvite";
3
3
  export * from "./patientInvite";
4
4
  export * from "./appointment";
5
+ export * from "./clinicWelcome";