@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.
@@ -40,6 +40,7 @@ __export(index_exports, {
40
40
  CalendarAdminService: () => CalendarAdminService,
41
41
  ClinicAggregationService: () => ClinicAggregationService,
42
42
  DocumentManagerAdminService: () => DocumentManagerAdminService,
43
+ ExistingPractitionerInviteMailingService: () => ExistingPractitionerInviteMailingService,
43
44
  FilledFormsAggregationService: () => FilledFormsAggregationService,
44
45
  Logger: () => Logger,
45
46
  MediaType: () => MediaType,
@@ -925,6 +926,7 @@ var PractitionerInviteStatus = /* @__PURE__ */ ((PractitionerInviteStatus2) => {
925
926
 
926
927
  // src/types/clinic/index.ts
927
928
  var CLINIC_GROUPS_COLLECTION = "clinic_groups";
929
+ var CLINIC_ADMINS_COLLECTION = "clinic_admins";
928
930
  var CLINICS_COLLECTION = "clinics";
929
931
 
930
932
  // src/types/patient/index.ts
@@ -1789,22 +1791,44 @@ var PractitionerInviteAggregationService = class {
1789
1791
  /**
1790
1792
  * Constructor for PractitionerInviteAggregationService.
1791
1793
  * @param firestore Optional Firestore instance. If not provided, it uses the default admin SDK instance.
1794
+ * @param mailingService Optional mailing service for sending emails
1792
1795
  */
1793
- constructor(firestore17) {
1796
+ constructor(firestore17, mailingService) {
1794
1797
  this.db = firestore17 || admin5.firestore();
1798
+ this.mailingService = mailingService;
1795
1799
  Logger.info("[PractitionerInviteAggregationService] Initialized.");
1796
1800
  }
1797
1801
  /**
1798
1802
  * Handles side effects when a practitioner invite is first created.
1799
1803
  * This function would typically be called by a Firestore onCreate trigger.
1800
1804
  * @param {PractitionerInvite} invite - The newly created PractitionerInvite object.
1805
+ * @param {object} emailConfig - Optional email configuration for sending invite emails
1801
1806
  * @returns {Promise<void>}
1802
1807
  */
1803
- async handleInviteCreate(invite) {
1808
+ async handleInviteCreate(invite, emailConfig) {
1804
1809
  Logger.info(
1805
1810
  `[PractitionerInviteAggService] Handling CREATE for invite: ${invite.id}, practitioner: ${invite.practitionerId}, clinic: ${invite.clinicId}, status: ${invite.status}`
1806
1811
  );
1807
1812
  try {
1813
+ if (this.mailingService && emailConfig && invite.status === "pending" /* PENDING */) {
1814
+ Logger.info(
1815
+ `[PractitionerInviteAggService] Sending invitation email for invite: ${invite.id}`
1816
+ );
1817
+ try {
1818
+ await this.mailingService.handleInviteCreationEvent(
1819
+ invite,
1820
+ emailConfig
1821
+ );
1822
+ Logger.info(
1823
+ `[PractitionerInviteAggService] Successfully sent invitation email for invite: ${invite.id}`
1824
+ );
1825
+ } catch (emailError) {
1826
+ Logger.error(
1827
+ `[PractitionerInviteAggService] Error sending invitation email for invite ${invite.id}:`,
1828
+ emailError
1829
+ );
1830
+ }
1831
+ }
1808
1832
  Logger.info(
1809
1833
  `[PractitionerInviteAggService] Successfully processed CREATE for invite: ${invite.id}`
1810
1834
  );
@@ -1821,9 +1845,10 @@ var PractitionerInviteAggregationService = class {
1821
1845
  * This function would typically be called by a Firestore onUpdate trigger.
1822
1846
  * @param {PractitionerInvite} before - The PractitionerInvite object before the update.
1823
1847
  * @param {PractitionerInvite} after - The PractitionerInvite object after the update.
1848
+ * @param {object} emailConfig - Optional email configuration for sending notification emails
1824
1849
  * @returns {Promise<void>}
1825
1850
  */
1826
- async handleInviteUpdate(before, after) {
1851
+ async handleInviteUpdate(before, after, emailConfig) {
1827
1852
  Logger.info(
1828
1853
  `[PractitionerInviteAggService] Handling UPDATE for invite: ${after.id}. Status ${before.status} -> ${after.status}`
1829
1854
  );
@@ -1837,12 +1862,12 @@ var PractitionerInviteAggregationService = class {
1837
1862
  Logger.info(
1838
1863
  `[PractitionerInviteAggService] Invite ${after.id} PENDING -> ACCEPTED. Adding practitioner to clinic.`
1839
1864
  );
1840
- await this.handleInviteAccepted(after);
1865
+ await this.handleInviteAccepted(after, emailConfig);
1841
1866
  } else if (before.status === "pending" /* PENDING */ && after.status === "rejected" /* REJECTED */) {
1842
1867
  Logger.info(
1843
1868
  `[PractitionerInviteAggService] Invite ${after.id} PENDING -> REJECTED.`
1844
1869
  );
1845
- await this.handleInviteRejected(after);
1870
+ await this.handleInviteRejected(after, emailConfig);
1846
1871
  } else if (before.status === "pending" /* PENDING */ && after.status === "cancelled" /* CANCELLED */) {
1847
1872
  Logger.info(
1848
1873
  `[PractitionerInviteAggService] Invite ${after.id} PENDING -> CANCELLED.`
@@ -1887,9 +1912,10 @@ var PractitionerInviteAggregationService = class {
1887
1912
  * Handles the business logic when a practitioner accepts an invite.
1888
1913
  * This includes adding the practitioner to the clinic and the clinic to the practitioner.
1889
1914
  * @param {PractitionerInvite} invite - The accepted invite
1915
+ * @param {object} emailConfig - Optional email configuration for sending notification emails
1890
1916
  * @returns {Promise<void>}
1891
1917
  */
1892
- async handleInviteAccepted(invite) {
1918
+ async handleInviteAccepted(invite, emailConfig) {
1893
1919
  var _a, _b, _c, _d;
1894
1920
  Logger.info(
1895
1921
  `[PractitionerInviteAggService] Processing accepted invite ${invite.id} for practitioner ${invite.practitionerId} and clinic ${invite.clinicId}`
@@ -1950,6 +1976,27 @@ var PractitionerInviteAggregationService = class {
1950
1976
  );
1951
1977
  await this.updatePractitionerWorkingHours(practitioner.id, invite);
1952
1978
  }
1979
+ if (this.mailingService && emailConfig) {
1980
+ Logger.info(
1981
+ `[PractitionerInviteAggService] Sending acceptance notification email for invite: ${invite.id}`
1982
+ );
1983
+ try {
1984
+ await this.sendAcceptanceNotificationEmail(
1985
+ invite,
1986
+ practitioner,
1987
+ clinic,
1988
+ emailConfig
1989
+ );
1990
+ Logger.info(
1991
+ `[PractitionerInviteAggService] Successfully sent acceptance notification email for invite: ${invite.id}`
1992
+ );
1993
+ } catch (emailError) {
1994
+ Logger.error(
1995
+ `[PractitionerInviteAggService] Error sending acceptance notification email for invite ${invite.id}:`,
1996
+ emailError
1997
+ );
1998
+ }
1999
+ }
1953
2000
  Logger.info(
1954
2001
  `[PractitionerInviteAggService] Successfully processed accepted invite ${invite.id}`
1955
2002
  );
@@ -1964,13 +2011,41 @@ var PractitionerInviteAggregationService = class {
1964
2011
  /**
1965
2012
  * Handles the business logic when a practitioner rejects an invite.
1966
2013
  * @param {PractitionerInvite} invite - The rejected invite
2014
+ * @param {object} emailConfig - Optional email configuration for sending notification emails
1967
2015
  * @returns {Promise<void>}
1968
2016
  */
1969
- async handleInviteRejected(invite) {
2017
+ async handleInviteRejected(invite, emailConfig) {
1970
2018
  Logger.info(
1971
2019
  `[PractitionerInviteAggService] Processing rejected invite ${invite.id}`
1972
2020
  );
1973
2021
  try {
2022
+ if (this.mailingService && emailConfig) {
2023
+ Logger.info(
2024
+ `[PractitionerInviteAggService] Sending rejection notification email for invite: ${invite.id}`
2025
+ );
2026
+ try {
2027
+ const [practitioner, clinic] = await Promise.all([
2028
+ this.fetchPractitionerById(invite.practitionerId),
2029
+ this.fetchClinicById(invite.clinicId)
2030
+ ]);
2031
+ if (practitioner && clinic) {
2032
+ await this.sendRejectionNotificationEmail(
2033
+ invite,
2034
+ practitioner,
2035
+ clinic,
2036
+ emailConfig
2037
+ );
2038
+ Logger.info(
2039
+ `[PractitionerInviteAggService] Successfully sent rejection notification email for invite: ${invite.id}`
2040
+ );
2041
+ }
2042
+ } catch (emailError) {
2043
+ Logger.error(
2044
+ `[PractitionerInviteAggService] Error sending rejection notification email for invite ${invite.id}:`,
2045
+ emailError
2046
+ );
2047
+ }
2048
+ }
1974
2049
  Logger.info(
1975
2050
  `[PractitionerInviteAggService] Successfully processed rejected invite ${invite.id}`
1976
2051
  );
@@ -2162,6 +2237,23 @@ var PractitionerInviteAggregationService = class {
2162
2237
  }
2163
2238
  }
2164
2239
  // --- Data Fetching Helpers ---
2240
+ /**
2241
+ * Fetches a clinic admin by ID
2242
+ * @param adminId The clinic admin ID
2243
+ * @returns The clinic admin or null if not found
2244
+ */
2245
+ async fetchClinicAdminById(adminId) {
2246
+ try {
2247
+ const doc = await this.db.collection(CLINIC_ADMINS_COLLECTION).doc(adminId).get();
2248
+ return doc.exists ? doc.data() : null;
2249
+ } catch (error) {
2250
+ Logger.error(
2251
+ `[PractitionerInviteAggService] Error fetching clinic admin ${adminId}:`,
2252
+ error
2253
+ );
2254
+ return null;
2255
+ }
2256
+ }
2165
2257
  /**
2166
2258
  * Fetches a practitioner by ID.
2167
2259
  * @param practitionerId The practitioner ID.
@@ -2196,6 +2288,188 @@ var PractitionerInviteAggregationService = class {
2196
2288
  return null;
2197
2289
  }
2198
2290
  }
2291
+ // --- Email Helper Methods ---
2292
+ /**
2293
+ * Sends acceptance notification email to clinic admin
2294
+ * @param invite The accepted invite
2295
+ * @param practitioner The practitioner who accepted
2296
+ * @param clinic The clinic that sent the invite
2297
+ * @param emailConfig Email configuration
2298
+ */
2299
+ async sendAcceptanceNotificationEmail(invite, practitioner, clinic, emailConfig) {
2300
+ var _a, _b, _c, _d, _e, _f;
2301
+ if (!this.mailingService) return;
2302
+ try {
2303
+ const admin17 = await this.fetchClinicAdminById(invite.invitedBy);
2304
+ if (!admin17) {
2305
+ Logger.warn(
2306
+ `[PractitionerInviteAggService] Admin ${invite.invitedBy} not found, using clinic contact email as fallback`
2307
+ );
2308
+ const notificationData2 = {
2309
+ invite,
2310
+ practitioner: {
2311
+ firstName: practitioner.basicInfo.firstName || "",
2312
+ lastName: practitioner.basicInfo.lastName || "",
2313
+ specialties: ((_b = (_a = practitioner.certification) == null ? void 0 : _a.specialties) == null ? void 0 : _b.map(
2314
+ (s) => s.name || s
2315
+ )) || [],
2316
+ profileImageUrl: typeof practitioner.basicInfo.profileImageUrl === "string" ? practitioner.basicInfo.profileImageUrl : null,
2317
+ experienceYears: void 0
2318
+ },
2319
+ clinic: {
2320
+ name: clinic.name,
2321
+ adminName: "Admin",
2322
+ adminEmail: clinic.contactInfo.email
2323
+ },
2324
+ context: {
2325
+ invitationDate: invite.createdAt.toDate().toLocaleDateString(),
2326
+ responseDate: ((_c = invite.acceptedAt) == null ? void 0 : _c.toDate().toLocaleDateString()) || (/* @__PURE__ */ new Date()).toLocaleDateString()
2327
+ },
2328
+ urls: {
2329
+ clinicDashboardUrl: emailConfig.clinicDashboardUrl,
2330
+ practitionerProfileUrl: emailConfig.practitionerProfileUrl
2331
+ },
2332
+ options: {
2333
+ fromAddress: emailConfig.fromAddress,
2334
+ mailgunDomain: emailConfig.domain
2335
+ }
2336
+ };
2337
+ await this.mailingService.sendAcceptedNotificationEmail(
2338
+ notificationData2
2339
+ );
2340
+ return;
2341
+ }
2342
+ const adminName = `${admin17.contactInfo.firstName} ${admin17.contactInfo.lastName}`;
2343
+ const notificationData = {
2344
+ invite,
2345
+ practitioner: {
2346
+ firstName: practitioner.basicInfo.firstName || "",
2347
+ lastName: practitioner.basicInfo.lastName || "",
2348
+ specialties: ((_e = (_d = practitioner.certification) == null ? void 0 : _d.specialties) == null ? void 0 : _e.map(
2349
+ (s) => s.name || s
2350
+ )) || [],
2351
+ profileImageUrl: typeof practitioner.basicInfo.profileImageUrl === "string" ? practitioner.basicInfo.profileImageUrl : null,
2352
+ experienceYears: void 0
2353
+ // This would need to be calculated or stored in practitioner data
2354
+ },
2355
+ clinic: {
2356
+ name: clinic.name,
2357
+ adminName,
2358
+ adminEmail: admin17.contactInfo.email
2359
+ // Use the specific admin's email
2360
+ },
2361
+ context: {
2362
+ invitationDate: invite.createdAt.toDate().toLocaleDateString(),
2363
+ responseDate: ((_f = invite.acceptedAt) == null ? void 0 : _f.toDate().toLocaleDateString()) || (/* @__PURE__ */ new Date()).toLocaleDateString()
2364
+ },
2365
+ urls: {
2366
+ clinicDashboardUrl: emailConfig.clinicDashboardUrl,
2367
+ practitionerProfileUrl: emailConfig.practitionerProfileUrl
2368
+ },
2369
+ options: {
2370
+ fromAddress: emailConfig.fromAddress,
2371
+ mailgunDomain: emailConfig.domain
2372
+ }
2373
+ };
2374
+ await this.mailingService.sendAcceptedNotificationEmail(notificationData);
2375
+ } catch (error) {
2376
+ Logger.error(
2377
+ `[PractitionerInviteAggService] Error sending acceptance notification email:`,
2378
+ error
2379
+ );
2380
+ throw error;
2381
+ }
2382
+ }
2383
+ /**
2384
+ * Sends rejection notification email to clinic admin
2385
+ * @param invite The rejected invite
2386
+ * @param practitioner The practitioner who rejected
2387
+ * @param clinic The clinic that sent the invite
2388
+ * @param emailConfig Email configuration
2389
+ */
2390
+ async sendRejectionNotificationEmail(invite, practitioner, clinic, emailConfig) {
2391
+ var _a, _b, _c, _d, _e, _f;
2392
+ if (!this.mailingService) return;
2393
+ try {
2394
+ const admin17 = await this.fetchClinicAdminById(invite.invitedBy);
2395
+ if (!admin17) {
2396
+ Logger.warn(
2397
+ `[PractitionerInviteAggService] Admin ${invite.invitedBy} not found, using clinic contact email as fallback`
2398
+ );
2399
+ const notificationData2 = {
2400
+ invite,
2401
+ practitioner: {
2402
+ firstName: practitioner.basicInfo.firstName || "",
2403
+ lastName: practitioner.basicInfo.lastName || "",
2404
+ specialties: ((_b = (_a = practitioner.certification) == null ? void 0 : _a.specialties) == null ? void 0 : _b.map(
2405
+ (s) => s.name || s
2406
+ )) || [],
2407
+ profileImageUrl: typeof practitioner.basicInfo.profileImageUrl === "string" ? practitioner.basicInfo.profileImageUrl : null
2408
+ },
2409
+ clinic: {
2410
+ name: clinic.name,
2411
+ adminName: "Admin",
2412
+ adminEmail: clinic.contactInfo.email
2413
+ },
2414
+ context: {
2415
+ invitationDate: invite.createdAt.toDate().toLocaleDateString(),
2416
+ responseDate: ((_c = invite.rejectedAt) == null ? void 0 : _c.toDate().toLocaleDateString()) || (/* @__PURE__ */ new Date()).toLocaleDateString(),
2417
+ rejectionReason: invite.rejectionReason || void 0
2418
+ },
2419
+ urls: {
2420
+ clinicDashboardUrl: emailConfig.clinicDashboardUrl,
2421
+ findPractitionersUrl: emailConfig.findPractitionersUrl
2422
+ },
2423
+ options: {
2424
+ fromAddress: emailConfig.fromAddress,
2425
+ mailgunDomain: emailConfig.domain
2426
+ }
2427
+ };
2428
+ await this.mailingService.sendRejectedNotificationEmail(
2429
+ notificationData2
2430
+ );
2431
+ return;
2432
+ }
2433
+ const adminName = `${admin17.contactInfo.firstName} ${admin17.contactInfo.lastName}`;
2434
+ const notificationData = {
2435
+ invite,
2436
+ practitioner: {
2437
+ firstName: practitioner.basicInfo.firstName || "",
2438
+ lastName: practitioner.basicInfo.lastName || "",
2439
+ specialties: ((_e = (_d = practitioner.certification) == null ? void 0 : _d.specialties) == null ? void 0 : _e.map(
2440
+ (s) => s.name || s
2441
+ )) || [],
2442
+ profileImageUrl: typeof practitioner.basicInfo.profileImageUrl === "string" ? practitioner.basicInfo.profileImageUrl : null
2443
+ },
2444
+ clinic: {
2445
+ name: clinic.name,
2446
+ adminName,
2447
+ adminEmail: admin17.contactInfo.email
2448
+ // Use the specific admin's email
2449
+ },
2450
+ context: {
2451
+ invitationDate: invite.createdAt.toDate().toLocaleDateString(),
2452
+ responseDate: ((_f = invite.rejectedAt) == null ? void 0 : _f.toDate().toLocaleDateString()) || (/* @__PURE__ */ new Date()).toLocaleDateString(),
2453
+ rejectionReason: invite.rejectionReason || void 0
2454
+ },
2455
+ urls: {
2456
+ clinicDashboardUrl: emailConfig.clinicDashboardUrl,
2457
+ findPractitionersUrl: emailConfig.findPractitionersUrl
2458
+ },
2459
+ options: {
2460
+ fromAddress: emailConfig.fromAddress,
2461
+ mailgunDomain: emailConfig.domain
2462
+ }
2463
+ };
2464
+ await this.mailingService.sendRejectedNotificationEmail(notificationData);
2465
+ } catch (error) {
2466
+ Logger.error(
2467
+ `[PractitionerInviteAggService] Error sending rejection notification email:`,
2468
+ error
2469
+ );
2470
+ throw error;
2471
+ }
2472
+ }
2199
2473
  };
2200
2474
 
2201
2475
  // src/admin/aggregation/procedure/procedure.aggregation.service.ts
@@ -5996,6 +6270,1005 @@ var PractitionerInviteMailingService = class extends BaseMailingService {
5996
6270
  }
5997
6271
  };
5998
6272
 
6273
+ // src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts
6274
+ var existingPractitionerInvitationTemplate = `
6275
+ <!DOCTYPE html>
6276
+ <html lang="en">
6277
+ <head>
6278
+ <meta charset="UTF-8">
6279
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6280
+ <title>Invitation to Join {{clinicName}}</title>
6281
+ <style>
6282
+ body {
6283
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
6284
+ line-height: 1.6;
6285
+ color: #333;
6286
+ max-width: 600px;
6287
+ margin: 0 auto;
6288
+ padding: 20px;
6289
+ background-color: #f4f4f4;
6290
+ }
6291
+ .container {
6292
+ background-color: #ffffff;
6293
+ padding: 30px;
6294
+ border-radius: 10px;
6295
+ box-shadow: 0 0 20px rgba(0,0,0,0.1);
6296
+ }
6297
+ .header {
6298
+ text-align: center;
6299
+ margin-bottom: 30px;
6300
+ border-bottom: 2px solid #007bff;
6301
+ padding-bottom: 20px;
6302
+ }
6303
+ .logo {
6304
+ font-size: 28px;
6305
+ font-weight: bold;
6306
+ color: #007bff;
6307
+ margin-bottom: 10px;
6308
+ }
6309
+ h1 {
6310
+ color: #007bff;
6311
+ text-align: center;
6312
+ margin-bottom: 20px;
6313
+ }
6314
+ .highlight {
6315
+ background-color: #e7f3ff;
6316
+ padding: 15px;
6317
+ border-left: 4px solid #007bff;
6318
+ margin: 20px 0;
6319
+ }
6320
+ .button {
6321
+ display: inline-block;
6322
+ background-color: #28a745;
6323
+ color: white;
6324
+ padding: 12px 25px;
6325
+ text-decoration: none;
6326
+ border-radius: 5px;
6327
+ margin: 10px 5px;
6328
+ font-weight: bold;
6329
+ text-align: center;
6330
+ }
6331
+ .button.reject {
6332
+ background-color: #dc3545;
6333
+ }
6334
+ .button:hover {
6335
+ opacity: 0.9;
6336
+ }
6337
+ .action-section {
6338
+ text-align: center;
6339
+ margin: 30px 0;
6340
+ padding: 20px;
6341
+ background-color: #f8f9fa;
6342
+ border-radius: 5px;
6343
+ }
6344
+ .details {
6345
+ background-color: #f8f9fa;
6346
+ padding: 15px;
6347
+ border-radius: 5px;
6348
+ margin: 15px 0;
6349
+ }
6350
+ .footer {
6351
+ margin-top: 30px;
6352
+ padding-top: 20px;
6353
+ border-top: 1px solid #eee;
6354
+ font-size: 14px;
6355
+ color: #666;
6356
+ text-align: center;
6357
+ }
6358
+ .contact-info {
6359
+ margin-top: 15px;
6360
+ padding: 10px;
6361
+ background-color: #e9ecef;
6362
+ border-radius: 5px;
6363
+ }
6364
+ </style>
6365
+ </head>
6366
+ <body>
6367
+ <div class="container">
6368
+ <div class="header">
6369
+ <div class="logo">MetaEstetics</div>
6370
+ <p>Professional Medical Network</p>
6371
+ </div>
6372
+
6373
+ <h1>You're Invited to Join {{clinicName}}!</h1>
6374
+
6375
+ <p>Dear Dr. {{practitionerName}},</p>
6376
+
6377
+ <p>We hope this message finds you well. You have been invited to join <strong>{{clinicName}}</strong> as a practicing medical professional.</p>
6378
+
6379
+ <div class="highlight">
6380
+ <strong>Invitation Details:</strong>
6381
+ <div class="details">
6382
+ <p><strong>Clinic:</strong> {{clinicName}}</p>
6383
+ <p><strong>Location:</strong> {{clinicAddress}}</p>
6384
+ <p><strong>Proposed Working Hours:</strong> {{workingHours}}</p>
6385
+ <p><strong>Invitation Expires:</strong> {{expirationDate}}</p>
6386
+ </div>
6387
+ </div>
6388
+
6389
+ <p>By accepting this invitation, you will:</p>
6390
+ <ul>
6391
+ <li>Join the {{clinicName}} team as a featured practitioner</li>
6392
+ <li>Have your profile displayed on their clinic page</li>
6393
+ <li>Be available for appointments at the specified working hours</li>
6394
+ <li>Access their patient booking system and clinic resources</li>
6395
+ </ul>
6396
+
6397
+ <div class="action-section">
6398
+ <p><strong>Please respond to this invitation:</strong></p>
6399
+ <a href="{{acceptUrl}}" class="button">Accept Invitation</a>
6400
+ <a href="{{rejectUrl}}" class="button reject">Decline Invitation</a>
6401
+ </div>
6402
+
6403
+ <div class="contact-info">
6404
+ <p><strong>Questions?</strong> Contact the clinic administrator:</p>
6405
+ <p>\u{1F4E7} {{contactEmail}}<br>
6406
+ \u{1F4DE} {{contactPhone}}</p>
6407
+ </div>
6408
+
6409
+ <p>We look forward to welcoming you to the {{clinicName}} team!</p>
6410
+
6411
+ <div class="footer">
6412
+ <p>Best regards,<br>
6413
+ <strong>The MetaEstetics Team</strong></p>
6414
+ <p>This invitation will expire on {{expirationDate}}. Please respond before this date.</p>
6415
+ <hr>
6416
+ <p style="font-size: 12px; color: #999;">
6417
+ This is an automated message from MetaEstetics. If you received this email in error, please ignore it.
6418
+ <br>\xA9 {{currentYear}} MetaEstetics. All rights reserved.
6419
+ </p>
6420
+ </div>
6421
+ </div>
6422
+ </body>
6423
+ </html>
6424
+ `;
6425
+
6426
+ // src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts
6427
+ var inviteAcceptedNotificationTemplate = `
6428
+ <!DOCTYPE html>
6429
+ <html lang="en">
6430
+ <head>
6431
+ <meta charset="UTF-8">
6432
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6433
+ <title>Practitioner Invitation Accepted - {{practitionerName}}</title>
6434
+ <style>
6435
+ body {
6436
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
6437
+ line-height: 1.6;
6438
+ color: #333;
6439
+ max-width: 600px;
6440
+ margin: 0 auto;
6441
+ padding: 20px;
6442
+ background-color: #f4f4f4;
6443
+ }
6444
+ .container {
6445
+ background-color: #ffffff;
6446
+ padding: 30px;
6447
+ border-radius: 10px;
6448
+ box-shadow: 0 0 20px rgba(0,0,0,0.1);
6449
+ }
6450
+ .header {
6451
+ text-align: center;
6452
+ margin-bottom: 30px;
6453
+ border-bottom: 2px solid #28a745;
6454
+ padding-bottom: 20px;
6455
+ }
6456
+ .logo {
6457
+ font-size: 28px;
6458
+ font-weight: bold;
6459
+ color: #28a745;
6460
+ margin-bottom: 10px;
6461
+ }
6462
+ h1 {
6463
+ color: #28a745;
6464
+ text-align: center;
6465
+ margin-bottom: 20px;
6466
+ }
6467
+ .success-badge {
6468
+ background-color: #d4edda;
6469
+ color: #155724;
6470
+ padding: 15px;
6471
+ border-left: 4px solid #28a745;
6472
+ margin: 20px 0;
6473
+ border-radius: 0 5px 5px 0;
6474
+ }
6475
+ .practitioner-card {
6476
+ background-color: #f8f9fa;
6477
+ padding: 20px;
6478
+ border-radius: 8px;
6479
+ margin: 20px 0;
6480
+ border: 1px solid #dee2e6;
6481
+ }
6482
+ .practitioner-photo {
6483
+ width: 80px;
6484
+ height: 80px;
6485
+ border-radius: 50%;
6486
+ object-fit: cover;
6487
+ float: left;
6488
+ margin-right: 15px;
6489
+ border: 3px solid #28a745;
6490
+ }
6491
+ .practitioner-info {
6492
+ overflow: hidden;
6493
+ }
6494
+ .practitioner-name {
6495
+ font-size: 18px;
6496
+ font-weight: bold;
6497
+ color: #333;
6498
+ margin-bottom: 5px;
6499
+ }
6500
+ .practitioner-details {
6501
+ color: #666;
6502
+ font-size: 14px;
6503
+ line-height: 1.4;
6504
+ }
6505
+ .details-grid {
6506
+ display: grid;
6507
+ grid-template-columns: 1fr 1fr;
6508
+ gap: 15px;
6509
+ margin: 20px 0;
6510
+ }
6511
+ .detail-item {
6512
+ background-color: #f8f9fa;
6513
+ padding: 10px;
6514
+ border-radius: 5px;
6515
+ border-left: 3px solid #28a745;
6516
+ }
6517
+ .detail-label {
6518
+ font-weight: bold;
6519
+ color: #495057;
6520
+ font-size: 12px;
6521
+ text-transform: uppercase;
6522
+ margin-bottom: 5px;
6523
+ }
6524
+ .detail-value {
6525
+ color: #333;
6526
+ font-size: 14px;
6527
+ }
6528
+ .button {
6529
+ display: inline-block;
6530
+ background-color: #007bff;
6531
+ color: white;
6532
+ padding: 12px 25px;
6533
+ text-decoration: none;
6534
+ border-radius: 5px;
6535
+ margin: 10px 5px;
6536
+ font-weight: bold;
6537
+ text-align: center;
6538
+ }
6539
+ .button:hover {
6540
+ opacity: 0.9;
6541
+ }
6542
+ .action-section {
6543
+ text-align: center;
6544
+ margin: 30px 0;
6545
+ padding: 20px;
6546
+ background-color: #e9ecef;
6547
+ border-radius: 5px;
6548
+ }
6549
+ .footer {
6550
+ margin-top: 30px;
6551
+ padding-top: 20px;
6552
+ border-top: 1px solid #eee;
6553
+ font-size: 14px;
6554
+ color: #666;
6555
+ text-align: center;
6556
+ }
6557
+ .working-hours {
6558
+ background-color: #fff3cd;
6559
+ border: 1px solid #ffeaa7;
6560
+ border-radius: 5px;
6561
+ padding: 15px;
6562
+ margin: 15px 0;
6563
+ }
6564
+ .working-hours h4 {
6565
+ color: #856404;
6566
+ margin-top: 0;
6567
+ }
6568
+ </style>
6569
+ </head>
6570
+ <body>
6571
+ <div class="container">
6572
+ <div class="header">
6573
+ <div class="logo">MetaEstetics</div>
6574
+ <p>Clinic Management System</p>
6575
+ </div>
6576
+
6577
+ <h1>\u{1F389} Great News! Invitation Accepted</h1>
6578
+
6579
+ <div class="success-badge">
6580
+ <strong>\u2705 Dr. {{practitionerName}} has accepted your invitation!</strong>
6581
+ <p style="margin: 5px 0 0 0;">They are now part of the {{clinicName}} team.</p>
6582
+ </div>
6583
+
6584
+ <p>Dear {{clinicAdminName}},</p>
6585
+
6586
+ <p>We're excited to inform you that <strong>Dr. {{practitionerName}}</strong> has accepted your invitation to join <strong>{{clinicName}}</strong>.</p>
6587
+
6588
+ <div class="practitioner-card">
6589
+ {{#practitionerPhoto}}
6590
+ <img src="{{practitionerPhoto}}" alt="{{practitionerName}}" class="practitioner-photo">
6591
+ {{/practitionerPhoto}}
6592
+ <div class="practitioner-info">
6593
+ <div class="practitioner-name">Dr. {{practitionerName}}</div>
6594
+ <div class="practitioner-details">
6595
+ {{#practitionerSpecialties}}
6596
+ <p><strong>Specialties:</strong> {{practitionerSpecialties}}</p>
6597
+ {{/practitionerSpecialties}}
6598
+ {{#practitionerExperience}}
6599
+ <p><strong>Experience:</strong> {{practitionerExperience}} years</p>
6600
+ {{/practitionerExperience}}
6601
+ <p><strong>Joined:</strong> {{acceptedDate}}</p>
6602
+ </div>
6603
+ </div>
6604
+ </div>
6605
+
6606
+ <div class="details-grid">
6607
+ <div class="detail-item">
6608
+ <div class="detail-label">Status</div>
6609
+ <div class="detail-value">\u2705 Active Team Member</div>
6610
+ </div>
6611
+ <div class="detail-item">
6612
+ <div class="detail-label">Invitation Date</div>
6613
+ <div class="detail-value">{{invitationDate}}</div>
6614
+ </div>
6615
+ </div>
6616
+
6617
+ <div class="working-hours">
6618
+ <h4>\u{1F4C5} Scheduled Working Hours</h4>
6619
+ <p>{{workingHours}}</p>
6620
+ </div>
6621
+
6622
+ <div class="action-section">
6623
+ <p><strong>Next Steps:</strong></p>
6624
+ <a href="{{clinicDashboardUrl}}" class="button">View Clinic Dashboard</a>
6625
+ <a href="{{practitionerProfileUrl}}" class="button">View Practitioner Profile</a>
6626
+ </div>
6627
+
6628
+ <p><strong>What happens next?</strong></p>
6629
+ <ul>
6630
+ <li>Dr. {{practitionerName}} is now visible on your clinic profile</li>
6631
+ <li>Patients can book appointments with them during their scheduled hours</li>
6632
+ <li>They have access to your clinic's patient management system</li>
6633
+ <li>You can manage their schedule and availability from your dashboard</li>
6634
+ </ul>
6635
+
6636
+ <p>Welcome Dr. {{practitionerName}} to the team! \u{1F38A}</p>
6637
+
6638
+ <div class="footer">
6639
+ <p>Best regards,<br>
6640
+ <strong>The MetaEstetics Team</strong></p>
6641
+ <hr>
6642
+ <p style="font-size: 12px; color: #999;">
6643
+ This is an automated notification from MetaEstetics.
6644
+ <br>\xA9 {{currentYear}} MetaEstetics. All rights reserved.
6645
+ </p>
6646
+ </div>
6647
+ </div>
6648
+ </body>
6649
+ </html>
6650
+ `;
6651
+
6652
+ // src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts
6653
+ var inviteRejectedNotificationTemplate = `
6654
+ <!DOCTYPE html>
6655
+ <html lang="en">
6656
+ <head>
6657
+ <meta charset="UTF-8">
6658
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6659
+ <title>Practitioner Invitation Declined - {{practitionerName}}</title>
6660
+ <style>
6661
+ body {
6662
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
6663
+ line-height: 1.6;
6664
+ color: #333;
6665
+ max-width: 600px;
6666
+ margin: 0 auto;
6667
+ padding: 20px;
6668
+ background-color: #f4f4f4;
6669
+ }
6670
+ .container {
6671
+ background-color: #ffffff;
6672
+ padding: 30px;
6673
+ border-radius: 10px;
6674
+ box-shadow: 0 0 20px rgba(0,0,0,0.1);
6675
+ }
6676
+ .header {
6677
+ text-align: center;
6678
+ margin-bottom: 30px;
6679
+ border-bottom: 2px solid #dc3545;
6680
+ padding-bottom: 20px;
6681
+ }
6682
+ .logo {
6683
+ font-size: 28px;
6684
+ font-weight: bold;
6685
+ color: #dc3545;
6686
+ margin-bottom: 10px;
6687
+ }
6688
+ h1 {
6689
+ color: #dc3545;
6690
+ text-align: center;
6691
+ margin-bottom: 20px;
6692
+ }
6693
+ .notice-badge {
6694
+ background-color: #f8d7da;
6695
+ color: #721c24;
6696
+ padding: 15px;
6697
+ border-left: 4px solid #dc3545;
6698
+ margin: 20px 0;
6699
+ border-radius: 0 5px 5px 0;
6700
+ }
6701
+ .practitioner-card {
6702
+ background-color: #f8f9fa;
6703
+ padding: 20px;
6704
+ border-radius: 8px;
6705
+ margin: 20px 0;
6706
+ border: 1px solid #dee2e6;
6707
+ }
6708
+ .practitioner-photo {
6709
+ width: 80px;
6710
+ height: 80px;
6711
+ border-radius: 50%;
6712
+ object-fit: cover;
6713
+ float: left;
6714
+ margin-right: 15px;
6715
+ border: 3px solid #dc3545;
6716
+ }
6717
+ .practitioner-info {
6718
+ overflow: hidden;
6719
+ }
6720
+ .practitioner-name {
6721
+ font-size: 18px;
6722
+ font-weight: bold;
6723
+ color: #333;
6724
+ margin-bottom: 5px;
6725
+ }
6726
+ .practitioner-details {
6727
+ color: #666;
6728
+ font-size: 14px;
6729
+ line-height: 1.4;
6730
+ }
6731
+ .details-grid {
6732
+ display: grid;
6733
+ grid-template-columns: 1fr 1fr;
6734
+ gap: 15px;
6735
+ margin: 20px 0;
6736
+ }
6737
+ .detail-item {
6738
+ background-color: #f8f9fa;
6739
+ padding: 10px;
6740
+ border-radius: 5px;
6741
+ border-left: 3px solid #dc3545;
6742
+ }
6743
+ .detail-label {
6744
+ font-weight: bold;
6745
+ color: #495057;
6746
+ font-size: 12px;
6747
+ text-transform: uppercase;
6748
+ margin-bottom: 5px;
6749
+ }
6750
+ .detail-value {
6751
+ color: #333;
6752
+ font-size: 14px;
6753
+ }
6754
+ .button {
6755
+ display: inline-block;
6756
+ background-color: #007bff;
6757
+ color: white;
6758
+ padding: 12px 25px;
6759
+ text-decoration: none;
6760
+ border-radius: 5px;
6761
+ margin: 10px 5px;
6762
+ font-weight: bold;
6763
+ text-align: center;
6764
+ }
6765
+ .button.secondary {
6766
+ background-color: #6c757d;
6767
+ }
6768
+ .button:hover {
6769
+ opacity: 0.9;
6770
+ }
6771
+ .action-section {
6772
+ text-align: center;
6773
+ margin: 30px 0;
6774
+ padding: 20px;
6775
+ background-color: #e9ecef;
6776
+ border-radius: 5px;
6777
+ }
6778
+ .footer {
6779
+ margin-top: 30px;
6780
+ padding-top: 20px;
6781
+ border-top: 1px solid #eee;
6782
+ font-size: 14px;
6783
+ color: #666;
6784
+ text-align: center;
6785
+ }
6786
+ .suggestions {
6787
+ background-color: #d1ecf1;
6788
+ border: 1px solid #bee5eb;
6789
+ border-radius: 5px;
6790
+ padding: 15px;
6791
+ margin: 15px 0;
6792
+ }
6793
+ .suggestions h4 {
6794
+ color: #0c5460;
6795
+ margin-top: 0;
6796
+ }
6797
+ .reason-box {
6798
+ background-color: #fff3cd;
6799
+ border: 1px solid #ffeaa7;
6800
+ border-radius: 5px;
6801
+ padding: 15px;
6802
+ margin: 15px 0;
6803
+ }
6804
+ </style>
6805
+ </head>
6806
+ <body>
6807
+ <div class="container">
6808
+ <div class="header">
6809
+ <div class="logo">MetaEstetics</div>
6810
+ <p>Clinic Management System</p>
6811
+ </div>
6812
+
6813
+ <h1>Invitation Update</h1>
6814
+
6815
+ <div class="notice-badge">
6816
+ <strong>\u274C Dr. {{practitionerName}} has declined your invitation</strong>
6817
+ <p style="margin: 5px 0 0 0;">They will not be joining {{clinicName}} at this time.</p>
6818
+ </div>
6819
+
6820
+ <p>Dear {{clinicAdminName}},</p>
6821
+
6822
+ <p>We wanted to let you know that <strong>Dr. {{practitionerName}}</strong> has declined your invitation to join <strong>{{clinicName}}</strong>.</p>
6823
+
6824
+ <div class="practitioner-card">
6825
+ {{#practitionerPhoto}}
6826
+ <img src="{{practitionerPhoto}}" alt="{{practitionerName}}" class="practitioner-photo">
6827
+ {{/practitionerPhoto}}
6828
+ <div class="practitioner-info">
6829
+ <div class="practitioner-name">Dr. {{practitionerName}}</div>
6830
+ <div class="practitioner-details">
6831
+ {{#practitionerSpecialties}}
6832
+ <p><strong>Specialties:</strong> {{practitionerSpecialties}}</p>
6833
+ {{/practitionerSpecialties}}
6834
+ <p><strong>Decision Date:</strong> {{rejectedDate}}</p>
6835
+ </div>
6836
+ </div>
6837
+ </div>
6838
+
6839
+ <div class="details-grid">
6840
+ <div class="detail-item">
6841
+ <div class="detail-label">Status</div>
6842
+ <div class="detail-value">\u274C Invitation Declined</div>
6843
+ </div>
6844
+ <div class="detail-item">
6845
+ <div class="detail-label">Original Invitation</div>
6846
+ <div class="detail-value">{{invitationDate}}</div>
6847
+ </div>
6848
+ </div>
6849
+
6850
+ {{#rejectionReason}}
6851
+ <div class="reason-box">
6852
+ <h4>\u{1F4DD} Reason Provided:</h4>
6853
+ <p>"{{rejectionReason}}"</p>
6854
+ </div>
6855
+ {{/rejectionReason}}
6856
+
6857
+ <div class="suggestions">
6858
+ <h4>\u{1F4A1} What you can do next:</h4>
6859
+ <ul>
6860
+ <li><strong>Reach out directly:</strong> Consider contacting Dr. {{practitionerName}} to discuss their concerns</li>
6861
+ <li><strong>Adjust your offer:</strong> Review your working hours, compensation, or clinic benefits</li>
6862
+ <li><strong>Invite other practitioners:</strong> Look for other qualified professionals in your area</li>
6863
+ <li><strong>Try again later:</strong> Their circumstances may change in the future</li>
6864
+ </ul>
6865
+ </div>
6866
+
6867
+ <div class="action-section">
6868
+ <p><strong>Explore your options:</strong></p>
6869
+ <a href="{{findPractitionersUrl}}" class="button">Find More Practitioners</a>
6870
+ <a href="{{clinicDashboardUrl}}" class="button secondary">View Dashboard</a>
6871
+ </div>
6872
+
6873
+ <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>
6874
+
6875
+ <div class="footer">
6876
+ <p>Best regards,<br>
6877
+ <strong>The MetaEstetics Team</strong></p>
6878
+ <hr>
6879
+ <p style="font-size: 12px; color: #999;">
6880
+ This is an automated notification from MetaEstetics.
6881
+ <br>\xA9 {{currentYear}} MetaEstetics. All rights reserved.
6882
+ </p>
6883
+ <p style="font-size: 12px; color: #999;">
6884
+ <strong>Note:</strong> Practitioner contact information is not shared without their consent.
6885
+ </p>
6886
+ </div>
6887
+ </div>
6888
+ </body>
6889
+ </html>
6890
+ `;
6891
+
6892
+ // src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts
6893
+ var ExistingPractitionerInviteMailingService = class extends BaseMailingService {
6894
+ /**
6895
+ * Constructor for ExistingPractitionerInviteMailingService
6896
+ * @param firestore Firestore instance provided by the caller
6897
+ * @param mailgunClient Mailgun client instance (mailgun.js v10+) provided by the caller
6898
+ */
6899
+ constructor(firestore17, mailgunClient) {
6900
+ super(firestore17, mailgunClient);
6901
+ this.DEFAULT_MAILGUN_DOMAIN = "mg.metaesthetics.net";
6902
+ this.DEFAULT_FROM_ADDRESS = "MetaEstetics <no-reply@mg.metaesthetics.net>";
6903
+ }
6904
+ /**
6905
+ * Sends an invitation email to an existing practitioner
6906
+ * @param data The invitation email data
6907
+ * @returns Promise resolved when email is sent
6908
+ */
6909
+ async sendPractitionerInvitationEmail(data) {
6910
+ var _a, _b, _c, _d;
6911
+ try {
6912
+ Logger.info(
6913
+ "[ExistingPractitionerInviteMailingService] Sending invitation email to practitioner",
6914
+ data.practitioner.email
6915
+ );
6916
+ const workingHours = this.formatWorkingHours(
6917
+ data.invite.proposedWorkingHours
6918
+ );
6919
+ const expiryDate = new Date(data.invite.createdAt.toDate());
6920
+ expiryDate.setDate(expiryDate.getDate() + 30);
6921
+ const expirationDate = expiryDate.toLocaleDateString("en-US", {
6922
+ weekday: "long",
6923
+ year: "numeric",
6924
+ month: "long",
6925
+ day: "numeric"
6926
+ });
6927
+ const practitionerName = `${data.practitioner.firstName} ${data.practitioner.lastName}`;
6928
+ const templateVariables = {
6929
+ clinicName: data.clinic.name,
6930
+ practitionerName,
6931
+ clinicAddress: data.clinic.address,
6932
+ workingHours,
6933
+ expirationDate,
6934
+ acceptUrl: data.urls.acceptUrl,
6935
+ rejectUrl: data.urls.rejectUrl,
6936
+ contactEmail: data.clinic.contactEmail,
6937
+ contactPhone: data.clinic.contactPhone || "Contact clinic directly",
6938
+ currentYear: (/* @__PURE__ */ new Date()).getFullYear().toString()
6939
+ };
6940
+ const html = this.renderTemplate(
6941
+ existingPractitionerInvitationTemplate,
6942
+ templateVariables
6943
+ );
6944
+ const subject = ((_a = data.options) == null ? void 0 : _a.customSubject) || `Invitation to Join ${data.clinic.name}`;
6945
+ const from = ((_b = data.options) == null ? void 0 : _b.fromAddress) || this.DEFAULT_FROM_ADDRESS;
6946
+ const domain = ((_c = data.options) == null ? void 0 : _c.mailgunDomain) || this.DEFAULT_MAILGUN_DOMAIN;
6947
+ const mailgunData = {
6948
+ to: data.practitioner.email,
6949
+ from,
6950
+ subject,
6951
+ html
6952
+ };
6953
+ Logger.info(
6954
+ "[ExistingPractitionerInviteMailingService] Sending email with data:",
6955
+ {
6956
+ domain,
6957
+ to: mailgunData.to,
6958
+ from: mailgunData.from,
6959
+ subject: mailgunData.subject,
6960
+ hasHtml: !!mailgunData.html
6961
+ }
6962
+ );
6963
+ const result = await this.sendEmail(domain, mailgunData);
6964
+ await this.logEmailAttempt(
6965
+ {
6966
+ to: data.practitioner.email,
6967
+ subject,
6968
+ templateName: "existing_practitioner_invitation"
6969
+ },
6970
+ true
6971
+ );
6972
+ return result;
6973
+ } catch (error) {
6974
+ Logger.error(
6975
+ "[ExistingPractitionerInviteMailingService] Error sending practitioner invitation:",
6976
+ {
6977
+ errorMessage: error.message,
6978
+ errorDetails: error.details,
6979
+ errorStatus: error.status,
6980
+ stack: error.stack
6981
+ }
6982
+ );
6983
+ await this.logEmailAttempt(
6984
+ {
6985
+ to: data.practitioner.email,
6986
+ subject: ((_d = data.options) == null ? void 0 : _d.customSubject) || `Invitation to Join ${data.clinic.name}`,
6987
+ templateName: "existing_practitioner_invitation"
6988
+ },
6989
+ false,
6990
+ error
6991
+ );
6992
+ throw error;
6993
+ }
6994
+ }
6995
+ /**
6996
+ * Sends a notification email to clinic admin when practitioner accepts invitation
6997
+ * @param data The notification email data
6998
+ * @returns Promise resolved when email is sent
6999
+ */
7000
+ async sendAcceptedNotificationEmail(data) {
7001
+ var _a, _b, _c, _d, _e, _f;
7002
+ try {
7003
+ Logger.info(
7004
+ "[ExistingPractitionerInviteMailingService] Sending acceptance notification to clinic admin",
7005
+ data.clinic.adminEmail
7006
+ );
7007
+ const practitionerName = `${data.practitioner.firstName} ${data.practitioner.lastName}`;
7008
+ const workingHours = this.formatWorkingHours(
7009
+ data.invite.proposedWorkingHours
7010
+ );
7011
+ const templateVariables = {
7012
+ clinicName: data.clinic.name,
7013
+ clinicAdminName: data.clinic.adminName || "Admin",
7014
+ practitionerName,
7015
+ practitionerPhoto: data.practitioner.profileImageUrl || "",
7016
+ practitionerSpecialties: ((_a = data.practitioner.specialties) == null ? void 0 : _a.join(", ")) || "",
7017
+ practitionerExperience: ((_b = data.practitioner.experienceYears) == null ? void 0 : _b.toString()) || "",
7018
+ invitationDate: data.context.invitationDate,
7019
+ acceptedDate: data.context.responseDate,
7020
+ workingHours,
7021
+ clinicDashboardUrl: data.urls.clinicDashboardUrl,
7022
+ practitionerProfileUrl: data.urls.practitionerProfileUrl || "#",
7023
+ currentYear: (/* @__PURE__ */ new Date()).getFullYear().toString()
7024
+ };
7025
+ const html = this.renderTemplate(
7026
+ inviteAcceptedNotificationTemplate,
7027
+ templateVariables
7028
+ );
7029
+ const subject = ((_c = data.options) == null ? void 0 : _c.customSubject) || `Great News! Dr. ${practitionerName} Accepted Your Invitation`;
7030
+ const from = ((_d = data.options) == null ? void 0 : _d.fromAddress) || this.DEFAULT_FROM_ADDRESS;
7031
+ const domain = ((_e = data.options) == null ? void 0 : _e.mailgunDomain) || this.DEFAULT_MAILGUN_DOMAIN;
7032
+ const mailgunData = {
7033
+ to: data.clinic.adminEmail,
7034
+ from,
7035
+ subject,
7036
+ html
7037
+ };
7038
+ const result = await this.sendEmail(domain, mailgunData);
7039
+ await this.logEmailAttempt(
7040
+ {
7041
+ to: data.clinic.adminEmail,
7042
+ subject,
7043
+ templateName: "invite_accepted_notification"
7044
+ },
7045
+ true
7046
+ );
7047
+ return result;
7048
+ } catch (error) {
7049
+ Logger.error(
7050
+ "[ExistingPractitionerInviteMailingService] Error sending acceptance notification:",
7051
+ error
7052
+ );
7053
+ await this.logEmailAttempt(
7054
+ {
7055
+ to: data.clinic.adminEmail,
7056
+ subject: ((_f = data.options) == null ? void 0 : _f.customSubject) || "Invitation Accepted",
7057
+ templateName: "invite_accepted_notification"
7058
+ },
7059
+ false,
7060
+ error
7061
+ );
7062
+ throw error;
7063
+ }
7064
+ }
7065
+ /**
7066
+ * Sends a notification email to clinic admin when practitioner rejects invitation
7067
+ * @param data The notification email data
7068
+ * @returns Promise resolved when email is sent
7069
+ */
7070
+ async sendRejectedNotificationEmail(data) {
7071
+ var _a, _b, _c, _d, _e;
7072
+ try {
7073
+ Logger.info(
7074
+ "[ExistingPractitionerInviteMailingService] Sending rejection notification to clinic admin",
7075
+ data.clinic.adminEmail
7076
+ );
7077
+ const practitionerName = `${data.practitioner.firstName} ${data.practitioner.lastName}`;
7078
+ const templateVariables = {
7079
+ clinicName: data.clinic.name,
7080
+ clinicAdminName: data.clinic.adminName || "Admin",
7081
+ practitionerName,
7082
+ practitionerPhoto: data.practitioner.profileImageUrl || "",
7083
+ practitionerSpecialties: ((_a = data.practitioner.specialties) == null ? void 0 : _a.join(", ")) || "",
7084
+ invitationDate: data.context.invitationDate,
7085
+ rejectedDate: data.context.responseDate,
7086
+ rejectionReason: data.context.rejectionReason || "",
7087
+ findPractitionersUrl: data.urls.findPractitionersUrl || "#",
7088
+ clinicDashboardUrl: data.urls.clinicDashboardUrl,
7089
+ currentYear: (/* @__PURE__ */ new Date()).getFullYear().toString()
7090
+ };
7091
+ const html = this.renderTemplate(
7092
+ inviteRejectedNotificationTemplate,
7093
+ templateVariables
7094
+ );
7095
+ const subject = ((_b = data.options) == null ? void 0 : _b.customSubject) || `Invitation Update: Dr. ${practitionerName} Declined`;
7096
+ const from = ((_c = data.options) == null ? void 0 : _c.fromAddress) || this.DEFAULT_FROM_ADDRESS;
7097
+ const domain = ((_d = data.options) == null ? void 0 : _d.mailgunDomain) || this.DEFAULT_MAILGUN_DOMAIN;
7098
+ const mailgunData = {
7099
+ to: data.clinic.adminEmail,
7100
+ from,
7101
+ subject,
7102
+ html
7103
+ };
7104
+ const result = await this.sendEmail(domain, mailgunData);
7105
+ await this.logEmailAttempt(
7106
+ {
7107
+ to: data.clinic.adminEmail,
7108
+ subject,
7109
+ templateName: "invite_rejected_notification"
7110
+ },
7111
+ true
7112
+ );
7113
+ return result;
7114
+ } catch (error) {
7115
+ Logger.error(
7116
+ "[ExistingPractitionerInviteMailingService] Error sending rejection notification:",
7117
+ error
7118
+ );
7119
+ await this.logEmailAttempt(
7120
+ {
7121
+ to: data.clinic.adminEmail,
7122
+ subject: ((_e = data.options) == null ? void 0 : _e.customSubject) || "Invitation Declined",
7123
+ templateName: "invite_rejected_notification"
7124
+ },
7125
+ false,
7126
+ error
7127
+ );
7128
+ throw error;
7129
+ }
7130
+ }
7131
+ /**
7132
+ * Handles the practitioner invite creation event
7133
+ * Fetches necessary data and sends the invitation email to the practitioner
7134
+ * @param invite The practitioner invite object
7135
+ * @param mailgunConfig Mailgun configuration
7136
+ * @returns Promise resolved when the email is sent
7137
+ */
7138
+ async handleInviteCreationEvent(invite, mailgunConfig) {
7139
+ var _a, _b;
7140
+ try {
7141
+ Logger.info(
7142
+ "[ExistingPractitionerInviteMailingService] Handling invite creation event for invite:",
7143
+ invite.id
7144
+ );
7145
+ if (!invite || !invite.id || !invite.practitionerId || !invite.clinicId) {
7146
+ throw new Error(
7147
+ `Invalid invite data: Missing required properties. Invite ID: ${invite == null ? void 0 : invite.id}`
7148
+ );
7149
+ }
7150
+ if (invite.status !== "pending" /* PENDING */) {
7151
+ Logger.info(
7152
+ "[ExistingPractitionerInviteMailingService] Invite is not pending, skipping email",
7153
+ { inviteId: invite.id, status: invite.status }
7154
+ );
7155
+ return;
7156
+ }
7157
+ const [practitioner, clinic] = await Promise.all([
7158
+ this.fetchPractitionerById(invite.practitionerId),
7159
+ this.fetchClinicById(invite.clinicId)
7160
+ ]);
7161
+ if (!practitioner) {
7162
+ throw new Error(`Practitioner ${invite.practitionerId} not found`);
7163
+ }
7164
+ if (!clinic) {
7165
+ throw new Error(`Clinic ${invite.clinicId} not found`);
7166
+ }
7167
+ const emailData = {
7168
+ invite,
7169
+ practitioner: {
7170
+ firstName: practitioner.basicInfo.firstName || "",
7171
+ lastName: practitioner.basicInfo.lastName || "",
7172
+ email: practitioner.basicInfo.email || "",
7173
+ specialties: ((_b = (_a = practitioner.certification) == null ? void 0 : _a.specialties) == null ? void 0 : _b.map(
7174
+ (s) => s.name || s
7175
+ )) || [],
7176
+ profileImageUrl: typeof practitioner.basicInfo.profileImageUrl === "string" ? practitioner.basicInfo.profileImageUrl : null
7177
+ },
7178
+ clinic: {
7179
+ name: clinic.name || "Medical Clinic",
7180
+ address: this.formatClinicAddress(clinic.location),
7181
+ contactEmail: clinic.contactInfo.email || "contact@clinic.com",
7182
+ contactPhone: clinic.contactInfo.phoneNumber
7183
+ },
7184
+ urls: {
7185
+ acceptUrl: mailgunConfig.acceptUrl,
7186
+ rejectUrl: mailgunConfig.rejectUrl
7187
+ },
7188
+ options: {
7189
+ fromAddress: mailgunConfig.fromAddress,
7190
+ mailgunDomain: mailgunConfig.domain
7191
+ }
7192
+ };
7193
+ await this.sendPractitionerInvitationEmail(emailData);
7194
+ Logger.info(
7195
+ "[ExistingPractitionerInviteMailingService] Invitation email sent successfully"
7196
+ );
7197
+ } catch (error) {
7198
+ Logger.error(
7199
+ "[ExistingPractitionerInviteMailingService] Error handling invite creation event:",
7200
+ {
7201
+ errorMessage: error.message,
7202
+ errorDetails: error.details,
7203
+ errorStatus: error.status,
7204
+ stack: error.stack,
7205
+ inviteId: invite == null ? void 0 : invite.id
7206
+ }
7207
+ );
7208
+ throw error;
7209
+ }
7210
+ }
7211
+ // --- Private Helper Methods ---
7212
+ /**
7213
+ * Formats working hours for display in emails
7214
+ * @param workingHours The working hours object
7215
+ * @returns Formatted string representation
7216
+ */
7217
+ formatWorkingHours(workingHours) {
7218
+ if (!workingHours) return "To be determined";
7219
+ return Object.entries(workingHours).map(([day, hours]) => `${day}: ${hours}`).join(", ");
7220
+ }
7221
+ /**
7222
+ * Formats clinic address for display
7223
+ * @param location The clinic location object
7224
+ * @returns Formatted address string
7225
+ */
7226
+ formatClinicAddress(location) {
7227
+ if (!location) return "Address not specified";
7228
+ const parts = [
7229
+ location.street,
7230
+ location.city,
7231
+ location.state,
7232
+ location.country
7233
+ ].filter(Boolean);
7234
+ return parts.join(", ");
7235
+ }
7236
+ /**
7237
+ * Fetches a practitioner by ID
7238
+ * @param practitionerId The practitioner ID
7239
+ * @returns The practitioner or null if not found
7240
+ */
7241
+ async fetchPractitionerById(practitionerId) {
7242
+ try {
7243
+ const doc = await this.db.collection(PRACTITIONERS_COLLECTION).doc(practitionerId).get();
7244
+ return doc.exists ? doc.data() : null;
7245
+ } catch (error) {
7246
+ Logger.error(
7247
+ "[ExistingPractitionerInviteMailingService] Error fetching practitioner:",
7248
+ error
7249
+ );
7250
+ return null;
7251
+ }
7252
+ }
7253
+ /**
7254
+ * Fetches a clinic by ID
7255
+ * @param clinicId The clinic ID
7256
+ * @returns The clinic or null if not found
7257
+ */
7258
+ async fetchClinicById(clinicId) {
7259
+ try {
7260
+ const doc = await this.db.collection(CLINICS_COLLECTION).doc(clinicId).get();
7261
+ return doc.exists ? doc.data() : null;
7262
+ } catch (error) {
7263
+ Logger.error(
7264
+ "[ExistingPractitionerInviteMailingService] Error fetching clinic:",
7265
+ error
7266
+ );
7267
+ return null;
7268
+ }
7269
+ }
7270
+ };
7271
+
5999
7272
  // src/admin/booking/booking.admin.ts
6000
7273
  var admin15 = __toESM(require("firebase-admin"));
6001
7274
 
@@ -7331,6 +8604,7 @@ TimestampUtils.enableServerMode();
7331
8604
  CalendarAdminService,
7332
8605
  ClinicAggregationService,
7333
8606
  DocumentManagerAdminService,
8607
+ ExistingPractitionerInviteMailingService,
7334
8608
  FilledFormsAggregationService,
7335
8609
  Logger,
7336
8610
  MediaType,