@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.
- package/dist/admin/index.d.mts +236 -2
- package/dist/admin/index.d.ts +236 -2
- package/dist/admin/index.js +11251 -10447
- package/dist/admin/index.mjs +11251 -10447
- package/dist/backoffice/index.d.mts +2 -0
- package/dist/backoffice/index.d.ts +2 -0
- package/dist/index.d.mts +50 -77
- package/dist/index.d.ts +50 -77
- package/dist/index.js +77 -305
- package/dist/index.mjs +78 -306
- package/package.json +1 -1
- package/src/admin/aggregation/appointment/README.md +128 -0
- package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1053 -0
- package/src/admin/booking/README.md +125 -0
- package/src/admin/booking/booking.admin.ts +638 -3
- package/src/admin/calendar/calendar.admin.service.ts +183 -0
- package/src/admin/documentation-templates/document-manager.admin.ts +131 -0
- package/src/admin/mailing/appointment/appointment.mailing.service.ts +264 -0
- package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -0
- package/src/admin/mailing/base.mailing.service.ts +1 -1
- package/src/admin/mailing/index.ts +2 -0
- package/src/admin/notifications/notifications.admin.ts +397 -1
- package/src/backoffice/types/product.types.ts +2 -0
- package/src/services/appointment/appointment.service.ts +89 -182
- package/src/services/procedure/procedure.service.ts +1 -0
- package/src/types/appointment/index.ts +3 -1
- package/src/types/notifications/index.ts +4 -2
- package/src/types/procedure/index.ts +7 -0
- package/src/validations/appointment.schema.ts +2 -3
- 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
|
}
|