@blackcode_sa/metaestetics-api 1.14.56 → 1.14.57

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blackcode_sa/metaestetics-api",
3
3
  "private": false,
4
- "version": "1.14.56",
4
+ "version": "1.14.57",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -16,6 +16,7 @@ import {
16
16
  getCountFromServer,
17
17
  doc,
18
18
  getDoc,
19
+ updateDoc,
19
20
  } from 'firebase/firestore';
20
21
  import { Auth } from 'firebase/auth';
21
22
  import { FirebaseApp } from 'firebase/app';
@@ -2296,6 +2297,14 @@ export class AppointmentService extends BaseService {
2296
2297
  finalizationNotes: currentMetadata.finalizationNotes,
2297
2298
  });
2298
2299
 
2300
+ // Convert empty strings to null for proper Firestore storage
2301
+ const normalizedSharedNotes = sharedNotes !== undefined
2302
+ ? (sharedNotes === '' || sharedNotes === null ? null : sharedNotes)
2303
+ : currentMetadata.finalizationNotesShared;
2304
+ const normalizedInternalNotes = internalNotes !== undefined
2305
+ ? (internalNotes === '' || internalNotes === null ? null : internalNotes)
2306
+ : currentMetadata.finalizationNotesInternal;
2307
+
2299
2308
  const metadataUpdate = {
2300
2309
  selectedZones: currentMetadata.selectedZones,
2301
2310
  zonePhotos: currentMetadata.zonePhotos,
@@ -2304,14 +2313,12 @@ export class AppointmentService extends BaseService {
2304
2313
  extendedProcedures: currentMetadata.extendedProcedures || [],
2305
2314
  recommendedProcedures: currentMetadata.recommendedProcedures || [],
2306
2315
  finalbilling: currentMetadata.finalbilling,
2307
- finalizationNotesShared:
2308
- sharedNotes !== undefined ? sharedNotes : currentMetadata.finalizationNotesShared,
2309
- finalizationNotesInternal:
2310
- internalNotes !== undefined ? internalNotes : currentMetadata.finalizationNotesInternal,
2316
+ finalizationNotesShared: normalizedSharedNotes,
2317
+ finalizationNotesInternal: normalizedInternalNotes,
2311
2318
  // Keep deprecated field for backward compatibility during migration
2312
2319
  finalizationNotes:
2313
- sharedNotes !== undefined
2314
- ? sharedNotes
2320
+ normalizedSharedNotes !== null && normalizedSharedNotes !== undefined
2321
+ ? normalizedSharedNotes
2315
2322
  : currentMetadata.finalizationNotes || currentMetadata.finalizationNotesShared,
2316
2323
  };
2317
2324
 
@@ -2319,14 +2326,39 @@ export class AppointmentService extends BaseService {
2319
2326
  finalizationNotesShared: metadataUpdate.finalizationNotesShared,
2320
2327
  finalizationNotesInternal: metadataUpdate.finalizationNotesInternal,
2321
2328
  finalizationNotes: metadataUpdate.finalizationNotes,
2329
+ normalizedSharedNotes,
2330
+ normalizedInternalNotes,
2322
2331
  });
2323
2332
 
2324
- const updateData: UpdateAppointmentData = {
2325
- metadata: metadataUpdate,
2333
+ // Use direct Firestore update with dot notation to ensure fields are saved correctly
2334
+ const appointmentRef = doc(this.db, APPOINTMENTS_COLLECTION, appointmentId);
2335
+ const updateFields: any = {
2336
+ 'metadata.finalizationNotesShared': normalizedSharedNotes,
2337
+ 'metadata.finalizationNotesInternal': normalizedInternalNotes,
2338
+ 'metadata.finalizationNotes':
2339
+ normalizedSharedNotes !== null && normalizedSharedNotes !== undefined
2340
+ ? normalizedSharedNotes
2341
+ : currentMetadata.finalizationNotes || currentMetadata.finalizationNotesShared,
2326
2342
  updatedAt: serverTimestamp(),
2327
2343
  };
2328
2344
 
2329
- const result = await this.updateAppointment(appointmentId, updateData);
2345
+ console.log('🔍 [APPOINTMENT_SERVICE] Direct Firestore update with dot notation:', {
2346
+ appointmentId,
2347
+ updateFields: {
2348
+ 'metadata.finalizationNotesShared': updateFields['metadata.finalizationNotesShared'],
2349
+ 'metadata.finalizationNotesInternal': updateFields['metadata.finalizationNotesInternal'],
2350
+ 'metadata.finalizationNotes': updateFields['metadata.finalizationNotes'],
2351
+ },
2352
+ });
2353
+
2354
+ // Update using dot notation to ensure nested fields are saved correctly
2355
+ await updateDoc(appointmentRef, updateFields);
2356
+
2357
+ // Fetch and return the updated appointment
2358
+ const result = await this.getAppointmentById(appointmentId);
2359
+ if (!result) {
2360
+ throw new Error(`Failed to retrieve updated appointment ${appointmentId}`);
2361
+ }
2330
2362
 
2331
2363
  console.log('🔍 [APPOINTMENT_SERVICE] After update, result metadata:', {
2332
2364
  finalizationNotesShared: result.metadata?.finalizationNotesShared,
@@ -99,6 +99,7 @@ async function createExtendedProcedureInfo(
99
99
  procedureId: procedureId,
100
100
  procedureName: data.name,
101
101
  procedureDescription: data.description || '',
102
+ procedurePrice: data.price || 0,
102
103
  procedureFamily: data.family, // Use embedded family object
103
104
  procedureCategoryId: data.category.id, // Access embedded category
104
105
  procedureCategoryName: data.category.name, // Access embedded category
@@ -218,6 +218,7 @@ export async function initializeFormsForExtendedProcedure(
218
218
  sortingOrder: templateRef.sortingOrder,
219
219
  status: existingForm.status || FilledDocumentStatus.PENDING,
220
220
  path: `${APPOINTMENTS_COLLECTION}/${appointmentId}/${formSubcollectionPath}/${existingForm.id}`,
221
+ procedureId: procedureId, // Track which procedure this form belongs to
221
222
  };
222
223
  initializedFormsInfo.push(linkedForm);
223
224
 
@@ -288,6 +289,7 @@ export async function initializeFormsForExtendedProcedure(
288
289
  sortingOrder: templateRef.sortingOrder,
289
290
  status: FilledDocumentStatus.PENDING,
290
291
  path: docRef.path,
292
+ procedureId: procedureId, // Track which procedure this form belongs to
291
293
  };
292
294
  initializedFormsInfo.push(linkedForm);
293
295
 
@@ -278,19 +278,45 @@ export async function updateZoneItemUtil(
278
278
  throw new Error(`Invalid item index ${itemIndex} for zone ${zoneId}`);
279
279
  }
280
280
 
281
+ // Filter out undefined values from updates (Firestore doesn't store undefined)
282
+ // Keep null values as they're used to explicitly delete/clear fields
283
+ // Convert empty strings to null for consistency
284
+ const cleanUpdates = Object.fromEntries(
285
+ Object.entries(updates)
286
+ .filter(([_, value]) => value !== undefined)
287
+ .map(([key, value]) => [
288
+ key,
289
+ value === '' ? null : value, // Convert empty strings to null
290
+ ])
291
+ ) as Partial<ZoneItemData>;
292
+
293
+ console.log(`[updateZoneItemUtil] Updates received:`, {
294
+ itemIndex,
295
+ zoneId,
296
+ rawUpdates: updates,
297
+ cleanUpdates,
298
+ hasIonNumber: 'ionNumber' in cleanUpdates,
299
+ hasExpiryDate: 'expiryDate' in cleanUpdates,
300
+ ionNumberValue: cleanUpdates.ionNumber,
301
+ expiryDateValue: cleanUpdates.expiryDate,
302
+ });
303
+
281
304
  // Update item with updatedAt timestamp
282
305
  items[itemIndex] = {
283
306
  ...items[itemIndex],
284
- ...updates,
307
+ ...cleanUpdates,
285
308
  updatedAt: new Date().toISOString(),
286
309
  };
287
310
 
288
- console.log(`[updateZoneItemUtil] BEFORE recalculation:`, {
311
+ console.log(`[updateZoneItemUtil] Item after update:`, {
289
312
  itemIndex,
313
+ ionNumber: items[itemIndex].ionNumber,
314
+ expiryDate: items[itemIndex].expiryDate,
290
315
  quantity: items[itemIndex].quantity,
291
316
  priceOverrideAmount: items[itemIndex].priceOverrideAmount,
292
317
  price: items[itemIndex].price,
293
318
  oldSubtotal: items[itemIndex].subtotal,
319
+ allItemKeys: Object.keys(items[itemIndex]),
294
320
  });
295
321
 
296
322
  // Recalculate subtotal for this item
@@ -304,6 +330,18 @@ export async function updateZoneItemUtil(
304
330
  // Recalculate final billing with Swiss tax rate (8.1%)
305
331
  const finalbilling = calculateFinalBilling(metadata.zonesData, 0.081);
306
332
 
333
+ // Log what we're about to save to Firestore
334
+ console.log(`[updateZoneItemUtil] Saving to Firestore:`, {
335
+ appointmentId,
336
+ zoneId,
337
+ itemIndex,
338
+ itemToSave: items[itemIndex],
339
+ itemIonNumber: items[itemIndex].ionNumber,
340
+ itemExpiryDate: items[itemIndex].expiryDate,
341
+ zonesDataKeys: Object.keys(metadata.zonesData),
342
+ zoneItemsCount: metadata.zonesData[zoneId]?.length,
343
+ });
344
+
307
345
  // Update appointment
308
346
  const appointmentRef = doc(db, APPOINTMENTS_COLLECTION, appointmentId);
309
347
  await updateDoc(appointmentRef, {
@@ -312,7 +350,16 @@ export async function updateZoneItemUtil(
312
350
  updatedAt: serverTimestamp(),
313
351
  });
314
352
 
315
- return getAppointmentOrThrow(db, appointmentId);
353
+ // Verify what was actually saved
354
+ const savedAppointment = await getAppointmentOrThrow(db, appointmentId);
355
+ const savedItem = savedAppointment.metadata?.zonesData?.[zoneId]?.[itemIndex];
356
+ console.log(`[updateZoneItemUtil] Verification after save:`, {
357
+ savedItemIonNumber: savedItem?.ionNumber,
358
+ savedItemExpiryDate: savedItem?.expiryDate,
359
+ savedItemKeys: savedItem ? Object.keys(savedItem) : [],
360
+ });
361
+
362
+ return savedAppointment;
316
363
  }
317
364
 
318
365
  /**
@@ -206,6 +206,7 @@ export async function getZonePhotoEntryUtil(
206
206
 
207
207
  /**
208
208
  * Updates visibility of a photo pair (before AND after together)
209
+ * Note: If photo pair visibility is disabled, note visibility is automatically disabled as well
209
210
  *
210
211
  * @param db Firestore instance
211
212
  * @param appointmentId Appointment ID
@@ -223,18 +224,27 @@ export async function updateZonePhotoVisibilityUtil(
223
224
  showToPatient: boolean,
224
225
  doctorId: string,
225
226
  ): Promise<Appointment> {
227
+ const updates: Partial<BeforeAfterPerZone> = { showToPatient };
228
+
229
+ // If hiding photo pair from patient, also hide notes (notes can't be visible if photos aren't)
230
+ if (!showToPatient) {
231
+ updates.beforeNoteVisibleToPatient = false;
232
+ updates.afterNoteVisibleToPatient = false;
233
+ }
234
+
226
235
  return updateZonePhotoEntryUtil(
227
236
  db,
228
237
  appointmentId,
229
238
  zoneId,
230
239
  photoIndex,
231
- { showToPatient },
240
+ updates,
232
241
  doctorId,
233
242
  );
234
243
  }
235
244
 
236
245
  /**
237
246
  * Updates visibility of a photo note (before or after)
247
+ * Note: Notes can only be visible to patient if the photo pair itself is visible to patient
238
248
  *
239
249
  * @param db Firestore instance
240
250
  * @param appointmentId Appointment ID
@@ -244,6 +254,7 @@ export async function updateZonePhotoVisibilityUtil(
244
254
  * @param visibleToPatient Whether the note should be visible to patient
245
255
  * @param doctorId ID of the doctor making the change (for audit trail)
246
256
  * @returns Updated appointment
257
+ * @throws Error if trying to make note visible when photo pair is not visible
247
258
  */
248
259
  export async function updateZonePhotoNoteVisibilityUtil(
249
260
  db: Firestore,
@@ -254,6 +265,33 @@ export async function updateZonePhotoNoteVisibilityUtil(
254
265
  visibleToPatient: boolean,
255
266
  doctorId: string,
256
267
  ): Promise<Appointment> {
268
+ // Get current appointment to check if photo pair is visible
269
+ const appointment = await getAppointmentOrThrow(db, appointmentId);
270
+
271
+ const zonePhotos = appointment.metadata?.zonePhotos as
272
+ | Record<string, BeforeAfterPerZone[]>
273
+ | undefined
274
+ | null;
275
+
276
+ if (!zonePhotos || !zonePhotos[zoneId] || !Array.isArray(zonePhotos[zoneId])) {
277
+ throw new Error(`No photos found for zone ${zoneId} in appointment ${appointmentId}`);
278
+ }
279
+
280
+ const zoneArray = zonePhotos[zoneId];
281
+ if (photoIndex < 0 || photoIndex >= zoneArray.length) {
282
+ throw new Error(`Invalid photo index ${photoIndex} for zone ${zoneId}. Must be between 0 and ${zoneArray.length - 1}`);
283
+ }
284
+
285
+ const photoEntry = zoneArray[photoIndex];
286
+
287
+ // Validate: Notes can only be visible if photo pair is visible
288
+ if (visibleToPatient && !photoEntry.showToPatient) {
289
+ throw new Error(
290
+ 'Cannot make note visible to patient: The photo pair must be visible to the patient first. ' +
291
+ 'Please enable "Show to Patient" for the photo pair before making notes visible.'
292
+ );
293
+ }
294
+
257
295
  const updates: Partial<BeforeAfterPerZone> =
258
296
  noteType === 'before'
259
297
  ? { beforeNoteVisibleToPatient: visibleToPatient }
@@ -100,6 +100,7 @@ export interface LinkedFormInfo {
100
100
  path: string; // Full Firestore path to the filled document (e.g., appointments/{aid}/user-forms/{fid})
101
101
  submittedAt?: Timestamp;
102
102
  completedAt?: Timestamp; // When the form reached a final state like 'completed' or 'signed'
103
+ procedureId?: string; // ID of the procedure this form belongs to (for extended procedure forms)
103
104
  }
104
105
 
105
106
  /**
@@ -168,8 +169,8 @@ export interface ZoneItemData {
168
169
  notes?: string;
169
170
  notesVisibleToPatient?: boolean; // Whether notes are visible to patient (privacy-first, default false)
170
171
  subtotal?: number;
171
- ionNumber?: string; // Batch/Lot number
172
- expiryDate?: string; // ISO date string (YYYY-MM-DD)
172
+ ionNumber?: string | null; // Batch/Lot number - can be null to clear/delete
173
+ expiryDate?: string | null; // ISO date string (YYYY-MM-DD) - can be null to clear/delete
173
174
  createdAt?: string; // ISO timestamp
174
175
  updatedAt?: string; // ISO timestamp
175
176
  }
@@ -226,6 +227,7 @@ export interface ExtendedProcedureInfo {
226
227
  procedureId: string;
227
228
  procedureName: string;
228
229
  procedureDescription?: string;
230
+ procedurePrice?: number;
229
231
  procedureFamily?: ProcedureFamily;
230
232
  procedureCategoryId: string;
231
233
  procedureCategoryName: string;
@@ -227,7 +227,8 @@ export const zoneItemDataSchema = z
227
227
  notes: z.string().max(MAX_STRING_LENGTH_LONG, 'Notes too long').optional(),
228
228
  notesVisibleToPatient: z.boolean().optional().default(false),
229
229
  subtotal: z.number().min(0, 'Subtotal must be non-negative').optional(),
230
- ionNumber: z.string().optional(),
230
+ ionNumber: z.string().nullable().optional(), // Batch/Lot number - can be null to clear/delete
231
+ expiryDate: z.string().nullable().optional(), // ISO date string (YYYY-MM-DD) - can be null to clear/delete
231
232
  createdAt: z.string().optional(),
232
233
  updatedAt: z.string().optional(),
233
234
  })