@blackcode_sa/metaestetics-api 1.6.4 → 1.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/admin/index.d.mts +236 -2
  2. package/dist/admin/index.d.ts +236 -2
  3. package/dist/admin/index.js +11251 -10447
  4. package/dist/admin/index.mjs +11251 -10447
  5. package/dist/backoffice/index.d.mts +2 -0
  6. package/dist/backoffice/index.d.ts +2 -0
  7. package/dist/index.d.mts +50 -77
  8. package/dist/index.d.ts +50 -77
  9. package/dist/index.js +77 -305
  10. package/dist/index.mjs +78 -306
  11. package/package.json +1 -1
  12. package/src/admin/aggregation/appointment/README.md +128 -0
  13. package/src/admin/aggregation/appointment/appointment.aggregation.service.ts +1053 -0
  14. package/src/admin/booking/README.md +125 -0
  15. package/src/admin/booking/booking.admin.ts +638 -3
  16. package/src/admin/calendar/calendar.admin.service.ts +183 -0
  17. package/src/admin/documentation-templates/document-manager.admin.ts +131 -0
  18. package/src/admin/mailing/appointment/appointment.mailing.service.ts +264 -0
  19. package/src/admin/mailing/appointment/templates/patient/appointment-confirmed.html +40 -0
  20. package/src/admin/mailing/base.mailing.service.ts +1 -1
  21. package/src/admin/mailing/index.ts +2 -0
  22. package/src/admin/notifications/notifications.admin.ts +397 -1
  23. package/src/backoffice/types/product.types.ts +2 -0
  24. package/src/services/appointment/appointment.service.ts +89 -182
  25. package/src/services/procedure/procedure.service.ts +1 -0
  26. package/src/types/appointment/index.ts +3 -1
  27. package/src/types/notifications/index.ts +4 -2
  28. package/src/types/procedure/index.ts +7 -0
  29. package/src/validations/appointment.schema.ts +2 -3
  30. package/src/validations/procedure.schema.ts +3 -0
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, httpsCallable } from "firebase/functions";
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 a new appointment.
12585
+ * Creates an appointment via the Cloud Function orchestrateAppointmentCreation
12810
12586
  *
12811
- * @param data Data needed to create the appointment
12587
+ * @param data - CreateAppointmentData object
12812
12588
  * @returns The created appointment
12813
12589
  */
12814
- async createAppointment(data) {
12590
+ async createAppointmentHttp(data) {
12815
12591
  try {
12816
- console.log("[APPOINTMENT_SERVICE] Creating appointment");
12817
- const validatedData = await createAppointmentSchema.parseAsync(data);
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 appointment = await createAppointmentUtil2(
12826
- this.db,
12827
- validatedData,
12828
- aggregatedInfo,
12829
- this.generateId.bind(this)
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] Appointment created with ID: ${appointment.id}`
12625
+ `[APPOINTMENT_SERVICE] Received response ${response.status}: ${response.statusText}`
12833
12626
  );
12834
- return appointment;
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("[APPOINTMENT_SERVICE] Error creating appointment:", 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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blackcode_sa/metaestetics-api",
3
3
  "private": false,
4
- "version": "1.6.4",
4
+ "version": "1.6.6",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.mjs",
@@ -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`.