@blackcode_sa/metaestetics-api 1.6.4 → 1.6.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.
Files changed (30) hide show
  1. package/dist/admin/index.d.mts +236 -2
  2. package/dist/admin/index.d.ts +236 -2
  3. package/dist/admin/index.js +11251 -10447
  4. package/dist/admin/index.mjs +11251 -10447
  5. package/dist/backoffice/index.d.mts +2 -0
  6. package/dist/backoffice/index.d.ts +2 -0
  7. package/dist/index.d.mts +50 -77
  8. package/dist/index.d.ts +50 -77
  9. package/dist/index.js +77 -305
  10. package/dist/index.mjs +78 -306
  11. package/package.json +1 -1
  12. package/src/admin/aggregation/appointment/README.md +128 -0
  13. package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1053 -0
  14. package/src/admin/booking/README.md +125 -0
  15. package/src/admin/booking/booking.admin.ts +638 -3
  16. package/src/admin/calendar/calendar.admin.service.ts +183 -0
  17. package/src/admin/documentation-templates/document-manager.admin.ts +131 -0
  18. package/src/admin/mailing/appointment/appointment.mailing.service.ts +264 -0
  19. package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -0
  20. package/src/admin/mailing/base.mailing.service.ts +1 -1
  21. package/src/admin/mailing/index.ts +2 -0
  22. package/src/admin/notifications/notifications.admin.ts +397 -1
  23. package/src/backoffice/types/product.types.ts +2 -0
  24. package/src/services/appointment/appointment.service.ts +89 -182
  25. package/src/services/procedure/procedure.service.ts +1 -0
  26. package/src/types/appointment/index.ts +3 -1
  27. package/src/types/notifications/index.ts +4 -2
  28. package/src/types/procedure/index.ts +7 -0
  29. package/src/validations/appointment.schema.ts +2 -3
  30. package/src/validations/procedure.schema.ts +3 -0
@@ -0,0 +1,183 @@
1
+ import * as admin from "firebase-admin";
2
+ import { Appointment, APPOINTMENTS_COLLECTION } from "../../types/appointment";
3
+ import {
4
+ CalendarEventStatus,
5
+ CalendarEventTime,
6
+ CALENDAR_COLLECTION,
7
+ } from "../../types/calendar";
8
+ import { Timestamp as FirebaseClientTimestamp } from "@firebase/firestore";
9
+ import { Logger } from "../logger";
10
+ import { PRACTITIONERS_COLLECTION } from "../../types/practitioner";
11
+ import { PATIENTS_COLLECTION } from "../../types/patient";
12
+ import { CLINICS_COLLECTION } from "../../types/clinic";
13
+
14
+ /**
15
+ * @class CalendarAdminService
16
+ * @description Handles administrative tasks for calendar events linked to appointments,
17
+ * such as status updates, time changes, or deletions, subsequent to their initial creation.
18
+ */
19
+ export class CalendarAdminService {
20
+ private db: admin.firestore.Firestore;
21
+
22
+ constructor(firestore?: admin.firestore.Firestore) {
23
+ this.db = firestore || admin.firestore();
24
+ Logger.info("[CalendarAdminService] Initialized.");
25
+ }
26
+
27
+ /**
28
+ * Updates the status of all three calendar events (practitioner, patient, clinic)
29
+ * associated with a given appointment.
30
+ *
31
+ * @param appointment - The appointment object containing references to its calendar events.
32
+ * @param newStatus - The new CalendarEventStatus to set.
33
+ * @returns {Promise<void>} A promise that resolves when all updates are attempted.
34
+ */
35
+ async updateAppointmentCalendarEventsStatus(
36
+ appointment: Appointment,
37
+ newStatus: CalendarEventStatus
38
+ ): Promise<void> {
39
+ Logger.info(
40
+ `[CalendarAdminService] Updating calendar event statuses for appointment ${appointment.id} to ${newStatus}`
41
+ );
42
+ const batch = this.db.batch();
43
+ const serverTimestamp = admin.firestore.FieldValue.serverTimestamp();
44
+
45
+ // Note: The Appointment type in booking.admin.ts has `calendarEventId` (practitioner's event)
46
+ // but not direct IDs for patient and clinic calendar events.
47
+ // We need a robust way to find these if they aren't directly on the appointment object.
48
+ // For now, assuming we can query them or they are added to the Appointment type.
49
+
50
+ // Let's assume the main appointment.calendarEventId is the practitioner's calendar event.
51
+ // The other calendar event IDs might need to be fetched or stored on the Appointment object.
52
+ // For this placeholder, we'll demonstrate updating one and note the others.
53
+
54
+ // TODO: Confirm how patient and clinic calendar event IDs are retrieved.
55
+ // Assuming they are named predictably or stored in appointment.additionalCalendarEventIds (example)
56
+
57
+ const practitionerCalendarEventPath = `${PRACTITIONERS_COLLECTION}/${appointment.practitionerId}/${CALENDAR_COLLECTION}/${appointment.calendarEventId}`;
58
+ // Example paths, these need to be accurate based on your data model:
59
+ // const patientCalendarEventPath = `${PATIENTS_COLLECTION}/${appointment.patientId}/${CALENDAR_COLLECTION}/${appointment.patientCalendarEventId}`;
60
+ // const clinicCalendarEventPath = `${CLINICS_COLLECTION}/${appointment.clinicBranchId}/${CALENDAR_COLLECTION}/${appointment.clinicCalendarEventId}`;
61
+
62
+ if (appointment.calendarEventId) {
63
+ // Practitioner event
64
+ const practitionerEventRef = this.db.doc(practitionerCalendarEventPath);
65
+ batch.update(practitionerEventRef, {
66
+ status: newStatus,
67
+ updatedAt: serverTimestamp,
68
+ });
69
+ }
70
+ // Add similar updates for patient and clinic calendar events once their refs are confirmed
71
+ // if (appointment.patientCalendarEventId) { ... }
72
+ // if (appointment.clinicCalendarEventId) { ... }
73
+
74
+ try {
75
+ await batch.commit();
76
+ Logger.info(
77
+ `[CalendarAdminService] Successfully updated calendar event statuses for appointment ${appointment.id}.`
78
+ );
79
+ } catch (error) {
80
+ Logger.error(
81
+ `[CalendarAdminService] Error updating calendar event statuses for appointment ${appointment.id}:`,
82
+ error
83
+ );
84
+ // Decide on error handling: re-throw, or log and continue?
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Updates the eventTime (start and end) of all three calendar events
90
+ * associated with a given appointment.
91
+ *
92
+ * @param appointment - The appointment object.
93
+ * @param newEventTime - The new CalendarEventTime object (using FirebaseClientTimestamp).
94
+ * @returns {Promise<void>} A promise that resolves when all updates are attempted.
95
+ */
96
+ async updateAppointmentCalendarEventsTime(
97
+ appointment: Appointment,
98
+ newEventTime: CalendarEventTime // Expecting { start: FirebaseClientTimestamp, end: FirebaseClientTimestamp }
99
+ ): Promise<void> {
100
+ Logger.info(
101
+ `[CalendarAdminService] Updating calendar event times for appointment ${appointment.id}`
102
+ );
103
+ const batch = this.db.batch();
104
+ const serverTimestamp = admin.firestore.FieldValue.serverTimestamp();
105
+
106
+ // Convert FirebaseClientTimestamp to a plain object for Firestore admin SDK if needed,
107
+ // or ensure the CalendarEventTime type is directly compatible.
108
+ // Firestore admin SDK usually handles { seconds: X, nanoseconds: Y } objects correctly.
109
+ const firestoreCompatibleEventTime = {
110
+ start: {
111
+ seconds: newEventTime.start.seconds,
112
+ nanoseconds: newEventTime.start.nanoseconds,
113
+ },
114
+ end: {
115
+ seconds: newEventTime.end.seconds,
116
+ nanoseconds: newEventTime.end.nanoseconds,
117
+ },
118
+ };
119
+
120
+ // TODO: Confirm paths as in updateAppointmentCalendarEventsStatus
121
+ if (appointment.calendarEventId) {
122
+ // Practitioner event
123
+ const practitionerEventRef = this.db.doc(
124
+ `${PRACTITIONERS_COLLECTION}/${appointment.practitionerId}/${CALENDAR_COLLECTION}/${appointment.calendarEventId}`
125
+ );
126
+ batch.update(practitionerEventRef, {
127
+ eventTime: firestoreCompatibleEventTime,
128
+ updatedAt: serverTimestamp,
129
+ });
130
+ }
131
+ // Add similar updates for patient and clinic calendar events
132
+
133
+ try {
134
+ await batch.commit();
135
+ Logger.info(
136
+ `[CalendarAdminService] Successfully updated calendar event times for appointment ${appointment.id}.`
137
+ );
138
+ } catch (error) {
139
+ Logger.error(
140
+ `[CalendarAdminService] Error updating calendar event times for appointment ${appointment.id}:`,
141
+ error
142
+ );
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Deletes all three calendar events associated with a given appointment.
148
+ * Note: This is a hard delete. Consider marking as CANCELLED instead if soft delete is preferred.
149
+ *
150
+ * @param appointment - The appointment object.
151
+ * @returns {Promise<void>} A promise that resolves when all deletions are attempted.
152
+ */
153
+ async deleteAppointmentCalendarEvents(
154
+ appointment: Appointment
155
+ ): Promise<void> {
156
+ Logger.info(
157
+ `[CalendarAdminService] Deleting calendar events for appointment ${appointment.id}`
158
+ );
159
+ const batch = this.db.batch();
160
+
161
+ // TODO: Confirm paths as in updateAppointmentCalendarEventsStatus
162
+ if (appointment.calendarEventId) {
163
+ // Practitioner event
164
+ const practitionerEventRef = this.db.doc(
165
+ `${PRACTITIONERS_COLLECTION}/${appointment.practitionerId}/${CALENDAR_COLLECTION}/${appointment.calendarEventId}`
166
+ );
167
+ batch.delete(practitionerEventRef);
168
+ }
169
+ // Add similar deletes for patient and clinic calendar events
170
+
171
+ try {
172
+ await batch.commit();
173
+ Logger.info(
174
+ `[CalendarAdminService] Successfully deleted calendar events for appointment ${appointment.id}.`
175
+ );
176
+ } catch (error) {
177
+ Logger.error(
178
+ `[CalendarAdminService] Error deleting calendar events for appointment ${appointment.id}:`,
179
+ error
180
+ );
181
+ }
182
+ }
183
+ }
@@ -0,0 +1,131 @@
1
+ import * as admin from "firebase-admin";
2
+ import {
3
+ DocumentTemplate,
4
+ FilledDocument,
5
+ FilledDocumentStatus,
6
+ USER_FORMS_SUBCOLLECTION,
7
+ DOCTOR_FORMS_SUBCOLLECTION,
8
+ } from "../../types/documentation-templates";
9
+ import {
10
+ APPOINTMENTS_COLLECTION,
11
+ LinkedFormInfo,
12
+ } from "../../types/appointment";
13
+
14
+ export interface InitializeAppointmentFormsResult {
15
+ initializedFormsInfo: LinkedFormInfo[];
16
+ pendingUserFormsIds: string[];
17
+ allLinkedTemplateIds: string[];
18
+ }
19
+
20
+ export class DocumentManagerAdminService {
21
+ private db: admin.firestore.Firestore;
22
+
23
+ constructor(firestore: admin.firestore.Firestore) {
24
+ this.db = firestore;
25
+ }
26
+
27
+ /**
28
+ * Adds operations to a Firestore batch to initialize all linked forms for a new appointment
29
+ * and returns an array of LinkedFormInfo objects, pendingUserFormsIds, and allLinkedTemplateIds.
30
+ *
31
+ * @param dbBatch - The Firestore batch to add operations to.
32
+ * @param appointmentId - The ID of the newly created appointment.
33
+ * @param procedureIdForForms - The ID of the procedure associated with this appointment.
34
+ * @param procedureTemplates - Array of document templates linked to the procedure.
35
+ * @param patientId - ID of the patient.
36
+ * @param practitionerId - ID of the practitioner associated with the procedure.
37
+ * @param clinicId - ID of the clinic where the procedure is performed.
38
+ * @param nowMillis - Current timestamp in milliseconds for createdAt/updatedAt.
39
+ * @returns An object containing initializedFormsInfo, pendingUserFormsIds, and allLinkedTemplateIds.
40
+ */
41
+ batchInitializeAppointmentForms(
42
+ dbBatch: admin.firestore.WriteBatch,
43
+ appointmentId: string,
44
+ procedureIdForForms: string,
45
+ procedureTemplates: DocumentTemplate[],
46
+ patientId: string,
47
+ practitionerId: string,
48
+ clinicId: string,
49
+ nowMillis: number
50
+ ): InitializeAppointmentFormsResult {
51
+ const initializedFormsInfo: LinkedFormInfo[] = [];
52
+ const pendingUserFormsIds: string[] = [];
53
+ const allLinkedTemplateIds: string[] = [];
54
+
55
+ if (!procedureTemplates || procedureTemplates.length === 0) {
56
+ console.log(
57
+ `[DocManagerAdmin] No document templates to initialize for appointment ${appointmentId}.`
58
+ );
59
+ return {
60
+ initializedFormsInfo,
61
+ pendingUserFormsIds,
62
+ allLinkedTemplateIds,
63
+ };
64
+ }
65
+
66
+ for (const template of procedureTemplates) {
67
+ const isUserForm = template.isUserForm || false;
68
+
69
+ const formSubcollectionPath = isUserForm
70
+ ? USER_FORMS_SUBCOLLECTION
71
+ : DOCTOR_FORMS_SUBCOLLECTION;
72
+
73
+ const filledDocumentId = this.db
74
+ .collection(APPOINTMENTS_COLLECTION)
75
+ .doc(appointmentId)
76
+ .collection(formSubcollectionPath)
77
+ .doc().id;
78
+
79
+ if (isUserForm && (template.isRequired || false)) {
80
+ pendingUserFormsIds.push(filledDocumentId);
81
+ }
82
+
83
+ allLinkedTemplateIds.push(filledDocumentId);
84
+ // Always initialize with PENDING status regardless of whether the form is required
85
+ // PENDING is the starting state, DRAFT is when a form is saved but not submitted
86
+ const initialStatus = FilledDocumentStatus.PENDING;
87
+
88
+ const filledDocumentData: FilledDocument = {
89
+ id: filledDocumentId,
90
+ templateId: template.id,
91
+ templateVersion: template.version,
92
+ isUserForm: isUserForm,
93
+ isRequired: template.isRequired || false,
94
+ appointmentId: appointmentId,
95
+ procedureId: procedureIdForForms,
96
+ patientId: patientId,
97
+ practitionerId: practitionerId,
98
+ clinicId: clinicId,
99
+ createdAt: nowMillis,
100
+ updatedAt: nowMillis,
101
+ values: {},
102
+ status: initialStatus,
103
+ };
104
+
105
+ const docRef = this.db
106
+ .collection(APPOINTMENTS_COLLECTION)
107
+ .doc(appointmentId)
108
+ .collection(formSubcollectionPath)
109
+ .doc(filledDocumentId);
110
+
111
+ dbBatch.set(docRef, filledDocumentData);
112
+
113
+ const linkedForm: LinkedFormInfo = {
114
+ formId: filledDocumentData.id,
115
+ templateId: template.id,
116
+ templateVersion: template.version,
117
+ title: template.title,
118
+ isUserForm: filledDocumentData.isUserForm,
119
+ isRequired: filledDocumentData.isRequired,
120
+ status: filledDocumentData.status,
121
+ path: docRef.path,
122
+ };
123
+ initializedFormsInfo.push(linkedForm);
124
+
125
+ console.log(
126
+ `[DocManagerAdmin] Added FilledDocument ${filledDocumentId} (template: ${template.id}) and its LinkedFormInfo to batch for appointment ${appointmentId}.`
127
+ );
128
+ }
129
+ return { initializedFormsInfo, pendingUserFormsIds, allLinkedTemplateIds };
130
+ }
131
+ }
@@ -0,0 +1,264 @@
1
+ import * as admin from "firebase-admin";
2
+ import { BaseMailingService, NewMailgunClient } from "../base.mailing.service";
3
+ import { Logger } from "../../logger";
4
+ import { Appointment } from "../../../types/appointment";
5
+ import { PatientProfile } from "../../../types/patient";
6
+ import { Practitioner } from "../../../types/practitioner";
7
+ import { Clinic } from "../../../types/clinic";
8
+ import { UserRole } from "../../../types";
9
+ import {
10
+ PractitionerProfileInfo,
11
+ PatientProfileInfo,
12
+ ClinicInfo,
13
+ } from "../../../types/profile";
14
+
15
+ // Simple string literals for placeholders, to be replaced by file loading later
16
+ const patientAppointmentConfirmedTemplate =
17
+ "<h1>Appointment Confirmed</h1><p>Dear {{patientName}},</p><p>Your appointment for {{procedureName}} on {{appointmentDate}} at {{appointmentTime}} with {{practitionerName}} at {{clinicName}} has been confirmed.</p><p>Thank you!</p>";
18
+ const clinicAppointmentRequestedTemplate =
19
+ "<h1>New Appointment Request</h1><p>Hello {{clinicName}} Admin,</p><p>A new appointment for {{procedureName}} has been requested by {{patientName}} for {{appointmentDate}} at {{appointmentTime}} with {{practitionerName}}.</p><p>Please review and confirm in the admin panel.</p>";
20
+
21
+ // --- Interface Definitions for Email Data ---
22
+
23
+ export interface AppointmentEmailDataBase {
24
+ appointment: Appointment;
25
+ options?: {
26
+ customSubject?: string;
27
+ fromAddress?: string;
28
+ mailgunDomain?: string;
29
+ };
30
+ }
31
+
32
+ export interface AppointmentConfirmationEmailData
33
+ extends AppointmentEmailDataBase {
34
+ recipientProfile: PatientProfileInfo | PractitionerProfileInfo;
35
+ recipientRole: "patient" | "practitioner";
36
+ }
37
+
38
+ export interface AppointmentRequestedEmailData
39
+ extends AppointmentEmailDataBase {
40
+ clinicProfile: ClinicInfo;
41
+ }
42
+
43
+ export interface AppointmentCancellationEmailData
44
+ extends AppointmentEmailDataBase {
45
+ recipientProfile: PatientProfileInfo | PractitionerProfileInfo;
46
+ recipientRole: "patient" | "practitioner";
47
+ cancellationReason?: string;
48
+ }
49
+
50
+ export interface AppointmentRescheduledProposalEmailData
51
+ extends AppointmentEmailDataBase {
52
+ patientProfile: PatientProfileInfo;
53
+ previousStartTime: admin.firestore.Timestamp;
54
+ previousEndTime: admin.firestore.Timestamp;
55
+ }
56
+
57
+ export interface ReviewRequestEmailData extends AppointmentEmailDataBase {
58
+ patientProfile: PatientProfileInfo;
59
+ reviewLink: string;
60
+ }
61
+
62
+ export interface ReviewAddedEmailData extends AppointmentEmailDataBase {
63
+ recipientProfile: PractitionerProfileInfo | ClinicInfo;
64
+ recipientRole: "practitioner" | "clinic";
65
+ reviewerName: string;
66
+ reviewRating: number;
67
+ reviewComment?: string;
68
+ }
69
+
70
+ /**
71
+ * Service for sending appointment-related emails.
72
+ */
73
+ export class AppointmentMailingService extends BaseMailingService {
74
+ private readonly DEFAULT_MAILGUN_DOMAIN = "mg.metaesthetics.net";
75
+
76
+ constructor(
77
+ firestore: admin.firestore.Firestore,
78
+ mailgunClient: NewMailgunClient
79
+ ) {
80
+ super(firestore, mailgunClient);
81
+ Logger.info("[AppointmentMailingService] Initialized.");
82
+ }
83
+
84
+ async sendAppointmentConfirmedEmail(
85
+ data: AppointmentConfirmationEmailData
86
+ ): Promise<any> {
87
+ Logger.info(
88
+ `[AppointmentMailingService] Preparing to send appointment confirmation email to ${data.recipientRole}: ${data.recipientProfile.id}`
89
+ );
90
+
91
+ const recipientEmail = data.recipientProfile.email;
92
+
93
+ if (!recipientEmail) {
94
+ Logger.error(
95
+ "[AppointmentMailingService] Recipient email not found for confirmation.",
96
+ { recipientId: data.recipientProfile.id, role: data.recipientRole }
97
+ );
98
+ throw new Error("Recipient email address is missing.");
99
+ }
100
+
101
+ const templateVariables = {
102
+ patientName: data.appointment.patientInfo.fullName,
103
+ procedureName: data.appointment.procedureInfo.name,
104
+ appointmentDate: data.appointment.appointmentStartTime
105
+ .toDate()
106
+ .toLocaleDateString(),
107
+ appointmentTime: data.appointment.appointmentStartTime
108
+ .toDate()
109
+ .toLocaleTimeString(),
110
+ practitionerName: data.appointment.practitionerInfo.name,
111
+ clinicName: data.appointment.clinicInfo.name,
112
+ };
113
+
114
+ const html = this.renderTemplate(
115
+ patientAppointmentConfirmedTemplate,
116
+ templateVariables
117
+ );
118
+ const subject =
119
+ data.options?.customSubject || "Your Appointment is Confirmed!";
120
+ const fromAddress =
121
+ data.options?.fromAddress ||
122
+ `MetaEstetics <no-reply@${
123
+ data.options?.mailgunDomain || this.DEFAULT_MAILGUN_DOMAIN
124
+ }>`;
125
+ const domainToSendFrom =
126
+ data.options?.mailgunDomain || this.DEFAULT_MAILGUN_DOMAIN;
127
+
128
+ const mailgunSendData = {
129
+ to: recipientEmail,
130
+ from: fromAddress,
131
+ subject,
132
+ html,
133
+ };
134
+
135
+ try {
136
+ const result = await this.sendEmail(domainToSendFrom, mailgunSendData);
137
+ await this.logEmailAttempt(
138
+ { to: recipientEmail, subject, templateName: "appointment_confirmed" },
139
+ true
140
+ );
141
+ return result;
142
+ } catch (error) {
143
+ await this.logEmailAttempt(
144
+ {
145
+ to: recipientEmail,
146
+ subject:
147
+ data.options?.customSubject || "Your Appointment is Confirmed!",
148
+ templateName: "appointment_confirmed",
149
+ },
150
+ false,
151
+ error
152
+ );
153
+ throw error;
154
+ }
155
+ }
156
+
157
+ async sendAppointmentRequestedEmailToClinic(
158
+ data: AppointmentRequestedEmailData
159
+ ): Promise<any> {
160
+ Logger.info(
161
+ `[AppointmentMailingService] Preparing to send appointment requested email to clinic: ${data.clinicProfile.id}`
162
+ );
163
+
164
+ const clinicEmail = data.clinicProfile.contactInfo?.email;
165
+ if (!clinicEmail) {
166
+ Logger.error(
167
+ "[AppointmentMailingService] Clinic contact email not found for request notification.",
168
+ { clinicId: data.clinicProfile.id }
169
+ );
170
+ throw new Error("Clinic contact email address is missing.");
171
+ }
172
+
173
+ const templateVariables = {
174
+ clinicName: data.clinicProfile.name,
175
+ patientName: data.appointment.patientInfo.fullName,
176
+ procedureName: data.appointment.procedureInfo.name,
177
+ appointmentDate: data.appointment.appointmentStartTime
178
+ .toDate()
179
+ .toLocaleDateString(),
180
+ appointmentTime: data.appointment.appointmentStartTime
181
+ .toDate()
182
+ .toLocaleTimeString(),
183
+ practitionerName: data.appointment.practitionerInfo.name,
184
+ };
185
+
186
+ const html = this.renderTemplate(
187
+ clinicAppointmentRequestedTemplate,
188
+ templateVariables
189
+ );
190
+ const subject =
191
+ data.options?.customSubject || "New Appointment Request Received";
192
+ const fromAddress =
193
+ data.options?.fromAddress ||
194
+ `MetaEstetics <no-reply@${
195
+ data.options?.mailgunDomain || this.DEFAULT_MAILGUN_DOMAIN
196
+ }>`;
197
+ const domainToSendFrom =
198
+ data.options?.mailgunDomain || this.DEFAULT_MAILGUN_DOMAIN;
199
+
200
+ const mailgunSendData = {
201
+ to: clinicEmail,
202
+ from: fromAddress,
203
+ subject,
204
+ html,
205
+ };
206
+
207
+ try {
208
+ const result = await this.sendEmail(domainToSendFrom, mailgunSendData);
209
+ await this.logEmailAttempt(
210
+ {
211
+ to: clinicEmail,
212
+ subject,
213
+ templateName: "appointment_requested_clinic",
214
+ },
215
+ true
216
+ );
217
+ return result;
218
+ } catch (error) {
219
+ await this.logEmailAttempt(
220
+ {
221
+ to: clinicEmail,
222
+ subject:
223
+ data.options?.customSubject || "New Appointment Request Received",
224
+ templateName: "appointment_requested_clinic",
225
+ },
226
+ false,
227
+ error
228
+ );
229
+ throw error;
230
+ }
231
+ }
232
+
233
+ async sendAppointmentCancelledEmail(
234
+ data: AppointmentCancellationEmailData
235
+ ): Promise<any> {
236
+ Logger.info(
237
+ `[AppointmentMailingService] Placeholder for sendAppointmentCancelledEmail for ${data.recipientRole}: ${data.recipientProfile.id}`
238
+ );
239
+ return Promise.resolve();
240
+ }
241
+
242
+ async sendAppointmentRescheduledProposalEmail(
243
+ data: AppointmentRescheduledProposalEmailData
244
+ ): Promise<any> {
245
+ Logger.info(
246
+ `[AppointmentMailingService] Placeholder for sendAppointmentRescheduledProposalEmail to patient: ${data.patientProfile.id}`
247
+ );
248
+ return Promise.resolve();
249
+ }
250
+
251
+ async sendReviewRequestEmail(data: ReviewRequestEmailData): Promise<any> {
252
+ Logger.info(
253
+ `[AppointmentMailingService] Placeholder for sendReviewRequestEmail to patient: ${data.patientProfile.id}`
254
+ );
255
+ return Promise.resolve();
256
+ }
257
+
258
+ async sendReviewAddedEmail(data: ReviewAddedEmailData): Promise<any> {
259
+ Logger.info(
260
+ `[AppointmentMailingService] Placeholder for sendReviewAddedEmail to ${data.recipientRole}: ${data.recipientProfile.id}`
261
+ );
262
+ return Promise.resolve();
263
+ }
264
+ }
@@ -0,0 +1,40 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Appointment Confirmed</title>
7
+ <style>
8
+ body {
9
+ font-family: Arial, sans-serif;
10
+ margin: 0;
11
+ padding: 20px;
12
+ color: #333;
13
+ }
14
+ .container {
15
+ background-color: #f9f9f9;
16
+ padding: 20px;
17
+ border-radius: 5px;
18
+ }
19
+ h1 {
20
+ color: #4caf50;
21
+ }
22
+ </style>
23
+ </head>
24
+ <body>
25
+ <div class="container">
26
+ <h1>Appointment Confirmed!</h1>
27
+ <p>Dear {{patientName}},</p>
28
+ <p>
29
+ This is a placeholder email to confirm your appointment for
30
+ <strong>{{procedureName}}</strong>.
31
+ </p>
32
+ <p>Date: {{appointmentDate}}</p>
33
+ <p>Time: {{appointmentTime}}</p>
34
+ <p>With: {{practitionerName}}</p>
35
+ <p>At: {{clinicName}}</p>
36
+ <p>We look forward to seeing you!</p>
37
+ <p>Sincerely,<br />The {{clinicName}} Team</p>
38
+ </div>
39
+ </body>
40
+ </html>
@@ -14,7 +14,7 @@ interface NewMailgunMessagesAPI {
14
14
  create(domain: string, data: any): Promise<any>;
15
15
  }
16
16
 
17
- interface NewMailgunClient {
17
+ export interface NewMailgunClient {
18
18
  messages: NewMailgunMessagesAPI;
19
19
  // Add other methods/properties if your BaseMailingService uses them
20
20
  }
@@ -1,2 +1,4 @@
1
1
  export * from "./base.mailing.service";
2
2
  export * from "./practitionerInvite";
3
+ export * from "./practitionerInvite/practitionerInvite.mailing";
4
+ export * from "./appointment/appointment.mailing.service";