@blackcode_sa/metaestetics-api 1.7.33 → 1.7.35

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.
@@ -865,6 +865,7 @@ var PractitionerInviteStatus = /* @__PURE__ */ ((PractitionerInviteStatus2) => {
865
865
 
866
866
  // src/types/clinic/index.ts
867
867
  var CLINIC_GROUPS_COLLECTION = "clinic_groups";
868
+ var CLINIC_ADMINS_COLLECTION = "clinic_admins";
868
869
  var CLINICS_COLLECTION = "clinics";
869
870
 
870
871
  // src/types/patient/index.ts
@@ -1729,22 +1730,44 @@ var PractitionerInviteAggregationService = class {
1729
1730
  /**
1730
1731
  * Constructor for PractitionerInviteAggregationService.
1731
1732
  * @param firestore Optional Firestore instance. If not provided, it uses the default admin SDK instance.
1733
+ * @param mailingService Optional mailing service for sending emails
1732
1734
  */
1733
- constructor(firestore17) {
1735
+ constructor(firestore17, mailingService) {
1734
1736
  this.db = firestore17 || admin5.firestore();
1737
+ this.mailingService = mailingService;
1735
1738
  Logger.info("[PractitionerInviteAggregationService] Initialized.");
1736
1739
  }
1737
1740
  /**
1738
1741
  * Handles side effects when a practitioner invite is first created.
1739
1742
  * This function would typically be called by a Firestore onCreate trigger.
1740
1743
  * @param {PractitionerInvite} invite - The newly created PractitionerInvite object.
1744
+ * @param {object} emailConfig - Optional email configuration for sending invite emails
1741
1745
  * @returns {Promise<void>}
1742
1746
  */
1743
- async handleInviteCreate(invite) {
1747
+ async handleInviteCreate(invite, emailConfig) {
1744
1748
  Logger.info(
1745
1749
  `[PractitionerInviteAggService] Handling CREATE for invite: ${invite.id}, practitioner: ${invite.practitionerId}, clinic: ${invite.clinicId}, status: ${invite.status}`
1746
1750
  );
1747
1751
  try {
1752
+ if (this.mailingService && emailConfig && invite.status === "pending" /* PENDING */) {
1753
+ Logger.info(
1754
+ `[PractitionerInviteAggService] Sending invitation email for invite: ${invite.id}`
1755
+ );
1756
+ try {
1757
+ await this.mailingService.handleInviteCreationEvent(
1758
+ invite,
1759
+ emailConfig
1760
+ );
1761
+ Logger.info(
1762
+ `[PractitionerInviteAggService] Successfully sent invitation email for invite: ${invite.id}`
1763
+ );
1764
+ } catch (emailError) {
1765
+ Logger.error(
1766
+ `[PractitionerInviteAggService] Error sending invitation email for invite ${invite.id}:`,
1767
+ emailError
1768
+ );
1769
+ }
1770
+ }
1748
1771
  Logger.info(
1749
1772
  `[PractitionerInviteAggService] Successfully processed CREATE for invite: ${invite.id}`
1750
1773
  );
@@ -1761,9 +1784,10 @@ var PractitionerInviteAggregationService = class {
1761
1784
  * This function would typically be called by a Firestore onUpdate trigger.
1762
1785
  * @param {PractitionerInvite} before - The PractitionerInvite object before the update.
1763
1786
  * @param {PractitionerInvite} after - The PractitionerInvite object after the update.
1787
+ * @param {object} emailConfig - Optional email configuration for sending notification emails
1764
1788
  * @returns {Promise<void>}
1765
1789
  */
1766
- async handleInviteUpdate(before, after) {
1790
+ async handleInviteUpdate(before, after, emailConfig) {
1767
1791
  Logger.info(
1768
1792
  `[PractitionerInviteAggService] Handling UPDATE for invite: ${after.id}. Status ${before.status} -> ${after.status}`
1769
1793
  );
@@ -1777,12 +1801,12 @@ var PractitionerInviteAggregationService = class {
1777
1801
  Logger.info(
1778
1802
  `[PractitionerInviteAggService] Invite ${after.id} PENDING -> ACCEPTED. Adding practitioner to clinic.`
1779
1803
  );
1780
- await this.handleInviteAccepted(after);
1804
+ await this.handleInviteAccepted(after, emailConfig);
1781
1805
  } else if (before.status === "pending" /* PENDING */ && after.status === "rejected" /* REJECTED */) {
1782
1806
  Logger.info(
1783
1807
  `[PractitionerInviteAggService] Invite ${after.id} PENDING -> REJECTED.`
1784
1808
  );
1785
- await this.handleInviteRejected(after);
1809
+ await this.handleInviteRejected(after, emailConfig);
1786
1810
  } else if (before.status === "pending" /* PENDING */ && after.status === "cancelled" /* CANCELLED */) {
1787
1811
  Logger.info(
1788
1812
  `[PractitionerInviteAggService] Invite ${after.id} PENDING -> CANCELLED.`
@@ -1827,9 +1851,10 @@ var PractitionerInviteAggregationService = class {
1827
1851
  * Handles the business logic when a practitioner accepts an invite.
1828
1852
  * This includes adding the practitioner to the clinic and the clinic to the practitioner.
1829
1853
  * @param {PractitionerInvite} invite - The accepted invite
1854
+ * @param {object} emailConfig - Optional email configuration for sending notification emails
1830
1855
  * @returns {Promise<void>}
1831
1856
  */
1832
- async handleInviteAccepted(invite) {
1857
+ async handleInviteAccepted(invite, emailConfig) {
1833
1858
  var _a, _b, _c, _d;
1834
1859
  Logger.info(
1835
1860
  `[PractitionerInviteAggService] Processing accepted invite ${invite.id} for practitioner ${invite.practitionerId} and clinic ${invite.clinicId}`
@@ -1890,6 +1915,27 @@ var PractitionerInviteAggregationService = class {
1890
1915
  );
1891
1916
  await this.updatePractitionerWorkingHours(practitioner.id, invite);
1892
1917
  }
1918
+ if (this.mailingService && emailConfig) {
1919
+ Logger.info(
1920
+ `[PractitionerInviteAggService] Sending acceptance notification email for invite: ${invite.id}`
1921
+ );
1922
+ try {
1923
+ await this.sendAcceptanceNotificationEmail(
1924
+ invite,
1925
+ practitioner,
1926
+ clinic,
1927
+ emailConfig
1928
+ );
1929
+ Logger.info(
1930
+ `[PractitionerInviteAggService] Successfully sent acceptance notification email for invite: ${invite.id}`
1931
+ );
1932
+ } catch (emailError) {
1933
+ Logger.error(
1934
+ `[PractitionerInviteAggService] Error sending acceptance notification email for invite ${invite.id}:`,
1935
+ emailError
1936
+ );
1937
+ }
1938
+ }
1893
1939
  Logger.info(
1894
1940
  `[PractitionerInviteAggService] Successfully processed accepted invite ${invite.id}`
1895
1941
  );
@@ -1904,13 +1950,41 @@ var PractitionerInviteAggregationService = class {
1904
1950
  /**
1905
1951
  * Handles the business logic when a practitioner rejects an invite.
1906
1952
  * @param {PractitionerInvite} invite - The rejected invite
1953
+ * @param {object} emailConfig - Optional email configuration for sending notification emails
1907
1954
  * @returns {Promise<void>}
1908
1955
  */
1909
- async handleInviteRejected(invite) {
1956
+ async handleInviteRejected(invite, emailConfig) {
1910
1957
  Logger.info(
1911
1958
  `[PractitionerInviteAggService] Processing rejected invite ${invite.id}`
1912
1959
  );
1913
1960
  try {
1961
+ if (this.mailingService && emailConfig) {
1962
+ Logger.info(
1963
+ `[PractitionerInviteAggService] Sending rejection notification email for invite: ${invite.id}`
1964
+ );
1965
+ try {
1966
+ const [practitioner, clinic] = await Promise.all([
1967
+ this.fetchPractitionerById(invite.practitionerId),
1968
+ this.fetchClinicById(invite.clinicId)
1969
+ ]);
1970
+ if (practitioner && clinic) {
1971
+ await this.sendRejectionNotificationEmail(
1972
+ invite,
1973
+ practitioner,
1974
+ clinic,
1975
+ emailConfig
1976
+ );
1977
+ Logger.info(
1978
+ `[PractitionerInviteAggService] Successfully sent rejection notification email for invite: ${invite.id}`
1979
+ );
1980
+ }
1981
+ } catch (emailError) {
1982
+ Logger.error(
1983
+ `[PractitionerInviteAggService] Error sending rejection notification email for invite ${invite.id}:`,
1984
+ emailError
1985
+ );
1986
+ }
1987
+ }
1914
1988
  Logger.info(
1915
1989
  `[PractitionerInviteAggService] Successfully processed rejected invite ${invite.id}`
1916
1990
  );
@@ -2102,6 +2176,23 @@ var PractitionerInviteAggregationService = class {
2102
2176
  }
2103
2177
  }
2104
2178
  // --- Data Fetching Helpers ---
2179
+ /**
2180
+ * Fetches a clinic admin by ID
2181
+ * @param adminId The clinic admin ID
2182
+ * @returns The clinic admin or null if not found
2183
+ */
2184
+ async fetchClinicAdminById(adminId) {
2185
+ try {
2186
+ const doc = await this.db.collection(CLINIC_ADMINS_COLLECTION).doc(adminId).get();
2187
+ return doc.exists ? doc.data() : null;
2188
+ } catch (error) {
2189
+ Logger.error(
2190
+ `[PractitionerInviteAggService] Error fetching clinic admin ${adminId}:`,
2191
+ error
2192
+ );
2193
+ return null;
2194
+ }
2195
+ }
2105
2196
  /**
2106
2197
  * Fetches a practitioner by ID.
2107
2198
  * @param practitionerId The practitioner ID.
@@ -2136,6 +2227,188 @@ var PractitionerInviteAggregationService = class {
2136
2227
  return null;
2137
2228
  }
2138
2229
  }
2230
+ // --- Email Helper Methods ---
2231
+ /**
2232
+ * Sends acceptance notification email to clinic admin
2233
+ * @param invite The accepted invite
2234
+ * @param practitioner The practitioner who accepted
2235
+ * @param clinic The clinic that sent the invite
2236
+ * @param emailConfig Email configuration
2237
+ */
2238
+ async sendAcceptanceNotificationEmail(invite, practitioner, clinic, emailConfig) {
2239
+ var _a, _b, _c, _d, _e, _f;
2240
+ if (!this.mailingService) return;
2241
+ try {
2242
+ const admin17 = await this.fetchClinicAdminById(invite.invitedBy);
2243
+ if (!admin17) {
2244
+ Logger.warn(
2245
+ `[PractitionerInviteAggService] Admin ${invite.invitedBy} not found, using clinic contact email as fallback`
2246
+ );
2247
+ const notificationData2 = {
2248
+ invite,
2249
+ practitioner: {
2250
+ firstName: practitioner.basicInfo.firstName || "",
2251
+ lastName: practitioner.basicInfo.lastName || "",
2252
+ specialties: ((_b = (_a = practitioner.certification) == null ? void 0 : _a.specialties) == null ? void 0 : _b.map(
2253
+ (s) => s.name || s
2254
+ )) || [],
2255
+ profileImageUrl: typeof practitioner.basicInfo.profileImageUrl === "string" ? practitioner.basicInfo.profileImageUrl : null,
2256
+ experienceYears: void 0
2257
+ },
2258
+ clinic: {
2259
+ name: clinic.name,
2260
+ adminName: "Admin",
2261
+ adminEmail: clinic.contactInfo.email
2262
+ },
2263
+ context: {
2264
+ invitationDate: invite.createdAt.toDate().toLocaleDateString(),
2265
+ responseDate: ((_c = invite.acceptedAt) == null ? void 0 : _c.toDate().toLocaleDateString()) || (/* @__PURE__ */ new Date()).toLocaleDateString()
2266
+ },
2267
+ urls: {
2268
+ clinicDashboardUrl: emailConfig.clinicDashboardUrl,
2269
+ practitionerProfileUrl: emailConfig.practitionerProfileUrl
2270
+ },
2271
+ options: {
2272
+ fromAddress: emailConfig.fromAddress,
2273
+ mailgunDomain: emailConfig.domain
2274
+ }
2275
+ };
2276
+ await this.mailingService.sendAcceptedNotificationEmail(
2277
+ notificationData2
2278
+ );
2279
+ return;
2280
+ }
2281
+ const adminName = `${admin17.contactInfo.firstName} ${admin17.contactInfo.lastName}`;
2282
+ const notificationData = {
2283
+ invite,
2284
+ practitioner: {
2285
+ firstName: practitioner.basicInfo.firstName || "",
2286
+ lastName: practitioner.basicInfo.lastName || "",
2287
+ specialties: ((_e = (_d = practitioner.certification) == null ? void 0 : _d.specialties) == null ? void 0 : _e.map(
2288
+ (s) => s.name || s
2289
+ )) || [],
2290
+ profileImageUrl: typeof practitioner.basicInfo.profileImageUrl === "string" ? practitioner.basicInfo.profileImageUrl : null,
2291
+ experienceYears: void 0
2292
+ // This would need to be calculated or stored in practitioner data
2293
+ },
2294
+ clinic: {
2295
+ name: clinic.name,
2296
+ adminName,
2297
+ adminEmail: admin17.contactInfo.email
2298
+ // Use the specific admin's email
2299
+ },
2300
+ context: {
2301
+ invitationDate: invite.createdAt.toDate().toLocaleDateString(),
2302
+ responseDate: ((_f = invite.acceptedAt) == null ? void 0 : _f.toDate().toLocaleDateString()) || (/* @__PURE__ */ new Date()).toLocaleDateString()
2303
+ },
2304
+ urls: {
2305
+ clinicDashboardUrl: emailConfig.clinicDashboardUrl,
2306
+ practitionerProfileUrl: emailConfig.practitionerProfileUrl
2307
+ },
2308
+ options: {
2309
+ fromAddress: emailConfig.fromAddress,
2310
+ mailgunDomain: emailConfig.domain
2311
+ }
2312
+ };
2313
+ await this.mailingService.sendAcceptedNotificationEmail(notificationData);
2314
+ } catch (error) {
2315
+ Logger.error(
2316
+ `[PractitionerInviteAggService] Error sending acceptance notification email:`,
2317
+ error
2318
+ );
2319
+ throw error;
2320
+ }
2321
+ }
2322
+ /**
2323
+ * Sends rejection notification email to clinic admin
2324
+ * @param invite The rejected invite
2325
+ * @param practitioner The practitioner who rejected
2326
+ * @param clinic The clinic that sent the invite
2327
+ * @param emailConfig Email configuration
2328
+ */
2329
+ async sendRejectionNotificationEmail(invite, practitioner, clinic, emailConfig) {
2330
+ var _a, _b, _c, _d, _e, _f;
2331
+ if (!this.mailingService) return;
2332
+ try {
2333
+ const admin17 = await this.fetchClinicAdminById(invite.invitedBy);
2334
+ if (!admin17) {
2335
+ Logger.warn(
2336
+ `[PractitionerInviteAggService] Admin ${invite.invitedBy} not found, using clinic contact email as fallback`
2337
+ );
2338
+ const notificationData2 = {
2339
+ invite,
2340
+ practitioner: {
2341
+ firstName: practitioner.basicInfo.firstName || "",
2342
+ lastName: practitioner.basicInfo.lastName || "",
2343
+ specialties: ((_b = (_a = practitioner.certification) == null ? void 0 : _a.specialties) == null ? void 0 : _b.map(
2344
+ (s) => s.name || s
2345
+ )) || [],
2346
+ profileImageUrl: typeof practitioner.basicInfo.profileImageUrl === "string" ? practitioner.basicInfo.profileImageUrl : null
2347
+ },
2348
+ clinic: {
2349
+ name: clinic.name,
2350
+ adminName: "Admin",
2351
+ adminEmail: clinic.contactInfo.email
2352
+ },
2353
+ context: {
2354
+ invitationDate: invite.createdAt.toDate().toLocaleDateString(),
2355
+ responseDate: ((_c = invite.rejectedAt) == null ? void 0 : _c.toDate().toLocaleDateString()) || (/* @__PURE__ */ new Date()).toLocaleDateString(),
2356
+ rejectionReason: invite.rejectionReason || void 0
2357
+ },
2358
+ urls: {
2359
+ clinicDashboardUrl: emailConfig.clinicDashboardUrl,
2360
+ findPractitionersUrl: emailConfig.findPractitionersUrl
2361
+ },
2362
+ options: {
2363
+ fromAddress: emailConfig.fromAddress,
2364
+ mailgunDomain: emailConfig.domain
2365
+ }
2366
+ };
2367
+ await this.mailingService.sendRejectedNotificationEmail(
2368
+ notificationData2
2369
+ );
2370
+ return;
2371
+ }
2372
+ const adminName = `${admin17.contactInfo.firstName} ${admin17.contactInfo.lastName}`;
2373
+ const notificationData = {
2374
+ invite,
2375
+ practitioner: {
2376
+ firstName: practitioner.basicInfo.firstName || "",
2377
+ lastName: practitioner.basicInfo.lastName || "",
2378
+ specialties: ((_e = (_d = practitioner.certification) == null ? void 0 : _d.specialties) == null ? void 0 : _e.map(
2379
+ (s) => s.name || s
2380
+ )) || [],
2381
+ profileImageUrl: typeof practitioner.basicInfo.profileImageUrl === "string" ? practitioner.basicInfo.profileImageUrl : null
2382
+ },
2383
+ clinic: {
2384
+ name: clinic.name,
2385
+ adminName,
2386
+ adminEmail: admin17.contactInfo.email
2387
+ // Use the specific admin's email
2388
+ },
2389
+ context: {
2390
+ invitationDate: invite.createdAt.toDate().toLocaleDateString(),
2391
+ responseDate: ((_f = invite.rejectedAt) == null ? void 0 : _f.toDate().toLocaleDateString()) || (/* @__PURE__ */ new Date()).toLocaleDateString(),
2392
+ rejectionReason: invite.rejectionReason || void 0
2393
+ },
2394
+ urls: {
2395
+ clinicDashboardUrl: emailConfig.clinicDashboardUrl,
2396
+ findPractitionersUrl: emailConfig.findPractitionersUrl
2397
+ },
2398
+ options: {
2399
+ fromAddress: emailConfig.fromAddress,
2400
+ mailgunDomain: emailConfig.domain
2401
+ }
2402
+ };
2403
+ await this.mailingService.sendRejectedNotificationEmail(notificationData);
2404
+ } catch (error) {
2405
+ Logger.error(
2406
+ `[PractitionerInviteAggService] Error sending rejection notification email:`,
2407
+ error
2408
+ );
2409
+ throw error;
2410
+ }
2411
+ }
2139
2412
  };
2140
2413
 
2141
2414
  // src/admin/aggregation/procedure/procedure.aggregation.service.ts
@@ -5936,6 +6209,1005 @@ var PractitionerInviteMailingService = class extends BaseMailingService {
5936
6209
  }
5937
6210
  };
5938
6211
 
6212
+ // src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts
6213
+ var existingPractitionerInvitationTemplate = `
6214
+ <!DOCTYPE html>
6215
+ <html lang="en">
6216
+ <head>
6217
+ <meta charset="UTF-8">
6218
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6219
+ <title>Invitation to Join {{clinicName}}</title>
6220
+ <style>
6221
+ body {
6222
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
6223
+ line-height: 1.6;
6224
+ color: #333;
6225
+ max-width: 600px;
6226
+ margin: 0 auto;
6227
+ padding: 20px;
6228
+ background-color: #f4f4f4;
6229
+ }
6230
+ .container {
6231
+ background-color: #ffffff;
6232
+ padding: 30px;
6233
+ border-radius: 10px;
6234
+ box-shadow: 0 0 20px rgba(0,0,0,0.1);
6235
+ }
6236
+ .header {
6237
+ text-align: center;
6238
+ margin-bottom: 30px;
6239
+ border-bottom: 2px solid #007bff;
6240
+ padding-bottom: 20px;
6241
+ }
6242
+ .logo {
6243
+ font-size: 28px;
6244
+ font-weight: bold;
6245
+ color: #007bff;
6246
+ margin-bottom: 10px;
6247
+ }
6248
+ h1 {
6249
+ color: #007bff;
6250
+ text-align: center;
6251
+ margin-bottom: 20px;
6252
+ }
6253
+ .highlight {
6254
+ background-color: #e7f3ff;
6255
+ padding: 15px;
6256
+ border-left: 4px solid #007bff;
6257
+ margin: 20px 0;
6258
+ }
6259
+ .button {
6260
+ display: inline-block;
6261
+ background-color: #28a745;
6262
+ color: white;
6263
+ padding: 12px 25px;
6264
+ text-decoration: none;
6265
+ border-radius: 5px;
6266
+ margin: 10px 5px;
6267
+ font-weight: bold;
6268
+ text-align: center;
6269
+ }
6270
+ .button.reject {
6271
+ background-color: #dc3545;
6272
+ }
6273
+ .button:hover {
6274
+ opacity: 0.9;
6275
+ }
6276
+ .action-section {
6277
+ text-align: center;
6278
+ margin: 30px 0;
6279
+ padding: 20px;
6280
+ background-color: #f8f9fa;
6281
+ border-radius: 5px;
6282
+ }
6283
+ .details {
6284
+ background-color: #f8f9fa;
6285
+ padding: 15px;
6286
+ border-radius: 5px;
6287
+ margin: 15px 0;
6288
+ }
6289
+ .footer {
6290
+ margin-top: 30px;
6291
+ padding-top: 20px;
6292
+ border-top: 1px solid #eee;
6293
+ font-size: 14px;
6294
+ color: #666;
6295
+ text-align: center;
6296
+ }
6297
+ .contact-info {
6298
+ margin-top: 15px;
6299
+ padding: 10px;
6300
+ background-color: #e9ecef;
6301
+ border-radius: 5px;
6302
+ }
6303
+ </style>
6304
+ </head>
6305
+ <body>
6306
+ <div class="container">
6307
+ <div class="header">
6308
+ <div class="logo">MetaEstetics</div>
6309
+ <p>Professional Medical Network</p>
6310
+ </div>
6311
+
6312
+ <h1>You're Invited to Join {{clinicName}}!</h1>
6313
+
6314
+ <p>Dear Dr. {{practitionerName}},</p>
6315
+
6316
+ <p>We hope this message finds you well. You have been invited to join <strong>{{clinicName}}</strong> as a practicing medical professional.</p>
6317
+
6318
+ <div class="highlight">
6319
+ <strong>Invitation Details:</strong>
6320
+ <div class="details">
6321
+ <p><strong>Clinic:</strong> {{clinicName}}</p>
6322
+ <p><strong>Location:</strong> {{clinicAddress}}</p>
6323
+ <p><strong>Proposed Working Hours:</strong> {{workingHours}}</p>
6324
+ <p><strong>Invitation Expires:</strong> {{expirationDate}}</p>
6325
+ </div>
6326
+ </div>
6327
+
6328
+ <p>By accepting this invitation, you will:</p>
6329
+ <ul>
6330
+ <li>Join the {{clinicName}} team as a featured practitioner</li>
6331
+ <li>Have your profile displayed on their clinic page</li>
6332
+ <li>Be available for appointments at the specified working hours</li>
6333
+ <li>Access their patient booking system and clinic resources</li>
6334
+ </ul>
6335
+
6336
+ <div class="action-section">
6337
+ <p><strong>Please respond to this invitation:</strong></p>
6338
+ <a href="{{acceptUrl}}" class="button">Accept Invitation</a>
6339
+ <a href="{{rejectUrl}}" class="button reject">Decline Invitation</a>
6340
+ </div>
6341
+
6342
+ <div class="contact-info">
6343
+ <p><strong>Questions?</strong> Contact the clinic administrator:</p>
6344
+ <p>\u{1F4E7} {{contactEmail}}<br>
6345
+ \u{1F4DE} {{contactPhone}}</p>
6346
+ </div>
6347
+
6348
+ <p>We look forward to welcoming you to the {{clinicName}} team!</p>
6349
+
6350
+ <div class="footer">
6351
+ <p>Best regards,<br>
6352
+ <strong>The MetaEstetics Team</strong></p>
6353
+ <p>This invitation will expire on {{expirationDate}}. Please respond before this date.</p>
6354
+ <hr>
6355
+ <p style="font-size: 12px; color: #999;">
6356
+ This is an automated message from MetaEstetics. If you received this email in error, please ignore it.
6357
+ <br>\xA9 {{currentYear}} MetaEstetics. All rights reserved.
6358
+ </p>
6359
+ </div>
6360
+ </div>
6361
+ </body>
6362
+ </html>
6363
+ `;
6364
+
6365
+ // src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts
6366
+ var inviteAcceptedNotificationTemplate = `
6367
+ <!DOCTYPE html>
6368
+ <html lang="en">
6369
+ <head>
6370
+ <meta charset="UTF-8">
6371
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6372
+ <title>Practitioner Invitation Accepted - {{practitionerName}}</title>
6373
+ <style>
6374
+ body {
6375
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
6376
+ line-height: 1.6;
6377
+ color: #333;
6378
+ max-width: 600px;
6379
+ margin: 0 auto;
6380
+ padding: 20px;
6381
+ background-color: #f4f4f4;
6382
+ }
6383
+ .container {
6384
+ background-color: #ffffff;
6385
+ padding: 30px;
6386
+ border-radius: 10px;
6387
+ box-shadow: 0 0 20px rgba(0,0,0,0.1);
6388
+ }
6389
+ .header {
6390
+ text-align: center;
6391
+ margin-bottom: 30px;
6392
+ border-bottom: 2px solid #28a745;
6393
+ padding-bottom: 20px;
6394
+ }
6395
+ .logo {
6396
+ font-size: 28px;
6397
+ font-weight: bold;
6398
+ color: #28a745;
6399
+ margin-bottom: 10px;
6400
+ }
6401
+ h1 {
6402
+ color: #28a745;
6403
+ text-align: center;
6404
+ margin-bottom: 20px;
6405
+ }
6406
+ .success-badge {
6407
+ background-color: #d4edda;
6408
+ color: #155724;
6409
+ padding: 15px;
6410
+ border-left: 4px solid #28a745;
6411
+ margin: 20px 0;
6412
+ border-radius: 0 5px 5px 0;
6413
+ }
6414
+ .practitioner-card {
6415
+ background-color: #f8f9fa;
6416
+ padding: 20px;
6417
+ border-radius: 8px;
6418
+ margin: 20px 0;
6419
+ border: 1px solid #dee2e6;
6420
+ }
6421
+ .practitioner-photo {
6422
+ width: 80px;
6423
+ height: 80px;
6424
+ border-radius: 50%;
6425
+ object-fit: cover;
6426
+ float: left;
6427
+ margin-right: 15px;
6428
+ border: 3px solid #28a745;
6429
+ }
6430
+ .practitioner-info {
6431
+ overflow: hidden;
6432
+ }
6433
+ .practitioner-name {
6434
+ font-size: 18px;
6435
+ font-weight: bold;
6436
+ color: #333;
6437
+ margin-bottom: 5px;
6438
+ }
6439
+ .practitioner-details {
6440
+ color: #666;
6441
+ font-size: 14px;
6442
+ line-height: 1.4;
6443
+ }
6444
+ .details-grid {
6445
+ display: grid;
6446
+ grid-template-columns: 1fr 1fr;
6447
+ gap: 15px;
6448
+ margin: 20px 0;
6449
+ }
6450
+ .detail-item {
6451
+ background-color: #f8f9fa;
6452
+ padding: 10px;
6453
+ border-radius: 5px;
6454
+ border-left: 3px solid #28a745;
6455
+ }
6456
+ .detail-label {
6457
+ font-weight: bold;
6458
+ color: #495057;
6459
+ font-size: 12px;
6460
+ text-transform: uppercase;
6461
+ margin-bottom: 5px;
6462
+ }
6463
+ .detail-value {
6464
+ color: #333;
6465
+ font-size: 14px;
6466
+ }
6467
+ .button {
6468
+ display: inline-block;
6469
+ background-color: #007bff;
6470
+ color: white;
6471
+ padding: 12px 25px;
6472
+ text-decoration: none;
6473
+ border-radius: 5px;
6474
+ margin: 10px 5px;
6475
+ font-weight: bold;
6476
+ text-align: center;
6477
+ }
6478
+ .button:hover {
6479
+ opacity: 0.9;
6480
+ }
6481
+ .action-section {
6482
+ text-align: center;
6483
+ margin: 30px 0;
6484
+ padding: 20px;
6485
+ background-color: #e9ecef;
6486
+ border-radius: 5px;
6487
+ }
6488
+ .footer {
6489
+ margin-top: 30px;
6490
+ padding-top: 20px;
6491
+ border-top: 1px solid #eee;
6492
+ font-size: 14px;
6493
+ color: #666;
6494
+ text-align: center;
6495
+ }
6496
+ .working-hours {
6497
+ background-color: #fff3cd;
6498
+ border: 1px solid #ffeaa7;
6499
+ border-radius: 5px;
6500
+ padding: 15px;
6501
+ margin: 15px 0;
6502
+ }
6503
+ .working-hours h4 {
6504
+ color: #856404;
6505
+ margin-top: 0;
6506
+ }
6507
+ </style>
6508
+ </head>
6509
+ <body>
6510
+ <div class="container">
6511
+ <div class="header">
6512
+ <div class="logo">MetaEstetics</div>
6513
+ <p>Clinic Management System</p>
6514
+ </div>
6515
+
6516
+ <h1>\u{1F389} Great News! Invitation Accepted</h1>
6517
+
6518
+ <div class="success-badge">
6519
+ <strong>\u2705 Dr. {{practitionerName}} has accepted your invitation!</strong>
6520
+ <p style="margin: 5px 0 0 0;">They are now part of the {{clinicName}} team.</p>
6521
+ </div>
6522
+
6523
+ <p>Dear {{clinicAdminName}},</p>
6524
+
6525
+ <p>We're excited to inform you that <strong>Dr. {{practitionerName}}</strong> has accepted your invitation to join <strong>{{clinicName}}</strong>.</p>
6526
+
6527
+ <div class="practitioner-card">
6528
+ {{#practitionerPhoto}}
6529
+ <img src="{{practitionerPhoto}}" alt="{{practitionerName}}" class="practitioner-photo">
6530
+ {{/practitionerPhoto}}
6531
+ <div class="practitioner-info">
6532
+ <div class="practitioner-name">Dr. {{practitionerName}}</div>
6533
+ <div class="practitioner-details">
6534
+ {{#practitionerSpecialties}}
6535
+ <p><strong>Specialties:</strong> {{practitionerSpecialties}}</p>
6536
+ {{/practitionerSpecialties}}
6537
+ {{#practitionerExperience}}
6538
+ <p><strong>Experience:</strong> {{practitionerExperience}} years</p>
6539
+ {{/practitionerExperience}}
6540
+ <p><strong>Joined:</strong> {{acceptedDate}}</p>
6541
+ </div>
6542
+ </div>
6543
+ </div>
6544
+
6545
+ <div class="details-grid">
6546
+ <div class="detail-item">
6547
+ <div class="detail-label">Status</div>
6548
+ <div class="detail-value">\u2705 Active Team Member</div>
6549
+ </div>
6550
+ <div class="detail-item">
6551
+ <div class="detail-label">Invitation Date</div>
6552
+ <div class="detail-value">{{invitationDate}}</div>
6553
+ </div>
6554
+ </div>
6555
+
6556
+ <div class="working-hours">
6557
+ <h4>\u{1F4C5} Scheduled Working Hours</h4>
6558
+ <p>{{workingHours}}</p>
6559
+ </div>
6560
+
6561
+ <div class="action-section">
6562
+ <p><strong>Next Steps:</strong></p>
6563
+ <a href="{{clinicDashboardUrl}}" class="button">View Clinic Dashboard</a>
6564
+ <a href="{{practitionerProfileUrl}}" class="button">View Practitioner Profile</a>
6565
+ </div>
6566
+
6567
+ <p><strong>What happens next?</strong></p>
6568
+ <ul>
6569
+ <li>Dr. {{practitionerName}} is now visible on your clinic profile</li>
6570
+ <li>Patients can book appointments with them during their scheduled hours</li>
6571
+ <li>They have access to your clinic's patient management system</li>
6572
+ <li>You can manage their schedule and availability from your dashboard</li>
6573
+ </ul>
6574
+
6575
+ <p>Welcome Dr. {{practitionerName}} to the team! \u{1F38A}</p>
6576
+
6577
+ <div class="footer">
6578
+ <p>Best regards,<br>
6579
+ <strong>The MetaEstetics Team</strong></p>
6580
+ <hr>
6581
+ <p style="font-size: 12px; color: #999;">
6582
+ This is an automated notification from MetaEstetics.
6583
+ <br>\xA9 {{currentYear}} MetaEstetics. All rights reserved.
6584
+ </p>
6585
+ </div>
6586
+ </div>
6587
+ </body>
6588
+ </html>
6589
+ `;
6590
+
6591
+ // src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts
6592
+ var inviteRejectedNotificationTemplate = `
6593
+ <!DOCTYPE html>
6594
+ <html lang="en">
6595
+ <head>
6596
+ <meta charset="UTF-8">
6597
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6598
+ <title>Practitioner Invitation Declined - {{practitionerName}}</title>
6599
+ <style>
6600
+ body {
6601
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
6602
+ line-height: 1.6;
6603
+ color: #333;
6604
+ max-width: 600px;
6605
+ margin: 0 auto;
6606
+ padding: 20px;
6607
+ background-color: #f4f4f4;
6608
+ }
6609
+ .container {
6610
+ background-color: #ffffff;
6611
+ padding: 30px;
6612
+ border-radius: 10px;
6613
+ box-shadow: 0 0 20px rgba(0,0,0,0.1);
6614
+ }
6615
+ .header {
6616
+ text-align: center;
6617
+ margin-bottom: 30px;
6618
+ border-bottom: 2px solid #dc3545;
6619
+ padding-bottom: 20px;
6620
+ }
6621
+ .logo {
6622
+ font-size: 28px;
6623
+ font-weight: bold;
6624
+ color: #dc3545;
6625
+ margin-bottom: 10px;
6626
+ }
6627
+ h1 {
6628
+ color: #dc3545;
6629
+ text-align: center;
6630
+ margin-bottom: 20px;
6631
+ }
6632
+ .notice-badge {
6633
+ background-color: #f8d7da;
6634
+ color: #721c24;
6635
+ padding: 15px;
6636
+ border-left: 4px solid #dc3545;
6637
+ margin: 20px 0;
6638
+ border-radius: 0 5px 5px 0;
6639
+ }
6640
+ .practitioner-card {
6641
+ background-color: #f8f9fa;
6642
+ padding: 20px;
6643
+ border-radius: 8px;
6644
+ margin: 20px 0;
6645
+ border: 1px solid #dee2e6;
6646
+ }
6647
+ .practitioner-photo {
6648
+ width: 80px;
6649
+ height: 80px;
6650
+ border-radius: 50%;
6651
+ object-fit: cover;
6652
+ float: left;
6653
+ margin-right: 15px;
6654
+ border: 3px solid #dc3545;
6655
+ }
6656
+ .practitioner-info {
6657
+ overflow: hidden;
6658
+ }
6659
+ .practitioner-name {
6660
+ font-size: 18px;
6661
+ font-weight: bold;
6662
+ color: #333;
6663
+ margin-bottom: 5px;
6664
+ }
6665
+ .practitioner-details {
6666
+ color: #666;
6667
+ font-size: 14px;
6668
+ line-height: 1.4;
6669
+ }
6670
+ .details-grid {
6671
+ display: grid;
6672
+ grid-template-columns: 1fr 1fr;
6673
+ gap: 15px;
6674
+ margin: 20px 0;
6675
+ }
6676
+ .detail-item {
6677
+ background-color: #f8f9fa;
6678
+ padding: 10px;
6679
+ border-radius: 5px;
6680
+ border-left: 3px solid #dc3545;
6681
+ }
6682
+ .detail-label {
6683
+ font-weight: bold;
6684
+ color: #495057;
6685
+ font-size: 12px;
6686
+ text-transform: uppercase;
6687
+ margin-bottom: 5px;
6688
+ }
6689
+ .detail-value {
6690
+ color: #333;
6691
+ font-size: 14px;
6692
+ }
6693
+ .button {
6694
+ display: inline-block;
6695
+ background-color: #007bff;
6696
+ color: white;
6697
+ padding: 12px 25px;
6698
+ text-decoration: none;
6699
+ border-radius: 5px;
6700
+ margin: 10px 5px;
6701
+ font-weight: bold;
6702
+ text-align: center;
6703
+ }
6704
+ .button.secondary {
6705
+ background-color: #6c757d;
6706
+ }
6707
+ .button:hover {
6708
+ opacity: 0.9;
6709
+ }
6710
+ .action-section {
6711
+ text-align: center;
6712
+ margin: 30px 0;
6713
+ padding: 20px;
6714
+ background-color: #e9ecef;
6715
+ border-radius: 5px;
6716
+ }
6717
+ .footer {
6718
+ margin-top: 30px;
6719
+ padding-top: 20px;
6720
+ border-top: 1px solid #eee;
6721
+ font-size: 14px;
6722
+ color: #666;
6723
+ text-align: center;
6724
+ }
6725
+ .suggestions {
6726
+ background-color: #d1ecf1;
6727
+ border: 1px solid #bee5eb;
6728
+ border-radius: 5px;
6729
+ padding: 15px;
6730
+ margin: 15px 0;
6731
+ }
6732
+ .suggestions h4 {
6733
+ color: #0c5460;
6734
+ margin-top: 0;
6735
+ }
6736
+ .reason-box {
6737
+ background-color: #fff3cd;
6738
+ border: 1px solid #ffeaa7;
6739
+ border-radius: 5px;
6740
+ padding: 15px;
6741
+ margin: 15px 0;
6742
+ }
6743
+ </style>
6744
+ </head>
6745
+ <body>
6746
+ <div class="container">
6747
+ <div class="header">
6748
+ <div class="logo">MetaEstetics</div>
6749
+ <p>Clinic Management System</p>
6750
+ </div>
6751
+
6752
+ <h1>Invitation Update</h1>
6753
+
6754
+ <div class="notice-badge">
6755
+ <strong>\u274C Dr. {{practitionerName}} has declined your invitation</strong>
6756
+ <p style="margin: 5px 0 0 0;">They will not be joining {{clinicName}} at this time.</p>
6757
+ </div>
6758
+
6759
+ <p>Dear {{clinicAdminName}},</p>
6760
+
6761
+ <p>We wanted to let you know that <strong>Dr. {{practitionerName}}</strong> has declined your invitation to join <strong>{{clinicName}}</strong>.</p>
6762
+
6763
+ <div class="practitioner-card">
6764
+ {{#practitionerPhoto}}
6765
+ <img src="{{practitionerPhoto}}" alt="{{practitionerName}}" class="practitioner-photo">
6766
+ {{/practitionerPhoto}}
6767
+ <div class="practitioner-info">
6768
+ <div class="practitioner-name">Dr. {{practitionerName}}</div>
6769
+ <div class="practitioner-details">
6770
+ {{#practitionerSpecialties}}
6771
+ <p><strong>Specialties:</strong> {{practitionerSpecialties}}</p>
6772
+ {{/practitionerSpecialties}}
6773
+ <p><strong>Decision Date:</strong> {{rejectedDate}}</p>
6774
+ </div>
6775
+ </div>
6776
+ </div>
6777
+
6778
+ <div class="details-grid">
6779
+ <div class="detail-item">
6780
+ <div class="detail-label">Status</div>
6781
+ <div class="detail-value">\u274C Invitation Declined</div>
6782
+ </div>
6783
+ <div class="detail-item">
6784
+ <div class="detail-label">Original Invitation</div>
6785
+ <div class="detail-value">{{invitationDate}}</div>
6786
+ </div>
6787
+ </div>
6788
+
6789
+ {{#rejectionReason}}
6790
+ <div class="reason-box">
6791
+ <h4>\u{1F4DD} Reason Provided:</h4>
6792
+ <p>"{{rejectionReason}}"</p>
6793
+ </div>
6794
+ {{/rejectionReason}}
6795
+
6796
+ <div class="suggestions">
6797
+ <h4>\u{1F4A1} What you can do next:</h4>
6798
+ <ul>
6799
+ <li><strong>Reach out directly:</strong> Consider contacting Dr. {{practitionerName}} to discuss their concerns</li>
6800
+ <li><strong>Adjust your offer:</strong> Review your working hours, compensation, or clinic benefits</li>
6801
+ <li><strong>Invite other practitioners:</strong> Look for other qualified professionals in your area</li>
6802
+ <li><strong>Try again later:</strong> Their circumstances may change in the future</li>
6803
+ </ul>
6804
+ </div>
6805
+
6806
+ <div class="action-section">
6807
+ <p><strong>Explore your options:</strong></p>
6808
+ <a href="{{findPractitionersUrl}}" class="button">Find More Practitioners</a>
6809
+ <a href="{{clinicDashboardUrl}}" class="button secondary">View Dashboard</a>
6810
+ </div>
6811
+
6812
+ <p>Don't let this discourage you! Finding the right team members takes time, and there are many talented practitioners who would be excited to join your clinic.</p>
6813
+
6814
+ <div class="footer">
6815
+ <p>Best regards,<br>
6816
+ <strong>The MetaEstetics Team</strong></p>
6817
+ <hr>
6818
+ <p style="font-size: 12px; color: #999;">
6819
+ This is an automated notification from MetaEstetics.
6820
+ <br>\xA9 {{currentYear}} MetaEstetics. All rights reserved.
6821
+ </p>
6822
+ <p style="font-size: 12px; color: #999;">
6823
+ <strong>Note:</strong> Practitioner contact information is not shared without their consent.
6824
+ </p>
6825
+ </div>
6826
+ </div>
6827
+ </body>
6828
+ </html>
6829
+ `;
6830
+
6831
+ // src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts
6832
+ var ExistingPractitionerInviteMailingService = class extends BaseMailingService {
6833
+ /**
6834
+ * Constructor for ExistingPractitionerInviteMailingService
6835
+ * @param firestore Firestore instance provided by the caller
6836
+ * @param mailgunClient Mailgun client instance (mailgun.js v10+) provided by the caller
6837
+ */
6838
+ constructor(firestore17, mailgunClient) {
6839
+ super(firestore17, mailgunClient);
6840
+ this.DEFAULT_MAILGUN_DOMAIN = "mg.metaesthetics.net";
6841
+ this.DEFAULT_FROM_ADDRESS = "MetaEstetics <no-reply@mg.metaesthetics.net>";
6842
+ }
6843
+ /**
6844
+ * Sends an invitation email to an existing practitioner
6845
+ * @param data The invitation email data
6846
+ * @returns Promise resolved when email is sent
6847
+ */
6848
+ async sendPractitionerInvitationEmail(data) {
6849
+ var _a, _b, _c, _d;
6850
+ try {
6851
+ Logger.info(
6852
+ "[ExistingPractitionerInviteMailingService] Sending invitation email to practitioner",
6853
+ data.practitioner.email
6854
+ );
6855
+ const workingHours = this.formatWorkingHours(
6856
+ data.invite.proposedWorkingHours
6857
+ );
6858
+ const expiryDate = new Date(data.invite.createdAt.toDate());
6859
+ expiryDate.setDate(expiryDate.getDate() + 30);
6860
+ const expirationDate = expiryDate.toLocaleDateString("en-US", {
6861
+ weekday: "long",
6862
+ year: "numeric",
6863
+ month: "long",
6864
+ day: "numeric"
6865
+ });
6866
+ const practitionerName = `${data.practitioner.firstName} ${data.practitioner.lastName}`;
6867
+ const templateVariables = {
6868
+ clinicName: data.clinic.name,
6869
+ practitionerName,
6870
+ clinicAddress: data.clinic.address,
6871
+ workingHours,
6872
+ expirationDate,
6873
+ acceptUrl: data.urls.acceptUrl,
6874
+ rejectUrl: data.urls.rejectUrl,
6875
+ contactEmail: data.clinic.contactEmail,
6876
+ contactPhone: data.clinic.contactPhone || "Contact clinic directly",
6877
+ currentYear: (/* @__PURE__ */ new Date()).getFullYear().toString()
6878
+ };
6879
+ const html = this.renderTemplate(
6880
+ existingPractitionerInvitationTemplate,
6881
+ templateVariables
6882
+ );
6883
+ const subject = ((_a = data.options) == null ? void 0 : _a.customSubject) || `Invitation to Join ${data.clinic.name}`;
6884
+ const from = ((_b = data.options) == null ? void 0 : _b.fromAddress) || this.DEFAULT_FROM_ADDRESS;
6885
+ const domain = ((_c = data.options) == null ? void 0 : _c.mailgunDomain) || this.DEFAULT_MAILGUN_DOMAIN;
6886
+ const mailgunData = {
6887
+ to: data.practitioner.email,
6888
+ from,
6889
+ subject,
6890
+ html
6891
+ };
6892
+ Logger.info(
6893
+ "[ExistingPractitionerInviteMailingService] Sending email with data:",
6894
+ {
6895
+ domain,
6896
+ to: mailgunData.to,
6897
+ from: mailgunData.from,
6898
+ subject: mailgunData.subject,
6899
+ hasHtml: !!mailgunData.html
6900
+ }
6901
+ );
6902
+ const result = await this.sendEmail(domain, mailgunData);
6903
+ await this.logEmailAttempt(
6904
+ {
6905
+ to: data.practitioner.email,
6906
+ subject,
6907
+ templateName: "existing_practitioner_invitation"
6908
+ },
6909
+ true
6910
+ );
6911
+ return result;
6912
+ } catch (error) {
6913
+ Logger.error(
6914
+ "[ExistingPractitionerInviteMailingService] Error sending practitioner invitation:",
6915
+ {
6916
+ errorMessage: error.message,
6917
+ errorDetails: error.details,
6918
+ errorStatus: error.status,
6919
+ stack: error.stack
6920
+ }
6921
+ );
6922
+ await this.logEmailAttempt(
6923
+ {
6924
+ to: data.practitioner.email,
6925
+ subject: ((_d = data.options) == null ? void 0 : _d.customSubject) || `Invitation to Join ${data.clinic.name}`,
6926
+ templateName: "existing_practitioner_invitation"
6927
+ },
6928
+ false,
6929
+ error
6930
+ );
6931
+ throw error;
6932
+ }
6933
+ }
6934
+ /**
6935
+ * Sends a notification email to clinic admin when practitioner accepts invitation
6936
+ * @param data The notification email data
6937
+ * @returns Promise resolved when email is sent
6938
+ */
6939
+ async sendAcceptedNotificationEmail(data) {
6940
+ var _a, _b, _c, _d, _e, _f;
6941
+ try {
6942
+ Logger.info(
6943
+ "[ExistingPractitionerInviteMailingService] Sending acceptance notification to clinic admin",
6944
+ data.clinic.adminEmail
6945
+ );
6946
+ const practitionerName = `${data.practitioner.firstName} ${data.practitioner.lastName}`;
6947
+ const workingHours = this.formatWorkingHours(
6948
+ data.invite.proposedWorkingHours
6949
+ );
6950
+ const templateVariables = {
6951
+ clinicName: data.clinic.name,
6952
+ clinicAdminName: data.clinic.adminName || "Admin",
6953
+ practitionerName,
6954
+ practitionerPhoto: data.practitioner.profileImageUrl || "",
6955
+ practitionerSpecialties: ((_a = data.practitioner.specialties) == null ? void 0 : _a.join(", ")) || "",
6956
+ practitionerExperience: ((_b = data.practitioner.experienceYears) == null ? void 0 : _b.toString()) || "",
6957
+ invitationDate: data.context.invitationDate,
6958
+ acceptedDate: data.context.responseDate,
6959
+ workingHours,
6960
+ clinicDashboardUrl: data.urls.clinicDashboardUrl,
6961
+ practitionerProfileUrl: data.urls.practitionerProfileUrl || "#",
6962
+ currentYear: (/* @__PURE__ */ new Date()).getFullYear().toString()
6963
+ };
6964
+ const html = this.renderTemplate(
6965
+ inviteAcceptedNotificationTemplate,
6966
+ templateVariables
6967
+ );
6968
+ const subject = ((_c = data.options) == null ? void 0 : _c.customSubject) || `Great News! Dr. ${practitionerName} Accepted Your Invitation`;
6969
+ const from = ((_d = data.options) == null ? void 0 : _d.fromAddress) || this.DEFAULT_FROM_ADDRESS;
6970
+ const domain = ((_e = data.options) == null ? void 0 : _e.mailgunDomain) || this.DEFAULT_MAILGUN_DOMAIN;
6971
+ const mailgunData = {
6972
+ to: data.clinic.adminEmail,
6973
+ from,
6974
+ subject,
6975
+ html
6976
+ };
6977
+ const result = await this.sendEmail(domain, mailgunData);
6978
+ await this.logEmailAttempt(
6979
+ {
6980
+ to: data.clinic.adminEmail,
6981
+ subject,
6982
+ templateName: "invite_accepted_notification"
6983
+ },
6984
+ true
6985
+ );
6986
+ return result;
6987
+ } catch (error) {
6988
+ Logger.error(
6989
+ "[ExistingPractitionerInviteMailingService] Error sending acceptance notification:",
6990
+ error
6991
+ );
6992
+ await this.logEmailAttempt(
6993
+ {
6994
+ to: data.clinic.adminEmail,
6995
+ subject: ((_f = data.options) == null ? void 0 : _f.customSubject) || "Invitation Accepted",
6996
+ templateName: "invite_accepted_notification"
6997
+ },
6998
+ false,
6999
+ error
7000
+ );
7001
+ throw error;
7002
+ }
7003
+ }
7004
+ /**
7005
+ * Sends a notification email to clinic admin when practitioner rejects invitation
7006
+ * @param data The notification email data
7007
+ * @returns Promise resolved when email is sent
7008
+ */
7009
+ async sendRejectedNotificationEmail(data) {
7010
+ var _a, _b, _c, _d, _e;
7011
+ try {
7012
+ Logger.info(
7013
+ "[ExistingPractitionerInviteMailingService] Sending rejection notification to clinic admin",
7014
+ data.clinic.adminEmail
7015
+ );
7016
+ const practitionerName = `${data.practitioner.firstName} ${data.practitioner.lastName}`;
7017
+ const templateVariables = {
7018
+ clinicName: data.clinic.name,
7019
+ clinicAdminName: data.clinic.adminName || "Admin",
7020
+ practitionerName,
7021
+ practitionerPhoto: data.practitioner.profileImageUrl || "",
7022
+ practitionerSpecialties: ((_a = data.practitioner.specialties) == null ? void 0 : _a.join(", ")) || "",
7023
+ invitationDate: data.context.invitationDate,
7024
+ rejectedDate: data.context.responseDate,
7025
+ rejectionReason: data.context.rejectionReason || "",
7026
+ findPractitionersUrl: data.urls.findPractitionersUrl || "#",
7027
+ clinicDashboardUrl: data.urls.clinicDashboardUrl,
7028
+ currentYear: (/* @__PURE__ */ new Date()).getFullYear().toString()
7029
+ };
7030
+ const html = this.renderTemplate(
7031
+ inviteRejectedNotificationTemplate,
7032
+ templateVariables
7033
+ );
7034
+ const subject = ((_b = data.options) == null ? void 0 : _b.customSubject) || `Invitation Update: Dr. ${practitionerName} Declined`;
7035
+ const from = ((_c = data.options) == null ? void 0 : _c.fromAddress) || this.DEFAULT_FROM_ADDRESS;
7036
+ const domain = ((_d = data.options) == null ? void 0 : _d.mailgunDomain) || this.DEFAULT_MAILGUN_DOMAIN;
7037
+ const mailgunData = {
7038
+ to: data.clinic.adminEmail,
7039
+ from,
7040
+ subject,
7041
+ html
7042
+ };
7043
+ const result = await this.sendEmail(domain, mailgunData);
7044
+ await this.logEmailAttempt(
7045
+ {
7046
+ to: data.clinic.adminEmail,
7047
+ subject,
7048
+ templateName: "invite_rejected_notification"
7049
+ },
7050
+ true
7051
+ );
7052
+ return result;
7053
+ } catch (error) {
7054
+ Logger.error(
7055
+ "[ExistingPractitionerInviteMailingService] Error sending rejection notification:",
7056
+ error
7057
+ );
7058
+ await this.logEmailAttempt(
7059
+ {
7060
+ to: data.clinic.adminEmail,
7061
+ subject: ((_e = data.options) == null ? void 0 : _e.customSubject) || "Invitation Declined",
7062
+ templateName: "invite_rejected_notification"
7063
+ },
7064
+ false,
7065
+ error
7066
+ );
7067
+ throw error;
7068
+ }
7069
+ }
7070
+ /**
7071
+ * Handles the practitioner invite creation event
7072
+ * Fetches necessary data and sends the invitation email to the practitioner
7073
+ * @param invite The practitioner invite object
7074
+ * @param mailgunConfig Mailgun configuration
7075
+ * @returns Promise resolved when the email is sent
7076
+ */
7077
+ async handleInviteCreationEvent(invite, mailgunConfig) {
7078
+ var _a, _b;
7079
+ try {
7080
+ Logger.info(
7081
+ "[ExistingPractitionerInviteMailingService] Handling invite creation event for invite:",
7082
+ invite.id
7083
+ );
7084
+ if (!invite || !invite.id || !invite.practitionerId || !invite.clinicId) {
7085
+ throw new Error(
7086
+ `Invalid invite data: Missing required properties. Invite ID: ${invite == null ? void 0 : invite.id}`
7087
+ );
7088
+ }
7089
+ if (invite.status !== "pending" /* PENDING */) {
7090
+ Logger.info(
7091
+ "[ExistingPractitionerInviteMailingService] Invite is not pending, skipping email",
7092
+ { inviteId: invite.id, status: invite.status }
7093
+ );
7094
+ return;
7095
+ }
7096
+ const [practitioner, clinic] = await Promise.all([
7097
+ this.fetchPractitionerById(invite.practitionerId),
7098
+ this.fetchClinicById(invite.clinicId)
7099
+ ]);
7100
+ if (!practitioner) {
7101
+ throw new Error(`Practitioner ${invite.practitionerId} not found`);
7102
+ }
7103
+ if (!clinic) {
7104
+ throw new Error(`Clinic ${invite.clinicId} not found`);
7105
+ }
7106
+ const emailData = {
7107
+ invite,
7108
+ practitioner: {
7109
+ firstName: practitioner.basicInfo.firstName || "",
7110
+ lastName: practitioner.basicInfo.lastName || "",
7111
+ email: practitioner.basicInfo.email || "",
7112
+ specialties: ((_b = (_a = practitioner.certification) == null ? void 0 : _a.specialties) == null ? void 0 : _b.map(
7113
+ (s) => s.name || s
7114
+ )) || [],
7115
+ profileImageUrl: typeof practitioner.basicInfo.profileImageUrl === "string" ? practitioner.basicInfo.profileImageUrl : null
7116
+ },
7117
+ clinic: {
7118
+ name: clinic.name || "Medical Clinic",
7119
+ address: this.formatClinicAddress(clinic.location),
7120
+ contactEmail: clinic.contactInfo.email || "contact@clinic.com",
7121
+ contactPhone: clinic.contactInfo.phoneNumber
7122
+ },
7123
+ urls: {
7124
+ acceptUrl: mailgunConfig.acceptUrl,
7125
+ rejectUrl: mailgunConfig.rejectUrl
7126
+ },
7127
+ options: {
7128
+ fromAddress: mailgunConfig.fromAddress,
7129
+ mailgunDomain: mailgunConfig.domain
7130
+ }
7131
+ };
7132
+ await this.sendPractitionerInvitationEmail(emailData);
7133
+ Logger.info(
7134
+ "[ExistingPractitionerInviteMailingService] Invitation email sent successfully"
7135
+ );
7136
+ } catch (error) {
7137
+ Logger.error(
7138
+ "[ExistingPractitionerInviteMailingService] Error handling invite creation event:",
7139
+ {
7140
+ errorMessage: error.message,
7141
+ errorDetails: error.details,
7142
+ errorStatus: error.status,
7143
+ stack: error.stack,
7144
+ inviteId: invite == null ? void 0 : invite.id
7145
+ }
7146
+ );
7147
+ throw error;
7148
+ }
7149
+ }
7150
+ // --- Private Helper Methods ---
7151
+ /**
7152
+ * Formats working hours for display in emails
7153
+ * @param workingHours The working hours object
7154
+ * @returns Formatted string representation
7155
+ */
7156
+ formatWorkingHours(workingHours) {
7157
+ if (!workingHours) return "To be determined";
7158
+ return Object.entries(workingHours).map(([day, hours]) => `${day}: ${hours}`).join(", ");
7159
+ }
7160
+ /**
7161
+ * Formats clinic address for display
7162
+ * @param location The clinic location object
7163
+ * @returns Formatted address string
7164
+ */
7165
+ formatClinicAddress(location) {
7166
+ if (!location) return "Address not specified";
7167
+ const parts = [
7168
+ location.street,
7169
+ location.city,
7170
+ location.state,
7171
+ location.country
7172
+ ].filter(Boolean);
7173
+ return parts.join(", ");
7174
+ }
7175
+ /**
7176
+ * Fetches a practitioner by ID
7177
+ * @param practitionerId The practitioner ID
7178
+ * @returns The practitioner or null if not found
7179
+ */
7180
+ async fetchPractitionerById(practitionerId) {
7181
+ try {
7182
+ const doc = await this.db.collection(PRACTITIONERS_COLLECTION).doc(practitionerId).get();
7183
+ return doc.exists ? doc.data() : null;
7184
+ } catch (error) {
7185
+ Logger.error(
7186
+ "[ExistingPractitionerInviteMailingService] Error fetching practitioner:",
7187
+ error
7188
+ );
7189
+ return null;
7190
+ }
7191
+ }
7192
+ /**
7193
+ * Fetches a clinic by ID
7194
+ * @param clinicId The clinic ID
7195
+ * @returns The clinic or null if not found
7196
+ */
7197
+ async fetchClinicById(clinicId) {
7198
+ try {
7199
+ const doc = await this.db.collection(CLINICS_COLLECTION).doc(clinicId).get();
7200
+ return doc.exists ? doc.data() : null;
7201
+ } catch (error) {
7202
+ Logger.error(
7203
+ "[ExistingPractitionerInviteMailingService] Error fetching clinic:",
7204
+ error
7205
+ );
7206
+ return null;
7207
+ }
7208
+ }
7209
+ };
7210
+
5939
7211
  // src/admin/booking/booking.admin.ts
5940
7212
  import * as admin15 from "firebase-admin";
5941
7213
 
@@ -7270,6 +8542,7 @@ export {
7270
8542
  CalendarAdminService,
7271
8543
  ClinicAggregationService,
7272
8544
  DocumentManagerAdminService,
8545
+ ExistingPractitionerInviteMailingService,
7273
8546
  FilledFormsAggregationService,
7274
8547
  Logger,
7275
8548
  MediaType,