@blackcode_sa/metaestetics-api 1.12.59 → 1.12.62

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/index.mjs CHANGED
@@ -4669,6 +4669,7 @@ var reviewSchema = z8.object({
4669
4669
  clinicReview: clinicReviewSchema.optional(),
4670
4670
  practitionerReview: practitionerReviewSchema.optional(),
4671
4671
  procedureReview: procedureReviewSchema.optional(),
4672
+ extendedProcedureReviews: z8.array(procedureReviewSchema).optional(),
4672
4673
  overallComment: z8.string().min(1).max(2e3),
4673
4674
  overallRating: z8.number().min(1).max(5)
4674
4675
  });
@@ -4677,13 +4678,14 @@ var createReviewSchema = z8.object({
4677
4678
  clinicReview: createClinicReviewSchema.optional(),
4678
4679
  practitionerReview: createPractitionerReviewSchema.optional(),
4679
4680
  procedureReview: createProcedureReviewSchema.optional(),
4681
+ extendedProcedureReviews: z8.array(createProcedureReviewSchema).optional(),
4680
4682
  overallComment: z8.string().min(1).max(2e3)
4681
4683
  }).refine(
4682
4684
  (data) => {
4683
- return data.clinicReview || data.practitionerReview || data.procedureReview;
4685
+ return data.clinicReview || data.practitionerReview || data.procedureReview || data.extendedProcedureReviews && data.extendedProcedureReviews.length > 0;
4684
4686
  },
4685
4687
  {
4686
- message: "At least one review type (clinic, practitioner, or procedure) must be provided",
4688
+ message: "At least one review type (clinic, practitioner, procedure, or extended procedure) must be provided",
4687
4689
  path: ["reviewType"]
4688
4690
  }
4689
4691
  );
@@ -17916,16 +17918,17 @@ var ReviewService = class extends BaseService {
17916
17918
  * @returns The created review
17917
17919
  */
17918
17920
  async createReview(data, appointmentId) {
17919
- var _a, _b, _c, _d, _e, _f;
17921
+ var _a, _b, _c, _d, _e, _f, _g, _h;
17920
17922
  try {
17921
17923
  console.log("\u{1F50D} ReviewService.createReview - Input data:", {
17922
17924
  appointmentId,
17923
17925
  hasClinicReview: !!data.clinicReview,
17924
17926
  hasPractitionerReview: !!data.practitionerReview,
17925
17927
  hasProcedureReview: !!data.procedureReview,
17926
- practitionerId: (_a = data.practitionerReview) == null ? void 0 : _a.practitionerId,
17927
- clinicId: (_b = data.clinicReview) == null ? void 0 : _b.clinicId,
17928
- procedureId: (_c = data.procedureReview) == null ? void 0 : _c.procedureId
17928
+ extendedProcedureReviewsCount: ((_a = data.extendedProcedureReviews) == null ? void 0 : _a.length) || 0,
17929
+ practitionerId: (_b = data.practitionerReview) == null ? void 0 : _b.practitionerId,
17930
+ clinicId: (_c = data.clinicReview) == null ? void 0 : _c.clinicId,
17931
+ procedureId: (_d = data.procedureReview) == null ? void 0 : _d.procedureId
17929
17932
  });
17930
17933
  const validatedData = createReviewSchema.parse(data);
17931
17934
  const ratings = [];
@@ -17965,6 +17968,20 @@ var ReviewService = class extends BaseService {
17965
17968
  data.procedureReview.overallRating = procedureAverage;
17966
17969
  ratings.push(procedureAverage);
17967
17970
  }
17971
+ if (data.extendedProcedureReviews && data.extendedProcedureReviews.length > 0) {
17972
+ data.extendedProcedureReviews.forEach((extendedReview) => {
17973
+ const extendedRatings = [
17974
+ extendedReview.effectivenessOfTreatment,
17975
+ extendedReview.outcomeExplanation,
17976
+ extendedReview.painManagement,
17977
+ extendedReview.followUpCare,
17978
+ extendedReview.valueForMoney
17979
+ ];
17980
+ const extendedAverage = this.calculateAverage(extendedRatings);
17981
+ extendedReview.overallRating = extendedAverage;
17982
+ ratings.push(extendedAverage);
17983
+ });
17984
+ }
17968
17985
  const overallRating = this.calculateAverage(ratings);
17969
17986
  const reviewId = this.generateId();
17970
17987
  if (data.clinicReview) {
@@ -17980,6 +17997,14 @@ var ReviewService = class extends BaseService {
17980
17997
  data.procedureReview.fullReviewId = reviewId;
17981
17998
  }
17982
17999
  const now = /* @__PURE__ */ new Date();
18000
+ if (data.extendedProcedureReviews && data.extendedProcedureReviews.length > 0) {
18001
+ data.extendedProcedureReviews.forEach((extendedReview) => {
18002
+ extendedReview.id = this.generateId();
18003
+ extendedReview.fullReviewId = reviewId;
18004
+ extendedReview.createdAt = now;
18005
+ extendedReview.updatedAt = now;
18006
+ });
18007
+ }
17983
18008
  const review = {
17984
18009
  id: reviewId,
17985
18010
  appointmentId,
@@ -17987,6 +18012,7 @@ var ReviewService = class extends BaseService {
17987
18012
  clinicReview: data.clinicReview,
17988
18013
  practitionerReview: data.practitionerReview,
17989
18014
  procedureReview: data.procedureReview,
18015
+ extendedProcedureReviews: data.extendedProcedureReviews,
17990
18016
  overallComment: data.overallComment,
17991
18017
  overallRating,
17992
18018
  createdAt: now,
@@ -18001,9 +18027,10 @@ var ReviewService = class extends BaseService {
18001
18027
  });
18002
18028
  console.log("\u2705 ReviewService.createReview - Review saved to Firestore:", {
18003
18029
  reviewId,
18004
- practitionerId: (_d = review.practitionerReview) == null ? void 0 : _d.practitionerId,
18005
- clinicId: (_e = review.clinicReview) == null ? void 0 : _e.clinicId,
18006
- procedureId: (_f = review.procedureReview) == null ? void 0 : _f.procedureId
18030
+ practitionerId: (_e = review.practitionerReview) == null ? void 0 : _e.practitionerId,
18031
+ clinicId: (_f = review.clinicReview) == null ? void 0 : _f.clinicId,
18032
+ procedureId: (_g = review.procedureReview) == null ? void 0 : _g.procedureId,
18033
+ extendedProcedureReviewsCount: ((_h = review.extendedProcedureReviews) == null ? void 0 : _h.length) || 0
18007
18034
  });
18008
18035
  return review;
18009
18036
  } catch (error) {
@@ -18019,7 +18046,7 @@ var ReviewService = class extends BaseService {
18019
18046
  * @returns The review with entity names if found, null otherwise
18020
18047
  */
18021
18048
  async getReview(reviewId) {
18022
- var _a, _b, _c;
18049
+ var _a, _b, _c, _d, _e;
18023
18050
  console.log("\u{1F50D} ReviewService.getReview - Getting review:", reviewId);
18024
18051
  const docRef = doc37(this.db, REVIEWS_COLLECTION, reviewId);
18025
18052
  const docSnap = await getDoc38(docRef);
@@ -18053,12 +18080,27 @@ var ReviewService = class extends BaseService {
18053
18080
  procedureName: appointment.procedureInfo.name
18054
18081
  };
18055
18082
  }
18083
+ if (enhancedReview.extendedProcedureReviews && enhancedReview.extendedProcedureReviews.length > 0) {
18084
+ const extendedProcedures = ((_a = appointment.metadata) == null ? void 0 : _a.extendedProcedures) || [];
18085
+ enhancedReview.extendedProcedureReviews = enhancedReview.extendedProcedureReviews.map((extendedReview) => {
18086
+ const procedureInfo = extendedProcedures.find(
18087
+ (ep) => ep.procedureId === extendedReview.procedureId
18088
+ );
18089
+ if (procedureInfo) {
18090
+ return {
18091
+ ...extendedReview,
18092
+ procedureName: procedureInfo.procedureName
18093
+ };
18094
+ }
18095
+ return extendedReview;
18096
+ });
18097
+ }
18056
18098
  if (appointment.patientInfo) {
18057
18099
  enhancedReview.patientName = appointment.patientInfo.fullName;
18058
18100
  }
18059
18101
  console.log("\u2705 ReviewService.getReview - Enhanced review:", {
18060
18102
  reviewId,
18061
- hasEntityNames: !!(((_a = enhancedReview.clinicReview) == null ? void 0 : _a.clinicName) || ((_b = enhancedReview.practitionerReview) == null ? void 0 : _b.practitionerName) || ((_c = enhancedReview.procedureReview) == null ? void 0 : _c.procedureName) || enhancedReview.patientName)
18103
+ hasEntityNames: !!(((_b = enhancedReview.clinicReview) == null ? void 0 : _b.clinicName) || ((_c = enhancedReview.practitionerReview) == null ? void 0 : _c.practitionerName) || ((_d = enhancedReview.procedureReview) == null ? void 0 : _d.procedureName) || enhancedReview.patientName || ((_e = enhancedReview.extendedProcedureReviews) == null ? void 0 : _e.some((epr) => epr.procedureName)))
18062
18104
  });
18063
18105
  return enhancedReview;
18064
18106
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blackcode_sa/metaestetics-api",
3
3
  "private": false,
4
- "version": "1.12.59",
4
+ "version": "1.12.62",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -56,16 +56,16 @@
56
56
  "author": "Blackcode SA",
57
57
  "license": "MIT",
58
58
  "scripts": {
59
- "clean": "rm -rf dist",
59
+ "clean": "node -e \"const fs=require('fs');const path=require('path');const dist='dist';if(fs.existsSync(dist))fs.rmSync(dist,{recursive:true,force:true})\"",
60
60
  "build:main": "tsup src/index.ts --format cjs,esm --dts --external react-native",
61
61
  "build:admin": "tsup src/admin/index.ts --format cjs,esm --dts --out-dir dist/admin --external react-native",
62
62
  "build:backoffice": "tsup src/backoffice/index.ts --format cjs,esm --dts --out-dir dist/backoffice",
63
63
  "build": "npm run clean && npm run build:main && npm run build:admin && npm run build:backoffice",
64
64
  "dev": "tsup src/index.ts --format cjs,esm --dts --watch --external react-native",
65
65
  "prepublishOnly": "npm run build",
66
- "publish:patch": "bash ./publish.sh patch",
67
- "publish:minor": "bash ./publish.sh minor",
68
- "publish:major": "bash ./publish.sh major",
66
+ "publish:patch": "node scripts/publish.js patch",
67
+ "publish:minor": "node scripts/publish.js minor",
68
+ "publish:major": "node scripts/publish.js major",
69
69
  "test": "jest",
70
70
  "test:watch": "jest --watch",
71
71
  "test:coverage": "jest --coverage"
@@ -25,6 +25,8 @@ import {
25
25
  } from '../../../types/patient';
26
26
  import { Practitioner, PRACTITIONERS_COLLECTION } from '../../../types/practitioner';
27
27
  import { Clinic, CLINICS_COLLECTION } from '../../../types/clinic';
28
+ import { Procedure, PROCEDURES_COLLECTION } from '../../../types/procedure';
29
+ import { RequirementSourceProcedure } from '../../../types/patient/patient-requirements';
28
30
  // import { UserRole } from "../../../types"; // Not directly used
29
31
 
30
32
  // Dependent Admin Services
@@ -38,6 +40,14 @@ import { CalendarEventStatus } from '../../../types/calendar';
38
40
 
39
41
  // Mailgun client will be injected via constructor
40
42
 
43
+ /**
44
+ * Type for requirement with source procedure tracking
45
+ */
46
+ type RequirementWithSource = {
47
+ requirement: RequirementTemplate;
48
+ sourceProcedures: RequirementSourceProcedure[];
49
+ };
50
+
41
51
  /**
42
52
  * @class AppointmentAggregationService
43
53
  * @description Handles aggregation tasks and side effects related to appointment lifecycle events.
@@ -861,11 +871,141 @@ export class AppointmentAggregationService {
861
871
  }
862
872
  }
863
873
 
874
+ /**
875
+ * Fetches post-requirements from a procedure document
876
+ * @param procedureId - The procedure ID to fetch requirements from
877
+ * @returns Promise resolving to array of post-requirements with source procedure info
878
+ */
879
+ private async fetchPostRequirementsFromProcedure(
880
+ procedureId: string,
881
+ ): Promise<RequirementWithSource[]> {
882
+ try {
883
+ const procedureDoc = await this.db.collection(PROCEDURES_COLLECTION).doc(procedureId).get();
884
+ if (!procedureDoc.exists) {
885
+ Logger.warn(`[AggService] Procedure ${procedureId} not found when fetching requirements`);
886
+ return [];
887
+ }
888
+
889
+ const procedure = procedureDoc.data() as Procedure;
890
+ const postRequirements = procedure.postRequirements || [];
891
+
892
+ if (postRequirements.length === 0) {
893
+ return [];
894
+ }
895
+
896
+ return postRequirements.map(req => ({
897
+ requirement: req,
898
+ sourceProcedures: [
899
+ {
900
+ procedureId: procedure.id,
901
+ procedureName: procedure.name,
902
+ },
903
+ ],
904
+ }));
905
+ } catch (error) {
906
+ Logger.error(
907
+ `[AggService] Error fetching post-requirements from procedure ${procedureId}:`,
908
+ error,
909
+ );
910
+ return [];
911
+ }
912
+ }
913
+
914
+ /**
915
+ * Collects all post-requirements from primary and extended procedures
916
+ * @param appointment - The appointment to collect requirements for
917
+ * @returns Promise resolving to array of requirements with source procedures
918
+ */
919
+ private async collectAllPostRequirements(
920
+ appointment: Appointment,
921
+ ): Promise<RequirementWithSource[]> {
922
+ const allRequirements: RequirementWithSource[] = [];
923
+
924
+ // Fetch from primary procedure
925
+ if (appointment.procedureId) {
926
+ const primaryRequirements = await this.fetchPostRequirementsFromProcedure(
927
+ appointment.procedureId,
928
+ );
929
+ allRequirements.push(...primaryRequirements);
930
+ }
931
+
932
+ // Fetch from extended procedures
933
+ const extendedProcedures = appointment.metadata?.extendedProcedures || [];
934
+ if (extendedProcedures.length > 0) {
935
+ Logger.info(
936
+ `[AggService] Fetching post-requirements from ${extendedProcedures.length} extended procedures`,
937
+ );
938
+
939
+ const extendedRequirementsPromises = extendedProcedures.map(extProc =>
940
+ this.fetchPostRequirementsFromProcedure(extProc.procedureId),
941
+ );
942
+
943
+ const extendedRequirementsArrays = await Promise.all(extendedRequirementsPromises);
944
+ extendedRequirementsArrays.forEach(reqs => {
945
+ allRequirements.push(...reqs);
946
+ });
947
+ }
948
+
949
+ return allRequirements;
950
+ }
951
+
952
+ /**
953
+ * Generates a unique key for a requirement based on ID and timeframe
954
+ * @param requirement - The requirement to generate a key for
955
+ * @returns Unique key string
956
+ */
957
+ private getRequirementKey(requirement: RequirementTemplate): string {
958
+ const timeframeSig = JSON.stringify({
959
+ duration: requirement.timeframe?.duration || 0,
960
+ unit: requirement.timeframe?.unit || '',
961
+ notifyAt: (requirement.timeframe?.notifyAt || []).slice().sort((a, b) => a - b),
962
+ });
963
+ return `${requirement.id}:${timeframeSig}`;
964
+ }
965
+
966
+ /**
967
+ * Deduplicates requirements based on requirement ID and timeframe
968
+ * Merges source procedures when requirements match
969
+ * @param requirements - Array of requirements with sources
970
+ * @returns Deduplicated array of requirements
971
+ */
972
+ private deduplicateRequirements(
973
+ requirements: RequirementWithSource[],
974
+ ): RequirementWithSource[] {
975
+ const requirementMap = new Map<string, RequirementWithSource>();
976
+
977
+ for (const reqWithSource of requirements) {
978
+ const key = this.getRequirementKey(reqWithSource.requirement);
979
+
980
+ if (requirementMap.has(key)) {
981
+ // Merge source procedures
982
+ const existing = requirementMap.get(key)!;
983
+ const existingProcedureIds = new Set(
984
+ existing.sourceProcedures.map(sp => sp.procedureId),
985
+ );
986
+
987
+ // Add new source procedures that don't already exist
988
+ reqWithSource.sourceProcedures.forEach(sp => {
989
+ if (!existingProcedureIds.has(sp.procedureId)) {
990
+ existing.sourceProcedures.push(sp);
991
+ }
992
+ });
993
+ } else {
994
+ // New requirement, add it
995
+ requirementMap.set(key, {
996
+ requirement: reqWithSource.requirement,
997
+ sourceProcedures: [...reqWithSource.sourceProcedures],
998
+ });
999
+ }
1000
+ }
1001
+
1002
+ return Array.from(requirementMap.values());
1003
+ }
1004
+
864
1005
  /**
865
1006
  * Creates POST_APPOINTMENT PatientRequirementInstance documents for a given appointment.
866
- * Uses the `appointment.postProcedureRequirements` array.
867
- * For each active POST requirement template, it constructs a new PatientRequirementInstance document
868
- * with derived instructions and batch writes them to Firestore under the patient's `patient_requirements` subcollection.
1007
+ * Fetches requirements from primary and extended procedures, deduplicates them,
1008
+ * and creates requirement instances with source procedure tracking.
869
1009
  *
870
1010
  * @param {Appointment} appointment - The appointment for which to create post-requirement instances.
871
1011
  * @returns {Promise<void>} A promise that resolves when the operation is complete.
@@ -882,39 +1022,49 @@ export class AppointmentAggregationService {
882
1022
  return;
883
1023
  }
884
1024
 
885
- if (
886
- !appointment.postProcedureRequirements ||
887
- appointment.postProcedureRequirements.length === 0
888
- ) {
1025
+ try {
1026
+ // Collect all post-requirements from primary and extended procedures
1027
+ const allRequirements = await this.collectAllPostRequirements(appointment);
1028
+
1029
+ if (allRequirements.length === 0) {
1030
+ Logger.info(
1031
+ `[AggService] No post-requirements found from any procedures for appointment ${appointment.id}. Nothing to create.`,
1032
+ );
1033
+ return;
1034
+ }
1035
+
1036
+ // Deduplicate requirements based on ID + timeframe
1037
+ const deduplicatedRequirements = this.deduplicateRequirements(allRequirements);
1038
+
889
1039
  Logger.info(
890
- `[AggService] No postProcedureRequirements found on appointment ${appointment.id}. Nothing to create.`,
1040
+ `[AggService] Found ${allRequirements.length} total post-requirements, ${deduplicatedRequirements.length} after deduplication`,
891
1041
  );
892
- return;
893
- }
894
-
895
- try {
896
- const batch = this.db.batch();
897
- let instancesCreatedCount = 0;
898
- // Store created instances for fallback direct creation if needed
899
- let createdInstances = [];
900
1042
 
901
- // Log more details about the post-requirements
1043
+ // Log details about the deduplicated requirements
902
1044
  Logger.info(
903
- `[AggService] Found ${
904
- appointment.postProcedureRequirements.length
905
- } post-requirements to process: ${JSON.stringify(
906
- appointment.postProcedureRequirements.map(r => ({
907
- id: r.id,
908
- name: r.name,
909
- type: r.type,
910
- isActive: r.isActive,
911
- hasTimeframe: !!r.timeframe,
912
- notifyAtLength: r.timeframe?.notifyAt?.length || 0,
1045
+ `[AggService] Processing deduplicated post-requirements: ${JSON.stringify(
1046
+ deduplicatedRequirements.map(r => ({
1047
+ id: r.requirement.id,
1048
+ name: r.requirement.name,
1049
+ type: r.requirement.type,
1050
+ isActive: r.requirement.isActive,
1051
+ hasTimeframe: !!r.requirement.timeframe,
1052
+ notifyAtLength: r.requirement.timeframe?.notifyAt?.length || 0,
1053
+ sourceProcedures: r.sourceProcedures.map(sp => ({
1054
+ procedureId: sp.procedureId,
1055
+ procedureName: sp.procedureName,
1056
+ })),
913
1057
  })),
914
1058
  )}`,
915
1059
  );
916
1060
 
917
- for (const template of appointment.postProcedureRequirements) {
1061
+ const batch = this.db.batch();
1062
+ let instancesCreatedCount = 0;
1063
+ // Store created instances for fallback direct creation if needed
1064
+ let createdInstances = [];
1065
+
1066
+ for (const reqWithSource of deduplicatedRequirements) {
1067
+ const template = reqWithSource.requirement;
918
1068
  if (!template) {
919
1069
  Logger.warn(
920
1070
  `[AggService] Found null/undefined template in postProcedureRequirements array`,
@@ -1000,6 +1150,7 @@ export class AppointmentAggregationService {
1000
1150
  requirementImportance: template.importance,
1001
1151
  overallStatus: PatientRequirementOverallStatus.ACTIVE,
1002
1152
  instructions: instructions,
1153
+ sourceProcedures: reqWithSource.sourceProcedures, // Track which procedures this requirement comes from
1003
1154
  createdAt: admin.firestore.FieldValue.serverTimestamp() as any,
1004
1155
  updatedAt: admin.firestore.FieldValue.serverTimestamp() as any,
1005
1156
  };
@@ -1012,6 +1163,10 @@ export class AppointmentAggregationService {
1012
1163
  appointmentId: newInstanceData.appointmentId,
1013
1164
  requirementName: newInstanceData.requirementName,
1014
1165
  instructionsCount: newInstanceData.instructions.length,
1166
+ sourceProcedures: newInstanceData.sourceProcedures?.map(sp => ({
1167
+ procedureId: sp.procedureId,
1168
+ procedureName: sp.procedureName,
1169
+ })) || [],
1015
1170
  })}`,
1016
1171
  );
1017
1172
 
@@ -65,6 +65,18 @@ export class ReviewsAggregationService {
65
65
  );
66
66
  }
67
67
 
68
+ // Update extended procedures if they exist
69
+ if (review.extendedProcedureReviews && review.extendedProcedureReviews.length > 0) {
70
+ console.log(
71
+ `[ReviewsAggregationService] Processing ${review.extendedProcedureReviews.length} extended procedure reviews`
72
+ );
73
+ review.extendedProcedureReviews.forEach((extendedReview) => {
74
+ updatePromises.push(
75
+ this.updateProcedureReviewInfo(extendedReview.procedureId)
76
+ );
77
+ });
78
+ }
79
+
68
80
  // Wait for all updates to complete
69
81
  await Promise.all(updatePromises);
70
82
  console.log(
@@ -117,6 +129,22 @@ export class ReviewsAggregationService {
117
129
  );
118
130
  }
119
131
 
132
+ // Update extended procedures if they exist
133
+ if (review.extendedProcedureReviews && review.extendedProcedureReviews.length > 0) {
134
+ console.log(
135
+ `[ReviewsAggregationService] Processing deletion of ${review.extendedProcedureReviews.length} extended procedure reviews`
136
+ );
137
+ review.extendedProcedureReviews.forEach((extendedReview) => {
138
+ updatePromises.push(
139
+ this.updateProcedureReviewInfo(
140
+ extendedReview.procedureId,
141
+ extendedReview,
142
+ true
143
+ )
144
+ );
145
+ });
146
+ }
147
+
120
148
  // Wait for all updates to complete
121
149
  await Promise.all(updatePromises);
122
150
  console.log(
@@ -430,14 +458,32 @@ export class ReviewsAggregationService {
430
458
  recommendationPercentage: 0,
431
459
  };
432
460
 
433
- // Get all reviews for this procedure
434
- const reviewsQuery = await this.db
461
+ // Get all reviews for this procedure (both main and extended)
462
+ // We need to check both procedureReview.procedureId and extendedProcedureReviews array
463
+ const allReviewsQuery = await this.db
435
464
  .collection(REVIEWS_COLLECTION)
436
- .where("procedureReview.procedureId", "==", procedureId)
437
465
  .get();
438
466
 
439
- // If we're removing the last review or there are no reviews, set default values
440
- if ((isRemoval && reviewsQuery.size <= 1) || reviewsQuery.empty) {
467
+ // Filter reviews that contain this procedure (either as main or extended)
468
+ const reviews = allReviewsQuery.docs.map((doc) => doc.data() as Review);
469
+ const procedureReviews: ProcedureReview[] = [];
470
+
471
+ reviews.forEach((review) => {
472
+ // Check if this is the main procedure
473
+ if (review.procedureReview && review.procedureReview.procedureId === procedureId) {
474
+ procedureReviews.push(review.procedureReview);
475
+ }
476
+ // Check if this is in extended procedures
477
+ if (review.extendedProcedureReviews && review.extendedProcedureReviews.length > 0) {
478
+ const matchingExtended = review.extendedProcedureReviews.filter(
479
+ (extReview) => extReview.procedureId === procedureId
480
+ );
481
+ procedureReviews.push(...matchingExtended);
482
+ }
483
+ });
484
+
485
+ // If there are no reviews, set default values
486
+ if (procedureReviews.length === 0) {
441
487
  const updatedReviewInfo: ProcedureReviewInfo = {
442
488
  totalReviews: 0,
443
489
  averageRating: 0,
@@ -460,12 +506,6 @@ export class ReviewsAggregationService {
460
506
  return updatedReviewInfo;
461
507
  }
462
508
 
463
- // Calculate new averages from all reviews
464
- const reviews = reviewsQuery.docs.map((doc) => doc.data() as Review);
465
- const procedureReviews = reviews
466
- .map((review) => review.procedureReview)
467
- .filter((review): review is ProcedureReview => review !== undefined);
468
-
469
509
  // Calculate averages
470
510
  let totalRating = 0;
471
511
  let totalEffectivenessOfTreatment = 0;
@@ -599,11 +639,19 @@ export class ReviewsAggregationService {
599
639
  review.procedureReview.isVerified = true;
600
640
  }
601
641
 
642
+ // Update extended procedure reviews if they exist
643
+ if (review.extendedProcedureReviews && review.extendedProcedureReviews.length > 0) {
644
+ review.extendedProcedureReviews.forEach((extReview) => {
645
+ extReview.isVerified = true;
646
+ });
647
+ }
648
+
602
649
  // Update the review
603
650
  batch.update(reviewRef, {
604
651
  clinicReview: review.clinicReview,
605
652
  practitionerReview: review.practitionerReview,
606
653
  procedureReview: review.procedureReview,
654
+ extendedProcedureReviews: review.extendedProcedureReviews,
607
655
  updatedAt: admin.firestore.FieldValue.serverTimestamp(),
608
656
  });
609
657
 
@@ -45,6 +45,7 @@ export class ReviewService extends BaseService {
45
45
  hasClinicReview: !!data.clinicReview,
46
46
  hasPractitionerReview: !!data.practitionerReview,
47
47
  hasProcedureReview: !!data.procedureReview,
48
+ extendedProcedureReviewsCount: data.extendedProcedureReviews?.length || 0,
48
49
  practitionerId: data.practitionerReview?.practitionerId,
49
50
  clinicId: data.clinicReview?.clinicId,
50
51
  procedureId: data.procedureReview?.procedureId,
@@ -95,6 +96,22 @@ export class ReviewService extends BaseService {
95
96
  ratings.push(procedureAverage);
96
97
  }
97
98
 
99
+ // Process extended procedure reviews
100
+ if (data.extendedProcedureReviews && data.extendedProcedureReviews.length > 0) {
101
+ data.extendedProcedureReviews.forEach((extendedReview) => {
102
+ const extendedRatings = [
103
+ extendedReview.effectivenessOfTreatment,
104
+ extendedReview.outcomeExplanation,
105
+ extendedReview.painManagement,
106
+ extendedReview.followUpCare,
107
+ extendedReview.valueForMoney,
108
+ ];
109
+ const extendedAverage = this.calculateAverage(extendedRatings);
110
+ extendedReview.overallRating = extendedAverage;
111
+ ratings.push(extendedAverage);
112
+ });
113
+ }
114
+
98
115
  const overallRating = this.calculateAverage(ratings);
99
116
 
100
117
  // Generate a unique ID for the main review
@@ -118,6 +135,16 @@ export class ReviewService extends BaseService {
118
135
 
119
136
  // Create the review object with timestamps
120
137
  const now = new Date();
138
+
139
+ // Add IDs to extended procedure reviews
140
+ if (data.extendedProcedureReviews && data.extendedProcedureReviews.length > 0) {
141
+ data.extendedProcedureReviews.forEach((extendedReview) => {
142
+ extendedReview.id = this.generateId();
143
+ extendedReview.fullReviewId = reviewId;
144
+ extendedReview.createdAt = now;
145
+ extendedReview.updatedAt = now;
146
+ });
147
+ }
121
148
  const review: Review = {
122
149
  id: reviewId,
123
150
  appointmentId,
@@ -125,6 +152,7 @@ export class ReviewService extends BaseService {
125
152
  clinicReview: data.clinicReview,
126
153
  practitionerReview: data.practitionerReview,
127
154
  procedureReview: data.procedureReview,
155
+ extendedProcedureReviews: data.extendedProcedureReviews,
128
156
  overallComment: data.overallComment,
129
157
  overallRating,
130
158
  createdAt: now,
@@ -147,6 +175,7 @@ export class ReviewService extends BaseService {
147
175
  practitionerId: review.practitionerReview?.practitionerId,
148
176
  clinicId: review.clinicReview?.clinicId,
149
177
  procedureId: review.procedureReview?.procedureId,
178
+ extendedProcedureReviewsCount: review.extendedProcedureReviews?.length || 0,
150
179
  });
151
180
 
152
181
  // Note: Related entity updates (clinic, practitioner, procedure) are now handled
@@ -212,6 +241,23 @@ export class ReviewService extends BaseService {
212
241
  };
213
242
  }
214
243
 
244
+ // Enhance extended procedure reviews with names
245
+ if (enhancedReview.extendedProcedureReviews && enhancedReview.extendedProcedureReviews.length > 0) {
246
+ const extendedProcedures = appointment.metadata?.extendedProcedures || [];
247
+ enhancedReview.extendedProcedureReviews = enhancedReview.extendedProcedureReviews.map((extendedReview) => {
248
+ const procedureInfo = extendedProcedures.find(
249
+ (ep) => ep.procedureId === extendedReview.procedureId
250
+ );
251
+ if (procedureInfo) {
252
+ return {
253
+ ...extendedReview,
254
+ procedureName: procedureInfo.procedureName,
255
+ };
256
+ }
257
+ return extendedReview;
258
+ });
259
+ }
260
+
215
261
  // Add patient name to the main review object
216
262
  if (appointment.patientInfo) {
217
263
  enhancedReview.patientName = appointment.patientInfo.fullName;
@@ -223,7 +269,8 @@ export class ReviewService extends BaseService {
223
269
  enhancedReview.clinicReview?.clinicName ||
224
270
  enhancedReview.practitionerReview?.practitionerName ||
225
271
  enhancedReview.procedureReview?.procedureName ||
226
- enhancedReview.patientName
272
+ enhancedReview.patientName ||
273
+ enhancedReview.extendedProcedureReviews?.some(epr => epr.procedureName)
227
274
  ),
228
275
  });
229
276
 
@@ -50,6 +50,14 @@ export enum PatientRequirementOverallStatus {
50
50
  FAILED_TO_PROCESS = "failedToProcess", // An error occurred during its creation or initial processing.
51
51
  }
52
52
 
53
+ /**
54
+ * Represents source procedure information for a requirement instance
55
+ */
56
+ export interface RequirementSourceProcedure {
57
+ procedureId: string;
58
+ procedureName: string;
59
+ }
60
+
53
61
  /**
54
62
  * Represents an instance of a backoffice Requirement, tailored to a specific patient and appointment.
55
63
  * This document lives in the patient's subcollection: `patients/{patientId}/patientRequirements/{instanceId}`.
@@ -70,6 +78,9 @@ export interface PatientRequirementInstance {
70
78
  // Contains each specific timed instruction derived from the Requirement's timeframe.notifyAt
71
79
  instructions: PatientRequirementInstruction[];
72
80
 
81
+ // NEW: Track which procedure(s) this requirement comes from
82
+ sourceProcedures?: RequirementSourceProcedure[];
83
+
73
84
  // Timestamps for the instance itself
74
85
  createdAt: Timestamp;
75
86
  updatedAt: Timestamp;