@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.
- package/dist/admin/index.d.mts +5 -4
- package/dist/admin/index.d.ts +5 -4
- package/dist/admin/index.js +3 -26
- package/dist/admin/index.mjs +3 -26
- package/dist/index.d.mts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +49 -44
- package/dist/index.mjs +49 -44
- package/package.json +1 -1
- package/src/admin/booking/booking.admin.ts +21 -17
- package/src/admin/free-consultation/free-consultation-utils.admin.ts +4 -31
- package/src/services/appointment/utils/appointment.utils.ts +2 -2
- package/src/services/appointment/utils/extended-procedure.utils.ts +82 -53
- package/src/services/appointment/utils/recommended-procedure.utils.ts +8 -6
- package/src/services/practitioner/practitioner.service.ts +2 -10
- package/src/services/procedure/procedure.service.ts +22 -22
- package/src/types/procedure/index.ts +5 -5
- package/src/validations/appointment.schema.ts +60 -53
- package/src/validations/procedure.schema.ts +4 -4
|
@@ -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:
|
|
1481
|
-
productsMetadata: transformedProductsMetadata, //
|
|
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
|
|
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
|
|
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
|
|
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
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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: '
|
|
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().
|
|
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).
|
|
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),
|