@blackcode_sa/metaestetics-api 1.14.44 → 1.14.45

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
@@ -3758,8 +3758,14 @@ var finalizedDetailsSchema = z3.object({
3758
3758
  var beforeAfterPerZoneSchema = z3.object({
3759
3759
  before: mediaResourceSchema.nullable(),
3760
3760
  after: mediaResourceSchema.nullable(),
3761
+ beforeNote: z3.string().nullable().optional(),
3761
3762
  afterNote: z3.string().nullable().optional(),
3762
- beforeNote: z3.string().nullable().optional()
3763
+ showToPatient: z3.boolean().optional().default(false),
3764
+ beforeNoteVisibleToPatient: z3.boolean().optional().default(false),
3765
+ afterNoteVisibleToPatient: z3.boolean().optional().default(false),
3766
+ visibilityUpdatedAt: z3.any().optional(),
3767
+ // Timestamp
3768
+ visibilityUpdatedBy: z3.string().optional()
3763
3769
  });
3764
3770
  var billingPerZoneSchema = z3.object({
3765
3771
  Product: z3.string().min(MIN_STRING_LENGTH, "Product name is required"),
@@ -3817,11 +3823,9 @@ var zoneItemDataSchema = z3.object({
3817
3823
  }
3818
3824
  ),
3819
3825
  notes: z3.string().max(MAX_STRING_LENGTH_LONG, "Notes too long").optional(),
3826
+ notesVisibleToPatient: z3.boolean().optional().default(false),
3820
3827
  subtotal: z3.number().min(0, "Subtotal must be non-negative").optional(),
3821
3828
  ionNumber: z3.string().optional(),
3822
- lotNumber: z3.string().max(MAX_STRING_LENGTH, "Lot number too long").optional(),
3823
- expiryDate: z3.string().optional(),
3824
- // ISO date string (YYYY-MM-DD format)
3825
3829
  createdAt: z3.string().optional(),
3826
3830
  updatedAt: z3.string().optional()
3827
3831
  }).refine(
@@ -3883,7 +3887,10 @@ var appointmentMetadataSchema = z3.object({
3883
3887
  recommendedProcedures: z3.array(recommendedProcedureSchema).optional().default([]),
3884
3888
  zoneBilling: z3.record(z3.string(), billingPerZoneSchema).nullable().optional(),
3885
3889
  finalbilling: finalBillingSchema.nullable(),
3886
- finalizationNotes: z3.string().nullable()
3890
+ finalizationNotesShared: z3.string().nullable().optional(),
3891
+ finalizationNotesInternal: z3.string().nullable().optional(),
3892
+ finalizationNotes: z3.string().nullable().optional()
3893
+ // @deprecated - kept for backward compatibility
3887
3894
  });
3888
3895
  var createAppointmentSchema = z3.object({
3889
3896
  clinicBranchId: z3.string().min(MIN_STRING_LENGTH, "Clinic branch ID is required"),
@@ -4715,10 +4722,14 @@ function initializeMetadata(appointment) {
4715
4722
  extendedProcedures: [],
4716
4723
  recommendedProcedures: [],
4717
4724
  finalbilling: null,
4725
+ finalizationNotesShared: null,
4726
+ finalizationNotesInternal: null,
4718
4727
  finalizationNotes: null
4728
+ // @deprecated - kept for backward compatibility
4719
4729
  };
4720
4730
  }
4721
4731
  async function addItemToZoneUtil(db, appointmentId, zoneId, item) {
4732
+ var _a;
4722
4733
  validateZoneKeyFormat(zoneId);
4723
4734
  const appointment = await getAppointmentOrThrow(db, appointmentId);
4724
4735
  const metadata = initializeMetadata(appointment);
@@ -4732,6 +4743,8 @@ async function addItemToZoneUtil(db, appointmentId, zoneId, item) {
4732
4743
  parentZone: zoneId,
4733
4744
  // Set parentZone to the zone key
4734
4745
  subtotal: calculateItemSubtotal(item),
4746
+ // Set default visibility to false (privacy-first) if notes exist and visibility not explicitly set
4747
+ notesVisibleToPatient: (_a = item.notesVisibleToPatient) != null ? _a : item.notes ? false : void 0,
4735
4748
  createdAt: now,
4736
4749
  updatedAt: now
4737
4750
  };
@@ -5231,7 +5244,17 @@ async function getRecommendedProceduresUtil(db, appointmentId) {
5231
5244
 
5232
5245
  // src/services/appointment/utils/zone-photo.utils.ts
5233
5246
  import { updateDoc as updateDoc6, serverTimestamp as serverTimestamp6, doc as doc9 } from "firebase/firestore";
5234
- async function updateZonePhotoEntryUtil(db, appointmentId, zoneId, photoIndex, updates) {
5247
+ function addVisibilityAudit(updates, doctorId) {
5248
+ if (updates.showToPatient !== void 0 || updates.beforeNoteVisibleToPatient !== void 0 || updates.afterNoteVisibleToPatient !== void 0) {
5249
+ return {
5250
+ ...updates,
5251
+ visibilityUpdatedAt: serverTimestamp6(),
5252
+ visibilityUpdatedBy: doctorId
5253
+ };
5254
+ }
5255
+ return updates;
5256
+ }
5257
+ async function updateZonePhotoEntryUtil(db, appointmentId, zoneId, photoIndex, updates, doctorId) {
5235
5258
  var _a;
5236
5259
  const appointment = await getAppointmentOrThrow(db, appointmentId);
5237
5260
  const zonePhotos = (_a = appointment.metadata) == null ? void 0 : _a.zonePhotos;
@@ -5242,11 +5265,12 @@ async function updateZonePhotoEntryUtil(db, appointmentId, zoneId, photoIndex, u
5242
5265
  if (photoIndex < 0 || photoIndex >= zoneArray.length) {
5243
5266
  throw new Error(`Invalid photo index ${photoIndex} for zone ${zoneId}. Must be between 0 and ${zoneArray.length - 1}`);
5244
5267
  }
5268
+ const updatesWithAudit = doctorId ? addVisibilityAudit(updates, doctorId) : updates;
5245
5269
  const updatedZonePhotos = { ...zonePhotos };
5246
5270
  updatedZonePhotos[zoneId] = [...zoneArray];
5247
5271
  updatedZonePhotos[zoneId][photoIndex] = {
5248
5272
  ...zoneArray[photoIndex],
5249
- ...updates
5273
+ ...updatesWithAudit
5250
5274
  };
5251
5275
  const appointmentRef = doc9(db, APPOINTMENTS_COLLECTION, appointmentId);
5252
5276
  await updateDoc6(appointmentRef, {
@@ -5290,6 +5314,20 @@ async function getZonePhotoEntryUtil(db, appointmentId, zoneId, photoIndex) {
5290
5314
  }
5291
5315
  return zoneArray[photoIndex];
5292
5316
  }
5317
+ async function updateZonePhotoVisibilityUtil(db, appointmentId, zoneId, photoIndex, showToPatient, doctorId) {
5318
+ return updateZonePhotoEntryUtil(
5319
+ db,
5320
+ appointmentId,
5321
+ zoneId,
5322
+ photoIndex,
5323
+ { showToPatient },
5324
+ doctorId
5325
+ );
5326
+ }
5327
+ async function updateZonePhotoNoteVisibilityUtil(db, appointmentId, zoneId, photoIndex, noteType, visibleToPatient, doctorId) {
5328
+ const updates = noteType === "before" ? { beforeNoteVisibleToPatient: visibleToPatient } : { afterNoteVisibleToPatient: visibleToPatient };
5329
+ return updateZonePhotoEntryUtil(db, appointmentId, zoneId, photoIndex, updates, doctorId);
5330
+ }
5293
5331
 
5294
5332
  // src/services/appointment/appointment.service.ts
5295
5333
  var AppointmentService = class extends BaseService {
@@ -6156,6 +6194,7 @@ var AppointmentService = class extends BaseService {
6156
6194
  * @returns The updated appointment
6157
6195
  */
6158
6196
  async updateAppointmentZonePhoto(appointmentId, zoneId, photoType, mediaMetadata, notes) {
6197
+ var _a, _b, _c, _d;
6159
6198
  try {
6160
6199
  console.log(
6161
6200
  `[APPOINTMENT_SERVICE] Updating appointment metadata for ${photoType} photo in zone ${zoneId}`
@@ -6173,7 +6212,10 @@ var AppointmentService = class extends BaseService {
6173
6212
  recommendedProcedures: [],
6174
6213
  zoneBilling: null,
6175
6214
  finalbilling: null,
6215
+ finalizationNotesShared: null,
6216
+ finalizationNotesInternal: null,
6176
6217
  finalizationNotes: null
6218
+ // @deprecated - kept for backward compatibility
6177
6219
  };
6178
6220
  let currentZonePhotos = {};
6179
6221
  if (currentMetadata.zonePhotos) {
@@ -6188,7 +6230,11 @@ var AppointmentService = class extends BaseService {
6188
6230
  before: oldData.before || null,
6189
6231
  after: oldData.after || null,
6190
6232
  beforeNote: null,
6191
- afterNote: null
6233
+ afterNote: null,
6234
+ showToPatient: false,
6235
+ // Default: not visible to patient (privacy-first)
6236
+ beforeNoteVisibleToPatient: false,
6237
+ afterNoteVisibleToPatient: false
6192
6238
  }
6193
6239
  ];
6194
6240
  }
@@ -6201,7 +6247,13 @@ var AppointmentService = class extends BaseService {
6201
6247
  before: photoType === "before" ? mediaMetadata.url : null,
6202
6248
  after: photoType === "after" ? mediaMetadata.url : null,
6203
6249
  beforeNote: photoType === "before" ? notes || null : null,
6204
- afterNote: photoType === "after" ? notes || null : null
6250
+ afterNote: photoType === "after" ? notes || null : null,
6251
+ showToPatient: false,
6252
+ // Default: not visible to patient
6253
+ beforeNoteVisibleToPatient: false,
6254
+ // Default: not visible to patient
6255
+ afterNoteVisibleToPatient: false
6256
+ // Default: not visible to patient
6205
6257
  };
6206
6258
  currentZonePhotos[zoneId] = [...currentZonePhotos[zoneId], newEntry];
6207
6259
  if (currentZonePhotos[zoneId].length > 10) {
@@ -6220,7 +6272,10 @@ var AppointmentService = class extends BaseService {
6220
6272
  zoneBilling: currentMetadata.zoneBilling
6221
6273
  },
6222
6274
  finalbilling: currentMetadata.finalbilling,
6223
- finalizationNotes: currentMetadata.finalizationNotes
6275
+ finalizationNotesShared: (_a = currentMetadata.finalizationNotesShared) != null ? _a : null,
6276
+ finalizationNotesInternal: (_b = currentMetadata.finalizationNotesInternal) != null ? _b : null,
6277
+ finalizationNotes: (_d = (_c = currentMetadata.finalizationNotes) != null ? _c : currentMetadata.finalizationNotesShared) != null ? _d : null
6278
+ // @deprecated - migrate from old field if exists
6224
6279
  },
6225
6280
  updatedAt: serverTimestamp7()
6226
6281
  };
@@ -6274,7 +6329,7 @@ var AppointmentService = class extends BaseService {
6274
6329
  * @returns The updated appointment
6275
6330
  */
6276
6331
  async deleteZonePhoto(appointmentId, zoneId, photoIndex) {
6277
- var _a, _b, _c, _d, _e, _f, _g, _h, _i;
6332
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p;
6278
6333
  try {
6279
6334
  console.log(
6280
6335
  `[APPOINTMENT_SERVICE] Deleting zone photo index ${photoIndex} for zone ${zoneId} in appointment ${appointmentId}`
@@ -6331,7 +6386,10 @@ var AppointmentService = class extends BaseService {
6331
6386
  zoneBilling: appointment.metadata.zoneBilling
6332
6387
  },
6333
6388
  finalbilling: ((_h = appointment.metadata) == null ? void 0 : _h.finalbilling) || null,
6334
- finalizationNotes: ((_i = appointment.metadata) == null ? void 0 : _i.finalizationNotes) || null
6389
+ finalizationNotesShared: (_l = (_k = (_i = appointment.metadata) == null ? void 0 : _i.finalizationNotesShared) != null ? _k : (_j = appointment.metadata) == null ? void 0 : _j.finalizationNotes) != null ? _l : null,
6390
+ finalizationNotesInternal: (_n = (_m = appointment.metadata) == null ? void 0 : _m.finalizationNotesInternal) != null ? _n : null,
6391
+ finalizationNotes: (_p = (_o = appointment.metadata) == null ? void 0 : _o.finalizationNotes) != null ? _p : null
6392
+ // @deprecated
6335
6393
  },
6336
6394
  updatedAt: serverTimestamp7()
6337
6395
  };
@@ -6530,7 +6588,7 @@ var AppointmentService = class extends BaseService {
6530
6588
  * @returns The updated appointment with recalculated billing
6531
6589
  */
6532
6590
  async recalculateFinalBilling(appointmentId, taxRate) {
6533
- var _a;
6591
+ var _a, _b, _c, _d, _e;
6534
6592
  try {
6535
6593
  console.log(
6536
6594
  `[APPOINTMENT_SERVICE] Recalculating final billing for appointment ${appointmentId}`
@@ -6552,7 +6610,10 @@ var AppointmentService = class extends BaseService {
6552
6610
  extendedProcedures: [],
6553
6611
  recommendedProcedures: [],
6554
6612
  finalbilling: null,
6613
+ finalizationNotesShared: null,
6614
+ finalizationNotesInternal: null,
6555
6615
  finalizationNotes: null
6616
+ // @deprecated
6556
6617
  };
6557
6618
  const shouldUpdatePaymentStatus = finalbilling.finalPrice > 0 && appointment.paymentStatus === "not_applicable" /* NOT_APPLICABLE */;
6558
6619
  const updateData = {
@@ -6568,7 +6629,10 @@ var AppointmentService = class extends BaseService {
6568
6629
  zoneBilling: currentMetadata.zoneBilling
6569
6630
  },
6570
6631
  finalbilling,
6571
- finalizationNotes: currentMetadata.finalizationNotes
6632
+ finalizationNotesShared: (_b = currentMetadata.finalizationNotesShared) != null ? _b : null,
6633
+ finalizationNotesInternal: (_c = currentMetadata.finalizationNotesInternal) != null ? _c : null,
6634
+ finalizationNotes: (_e = (_d = currentMetadata.finalizationNotes) != null ? _d : currentMetadata.finalizationNotesShared) != null ? _e : null
6635
+ // @deprecated
6572
6636
  },
6573
6637
  ...shouldUpdatePaymentStatus && {
6574
6638
  paymentStatus: "unpaid" /* UNPAID */
@@ -6768,6 +6832,64 @@ var AppointmentService = class extends BaseService {
6768
6832
  throw error;
6769
6833
  }
6770
6834
  }
6835
+ /**
6836
+ * Updates visibility of a photo pair (before AND after together)
6837
+ *
6838
+ * @param appointmentId ID of the appointment
6839
+ * @param zoneId Zone ID
6840
+ * @param photoIndex Index of the photo entry
6841
+ * @param showToPatient Whether the photo pair should be visible to patient
6842
+ * @param doctorId ID of the doctor making the change (for audit trail)
6843
+ * @returns The updated appointment
6844
+ */
6845
+ async updateZonePhotoVisibility(appointmentId, zoneId, photoIndex, showToPatient, doctorId) {
6846
+ try {
6847
+ console.log(
6848
+ `[APPOINTMENT_SERVICE] Updating photo visibility at index ${photoIndex} for zone ${zoneId} to ${showToPatient}`
6849
+ );
6850
+ return await updateZonePhotoVisibilityUtil(
6851
+ this.db,
6852
+ appointmentId,
6853
+ zoneId,
6854
+ photoIndex,
6855
+ showToPatient,
6856
+ doctorId
6857
+ );
6858
+ } catch (error) {
6859
+ console.error(`[APPOINTMENT_SERVICE] Error updating zone photo visibility:`, error);
6860
+ throw error;
6861
+ }
6862
+ }
6863
+ /**
6864
+ * Updates visibility of a photo note (before or after)
6865
+ *
6866
+ * @param appointmentId ID of the appointment
6867
+ * @param zoneId Zone ID
6868
+ * @param photoIndex Index of the photo entry
6869
+ * @param noteType Type of note ('before' or 'after')
6870
+ * @param visibleToPatient Whether the note should be visible to patient
6871
+ * @param doctorId ID of the doctor making the change (for audit trail)
6872
+ * @returns The updated appointment
6873
+ */
6874
+ async updateZonePhotoNoteVisibility(appointmentId, zoneId, photoIndex, noteType, visibleToPatient, doctorId) {
6875
+ try {
6876
+ console.log(
6877
+ `[APPOINTMENT_SERVICE] Updating ${noteType} note visibility at index ${photoIndex} for zone ${zoneId} to ${visibleToPatient}`
6878
+ );
6879
+ return await updateZonePhotoNoteVisibilityUtil(
6880
+ this.db,
6881
+ appointmentId,
6882
+ zoneId,
6883
+ photoIndex,
6884
+ noteType,
6885
+ visibleToPatient,
6886
+ doctorId
6887
+ );
6888
+ } catch (error) {
6889
+ console.error(`[APPOINTMENT_SERVICE] Error updating zone photo note visibility:`, error);
6890
+ throw error;
6891
+ }
6892
+ }
6771
6893
  /**
6772
6894
  * Gets a specific photo entry from a zone
6773
6895
  *
@@ -6787,6 +6909,54 @@ var AppointmentService = class extends BaseService {
6787
6909
  throw error;
6788
6910
  }
6789
6911
  }
6912
+ /**
6913
+ * Updates finalization notes (shared with patient and/or internal only)
6914
+ *
6915
+ * @param appointmentId ID of the appointment
6916
+ * @param sharedNotes Notes to be shared with patient (optional)
6917
+ * @param internalNotes Notes for internal use only (optional)
6918
+ * @returns The updated appointment
6919
+ */
6920
+ async updateFinalizationNotes(appointmentId, sharedNotes, internalNotes) {
6921
+ try {
6922
+ const appointment = await this.getAppointmentById(appointmentId);
6923
+ if (!appointment) {
6924
+ throw new Error(`Appointment ${appointmentId} not found`);
6925
+ }
6926
+ const currentMetadata = appointment.metadata || {
6927
+ selectedZones: null,
6928
+ zonePhotos: null,
6929
+ zonesData: null,
6930
+ appointmentProducts: [],
6931
+ extendedProcedures: [],
6932
+ recommendedProcedures: [],
6933
+ finalbilling: null,
6934
+ finalizationNotesShared: null,
6935
+ finalizationNotesInternal: null,
6936
+ finalizationNotes: null
6937
+ };
6938
+ const updateData = {
6939
+ metadata: {
6940
+ selectedZones: currentMetadata.selectedZones,
6941
+ zonePhotos: currentMetadata.zonePhotos,
6942
+ zonesData: currentMetadata.zonesData || null,
6943
+ appointmentProducts: currentMetadata.appointmentProducts || [],
6944
+ extendedProcedures: currentMetadata.extendedProcedures || [],
6945
+ recommendedProcedures: currentMetadata.recommendedProcedures || [],
6946
+ finalbilling: currentMetadata.finalbilling,
6947
+ finalizationNotesShared: sharedNotes !== void 0 ? sharedNotes : currentMetadata.finalizationNotesShared,
6948
+ finalizationNotesInternal: internalNotes !== void 0 ? internalNotes : currentMetadata.finalizationNotesInternal,
6949
+ // Keep deprecated field for backward compatibility during migration
6950
+ finalizationNotes: sharedNotes !== void 0 ? sharedNotes : currentMetadata.finalizationNotes || currentMetadata.finalizationNotesShared
6951
+ },
6952
+ updatedAt: serverTimestamp7()
6953
+ };
6954
+ return await this.updateAppointment(appointmentId, updateData);
6955
+ } catch (error) {
6956
+ console.error(`[APPOINTMENT_SERVICE] Error updating finalization notes:`, error);
6957
+ throw error;
6958
+ }
6959
+ }
6790
6960
  /**
6791
6961
  * Gets all next steps recommendations for a patient from their past appointments.
6792
6962
  * Returns recommendations with context about which appointment, practitioner, and clinic suggested them.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blackcode_sa/metaestetics-api",
3
3
  "private": false,
4
- "version": "1.14.44",
4
+ "version": "1.14.45",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -92,6 +92,8 @@ import {
92
92
  removeAfterPhotoFromEntryUtil,
93
93
  updateZonePhotoNotesUtil,
94
94
  getZonePhotoEntryUtil,
95
+ updateZonePhotoVisibilityUtil,
96
+ updateZonePhotoNoteVisibilityUtil,
95
97
  } from './utils/zone-photo.utils';
96
98
 
97
99
  /**
@@ -1364,7 +1366,9 @@ export class AppointmentService extends BaseService {
1364
1366
  recommendedProcedures: [],
1365
1367
  zoneBilling: null,
1366
1368
  finalbilling: null,
1367
- finalizationNotes: null,
1369
+ finalizationNotesShared: null,
1370
+ finalizationNotesInternal: null,
1371
+ finalizationNotes: null, // @deprecated - kept for backward compatibility
1368
1372
  };
1369
1373
 
1370
1374
  // Initialize zonePhotos if it doesn't exist (array model per zone)
@@ -1386,6 +1390,9 @@ export class AppointmentService extends BaseService {
1386
1390
  after: oldData.after || null,
1387
1391
  beforeNote: null,
1388
1392
  afterNote: null,
1393
+ showToPatient: false, // Default: not visible to patient (privacy-first)
1394
+ beforeNoteVisibleToPatient: false,
1395
+ afterNoteVisibleToPatient: false,
1389
1396
  },
1390
1397
  ];
1391
1398
  }
@@ -1398,11 +1405,15 @@ export class AppointmentService extends BaseService {
1398
1405
  }
1399
1406
 
1400
1407
  // Create a new entry for this uploaded photo with per-photo notes
1408
+ // Default visibility is false (privacy-first approach)
1401
1409
  const newEntry: BeforeAfterPerZone = {
1402
1410
  before: photoType === 'before' ? mediaMetadata.url : null,
1403
1411
  after: photoType === 'after' ? mediaMetadata.url : null,
1404
1412
  beforeNote: photoType === 'before' ? notes || null : null,
1405
1413
  afterNote: photoType === 'after' ? notes || null : null,
1414
+ showToPatient: false, // Default: not visible to patient
1415
+ beforeNoteVisibleToPatient: false, // Default: not visible to patient
1416
+ afterNoteVisibleToPatient: false, // Default: not visible to patient
1406
1417
  };
1407
1418
 
1408
1419
  // Append to the zone's photo list
@@ -1426,7 +1437,12 @@ export class AppointmentService extends BaseService {
1426
1437
  zoneBilling: currentMetadata.zoneBilling,
1427
1438
  }),
1428
1439
  finalbilling: currentMetadata.finalbilling,
1429
- finalizationNotes: currentMetadata.finalizationNotes,
1440
+ finalizationNotesShared: currentMetadata.finalizationNotesShared ?? null,
1441
+ finalizationNotesInternal: currentMetadata.finalizationNotesInternal ?? null,
1442
+ finalizationNotes:
1443
+ currentMetadata.finalizationNotes ??
1444
+ currentMetadata.finalizationNotesShared ??
1445
+ null, // @deprecated - migrate from old field if exists
1430
1446
  },
1431
1447
  updatedAt: serverTimestamp(),
1432
1448
  };
@@ -1571,7 +1587,12 @@ export class AppointmentService extends BaseService {
1571
1587
  zoneBilling: appointment.metadata.zoneBilling,
1572
1588
  }),
1573
1589
  finalbilling: appointment.metadata?.finalbilling || null,
1574
- finalizationNotes: appointment.metadata?.finalizationNotes || null,
1590
+ finalizationNotesShared:
1591
+ appointment.metadata?.finalizationNotesShared ??
1592
+ appointment.metadata?.finalizationNotes ??
1593
+ null,
1594
+ finalizationNotesInternal: appointment.metadata?.finalizationNotesInternal ?? null,
1595
+ finalizationNotes: appointment.metadata?.finalizationNotes ?? null, // @deprecated
1575
1596
  },
1576
1597
  updatedAt: serverTimestamp(),
1577
1598
  };
@@ -1831,7 +1852,9 @@ export class AppointmentService extends BaseService {
1831
1852
  extendedProcedures: [],
1832
1853
  recommendedProcedures: [],
1833
1854
  finalbilling: null,
1834
- finalizationNotes: null,
1855
+ finalizationNotesShared: null,
1856
+ finalizationNotesInternal: null,
1857
+ finalizationNotes: null, // @deprecated
1835
1858
  };
1836
1859
 
1837
1860
  // Update payment status if billing data exists but status is NOT_APPLICABLE
@@ -1853,7 +1876,12 @@ export class AppointmentService extends BaseService {
1853
1876
  zoneBilling: currentMetadata.zoneBilling,
1854
1877
  }),
1855
1878
  finalbilling,
1856
- finalizationNotes: currentMetadata.finalizationNotes,
1879
+ finalizationNotesShared: currentMetadata.finalizationNotesShared ?? null,
1880
+ finalizationNotesInternal: currentMetadata.finalizationNotesInternal ?? null,
1881
+ finalizationNotes:
1882
+ currentMetadata.finalizationNotes ??
1883
+ currentMetadata.finalizationNotesShared ??
1884
+ null, // @deprecated
1857
1885
  },
1858
1886
  ...(shouldUpdatePaymentStatus && {
1859
1887
  paymentStatus: PaymentStatus.UNPAID,
@@ -2099,6 +2127,79 @@ export class AppointmentService extends BaseService {
2099
2127
  }
2100
2128
  }
2101
2129
 
2130
+ /**
2131
+ * Updates visibility of a photo pair (before AND after together)
2132
+ *
2133
+ * @param appointmentId ID of the appointment
2134
+ * @param zoneId Zone ID
2135
+ * @param photoIndex Index of the photo entry
2136
+ * @param showToPatient Whether the photo pair should be visible to patient
2137
+ * @param doctorId ID of the doctor making the change (for audit trail)
2138
+ * @returns The updated appointment
2139
+ */
2140
+ async updateZonePhotoVisibility(
2141
+ appointmentId: string,
2142
+ zoneId: string,
2143
+ photoIndex: number,
2144
+ showToPatient: boolean,
2145
+ doctorId: string,
2146
+ ): Promise<Appointment> {
2147
+ try {
2148
+ console.log(
2149
+ `[APPOINTMENT_SERVICE] Updating photo visibility at index ${photoIndex} for zone ${zoneId} to ${showToPatient}`,
2150
+ );
2151
+ return await updateZonePhotoVisibilityUtil(
2152
+ this.db,
2153
+ appointmentId,
2154
+ zoneId,
2155
+ photoIndex,
2156
+ showToPatient,
2157
+ doctorId,
2158
+ );
2159
+ } catch (error) {
2160
+ console.error(`[APPOINTMENT_SERVICE] Error updating zone photo visibility:`, error);
2161
+ throw error;
2162
+ }
2163
+ }
2164
+
2165
+ /**
2166
+ * Updates visibility of a photo note (before or after)
2167
+ *
2168
+ * @param appointmentId ID of the appointment
2169
+ * @param zoneId Zone ID
2170
+ * @param photoIndex Index of the photo entry
2171
+ * @param noteType Type of note ('before' or 'after')
2172
+ * @param visibleToPatient Whether the note should be visible to patient
2173
+ * @param doctorId ID of the doctor making the change (for audit trail)
2174
+ * @returns The updated appointment
2175
+ */
2176
+ async updateZonePhotoNoteVisibility(
2177
+ appointmentId: string,
2178
+ zoneId: string,
2179
+ photoIndex: number,
2180
+ noteType: 'before' | 'after',
2181
+ visibleToPatient: boolean,
2182
+ doctorId: string,
2183
+ ): Promise<Appointment> {
2184
+ try {
2185
+ console.log(
2186
+ `[APPOINTMENT_SERVICE] Updating ${noteType} note visibility at index ${photoIndex} for zone ${zoneId} to ${visibleToPatient}`,
2187
+ );
2188
+ return await updateZonePhotoNoteVisibilityUtil(
2189
+ this.db,
2190
+ appointmentId,
2191
+ zoneId,
2192
+ photoIndex,
2193
+ noteType,
2194
+ visibleToPatient,
2195
+ doctorId,
2196
+ );
2197
+ } catch (error) {
2198
+ console.error(`[APPOINTMENT_SERVICE] Error updating zone photo note visibility:`, error);
2199
+ throw error;
2200
+ }
2201
+ }
2202
+
2102
2203
  /**
2103
2204
  * Gets a specific photo entry from a zone
2104
2205
  *
@@ -2123,6 +2224,67 @@ export class AppointmentService extends BaseService {
2123
2224
  }
2124
2225
  }
2125
2226
 
2227
+ /**
2228
+ * Updates finalization notes (shared with patient and/or internal only)
2229
+ *
2230
+ * @param appointmentId ID of the appointment
2231
+ * @param sharedNotes Notes to be shared with patient (optional)
2232
+ * @param internalNotes Notes for internal use only (optional)
2233
+ * @returns The updated appointment
2234
+ */
2235
+ async updateFinalizationNotes(
2236
+ appointmentId: string,
2237
+ sharedNotes?: string | null,
2238
+ internalNotes?: string | null,
2239
+ ): Promise<Appointment> {
2240
+ try {
2241
+ const appointment = await this.getAppointmentById(appointmentId);
2242
+ if (!appointment) {
2243
+ throw new Error(`Appointment ${appointmentId} not found`);
2244
+ }
2245
+
2246
+ const currentMetadata = appointment.metadata || {
2247
+ selectedZones: null,
2248
+ zonePhotos: null,
2249
+ zonesData: null,
2250
+ appointmentProducts: [],
2251
+ extendedProcedures: [],
2252
+ recommendedProcedures: [],
2253
+ finalbilling: null,
2254
+ finalizationNotesShared: null,
2255
+ finalizationNotesInternal: null,
2256
+ finalizationNotes: null,
2257
+ };
2258
+
2259
+ const updateData: UpdateAppointmentData = {
2260
+ metadata: {
2261
+ selectedZones: currentMetadata.selectedZones,
2262
+ zonePhotos: currentMetadata.zonePhotos,
2263
+ zonesData: currentMetadata.zonesData || null,
2264
+ appointmentProducts: currentMetadata.appointmentProducts || [],
2265
+ extendedProcedures: currentMetadata.extendedProcedures || [],
2266
+ recommendedProcedures: currentMetadata.recommendedProcedures || [],
2267
+ finalbilling: currentMetadata.finalbilling,
2268
+ finalizationNotesShared:
2269
+ sharedNotes !== undefined ? sharedNotes : currentMetadata.finalizationNotesShared,
2270
+ finalizationNotesInternal:
2271
+ internalNotes !== undefined ? internalNotes : currentMetadata.finalizationNotesInternal,
2272
+ // Keep deprecated field for backward compatibility during migration
2273
+ finalizationNotes:
2274
+ sharedNotes !== undefined
2275
+ ? sharedNotes
2276
+ : currentMetadata.finalizationNotes || currentMetadata.finalizationNotesShared,
2277
+ },
2278
+ updatedAt: serverTimestamp(),
2279
+ };
2280
+
2281
+ return await this.updateAppointment(appointmentId, updateData);
2282
+ } catch (error) {
2283
+ console.error(`[APPOINTMENT_SERVICE] Error updating finalization notes:`, error);
2284
+ throw error;
2285
+ }
2286
+ }
2287
+
2126
2288
  /**
2127
2289
  * Gets all next steps recommendations for a patient from their past appointments.
2128
2290
  * Returns recommendations with context about which appointment, practitioner, and clinic suggested them.
@@ -121,7 +121,9 @@ export function initializeMetadata(appointment: Appointment): AppointmentMetadat
121
121
  extendedProcedures: [],
122
122
  recommendedProcedures: [],
123
123
  finalbilling: null,
124
- finalizationNotes: null,
124
+ finalizationNotesShared: null,
125
+ finalizationNotesInternal: null,
126
+ finalizationNotes: null, // @deprecated - kept for backward compatibility
125
127
  }
126
128
  );
127
129
  }
@@ -161,6 +163,8 @@ export async function addItemToZoneUtil(
161
163
  ...item,
162
164
  parentZone: zoneId, // Set parentZone to the zone key
163
165
  subtotal: calculateItemSubtotal(item),
166
+ // Set default visibility to false (privacy-first) if notes exist and visibility not explicitly set
167
+ notesVisibleToPatient: item.notesVisibleToPatient ?? (item.notes ? false : undefined),
164
168
  createdAt: now,
165
169
  updatedAt: now,
166
170
  };