@blackcode_sa/metaestetics-api 1.5.29 → 1.5.30
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 +126 -1
- package/dist/admin/index.d.ts +126 -1
- package/dist/admin/index.js +347 -10
- package/dist/admin/index.mjs +345 -10
- package/dist/index.d.mts +64 -71
- package/dist/index.d.ts +64 -71
- package/dist/index.js +323 -688
- package/dist/index.mjs +359 -728
- package/package.json +2 -1
- package/src/admin/aggregation/README.md +79 -0
- package/src/admin/aggregation/clinic/README.md +52 -0
- package/src/admin/aggregation/patient/README.md +27 -0
- package/src/admin/aggregation/practitioner/README.md +42 -0
- package/src/admin/aggregation/procedure/README.md +43 -0
- package/src/admin/index.ts +9 -2
- package/src/admin/mailing/README.md +95 -0
- package/src/admin/mailing/base.mailing.service.ts +131 -0
- package/src/admin/mailing/index.ts +2 -0
- package/src/admin/mailing/practitionerInvite/index.ts +1 -0
- package/src/admin/mailing/practitionerInvite/practitionerInvite.mailing.ts +256 -0
- package/src/admin/mailing/practitionerInvite/templates/invitation.template.ts +101 -0
- package/src/services/README.md +106 -0
- package/src/services/clinic/README.md +87 -0
- package/src/services/clinic/clinic.service.ts +3 -126
- package/src/services/clinic/utils/clinic.utils.ts +2 -2
- package/src/services/practitioner/README.md +145 -0
- package/src/services/practitioner/practitioner.service.ts +119 -395
- package/src/services/procedure/README.md +88 -0
- package/src/services/procedure/procedure.service.ts +332 -369
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blackcode_sa/metaestetics-api",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "1.5.
|
|
4
|
+
"version": "1.5.30",
|
|
5
5
|
"description": "Firebase authentication service with anonymous upgrade support",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"module": "./dist/index.mjs",
|
|
@@ -83,6 +83,7 @@
|
|
|
83
83
|
"devDependencies": {
|
|
84
84
|
"@testing-library/jest-dom": "^6.6.3",
|
|
85
85
|
"@types/jest": "^29.5.14",
|
|
86
|
+
"@types/mailgun-js": "^0.22.18",
|
|
86
87
|
"@types/node": "^20.17.13",
|
|
87
88
|
"@types/react": "~18.3.12",
|
|
88
89
|
"expo-crypto": "^14.0.0",
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Data Aggregation Services
|
|
2
|
+
|
|
3
|
+
This directory contains services responsible for maintaining data consistency across related Firestore collections. These aggregation services are typically used by Cloud Functions that react to document changes (create/update/delete operations).
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
When a document is created, updated, or deleted in one collection, related documents in other collections often need to be updated to maintain data consistency and support efficient querying. For example:
|
|
8
|
+
|
|
9
|
+
- When a practitioner is updated, their information needs to be updated in associated clinics
|
|
10
|
+
- When a procedure is added, summaries need to be added to both practitioner and clinic documents
|
|
11
|
+
- When a clinic is deleted, related calendar events need to be canceled
|
|
12
|
+
|
|
13
|
+
Rather than embedding this logic in the main service classes, these aggregation services keep the concerns separate and can be independently triggered by Cloud Functions.
|
|
14
|
+
|
|
15
|
+
## Design Pattern
|
|
16
|
+
|
|
17
|
+
Each aggregation service:
|
|
18
|
+
|
|
19
|
+
1. Handles a specific domain entity (clinic, practitioner, procedure, etc.)
|
|
20
|
+
2. Contains methods organized by event type (creation, update, deletion)
|
|
21
|
+
3. Focuses solely on updating related collections
|
|
22
|
+
4. Uses transactions where appropriate to ensure consistency
|
|
23
|
+
|
|
24
|
+
## Available Aggregation Services
|
|
25
|
+
|
|
26
|
+
- `ClinicAggregationService`: Handles clinic-related aggregations
|
|
27
|
+
- `PractitionerAggregationService`: Handles practitioner-related aggregations
|
|
28
|
+
- `ProcedureAggregationService`: Handles procedure-related aggregations
|
|
29
|
+
- `PatientAggregationService`: Handles patient-related aggregations
|
|
30
|
+
|
|
31
|
+
## Implementation Notes
|
|
32
|
+
|
|
33
|
+
- These services are for admin/internal use only
|
|
34
|
+
- They operate with admin Firestore privileges
|
|
35
|
+
- They are designed for use by Cloud Functions, not directly by client applications
|
|
36
|
+
- They include detailed logging for monitoring and debugging
|
|
37
|
+
|
|
38
|
+
## Example Usage
|
|
39
|
+
|
|
40
|
+
In a Cloud Function:
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
exports.onPractitionerUpdate = functions.firestore
|
|
44
|
+
.document('practitioners/{practitionerId}')
|
|
45
|
+
.onUpdate(async (change, context) => {
|
|
46
|
+
const beforeData = change.before.data();
|
|
47
|
+
const afterData = change.after.data();
|
|
48
|
+
const practitionerId = context.params.practitionerId;
|
|
49
|
+
|
|
50
|
+
// Only run aggregation if specific fields changed
|
|
51
|
+
if (beforeData.basicInfo.firstName !== afterData.basicInfo.firstName ||
|
|
52
|
+
beforeData.basicInfo.lastName !== afterData.basicInfo.lastName ||
|
|
53
|
+
beforeData.basicInfo.profileImageUrl !== afterData.basicInfo.profileImageUrl) {
|
|
54
|
+
|
|
55
|
+
const aggregationService = new PractitionerAggregationService();
|
|
56
|
+
|
|
57
|
+
// Build doctorInfo object from updated data
|
|
58
|
+
const doctorInfo = {
|
|
59
|
+
id: practitionerId,
|
|
60
|
+
name: `${afterData.basicInfo.firstName} ${afterData.basicInfo.lastName}`,
|
|
61
|
+
photo: afterData.basicInfo.profileImageUrl || '',
|
|
62
|
+
// other fields...
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Update practitioner info in all related clinics
|
|
66
|
+
await aggregationService.updatePractitionerInfoInClinics(
|
|
67
|
+
afterData.clinics,
|
|
68
|
+
doctorInfo
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Update practitioner info in all related procedures
|
|
72
|
+
await aggregationService.updatePractitionerInfoInProcedures(
|
|
73
|
+
afterData.procedures,
|
|
74
|
+
doctorInfo
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
</rewritten_file>
|
|
79
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Clinic Aggregation Service (Admin)
|
|
2
|
+
|
|
3
|
+
This service centralizes the logic for updating aggregated data in various Firestore collections _when a clinic is created, updated, or deleted_. It is designed primarily for use by Cloud Functions triggered by changes in the `clinics` collection.
|
|
4
|
+
|
|
5
|
+
## `ClinicAggregationService` Class
|
|
6
|
+
|
|
7
|
+
### Constructor
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
constructor(firestore?: admin.firestore.Firestore)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Initializes the service with an Admin Firestore instance. Uses the default instance if none is provided.
|
|
14
|
+
|
|
15
|
+
### Methods Triggered by Clinic **Creation**
|
|
16
|
+
|
|
17
|
+
- **`addClinicToClinicGroup(clinicGroupId: string, clinicInfo: ClinicInfo): Promise<void>`**
|
|
18
|
+
- Adds the `clinicInfo` object and `clinicId` to the `clinicsInfo` and `clinicIds` arrays respectively in the specified parent `ClinicGroup` document.
|
|
19
|
+
|
|
20
|
+
### Methods Triggered by Clinic **Update**
|
|
21
|
+
|
|
22
|
+
- **`updateClinicInfoInPractitioners(practitionerIds: string[], clinicInfo: ClinicInfo): Promise<void>`**
|
|
23
|
+
- Updates the aggregated `clinicsInfo` array within multiple `Practitioner` documents.
|
|
24
|
+
- Uses a batch write to first remove the old `ClinicInfo` (matching by `id`) and then add the updated `clinicInfo`.
|
|
25
|
+
- **`updateClinicInfoInProcedures(procedureIds: string[], clinicInfo: ClinicInfo): Promise<void>`**
|
|
26
|
+
- Updates the embedded `clinicInfo` object within multiple `Procedure` documents associated with the updated clinic.
|
|
27
|
+
- **`updateClinicInfoInClinicGroup(clinicGroupId: string, clinicInfo: ClinicInfo): Promise<void>`**
|
|
28
|
+
- Updates the `clinicsInfo` array within the parent `ClinicGroup` document.
|
|
29
|
+
- Uses a batch write to first remove the old `ClinicInfo` (matching by `id`) and then add the updated `clinicInfo`.
|
|
30
|
+
- **`updateClinicInfoInPatients(patientIds: string[], clinicInfo: ClinicInfo, clinicIsActive: boolean): Promise<void>`**
|
|
31
|
+
- Currently logs a warning as the `PatientProfile` doesn't store full `ClinicInfo`.
|
|
32
|
+
- Intended place for logic if patient records need updates based on clinic changes (e.g., clinic deactivation).
|
|
33
|
+
- **`updateClinicLocationInCalendarEvents(clinicId: string, newLocation: ClinicLocation): Promise<void>`**
|
|
34
|
+
- Uses a Collection Group query (`calendar` subcollection) to find all _upcoming_ calendar events associated with the `clinicId`.
|
|
35
|
+
- Updates the `eventLocation` field in these calendar events with the `newLocation`.
|
|
36
|
+
- **`updateClinicInfoInCalendarEvents(clinicId: string, clinicInfo: ClinicInfo): Promise<void>`**
|
|
37
|
+
- Uses a Collection Group query (`calendar` subcollection) to find all _upcoming_ calendar events associated with the `clinicId`.
|
|
38
|
+
- Updates the embedded `clinicInfo` object in these calendar events.
|
|
39
|
+
|
|
40
|
+
### Methods Triggered by Clinic **Deletion**
|
|
41
|
+
|
|
42
|
+
- **`removeClinicFromPractitioners(practitionerIds: string[], clinicId: string): Promise<void>`**
|
|
43
|
+
- Removes the `clinicId` from the `clinicIds` array and the corresponding `ClinicInfo` object (matching by `id`) from the `clinicsInfo` array in multiple `Practitioner` documents.
|
|
44
|
+
- **`inactivateProceduresForClinic(procedureIds: string[]): Promise<void>`**
|
|
45
|
+
- Sets the `isActive` flag to `false` for multiple `Procedure` documents associated with the deleted clinic.
|
|
46
|
+
- **`removeClinicFromClinicGroup(clinicGroupId: string, clinicId: string): Promise<void>`**
|
|
47
|
+
- Removes the `clinicId` from the `clinicIds` array and the corresponding `ClinicInfo` object (matching by `id`) from the `clinicsInfo` array in the parent `ClinicGroup` document.
|
|
48
|
+
- **`removeClinicFromPatients(patientIds: string[], clinicId: string): Promise<void>`**
|
|
49
|
+
- Removes the `clinicId` from the `clinicIds` array in multiple `Patient` documents.
|
|
50
|
+
- **`cancelUpcomingCalendarEventsForClinic(clinicId: string): Promise<void>`**
|
|
51
|
+
- Uses a Collection Group query (`calendar` subcollection) to find all _upcoming_ calendar events associated with the `clinicId`.
|
|
52
|
+
- Sets the `status` of these events to `CANCELED` and adds a `cancelReason`.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Patient Aggregation Service (Admin)
|
|
2
|
+
|
|
3
|
+
This service centralizes the logic for updating aggregated data in various Firestore collections _when patient data is updated or deleted_. It is designed primarily for use by Cloud Functions triggered by changes in the `patients` collection.
|
|
4
|
+
|
|
5
|
+
**Note:** No specific aggregations are currently defined for patient _creation_ events in this service.
|
|
6
|
+
|
|
7
|
+
## `PatientAggregationService` Class
|
|
8
|
+
|
|
9
|
+
### Constructor
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
constructor(firestore?: admin.firestore.Firestore)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Initializes the service with an Admin Firestore instance. Uses the default instance if none is provided.
|
|
16
|
+
|
|
17
|
+
### Methods Triggered by Patient **Update**
|
|
18
|
+
|
|
19
|
+
- **`updatePatientInfoInCalendarEvents(patientId: string, patientInfo: any): Promise<void>`**
|
|
20
|
+
- Uses a Collection Group query (`calendar` subcollection) to find all _upcoming_ calendar events associated with the `patientId`.
|
|
21
|
+
- Updates the embedded `patientInfo` object in these calendar events with the provided `patientInfo`.
|
|
22
|
+
|
|
23
|
+
### Methods Triggered by Patient **Deletion**
|
|
24
|
+
|
|
25
|
+
- **`cancelUpcomingCalendarEventsForPatient(patientId: string): Promise<void>`**
|
|
26
|
+
- Uses a Collection Group query (`calendar` subcollection) to find all _upcoming_ calendar events associated with the `patientId`.
|
|
27
|
+
- Sets the `status` of these events to `CANCELED` and adds a `cancelReason` ("Patient deleted").
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Practitioner Aggregation Service (Admin)
|
|
2
|
+
|
|
3
|
+
This service centralizes the logic for updating aggregated data in various Firestore collections _when a practitioner is created, updated, or deleted_. It is designed primarily for use by Cloud Functions triggered by changes in the `practitioners` collection.
|
|
4
|
+
|
|
5
|
+
## `PractitionerAggregationService` Class
|
|
6
|
+
|
|
7
|
+
### Constructor
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
constructor(firestore?: admin.firestore.Firestore)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Initializes the service with an Admin Firestore instance. Uses the default instance if none is provided.
|
|
14
|
+
|
|
15
|
+
### Methods Triggered by Practitioner **Creation**
|
|
16
|
+
|
|
17
|
+
- **`addPractitionerToClinic(clinicId: string, doctorInfo: DoctorInfo): Promise<void>`**
|
|
18
|
+
- Adds the `practitionerId` (from `doctorInfo.id`) and the `doctorInfo` object to the `doctors` and `doctorsInfo` arrays respectively in the specified `Clinic` document.
|
|
19
|
+
- Typically called for each clinic the new practitioner is associated with.
|
|
20
|
+
|
|
21
|
+
### Methods Triggered by Practitioner **Update**
|
|
22
|
+
|
|
23
|
+
- **`updatePractitionerInfoInClinics(clinicIds: string[], doctorInfo: DoctorInfo): Promise<void>`**
|
|
24
|
+
- Updates the aggregated `doctorsInfo` array within multiple `Clinic` documents associated with the practitioner.
|
|
25
|
+
- Uses a batch write to first remove the old `DoctorInfo` (matching by `id`) and then add the updated `doctorInfo`.
|
|
26
|
+
- **`updatePractitionerInfoInProcedures(procedureIds: string[], doctorInfo: DoctorInfo): Promise<void>`**
|
|
27
|
+
- Updates the embedded `doctorInfo` object within multiple `Procedure` documents associated with the updated practitioner.
|
|
28
|
+
- **`updatePractitionerInfoInCalendarEvents(practitionerId: string, practitionerInfo: DoctorInfo): Promise<void>`**
|
|
29
|
+
- Uses a Collection Group query (`calendar` subcollection) to find all _upcoming_ calendar events associated with the `practitionerId`.
|
|
30
|
+
- Updates the embedded `practitionerInfo` object in these calendar events.
|
|
31
|
+
|
|
32
|
+
### Methods Triggered by Practitioner **Deletion**
|
|
33
|
+
|
|
34
|
+
- **`removePractitionerFromClinics(clinicIds: string[], practitionerId: string): Promise<void>`**
|
|
35
|
+
- Removes the `practitionerId` from the `doctors` array and the corresponding `DoctorInfo` object (matching by `id`) from the `doctorsInfo` array in multiple `Clinic` documents.
|
|
36
|
+
- **`cancelUpcomingCalendarEventsForPractitioner(practitionerId: string): Promise<void>`**
|
|
37
|
+
- Uses a Collection Group query (`calendar` subcollection) to find all _upcoming_ calendar events associated with the `practitionerId`.
|
|
38
|
+
- Sets the `status` of these events to `CANCELED` and adds a `cancelReason` ("Practitioner deleted").
|
|
39
|
+
- **`removePractitionerFromPatients(patientIds: string[], practitionerId: string): Promise<void>`**
|
|
40
|
+
- Removes the `practitionerId` from the `doctorIds` array in multiple `Patient` documents.
|
|
41
|
+
- **`inactivateProceduresForPractitioner(procedureIds: string[]): Promise<void>`**
|
|
42
|
+
- Sets the `isActive` flag to `false` for multiple `Procedure` documents associated with the deleted practitioner.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Procedure Aggregation Service (Admin)
|
|
2
|
+
|
|
3
|
+
This service centralizes the logic for updating aggregated data in various Firestore collections _when a procedure is created, updated, or deleted/inactivated_. It is designed primarily for use by Cloud Functions triggered by changes in the `procedures` collection.
|
|
4
|
+
|
|
5
|
+
## `ProcedureAggregationService` Class
|
|
6
|
+
|
|
7
|
+
### Constructor
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
constructor(firestore?: admin.firestore.Firestore)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Initializes the service with an Admin Firestore instance. Uses the default instance if none is provided.
|
|
14
|
+
|
|
15
|
+
### Methods Triggered by Procedure **Creation**
|
|
16
|
+
|
|
17
|
+
- **`addProcedureToPractitioner(practitionerId: string, procedureSummary: ProcedureSummaryInfo): Promise<void>`**
|
|
18
|
+
- Adds the `procedureId` (from `procedureSummary.id`) and the `procedureSummary` object to the `procedureIds` and `proceduresInfo` arrays respectively in the specified `Practitioner` document.
|
|
19
|
+
- **`addProcedureToClinic(clinicId: string, procedureSummary: ProcedureSummaryInfo): Promise<void>`**
|
|
20
|
+
- Adds the `procedureId` (from `procedureSummary.id`) and the `procedureSummary` object to the `procedures` and `proceduresInfo` arrays respectively in the specified `Clinic` document.
|
|
21
|
+
|
|
22
|
+
### Methods Triggered by Procedure **Update**
|
|
23
|
+
|
|
24
|
+
- **`updateProcedureInfoInPractitioner(practitionerId: string, procedureSummary: ProcedureSummaryInfo): Promise<void>`**
|
|
25
|
+
- Updates the `proceduresInfo` array within the specified `Practitioner` document.
|
|
26
|
+
- Uses a transaction to read the existing array, filter out the old summary (matching by `id`), add the updated `procedureSummary`, and write the modified array back. This prevents race conditions.
|
|
27
|
+
- **`updateProcedureInfoInClinic(clinicId: string, procedureSummary: ProcedureSummaryInfo): Promise<void>`**
|
|
28
|
+
- Updates the `proceduresInfo` array within the specified `Clinic` document using the same transaction-based read-modify-write pattern as above.
|
|
29
|
+
- **`updateProcedureInfoInCalendarEvents(procedureId: string, procedureInfo: any): Promise<void>`**
|
|
30
|
+
- Uses a Collection Group query (`calendar` subcollection) to find all _upcoming_ calendar events associated with the `procedureId`.
|
|
31
|
+
- Updates the embedded `procedureInfo` object in these calendar events with the provided `procedureInfo`.
|
|
32
|
+
|
|
33
|
+
### Methods Triggered by Procedure **Deletion / Deactivation**
|
|
34
|
+
|
|
35
|
+
- **`cancelUpcomingCalendarEventsForProcedure(procedureId: string): Promise<void>`**
|
|
36
|
+
- Uses a Collection Group query (`calendar` subcollection) to find all _upcoming_ calendar events associated with the `procedureId`.
|
|
37
|
+
- Sets the `status` of these events to `CANCELED` and adds a `cancelReason` ("Procedure deleted or inactivated").
|
|
38
|
+
- **`removeProcedureFromPractitioner(practitionerId: string, procedureId: string): Promise<void>`**
|
|
39
|
+
- Removes the `procedureId` from the `procedureIds` array and the corresponding `ProcedureSummaryInfo` object (matching by `id`) from the `proceduresInfo` array in the specified `Practitioner` document.
|
|
40
|
+
- Uses a transaction-based read-modify-write pattern.
|
|
41
|
+
- **`removeProcedureFromClinic(clinicId: string, procedureId: string): Promise<void>`**
|
|
42
|
+
- Removes the `procedureId` from the `procedures` array and the corresponding `ProcedureSummaryInfo` object (matching by `id`) from the `proceduresInfo` array in the specified `Clinic` document.
|
|
43
|
+
- Uses a transaction-based read-modify-write pattern.
|
package/src/admin/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { UserRole } from "../types";
|
|
|
8
8
|
// Import types needed by admin consumers (like Cloud Functions)
|
|
9
9
|
import { Clinic, ClinicLocation } from "../types/clinic";
|
|
10
10
|
import { ClinicInfo } from "../types/profile";
|
|
11
|
-
import { Practitioner } from "../types/practitioner";
|
|
11
|
+
import { Practitioner, PractitionerToken } from "../types/practitioner";
|
|
12
12
|
import { DoctorInfo } from "../types/clinic";
|
|
13
13
|
import { Procedure, ProcedureSummaryInfo } from "../types/procedure";
|
|
14
14
|
import { PatientProfile } from "../types/patient";
|
|
@@ -19,6 +19,10 @@ import { PractitionerAggregationService } from "./aggregation/practitioner/pract
|
|
|
19
19
|
import { ProcedureAggregationService } from "./aggregation/procedure/procedure.aggregation.service";
|
|
20
20
|
import { PatientAggregationService } from "./aggregation/patient/patient.aggregation.service";
|
|
21
21
|
|
|
22
|
+
// Import mailing services
|
|
23
|
+
import { BaseMailingService } from "./mailing/base.mailing.service";
|
|
24
|
+
import { PractitionerInviteMailingService } from "./mailing/practitionerInvite/practitionerInvite.mailing";
|
|
25
|
+
|
|
22
26
|
// Re-export types
|
|
23
27
|
export type {
|
|
24
28
|
Notification,
|
|
@@ -32,7 +36,7 @@ export type {
|
|
|
32
36
|
// Re-export types needed by cloud functions
|
|
33
37
|
export type { Clinic, ClinicLocation } from "../types/clinic";
|
|
34
38
|
export type { ClinicInfo } from "../types/profile";
|
|
35
|
-
export type { Practitioner } from "../types/practitioner";
|
|
39
|
+
export type { Practitioner, PractitionerToken } from "../types/practitioner";
|
|
36
40
|
export type { DoctorInfo } from "../types/clinic";
|
|
37
41
|
export type { Procedure, ProcedureSummaryInfo } from "../types/procedure";
|
|
38
42
|
export type { PatientProfile as Patient } from "../types/patient";
|
|
@@ -54,6 +58,9 @@ export {
|
|
|
54
58
|
PatientAggregationService,
|
|
55
59
|
};
|
|
56
60
|
|
|
61
|
+
// Export mailing services
|
|
62
|
+
export { BaseMailingService, PractitionerInviteMailingService };
|
|
63
|
+
|
|
57
64
|
/**
|
|
58
65
|
* Main entry point for the Admin module.
|
|
59
66
|
* This module contains services and utilities intended for administrative tasks,
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Mailing Services
|
|
2
|
+
|
|
3
|
+
This module provides mailing services for sending automated emails from the application using Mailgun.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
### 1. Install Dependencies
|
|
8
|
+
|
|
9
|
+
Make sure to install the required dependencies:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install mailgun-js firebase-admin firebase-functions
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### 2. Configure Mailgun
|
|
16
|
+
|
|
17
|
+
To use the mailing services, you need to configure Mailgun credentials in your Firebase project:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
firebase functions:config:set mailgun.api_key="your-mailgun-api-key" mailgun.domain="your-mailgun-domain" mailgun.from="MedClinic <no-reply@your-domain.com>"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
To enable test mode (emails won't actually be sent):
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
firebase functions:config:set mailgun.test_mode="true"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 3. Deploy Cloud Functions
|
|
30
|
+
|
|
31
|
+
Deploy the cloud functions to start handling email sending events:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
firebase deploy --only functions
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Available Services
|
|
38
|
+
|
|
39
|
+
### Practitioner Invitation Service
|
|
40
|
+
|
|
41
|
+
Sends email invitations to practitioners when they are invited to join a clinic.
|
|
42
|
+
|
|
43
|
+
#### How it works:
|
|
44
|
+
|
|
45
|
+
1. When a new practitioner token is created in the Firestore database, the `onPractitionerTokenCreated` cloud function is triggered.
|
|
46
|
+
2. The function uses the `PractitionerInviteMailingService` to send an invitation email to the practitioner.
|
|
47
|
+
3. The email contains the token needed to claim their profile and instructions for registration.
|
|
48
|
+
|
|
49
|
+
#### Example Usage in Code:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { PractitionerInviteMailingService } from "./admin/mailing";
|
|
53
|
+
|
|
54
|
+
// Create a new instance of the service
|
|
55
|
+
const mailingService = new PractitionerInviteMailingService();
|
|
56
|
+
|
|
57
|
+
// Send an invitation email
|
|
58
|
+
await mailingService.sendInvitationEmail({
|
|
59
|
+
token: {
|
|
60
|
+
id: "token-id",
|
|
61
|
+
token: "ABC123",
|
|
62
|
+
practitionerId: "practitioner-id",
|
|
63
|
+
email: "doctor@example.com",
|
|
64
|
+
clinicId: "clinic-id",
|
|
65
|
+
expiresAt: admin.firestore.Timestamp.fromDate(new Date()),
|
|
66
|
+
},
|
|
67
|
+
practitioner: {
|
|
68
|
+
firstName: "John",
|
|
69
|
+
lastName: "Doe",
|
|
70
|
+
},
|
|
71
|
+
clinic: {
|
|
72
|
+
name: "Example Clinic",
|
|
73
|
+
contactEmail: "admin@example.com",
|
|
74
|
+
contactName: "Clinic Admin",
|
|
75
|
+
},
|
|
76
|
+
options: {
|
|
77
|
+
registrationUrl: "https://your-app.com/register",
|
|
78
|
+
customSubject: "Join Our Clinic",
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Adding New Mailing Services
|
|
84
|
+
|
|
85
|
+
To add a new mailing service:
|
|
86
|
+
|
|
87
|
+
1. Create a new directory in the `mailing` folder for your service
|
|
88
|
+
2. Create a service class that extends `BaseMailingService`
|
|
89
|
+
3. Create an HTML template in a `templates` subfolder
|
|
90
|
+
4. Create a cloud function to trigger the email sending
|
|
91
|
+
5. Export your new service in the index files
|
|
92
|
+
|
|
93
|
+
## Logging
|
|
94
|
+
|
|
95
|
+
All email sending attempts are logged to the `email_logs` collection in Firestore for tracking and debugging purposes.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import * as mailgun from "mailgun-js";
|
|
2
|
+
import * as admin from "firebase-admin";
|
|
3
|
+
// Configuration is no longer read here
|
|
4
|
+
// import {
|
|
5
|
+
// getMailgunConfig,
|
|
6
|
+
// createMailgunClient,
|
|
7
|
+
// MailgunConfig,
|
|
8
|
+
// } from "./mailgun.config";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Base mailing service class that provides common functionality for all mailing services
|
|
12
|
+
*/
|
|
13
|
+
export class BaseMailingService {
|
|
14
|
+
protected db: FirebaseFirestore.Firestore;
|
|
15
|
+
protected mailgunClient: mailgun.Mailgun;
|
|
16
|
+
// Removed config property as it's no longer managed here
|
|
17
|
+
// protected config: MailgunConfig;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Constructor for BaseMailingService
|
|
21
|
+
* @param firestore Firestore instance provided by the caller
|
|
22
|
+
* @param mailgunClient Mailgun client instance provided by the caller
|
|
23
|
+
*/
|
|
24
|
+
constructor(
|
|
25
|
+
firestore: FirebaseFirestore.Firestore,
|
|
26
|
+
mailgunClient: mailgun.Mailgun // Mailgun client is now required
|
|
27
|
+
// mailgunConfig?: MailgunConfig // Removed config parameter
|
|
28
|
+
) {
|
|
29
|
+
// Use provided instances
|
|
30
|
+
this.db = firestore;
|
|
31
|
+
this.mailgunClient = mailgunClient;
|
|
32
|
+
// Removed internal config reading and client creation
|
|
33
|
+
// this.config = mailgunConfig || getMailgunConfig();
|
|
34
|
+
// this.mailgunClient = createMailgunClient(this.config);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Sends an email using Mailgun
|
|
39
|
+
* @param data Email data to send, including the 'from' address
|
|
40
|
+
* @returns Promise with the sending result
|
|
41
|
+
*/
|
|
42
|
+
protected async sendEmail(
|
|
43
|
+
data: mailgun.messages.SendData // Caller must provide 'from'
|
|
44
|
+
): Promise<mailgun.messages.SendResponse> {
|
|
45
|
+
try {
|
|
46
|
+
// Ensure 'from' field is provided by the caller
|
|
47
|
+
if (!data.from) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
"Email 'from' address must be provided in sendEmail data."
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
// Removed fallback to internal config.from
|
|
53
|
+
// const emailData = {
|
|
54
|
+
// ...data,
|
|
55
|
+
// from: data.from || this.config.from,
|
|
56
|
+
// };
|
|
57
|
+
|
|
58
|
+
// Send the email
|
|
59
|
+
return await new Promise<mailgun.messages.SendResponse>(
|
|
60
|
+
(resolve, reject) => {
|
|
61
|
+
this.mailgunClient.messages().send(data, (error, body) => {
|
|
62
|
+
if (error) {
|
|
63
|
+
console.error("[BaseMailingService] Error sending email:", error);
|
|
64
|
+
reject(error);
|
|
65
|
+
} else {
|
|
66
|
+
console.log(
|
|
67
|
+
"[BaseMailingService] Email sent successfully:",
|
|
68
|
+
body
|
|
69
|
+
);
|
|
70
|
+
resolve(body);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error("[BaseMailingService] Error in sendEmail:", error);
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Logs email sending attempt to Firestore for tracking
|
|
83
|
+
* @param emailData Email data that was sent
|
|
84
|
+
* @param success Whether the email was sent successfully
|
|
85
|
+
* @param error Error object if the email failed to send
|
|
86
|
+
*/
|
|
87
|
+
protected async logEmailAttempt(
|
|
88
|
+
emailData: { to: string; subject: string; templateName?: string },
|
|
89
|
+
success: boolean,
|
|
90
|
+
error?: any
|
|
91
|
+
): Promise<void> {
|
|
92
|
+
try {
|
|
93
|
+
const emailLogRef = this.db.collection("email_logs").doc();
|
|
94
|
+
await emailLogRef.set({
|
|
95
|
+
to: emailData.to,
|
|
96
|
+
subject: emailData.subject,
|
|
97
|
+
templateName: emailData.templateName,
|
|
98
|
+
success,
|
|
99
|
+
error: error ? JSON.stringify(error) : null,
|
|
100
|
+
sentAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
101
|
+
});
|
|
102
|
+
} catch (logError) {
|
|
103
|
+
console.error(
|
|
104
|
+
"[BaseMailingService] Error logging email attempt:",
|
|
105
|
+
logError
|
|
106
|
+
);
|
|
107
|
+
// Don't throw here to prevent disrupting the main flow
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Renders a simple HTML email template with variables
|
|
113
|
+
* @param template HTML template string
|
|
114
|
+
* @param variables Key-value pairs to replace in the template
|
|
115
|
+
* @returns Rendered HTML string
|
|
116
|
+
*/
|
|
117
|
+
protected renderTemplate(
|
|
118
|
+
template: string,
|
|
119
|
+
variables: Record<string, string>
|
|
120
|
+
): string {
|
|
121
|
+
let rendered = template;
|
|
122
|
+
|
|
123
|
+
// Replace template variables (format: {{variable_name}})
|
|
124
|
+
Object.entries(variables).forEach(([key, value]) => {
|
|
125
|
+
const regex = new RegExp(`{{\\s*${key}\\s*}}`, "g");
|
|
126
|
+
rendered = rendered.replace(regex, value);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return rendered;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./practitionerInvite.mailing";
|