@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/index.mjs CHANGED
@@ -529,7 +529,8 @@ var recommendedProcedureTimeframeSchema = z3.object({
529
529
  });
530
530
  var recommendedProcedureSchema = z3.object({
531
531
  procedure: extendedProcedureInfoSchema,
532
- note: z3.string().min(1, "Note is required").max(MAX_STRING_LENGTH_LONG, "Note too long"),
532
+ note: z3.string().max(MAX_STRING_LENGTH_LONG, "Note too long"),
533
+ // Note is now optional (no min length)
533
534
  timeframe: recommendedProcedureTimeframeSchema
534
535
  });
535
536
  var appointmentMetadataSchema = z3.object({
@@ -1739,7 +1740,7 @@ async function aggregateProductsFromProcedure(db, procedureId, existingProducts)
1739
1740
  }
1740
1741
  const procedureData = procedureSnap.data();
1741
1742
  const productsMetadata = procedureData.productsMetadata || [];
1742
- const newProducts = productsMetadata.map((pp) => {
1743
+ const newProducts = productsMetadata.filter((pp) => pp && pp.product).map((pp) => {
1743
1744
  const product = pp.product;
1744
1745
  return {
1745
1746
  productId: product.id,
@@ -1792,7 +1793,7 @@ async function createExtendedProcedureInfo(db, procedureId) {
1792
1793
  // Access embedded technology
1793
1794
  procedureTechnologyName: data.technology.name,
1794
1795
  // Access embedded technology
1795
- procedureProducts: (data.productsMetadata || []).map((pp) => ({
1796
+ procedureProducts: (data.productsMetadata || []).filter((pp) => pp && pp.product).map((pp) => ({
1796
1797
  productId: pp.product.id,
1797
1798
  // Access embedded product
1798
1799
  productName: pp.product.name,
@@ -1808,9 +1809,7 @@ async function addExtendedProcedureUtil(db, appointmentId, procedureId) {
1808
1809
  var _a;
1809
1810
  const appointment = await getAppointmentOrThrow(db, appointmentId);
1810
1811
  const metadata = initializeMetadata(appointment);
1811
- const existingProcedure = (_a = metadata.extendedProcedures) == null ? void 0 : _a.find(
1812
- (p) => p.procedureId === procedureId
1813
- );
1812
+ const existingProcedure = (_a = metadata.extendedProcedures) == null ? void 0 : _a.find((p) => p.procedureId === procedureId);
1814
1813
  if (existingProcedure) {
1815
1814
  throw new Error(`Procedure ${procedureId} is already added to this appointment`);
1816
1815
  }
@@ -1841,7 +1840,10 @@ async function addExtendedProcedureUtil(db, appointmentId, procedureId) {
1841
1840
  );
1842
1841
  updatedLinkedFormIds = [...updatedLinkedFormIds, ...formInitResult.allLinkedFormIds];
1843
1842
  updatedLinkedForms = [...updatedLinkedForms, ...formInitResult.initializedFormsInfo];
1844
- updatedPendingUserFormsIds = [...updatedPendingUserFormsIds, ...formInitResult.pendingUserFormsIds];
1843
+ updatedPendingUserFormsIds = [
1844
+ ...updatedPendingUserFormsIds,
1845
+ ...formInitResult.pendingUserFormsIds
1846
+ ];
1845
1847
  }
1846
1848
  const extendedProcedures = [...metadata.extendedProcedures || [], extendedProcedureInfo];
1847
1849
  const appointmentRef = doc5(db, APPOINTMENTS_COLLECTION, appointmentId);
@@ -1861,9 +1863,7 @@ async function removeExtendedProcedureUtil(db, appointmentId, procedureId) {
1861
1863
  if (!metadata.extendedProcedures || metadata.extendedProcedures.length === 0) {
1862
1864
  throw new Error("No extended procedures found for this appointment");
1863
1865
  }
1864
- const procedureIndex = metadata.extendedProcedures.findIndex(
1865
- (p) => p.procedureId === procedureId
1866
- );
1866
+ const procedureIndex = metadata.extendedProcedures.findIndex((p) => p.procedureId === procedureId);
1867
1867
  if (procedureIndex === -1) {
1868
1868
  throw new Error(`Extended procedure ${procedureId} not found in this appointment`);
1869
1869
  }
@@ -1871,6 +1871,20 @@ async function removeExtendedProcedureUtil(db, appointmentId, procedureId) {
1871
1871
  const updatedProducts = (metadata.appointmentProducts || []).filter(
1872
1872
  (p) => p.procedureId !== procedureId
1873
1873
  );
1874
+ const updatedZonesData = { ...metadata.zonesData || {} };
1875
+ let productsRemovedFromZones = 0;
1876
+ Object.keys(updatedZonesData).forEach((zoneId) => {
1877
+ const originalLength = updatedZonesData[zoneId].length;
1878
+ updatedZonesData[zoneId] = updatedZonesData[zoneId].filter((item) => {
1879
+ if (item.type === "note") return true;
1880
+ if (item.type === "item" && item.belongingProcedureId !== procedureId) return true;
1881
+ return false;
1882
+ });
1883
+ productsRemovedFromZones += originalLength - updatedZonesData[zoneId].length;
1884
+ });
1885
+ console.log(
1886
+ `\u{1F5D1}\uFE0F [removeExtendedProcedure] Removed ${productsRemovedFromZones} products from zones for procedure ${procedureId}`
1887
+ );
1874
1888
  const removedFormIds = await removeFormsForExtendedProcedure(db, appointmentId, procedureId);
1875
1889
  const updatedLinkedFormIds = (appointment.linkedFormIds || []).filter(
1876
1890
  (formId) => !removedFormIds.includes(formId)
@@ -1885,6 +1899,7 @@ async function removeExtendedProcedureUtil(db, appointmentId, procedureId) {
1885
1899
  await updateDoc4(appointmentRef, {
1886
1900
  "metadata.extendedProcedures": metadata.extendedProcedures,
1887
1901
  "metadata.appointmentProducts": updatedProducts,
1902
+ "metadata.zonesData": updatedZonesData,
1888
1903
  linkedFormIds: updatedLinkedFormIds,
1889
1904
  linkedForms: updatedLinkedForms,
1890
1905
  pendingUserFormsIds: updatedPendingUserFormsIds,
@@ -1923,7 +1938,7 @@ async function createExtendedProcedureInfoForRecommended(db, procedureId) {
1923
1938
  procedureSubCategoryName: data.subcategory.name,
1924
1939
  procedureTechnologyId: data.technology.id,
1925
1940
  procedureTechnologyName: data.technology.name,
1926
- procedureProducts: (data.productsMetadata || []).map((pp) => ({
1941
+ procedureProducts: (data.productsMetadata || []).filter((pp) => pp && pp.product).map((pp) => ({
1927
1942
  productId: pp.product.id,
1928
1943
  productName: pp.product.name,
1929
1944
  brandId: pp.product.brandId,
@@ -8747,15 +8762,8 @@ var PractitionerService = class extends BaseService {
8747
8762
  price: 0,
8748
8763
  currency: "EUR" /* EUR */,
8749
8764
  pricingMeasure: "per_session" /* PER_SESSION */,
8750
- productsMetadata: [
8751
- {
8752
- productId: "free-consultation-product",
8753
- price: 0,
8754
- currency: "EUR" /* EUR */,
8755
- pricingMeasure: "per_session" /* PER_SESSION */,
8756
- isDefault: true
8757
- }
8758
- ],
8765
+ productsMetadata: void 0,
8766
+ // No products needed for consultations
8759
8767
  duration: 30,
8760
8768
  // 30 minutes consultation
8761
8769
  practitionerId,
@@ -16562,11 +16570,11 @@ var createProcedureSchema = z26.object({
16562
16570
  categoryId: z26.string().min(1),
16563
16571
  subcategoryId: z26.string().min(1),
16564
16572
  technologyId: z26.string().min(1),
16565
- productId: z26.string().min(1),
16573
+ productId: z26.string().min(1).optional(),
16566
16574
  price: z26.number().min(0),
16567
16575
  currency: z26.nativeEnum(Currency),
16568
16576
  pricingMeasure: z26.nativeEnum(PricingMeasure),
16569
- productsMetadata: z26.array(procedureProductDataSchema).min(1),
16577
+ productsMetadata: z26.array(procedureProductDataSchema).min(1).optional(),
16570
16578
  duration: z26.number().min(1).max(480),
16571
16579
  // Max 8 hours
16572
16580
  practitionerId: z26.string().min(1),
@@ -16603,10 +16611,10 @@ var procedureSchema = z26.object({
16603
16611
  // We'll validate the full subcategory object separately
16604
16612
  technology: z26.any(),
16605
16613
  // We'll validate the full technology object separately
16606
- product: z26.any(),
16607
- // We'll validate the full product object separately
16608
- productsMetadata: z26.array(storedProcedureProductSchema).min(1),
16609
- // Use stored format schema
16614
+ product: z26.any().optional(),
16615
+ // We'll validate the full product object separately (optional for consultations)
16616
+ productsMetadata: z26.array(storedProcedureProductSchema).optional(),
16617
+ // Use stored format schema (optional for consultations)
16610
16618
  price: z26.number().min(0),
16611
16619
  currency: z26.nativeEnum(Currency),
16612
16620
  pricingMeasure: z26.nativeEnum(PricingMeasure),
@@ -16699,11 +16707,14 @@ var ProcedureService = class extends BaseService {
16699
16707
  }
16700
16708
  /**
16701
16709
  * Transforms validated procedure product data (with productId) to ProcedureProduct objects (with full product)
16702
- * @param productsMetadata Array of validated procedure product data
16710
+ * @param productsMetadata Array of validated procedure product data (optional)
16703
16711
  * @param technologyId Technology ID to fetch products from
16704
16712
  * @returns Array of ProcedureProduct objects with full product information
16705
16713
  */
16706
16714
  async transformProductsMetadata(productsMetadata, technologyId) {
16715
+ if (!productsMetadata || productsMetadata.length === 0) {
16716
+ return [];
16717
+ }
16707
16718
  const transformedProducts = [];
16708
16719
  for (const productData of productsMetadata) {
16709
16720
  const product = await this.productService.getById(technologyId, productData.productId);
@@ -16730,12 +16741,16 @@ var ProcedureService = class extends BaseService {
16730
16741
  async createProcedure(data) {
16731
16742
  var _a, _b, _c;
16732
16743
  const validatedData = createProcedureSchema.parse(data);
16744
+ if (!validatedData.productId) {
16745
+ throw new Error("productId is required for regular procedures. Use createConsultationProcedure for product-free procedures.");
16746
+ }
16733
16747
  const procedureId = this.generateId();
16734
16748
  const [category, subcategory, technology, product] = await Promise.all([
16735
16749
  this.categoryService.getById(validatedData.categoryId),
16736
16750
  this.subcategoryService.getById(validatedData.categoryId, validatedData.subcategoryId),
16737
16751
  this.technologyService.getById(validatedData.technologyId),
16738
16752
  this.productService.getById(validatedData.technologyId, validatedData.productId)
16753
+ // Safe: validated above
16739
16754
  ]);
16740
16755
  if (!category || !subcategory || !technology || !product) {
16741
16756
  throw new Error("One or more required base entities not found");
@@ -16844,6 +16859,9 @@ var ProcedureService = class extends BaseService {
16844
16859
  if (!practitionerIds || practitionerIds.length === 0) {
16845
16860
  throw new Error("Practitioner IDs array cannot be empty.");
16846
16861
  }
16862
+ if (!baseData.productId) {
16863
+ throw new Error("productId is required for regular procedures. Use createConsultationProcedure for product-free procedures.");
16864
+ }
16847
16865
  const validationData = { ...baseData, practitionerId: practitionerIds[0] };
16848
16866
  const validatedData = createProcedureSchema.parse(validationData);
16849
16867
  const [category, subcategory, technology, product, clinicSnapshot] = await Promise.all([
@@ -16851,6 +16869,7 @@ var ProcedureService = class extends BaseService {
16851
16869
  this.subcategoryService.getById(validatedData.categoryId, validatedData.subcategoryId),
16852
16870
  this.technologyService.getById(validatedData.technologyId),
16853
16871
  this.productService.getById(validatedData.technologyId, validatedData.productId),
16872
+ // Safe: validated above
16854
16873
  getDoc37(doc36(this.db, CLINICS_COLLECTION, validatedData.clinicBranchId))
16855
16874
  ]);
16856
16875
  if (!category || !subcategory || !technology || !product) {
@@ -17711,20 +17730,6 @@ var ProcedureService = class extends BaseService {
17711
17730
  rating: ((_a = practitioner.reviewInfo) == null ? void 0 : _a.averageRating) || 0,
17712
17731
  services: practitioner.procedures || []
17713
17732
  };
17714
- const consultationProduct = {
17715
- id: "consultation-no-product",
17716
- name: "No Product Required",
17717
- description: "Consultation procedures do not require specific products",
17718
- brandId: "consultation-brand",
17719
- brandName: "Consultation",
17720
- technologyId: data.technologyId,
17721
- technologyName: technology.name,
17722
- categoryId: technology.categoryId,
17723
- subcategoryId: technology.subcategoryId,
17724
- isActive: true,
17725
- createdAt: /* @__PURE__ */ new Date(),
17726
- updatedAt: /* @__PURE__ */ new Date()
17727
- };
17728
17733
  const { productsMetadata: _, ...dataWithoutProductsMetadata } = data;
17729
17734
  const newProcedure = {
17730
17735
  id: procedureId,
@@ -17734,10 +17739,10 @@ var ProcedureService = class extends BaseService {
17734
17739
  category,
17735
17740
  subcategory,
17736
17741
  technology,
17737
- product: consultationProduct,
17738
- // Use placeholder product
17742
+ product: void 0,
17743
+ // No product needed for consultations
17739
17744
  productsMetadata: transformedProductsMetadata,
17740
- // Use transformed data, not original
17745
+ // Empty array for consultations
17741
17746
  blockingConditions: technology.blockingConditions,
17742
17747
  contraindications: technology.contraindications || [],
17743
17748
  contraindicationIds: ((_b = technology.contraindications) == null ? void 0 : _b.map((c) => c.id)) || [],
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blackcode_sa/metaestetics-api",
3
3
  "private": false,
4
- "version": "1.12.46",
4
+ "version": "1.12.47",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -1004,29 +1004,33 @@ export class BookingAdmin {
1004
1004
  procedureTechnologyName: procedureTechnology?.name || "",
1005
1005
  procedureProductBrandId: procedureProduct?.brandId || "",
1006
1006
  procedureProductBrandName: procedureProduct?.brandName || "",
1007
- procedureProducts: productsMetadata.map((pp: any) => ({
1008
- productId: pp.product.id,
1009
- productName: pp.product.name,
1010
- brandId: pp.product.brandId,
1011
- brandName: pp.product.brandName,
1012
- })),
1007
+ procedureProducts: productsMetadata
1008
+ .filter((pp: any) => pp && pp.product) // Safety check for product-free procedures
1009
+ .map((pp: any) => ({
1010
+ productId: pp.product.id,
1011
+ productName: pp.product.name,
1012
+ brandId: pp.product.brandId,
1013
+ brandName: pp.product.brandName,
1014
+ })),
1013
1015
  };
1014
1016
  }
1015
1017
 
1016
1018
  private _generateAppointmentProductsFromProcedure(procedure: Procedure): any[] {
1017
1019
  const productsMetadata = procedure.productsMetadata || [];
1018
1020
 
1019
- return productsMetadata.map((pp: any) => {
1020
- const product = pp.product;
1021
- return {
1022
- productId: product.id,
1023
- productName: product.name,
1024
- brandId: product.brandId,
1025
- brandName: product.brandName,
1026
- procedureId: procedure.id,
1027
- price: pp.price,
1028
- currency: pp.currency,
1029
- unitOfMeasurement: pp.pricingMeasure,
1021
+ return productsMetadata
1022
+ .filter((pp: any) => pp && pp.product) // Safety check for product-free procedures
1023
+ .map((pp: any) => {
1024
+ const product = pp.product;
1025
+ return {
1026
+ productId: product.id,
1027
+ productName: product.name,
1028
+ brandId: product.brandId,
1029
+ brandName: product.brandName,
1030
+ procedureId: procedure.id,
1031
+ price: pp.price,
1032
+ currency: pp.currency,
1033
+ unitOfMeasurement: pp.pricingMeasure,
1030
1034
  };
1031
1035
  });
1032
1036
  }
@@ -7,11 +7,11 @@ import {
7
7
  import { CATEGORIES_COLLECTION } from '../../backoffice/types/category.types';
8
8
  import { SUBCATEGORIES_COLLECTION } from '../../backoffice/types/subcategory.types';
9
9
  import { TECHNOLOGIES_COLLECTION } from '../../backoffice/types/technology.types';
10
- import { PRODUCTS_COLLECTION } from '../../backoffice/types/product.types';
11
10
 
12
11
  /**
13
12
  * Ensures that the free consultation infrastructure exists in the database
14
- * Creates category, subcategory, technology, and product if they don't exist
13
+ * Creates category, subcategory, and technology if they don't exist
14
+ * Note: Consultations are product-free procedures, so no product is created
15
15
  * @param db - Firestore database instance (optional, defaults to admin.firestore())
16
16
  * @returns Promise<boolean> - Always returns true after ensuring infrastructure exists
17
17
  */
@@ -54,7 +54,7 @@ export async function freeConsultationInfrastructure(
54
54
 
55
55
  /**
56
56
  * Creates the complete free consultation infrastructure
57
- * Creates category, subcategory, technology, and product in the correct order
57
+ * Creates category, subcategory, and technology (no product needed)
58
58
  * @param db - Firestore database instance
59
59
  */
60
60
  async function createFreeConsultationInfrastructure(db: admin.firestore.Firestore): Promise<void> {
@@ -125,33 +125,6 @@ async function createFreeConsultationInfrastructure(db: admin.firestore.Firestor
125
125
  };
126
126
  batch.set(technologyRef, technologyData);
127
127
 
128
- // 4. Create Product: "free-consultation-product"
129
- const productRef = db
130
- .collection(TECHNOLOGIES_COLLECTION)
131
- .doc('free-consultation-tech')
132
- .collection(PRODUCTS_COLLECTION)
133
- .doc('free-consultation-product');
134
-
135
- const productData = {
136
- id: 'free-consultation-product',
137
- name: 'Free Consultation Service',
138
- description: 'No physical product required for consultation services',
139
- brandId: 'consultation-brand',
140
- brandName: 'Consultation Services',
141
- technologyId: 'free-consultation-tech',
142
- technologyName: 'Free Consultation Technology',
143
- categoryId: 'consultation',
144
- subcategoryId: 'free-consultation',
145
- isActive: true,
146
- createdAt: now,
147
- updatedAt: now,
148
- technicalDetails: 'Virtual consultation service - no physical product required',
149
- warnings: [],
150
- indications: ['Initial patient assessment', 'Treatment planning', 'Patient education'],
151
- contraindications: [],
152
- };
153
- batch.set(productRef, productData);
154
-
155
128
  // Commit all changes atomically
156
129
  await batch.commit();
157
130
 
@@ -159,5 +132,5 @@ async function createFreeConsultationInfrastructure(db: admin.firestore.Firestor
159
132
  console.log(' ✓ Category: consultation');
160
133
  console.log(' ✓ Subcategory: free-consultation');
161
134
  console.log(' ✓ Technology: free-consultation-tech');
162
- console.log(' Product: free-consultation-product');
135
+ console.log(' No product needed - consultations are product-free procedures');
163
136
  }
@@ -135,8 +135,8 @@ export async function fetchAggregatedInfoUtil(
135
135
  categoryName: procedureData.category?.name || '',
136
136
  subcategoryName: procedureData.subcategory?.name || '',
137
137
  technologyName: procedureData.technology?.name || '',
138
- brandName: procedureData.product?.brand || '',
139
- productName: procedureData.product?.name || '',
138
+ brandName: procedureData.product?.brandName || '', // Safe: optional chaining
139
+ productName: procedureData.product?.name || '', // Safe: optional chaining
140
140
  price: procedureData.price || 0,
141
141
  pricingMeasure: procedureData.pricingMeasure,
142
142
  currency: procedureData.currency,
@@ -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