@blackcode_sa/metaestetics-api 1.14.43 → 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,6 +3823,7 @@ 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
3829
  createdAt: z3.string().optional(),
@@ -3880,7 +3887,10 @@ var appointmentMetadataSchema = z3.object({
3880
3887
  recommendedProcedures: z3.array(recommendedProcedureSchema).optional().default([]),
3881
3888
  zoneBilling: z3.record(z3.string(), billingPerZoneSchema).nullable().optional(),
3882
3889
  finalbilling: finalBillingSchema.nullable(),
3883
- 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
3884
3894
  });
3885
3895
  var createAppointmentSchema = z3.object({
3886
3896
  clinicBranchId: z3.string().min(MIN_STRING_LENGTH, "Clinic branch ID is required"),
@@ -4712,10 +4722,14 @@ function initializeMetadata(appointment) {
4712
4722
  extendedProcedures: [],
4713
4723
  recommendedProcedures: [],
4714
4724
  finalbilling: null,
4725
+ finalizationNotesShared: null,
4726
+ finalizationNotesInternal: null,
4715
4727
  finalizationNotes: null
4728
+ // @deprecated - kept for backward compatibility
4716
4729
  };
4717
4730
  }
4718
4731
  async function addItemToZoneUtil(db, appointmentId, zoneId, item) {
4732
+ var _a;
4719
4733
  validateZoneKeyFormat(zoneId);
4720
4734
  const appointment = await getAppointmentOrThrow(db, appointmentId);
4721
4735
  const metadata = initializeMetadata(appointment);
@@ -4729,6 +4743,8 @@ async function addItemToZoneUtil(db, appointmentId, zoneId, item) {
4729
4743
  parentZone: zoneId,
4730
4744
  // Set parentZone to the zone key
4731
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,
4732
4748
  createdAt: now,
4733
4749
  updatedAt: now
4734
4750
  };
@@ -5228,7 +5244,17 @@ async function getRecommendedProceduresUtil(db, appointmentId) {
5228
5244
 
5229
5245
  // src/services/appointment/utils/zone-photo.utils.ts
5230
5246
  import { updateDoc as updateDoc6, serverTimestamp as serverTimestamp6, doc as doc9 } from "firebase/firestore";
5231
- 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) {
5232
5258
  var _a;
5233
5259
  const appointment = await getAppointmentOrThrow(db, appointmentId);
5234
5260
  const zonePhotos = (_a = appointment.metadata) == null ? void 0 : _a.zonePhotos;
@@ -5239,11 +5265,12 @@ async function updateZonePhotoEntryUtil(db, appointmentId, zoneId, photoIndex, u
5239
5265
  if (photoIndex < 0 || photoIndex >= zoneArray.length) {
5240
5266
  throw new Error(`Invalid photo index ${photoIndex} for zone ${zoneId}. Must be between 0 and ${zoneArray.length - 1}`);
5241
5267
  }
5268
+ const updatesWithAudit = doctorId ? addVisibilityAudit(updates, doctorId) : updates;
5242
5269
  const updatedZonePhotos = { ...zonePhotos };
5243
5270
  updatedZonePhotos[zoneId] = [...zoneArray];
5244
5271
  updatedZonePhotos[zoneId][photoIndex] = {
5245
5272
  ...zoneArray[photoIndex],
5246
- ...updates
5273
+ ...updatesWithAudit
5247
5274
  };
5248
5275
  const appointmentRef = doc9(db, APPOINTMENTS_COLLECTION, appointmentId);
5249
5276
  await updateDoc6(appointmentRef, {
@@ -5287,6 +5314,20 @@ async function getZonePhotoEntryUtil(db, appointmentId, zoneId, photoIndex) {
5287
5314
  }
5288
5315
  return zoneArray[photoIndex];
5289
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
+ }
5290
5331
 
5291
5332
  // src/services/appointment/appointment.service.ts
5292
5333
  var AppointmentService = class extends BaseService {
@@ -6153,6 +6194,7 @@ var AppointmentService = class extends BaseService {
6153
6194
  * @returns The updated appointment
6154
6195
  */
6155
6196
  async updateAppointmentZonePhoto(appointmentId, zoneId, photoType, mediaMetadata, notes) {
6197
+ var _a, _b, _c, _d;
6156
6198
  try {
6157
6199
  console.log(
6158
6200
  `[APPOINTMENT_SERVICE] Updating appointment metadata for ${photoType} photo in zone ${zoneId}`
@@ -6170,7 +6212,10 @@ var AppointmentService = class extends BaseService {
6170
6212
  recommendedProcedures: [],
6171
6213
  zoneBilling: null,
6172
6214
  finalbilling: null,
6215
+ finalizationNotesShared: null,
6216
+ finalizationNotesInternal: null,
6173
6217
  finalizationNotes: null
6218
+ // @deprecated - kept for backward compatibility
6174
6219
  };
6175
6220
  let currentZonePhotos = {};
6176
6221
  if (currentMetadata.zonePhotos) {
@@ -6185,7 +6230,11 @@ var AppointmentService = class extends BaseService {
6185
6230
  before: oldData.before || null,
6186
6231
  after: oldData.after || null,
6187
6232
  beforeNote: null,
6188
- afterNote: null
6233
+ afterNote: null,
6234
+ showToPatient: false,
6235
+ // Default: not visible to patient (privacy-first)
6236
+ beforeNoteVisibleToPatient: false,
6237
+ afterNoteVisibleToPatient: false
6189
6238
  }
6190
6239
  ];
6191
6240
  }
@@ -6198,7 +6247,13 @@ var AppointmentService = class extends BaseService {
6198
6247
  before: photoType === "before" ? mediaMetadata.url : null,
6199
6248
  after: photoType === "after" ? mediaMetadata.url : null,
6200
6249
  beforeNote: photoType === "before" ? notes || null : null,
6201
- 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
6202
6257
  };
6203
6258
  currentZonePhotos[zoneId] = [...currentZonePhotos[zoneId], newEntry];
6204
6259
  if (currentZonePhotos[zoneId].length > 10) {
@@ -6217,7 +6272,10 @@ var AppointmentService = class extends BaseService {
6217
6272
  zoneBilling: currentMetadata.zoneBilling
6218
6273
  },
6219
6274
  finalbilling: currentMetadata.finalbilling,
6220
- 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
6221
6279
  },
6222
6280
  updatedAt: serverTimestamp7()
6223
6281
  };
@@ -6271,7 +6329,7 @@ var AppointmentService = class extends BaseService {
6271
6329
  * @returns The updated appointment
6272
6330
  */
6273
6331
  async deleteZonePhoto(appointmentId, zoneId, photoIndex) {
6274
- 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;
6275
6333
  try {
6276
6334
  console.log(
6277
6335
  `[APPOINTMENT_SERVICE] Deleting zone photo index ${photoIndex} for zone ${zoneId} in appointment ${appointmentId}`
@@ -6328,7 +6386,10 @@ var AppointmentService = class extends BaseService {
6328
6386
  zoneBilling: appointment.metadata.zoneBilling
6329
6387
  },
6330
6388
  finalbilling: ((_h = appointment.metadata) == null ? void 0 : _h.finalbilling) || null,
6331
- 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
6332
6393
  },
6333
6394
  updatedAt: serverTimestamp7()
6334
6395
  };
@@ -6527,7 +6588,7 @@ var AppointmentService = class extends BaseService {
6527
6588
  * @returns The updated appointment with recalculated billing
6528
6589
  */
6529
6590
  async recalculateFinalBilling(appointmentId, taxRate) {
6530
- var _a;
6591
+ var _a, _b, _c, _d, _e;
6531
6592
  try {
6532
6593
  console.log(
6533
6594
  `[APPOINTMENT_SERVICE] Recalculating final billing for appointment ${appointmentId}`
@@ -6549,7 +6610,10 @@ var AppointmentService = class extends BaseService {
6549
6610
  extendedProcedures: [],
6550
6611
  recommendedProcedures: [],
6551
6612
  finalbilling: null,
6613
+ finalizationNotesShared: null,
6614
+ finalizationNotesInternal: null,
6552
6615
  finalizationNotes: null
6616
+ // @deprecated
6553
6617
  };
6554
6618
  const shouldUpdatePaymentStatus = finalbilling.finalPrice > 0 && appointment.paymentStatus === "not_applicable" /* NOT_APPLICABLE */;
6555
6619
  const updateData = {
@@ -6565,7 +6629,10 @@ var AppointmentService = class extends BaseService {
6565
6629
  zoneBilling: currentMetadata.zoneBilling
6566
6630
  },
6567
6631
  finalbilling,
6568
- 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
6569
6636
  },
6570
6637
  ...shouldUpdatePaymentStatus && {
6571
6638
  paymentStatus: "unpaid" /* UNPAID */
@@ -6765,6 +6832,64 @@ var AppointmentService = class extends BaseService {
6765
6832
  throw error;
6766
6833
  }
6767
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
+ }
6768
6893
  /**
6769
6894
  * Gets a specific photo entry from a zone
6770
6895
  *
@@ -6784,6 +6909,54 @@ var AppointmentService = class extends BaseService {
6784
6909
  throw error;
6785
6910
  }
6786
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
+ }
6787
6960
  /**
6788
6961
  * Gets all next steps recommendations for a patient from their past appointments.
6789
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.43",
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
  };