@blackcode_sa/metaestetics-api 1.12.46 → 1.12.48

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.
@@ -7,7 +7,10 @@ import {
7
7
  } from '../../../types/appointment';
8
8
  import { getAppointmentOrThrow, initializeMetadata } from './zone-management.utils';
9
9
  import { PROCEDURES_COLLECTION } from '../../../types/procedure';
10
- import { initializeFormsForExtendedProcedure, removeFormsForExtendedProcedure } from './form-initialization.utils';
10
+ import {
11
+ initializeFormsForExtendedProcedure,
12
+ removeFormsForExtendedProcedure,
13
+ } from './form-initialization.utils';
11
14
 
12
15
  /**
13
16
  * Aggregates products from a procedure into appointmentProducts
@@ -19,7 +22,7 @@ import { initializeFormsForExtendedProcedure, removeFormsForExtendedProcedure }
19
22
  async function aggregateProductsFromProcedure(
20
23
  db: Firestore,
21
24
  procedureId: string,
22
- existingProducts: AppointmentProductMetadata[]
25
+ existingProducts: AppointmentProductMetadata[],
23
26
  ): Promise<AppointmentProductMetadata[]> {
24
27
  const procedureRef = doc(db, PROCEDURES_COLLECTION, procedureId);
25
28
  const procedureSnap = await getDoc(procedureRef);
@@ -34,21 +37,24 @@ async function aggregateProductsFromProcedure(
34
37
  const productsMetadata = procedureData.productsMetadata || [];
35
38
 
36
39
  // Map procedure products to AppointmentProductMetadata
37
- const newProducts: AppointmentProductMetadata[] = productsMetadata.map((pp: any) => {
38
- // Each item in productsMetadata is a ProcedureProduct with embedded Product
39
- const product = pp.product;
40
-
41
- return {
42
- productId: product.id,
43
- productName: product.name,
44
- brandId: product.brandId,
45
- brandName: product.brandName,
46
- procedureId: procedureId,
47
- price: pp.price, // Price from ProcedureProduct
48
- currency: pp.currency, // Currency from ProcedureProduct
49
- unitOfMeasurement: pp.pricingMeasure, // PricingMeasure from ProcedureProduct
50
- };
51
- });
40
+ // Filter out any entries without products (safety check for product-free procedures like consultations)
41
+ const newProducts: AppointmentProductMetadata[] = productsMetadata
42
+ .filter((pp: any) => pp && pp.product)
43
+ .map((pp: any) => {
44
+ // Each item in productsMetadata is a ProcedureProduct with embedded Product
45
+ const product = pp.product;
46
+
47
+ return {
48
+ productId: product.id,
49
+ productName: product.name,
50
+ brandId: product.brandId,
51
+ brandName: product.brandName,
52
+ procedureId: procedureId,
53
+ price: pp.price, // Price from ProcedureProduct
54
+ currency: pp.currency, // Currency from ProcedureProduct
55
+ unitOfMeasurement: pp.pricingMeasure, // PricingMeasure from ProcedureProduct
56
+ };
57
+ });
52
58
 
53
59
  // Merge with existing products, avoiding duplicates
54
60
  const productMap = new Map<string, AppointmentProductMetadata>();
@@ -78,7 +84,7 @@ async function aggregateProductsFromProcedure(
78
84
  */
79
85
  async function createExtendedProcedureInfo(
80
86
  db: Firestore,
81
- procedureId: string
87
+ procedureId: string,
82
88
  ): Promise<ExtendedProcedureInfo> {
83
89
  const procedureRef = doc(db, PROCEDURES_COLLECTION, procedureId);
84
90
  const procedureSnap = await getDoc(procedureRef);
@@ -92,19 +98,21 @@ async function createExtendedProcedureInfo(
92
98
  return {
93
99
  procedureId: procedureId,
94
100
  procedureName: data.name,
95
- procedureFamily: data.family, // Use embedded family object
96
- procedureCategoryId: data.category.id, // Access embedded category
97
- procedureCategoryName: data.category.name, // Access embedded category
98
- procedureSubCategoryId: data.subcategory.id, // Access embedded subcategory
101
+ procedureFamily: data.family, // Use embedded family object
102
+ procedureCategoryId: data.category.id, // Access embedded category
103
+ procedureCategoryName: data.category.name, // Access embedded category
104
+ procedureSubCategoryId: data.subcategory.id, // Access embedded subcategory
99
105
  procedureSubCategoryName: data.subcategory.name, // Access embedded subcategory
100
- procedureTechnologyId: data.technology.id, // Access embedded technology
101
- procedureTechnologyName: data.technology.name, // Access embedded technology
102
- procedureProducts: (data.productsMetadata || []).map((pp: any) => ({
103
- productId: pp.product.id, // Access embedded product
104
- productName: pp.product.name, // Access embedded product
105
- brandId: pp.product.brandId, // Access embedded product
106
- brandName: pp.product.brandName, // Access embedded product
107
- })),
106
+ procedureTechnologyId: data.technology.id, // Access embedded technology
107
+ procedureTechnologyName: data.technology.name, // Access embedded technology
108
+ procedureProducts: (data.productsMetadata || [])
109
+ .filter((pp: any) => pp && pp.product) // Safety check for product-free procedures
110
+ .map((pp: any) => ({
111
+ productId: pp.product.id, // Access embedded product
112
+ productName: pp.product.name, // Access embedded product
113
+ brandId: pp.product.brandId, // Access embedded product
114
+ brandName: pp.product.brandName, // Access embedded product
115
+ })),
108
116
  };
109
117
  }
110
118
 
@@ -119,15 +127,13 @@ async function createExtendedProcedureInfo(
119
127
  export async function addExtendedProcedureUtil(
120
128
  db: Firestore,
121
129
  appointmentId: string,
122
- procedureId: string
130
+ procedureId: string,
123
131
  ): Promise<Appointment> {
124
132
  const appointment = await getAppointmentOrThrow(db, appointmentId);
125
133
  const metadata = initializeMetadata(appointment);
126
134
 
127
135
  // Check if procedure is already added
128
- const existingProcedure = metadata.extendedProcedures?.find(
129
- p => p.procedureId === procedureId
130
- );
136
+ const existingProcedure = metadata.extendedProcedures?.find(p => p.procedureId === procedureId);
131
137
  if (existingProcedure) {
132
138
  throw new Error(`Procedure ${procedureId} is already added to this appointment`);
133
139
  }
@@ -138,18 +144,18 @@ export async function addExtendedProcedureUtil(
138
144
  // Get procedure data for forms and products
139
145
  const procedureRef = doc(db, PROCEDURES_COLLECTION, procedureId);
140
146
  const procedureSnap = await getDoc(procedureRef);
141
-
147
+
142
148
  if (!procedureSnap.exists()) {
143
149
  throw new Error(`Procedure with ID ${procedureId} not found`);
144
150
  }
145
-
151
+
146
152
  const procedureData = procedureSnap.data();
147
153
 
148
154
  // Aggregate products
149
155
  const updatedProducts = await aggregateProductsFromProcedure(
150
156
  db,
151
157
  procedureId,
152
- metadata.appointmentProducts || []
158
+ metadata.appointmentProducts || [],
153
159
  );
154
160
 
155
161
  // Initialize forms for extended procedure
@@ -165,13 +171,16 @@ export async function addExtendedProcedureUtil(
165
171
  procedureData.documentationTemplates,
166
172
  appointment.patientId,
167
173
  appointment.practitionerId,
168
- appointment.clinicBranchId
174
+ appointment.clinicBranchId,
169
175
  );
170
176
 
171
177
  // Merge form IDs and info
172
178
  updatedLinkedFormIds = [...updatedLinkedFormIds, ...formInitResult.allLinkedFormIds];
173
179
  updatedLinkedForms = [...updatedLinkedForms, ...formInitResult.initializedFormsInfo];
174
- updatedPendingUserFormsIds = [...updatedPendingUserFormsIds, ...formInitResult.pendingUserFormsIds];
180
+ updatedPendingUserFormsIds = [
181
+ ...updatedPendingUserFormsIds,
182
+ ...formInitResult.pendingUserFormsIds,
183
+ ];
175
184
  }
176
185
 
177
186
  // Add extended procedure
@@ -193,7 +202,10 @@ export async function addExtendedProcedureUtil(
193
202
 
194
203
  /**
195
204
  * Removes an extended procedure from an appointment
196
- * Also removes associated products from appointmentProducts
205
+ * Also removes:
206
+ * - Associated products from appointmentProducts
207
+ * - Associated products from zonesData (all zones)
208
+ * - Associated forms
197
209
  * @param db Firestore instance
198
210
  * @param appointmentId Appointment ID
199
211
  * @param procedureId Procedure ID to remove
@@ -202,7 +214,7 @@ export async function addExtendedProcedureUtil(
202
214
  export async function removeExtendedProcedureUtil(
203
215
  db: Firestore,
204
216
  appointmentId: string,
205
- procedureId: string
217
+ procedureId: string,
206
218
  ): Promise<Appointment> {
207
219
  const appointment = await getAppointmentOrThrow(db, appointmentId);
208
220
  const metadata = initializeMetadata(appointment);
@@ -212,9 +224,7 @@ export async function removeExtendedProcedureUtil(
212
224
  }
213
225
 
214
226
  // Find and remove the procedure
215
- const procedureIndex = metadata.extendedProcedures.findIndex(
216
- p => p.procedureId === procedureId
217
- );
227
+ const procedureIndex = metadata.extendedProcedures.findIndex(p => p.procedureId === procedureId);
218
228
  if (procedureIndex === -1) {
219
229
  throw new Error(`Extended procedure ${procedureId} not found in this appointment`);
220
230
  }
@@ -222,23 +232,42 @@ export async function removeExtendedProcedureUtil(
222
232
  // Remove procedure
223
233
  metadata.extendedProcedures.splice(procedureIndex, 1);
224
234
 
225
- // Remove products associated with this procedure
235
+ // Remove products associated with this procedure from appointmentProducts
226
236
  const updatedProducts = (metadata.appointmentProducts || []).filter(
227
- p => p.procedureId !== procedureId
237
+ p => p.procedureId !== procedureId,
238
+ );
239
+
240
+ // Remove products from zonesData that belong to this procedure
241
+ const updatedZonesData = { ...(metadata.zonesData || {}) };
242
+ let productsRemovedFromZones = 0;
243
+
244
+ Object.keys(updatedZonesData).forEach(zoneId => {
245
+ const originalLength = updatedZonesData[zoneId].length;
246
+ updatedZonesData[zoneId] = updatedZonesData[zoneId].filter(item => {
247
+ // Keep notes and items that don't belong to this procedure
248
+ if (item.type === 'note') return true;
249
+ if (item.type === 'item' && item.belongingProcedureId !== procedureId) return true;
250
+ return false;
251
+ });
252
+ productsRemovedFromZones += originalLength - updatedZonesData[zoneId].length;
253
+ });
254
+
255
+ console.log(
256
+ `🗑️ [removeExtendedProcedure] Removed ${productsRemovedFromZones} products from zones for procedure ${procedureId}`,
228
257
  );
229
258
 
230
259
  // Remove forms associated with this procedure
231
260
  const removedFormIds = await removeFormsForExtendedProcedure(db, appointmentId, procedureId);
232
-
261
+
233
262
  // Update appointment form arrays
234
263
  const updatedLinkedFormIds = (appointment.linkedFormIds || []).filter(
235
- formId => !removedFormIds.includes(formId)
264
+ formId => !removedFormIds.includes(formId),
236
265
  );
237
266
  const updatedLinkedForms = (appointment.linkedForms || []).filter(
238
- form => !removedFormIds.includes(form.formId)
267
+ form => !removedFormIds.includes(form.formId),
239
268
  );
240
269
  const updatedPendingUserFormsIds = (appointment.pendingUserFormsIds || []).filter(
241
- formId => !removedFormIds.includes(formId)
270
+ formId => !removedFormIds.includes(formId),
242
271
  );
243
272
 
244
273
  // Update appointment
@@ -246,6 +275,7 @@ export async function removeExtendedProcedureUtil(
246
275
  await updateDoc(appointmentRef, {
247
276
  'metadata.extendedProcedures': metadata.extendedProcedures,
248
277
  'metadata.appointmentProducts': updatedProducts,
278
+ 'metadata.zonesData': updatedZonesData,
249
279
  linkedFormIds: updatedLinkedFormIds,
250
280
  linkedForms: updatedLinkedForms,
251
281
  pendingUserFormsIds: updatedPendingUserFormsIds,
@@ -263,7 +293,7 @@ export async function removeExtendedProcedureUtil(
263
293
  */
264
294
  export async function getExtendedProceduresUtil(
265
295
  db: Firestore,
266
- appointmentId: string
296
+ appointmentId: string,
267
297
  ): Promise<ExtendedProcedureInfo[]> {
268
298
  const appointment = await getAppointmentOrThrow(db, appointmentId);
269
299
  return appointment.metadata?.extendedProcedures || [];
@@ -277,9 +307,8 @@ export async function getExtendedProceduresUtil(
277
307
  */
278
308
  export async function getAppointmentProductsUtil(
279
309
  db: Firestore,
280
- appointmentId: string
310
+ appointmentId: string,
281
311
  ): Promise<AppointmentProductMetadata[]> {
282
312
  const appointment = await getAppointmentOrThrow(db, appointmentId);
283
313
  return appointment.metadata?.appointmentProducts || [];
284
314
  }
285
-
@@ -38,12 +38,14 @@ async function createExtendedProcedureInfoForRecommended(
38
38
  procedureSubCategoryName: data.subcategory.name,
39
39
  procedureTechnologyId: data.technology.id,
40
40
  procedureTechnologyName: data.technology.name,
41
- procedureProducts: (data.productsMetadata || []).map((pp: any) => ({
42
- productId: pp.product.id,
43
- productName: pp.product.name,
44
- brandId: pp.product.brandId,
45
- brandName: pp.product.brandName,
46
- })),
41
+ procedureProducts: (data.productsMetadata || [])
42
+ .filter((pp: any) => pp && pp.product) // Safety check for product-free procedures
43
+ .map((pp: any) => ({
44
+ productId: pp.product.id,
45
+ productName: pp.product.name,
46
+ brandId: pp.product.brandId,
47
+ brandName: pp.product.brandName,
48
+ })),
47
49
  };
48
50
  }
49
51
 
@@ -1553,7 +1553,7 @@ export class PractitionerService extends BaseService {
1553
1553
  }
1554
1554
  }
1555
1555
 
1556
- // Create procedure data for free consultation (without productId)
1556
+ // Create procedure data for free consultation (without productId or productsMetadata)
1557
1557
  const consultationData: Omit<CreateProcedureData, "productId"> = {
1558
1558
  name: "Free Consultation",
1559
1559
  nameLower: "free consultation",
@@ -1566,15 +1566,7 @@ export class PractitionerService extends BaseService {
1566
1566
  price: 0,
1567
1567
  currency: Currency.EUR,
1568
1568
  pricingMeasure: PricingMeasure.PER_SESSION,
1569
- productsMetadata: [
1570
- {
1571
- productId: "free-consultation-product",
1572
- price: 0,
1573
- currency: Currency.EUR,
1574
- pricingMeasure: PricingMeasure.PER_SESSION,
1575
- isDefault: true,
1576
- },
1577
- ],
1569
+ productsMetadata: undefined, // No products needed for consultations
1578
1570
  duration: 30, // 30 minutes consultation
1579
1571
  practitionerId: practitionerId,
1580
1572
  clinicBranchId: clinicId,
@@ -143,7 +143,7 @@ export class ProcedureService extends BaseService {
143
143
 
144
144
  /**
145
145
  * Transforms validated procedure product data (with productId) to ProcedureProduct objects (with full product)
146
- * @param productsMetadata Array of validated procedure product data
146
+ * @param productsMetadata Array of validated procedure product data (optional)
147
147
  * @param technologyId Technology ID to fetch products from
148
148
  * @returns Array of ProcedureProduct objects with full product information
149
149
  */
@@ -154,9 +154,14 @@ export class ProcedureService extends BaseService {
154
154
  currency: Currency;
155
155
  pricingMeasure: PricingMeasure;
156
156
  isDefault?: boolean;
157
- }[],
157
+ }[] | undefined,
158
158
  technologyId: string,
159
159
  ): Promise<ProcedureProduct[]> {
160
+ // Return empty array if no products metadata provided (for product-free procedures like consultations)
161
+ if (!productsMetadata || productsMetadata.length === 0) {
162
+ return [];
163
+ }
164
+
160
165
  const transformedProducts: ProcedureProduct[] = [];
161
166
 
162
167
  for (const productData of productsMetadata) {
@@ -189,21 +194,41 @@ export class ProcedureService extends BaseService {
189
194
  async createProcedure(data: CreateProcedureData): Promise<Procedure> {
190
195
  const validatedData = createProcedureSchema.parse(data);
191
196
 
197
+ // Check if this is a product-free procedure (e.g., free consultation)
198
+ const isProductFree = !validatedData.productId;
199
+
192
200
  // Generate procedure ID first so we can use it for media uploads
193
201
  const procedureId = this.generateId();
194
202
 
195
- // Get references to related entities (Category, Subcategory, Technology, Product)
196
- const [category, subcategory, technology, product] = await Promise.all([
203
+ // Get references to related entities (Category, Subcategory, Technology, and optionally Product)
204
+ const baseEntitiesPromises = [
197
205
  this.categoryService.getById(validatedData.categoryId),
198
206
  this.subcategoryService.getById(validatedData.categoryId, validatedData.subcategoryId),
199
207
  this.technologyService.getById(validatedData.technologyId),
200
- this.productService.getById(validatedData.technologyId, validatedData.productId),
201
- ]);
208
+ ];
209
+
210
+ // Only fetch product if productId is provided
211
+ if (!isProductFree) {
212
+ baseEntitiesPromises.push(
213
+ this.productService.getById(validatedData.technologyId, validatedData.productId!)
214
+ );
215
+ }
202
216
 
203
- if (!category || !subcategory || !technology || !product) {
217
+ const results = await Promise.all(baseEntitiesPromises);
218
+ const category = results[0] as Category | null;
219
+ const subcategory = results[1] as Subcategory | null;
220
+ const technology = results[2] as Technology | null;
221
+ const product = isProductFree ? undefined : ((results[3] as Product | null) || undefined);
222
+
223
+ if (!category || !subcategory || !technology) {
204
224
  throw new Error('One or more required base entities not found');
205
225
  }
206
226
 
227
+ // For regular procedures, validate product exists
228
+ if (!isProductFree && !product) {
229
+ throw new Error('Product not found for regular procedure');
230
+ }
231
+
207
232
  // Get clinic and practitioner information for aggregation
208
233
  const clinicRef = doc(this.db, CLINICS_COLLECTION, validatedData.clinicBranchId);
209
234
  const clinicSnapshot = await getDoc(clinicRef);
@@ -333,23 +358,50 @@ export class ProcedureService extends BaseService {
333
358
  throw new Error('Practitioner IDs array cannot be empty.');
334
359
  }
335
360
 
361
+ // Check if this is a product-free procedure
362
+ const isProductFree = !baseData.productId;
363
+
336
364
  // Add a dummy practitionerId for the validation schema to pass
337
365
  const validationData = { ...baseData, practitionerId: practitionerIds[0] };
338
366
  const validatedData = createProcedureSchema.parse(validationData);
339
367
 
340
368
  // 2. Fetch common data once to avoid redundant reads
341
- const [category, subcategory, technology, product, clinicSnapshot] = await Promise.all([
369
+ const baseEntitiesPromises = [
342
370
  this.categoryService.getById(validatedData.categoryId),
343
371
  this.subcategoryService.getById(validatedData.categoryId, validatedData.subcategoryId),
344
372
  this.technologyService.getById(validatedData.technologyId),
345
- this.productService.getById(validatedData.technologyId, validatedData.productId),
346
- getDoc(doc(this.db, CLINICS_COLLECTION, validatedData.clinicBranchId)),
373
+ ];
374
+
375
+ // Only fetch product if productId is provided
376
+ if (!isProductFree) {
377
+ baseEntitiesPromises.push(
378
+ this.productService.getById(validatedData.technologyId, validatedData.productId!)
379
+ );
380
+ }
381
+
382
+ // Fetch clinic separately to maintain type safety
383
+ const clinicSnapshotPromise = getDoc(doc(this.db, CLINICS_COLLECTION, validatedData.clinicBranchId));
384
+
385
+ const [baseResults, clinicSnapshot] = await Promise.all([
386
+ Promise.all(baseEntitiesPromises),
387
+ clinicSnapshotPromise
347
388
  ]);
389
+
390
+ const category = baseResults[0] as Category | null;
391
+ const subcategory = baseResults[1] as Subcategory | null;
392
+ const technology = baseResults[2] as Technology | null;
393
+ const product = isProductFree ? undefined : ((baseResults[3] as Product | null) || undefined);
348
394
 
349
- if (!category || !subcategory || !technology || !product) {
395
+ if (!category || !subcategory || !technology) {
350
396
  throw new Error('One or more required base entities not found');
351
397
  }
352
- if (!clinicSnapshot.exists()) {
398
+
399
+ // For regular procedures, validate product exists
400
+ if (!isProductFree && !product) {
401
+ throw new Error('Product not found for regular procedure');
402
+ }
403
+
404
+ if (!clinicSnapshot || !clinicSnapshot.exists()) {
353
405
  throw new Error(`Clinic with ID ${validatedData.clinicBranchId} not found`);
354
406
  }
355
407
  const clinic = clinicSnapshot.data() as Clinic;
@@ -1416,6 +1468,7 @@ export class ProcedureService extends BaseService {
1416
1468
  }
1417
1469
 
1418
1470
  // Transform productsMetadata from validation format to ProcedureProduct format
1471
+ // For consultations, this will return empty array since no products are provided
1419
1472
  const transformedProductsMetadata = await this.transformProductsMetadata(
1420
1473
  data.productsMetadata,
1421
1474
  data.technologyId,
@@ -1451,22 +1504,6 @@ export class ProcedureService extends BaseService {
1451
1504
  services: practitioner.procedures || [],
1452
1505
  };
1453
1506
 
1454
- // Create a placeholder product for consultation procedures
1455
- const consultationProduct: Product = {
1456
- id: 'consultation-no-product',
1457
- name: 'No Product Required',
1458
- description: 'Consultation procedures do not require specific products',
1459
- brandId: 'consultation-brand',
1460
- brandName: 'Consultation',
1461
- technologyId: data.technologyId,
1462
- technologyName: technology.name,
1463
- categoryId: technology.categoryId,
1464
- subcategoryId: technology.subcategoryId,
1465
- isActive: true,
1466
- createdAt: new Date(),
1467
- updatedAt: new Date(),
1468
- };
1469
-
1470
1507
  // Create the procedure object
1471
1508
  const { productsMetadata: _, ...dataWithoutProductsMetadata } = data;
1472
1509
  const newProcedure: Omit<Procedure, 'createdAt' | 'updatedAt'> = {
@@ -1477,8 +1514,8 @@ export class ProcedureService extends BaseService {
1477
1514
  category,
1478
1515
  subcategory,
1479
1516
  technology,
1480
- product: consultationProduct, // Use placeholder product
1481
- productsMetadata: transformedProductsMetadata, // Use transformed data, not original
1517
+ product: undefined, // No product needed for consultations
1518
+ productsMetadata: transformedProductsMetadata, // Empty array for consultations
1482
1519
  blockingConditions: technology.blockingConditions,
1483
1520
  contraindications: technology.contraindications || [],
1484
1521
  contraindicationIds: technology.contraindications?.map(c => c.id) || [],
@@ -45,8 +45,8 @@ export interface Procedure {
45
45
  subcategory: Subcategory;
46
46
  /** Technology used in this procedure */
47
47
  technology: Technology;
48
- /** Default product used in this procedure */
49
- product: Product;
48
+ /** Default product used in this procedure (optional for consultations) */
49
+ product?: Product;
50
50
  /** Default price of the procedure */
51
51
  price: number;
52
52
  /** Currency for the price */
@@ -54,7 +54,7 @@ export interface Procedure {
54
54
  /** How the price is measured (per ml, per zone, etc.) - for default product*/
55
55
  pricingMeasure: PricingMeasure;
56
56
  /** Duration of the procedure in minutes */
57
- productsMetadata: ProcedureProduct[];
57
+ productsMetadata?: ProcedureProduct[];
58
58
  duration: number;
59
59
  /** Blocking conditions that prevent this procedure */
60
60
  blockingConditions: BlockingCondition[];
@@ -104,9 +104,9 @@ export interface CreateProcedureData {
104
104
  categoryId: string;
105
105
  subcategoryId: string;
106
106
  technologyId: string;
107
- productId: string;
107
+ productId?: string;
108
108
  price: number;
109
- productsMetadata: {
109
+ productsMetadata?: {
110
110
  productId: string;
111
111
  price: number;
112
112
  currency: Currency;
@@ -59,7 +59,7 @@ export const procedureExtendedInfoSchema = z.object({
59
59
  productName: z.string().min(MIN_STRING_LENGTH, 'Product name is required'),
60
60
  brandId: z.string().min(MIN_STRING_LENGTH, 'Brand ID is required'),
61
61
  brandName: z.string().min(MIN_STRING_LENGTH, 'Brand name is required'),
62
- })
62
+ }),
63
63
  ),
64
64
  });
65
65
 
@@ -176,59 +176,66 @@ export const finalBillingSchema = z.object({
176
176
  /**
177
177
  * Schema for zone item data (product or note per zone)
178
178
  */
179
- export const zoneItemDataSchema = z.object({
180
- productId: z.string().optional(),
181
- productName: z.string().optional(),
182
- productBrandId: z.string().optional(),
183
- productBrandName: z.string().optional(),
184
- belongingProcedureId: z.string().min(MIN_STRING_LENGTH, 'Belonging procedure ID is required'),
185
- type: z.enum(['item', 'note'], {
186
- required_error: 'Type must be either "item" or "note"',
187
- }),
188
- stage: z.enum(['before', 'after'], {
189
- required_error: 'Stage must be either "before" or "after"',
190
- }).optional(),
191
- price: z.number().min(0, 'Price must be non-negative').optional(),
192
- currency: z.nativeEnum(Currency).optional(),
193
- unitOfMeasurement: z.nativeEnum(PricingMeasure).optional(),
194
- priceOverrideAmount: z.number().min(0, 'Price override amount must be non-negative').optional(),
195
- quantity: z.number().min(0, 'Quantity must be non-negative').optional(),
196
- parentZone: z.string().min(MIN_STRING_LENGTH, 'Parent zone is required').refine(
197
- val => {
198
- const parts = val.split('.');
199
- return parts.length === 2;
179
+ export const zoneItemDataSchema = z
180
+ .object({
181
+ productId: z.string().optional(),
182
+ productName: z.string().optional(),
183
+ productBrandId: z.string().optional(),
184
+ productBrandName: z.string().optional(),
185
+ belongingProcedureId: z.string().min(MIN_STRING_LENGTH, 'Belonging procedure ID is required'),
186
+ type: z.enum(['item', 'note'], {
187
+ required_error: 'Type must be either "item" or "note"',
188
+ }),
189
+ stage: z
190
+ .enum(['before', 'after'], {
191
+ required_error: 'Stage must be either "before" or "after"',
192
+ })
193
+ .optional(),
194
+ price: z.number().min(0, 'Price must be non-negative').optional(),
195
+ currency: z.nativeEnum(Currency).optional(),
196
+ unitOfMeasurement: z.nativeEnum(PricingMeasure).optional(),
197
+ priceOverrideAmount: z.number().min(0, 'Price override amount must be non-negative').optional(),
198
+ quantity: z.number().min(0, 'Quantity must be non-negative').optional(),
199
+ parentZone: z
200
+ .string()
201
+ .min(MIN_STRING_LENGTH, 'Parent zone is required')
202
+ .refine(
203
+ val => {
204
+ const parts = val.split('.');
205
+ return parts.length === 2;
206
+ },
207
+ {
208
+ message: 'Parent zone must be in "category.zone" format (e.g., "face.forehead")',
209
+ },
210
+ ),
211
+ subzones: z.array(z.string()).refine(
212
+ val => {
213
+ return val.every(subzone => {
214
+ const parts = subzone.split('.');
215
+ return parts.length === 3;
216
+ });
217
+ },
218
+ {
219
+ message: 'Subzones must be in "category.zone.subzone" format (e.g., "face.forehead.left")',
220
+ },
221
+ ),
222
+ notes: z.string().max(MAX_STRING_LENGTH_LONG, 'Notes too long').optional(),
223
+ subtotal: z.number().min(0, 'Subtotal must be non-negative').optional(),
224
+ ionNumber: z.string().optional(),
225
+ createdAt: z.string().optional(),
226
+ updatedAt: z.string().optional(),
227
+ })
228
+ .refine(
229
+ data => {
230
+ if (data.type === 'item') {
231
+ return !!(data.productId && data.productName && (data.price || data.priceOverrideAmount));
232
+ }
233
+ return true;
200
234
  },
201
235
  {
202
- message: 'Parent zone must be in "category.zone" format (e.g., "face.forehead")',
203
- }
204
- ),
205
- subzones: z.array(z.string()).refine(
206
- val => {
207
- return val.every(subzone => {
208
- const parts = subzone.split('.');
209
- return parts.length === 3;
210
- });
236
+ message: 'Item type requires productId, productName, and either price or priceOverrideAmount',
211
237
  },
212
- {
213
- message: 'Subzones must be in "category.zone.subzone" format (e.g., "face.forehead.left")',
214
- }
215
- ),
216
- notes: z.string().max(MAX_STRING_LENGTH_LONG, 'Notes too long').optional(),
217
- subtotal: z.number().min(0, 'Subtotal must be non-negative').optional(),
218
- ionNumber: z.string().optional(),
219
- createdAt: z.string().optional(),
220
- updatedAt: z.string().optional(),
221
- }).refine(
222
- data => {
223
- if (data.type === 'item') {
224
- return !!(data.productId && data.productName && (data.price || data.priceOverrideAmount));
225
- }
226
- return true;
227
- },
228
- {
229
- message: 'Item type requires productId, productName, and either price or priceOverrideAmount',
230
- }
231
- );
238
+ );
232
239
 
233
240
  /**
234
241
  * Schema for appointment product metadata
@@ -263,7 +270,7 @@ export const extendedProcedureInfoSchema = z.object({
263
270
  productName: z.string().min(MIN_STRING_LENGTH, 'Product name is required'),
264
271
  brandId: z.string().min(MIN_STRING_LENGTH, 'Brand ID is required'),
265
272
  brandName: z.string().min(MIN_STRING_LENGTH, 'Brand name is required'),
266
- })
273
+ }),
267
274
  ),
268
275
  });
269
276
 
@@ -277,7 +284,7 @@ export const recommendedProcedureTimeframeSchema = z.object({
277
284
 
278
285
  export const recommendedProcedureSchema = z.object({
279
286
  procedure: extendedProcedureInfoSchema,
280
- note: z.string().min(1, 'Note is required').max(MAX_STRING_LENGTH_LONG, 'Note too long'),
287
+ note: z.string().max(MAX_STRING_LENGTH_LONG, 'Note too long'), // Note is now optional (no min length)
281
288
  timeframe: recommendedProcedureTimeframeSchema,
282
289
  });
283
290