@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
package/dist/index.mjs
CHANGED
|
@@ -311,6 +311,7 @@ var linkedFormInfoSchema = z2.object({
|
|
|
311
311
|
templateVersion: z2.number().int().positive("Template version must be a positive integer"),
|
|
312
312
|
title: z2.string().min(MIN_STRING_LENGTH, "Form title is required"),
|
|
313
313
|
isUserForm: z2.boolean(),
|
|
314
|
+
isRequired: z2.boolean().optional(),
|
|
314
315
|
status: filledDocumentStatusSchema,
|
|
315
316
|
path: z2.string().min(MIN_STRING_LENGTH, "Form path is required"),
|
|
316
317
|
submittedAt: z2.any().refine(
|
|
@@ -340,7 +341,6 @@ var finalizedDetailsSchema = z2.object({
|
|
|
340
341
|
notes: z2.string().max(MAX_STRING_LENGTH_LONG, "Finalization notes too long").optional()
|
|
341
342
|
});
|
|
342
343
|
var createAppointmentSchema = z2.object({
|
|
343
|
-
calendarEventId: z2.string().min(MIN_STRING_LENGTH, "Calendar event ID is required"),
|
|
344
344
|
clinicBranchId: z2.string().min(MIN_STRING_LENGTH, "Clinic branch ID is required"),
|
|
345
345
|
practitionerId: z2.string().min(MIN_STRING_LENGTH, "Practitioner ID is required"),
|
|
346
346
|
patientId: z2.string().min(MIN_STRING_LENGTH, "Patient ID is required"),
|
|
@@ -377,6 +377,7 @@ var updateAppointmentSchema = z2.object({
|
|
|
377
377
|
paymentTransactionId: z2.any().optional().nullable(),
|
|
378
378
|
completedPreRequirements: z2.union([z2.array(z2.string()), z2.any()]).optional(),
|
|
379
379
|
completedPostRequirements: z2.union([z2.array(z2.string()), z2.any()]).optional(),
|
|
380
|
+
linkedFormIds: z2.union([z2.array(z2.string()), z2.any()]).optional(),
|
|
380
381
|
pendingUserFormsIds: z2.union([z2.array(z2.string()), z2.any()]).optional(),
|
|
381
382
|
appointmentStartTime: z2.any().refine(
|
|
382
383
|
(val) => val === void 0 || val instanceof Date || (val == null ? void 0 : val._seconds) !== void 0 || typeof val === "number",
|
|
@@ -7469,6 +7470,8 @@ var NotificationType = /* @__PURE__ */ ((NotificationType3) => {
|
|
|
7469
7470
|
NotificationType3["APPOINTMENT_STATUS_CHANGE"] = "appointmentStatusChange";
|
|
7470
7471
|
NotificationType3["APPOINTMENT_RESCHEDULED_PROPOSAL"] = "appointmentRescheduledProposal";
|
|
7471
7472
|
NotificationType3["APPOINTMENT_CANCELLED"] = "appointmentCancelled";
|
|
7473
|
+
NotificationType3["PRE_REQUIREMENT_INSTRUCTION_DUE"] = "preRequirementInstructionDue";
|
|
7474
|
+
NotificationType3["POST_REQUIREMENT_INSTRUCTION_DUE"] = "postRequirementInstructionDue";
|
|
7472
7475
|
NotificationType3["REQUIREMENT_INSTRUCTION_DUE"] = "requirementInstructionDue";
|
|
7473
7476
|
NotificationType3["FORM_REMINDER"] = "formReminder";
|
|
7474
7477
|
NotificationType3["FORM_SUBMISSION_CONFIRMATION"] = "formSubmissionConfirmation";
|
|
@@ -7690,7 +7693,8 @@ var createProcedureSchema = z20.object({
|
|
|
7690
7693
|
duration: z20.number().min(1).max(480),
|
|
7691
7694
|
// Max 8 hours
|
|
7692
7695
|
practitionerId: z20.string().min(1),
|
|
7693
|
-
clinicBranchId: z20.string().min(1)
|
|
7696
|
+
clinicBranchId: z20.string().min(1),
|
|
7697
|
+
photos: z20.array(z20.string()).optional()
|
|
7694
7698
|
});
|
|
7695
7699
|
var updateProcedureSchema = z20.object({
|
|
7696
7700
|
name: z20.string().min(3).max(100).optional(),
|
|
@@ -7705,7 +7709,8 @@ var updateProcedureSchema = z20.object({
|
|
|
7705
7709
|
subcategoryId: z20.string().optional(),
|
|
7706
7710
|
technologyId: z20.string().optional(),
|
|
7707
7711
|
productId: z20.string().optional(),
|
|
7708
|
-
clinicBranchId: z20.string().optional()
|
|
7712
|
+
clinicBranchId: z20.string().optional(),
|
|
7713
|
+
photos: z20.array(z20.string()).optional()
|
|
7709
7714
|
});
|
|
7710
7715
|
var procedureSchema = createProcedureSchema.extend({
|
|
7711
7716
|
id: z20.string().min(1),
|
|
@@ -7719,6 +7724,8 @@ var procedureSchema = createProcedureSchema.extend({
|
|
|
7719
7724
|
// We'll validate the full product object separately
|
|
7720
7725
|
blockingConditions: z20.array(z20.any()),
|
|
7721
7726
|
// We'll validate blocking conditions separately
|
|
7727
|
+
contraindications: z20.array(z20.any()),
|
|
7728
|
+
// We'll validate contraindications separately
|
|
7722
7729
|
treatmentBenefits: z20.array(z20.any()),
|
|
7723
7730
|
// We'll validate treatment benefits separately
|
|
7724
7731
|
preRequirements: z20.array(z20.any()),
|
|
@@ -7823,6 +7830,7 @@ var ProcedureService = class extends BaseService {
|
|
|
7823
7830
|
technology,
|
|
7824
7831
|
product,
|
|
7825
7832
|
blockingConditions: technology.blockingConditions,
|
|
7833
|
+
contraindications: technology.contraindications || [],
|
|
7826
7834
|
treatmentBenefits: technology.benefits,
|
|
7827
7835
|
preRequirements: technology.requirements.pre,
|
|
7828
7836
|
postRequirements: technology.requirements.post,
|
|
@@ -12243,7 +12251,7 @@ import {
|
|
|
12243
12251
|
arrayUnion as arrayUnion8,
|
|
12244
12252
|
arrayRemove as arrayRemove7
|
|
12245
12253
|
} from "firebase/firestore";
|
|
12246
|
-
import { getFunctions
|
|
12254
|
+
import { getFunctions } from "firebase/functions";
|
|
12247
12255
|
|
|
12248
12256
|
// src/services/appointment/utils/appointment.utils.ts
|
|
12249
12257
|
import {
|
|
@@ -12266,166 +12274,6 @@ import {
|
|
|
12266
12274
|
var TECHNOLOGIES_COLLECTION = "technologies";
|
|
12267
12275
|
|
|
12268
12276
|
// src/services/appointment/utils/appointment.utils.ts
|
|
12269
|
-
async function fetchAggregatedInfoUtil(db, clinicId, practitionerId, patientId, procedureId) {
|
|
12270
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n;
|
|
12271
|
-
try {
|
|
12272
|
-
const [clinicDoc, practitionerDoc, patientDoc, procedureDoc] = await Promise.all([
|
|
12273
|
-
getDoc28(doc26(db, CLINICS_COLLECTION, clinicId)),
|
|
12274
|
-
getDoc28(doc26(db, PRACTITIONERS_COLLECTION, practitionerId)),
|
|
12275
|
-
getDoc28(doc26(db, PATIENTS_COLLECTION, patientId)),
|
|
12276
|
-
getDoc28(doc26(db, PROCEDURES_COLLECTION, procedureId))
|
|
12277
|
-
]);
|
|
12278
|
-
if (!clinicDoc.exists()) {
|
|
12279
|
-
throw new Error(`Clinic with ID ${clinicId} not found`);
|
|
12280
|
-
}
|
|
12281
|
-
if (!practitionerDoc.exists()) {
|
|
12282
|
-
throw new Error(`Practitioner with ID ${practitionerId} not found`);
|
|
12283
|
-
}
|
|
12284
|
-
if (!patientDoc.exists()) {
|
|
12285
|
-
throw new Error(`Patient with ID ${patientId} not found`);
|
|
12286
|
-
}
|
|
12287
|
-
if (!procedureDoc.exists()) {
|
|
12288
|
-
throw new Error(`Procedure with ID ${procedureId} not found`);
|
|
12289
|
-
}
|
|
12290
|
-
const clinicData = clinicDoc.data();
|
|
12291
|
-
const practitionerData = practitionerDoc.data();
|
|
12292
|
-
const patientData = patientDoc.data();
|
|
12293
|
-
const procedureData = procedureDoc.data();
|
|
12294
|
-
const clinicInfo = {
|
|
12295
|
-
id: clinicId,
|
|
12296
|
-
featuredPhoto: ((_a = clinicData.featuredPhotos) == null ? void 0 : _a[0]) || "",
|
|
12297
|
-
name: clinicData.name,
|
|
12298
|
-
description: clinicData.description || null,
|
|
12299
|
-
location: clinicData.location,
|
|
12300
|
-
contactInfo: clinicData.contactInfo
|
|
12301
|
-
};
|
|
12302
|
-
const practitionerInfo = {
|
|
12303
|
-
id: practitionerId,
|
|
12304
|
-
practitionerPhoto: ((_b = practitionerData.basicInfo) == null ? void 0 : _b.profileImageUrl) || null,
|
|
12305
|
-
name: `${((_c = practitionerData.basicInfo) == null ? void 0 : _c.firstName) || ""} ${((_d = practitionerData.basicInfo) == null ? void 0 : _d.lastName) || ""}`.trim(),
|
|
12306
|
-
email: ((_e = practitionerData.basicInfo) == null ? void 0 : _e.email) || "",
|
|
12307
|
-
phone: ((_f = practitionerData.basicInfo) == null ? void 0 : _f.phoneNumber) || null,
|
|
12308
|
-
certification: practitionerData.certification
|
|
12309
|
-
};
|
|
12310
|
-
const patientInfo = {
|
|
12311
|
-
id: patientId,
|
|
12312
|
-
fullName: patientData.displayName || "",
|
|
12313
|
-
email: patientData.email || "",
|
|
12314
|
-
phone: patientData.phoneNumber || null,
|
|
12315
|
-
dateOfBirth: patientData.dateOfBirth || Timestamp27.now(),
|
|
12316
|
-
gender: patientData.gender || "other"
|
|
12317
|
-
};
|
|
12318
|
-
const procedureInfo = {
|
|
12319
|
-
id: procedureId,
|
|
12320
|
-
name: procedureData.name,
|
|
12321
|
-
description: procedureData.description,
|
|
12322
|
-
photo: procedureData.photo || "",
|
|
12323
|
-
family: procedureData.family,
|
|
12324
|
-
categoryName: ((_g = procedureData.category) == null ? void 0 : _g.name) || "",
|
|
12325
|
-
subcategoryName: ((_h = procedureData.subcategory) == null ? void 0 : _h.name) || "",
|
|
12326
|
-
technologyName: ((_i = procedureData.technology) == null ? void 0 : _i.name) || "",
|
|
12327
|
-
brandName: ((_j = procedureData.product) == null ? void 0 : _j.brand) || "",
|
|
12328
|
-
productName: ((_k = procedureData.product) == null ? void 0 : _k.name) || "",
|
|
12329
|
-
price: procedureData.price || 0,
|
|
12330
|
-
pricingMeasure: procedureData.pricingMeasure,
|
|
12331
|
-
currency: procedureData.currency,
|
|
12332
|
-
duration: procedureData.duration || 0,
|
|
12333
|
-
clinicId,
|
|
12334
|
-
clinicName: clinicInfo.name,
|
|
12335
|
-
practitionerId,
|
|
12336
|
-
practitionerName: practitionerInfo.name
|
|
12337
|
-
};
|
|
12338
|
-
let technologyId = "";
|
|
12339
|
-
if ((_l = procedureData.technology) == null ? void 0 : _l.id) {
|
|
12340
|
-
technologyId = procedureData.technology.id;
|
|
12341
|
-
}
|
|
12342
|
-
let blockingConditions = [];
|
|
12343
|
-
let contraindications = [];
|
|
12344
|
-
let preProcedureRequirements = [];
|
|
12345
|
-
let postProcedureRequirements = [];
|
|
12346
|
-
if (technologyId) {
|
|
12347
|
-
const technologyDoc = await getDoc28(
|
|
12348
|
-
doc26(db, TECHNOLOGIES_COLLECTION, technologyId)
|
|
12349
|
-
);
|
|
12350
|
-
if (technologyDoc.exists()) {
|
|
12351
|
-
const technologyData = technologyDoc.data();
|
|
12352
|
-
blockingConditions = technologyData.blockingConditions || [];
|
|
12353
|
-
contraindications = technologyData.contraindications || [];
|
|
12354
|
-
preProcedureRequirements = ((_m = technologyData.requirements) == null ? void 0 : _m.pre) || [];
|
|
12355
|
-
postProcedureRequirements = ((_n = technologyData.requirements) == null ? void 0 : _n.post) || [];
|
|
12356
|
-
}
|
|
12357
|
-
} else {
|
|
12358
|
-
blockingConditions = procedureData.blockingConditions || [];
|
|
12359
|
-
contraindications = procedureData.contraindications || [];
|
|
12360
|
-
preProcedureRequirements = procedureData.preRequirements || [];
|
|
12361
|
-
postProcedureRequirements = procedureData.postRequirements || [];
|
|
12362
|
-
}
|
|
12363
|
-
return {
|
|
12364
|
-
clinicInfo,
|
|
12365
|
-
practitionerInfo,
|
|
12366
|
-
patientInfo,
|
|
12367
|
-
procedureInfo,
|
|
12368
|
-
blockingConditions,
|
|
12369
|
-
contraindications,
|
|
12370
|
-
preProcedureRequirements,
|
|
12371
|
-
postProcedureRequirements
|
|
12372
|
-
};
|
|
12373
|
-
} catch (error) {
|
|
12374
|
-
console.error("Error fetching aggregated info:", error);
|
|
12375
|
-
throw error;
|
|
12376
|
-
}
|
|
12377
|
-
}
|
|
12378
|
-
async function createAppointmentUtil2(db, data, aggregatedInfo, generateId2) {
|
|
12379
|
-
try {
|
|
12380
|
-
const appointmentId = generateId2();
|
|
12381
|
-
const appointment = {
|
|
12382
|
-
id: appointmentId,
|
|
12383
|
-
calendarEventId: data.calendarEventId,
|
|
12384
|
-
clinicBranchId: data.clinicBranchId,
|
|
12385
|
-
clinicInfo: aggregatedInfo.clinicInfo,
|
|
12386
|
-
practitionerId: data.practitionerId,
|
|
12387
|
-
practitionerInfo: aggregatedInfo.practitionerInfo,
|
|
12388
|
-
patientId: data.patientId,
|
|
12389
|
-
patientInfo: aggregatedInfo.patientInfo,
|
|
12390
|
-
procedureId: data.procedureId,
|
|
12391
|
-
procedureInfo: aggregatedInfo.procedureInfo,
|
|
12392
|
-
status: data.initialStatus,
|
|
12393
|
-
bookingTime: Timestamp27.now(),
|
|
12394
|
-
appointmentStartTime: data.appointmentStartTime,
|
|
12395
|
-
appointmentEndTime: data.appointmentEndTime,
|
|
12396
|
-
patientNotes: data.patientNotes || null,
|
|
12397
|
-
cost: data.cost,
|
|
12398
|
-
currency: data.currency,
|
|
12399
|
-
paymentStatus: data.initialPaymentStatus || "unpaid" /* UNPAID */,
|
|
12400
|
-
blockingConditions: aggregatedInfo.blockingConditions,
|
|
12401
|
-
contraindications: aggregatedInfo.contraindications,
|
|
12402
|
-
preProcedureRequirements: aggregatedInfo.preProcedureRequirements,
|
|
12403
|
-
postProcedureRequirements: aggregatedInfo.postProcedureRequirements,
|
|
12404
|
-
completedPreRequirements: [],
|
|
12405
|
-
completedPostRequirements: [],
|
|
12406
|
-
createdAt: serverTimestamp23(),
|
|
12407
|
-
updatedAt: serverTimestamp23()
|
|
12408
|
-
};
|
|
12409
|
-
if (data.initialStatus === "confirmed" /* CONFIRMED */) {
|
|
12410
|
-
appointment.confirmationTime = Timestamp27.now();
|
|
12411
|
-
}
|
|
12412
|
-
await setDoc23(doc26(db, APPOINTMENTS_COLLECTION, appointmentId), appointment);
|
|
12413
|
-
const calendarEventRef = doc26(db, CALENDAR_COLLECTION, data.calendarEventId);
|
|
12414
|
-
await updateDoc25(calendarEventRef, {
|
|
12415
|
-
appointmentId,
|
|
12416
|
-
updatedAt: serverTimestamp23()
|
|
12417
|
-
});
|
|
12418
|
-
const now = Timestamp27.now();
|
|
12419
|
-
return {
|
|
12420
|
-
...appointment,
|
|
12421
|
-
createdAt: now,
|
|
12422
|
-
updatedAt: now
|
|
12423
|
-
};
|
|
12424
|
-
} catch (error) {
|
|
12425
|
-
console.error("Error creating appointment:", error);
|
|
12426
|
-
throw error;
|
|
12427
|
-
}
|
|
12428
|
-
}
|
|
12429
12277
|
async function updateAppointmentUtil2(db, appointmentId, data) {
|
|
12430
12278
|
try {
|
|
12431
12279
|
const appointmentRef = doc26(db, APPOINTMENTS_COLLECTION, appointmentId);
|
|
@@ -12633,78 +12481,6 @@ var AppointmentService = class extends BaseService {
|
|
|
12633
12481
|
this.filledDocumentService = filledDocumentService;
|
|
12634
12482
|
this.functions = getFunctions(app, "europe-west6");
|
|
12635
12483
|
}
|
|
12636
|
-
/**
|
|
12637
|
-
* Test method using the callable function version of getAvailableBookingSlots
|
|
12638
|
-
* For development and testing purposes only - not for production use
|
|
12639
|
-
*
|
|
12640
|
-
* @param clinicId ID of the clinic
|
|
12641
|
-
* @param practitionerId ID of the practitioner
|
|
12642
|
-
* @param procedureId ID of the procedure
|
|
12643
|
-
* @param startDate Start date of the time range to check
|
|
12644
|
-
* @param endDate End date of the time range to check
|
|
12645
|
-
* @returns Test result from the callable function
|
|
12646
|
-
*/
|
|
12647
|
-
async testGetAvailableBookingSlots(clinicId, practitionerId, procedureId, startDate, endDate) {
|
|
12648
|
-
try {
|
|
12649
|
-
console.log(
|
|
12650
|
-
`[APPOINTMENT_SERVICE] Testing callable function for clinic: ${clinicId}, practitioner: ${practitionerId}, procedure: ${procedureId}`
|
|
12651
|
-
);
|
|
12652
|
-
const getAvailableBookingSlotsCallable = httpsCallable(
|
|
12653
|
-
this.functions,
|
|
12654
|
-
"getAvailableBookingSlots"
|
|
12655
|
-
);
|
|
12656
|
-
const result = await getAvailableBookingSlotsCallable({
|
|
12657
|
-
clinicId,
|
|
12658
|
-
practitionerId,
|
|
12659
|
-
procedureId,
|
|
12660
|
-
timeframe: {
|
|
12661
|
-
start: startDate.getTime(),
|
|
12662
|
-
end: endDate.getTime()
|
|
12663
|
-
}
|
|
12664
|
-
});
|
|
12665
|
-
console.log(
|
|
12666
|
-
"[APPOINTMENT_SERVICE] Callable function test result:",
|
|
12667
|
-
result.data
|
|
12668
|
-
);
|
|
12669
|
-
return result.data;
|
|
12670
|
-
} catch (error) {
|
|
12671
|
-
console.error(
|
|
12672
|
-
"[APPOINTMENT_SERVICE] Error testing callable function:",
|
|
12673
|
-
error
|
|
12674
|
-
);
|
|
12675
|
-
throw error;
|
|
12676
|
-
}
|
|
12677
|
-
}
|
|
12678
|
-
/**
|
|
12679
|
-
* Gets available booking slots for a specific clinic, practitioner, and procedure.
|
|
12680
|
-
*
|
|
12681
|
-
* @param clinicId ID of the clinic
|
|
12682
|
-
* @param practitionerId ID of the practitioner
|
|
12683
|
-
* @param procedureId ID of the procedure
|
|
12684
|
-
* @param startDate Start date of the time range to check
|
|
12685
|
-
* @param endDate End date of the time range to check
|
|
12686
|
-
* @returns Array of available booking slots
|
|
12687
|
-
*/
|
|
12688
|
-
async getAvailableBookingSlots(clinicId, practitionerId, procedureId, startDate, endDate) {
|
|
12689
|
-
try {
|
|
12690
|
-
console.log(
|
|
12691
|
-
`[APPOINTMENT_SERVICE] Getting available booking slots for clinic: ${clinicId}, practitioner: ${practitionerId}, procedure: ${procedureId}`
|
|
12692
|
-
);
|
|
12693
|
-
return this.getAvailableBookingSlotsHttp(
|
|
12694
|
-
clinicId,
|
|
12695
|
-
practitionerId,
|
|
12696
|
-
procedureId,
|
|
12697
|
-
startDate,
|
|
12698
|
-
endDate
|
|
12699
|
-
);
|
|
12700
|
-
} catch (error) {
|
|
12701
|
-
console.error(
|
|
12702
|
-
"[APPOINTMENT_SERVICE] Error getting available booking slots:",
|
|
12703
|
-
error
|
|
12704
|
-
);
|
|
12705
|
-
throw error;
|
|
12706
|
-
}
|
|
12707
|
-
}
|
|
12708
12484
|
/**
|
|
12709
12485
|
* Gets available booking slots for a specific clinic, practitioner, and procedure using HTTP request.
|
|
12710
12486
|
* This is an alternative implementation using direct HTTP request instead of callable function.
|
|
@@ -12806,34 +12582,81 @@ var AppointmentService = class extends BaseService {
|
|
|
12806
12582
|
}
|
|
12807
12583
|
}
|
|
12808
12584
|
/**
|
|
12809
|
-
* Creates
|
|
12585
|
+
* Creates an appointment via the Cloud Function orchestrateAppointmentCreation
|
|
12810
12586
|
*
|
|
12811
|
-
* @param data
|
|
12587
|
+
* @param data - CreateAppointmentData object
|
|
12812
12588
|
* @returns The created appointment
|
|
12813
12589
|
*/
|
|
12814
|
-
async
|
|
12590
|
+
async createAppointmentHttp(data) {
|
|
12815
12591
|
try {
|
|
12816
|
-
console.log(
|
|
12817
|
-
|
|
12818
|
-
const aggregatedInfo = await fetchAggregatedInfoUtil(
|
|
12819
|
-
this.db,
|
|
12820
|
-
validatedData.clinicBranchId,
|
|
12821
|
-
validatedData.practitionerId,
|
|
12822
|
-
validatedData.patientId,
|
|
12823
|
-
validatedData.procedureId
|
|
12592
|
+
console.log(
|
|
12593
|
+
"[APPOINTMENT_SERVICE] Creating appointment via cloud function"
|
|
12824
12594
|
);
|
|
12825
|
-
const
|
|
12826
|
-
|
|
12827
|
-
|
|
12828
|
-
|
|
12829
|
-
|
|
12595
|
+
const currentUser = this.auth.currentUser;
|
|
12596
|
+
if (!currentUser) {
|
|
12597
|
+
throw new Error("User must be authenticated to create an appointment");
|
|
12598
|
+
}
|
|
12599
|
+
const idToken = await currentUser.getIdToken();
|
|
12600
|
+
const functionUrl = `https://europe-west6-metaestetics.cloudfunctions.net/bookingApi/orchestrateAppointmentCreation`;
|
|
12601
|
+
const requestData = {
|
|
12602
|
+
patientId: data.patientId,
|
|
12603
|
+
procedureId: data.procedureId,
|
|
12604
|
+
appointmentStartTime: data.appointmentStartTime.toMillis ? data.appointmentStartTime.toMillis() : new Date(data.appointmentStartTime).getTime(),
|
|
12605
|
+
appointmentEndTime: data.appointmentEndTime.toMillis ? data.appointmentEndTime.toMillis() : new Date(data.appointmentEndTime).getTime(),
|
|
12606
|
+
patientNotes: data.patientNotes || null
|
|
12607
|
+
};
|
|
12608
|
+
console.log(
|
|
12609
|
+
`[APPOINTMENT_SERVICE] Making fetch request to ${functionUrl}`
|
|
12830
12610
|
);
|
|
12611
|
+
const response = await fetch(functionUrl, {
|
|
12612
|
+
method: "POST",
|
|
12613
|
+
mode: "cors",
|
|
12614
|
+
cache: "no-cache",
|
|
12615
|
+
credentials: "omit",
|
|
12616
|
+
headers: {
|
|
12617
|
+
"Content-Type": "application/json",
|
|
12618
|
+
Authorization: `Bearer ${idToken}`
|
|
12619
|
+
},
|
|
12620
|
+
redirect: "follow",
|
|
12621
|
+
referrerPolicy: "no-referrer",
|
|
12622
|
+
body: JSON.stringify(requestData)
|
|
12623
|
+
});
|
|
12831
12624
|
console.log(
|
|
12832
|
-
`[APPOINTMENT_SERVICE]
|
|
12625
|
+
`[APPOINTMENT_SERVICE] Received response ${response.status}: ${response.statusText}`
|
|
12833
12626
|
);
|
|
12834
|
-
|
|
12627
|
+
if (!response.ok) {
|
|
12628
|
+
const errorText = await response.text();
|
|
12629
|
+
console.error(
|
|
12630
|
+
`[APPOINTMENT_SERVICE] Error response details: ${errorText}`
|
|
12631
|
+
);
|
|
12632
|
+
throw new Error(
|
|
12633
|
+
`Failed to create appointment: ${response.status} ${response.statusText} - ${errorText}`
|
|
12634
|
+
);
|
|
12635
|
+
}
|
|
12636
|
+
const result = await response.json();
|
|
12637
|
+
if (!result.success) {
|
|
12638
|
+
throw new Error(result.error || "Failed to create appointment");
|
|
12639
|
+
}
|
|
12640
|
+
if (result.appointmentData) {
|
|
12641
|
+
console.log(
|
|
12642
|
+
`[APPOINTMENT_SERVICE] Appointment created with ID: ${result.appointmentId}`
|
|
12643
|
+
);
|
|
12644
|
+
return result.appointmentData;
|
|
12645
|
+
}
|
|
12646
|
+
const createdAppointment = await this.getAppointmentById(
|
|
12647
|
+
result.appointmentId
|
|
12648
|
+
);
|
|
12649
|
+
if (!createdAppointment) {
|
|
12650
|
+
throw new Error(
|
|
12651
|
+
`Failed to retrieve created appointment with ID: ${result.appointmentId}`
|
|
12652
|
+
);
|
|
12653
|
+
}
|
|
12654
|
+
return createdAppointment;
|
|
12835
12655
|
} catch (error) {
|
|
12836
|
-
console.error(
|
|
12656
|
+
console.error(
|
|
12657
|
+
"[APPOINTMENT_SERVICE] Error creating appointment via cloud function:",
|
|
12658
|
+
error
|
|
12659
|
+
);
|
|
12837
12660
|
throw error;
|
|
12838
12661
|
}
|
|
12839
12662
|
}
|
|
@@ -13323,38 +13146,6 @@ var AppointmentService = class extends BaseService {
|
|
|
13323
13146
|
};
|
|
13324
13147
|
return this.updateAppointment(appointmentId, updateData);
|
|
13325
13148
|
}
|
|
13326
|
-
/**
|
|
13327
|
-
* Marks pre-procedure requirements as completed.
|
|
13328
|
-
*
|
|
13329
|
-
* @param appointmentId ID of the appointment
|
|
13330
|
-
* @param requirementIds IDs of the requirements to mark as completed
|
|
13331
|
-
* @returns The updated appointment
|
|
13332
|
-
*/
|
|
13333
|
-
async completePreRequirements(appointmentId, requirementIds) {
|
|
13334
|
-
console.log(
|
|
13335
|
-
`[APPOINTMENT_SERVICE] Marking pre-requirements as completed for appointment: ${appointmentId}`
|
|
13336
|
-
);
|
|
13337
|
-
const updateData = {
|
|
13338
|
-
completedPreRequirements: requirementIds
|
|
13339
|
-
};
|
|
13340
|
-
return this.updateAppointment(appointmentId, updateData);
|
|
13341
|
-
}
|
|
13342
|
-
/**
|
|
13343
|
-
* Marks post-procedure requirements as completed.
|
|
13344
|
-
*
|
|
13345
|
-
* @param appointmentId ID of the appointment
|
|
13346
|
-
* @param requirementIds IDs of the requirements to mark as completed
|
|
13347
|
-
* @returns The updated appointment
|
|
13348
|
-
*/
|
|
13349
|
-
async completePostRequirements(appointmentId, requirementIds) {
|
|
13350
|
-
console.log(
|
|
13351
|
-
`[APPOINTMENT_SERVICE] Marking post-requirements as completed for appointment: ${appointmentId}`
|
|
13352
|
-
);
|
|
13353
|
-
const updateData = {
|
|
13354
|
-
completedPostRequirements: requirementIds
|
|
13355
|
-
};
|
|
13356
|
-
return this.updateAppointment(appointmentId, updateData);
|
|
13357
|
-
}
|
|
13358
13149
|
/**
|
|
13359
13150
|
* Updates the internal notes of an appointment.
|
|
13360
13151
|
*
|
|
@@ -13371,25 +13162,6 @@ var AppointmentService = class extends BaseService {
|
|
|
13371
13162
|
};
|
|
13372
13163
|
return this.updateAppointment(appointmentId, updateData);
|
|
13373
13164
|
}
|
|
13374
|
-
/**
|
|
13375
|
-
* Debug helper: Get the current user's ID token for testing purposes
|
|
13376
|
-
* Use this token in Postman with Authorization: Bearer TOKEN
|
|
13377
|
-
*/
|
|
13378
|
-
async getDebugToken() {
|
|
13379
|
-
try {
|
|
13380
|
-
const currentUser = this.auth.currentUser;
|
|
13381
|
-
if (!currentUser) {
|
|
13382
|
-
console.log("[APPOINTMENT_SERVICE] No user is signed in");
|
|
13383
|
-
return null;
|
|
13384
|
-
}
|
|
13385
|
-
const idToken = await currentUser.getIdToken();
|
|
13386
|
-
console.log("[APPOINTMENT_SERVICE] Debug token:", idToken);
|
|
13387
|
-
return idToken;
|
|
13388
|
-
} catch (error) {
|
|
13389
|
-
console.error("[APPOINTMENT_SERVICE] Error getting debug token:", error);
|
|
13390
|
-
return null;
|
|
13391
|
-
}
|
|
13392
|
-
}
|
|
13393
13165
|
};
|
|
13394
13166
|
|
|
13395
13167
|
// src/services/patient/patientRequirements.service.ts
|
package/package.json
CHANGED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Appointment Aggregation Service (`appointment.aggregation.service.ts`)
|
|
2
|
+
|
|
3
|
+
This service is responsible for handling side effects and data aggregation tasks related to the lifecycle of appointments within the system. It is primarily designed to be invoked by Firestore triggers (`onCreate`, `onUpdate`, `onDelete`) for the `appointments` collection.
|
|
4
|
+
|
|
5
|
+
## Core Responsibilities
|
|
6
|
+
|
|
7
|
+
- Managing denormalized data links (e.g., patient-clinic, patient-practitioner).
|
|
8
|
+
- Creating and updating `PatientRequirementInstance` documents based on appointment status and associated procedure requirements.
|
|
9
|
+
- Orchestrating notifications (email and push) to patients, practitioners, and clinic admins at various stages of the appointment lifecycle.
|
|
10
|
+
- Interfacing with other admin services like `AppointmentMailingService`, `NotificationsAdmin`, and `CalendarAdminService` (though calendar interactions are mostly TODOs).
|
|
11
|
+
|
|
12
|
+
## Implemented Functionality
|
|
13
|
+
|
|
14
|
+
### Constructor
|
|
15
|
+
|
|
16
|
+
- Initializes with a Firestore instance and a Mailgun client.
|
|
17
|
+
- Sets up instances of dependent services: `AppointmentMailingService`, `NotificationsAdmin`, `CalendarAdminService`, and `PatientRequirementsAdminService`.
|
|
18
|
+
- **Note**: `PatientRequirementsAdminService` is initialized but its methods are not (and should not be) directly called by this aggregation service for creating/managing requirement instances. That logic is correctly handled by local private methods in this service.
|
|
19
|
+
|
|
20
|
+
### `handleAppointmentCreate(appointment: Appointment)`
|
|
21
|
+
|
|
22
|
+
- **Patient-Clinic-Practitioner Links**: Calls `managePatientClinicPractitionerLinks` to add links.
|
|
23
|
+
- **Data Fetching**: Fetches `PatientProfile`, `PatientSensitiveInfo`, `PractitionerProfile`, and `Clinic` data.
|
|
24
|
+
- **Status: `CONFIRMED`**
|
|
25
|
+
- Creates pre-appointment requirement instances via `createPreAppointmentRequirementInstances()`.
|
|
26
|
+
- Sends confirmation email to patient via `appointmentMailingService.sendAppointmentConfirmedEmail()`.
|
|
27
|
+
- Sends confirmation email to practitioner via `appointmentMailingService.sendAppointmentConfirmedEmail()`.
|
|
28
|
+
- Initiates confirmed push notification to patient via `notificationsAdmin.sendAppointmentConfirmedPush()`.
|
|
29
|
+
- **Status: `PENDING`**
|
|
30
|
+
- Sends appointment requested email to clinic admin via `appointmentMailingService.sendAppointmentRequestedEmailToClinic()`.
|
|
31
|
+
|
|
32
|
+
### `handleAppointmentUpdate(before: Appointment, after: Appointment)`
|
|
33
|
+
|
|
34
|
+
- **Data Fetching**: Fetches `PatientProfile`, `PatientSensitiveInfo`, `PractitionerProfile`, and `Clinic` data for the `after` state.
|
|
35
|
+
- **Status Change: `PENDING -> CONFIRMED`**
|
|
36
|
+
- Creates pre-appointment requirement instances via `createPreAppointmentRequirementInstances(after)`.
|
|
37
|
+
- Sends confirmation email to patient.
|
|
38
|
+
- Sends confirmation email to practitioner.
|
|
39
|
+
- Initiates confirmed push notification to patient.
|
|
40
|
+
- **Status Change: `Any -> CANCELLED_*` (includes `NO_SHOW`)**
|
|
41
|
+
- Updates related patient requirement instances to `CANCELLED_APPOINTMENT` via `updateRelatedPatientRequirementInstances()`.
|
|
42
|
+
- Removes patient-clinic-practitioner links via `managePatientClinicPractitionerLinks()`.
|
|
43
|
+
- Sends cancellation email to patient.
|
|
44
|
+
- Sends cancellation email to practitioner.
|
|
45
|
+
- **Status Change: `Any -> COMPLETED`**
|
|
46
|
+
- Creates post-appointment requirement instances via `createPostAppointmentRequirementInstances(after)`.
|
|
47
|
+
- Sends review request email to patient.
|
|
48
|
+
- **Status Change: `Any -> RESCHEDULED_BY_CLINIC`**
|
|
49
|
+
- Updates related (old) patient requirement instances to `SUPERSEDED_RESCHEDULE`.
|
|
50
|
+
- Sends reschedule proposal email to patient.
|
|
51
|
+
|
|
52
|
+
### `handleAppointmentDelete(deletedAppointment: Appointment)`
|
|
53
|
+
|
|
54
|
+
- Updates related patient requirement instances to `CANCELLED_APPOINTMENT`.
|
|
55
|
+
- Removes patient-clinic-practitioner links.
|
|
56
|
+
|
|
57
|
+
### Helper Methods
|
|
58
|
+
|
|
59
|
+
- **`createPreAppointmentRequirementInstances(appointment: Appointment)`**:
|
|
60
|
+
- Creates `PatientRequirementInstance` documents for pre-appointment requirements based on `appointment.preProcedureRequirements`.
|
|
61
|
+
- Calculates due times for instructions based on `appointment.appointmentStartTime`.
|
|
62
|
+
- Sets `actionableWindow` with a placeholder value (TODO noted).
|
|
63
|
+
- Batch writes instances to Firestore.
|
|
64
|
+
- **`createPostAppointmentRequirementInstances(appointment: Appointment)`**:
|
|
65
|
+
- Creates `PatientRequirementInstance` documents for post-appointment requirements based on `appointment.postProcedureRequirements`.
|
|
66
|
+
- Calculates due times for instructions based on `appointment.appointmentEndTime`.
|
|
67
|
+
- Sets `actionableWindow` with a placeholder value (TODO noted).
|
|
68
|
+
- Batch writes instances to Firestore.
|
|
69
|
+
- **`updateRelatedPatientRequirementInstances(appointment: Appointment, newOverallStatus: PatientRequirementOverallStatus)`**:
|
|
70
|
+
- Queries for all `PatientRequirementInstance` documents linked to the appointment.
|
|
71
|
+
- Batch updates their `overallStatus` and `updatedAt` fields.
|
|
72
|
+
- **`managePatientClinicPractitionerLinks(appointment: Appointment, action: "create" | "cancel")`**:
|
|
73
|
+
- Adds/removes the patient's ID from `patientIds` arrays on the respective clinic and practitioner documents using `FieldValue.arrayUnion` or `FieldValue.arrayRemove`.
|
|
74
|
+
- **Data Fetching Helpers (`fetchPatientProfile`, `fetchPatientSensitiveInfo`, `fetchPractitionerProfile`, `fetchClinicInfo`)**:
|
|
75
|
+
- Basic methods to retrieve documents from Firestore by ID.
|
|
76
|
+
|
|
77
|
+
## Pending `TODO` Items & Areas for Future Work
|
|
78
|
+
|
|
79
|
+
### General / Cross-Cutting
|
|
80
|
+
|
|
81
|
+
- **Type Imports for Email Data Objects**:
|
|
82
|
+
- Throughout `handleAppointmentCreate` and `handleAppointmentUpdate`, calls to the `appointmentMailingService` use `as any` for the data payload (e.g., `emailData as any`).
|
|
83
|
+
- `TODO`: Properly import `PatientProfileInfo`, `PractitionerProfileInfo`, `ClinicInfo` from a shared location (e.g., `Api/src/types/profile.ts` or `Api/src/types/index.ts`) and ensure these types are correctly used, removing the `as any` casts. This may require exporting these types if they are not already.
|
|
84
|
+
- **`actionableWindow` in Requirement Instances**:
|
|
85
|
+
- In `createPreAppointmentRequirementInstances` and `createPostAppointmentRequirementInstances`, the `actionableWindow` for instructions is currently a placeholder (e.g., based on importance or a fixed value like 24 hours).
|
|
86
|
+
- `TODO`: Determine the correct logic or source for `actionableWindow` based on business requirements or template definitions.
|
|
87
|
+
- **Error Handling**: Enhance error handling in various methods to potentially update appointment status to an error state or implement more sophisticated retry mechanisms if critical operations fail.
|
|
88
|
+
|
|
89
|
+
### `handleAppointmentCreate(appointment: Appointment)`
|
|
90
|
+
|
|
91
|
+
- **Push Notifications**:
|
|
92
|
+
- `TODO`: Implement sending appointment confirmed push to practitioner (if they have expo tokens).
|
|
93
|
+
- `TODO`: Implement sending pending appointment push notification to clinic admin (if applicable).
|
|
94
|
+
|
|
95
|
+
### `handleAppointmentUpdate(before: Appointment, after: Appointment)`
|
|
96
|
+
|
|
97
|
+
- **Push Notifications**:
|
|
98
|
+
- For `PENDING -> CONFIRMED`: `TODO`: Send appointment confirmed push to practitioner.
|
|
99
|
+
- For `Any -> CANCELLED_*`: `TODO`: Send cancellation push notifications to patient and practitioner.
|
|
100
|
+
- For `Any -> COMPLETED`: `TODO`: Send review request push notification to patient.
|
|
101
|
+
- For `Any -> RESCHEDULED_BY_CLINIC`: `TODO`: Send reschedule proposal push notifications to patient (and practitioner if applicable).
|
|
102
|
+
- **Calendar Event Updates**:
|
|
103
|
+
- For `Any -> CANCELLED_*`: `TODO`: Update/cancel calendar event via `calendarAdminService.updateAppointmentCalendarEventStatus(after, CalendarEventStatus.CANCELED)` (or similar method).
|
|
104
|
+
- For `Any -> RESCHEDULED_BY_CLINIC`: `TODO`: Update calendar event to reflect proposed new time via `calendarAdminService`.
|
|
105
|
+
- **Reschedule Logic for Requirements (`RESCHEDULED_BY_CLINIC`)**:
|
|
106
|
+
- `TODO`: Handle RESCHEDULE logic for `PatientRequirementInstance` creation/cancellation more carefully based on the specific confirmation flow of a reschedule. For instance, when are new pre-requirements created if it's just a proposal?
|
|
107
|
+
- **Reschedule Notification to Practitioner (`RESCHEDULED_BY_CLINIC`)**:
|
|
108
|
+
- `TODO`: Implement sending reschedule proposal email/notification to the practitioner.
|
|
109
|
+
- **Independent Time Change**:
|
|
110
|
+
- `TODO`: Fully implement logic for when `appointmentStartTime` or `appointmentEndTime` changes without an accompanying status change. This might involve updating requirements and calendar events.
|
|
111
|
+
- `Logger.warn` currently flags this scenario.
|
|
112
|
+
- **Payment Status Change**:
|
|
113
|
+
- `TODO`: Implement logic for `before.paymentStatus !== after.paymentStatus`. This could involve sending payment confirmation notifications (email/push).
|
|
114
|
+
- **Review Added**:
|
|
115
|
+
- `TODO`: Implement logic for when `!before.reviewInfo && after.reviewInfo`. This could involve notifying the clinic admin or practitioner.
|
|
116
|
+
|
|
117
|
+
### `handleAppointmentDelete(deletedAppointment: Appointment)`
|
|
118
|
+
|
|
119
|
+
- **Notifications**: `TODO`: Send cancellation/deletion notifications if appropriate (though data is gone, so context might be limited).
|
|
120
|
+
- **Calendar Events**: Placeholder comment `// await this.calendarAdminService.deleteAppointmentCalendarEvents(deletedAppointment);` exists.
|
|
121
|
+
- `TODO`: Implement actual call to delete associated calendar events.
|
|
122
|
+
|
|
123
|
+
### `managePatientClinicPractitionerLinks(...)`
|
|
124
|
+
|
|
125
|
+
- **Robust Removal Logic**:
|
|
126
|
+
- `TODO`: For the 'cancel' action, implement more robust removal logic. Currently, it removes the patient ID regardless. It should ideally only remove the link if this was the last active/confirmed appointment connecting the patient to the practitioner/clinic.
|
|
127
|
+
|
|
128
|
+
This README should serve as a good guide to the current state and future development of the `AppointmentAggregationService`.
|