@blackcode_sa/metaestetics-api 1.7.33 → 1.7.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blackcode_sa/metaestetics-api",
3
3
  "private": false,
4
- "version": "1.7.33",
4
+ "version": "1.7.34",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.mjs",
@@ -13,6 +13,7 @@ import {
13
13
  import { CLINICS_COLLECTION, Clinic } from "../../../types/clinic";
14
14
  import { ClinicInfo } from "../../../types/profile";
15
15
  import { Logger } from "../../logger";
16
+ import { ExistingPractitionerInviteMailingService } from "../../mailing/practitionerInvite/existing-practitioner-invite.mailing";
16
17
 
17
18
  /**
18
19
  * @class PractitionerInviteAggregationService
@@ -22,13 +23,19 @@ import { Logger } from "../../logger";
22
23
  */
23
24
  export class PractitionerInviteAggregationService {
24
25
  private db: admin.firestore.Firestore;
26
+ private mailingService?: ExistingPractitionerInviteMailingService;
25
27
 
26
28
  /**
27
29
  * Constructor for PractitionerInviteAggregationService.
28
30
  * @param firestore Optional Firestore instance. If not provided, it uses the default admin SDK instance.
31
+ * @param mailingService Optional mailing service for sending emails
29
32
  */
30
- constructor(firestore?: admin.firestore.Firestore) {
33
+ constructor(
34
+ firestore?: admin.firestore.Firestore,
35
+ mailingService?: ExistingPractitionerInviteMailingService
36
+ ) {
31
37
  this.db = firestore || admin.firestore();
38
+ this.mailingService = mailingService;
32
39
  Logger.info("[PractitionerInviteAggregationService] Initialized.");
33
40
  }
34
41
 
@@ -36,17 +43,49 @@ export class PractitionerInviteAggregationService {
36
43
  * Handles side effects when a practitioner invite is first created.
37
44
  * This function would typically be called by a Firestore onCreate trigger.
38
45
  * @param {PractitionerInvite} invite - The newly created PractitionerInvite object.
46
+ * @param {object} emailConfig - Optional email configuration for sending invite emails
39
47
  * @returns {Promise<void>}
40
48
  */
41
- async handleInviteCreate(invite: PractitionerInvite): Promise<void> {
49
+ async handleInviteCreate(
50
+ invite: PractitionerInvite,
51
+ emailConfig?: {
52
+ fromAddress: string;
53
+ domain: string;
54
+ acceptUrl: string;
55
+ rejectUrl: string;
56
+ }
57
+ ): Promise<void> {
42
58
  Logger.info(
43
59
  `[PractitionerInviteAggService] Handling CREATE for invite: ${invite.id}, practitioner: ${invite.practitionerId}, clinic: ${invite.clinicId}, status: ${invite.status}`
44
60
  );
45
61
 
46
62
  try {
47
- // TODO: Add any side effects needed when an invite is created
48
- // For example: Send notification emails, update counters, etc.
49
- // Currently, the main email sending is handled by the mailing service
63
+ // Send invitation email to practitioner if mailing service is available
64
+ if (
65
+ this.mailingService &&
66
+ emailConfig &&
67
+ invite.status === PractitionerInviteStatus.PENDING
68
+ ) {
69
+ Logger.info(
70
+ `[PractitionerInviteAggService] Sending invitation email for invite: ${invite.id}`
71
+ );
72
+
73
+ try {
74
+ await this.mailingService.handleInviteCreationEvent(
75
+ invite,
76
+ emailConfig
77
+ );
78
+ Logger.info(
79
+ `[PractitionerInviteAggService] Successfully sent invitation email for invite: ${invite.id}`
80
+ );
81
+ } catch (emailError) {
82
+ Logger.error(
83
+ `[PractitionerInviteAggService] Error sending invitation email for invite ${invite.id}:`,
84
+ emailError
85
+ );
86
+ // Don't throw - email failure shouldn't break the invite creation
87
+ }
88
+ }
50
89
 
51
90
  Logger.info(
52
91
  `[PractitionerInviteAggService] Successfully processed CREATE for invite: ${invite.id}`
@@ -65,11 +104,19 @@ export class PractitionerInviteAggregationService {
65
104
  * This function would typically be called by a Firestore onUpdate trigger.
66
105
  * @param {PractitionerInvite} before - The PractitionerInvite object before the update.
67
106
  * @param {PractitionerInvite} after - The PractitionerInvite object after the update.
107
+ * @param {object} emailConfig - Optional email configuration for sending notification emails
68
108
  * @returns {Promise<void>}
69
109
  */
70
110
  async handleInviteUpdate(
71
111
  before: PractitionerInvite,
72
- after: PractitionerInvite
112
+ after: PractitionerInvite,
113
+ emailConfig?: {
114
+ fromAddress: string;
115
+ domain: string;
116
+ clinicDashboardUrl: string;
117
+ practitionerProfileUrl?: string;
118
+ findPractitionersUrl?: string;
119
+ }
73
120
  ): Promise<void> {
74
121
  Logger.info(
75
122
  `[PractitionerInviteAggService] Handling UPDATE for invite: ${after.id}. Status ${before.status} -> ${after.status}`
@@ -91,7 +138,7 @@ export class PractitionerInviteAggregationService {
91
138
  Logger.info(
92
139
  `[PractitionerInviteAggService] Invite ${after.id} PENDING -> ACCEPTED. Adding practitioner to clinic.`
93
140
  );
94
- await this.handleInviteAccepted(after);
141
+ await this.handleInviteAccepted(after, emailConfig);
95
142
  }
96
143
 
97
144
  // Handle PENDING -> REJECTED
@@ -102,7 +149,7 @@ export class PractitionerInviteAggregationService {
102
149
  Logger.info(
103
150
  `[PractitionerInviteAggService] Invite ${after.id} PENDING -> REJECTED.`
104
151
  );
105
- await this.handleInviteRejected(after);
152
+ await this.handleInviteRejected(after, emailConfig);
106
153
  }
107
154
 
108
155
  // Handle PENDING -> CANCELLED
@@ -162,10 +209,17 @@ export class PractitionerInviteAggregationService {
162
209
  * Handles the business logic when a practitioner accepts an invite.
163
210
  * This includes adding the practitioner to the clinic and the clinic to the practitioner.
164
211
  * @param {PractitionerInvite} invite - The accepted invite
212
+ * @param {object} emailConfig - Optional email configuration for sending notification emails
165
213
  * @returns {Promise<void>}
166
214
  */
167
215
  private async handleInviteAccepted(
168
- invite: PractitionerInvite
216
+ invite: PractitionerInvite,
217
+ emailConfig?: {
218
+ fromAddress: string;
219
+ domain: string;
220
+ clinicDashboardUrl: string;
221
+ practitionerProfileUrl?: string;
222
+ }
169
223
  ): Promise<void> {
170
224
  Logger.info(
171
225
  `[PractitionerInviteAggService] Processing accepted invite ${invite.id} for practitioner ${invite.practitionerId} and clinic ${invite.clinicId}`
@@ -251,6 +305,31 @@ export class PractitionerInviteAggregationService {
251
305
  await this.updatePractitionerWorkingHours(practitioner.id, invite);
252
306
  }
253
307
 
308
+ // Send acceptance notification email to clinic admin if mailing service is available
309
+ if (this.mailingService && emailConfig) {
310
+ Logger.info(
311
+ `[PractitionerInviteAggService] Sending acceptance notification email for invite: ${invite.id}`
312
+ );
313
+
314
+ try {
315
+ await this.sendAcceptanceNotificationEmail(
316
+ invite,
317
+ practitioner,
318
+ clinic,
319
+ emailConfig
320
+ );
321
+ Logger.info(
322
+ `[PractitionerInviteAggService] Successfully sent acceptance notification email for invite: ${invite.id}`
323
+ );
324
+ } catch (emailError) {
325
+ Logger.error(
326
+ `[PractitionerInviteAggService] Error sending acceptance notification email for invite ${invite.id}:`,
327
+ emailError
328
+ );
329
+ // Don't throw - email failure shouldn't break the acceptance logic
330
+ }
331
+ }
332
+
254
333
  Logger.info(
255
334
  `[PractitionerInviteAggService] Successfully processed accepted invite ${invite.id}`
256
335
  );
@@ -266,18 +345,54 @@ export class PractitionerInviteAggregationService {
266
345
  /**
267
346
  * Handles the business logic when a practitioner rejects an invite.
268
347
  * @param {PractitionerInvite} invite - The rejected invite
348
+ * @param {object} emailConfig - Optional email configuration for sending notification emails
269
349
  * @returns {Promise<void>}
270
350
  */
271
351
  private async handleInviteRejected(
272
- invite: PractitionerInvite
352
+ invite: PractitionerInvite,
353
+ emailConfig?: {
354
+ fromAddress: string;
355
+ domain: string;
356
+ clinicDashboardUrl: string;
357
+ findPractitionersUrl?: string;
358
+ }
273
359
  ): Promise<void> {
274
360
  Logger.info(
275
361
  `[PractitionerInviteAggService] Processing rejected invite ${invite.id}`
276
362
  );
277
363
 
278
364
  try {
279
- // TODO: Add any side effects for rejected invites
280
- // For example: Update counters, send notifications, etc.
365
+ // Send rejection notification email to clinic admin if mailing service is available
366
+ if (this.mailingService && emailConfig) {
367
+ Logger.info(
368
+ `[PractitionerInviteAggService] Sending rejection notification email for invite: ${invite.id}`
369
+ );
370
+
371
+ try {
372
+ const [practitioner, clinic] = await Promise.all([
373
+ this.fetchPractitionerById(invite.practitionerId),
374
+ this.fetchClinicById(invite.clinicId),
375
+ ]);
376
+
377
+ if (practitioner && clinic) {
378
+ await this.sendRejectionNotificationEmail(
379
+ invite,
380
+ practitioner,
381
+ clinic,
382
+ emailConfig
383
+ );
384
+ Logger.info(
385
+ `[PractitionerInviteAggService] Successfully sent rejection notification email for invite: ${invite.id}`
386
+ );
387
+ }
388
+ } catch (emailError) {
389
+ Logger.error(
390
+ `[PractitionerInviteAggService] Error sending rejection notification email for invite ${invite.id}:`,
391
+ emailError
392
+ );
393
+ // Don't throw - email failure shouldn't break the rejection logic
394
+ }
395
+ }
281
396
 
282
397
  Logger.info(
283
398
  `[PractitionerInviteAggService] Successfully processed rejected invite ${invite.id}`
@@ -573,4 +688,124 @@ export class PractitionerInviteAggregationService {
573
688
  return null;
574
689
  }
575
690
  }
691
+
692
+ // --- Email Helper Methods ---
693
+
694
+ /**
695
+ * Sends acceptance notification email to clinic admin
696
+ * @param invite The accepted invite
697
+ * @param practitioner The practitioner who accepted
698
+ * @param clinic The clinic that sent the invite
699
+ * @param emailConfig Email configuration
700
+ */
701
+ private async sendAcceptanceNotificationEmail(
702
+ invite: PractitionerInvite,
703
+ practitioner: Practitioner,
704
+ clinic: Clinic,
705
+ emailConfig: {
706
+ fromAddress: string;
707
+ domain: string;
708
+ clinicDashboardUrl: string;
709
+ practitionerProfileUrl?: string;
710
+ }
711
+ ): Promise<void> {
712
+ if (!this.mailingService) return;
713
+
714
+ const notificationData = {
715
+ invite,
716
+ practitioner: {
717
+ firstName: practitioner.basicInfo.firstName || "",
718
+ lastName: practitioner.basicInfo.lastName || "",
719
+ specialties:
720
+ practitioner.certification?.specialties?.map(
721
+ (s: any) => s.name || s
722
+ ) || [],
723
+ profileImageUrl:
724
+ typeof practitioner.basicInfo.profileImageUrl === "string"
725
+ ? practitioner.basicInfo.profileImageUrl
726
+ : null,
727
+ experienceYears: undefined, // This would need to be calculated or stored in practitioner data
728
+ },
729
+ clinic: {
730
+ name: clinic.name,
731
+ adminName: undefined, // This would need to be fetched from clinic admin data
732
+ adminEmail: clinic.contactInfo.email,
733
+ },
734
+ context: {
735
+ invitationDate: invite.createdAt.toDate().toLocaleDateString(),
736
+ responseDate:
737
+ invite.acceptedAt?.toDate().toLocaleDateString() ||
738
+ new Date().toLocaleDateString(),
739
+ },
740
+ urls: {
741
+ clinicDashboardUrl: emailConfig.clinicDashboardUrl,
742
+ practitionerProfileUrl: emailConfig.practitionerProfileUrl,
743
+ },
744
+ options: {
745
+ fromAddress: emailConfig.fromAddress,
746
+ mailgunDomain: emailConfig.domain,
747
+ },
748
+ };
749
+
750
+ await this.mailingService.sendAcceptedNotificationEmail(notificationData);
751
+ }
752
+
753
+ /**
754
+ * Sends rejection notification email to clinic admin
755
+ * @param invite The rejected invite
756
+ * @param practitioner The practitioner who rejected
757
+ * @param clinic The clinic that sent the invite
758
+ * @param emailConfig Email configuration
759
+ */
760
+ private async sendRejectionNotificationEmail(
761
+ invite: PractitionerInvite,
762
+ practitioner: Practitioner,
763
+ clinic: Clinic,
764
+ emailConfig: {
765
+ fromAddress: string;
766
+ domain: string;
767
+ clinicDashboardUrl: string;
768
+ findPractitionersUrl?: string;
769
+ }
770
+ ): Promise<void> {
771
+ if (!this.mailingService) return;
772
+
773
+ const notificationData = {
774
+ invite,
775
+ practitioner: {
776
+ firstName: practitioner.basicInfo.firstName || "",
777
+ lastName: practitioner.basicInfo.lastName || "",
778
+ specialties:
779
+ practitioner.certification?.specialties?.map(
780
+ (s: any) => s.name || s
781
+ ) || [],
782
+ profileImageUrl:
783
+ typeof practitioner.basicInfo.profileImageUrl === "string"
784
+ ? practitioner.basicInfo.profileImageUrl
785
+ : null,
786
+ },
787
+ clinic: {
788
+ name: clinic.name,
789
+ adminName: undefined, // This would need to be fetched from clinic admin data
790
+ adminEmail: clinic.contactInfo.email,
791
+ },
792
+ context: {
793
+ invitationDate: invite.createdAt.toDate().toLocaleDateString(),
794
+ responseDate:
795
+ invite.rejectedAt?.toDate().toLocaleDateString() ||
796
+ new Date().toLocaleDateString(),
797
+ rejectionReason: invite.rejectionReason || undefined,
798
+ },
799
+ urls: {
800
+ clinicDashboardUrl: emailConfig.clinicDashboardUrl,
801
+ findPractitionersUrl: emailConfig.findPractitionersUrl,
802
+ },
803
+ options: {
804
+ fromAddress: emailConfig.fromAddress,
805
+ mailgunDomain: emailConfig.domain,
806
+ },
807
+ };
808
+
809
+ await this.mailingService.sendRejectedNotificationEmail(notificationData);
810
+ }
576
811
  }
@@ -33,6 +33,7 @@ import { ReviewsAggregationService } from "./aggregation/reviews/reviews.aggrega
33
33
  // Import mailing services
34
34
  import { BaseMailingService } from "./mailing/base.mailing.service";
35
35
  import { PractitionerInviteMailingService } from "./mailing/practitionerInvite/practitionerInvite.mailing";
36
+ import { ExistingPractitionerInviteMailingService } from "./mailing/practitionerInvite/existing-practitioner-invite.mailing";
36
37
 
37
38
  // Import booking services
38
39
  import { BookingAdmin } from "./booking/booking.admin";
@@ -87,7 +88,17 @@ export {
87
88
  };
88
89
 
89
90
  // Export mailing services
90
- export { BaseMailingService, PractitionerInviteMailingService };
91
+ export {
92
+ BaseMailingService,
93
+ PractitionerInviteMailingService,
94
+ ExistingPractitionerInviteMailingService,
95
+ };
96
+
97
+ // Export mailing service interfaces
98
+ export type {
99
+ ExistingPractitionerInviteEmailData,
100
+ ClinicAdminNotificationData,
101
+ } from "./mailing/practitionerInvite/existing-practitioner-invite.mailing";
91
102
 
92
103
  // Export booking services
93
104
  export { BookingAdmin };