@blackcode_sa/metaestetics-api 1.12.46 → 1.12.47

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.
@@ -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,6 +194,11 @@ export class ProcedureService extends BaseService {
189
194
  async createProcedure(data: CreateProcedureData): Promise<Procedure> {
190
195
  const validatedData = createProcedureSchema.parse(data);
191
196
 
197
+ // Validate that productId is provided (regular procedures require products)
198
+ if (!validatedData.productId) {
199
+ throw new Error('productId is required for regular procedures. Use createConsultationProcedure for product-free procedures.');
200
+ }
201
+
192
202
  // Generate procedure ID first so we can use it for media uploads
193
203
  const procedureId = this.generateId();
194
204
 
@@ -197,7 +207,7 @@ export class ProcedureService extends BaseService {
197
207
  this.categoryService.getById(validatedData.categoryId),
198
208
  this.subcategoryService.getById(validatedData.categoryId, validatedData.subcategoryId),
199
209
  this.technologyService.getById(validatedData.technologyId),
200
- this.productService.getById(validatedData.technologyId, validatedData.productId),
210
+ this.productService.getById(validatedData.technologyId, validatedData.productId!), // Safe: validated above
201
211
  ]);
202
212
 
203
213
  if (!category || !subcategory || !technology || !product) {
@@ -333,6 +343,11 @@ export class ProcedureService extends BaseService {
333
343
  throw new Error('Practitioner IDs array cannot be empty.');
334
344
  }
335
345
 
346
+ // Validate that productId is provided (regular procedures require products)
347
+ if (!baseData.productId) {
348
+ throw new Error('productId is required for regular procedures. Use createConsultationProcedure for product-free procedures.');
349
+ }
350
+
336
351
  // Add a dummy practitionerId for the validation schema to pass
337
352
  const validationData = { ...baseData, practitionerId: practitionerIds[0] };
338
353
  const validatedData = createProcedureSchema.parse(validationData);
@@ -342,7 +357,7 @@ export class ProcedureService extends BaseService {
342
357
  this.categoryService.getById(validatedData.categoryId),
343
358
  this.subcategoryService.getById(validatedData.categoryId, validatedData.subcategoryId),
344
359
  this.technologyService.getById(validatedData.technologyId),
345
- this.productService.getById(validatedData.technologyId, validatedData.productId),
360
+ this.productService.getById(validatedData.technologyId, validatedData.productId!), // Safe: validated above
346
361
  getDoc(doc(this.db, CLINICS_COLLECTION, validatedData.clinicBranchId)),
347
362
  ]);
348
363
 
@@ -1416,6 +1431,7 @@ export class ProcedureService extends BaseService {
1416
1431
  }
1417
1432
 
1418
1433
  // Transform productsMetadata from validation format to ProcedureProduct format
1434
+ // For consultations, this will return empty array since no products are provided
1419
1435
  const transformedProductsMetadata = await this.transformProductsMetadata(
1420
1436
  data.productsMetadata,
1421
1437
  data.technologyId,
@@ -1451,22 +1467,6 @@ export class ProcedureService extends BaseService {
1451
1467
  services: practitioner.procedures || [],
1452
1468
  };
1453
1469
 
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
1470
  // Create the procedure object
1471
1471
  const { productsMetadata: _, ...dataWithoutProductsMetadata } = data;
1472
1472
  const newProcedure: Omit<Procedure, 'createdAt' | 'updatedAt'> = {
@@ -1477,8 +1477,8 @@ export class ProcedureService extends BaseService {
1477
1477
  category,
1478
1478
  subcategory,
1479
1479
  technology,
1480
- product: consultationProduct, // Use placeholder product
1481
- productsMetadata: transformedProductsMetadata, // Use transformed data, not original
1480
+ product: undefined, // No product needed for consultations
1481
+ productsMetadata: transformedProductsMetadata, // Empty array for consultations
1482
1482
  blockingConditions: technology.blockingConditions,
1483
1483
  contraindications: technology.contraindications || [],
1484
1484
  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
 
@@ -52,11 +52,11 @@ export const createProcedureSchema = z.object({
52
52
  categoryId: z.string().min(1),
53
53
  subcategoryId: z.string().min(1),
54
54
  technologyId: z.string().min(1),
55
- productId: z.string().min(1),
55
+ productId: z.string().min(1).optional(),
56
56
  price: z.number().min(0),
57
57
  currency: z.nativeEnum(Currency),
58
58
  pricingMeasure: z.nativeEnum(PricingMeasure),
59
- productsMetadata: z.array(procedureProductDataSchema).min(1),
59
+ productsMetadata: z.array(procedureProductDataSchema).min(1).optional(),
60
60
  duration: z.number().min(1).max(480), // Max 8 hours
61
61
  practitionerId: z.string().min(1),
62
62
  clinicBranchId: z.string().min(1),
@@ -97,8 +97,8 @@ export const procedureSchema = z.object({
97
97
  category: z.any(), // We'll validate the full category object separately
98
98
  subcategory: z.any(), // We'll validate the full subcategory object separately
99
99
  technology: z.any(), // We'll validate the full technology object separately
100
- product: z.any(), // We'll validate the full product object separately
101
- productsMetadata: z.array(storedProcedureProductSchema).min(1), // Use stored format schema
100
+ product: z.any().optional(), // We'll validate the full product object separately (optional for consultations)
101
+ productsMetadata: z.array(storedProcedureProductSchema).optional(), // Use stored format schema (optional for consultations)
102
102
  price: z.number().min(0),
103
103
  currency: z.nativeEnum(Currency),
104
104
  pricingMeasure: z.nativeEnum(PricingMeasure),