@blackcode_sa/metaestetics-api 1.13.5 → 1.13.6
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 +20 -1
- package/dist/admin/index.d.ts +20 -1
- package/dist/admin/index.js +217 -1
- package/dist/admin/index.mjs +217 -1
- package/package.json +121 -121
- package/src/__mocks__/firstore.ts +10 -10
- package/src/admin/aggregation/README.md +79 -79
- package/src/admin/aggregation/appointment/README.md +128 -128
- package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1984 -1984
- package/src/admin/aggregation/appointment/index.ts +1 -1
- package/src/admin/aggregation/clinic/README.md +52 -52
- package/src/admin/aggregation/clinic/clinic.aggregation.service.ts +966 -703
- package/src/admin/aggregation/clinic/index.ts +1 -1
- package/src/admin/aggregation/forms/README.md +13 -13
- package/src/admin/aggregation/forms/filled-forms.aggregation.service.ts +322 -322
- package/src/admin/aggregation/forms/index.ts +1 -1
- package/src/admin/aggregation/index.ts +8 -8
- package/src/admin/aggregation/patient/README.md +27 -27
- package/src/admin/aggregation/patient/index.ts +1 -1
- package/src/admin/aggregation/patient/patient.aggregation.service.ts +141 -141
- package/src/admin/aggregation/practitioner/README.md +42 -42
- package/src/admin/aggregation/practitioner/index.ts +1 -1
- package/src/admin/aggregation/practitioner/practitioner.aggregation.service.ts +433 -433
- package/src/admin/aggregation/practitioner-invite/index.ts +1 -1
- package/src/admin/aggregation/practitioner-invite/practitioner-invite.aggregation.service.ts +961 -961
- package/src/admin/aggregation/procedure/README.md +43 -43
- package/src/admin/aggregation/procedure/index.ts +1 -1
- package/src/admin/aggregation/procedure/procedure.aggregation.service.ts +702 -702
- package/src/admin/aggregation/reviews/index.ts +1 -1
- package/src/admin/aggregation/reviews/reviews.aggregation.service.ts +689 -689
- package/src/admin/analytics/analytics.admin.service.ts +278 -278
- package/src/admin/analytics/index.ts +2 -2
- package/src/admin/booking/README.md +125 -125
- package/src/admin/booking/booking.admin.ts +1037 -1037
- package/src/admin/booking/booking.calculator.ts +712 -712
- package/src/admin/booking/booking.types.ts +59 -59
- package/src/admin/booking/index.ts +3 -3
- package/src/admin/booking/timezones-problem.md +185 -185
- package/src/admin/calendar/README.md +7 -7
- package/src/admin/calendar/calendar.admin.service.ts +345 -345
- package/src/admin/calendar/index.ts +1 -1
- package/src/admin/documentation-templates/document-manager.admin.ts +260 -260
- package/src/admin/documentation-templates/index.ts +1 -1
- package/src/admin/free-consultation/free-consultation-utils.admin.ts +148 -148
- package/src/admin/free-consultation/index.ts +1 -1
- package/src/admin/index.ts +81 -81
- package/src/admin/logger/index.ts +78 -78
- package/src/admin/mailing/README.md +95 -95
- package/src/admin/mailing/appointment/appointment.mailing.service.ts +732 -732
- package/src/admin/mailing/appointment/index.ts +1 -1
- package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -40
- package/src/admin/mailing/base.mailing.service.ts +208 -208
- package/src/admin/mailing/index.ts +3 -3
- package/src/admin/mailing/practitionerInvite/existing-practitioner-invite.mailing.ts +611 -611
- package/src/admin/mailing/practitionerInvite/index.ts +2 -2
- package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +395 -395
- package/src/admin/mailing/practitionerInvite/templates/existing-practitioner-invitation.template.ts +155 -155
- package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -101
- package/src/admin/mailing/practitionerInvite/templates/invite-accepted-notification.template.ts +228 -228
- package/src/admin/mailing/practitionerInvite/templates/invite-rejected-notification.template.ts +242 -242
- package/src/admin/notifications/index.ts +1 -1
- package/src/admin/notifications/notifications.admin.ts +710 -710
- package/src/admin/requirements/README.md +128 -128
- package/src/admin/requirements/index.ts +1 -1
- package/src/admin/requirements/patient-requirements.admin.service.ts +475 -475
- package/src/admin/users/index.ts +1 -1
- package/src/admin/users/user-profile.admin.ts +405 -405
- package/src/backoffice/constants/certification.constants.ts +13 -13
- package/src/backoffice/constants/index.ts +1 -1
- package/src/backoffice/errors/backoffice.errors.ts +181 -181
- package/src/backoffice/errors/index.ts +1 -1
- package/src/backoffice/expo-safe/README.md +26 -26
- package/src/backoffice/expo-safe/index.ts +41 -41
- package/src/backoffice/index.ts +5 -5
- package/src/backoffice/services/FIXES_README.md +102 -102
- package/src/backoffice/services/README.md +57 -57
- package/src/backoffice/services/analytics.service.proposal.md +863 -863
- package/src/backoffice/services/analytics.service.summary.md +143 -143
- package/src/backoffice/services/brand.service.ts +256 -256
- package/src/backoffice/services/category.service.ts +384 -384
- package/src/backoffice/services/constants.service.ts +385 -385
- package/src/backoffice/services/documentation-template.service.ts +202 -202
- package/src/backoffice/services/index.ts +10 -10
- package/src/backoffice/services/migrate-products.ts +116 -116
- package/src/backoffice/services/product.service.ts +553 -553
- package/src/backoffice/services/requirement.service.ts +235 -235
- package/src/backoffice/services/subcategory.service.ts +461 -461
- package/src/backoffice/services/technology.service.ts +1151 -1151
- package/src/backoffice/types/README.md +12 -12
- package/src/backoffice/types/admin-constants.types.ts +69 -69
- package/src/backoffice/types/brand.types.ts +29 -29
- package/src/backoffice/types/category.types.ts +67 -67
- package/src/backoffice/types/documentation-templates.types.ts +28 -28
- package/src/backoffice/types/index.ts +10 -10
- package/src/backoffice/types/procedure-product.types.ts +38 -38
- package/src/backoffice/types/product.types.ts +240 -240
- package/src/backoffice/types/requirement.types.ts +63 -63
- package/src/backoffice/types/static/README.md +18 -18
- package/src/backoffice/types/static/blocking-condition.types.ts +21 -21
- package/src/backoffice/types/static/certification.types.ts +37 -37
- package/src/backoffice/types/static/contraindication.types.ts +19 -19
- package/src/backoffice/types/static/index.ts +6 -6
- package/src/backoffice/types/static/pricing.types.ts +16 -16
- package/src/backoffice/types/static/procedure-family.types.ts +14 -14
- package/src/backoffice/types/static/treatment-benefit.types.ts +22 -22
- package/src/backoffice/types/subcategory.types.ts +34 -34
- package/src/backoffice/types/technology.types.ts +168 -168
- package/src/backoffice/validations/index.ts +1 -1
- package/src/backoffice/validations/schemas.ts +164 -164
- package/src/config/__mocks__/firebase.ts +99 -99
- package/src/config/firebase.ts +78 -78
- package/src/config/index.ts +9 -9
- package/src/errors/auth.error.ts +6 -6
- package/src/errors/auth.errors.ts +200 -200
- package/src/errors/clinic.errors.ts +32 -32
- package/src/errors/firebase.errors.ts +47 -47
- package/src/errors/user.errors.ts +99 -99
- package/src/index.backup.ts +407 -407
- package/src/index.ts +6 -6
- package/src/locales/en.ts +31 -31
- package/src/recommender/admin/index.ts +1 -1
- package/src/recommender/admin/services/recommender.service.admin.ts +5 -5
- package/src/recommender/front/index.ts +1 -1
- package/src/recommender/front/services/onboarding.service.ts +5 -5
- package/src/recommender/front/services/recommender.service.ts +3 -3
- package/src/recommender/index.ts +1 -1
- package/src/services/PATIENTAUTH.MD +197 -197
- package/src/services/README.md +106 -106
- package/src/services/__tests__/auth/auth.mock.test.ts +17 -17
- package/src/services/__tests__/auth/auth.setup.ts +293 -293
- package/src/services/__tests__/auth.service.test.ts +346 -346
- package/src/services/__tests__/base.service.test.ts +77 -77
- package/src/services/__tests__/user.service.test.ts +528 -528
- package/src/services/analytics/ARCHITECTURE.md +199 -199
- package/src/services/analytics/CLOUD_FUNCTIONS.md +225 -225
- package/src/services/analytics/GROUPED_ANALYTICS.md +501 -501
- package/src/services/analytics/QUICK_START.md +393 -393
- package/src/services/analytics/README.md +304 -304
- package/src/services/analytics/SUMMARY.md +141 -141
- package/src/services/analytics/TRENDS.md +380 -380
- package/src/services/analytics/USAGE_GUIDE.md +518 -518
- package/src/services/analytics/analytics-cloud.service.ts +222 -222
- package/src/services/analytics/analytics.service.ts +2142 -2142
- package/src/services/analytics/index.ts +4 -4
- package/src/services/analytics/review-analytics.service.ts +941 -941
- package/src/services/analytics/utils/appointment-filtering.utils.ts +138 -138
- package/src/services/analytics/utils/cost-calculation.utils.ts +182 -182
- package/src/services/analytics/utils/grouping.utils.ts +434 -434
- package/src/services/analytics/utils/stored-analytics.utils.ts +347 -347
- package/src/services/analytics/utils/time-calculation.utils.ts +186 -186
- package/src/services/analytics/utils/trend-calculation.utils.ts +200 -200
- package/src/services/appointment/README.md +17 -17
- package/src/services/appointment/appointment.service.ts +2558 -2558
- package/src/services/appointment/index.ts +1 -1
- package/src/services/appointment/utils/appointment.utils.ts +552 -552
- package/src/services/appointment/utils/extended-procedure.utils.ts +314 -314
- package/src/services/appointment/utils/form-initialization.utils.ts +225 -225
- package/src/services/appointment/utils/recommended-procedure.utils.ts +195 -195
- package/src/services/appointment/utils/zone-management.utils.ts +353 -353
- package/src/services/appointment/utils/zone-photo.utils.ts +152 -152
- package/src/services/auth/auth.service.ts +989 -989
- package/src/services/auth/auth.v2.service.ts +961 -961
- package/src/services/auth/index.ts +7 -7
- package/src/services/auth/utils/error.utils.ts +90 -90
- package/src/services/auth/utils/firebase.utils.ts +49 -49
- package/src/services/auth/utils/index.ts +21 -21
- package/src/services/auth/utils/practitioner.utils.ts +125 -125
- package/src/services/base.service.ts +41 -41
- package/src/services/calendar/calendar.service.ts +1077 -1077
- package/src/services/calendar/calendar.v2.service.ts +1683 -1683
- package/src/services/calendar/calendar.v3.service.ts +313 -313
- package/src/services/calendar/externalCalendar.service.ts +178 -178
- package/src/services/calendar/index.ts +5 -5
- package/src/services/calendar/synced-calendars.service.ts +743 -743
- package/src/services/calendar/utils/appointment.utils.ts +265 -265
- package/src/services/calendar/utils/calendar-event.utils.ts +646 -646
- package/src/services/calendar/utils/clinic.utils.ts +237 -237
- package/src/services/calendar/utils/docs.utils.ts +157 -157
- package/src/services/calendar/utils/google-calendar.utils.ts +697 -697
- package/src/services/calendar/utils/index.ts +8 -8
- package/src/services/calendar/utils/patient.utils.ts +198 -198
- package/src/services/calendar/utils/practitioner.utils.ts +221 -221
- package/src/services/calendar/utils/synced-calendar.utils.ts +472 -472
- package/src/services/clinic/README.md +204 -204
- package/src/services/clinic/__tests__/clinic-admin.service.test.ts +287 -287
- package/src/services/clinic/__tests__/clinic-group.service.test.ts +352 -352
- package/src/services/clinic/__tests__/clinic.service.test.ts +354 -354
- package/src/services/clinic/billing-transactions.service.ts +217 -217
- package/src/services/clinic/clinic-admin.service.ts +202 -202
- package/src/services/clinic/clinic-group.service.ts +310 -310
- package/src/services/clinic/clinic.service.ts +708 -708
- package/src/services/clinic/index.ts +5 -5
- package/src/services/clinic/practitioner-invite.service.ts +519 -519
- package/src/services/clinic/utils/admin.utils.ts +551 -551
- package/src/services/clinic/utils/clinic-group.utils.ts +646 -646
- package/src/services/clinic/utils/clinic.utils.ts +949 -949
- package/src/services/clinic/utils/filter.utils.d.ts +23 -23
- package/src/services/clinic/utils/filter.utils.ts +446 -446
- package/src/services/clinic/utils/index.ts +11 -11
- package/src/services/clinic/utils/photos.utils.ts +188 -188
- package/src/services/clinic/utils/search.utils.ts +84 -84
- package/src/services/clinic/utils/tag.utils.ts +124 -124
- package/src/services/documentation-templates/documentation-template.service.ts +537 -537
- package/src/services/documentation-templates/filled-document.service.ts +587 -587
- package/src/services/documentation-templates/index.ts +2 -2
- package/src/services/index.ts +14 -14
- package/src/services/media/index.ts +1 -1
- package/src/services/media/media.service.ts +418 -418
- package/src/services/notifications/__tests__/notification.service.test.ts +242 -242
- package/src/services/notifications/index.ts +1 -1
- package/src/services/notifications/notification.service.ts +215 -215
- package/src/services/patient/README.md +48 -48
- package/src/services/patient/To-Do.md +43 -43
- package/src/services/patient/__tests__/patient.service.test.ts +294 -294
- package/src/services/patient/index.ts +2 -2
- package/src/services/patient/patient.service.ts +883 -883
- package/src/services/patient/patientRequirements.service.ts +285 -285
- package/src/services/patient/utils/aesthetic-analysis.utils.ts +176 -176
- package/src/services/patient/utils/clinic.utils.ts +80 -80
- package/src/services/patient/utils/docs.utils.ts +142 -142
- package/src/services/patient/utils/index.ts +9 -9
- package/src/services/patient/utils/location.utils.ts +126 -126
- package/src/services/patient/utils/medical-stuff.utils.ts +143 -143
- package/src/services/patient/utils/medical.utils.ts +458 -458
- package/src/services/patient/utils/practitioner.utils.ts +260 -260
- package/src/services/patient/utils/profile.utils.ts +510 -510
- package/src/services/patient/utils/sensitive.utils.ts +260 -260
- package/src/services/patient/utils/token.utils.ts +211 -211
- package/src/services/practitioner/README.md +145 -145
- package/src/services/practitioner/index.ts +1 -1
- package/src/services/practitioner/practitioner.service.ts +1742 -1742
- package/src/services/procedure/README.md +163 -163
- package/src/services/procedure/index.ts +1 -1
- package/src/services/procedure/procedure.service.ts +2200 -2200
- package/src/services/reviews/index.ts +1 -1
- package/src/services/reviews/reviews.service.ts +734 -734
- package/src/services/user/index.ts +1 -1
- package/src/services/user/user.service.ts +489 -489
- package/src/services/user/user.v2.service.ts +466 -466
- package/src/types/analytics/analytics.types.ts +597 -597
- package/src/types/analytics/grouped-analytics.types.ts +173 -173
- package/src/types/analytics/index.ts +4 -4
- package/src/types/analytics/stored-analytics.types.ts +137 -137
- package/src/types/appointment/index.ts +480 -480
- package/src/types/calendar/index.ts +258 -258
- package/src/types/calendar/synced-calendar.types.ts +66 -66
- package/src/types/clinic/index.ts +498 -498
- package/src/types/clinic/practitioner-invite.types.ts +91 -91
- package/src/types/clinic/preferences.types.ts +159 -159
- package/src/types/clinic/to-do +3 -3
- package/src/types/documentation-templates/index.ts +308 -308
- package/src/types/index.ts +47 -47
- package/src/types/notifications/README.md +77 -77
- package/src/types/notifications/index.ts +286 -286
- package/src/types/patient/aesthetic-analysis.types.ts +66 -66
- package/src/types/patient/allergies.ts +58 -58
- package/src/types/patient/index.ts +275 -275
- package/src/types/patient/medical-info.types.ts +152 -152
- package/src/types/patient/patient-requirements.ts +92 -92
- package/src/types/patient/token.types.ts +61 -61
- package/src/types/practitioner/index.ts +206 -206
- package/src/types/procedure/index.ts +181 -181
- package/src/types/profile/index.ts +39 -39
- package/src/types/reviews/index.ts +132 -132
- package/src/types/tz-lookup.d.ts +4 -4
- package/src/types/user/index.ts +38 -38
- package/src/utils/TIMESTAMPS.md +176 -176
- package/src/utils/TimestampUtils.ts +241 -241
- package/src/utils/index.ts +1 -1
- package/src/validations/appointment.schema.ts +574 -574
- package/src/validations/calendar.schema.ts +225 -225
- package/src/validations/clinic.schema.ts +494 -494
- package/src/validations/common.schema.ts +25 -25
- package/src/validations/documentation-templates/index.ts +1 -1
- package/src/validations/documentation-templates/template.schema.ts +220 -220
- package/src/validations/documentation-templates.schema.ts +10 -10
- package/src/validations/index.ts +20 -20
- package/src/validations/media.schema.ts +10 -10
- package/src/validations/notification.schema.ts +90 -90
- package/src/validations/patient/aesthetic-analysis.schema.ts +55 -55
- package/src/validations/patient/medical-info.schema.ts +125 -125
- package/src/validations/patient/patient-requirements.schema.ts +84 -84
- package/src/validations/patient/token.schema.ts +29 -29
- package/src/validations/patient.schema.ts +217 -217
- package/src/validations/practitioner.schema.ts +222 -222
- package/src/validations/procedure-product.schema.ts +41 -41
- package/src/validations/procedure.schema.ts +124 -124
- package/src/validations/profile-info.schema.ts +41 -41
- package/src/validations/reviews.schema.ts +195 -195
- package/src/validations/schemas.ts +104 -104
- package/src/validations/shared.schema.ts +78 -78
|
@@ -1,611 +1,611 @@
|
|
|
1
|
-
import * as admin from "firebase-admin";
|
|
2
|
-
import { BaseMailingService } from "../base.mailing.service";
|
|
3
|
-
import { existingPractitionerInvitationTemplate } from "./templates/existing-practitioner-invitation.template";
|
|
4
|
-
import { inviteAcceptedNotificationTemplate } from "./templates/invite-accepted-notification.template";
|
|
5
|
-
import { inviteRejectedNotificationTemplate } from "./templates/invite-rejected-notification.template";
|
|
6
|
-
import { Logger } from "../../logger";
|
|
7
|
-
|
|
8
|
-
// Import specific types and collection constants
|
|
9
|
-
import {
|
|
10
|
-
PractitionerInvite,
|
|
11
|
-
PractitionerInviteStatus,
|
|
12
|
-
} from "../../../types/clinic/practitioner-invite.types";
|
|
13
|
-
import {
|
|
14
|
-
Practitioner,
|
|
15
|
-
PRACTITIONERS_COLLECTION,
|
|
16
|
-
} from "../../../types/practitioner";
|
|
17
|
-
import { Clinic, CLINICS_COLLECTION } from "../../../types/clinic";
|
|
18
|
-
|
|
19
|
-
// Define minimal interface for the mailgun.js client
|
|
20
|
-
interface NewMailgunMessagesAPI {
|
|
21
|
-
create(domain: string, data: any): Promise<any>;
|
|
22
|
-
}
|
|
23
|
-
interface NewMailgunClient {
|
|
24
|
-
messages: NewMailgunMessagesAPI;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Interface for sending practitioner invitation email data
|
|
29
|
-
*/
|
|
30
|
-
export interface ExistingPractitionerInviteEmailData {
|
|
31
|
-
/** The practitioner invite object */
|
|
32
|
-
invite: PractitionerInvite;
|
|
33
|
-
|
|
34
|
-
/** Practitioner details */
|
|
35
|
-
practitioner: {
|
|
36
|
-
firstName: string;
|
|
37
|
-
lastName: string;
|
|
38
|
-
email: string;
|
|
39
|
-
specialties?: string[];
|
|
40
|
-
profileImageUrl?: string | null;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
/** Clinic details */
|
|
44
|
-
clinic: {
|
|
45
|
-
name: string;
|
|
46
|
-
address: string;
|
|
47
|
-
contactEmail: string;
|
|
48
|
-
contactPhone?: string;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
/** URLs for accept/reject actions */
|
|
52
|
-
urls: {
|
|
53
|
-
acceptUrl: string;
|
|
54
|
-
rejectUrl: string;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
/** Configuration options */
|
|
58
|
-
options?: {
|
|
59
|
-
customSubject?: string;
|
|
60
|
-
fromAddress?: string;
|
|
61
|
-
mailgunDomain?: string;
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Interface for clinic admin notification email data
|
|
67
|
-
*/
|
|
68
|
-
export interface ClinicAdminNotificationData {
|
|
69
|
-
/** The practitioner invite object */
|
|
70
|
-
invite: PractitionerInvite;
|
|
71
|
-
|
|
72
|
-
/** Practitioner details */
|
|
73
|
-
practitioner: {
|
|
74
|
-
firstName: string;
|
|
75
|
-
lastName: string;
|
|
76
|
-
specialties?: string[];
|
|
77
|
-
profileImageUrl?: string | null;
|
|
78
|
-
experienceYears?: number;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
/** Clinic details */
|
|
82
|
-
clinic: {
|
|
83
|
-
name: string;
|
|
84
|
-
adminName?: string;
|
|
85
|
-
adminEmail: string;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
/** Additional context */
|
|
89
|
-
context: {
|
|
90
|
-
invitationDate: string;
|
|
91
|
-
responseDate: string;
|
|
92
|
-
rejectionReason?: string;
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
/** URLs for dashboard access */
|
|
96
|
-
urls: {
|
|
97
|
-
clinicDashboardUrl: string;
|
|
98
|
-
practitionerProfileUrl?: string;
|
|
99
|
-
findPractitionersUrl?: string;
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
/** Configuration options */
|
|
103
|
-
options?: {
|
|
104
|
-
customSubject?: string;
|
|
105
|
-
fromAddress?: string;
|
|
106
|
-
mailgunDomain?: string;
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Service for sending existing practitioner invitation emails
|
|
112
|
-
* This service handles the email flow for inviting active practitioners to join clinics
|
|
113
|
-
*/
|
|
114
|
-
export class ExistingPractitionerInviteMailingService extends BaseMailingService {
|
|
115
|
-
private readonly DEFAULT_MAILGUN_DOMAIN = "mg.metaesthetics.net";
|
|
116
|
-
private readonly DEFAULT_FROM_ADDRESS =
|
|
117
|
-
"MetaEstetics <no-reply@mg.metaesthetics.net>";
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Constructor for ExistingPractitionerInviteMailingService
|
|
121
|
-
* @param firestore Firestore instance provided by the caller
|
|
122
|
-
* @param mailgunClient Mailgun client instance (mailgun.js v10+) provided by the caller
|
|
123
|
-
*/
|
|
124
|
-
constructor(
|
|
125
|
-
firestore: FirebaseFirestore.Firestore,
|
|
126
|
-
mailgunClient: NewMailgunClient
|
|
127
|
-
) {
|
|
128
|
-
super(firestore, mailgunClient);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Sends an invitation email to an existing practitioner
|
|
133
|
-
* @param data The invitation email data
|
|
134
|
-
* @returns Promise resolved when email is sent
|
|
135
|
-
*/
|
|
136
|
-
async sendPractitionerInvitationEmail(
|
|
137
|
-
data: ExistingPractitionerInviteEmailData
|
|
138
|
-
): Promise<any> {
|
|
139
|
-
try {
|
|
140
|
-
Logger.info(
|
|
141
|
-
"[ExistingPractitionerInviteMailingService] Sending invitation email to practitioner",
|
|
142
|
-
data.practitioner.email
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
// Format working hours for display
|
|
146
|
-
const workingHours = this.formatWorkingHours(
|
|
147
|
-
data.invite.proposedWorkingHours
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
// Format expiration date - using createdAt + 30 days as default expiry
|
|
151
|
-
const expiryDate = new Date(data.invite.createdAt.toDate());
|
|
152
|
-
expiryDate.setDate(expiryDate.getDate() + 30); // 30 days from creation
|
|
153
|
-
|
|
154
|
-
const expirationDate = expiryDate.toLocaleDateString("en-US", {
|
|
155
|
-
weekday: "long",
|
|
156
|
-
year: "numeric",
|
|
157
|
-
month: "long",
|
|
158
|
-
day: "numeric",
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
// Practitioner full name
|
|
162
|
-
const practitionerName = `${data.practitioner.firstName} ${data.practitioner.lastName}`;
|
|
163
|
-
|
|
164
|
-
// Template variables
|
|
165
|
-
const templateVariables = {
|
|
166
|
-
clinicName: data.clinic.name,
|
|
167
|
-
practitionerName,
|
|
168
|
-
clinicAddress: data.clinic.address,
|
|
169
|
-
workingHours,
|
|
170
|
-
expirationDate,
|
|
171
|
-
acceptUrl: data.urls.acceptUrl,
|
|
172
|
-
rejectUrl: data.urls.rejectUrl,
|
|
173
|
-
contactEmail: data.clinic.contactEmail,
|
|
174
|
-
contactPhone: data.clinic.contactPhone || "Contact clinic directly",
|
|
175
|
-
currentYear: new Date().getFullYear().toString(),
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
// Render HTML email
|
|
179
|
-
const html = this.renderTemplate(
|
|
180
|
-
existingPractitionerInvitationTemplate,
|
|
181
|
-
templateVariables
|
|
182
|
-
);
|
|
183
|
-
|
|
184
|
-
// Email configuration
|
|
185
|
-
const subject =
|
|
186
|
-
data.options?.customSubject || `Invitation to Join ${data.clinic.name}`;
|
|
187
|
-
const from = data.options?.fromAddress || this.DEFAULT_FROM_ADDRESS;
|
|
188
|
-
const domain = data.options?.mailgunDomain || this.DEFAULT_MAILGUN_DOMAIN;
|
|
189
|
-
|
|
190
|
-
const mailgunData = {
|
|
191
|
-
to: data.practitioner.email,
|
|
192
|
-
from,
|
|
193
|
-
subject,
|
|
194
|
-
html,
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
Logger.info(
|
|
198
|
-
"[ExistingPractitionerInviteMailingService] Sending email with data:",
|
|
199
|
-
{
|
|
200
|
-
domain,
|
|
201
|
-
to: mailgunData.to,
|
|
202
|
-
from: mailgunData.from,
|
|
203
|
-
subject: mailgunData.subject,
|
|
204
|
-
hasHtml: !!mailgunData.html,
|
|
205
|
-
}
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
// Send the email
|
|
209
|
-
const result = await this.sendEmail(domain, mailgunData);
|
|
210
|
-
|
|
211
|
-
// Log success
|
|
212
|
-
await this.logEmailAttempt(
|
|
213
|
-
{
|
|
214
|
-
to: data.practitioner.email,
|
|
215
|
-
subject,
|
|
216
|
-
templateName: "existing_practitioner_invitation",
|
|
217
|
-
},
|
|
218
|
-
true
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
return result;
|
|
222
|
-
} catch (error: any) {
|
|
223
|
-
Logger.error(
|
|
224
|
-
"[ExistingPractitionerInviteMailingService] Error sending practitioner invitation:",
|
|
225
|
-
{
|
|
226
|
-
errorMessage: error.message,
|
|
227
|
-
errorDetails: error.details,
|
|
228
|
-
errorStatus: error.status,
|
|
229
|
-
stack: error.stack,
|
|
230
|
-
}
|
|
231
|
-
);
|
|
232
|
-
|
|
233
|
-
// Log failure
|
|
234
|
-
await this.logEmailAttempt(
|
|
235
|
-
{
|
|
236
|
-
to: data.practitioner.email,
|
|
237
|
-
subject:
|
|
238
|
-
data.options?.customSubject ||
|
|
239
|
-
`Invitation to Join ${data.clinic.name}`,
|
|
240
|
-
templateName: "existing_practitioner_invitation",
|
|
241
|
-
},
|
|
242
|
-
false,
|
|
243
|
-
error
|
|
244
|
-
);
|
|
245
|
-
|
|
246
|
-
throw error;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Sends a notification email to clinic admin when practitioner accepts invitation
|
|
252
|
-
* @param data The notification email data
|
|
253
|
-
* @returns Promise resolved when email is sent
|
|
254
|
-
*/
|
|
255
|
-
async sendAcceptedNotificationEmail(
|
|
256
|
-
data: ClinicAdminNotificationData
|
|
257
|
-
): Promise<any> {
|
|
258
|
-
try {
|
|
259
|
-
Logger.info(
|
|
260
|
-
"[ExistingPractitionerInviteMailingService] Sending acceptance notification to clinic admin",
|
|
261
|
-
data.clinic.adminEmail
|
|
262
|
-
);
|
|
263
|
-
|
|
264
|
-
const practitionerName = `${data.practitioner.firstName} ${data.practitioner.lastName}`;
|
|
265
|
-
const workingHours = this.formatWorkingHours(
|
|
266
|
-
data.invite.proposedWorkingHours
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
// Template variables - ensure all values are strings
|
|
270
|
-
const templateVariables: Record<string, string> = {
|
|
271
|
-
clinicName: data.clinic.name,
|
|
272
|
-
clinicAdminName: data.clinic.adminName || "Admin",
|
|
273
|
-
practitionerName,
|
|
274
|
-
practitionerPhoto: data.practitioner.profileImageUrl || "",
|
|
275
|
-
practitionerSpecialties:
|
|
276
|
-
data.practitioner.specialties?.join(", ") || "",
|
|
277
|
-
practitionerExperience:
|
|
278
|
-
data.practitioner.experienceYears?.toString() || "",
|
|
279
|
-
invitationDate: data.context.invitationDate,
|
|
280
|
-
acceptedDate: data.context.responseDate,
|
|
281
|
-
workingHours,
|
|
282
|
-
clinicDashboardUrl: data.urls.clinicDashboardUrl,
|
|
283
|
-
practitionerProfileUrl: data.urls.practitionerProfileUrl || "#",
|
|
284
|
-
currentYear: new Date().getFullYear().toString(),
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
// Render HTML email
|
|
288
|
-
const html = this.renderTemplate(
|
|
289
|
-
inviteAcceptedNotificationTemplate,
|
|
290
|
-
templateVariables
|
|
291
|
-
);
|
|
292
|
-
|
|
293
|
-
// Email configuration
|
|
294
|
-
const subject =
|
|
295
|
-
data.options?.customSubject ||
|
|
296
|
-
`Great News! Dr. ${practitionerName} Accepted Your Invitation`;
|
|
297
|
-
const from = data.options?.fromAddress || this.DEFAULT_FROM_ADDRESS;
|
|
298
|
-
const domain = data.options?.mailgunDomain || this.DEFAULT_MAILGUN_DOMAIN;
|
|
299
|
-
|
|
300
|
-
const mailgunData = {
|
|
301
|
-
to: data.clinic.adminEmail,
|
|
302
|
-
from,
|
|
303
|
-
subject,
|
|
304
|
-
html,
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
// Send the email
|
|
308
|
-
const result = await this.sendEmail(domain, mailgunData);
|
|
309
|
-
|
|
310
|
-
// Log success
|
|
311
|
-
await this.logEmailAttempt(
|
|
312
|
-
{
|
|
313
|
-
to: data.clinic.adminEmail,
|
|
314
|
-
subject,
|
|
315
|
-
templateName: "invite_accepted_notification",
|
|
316
|
-
},
|
|
317
|
-
true
|
|
318
|
-
);
|
|
319
|
-
|
|
320
|
-
return result;
|
|
321
|
-
} catch (error: any) {
|
|
322
|
-
Logger.error(
|
|
323
|
-
"[ExistingPractitionerInviteMailingService] Error sending acceptance notification:",
|
|
324
|
-
error
|
|
325
|
-
);
|
|
326
|
-
|
|
327
|
-
// Log failure
|
|
328
|
-
await this.logEmailAttempt(
|
|
329
|
-
{
|
|
330
|
-
to: data.clinic.adminEmail,
|
|
331
|
-
subject: data.options?.customSubject || "Invitation Accepted",
|
|
332
|
-
templateName: "invite_accepted_notification",
|
|
333
|
-
},
|
|
334
|
-
false,
|
|
335
|
-
error
|
|
336
|
-
);
|
|
337
|
-
|
|
338
|
-
throw error;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Sends a notification email to clinic admin when practitioner rejects invitation
|
|
344
|
-
* @param data The notification email data
|
|
345
|
-
* @returns Promise resolved when email is sent
|
|
346
|
-
*/
|
|
347
|
-
async sendRejectedNotificationEmail(
|
|
348
|
-
data: ClinicAdminNotificationData
|
|
349
|
-
): Promise<any> {
|
|
350
|
-
try {
|
|
351
|
-
Logger.info(
|
|
352
|
-
"[ExistingPractitionerInviteMailingService] Sending rejection notification to clinic admin",
|
|
353
|
-
data.clinic.adminEmail
|
|
354
|
-
);
|
|
355
|
-
|
|
356
|
-
const practitionerName = `${data.practitioner.firstName} ${data.practitioner.lastName}`;
|
|
357
|
-
|
|
358
|
-
// Template variables - ensure all values are strings
|
|
359
|
-
const templateVariables: Record<string, string> = {
|
|
360
|
-
clinicName: data.clinic.name,
|
|
361
|
-
clinicAdminName: data.clinic.adminName || "Admin",
|
|
362
|
-
practitionerName,
|
|
363
|
-
practitionerPhoto: data.practitioner.profileImageUrl || "",
|
|
364
|
-
practitionerSpecialties:
|
|
365
|
-
data.practitioner.specialties?.join(", ") || "",
|
|
366
|
-
invitationDate: data.context.invitationDate,
|
|
367
|
-
rejectedDate: data.context.responseDate,
|
|
368
|
-
rejectionReason: data.context.rejectionReason || "",
|
|
369
|
-
findPractitionersUrl: data.urls.findPractitionersUrl || "#",
|
|
370
|
-
clinicDashboardUrl: data.urls.clinicDashboardUrl,
|
|
371
|
-
currentYear: new Date().getFullYear().toString(),
|
|
372
|
-
};
|
|
373
|
-
|
|
374
|
-
// Render HTML email
|
|
375
|
-
const html = this.renderTemplate(
|
|
376
|
-
inviteRejectedNotificationTemplate,
|
|
377
|
-
templateVariables
|
|
378
|
-
);
|
|
379
|
-
|
|
380
|
-
// Email configuration
|
|
381
|
-
const subject =
|
|
382
|
-
data.options?.customSubject ||
|
|
383
|
-
`Invitation Update: Dr. ${practitionerName} Declined`;
|
|
384
|
-
const from = data.options?.fromAddress || this.DEFAULT_FROM_ADDRESS;
|
|
385
|
-
const domain = data.options?.mailgunDomain || this.DEFAULT_MAILGUN_DOMAIN;
|
|
386
|
-
|
|
387
|
-
const mailgunData = {
|
|
388
|
-
to: data.clinic.adminEmail,
|
|
389
|
-
from,
|
|
390
|
-
subject,
|
|
391
|
-
html,
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
// Send the email
|
|
395
|
-
const result = await this.sendEmail(domain, mailgunData);
|
|
396
|
-
|
|
397
|
-
// Log success
|
|
398
|
-
await this.logEmailAttempt(
|
|
399
|
-
{
|
|
400
|
-
to: data.clinic.adminEmail,
|
|
401
|
-
subject,
|
|
402
|
-
templateName: "invite_rejected_notification",
|
|
403
|
-
},
|
|
404
|
-
true
|
|
405
|
-
);
|
|
406
|
-
|
|
407
|
-
return result;
|
|
408
|
-
} catch (error: any) {
|
|
409
|
-
Logger.error(
|
|
410
|
-
"[ExistingPractitionerInviteMailingService] Error sending rejection notification:",
|
|
411
|
-
error
|
|
412
|
-
);
|
|
413
|
-
|
|
414
|
-
// Log failure
|
|
415
|
-
await this.logEmailAttempt(
|
|
416
|
-
{
|
|
417
|
-
to: data.clinic.adminEmail,
|
|
418
|
-
subject: data.options?.customSubject || "Invitation Declined",
|
|
419
|
-
templateName: "invite_rejected_notification",
|
|
420
|
-
},
|
|
421
|
-
false,
|
|
422
|
-
error
|
|
423
|
-
);
|
|
424
|
-
|
|
425
|
-
throw error;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
/**
|
|
430
|
-
* Handles the practitioner invite creation event
|
|
431
|
-
* Fetches necessary data and sends the invitation email to the practitioner
|
|
432
|
-
* @param invite The practitioner invite object
|
|
433
|
-
* @param mailgunConfig Mailgun configuration
|
|
434
|
-
* @returns Promise resolved when the email is sent
|
|
435
|
-
*/
|
|
436
|
-
async handleInviteCreationEvent(
|
|
437
|
-
invite: PractitionerInvite,
|
|
438
|
-
mailgunConfig: {
|
|
439
|
-
fromAddress: string;
|
|
440
|
-
domain: string;
|
|
441
|
-
acceptUrl: string;
|
|
442
|
-
rejectUrl: string;
|
|
443
|
-
}
|
|
444
|
-
): Promise<void> {
|
|
445
|
-
try {
|
|
446
|
-
Logger.info(
|
|
447
|
-
"[ExistingPractitionerInviteMailingService] Handling invite creation event for invite:",
|
|
448
|
-
invite.id
|
|
449
|
-
);
|
|
450
|
-
|
|
451
|
-
// Validate invite data
|
|
452
|
-
if (!invite || !invite.id || !invite.practitionerId || !invite.clinicId) {
|
|
453
|
-
throw new Error(
|
|
454
|
-
`Invalid invite data: Missing required properties. Invite ID: ${invite?.id}`
|
|
455
|
-
);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// Only send email for pending invites
|
|
459
|
-
if (invite.status !== PractitionerInviteStatus.PENDING) {
|
|
460
|
-
Logger.info(
|
|
461
|
-
"[ExistingPractitionerInviteMailingService] Invite is not pending, skipping email",
|
|
462
|
-
{ inviteId: invite.id, status: invite.status }
|
|
463
|
-
);
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// Fetch practitioner and clinic data
|
|
468
|
-
const [practitioner, clinic] = await Promise.all([
|
|
469
|
-
this.fetchPractitionerById(invite.practitionerId),
|
|
470
|
-
this.fetchClinicById(invite.clinicId),
|
|
471
|
-
]);
|
|
472
|
-
|
|
473
|
-
if (!practitioner) {
|
|
474
|
-
throw new Error(`Practitioner ${invite.practitionerId} not found`);
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
if (!clinic) {
|
|
478
|
-
throw new Error(`Clinic ${invite.clinicId} not found`);
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// Prepare email data
|
|
482
|
-
const emailData: ExistingPractitionerInviteEmailData = {
|
|
483
|
-
invite,
|
|
484
|
-
practitioner: {
|
|
485
|
-
firstName: practitioner.basicInfo.firstName || "",
|
|
486
|
-
lastName: practitioner.basicInfo.lastName || "",
|
|
487
|
-
email: practitioner.basicInfo.email || "",
|
|
488
|
-
specialties:
|
|
489
|
-
practitioner.certification?.specialties?.map(
|
|
490
|
-
(s: any) => s.name || s
|
|
491
|
-
) || [],
|
|
492
|
-
profileImageUrl:
|
|
493
|
-
typeof practitioner.basicInfo.profileImageUrl === "string"
|
|
494
|
-
? practitioner.basicInfo.profileImageUrl
|
|
495
|
-
: null,
|
|
496
|
-
},
|
|
497
|
-
clinic: {
|
|
498
|
-
name: clinic.name || "Medical Clinic",
|
|
499
|
-
address: this.formatClinicAddress(clinic.location),
|
|
500
|
-
contactEmail: clinic.contactInfo.email || "contact@clinic.com",
|
|
501
|
-
contactPhone: clinic.contactInfo.phoneNumber,
|
|
502
|
-
},
|
|
503
|
-
urls: {
|
|
504
|
-
acceptUrl: mailgunConfig.acceptUrl,
|
|
505
|
-
rejectUrl: mailgunConfig.rejectUrl,
|
|
506
|
-
},
|
|
507
|
-
options: {
|
|
508
|
-
fromAddress: mailgunConfig.fromAddress,
|
|
509
|
-
mailgunDomain: mailgunConfig.domain,
|
|
510
|
-
},
|
|
511
|
-
};
|
|
512
|
-
|
|
513
|
-
// Send the invitation email
|
|
514
|
-
await this.sendPractitionerInvitationEmail(emailData);
|
|
515
|
-
|
|
516
|
-
Logger.info(
|
|
517
|
-
"[ExistingPractitionerInviteMailingService] Invitation email sent successfully"
|
|
518
|
-
);
|
|
519
|
-
} catch (error: any) {
|
|
520
|
-
Logger.error(
|
|
521
|
-
"[ExistingPractitionerInviteMailingService] Error handling invite creation event:",
|
|
522
|
-
{
|
|
523
|
-
errorMessage: error.message,
|
|
524
|
-
errorDetails: error.details,
|
|
525
|
-
errorStatus: error.status,
|
|
526
|
-
stack: error.stack,
|
|
527
|
-
inviteId: invite?.id,
|
|
528
|
-
}
|
|
529
|
-
);
|
|
530
|
-
throw error;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// --- Private Helper Methods ---
|
|
535
|
-
|
|
536
|
-
/**
|
|
537
|
-
* Formats working hours for display in emails
|
|
538
|
-
* @param workingHours The working hours object
|
|
539
|
-
* @returns Formatted string representation
|
|
540
|
-
*/
|
|
541
|
-
private formatWorkingHours(workingHours: any): string {
|
|
542
|
-
if (!workingHours) return "To be determined";
|
|
543
|
-
|
|
544
|
-
// This is a simplified formatter - you may want to enhance based on your actual working hours structure
|
|
545
|
-
return Object.entries(workingHours)
|
|
546
|
-
.map(([day, hours]) => `${day}: ${hours}`)
|
|
547
|
-
.join(", ");
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
/**
|
|
551
|
-
* Formats clinic address for display
|
|
552
|
-
* @param location The clinic location object
|
|
553
|
-
* @returns Formatted address string
|
|
554
|
-
*/
|
|
555
|
-
private formatClinicAddress(location: any): string {
|
|
556
|
-
if (!location) return "Address not specified";
|
|
557
|
-
|
|
558
|
-
const parts = [
|
|
559
|
-
location.street,
|
|
560
|
-
location.city,
|
|
561
|
-
location.state,
|
|
562
|
-
location.country,
|
|
563
|
-
].filter(Boolean);
|
|
564
|
-
|
|
565
|
-
return parts.join(", ");
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
/**
|
|
569
|
-
* Fetches a practitioner by ID
|
|
570
|
-
* @param practitionerId The practitioner ID
|
|
571
|
-
* @returns The practitioner or null if not found
|
|
572
|
-
*/
|
|
573
|
-
private async fetchPractitionerById(
|
|
574
|
-
practitionerId: string
|
|
575
|
-
): Promise<Practitioner | null> {
|
|
576
|
-
try {
|
|
577
|
-
const doc = await this.db
|
|
578
|
-
.collection(PRACTITIONERS_COLLECTION)
|
|
579
|
-
.doc(practitionerId)
|
|
580
|
-
.get();
|
|
581
|
-
return doc.exists ? (doc.data() as Practitioner) : null;
|
|
582
|
-
} catch (error) {
|
|
583
|
-
Logger.error(
|
|
584
|
-
"[ExistingPractitionerInviteMailingService] Error fetching practitioner:",
|
|
585
|
-
error
|
|
586
|
-
);
|
|
587
|
-
return null;
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
/**
|
|
592
|
-
* Fetches a clinic by ID
|
|
593
|
-
* @param clinicId The clinic ID
|
|
594
|
-
* @returns The clinic or null if not found
|
|
595
|
-
*/
|
|
596
|
-
private async fetchClinicById(clinicId: string): Promise<Clinic | null> {
|
|
597
|
-
try {
|
|
598
|
-
const doc = await this.db
|
|
599
|
-
.collection(CLINICS_COLLECTION)
|
|
600
|
-
.doc(clinicId)
|
|
601
|
-
.get();
|
|
602
|
-
return doc.exists ? (doc.data() as Clinic) : null;
|
|
603
|
-
} catch (error) {
|
|
604
|
-
Logger.error(
|
|
605
|
-
"[ExistingPractitionerInviteMailingService] Error fetching clinic:",
|
|
606
|
-
error
|
|
607
|
-
);
|
|
608
|
-
return null;
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
}
|
|
1
|
+
import * as admin from "firebase-admin";
|
|
2
|
+
import { BaseMailingService } from "../base.mailing.service";
|
|
3
|
+
import { existingPractitionerInvitationTemplate } from "./templates/existing-practitioner-invitation.template";
|
|
4
|
+
import { inviteAcceptedNotificationTemplate } from "./templates/invite-accepted-notification.template";
|
|
5
|
+
import { inviteRejectedNotificationTemplate } from "./templates/invite-rejected-notification.template";
|
|
6
|
+
import { Logger } from "../../logger";
|
|
7
|
+
|
|
8
|
+
// Import specific types and collection constants
|
|
9
|
+
import {
|
|
10
|
+
PractitionerInvite,
|
|
11
|
+
PractitionerInviteStatus,
|
|
12
|
+
} from "../../../types/clinic/practitioner-invite.types";
|
|
13
|
+
import {
|
|
14
|
+
Practitioner,
|
|
15
|
+
PRACTITIONERS_COLLECTION,
|
|
16
|
+
} from "../../../types/practitioner";
|
|
17
|
+
import { Clinic, CLINICS_COLLECTION } from "../../../types/clinic";
|
|
18
|
+
|
|
19
|
+
// Define minimal interface for the mailgun.js client
|
|
20
|
+
interface NewMailgunMessagesAPI {
|
|
21
|
+
create(domain: string, data: any): Promise<any>;
|
|
22
|
+
}
|
|
23
|
+
interface NewMailgunClient {
|
|
24
|
+
messages: NewMailgunMessagesAPI;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Interface for sending practitioner invitation email data
|
|
29
|
+
*/
|
|
30
|
+
export interface ExistingPractitionerInviteEmailData {
|
|
31
|
+
/** The practitioner invite object */
|
|
32
|
+
invite: PractitionerInvite;
|
|
33
|
+
|
|
34
|
+
/** Practitioner details */
|
|
35
|
+
practitioner: {
|
|
36
|
+
firstName: string;
|
|
37
|
+
lastName: string;
|
|
38
|
+
email: string;
|
|
39
|
+
specialties?: string[];
|
|
40
|
+
profileImageUrl?: string | null;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Clinic details */
|
|
44
|
+
clinic: {
|
|
45
|
+
name: string;
|
|
46
|
+
address: string;
|
|
47
|
+
contactEmail: string;
|
|
48
|
+
contactPhone?: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** URLs for accept/reject actions */
|
|
52
|
+
urls: {
|
|
53
|
+
acceptUrl: string;
|
|
54
|
+
rejectUrl: string;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** Configuration options */
|
|
58
|
+
options?: {
|
|
59
|
+
customSubject?: string;
|
|
60
|
+
fromAddress?: string;
|
|
61
|
+
mailgunDomain?: string;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Interface for clinic admin notification email data
|
|
67
|
+
*/
|
|
68
|
+
export interface ClinicAdminNotificationData {
|
|
69
|
+
/** The practitioner invite object */
|
|
70
|
+
invite: PractitionerInvite;
|
|
71
|
+
|
|
72
|
+
/** Practitioner details */
|
|
73
|
+
practitioner: {
|
|
74
|
+
firstName: string;
|
|
75
|
+
lastName: string;
|
|
76
|
+
specialties?: string[];
|
|
77
|
+
profileImageUrl?: string | null;
|
|
78
|
+
experienceYears?: number;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** Clinic details */
|
|
82
|
+
clinic: {
|
|
83
|
+
name: string;
|
|
84
|
+
adminName?: string;
|
|
85
|
+
adminEmail: string;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/** Additional context */
|
|
89
|
+
context: {
|
|
90
|
+
invitationDate: string;
|
|
91
|
+
responseDate: string;
|
|
92
|
+
rejectionReason?: string;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** URLs for dashboard access */
|
|
96
|
+
urls: {
|
|
97
|
+
clinicDashboardUrl: string;
|
|
98
|
+
practitionerProfileUrl?: string;
|
|
99
|
+
findPractitionersUrl?: string;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** Configuration options */
|
|
103
|
+
options?: {
|
|
104
|
+
customSubject?: string;
|
|
105
|
+
fromAddress?: string;
|
|
106
|
+
mailgunDomain?: string;
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Service for sending existing practitioner invitation emails
|
|
112
|
+
* This service handles the email flow for inviting active practitioners to join clinics
|
|
113
|
+
*/
|
|
114
|
+
export class ExistingPractitionerInviteMailingService extends BaseMailingService {
|
|
115
|
+
private readonly DEFAULT_MAILGUN_DOMAIN = "mg.metaesthetics.net";
|
|
116
|
+
private readonly DEFAULT_FROM_ADDRESS =
|
|
117
|
+
"MetaEstetics <no-reply@mg.metaesthetics.net>";
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Constructor for ExistingPractitionerInviteMailingService
|
|
121
|
+
* @param firestore Firestore instance provided by the caller
|
|
122
|
+
* @param mailgunClient Mailgun client instance (mailgun.js v10+) provided by the caller
|
|
123
|
+
*/
|
|
124
|
+
constructor(
|
|
125
|
+
firestore: FirebaseFirestore.Firestore,
|
|
126
|
+
mailgunClient: NewMailgunClient
|
|
127
|
+
) {
|
|
128
|
+
super(firestore, mailgunClient);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Sends an invitation email to an existing practitioner
|
|
133
|
+
* @param data The invitation email data
|
|
134
|
+
* @returns Promise resolved when email is sent
|
|
135
|
+
*/
|
|
136
|
+
async sendPractitionerInvitationEmail(
|
|
137
|
+
data: ExistingPractitionerInviteEmailData
|
|
138
|
+
): Promise<any> {
|
|
139
|
+
try {
|
|
140
|
+
Logger.info(
|
|
141
|
+
"[ExistingPractitionerInviteMailingService] Sending invitation email to practitioner",
|
|
142
|
+
data.practitioner.email
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// Format working hours for display
|
|
146
|
+
const workingHours = this.formatWorkingHours(
|
|
147
|
+
data.invite.proposedWorkingHours
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// Format expiration date - using createdAt + 30 days as default expiry
|
|
151
|
+
const expiryDate = new Date(data.invite.createdAt.toDate());
|
|
152
|
+
expiryDate.setDate(expiryDate.getDate() + 30); // 30 days from creation
|
|
153
|
+
|
|
154
|
+
const expirationDate = expiryDate.toLocaleDateString("en-US", {
|
|
155
|
+
weekday: "long",
|
|
156
|
+
year: "numeric",
|
|
157
|
+
month: "long",
|
|
158
|
+
day: "numeric",
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Practitioner full name
|
|
162
|
+
const practitionerName = `${data.practitioner.firstName} ${data.practitioner.lastName}`;
|
|
163
|
+
|
|
164
|
+
// Template variables
|
|
165
|
+
const templateVariables = {
|
|
166
|
+
clinicName: data.clinic.name,
|
|
167
|
+
practitionerName,
|
|
168
|
+
clinicAddress: data.clinic.address,
|
|
169
|
+
workingHours,
|
|
170
|
+
expirationDate,
|
|
171
|
+
acceptUrl: data.urls.acceptUrl,
|
|
172
|
+
rejectUrl: data.urls.rejectUrl,
|
|
173
|
+
contactEmail: data.clinic.contactEmail,
|
|
174
|
+
contactPhone: data.clinic.contactPhone || "Contact clinic directly",
|
|
175
|
+
currentYear: new Date().getFullYear().toString(),
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// Render HTML email
|
|
179
|
+
const html = this.renderTemplate(
|
|
180
|
+
existingPractitionerInvitationTemplate,
|
|
181
|
+
templateVariables
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// Email configuration
|
|
185
|
+
const subject =
|
|
186
|
+
data.options?.customSubject || `Invitation to Join ${data.clinic.name}`;
|
|
187
|
+
const from = data.options?.fromAddress || this.DEFAULT_FROM_ADDRESS;
|
|
188
|
+
const domain = data.options?.mailgunDomain || this.DEFAULT_MAILGUN_DOMAIN;
|
|
189
|
+
|
|
190
|
+
const mailgunData = {
|
|
191
|
+
to: data.practitioner.email,
|
|
192
|
+
from,
|
|
193
|
+
subject,
|
|
194
|
+
html,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
Logger.info(
|
|
198
|
+
"[ExistingPractitionerInviteMailingService] Sending email with data:",
|
|
199
|
+
{
|
|
200
|
+
domain,
|
|
201
|
+
to: mailgunData.to,
|
|
202
|
+
from: mailgunData.from,
|
|
203
|
+
subject: mailgunData.subject,
|
|
204
|
+
hasHtml: !!mailgunData.html,
|
|
205
|
+
}
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Send the email
|
|
209
|
+
const result = await this.sendEmail(domain, mailgunData);
|
|
210
|
+
|
|
211
|
+
// Log success
|
|
212
|
+
await this.logEmailAttempt(
|
|
213
|
+
{
|
|
214
|
+
to: data.practitioner.email,
|
|
215
|
+
subject,
|
|
216
|
+
templateName: "existing_practitioner_invitation",
|
|
217
|
+
},
|
|
218
|
+
true
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
return result;
|
|
222
|
+
} catch (error: any) {
|
|
223
|
+
Logger.error(
|
|
224
|
+
"[ExistingPractitionerInviteMailingService] Error sending practitioner invitation:",
|
|
225
|
+
{
|
|
226
|
+
errorMessage: error.message,
|
|
227
|
+
errorDetails: error.details,
|
|
228
|
+
errorStatus: error.status,
|
|
229
|
+
stack: error.stack,
|
|
230
|
+
}
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
// Log failure
|
|
234
|
+
await this.logEmailAttempt(
|
|
235
|
+
{
|
|
236
|
+
to: data.practitioner.email,
|
|
237
|
+
subject:
|
|
238
|
+
data.options?.customSubject ||
|
|
239
|
+
`Invitation to Join ${data.clinic.name}`,
|
|
240
|
+
templateName: "existing_practitioner_invitation",
|
|
241
|
+
},
|
|
242
|
+
false,
|
|
243
|
+
error
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Sends a notification email to clinic admin when practitioner accepts invitation
|
|
252
|
+
* @param data The notification email data
|
|
253
|
+
* @returns Promise resolved when email is sent
|
|
254
|
+
*/
|
|
255
|
+
async sendAcceptedNotificationEmail(
|
|
256
|
+
data: ClinicAdminNotificationData
|
|
257
|
+
): Promise<any> {
|
|
258
|
+
try {
|
|
259
|
+
Logger.info(
|
|
260
|
+
"[ExistingPractitionerInviteMailingService] Sending acceptance notification to clinic admin",
|
|
261
|
+
data.clinic.adminEmail
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const practitionerName = `${data.practitioner.firstName} ${data.practitioner.lastName}`;
|
|
265
|
+
const workingHours = this.formatWorkingHours(
|
|
266
|
+
data.invite.proposedWorkingHours
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
// Template variables - ensure all values are strings
|
|
270
|
+
const templateVariables: Record<string, string> = {
|
|
271
|
+
clinicName: data.clinic.name,
|
|
272
|
+
clinicAdminName: data.clinic.adminName || "Admin",
|
|
273
|
+
practitionerName,
|
|
274
|
+
practitionerPhoto: data.practitioner.profileImageUrl || "",
|
|
275
|
+
practitionerSpecialties:
|
|
276
|
+
data.practitioner.specialties?.join(", ") || "",
|
|
277
|
+
practitionerExperience:
|
|
278
|
+
data.practitioner.experienceYears?.toString() || "",
|
|
279
|
+
invitationDate: data.context.invitationDate,
|
|
280
|
+
acceptedDate: data.context.responseDate,
|
|
281
|
+
workingHours,
|
|
282
|
+
clinicDashboardUrl: data.urls.clinicDashboardUrl,
|
|
283
|
+
practitionerProfileUrl: data.urls.practitionerProfileUrl || "#",
|
|
284
|
+
currentYear: new Date().getFullYear().toString(),
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// Render HTML email
|
|
288
|
+
const html = this.renderTemplate(
|
|
289
|
+
inviteAcceptedNotificationTemplate,
|
|
290
|
+
templateVariables
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
// Email configuration
|
|
294
|
+
const subject =
|
|
295
|
+
data.options?.customSubject ||
|
|
296
|
+
`Great News! Dr. ${practitionerName} Accepted Your Invitation`;
|
|
297
|
+
const from = data.options?.fromAddress || this.DEFAULT_FROM_ADDRESS;
|
|
298
|
+
const domain = data.options?.mailgunDomain || this.DEFAULT_MAILGUN_DOMAIN;
|
|
299
|
+
|
|
300
|
+
const mailgunData = {
|
|
301
|
+
to: data.clinic.adminEmail,
|
|
302
|
+
from,
|
|
303
|
+
subject,
|
|
304
|
+
html,
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// Send the email
|
|
308
|
+
const result = await this.sendEmail(domain, mailgunData);
|
|
309
|
+
|
|
310
|
+
// Log success
|
|
311
|
+
await this.logEmailAttempt(
|
|
312
|
+
{
|
|
313
|
+
to: data.clinic.adminEmail,
|
|
314
|
+
subject,
|
|
315
|
+
templateName: "invite_accepted_notification",
|
|
316
|
+
},
|
|
317
|
+
true
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
return result;
|
|
321
|
+
} catch (error: any) {
|
|
322
|
+
Logger.error(
|
|
323
|
+
"[ExistingPractitionerInviteMailingService] Error sending acceptance notification:",
|
|
324
|
+
error
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
// Log failure
|
|
328
|
+
await this.logEmailAttempt(
|
|
329
|
+
{
|
|
330
|
+
to: data.clinic.adminEmail,
|
|
331
|
+
subject: data.options?.customSubject || "Invitation Accepted",
|
|
332
|
+
templateName: "invite_accepted_notification",
|
|
333
|
+
},
|
|
334
|
+
false,
|
|
335
|
+
error
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Sends a notification email to clinic admin when practitioner rejects invitation
|
|
344
|
+
* @param data The notification email data
|
|
345
|
+
* @returns Promise resolved when email is sent
|
|
346
|
+
*/
|
|
347
|
+
async sendRejectedNotificationEmail(
|
|
348
|
+
data: ClinicAdminNotificationData
|
|
349
|
+
): Promise<any> {
|
|
350
|
+
try {
|
|
351
|
+
Logger.info(
|
|
352
|
+
"[ExistingPractitionerInviteMailingService] Sending rejection notification to clinic admin",
|
|
353
|
+
data.clinic.adminEmail
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
const practitionerName = `${data.practitioner.firstName} ${data.practitioner.lastName}`;
|
|
357
|
+
|
|
358
|
+
// Template variables - ensure all values are strings
|
|
359
|
+
const templateVariables: Record<string, string> = {
|
|
360
|
+
clinicName: data.clinic.name,
|
|
361
|
+
clinicAdminName: data.clinic.adminName || "Admin",
|
|
362
|
+
practitionerName,
|
|
363
|
+
practitionerPhoto: data.practitioner.profileImageUrl || "",
|
|
364
|
+
practitionerSpecialties:
|
|
365
|
+
data.practitioner.specialties?.join(", ") || "",
|
|
366
|
+
invitationDate: data.context.invitationDate,
|
|
367
|
+
rejectedDate: data.context.responseDate,
|
|
368
|
+
rejectionReason: data.context.rejectionReason || "",
|
|
369
|
+
findPractitionersUrl: data.urls.findPractitionersUrl || "#",
|
|
370
|
+
clinicDashboardUrl: data.urls.clinicDashboardUrl,
|
|
371
|
+
currentYear: new Date().getFullYear().toString(),
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// Render HTML email
|
|
375
|
+
const html = this.renderTemplate(
|
|
376
|
+
inviteRejectedNotificationTemplate,
|
|
377
|
+
templateVariables
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
// Email configuration
|
|
381
|
+
const subject =
|
|
382
|
+
data.options?.customSubject ||
|
|
383
|
+
`Invitation Update: Dr. ${practitionerName} Declined`;
|
|
384
|
+
const from = data.options?.fromAddress || this.DEFAULT_FROM_ADDRESS;
|
|
385
|
+
const domain = data.options?.mailgunDomain || this.DEFAULT_MAILGUN_DOMAIN;
|
|
386
|
+
|
|
387
|
+
const mailgunData = {
|
|
388
|
+
to: data.clinic.adminEmail,
|
|
389
|
+
from,
|
|
390
|
+
subject,
|
|
391
|
+
html,
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// Send the email
|
|
395
|
+
const result = await this.sendEmail(domain, mailgunData);
|
|
396
|
+
|
|
397
|
+
// Log success
|
|
398
|
+
await this.logEmailAttempt(
|
|
399
|
+
{
|
|
400
|
+
to: data.clinic.adminEmail,
|
|
401
|
+
subject,
|
|
402
|
+
templateName: "invite_rejected_notification",
|
|
403
|
+
},
|
|
404
|
+
true
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
return result;
|
|
408
|
+
} catch (error: any) {
|
|
409
|
+
Logger.error(
|
|
410
|
+
"[ExistingPractitionerInviteMailingService] Error sending rejection notification:",
|
|
411
|
+
error
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
// Log failure
|
|
415
|
+
await this.logEmailAttempt(
|
|
416
|
+
{
|
|
417
|
+
to: data.clinic.adminEmail,
|
|
418
|
+
subject: data.options?.customSubject || "Invitation Declined",
|
|
419
|
+
templateName: "invite_rejected_notification",
|
|
420
|
+
},
|
|
421
|
+
false,
|
|
422
|
+
error
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
throw error;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Handles the practitioner invite creation event
|
|
431
|
+
* Fetches necessary data and sends the invitation email to the practitioner
|
|
432
|
+
* @param invite The practitioner invite object
|
|
433
|
+
* @param mailgunConfig Mailgun configuration
|
|
434
|
+
* @returns Promise resolved when the email is sent
|
|
435
|
+
*/
|
|
436
|
+
async handleInviteCreationEvent(
|
|
437
|
+
invite: PractitionerInvite,
|
|
438
|
+
mailgunConfig: {
|
|
439
|
+
fromAddress: string;
|
|
440
|
+
domain: string;
|
|
441
|
+
acceptUrl: string;
|
|
442
|
+
rejectUrl: string;
|
|
443
|
+
}
|
|
444
|
+
): Promise<void> {
|
|
445
|
+
try {
|
|
446
|
+
Logger.info(
|
|
447
|
+
"[ExistingPractitionerInviteMailingService] Handling invite creation event for invite:",
|
|
448
|
+
invite.id
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
// Validate invite data
|
|
452
|
+
if (!invite || !invite.id || !invite.practitionerId || !invite.clinicId) {
|
|
453
|
+
throw new Error(
|
|
454
|
+
`Invalid invite data: Missing required properties. Invite ID: ${invite?.id}`
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Only send email for pending invites
|
|
459
|
+
if (invite.status !== PractitionerInviteStatus.PENDING) {
|
|
460
|
+
Logger.info(
|
|
461
|
+
"[ExistingPractitionerInviteMailingService] Invite is not pending, skipping email",
|
|
462
|
+
{ inviteId: invite.id, status: invite.status }
|
|
463
|
+
);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Fetch practitioner and clinic data
|
|
468
|
+
const [practitioner, clinic] = await Promise.all([
|
|
469
|
+
this.fetchPractitionerById(invite.practitionerId),
|
|
470
|
+
this.fetchClinicById(invite.clinicId),
|
|
471
|
+
]);
|
|
472
|
+
|
|
473
|
+
if (!practitioner) {
|
|
474
|
+
throw new Error(`Practitioner ${invite.practitionerId} not found`);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (!clinic) {
|
|
478
|
+
throw new Error(`Clinic ${invite.clinicId} not found`);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Prepare email data
|
|
482
|
+
const emailData: ExistingPractitionerInviteEmailData = {
|
|
483
|
+
invite,
|
|
484
|
+
practitioner: {
|
|
485
|
+
firstName: practitioner.basicInfo.firstName || "",
|
|
486
|
+
lastName: practitioner.basicInfo.lastName || "",
|
|
487
|
+
email: practitioner.basicInfo.email || "",
|
|
488
|
+
specialties:
|
|
489
|
+
practitioner.certification?.specialties?.map(
|
|
490
|
+
(s: any) => s.name || s
|
|
491
|
+
) || [],
|
|
492
|
+
profileImageUrl:
|
|
493
|
+
typeof practitioner.basicInfo.profileImageUrl === "string"
|
|
494
|
+
? practitioner.basicInfo.profileImageUrl
|
|
495
|
+
: null,
|
|
496
|
+
},
|
|
497
|
+
clinic: {
|
|
498
|
+
name: clinic.name || "Medical Clinic",
|
|
499
|
+
address: this.formatClinicAddress(clinic.location),
|
|
500
|
+
contactEmail: clinic.contactInfo.email || "contact@clinic.com",
|
|
501
|
+
contactPhone: clinic.contactInfo.phoneNumber,
|
|
502
|
+
},
|
|
503
|
+
urls: {
|
|
504
|
+
acceptUrl: mailgunConfig.acceptUrl,
|
|
505
|
+
rejectUrl: mailgunConfig.rejectUrl,
|
|
506
|
+
},
|
|
507
|
+
options: {
|
|
508
|
+
fromAddress: mailgunConfig.fromAddress,
|
|
509
|
+
mailgunDomain: mailgunConfig.domain,
|
|
510
|
+
},
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// Send the invitation email
|
|
514
|
+
await this.sendPractitionerInvitationEmail(emailData);
|
|
515
|
+
|
|
516
|
+
Logger.info(
|
|
517
|
+
"[ExistingPractitionerInviteMailingService] Invitation email sent successfully"
|
|
518
|
+
);
|
|
519
|
+
} catch (error: any) {
|
|
520
|
+
Logger.error(
|
|
521
|
+
"[ExistingPractitionerInviteMailingService] Error handling invite creation event:",
|
|
522
|
+
{
|
|
523
|
+
errorMessage: error.message,
|
|
524
|
+
errorDetails: error.details,
|
|
525
|
+
errorStatus: error.status,
|
|
526
|
+
stack: error.stack,
|
|
527
|
+
inviteId: invite?.id,
|
|
528
|
+
}
|
|
529
|
+
);
|
|
530
|
+
throw error;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// --- Private Helper Methods ---
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Formats working hours for display in emails
|
|
538
|
+
* @param workingHours The working hours object
|
|
539
|
+
* @returns Formatted string representation
|
|
540
|
+
*/
|
|
541
|
+
private formatWorkingHours(workingHours: any): string {
|
|
542
|
+
if (!workingHours) return "To be determined";
|
|
543
|
+
|
|
544
|
+
// This is a simplified formatter - you may want to enhance based on your actual working hours structure
|
|
545
|
+
return Object.entries(workingHours)
|
|
546
|
+
.map(([day, hours]) => `${day}: ${hours}`)
|
|
547
|
+
.join(", ");
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Formats clinic address for display
|
|
552
|
+
* @param location The clinic location object
|
|
553
|
+
* @returns Formatted address string
|
|
554
|
+
*/
|
|
555
|
+
private formatClinicAddress(location: any): string {
|
|
556
|
+
if (!location) return "Address not specified";
|
|
557
|
+
|
|
558
|
+
const parts = [
|
|
559
|
+
location.street,
|
|
560
|
+
location.city,
|
|
561
|
+
location.state,
|
|
562
|
+
location.country,
|
|
563
|
+
].filter(Boolean);
|
|
564
|
+
|
|
565
|
+
return parts.join(", ");
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Fetches a practitioner by ID
|
|
570
|
+
* @param practitionerId The practitioner ID
|
|
571
|
+
* @returns The practitioner or null if not found
|
|
572
|
+
*/
|
|
573
|
+
private async fetchPractitionerById(
|
|
574
|
+
practitionerId: string
|
|
575
|
+
): Promise<Practitioner | null> {
|
|
576
|
+
try {
|
|
577
|
+
const doc = await this.db
|
|
578
|
+
.collection(PRACTITIONERS_COLLECTION)
|
|
579
|
+
.doc(practitionerId)
|
|
580
|
+
.get();
|
|
581
|
+
return doc.exists ? (doc.data() as Practitioner) : null;
|
|
582
|
+
} catch (error) {
|
|
583
|
+
Logger.error(
|
|
584
|
+
"[ExistingPractitionerInviteMailingService] Error fetching practitioner:",
|
|
585
|
+
error
|
|
586
|
+
);
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Fetches a clinic by ID
|
|
593
|
+
* @param clinicId The clinic ID
|
|
594
|
+
* @returns The clinic or null if not found
|
|
595
|
+
*/
|
|
596
|
+
private async fetchClinicById(clinicId: string): Promise<Clinic | null> {
|
|
597
|
+
try {
|
|
598
|
+
const doc = await this.db
|
|
599
|
+
.collection(CLINICS_COLLECTION)
|
|
600
|
+
.doc(clinicId)
|
|
601
|
+
.get();
|
|
602
|
+
return doc.exists ? (doc.data() as Clinic) : null;
|
|
603
|
+
} catch (error) {
|
|
604
|
+
Logger.error(
|
|
605
|
+
"[ExistingPractitionerInviteMailingService] Error fetching clinic:",
|
|
606
|
+
error
|
|
607
|
+
);
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|