@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/dist/admin/index.d.mts +238 -57
- package/dist/admin/index.d.ts +238 -57
- package/dist/admin/index.js +1167 -7
- package/dist/admin/index.mjs +1166 -7
- package/package.json +1 -1
- package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts +247 -12
- package/src/admin/index.ts +12 -1
- package/src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts +611 -0
- package/src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts +155 -0
- package/src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts +228 -0
- package/src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts +242 -0
package/package.json
CHANGED
package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
//
|
|
280
|
-
|
|
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
|
}
|
package/src/admin/index.ts
CHANGED
|
@@ -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 {
|
|
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 };
|