@blackcode_sa/metaestetics-api 1.12.40 → 1.12.41

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
@@ -2236,7 +2236,7 @@ var AppointmentService = class extends BaseService {
2236
2236
  * @returns The updated appointment
2237
2237
  */
2238
2238
  async updateAppointment(appointmentId, data) {
2239
- var _a, _b;
2239
+ var _a, _b, _c, _d;
2240
2240
  try {
2241
2241
  console.log(`[APPOINTMENT_SERVICE] Updating appointment with ID: ${appointmentId}`);
2242
2242
  if ((_a = data.metadata) == null ? void 0 : _a.zonePhotos) {
@@ -2259,18 +2259,34 @@ var AppointmentService = class extends BaseService {
2259
2259
  }
2260
2260
  data.metadata.zonePhotos = migratedZonePhotos;
2261
2261
  }
2262
- if (((_b = data.metadata) == null ? void 0 : _b.recommendedProcedures) && Array.isArray(data.metadata.recommendedProcedures)) {
2263
- const validRecommendations = data.metadata.recommendedProcedures.filter(
2264
- (rec) => rec.note && typeof rec.note === "string" && rec.note.trim().length > 0
2265
- );
2262
+ console.log(
2263
+ "[APPOINTMENT_SERVICE] \u{1F50D} BEFORE CLEANUP - recommendedProcedures:",
2264
+ JSON.stringify((_b = data.metadata) == null ? void 0 : _b.recommendedProcedures, null, 2)
2265
+ );
2266
+ if (((_c = data.metadata) == null ? void 0 : _c.recommendedProcedures) && Array.isArray(data.metadata.recommendedProcedures)) {
2267
+ const validRecommendations = data.metadata.recommendedProcedures.filter((rec) => {
2268
+ const isValid = rec.note && typeof rec.note === "string" && rec.note.trim().length > 0;
2269
+ if (!isValid) {
2270
+ console.log("[APPOINTMENT_SERVICE] \u274C INVALID recommendation found:", rec);
2271
+ }
2272
+ return isValid;
2273
+ });
2266
2274
  if (validRecommendations.length !== data.metadata.recommendedProcedures.length) {
2267
2275
  console.log(
2268
- `[APPOINTMENT_SERVICE] Removing ${data.metadata.recommendedProcedures.length - validRecommendations.length} invalid recommended procedures with empty notes`
2276
+ `[APPOINTMENT_SERVICE] \u{1F9F9} Removing ${data.metadata.recommendedProcedures.length - validRecommendations.length} invalid recommended procedures with empty notes`
2269
2277
  );
2270
2278
  data.metadata.recommendedProcedures = validRecommendations;
2279
+ } else {
2280
+ console.log("[APPOINTMENT_SERVICE] \u2705 All recommendedProcedures are valid");
2271
2281
  }
2272
2282
  }
2283
+ console.log(
2284
+ "[APPOINTMENT_SERVICE] \u{1F50D} AFTER CLEANUP - recommendedProcedures:",
2285
+ JSON.stringify((_d = data.metadata) == null ? void 0 : _d.recommendedProcedures, null, 2)
2286
+ );
2287
+ console.log("[APPOINTMENT_SERVICE] \u{1F50D} Starting Zod validation...");
2273
2288
  const validatedData = await updateAppointmentSchema.parseAsync(data);
2289
+ console.log("[APPOINTMENT_SERVICE] \u2705 Zod validation passed!");
2274
2290
  const updatedAppointment = await updateAppointmentUtil(this.db, appointmentId, validatedData);
2275
2291
  console.log(`[APPOINTMENT_SERVICE] Appointment ${appointmentId} updated successfully`);
2276
2292
  return updatedAppointment;
@@ -18943,8 +18959,14 @@ import {
18943
18959
  updateDoc as updateDoc38,
18944
18960
  where as where36,
18945
18961
  arrayUnion as arrayUnion9,
18946
- arrayRemove as arrayRemove8
18962
+ arrayRemove as arrayRemove8,
18963
+ writeBatch as writeBatch7
18947
18964
  } from "firebase/firestore";
18965
+
18966
+ // src/backoffice/types/product.types.ts
18967
+ var PRODUCTS_COLLECTION = "products";
18968
+
18969
+ // src/backoffice/services/technology.service.ts
18948
18970
  var DEFAULT_CERTIFICATION_REQUIREMENT = {
18949
18971
  minimumLevel: "aesthetician" /* AESTHETICIAN */,
18950
18972
  requiredSpecialties: []
@@ -19542,6 +19564,95 @@ var TechnologyService = class extends BaseService {
19542
19564
  })
19543
19565
  );
19544
19566
  }
19567
+ // ==========================================
19568
+ // NEW METHODS: Product assignment management
19569
+ // ==========================================
19570
+ /**
19571
+ * Assigns multiple products to a technology
19572
+ * Updates each product's assignedTechnologyIds array
19573
+ */
19574
+ async assignProducts(technologyId, productIds) {
19575
+ const batch = writeBatch7(this.db);
19576
+ for (const productId of productIds) {
19577
+ const productRef = doc41(this.db, PRODUCTS_COLLECTION, productId);
19578
+ batch.update(productRef, {
19579
+ assignedTechnologyIds: arrayUnion9(technologyId),
19580
+ updatedAt: /* @__PURE__ */ new Date()
19581
+ });
19582
+ }
19583
+ await batch.commit();
19584
+ }
19585
+ /**
19586
+ * Unassigns multiple products from a technology
19587
+ * Updates each product's assignedTechnologyIds array
19588
+ */
19589
+ async unassignProducts(technologyId, productIds) {
19590
+ const batch = writeBatch7(this.db);
19591
+ for (const productId of productIds) {
19592
+ const productRef = doc41(this.db, PRODUCTS_COLLECTION, productId);
19593
+ batch.update(productRef, {
19594
+ assignedTechnologyIds: arrayRemove8(technologyId),
19595
+ updatedAt: /* @__PURE__ */ new Date()
19596
+ });
19597
+ }
19598
+ await batch.commit();
19599
+ }
19600
+ /**
19601
+ * Gets products assigned to a specific technology
19602
+ * Reads from top-level collection for immediate consistency (Cloud Functions may lag)
19603
+ */
19604
+ async getAssignedProducts(technologyId) {
19605
+ const q = query36(
19606
+ collection36(this.db, PRODUCTS_COLLECTION),
19607
+ where36("assignedTechnologyIds", "array-contains", technologyId),
19608
+ where36("isActive", "==", true),
19609
+ orderBy22("name")
19610
+ );
19611
+ const snapshot = await getDocs36(q);
19612
+ return snapshot.docs.map(
19613
+ (doc44) => ({
19614
+ id: doc44.id,
19615
+ ...doc44.data()
19616
+ })
19617
+ );
19618
+ }
19619
+ /**
19620
+ * Gets products NOT assigned to a specific technology
19621
+ */
19622
+ async getUnassignedProducts(technologyId) {
19623
+ const q = query36(
19624
+ collection36(this.db, PRODUCTS_COLLECTION),
19625
+ where36("isActive", "==", true),
19626
+ orderBy22("name")
19627
+ );
19628
+ const snapshot = await getDocs36(q);
19629
+ const allProducts = snapshot.docs.map(
19630
+ (doc44) => ({
19631
+ id: doc44.id,
19632
+ ...doc44.data()
19633
+ })
19634
+ );
19635
+ return allProducts.filter(
19636
+ (product) => {
19637
+ var _a;
19638
+ return !((_a = product.assignedTechnologyIds) == null ? void 0 : _a.includes(technologyId));
19639
+ }
19640
+ );
19641
+ }
19642
+ /**
19643
+ * Gets product assignment statistics for a technology
19644
+ */
19645
+ async getProductStats(technologyId) {
19646
+ const products = await this.getAssignedProducts(technologyId);
19647
+ const byBrand = {};
19648
+ products.forEach((product) => {
19649
+ byBrand[product.brandName] = (byBrand[product.brandName] || 0) + 1;
19650
+ });
19651
+ return {
19652
+ totalAssigned: products.length,
19653
+ byBrand
19654
+ };
19655
+ }
19545
19656
  };
19546
19657
 
19547
19658
  // src/backoffice/services/product.service.ts
@@ -19558,16 +19669,20 @@ import {
19558
19669
  limit as limit21,
19559
19670
  orderBy as orderBy23,
19560
19671
  startAfter as startAfter19,
19561
- getCountFromServer as getCountFromServer7
19672
+ getCountFromServer as getCountFromServer7,
19673
+ arrayUnion as arrayUnion10,
19674
+ arrayRemove as arrayRemove9
19562
19675
  } from "firebase/firestore";
19563
-
19564
- // src/backoffice/types/product.types.ts
19565
- var PRODUCTS_COLLECTION = "products";
19566
-
19567
- // src/backoffice/services/product.service.ts
19568
19676
  var ProductService = class extends BaseService {
19569
19677
  /**
19570
- * Gets reference to products collection under a technology
19678
+ * Gets reference to top-level products collection (source of truth)
19679
+ * @returns Firestore collection reference
19680
+ */
19681
+ getTopLevelProductsRef() {
19682
+ return collection37(this.db, PRODUCTS_COLLECTION);
19683
+ }
19684
+ /**
19685
+ * Gets reference to products collection under a technology (backward compatibility)
19571
19686
  * @param technologyId - ID of the technology
19572
19687
  * @returns Firestore collection reference
19573
19688
  */
@@ -19583,6 +19698,7 @@ var ProductService = class extends BaseService {
19583
19698
  ...product,
19584
19699
  brandId,
19585
19700
  technologyId,
19701
+ // Required for old subcollection structure
19586
19702
  createdAt: now,
19587
19703
  updatedAt: now,
19588
19704
  isActive: true
@@ -19641,31 +19757,24 @@ var ProductService = class extends BaseService {
19641
19757
  return snapshot.data().count;
19642
19758
  }
19643
19759
  /**
19644
- * Gets counts of active products grouped by category, subcategory, and technology.
19645
- * This uses a single collectionGroup query for efficiency.
19760
+ * Gets counts of active products grouped by technology.
19761
+ * NOTE: Only counts top-level collection to avoid duplication during migration.
19762
+ * Categories/subcategories not available in top-level structure.
19646
19763
  */
19647
19764
  async getProductCounts() {
19648
- const q = query37(collectionGroup3(this.db, PRODUCTS_COLLECTION), where37("isActive", "==", true));
19649
- const snapshot = await getDocs37(q);
19650
19765
  const counts = {
19651
19766
  byCategory: {},
19652
19767
  bySubcategory: {},
19653
19768
  byTechnology: {}
19654
19769
  };
19655
- if (snapshot.empty) {
19656
- return counts;
19657
- }
19770
+ const q = query37(this.getTopLevelProductsRef(), where37("isActive", "==", true));
19771
+ const snapshot = await getDocs37(q);
19658
19772
  snapshot.docs.forEach((doc44) => {
19659
19773
  const product = doc44.data();
19660
- const { categoryId, subcategoryId, technologyId } = product;
19661
- if (categoryId) {
19662
- counts.byCategory[categoryId] = (counts.byCategory[categoryId] || 0) + 1;
19663
- }
19664
- if (subcategoryId) {
19665
- counts.bySubcategory[subcategoryId] = (counts.bySubcategory[subcategoryId] || 0) + 1;
19666
- }
19667
- if (technologyId) {
19668
- counts.byTechnology[technologyId] = (counts.byTechnology[technologyId] || 0) + 1;
19774
+ if (product.assignedTechnologyIds && Array.isArray(product.assignedTechnologyIds)) {
19775
+ product.assignedTechnologyIds.forEach((techId) => {
19776
+ counts.byTechnology[techId] = (counts.byTechnology[techId] || 0) + 1;
19777
+ });
19669
19778
  }
19670
19779
  });
19671
19780
  return counts;
@@ -19744,12 +19853,166 @@ var ProductService = class extends BaseService {
19744
19853
  ...docSnap.data()
19745
19854
  };
19746
19855
  }
19856
+ // ==========================================
19857
+ // NEW METHODS: Top-level collection (preferred)
19858
+ // ==========================================
19859
+ /**
19860
+ * Creates a new product in the top-level collection
19861
+ */
19862
+ async createTopLevel(brandId, product, technologyIds = []) {
19863
+ const now = /* @__PURE__ */ new Date();
19864
+ const newProduct = {
19865
+ ...product,
19866
+ brandId,
19867
+ assignedTechnologyIds: technologyIds,
19868
+ createdAt: now,
19869
+ updatedAt: now,
19870
+ isActive: true
19871
+ };
19872
+ const productRef = await addDoc8(this.getTopLevelProductsRef(), newProduct);
19873
+ return { id: productRef.id, ...newProduct };
19874
+ }
19875
+ /**
19876
+ * Gets all products from the top-level collection
19877
+ */
19878
+ async getAllTopLevel(options) {
19879
+ const { rowsPerPage, lastVisible, brandId } = options;
19880
+ const constraints = [where37("isActive", "==", true), orderBy23("name")];
19881
+ if (brandId) {
19882
+ constraints.push(where37("brandId", "==", brandId));
19883
+ }
19884
+ if (lastVisible) {
19885
+ constraints.push(startAfter19(lastVisible));
19886
+ }
19887
+ constraints.push(limit21(rowsPerPage));
19888
+ const q = query37(this.getTopLevelProductsRef(), ...constraints);
19889
+ const snapshot = await getDocs37(q);
19890
+ const products = snapshot.docs.map(
19891
+ (doc44) => ({
19892
+ id: doc44.id,
19893
+ ...doc44.data()
19894
+ })
19895
+ );
19896
+ const newLastVisible = snapshot.docs[snapshot.docs.length - 1];
19897
+ return { products, lastVisible: newLastVisible };
19898
+ }
19899
+ /**
19900
+ * Gets a product by ID from the top-level collection
19901
+ */
19902
+ async getByIdTopLevel(productId) {
19903
+ const docRef = doc42(this.getTopLevelProductsRef(), productId);
19904
+ const docSnap = await getDoc43(docRef);
19905
+ if (!docSnap.exists()) return null;
19906
+ return {
19907
+ id: docSnap.id,
19908
+ ...docSnap.data()
19909
+ };
19910
+ }
19911
+ /**
19912
+ * Updates a product in the top-level collection
19913
+ */
19914
+ async updateTopLevel(productId, product) {
19915
+ const updateData = {
19916
+ ...product,
19917
+ updatedAt: /* @__PURE__ */ new Date()
19918
+ };
19919
+ const docRef = doc42(this.getTopLevelProductsRef(), productId);
19920
+ await updateDoc39(docRef, updateData);
19921
+ return this.getByIdTopLevel(productId);
19922
+ }
19923
+ /**
19924
+ * Deletes a product from the top-level collection (soft delete)
19925
+ */
19926
+ async deleteTopLevel(productId) {
19927
+ await this.updateTopLevel(productId, {
19928
+ isActive: false
19929
+ });
19930
+ }
19931
+ /**
19932
+ * Assigns a product to a technology
19933
+ */
19934
+ async assignToTechnology(productId, technologyId) {
19935
+ const docRef = doc42(this.getTopLevelProductsRef(), productId);
19936
+ await updateDoc39(docRef, {
19937
+ assignedTechnologyIds: arrayUnion10(technologyId),
19938
+ updatedAt: /* @__PURE__ */ new Date()
19939
+ });
19940
+ }
19941
+ /**
19942
+ * Unassigns a product from a technology
19943
+ */
19944
+ async unassignFromTechnology(productId, technologyId) {
19945
+ const docRef = doc42(this.getTopLevelProductsRef(), productId);
19946
+ await updateDoc39(docRef, {
19947
+ assignedTechnologyIds: arrayRemove9(technologyId),
19948
+ updatedAt: /* @__PURE__ */ new Date()
19949
+ });
19950
+ }
19951
+ /**
19952
+ * Gets products assigned to a specific technology
19953
+ */
19954
+ async getAssignedProducts(technologyId) {
19955
+ const q = query37(
19956
+ this.getTopLevelProductsRef(),
19957
+ where37("assignedTechnologyIds", "array-contains", technologyId),
19958
+ where37("isActive", "==", true),
19959
+ orderBy23("name")
19960
+ );
19961
+ const snapshot = await getDocs37(q);
19962
+ return snapshot.docs.map(
19963
+ (doc44) => ({
19964
+ id: doc44.id,
19965
+ ...doc44.data()
19966
+ })
19967
+ );
19968
+ }
19969
+ /**
19970
+ * Gets products NOT assigned to a specific technology
19971
+ */
19972
+ async getUnassignedProducts(technologyId) {
19973
+ const q = query37(
19974
+ this.getTopLevelProductsRef(),
19975
+ where37("isActive", "==", true),
19976
+ orderBy23("name")
19977
+ );
19978
+ const snapshot = await getDocs37(q);
19979
+ const allProducts = snapshot.docs.map(
19980
+ (doc44) => ({
19981
+ id: doc44.id,
19982
+ ...doc44.data()
19983
+ })
19984
+ );
19985
+ return allProducts.filter(
19986
+ (product) => {
19987
+ var _a;
19988
+ return !((_a = product.assignedTechnologyIds) == null ? void 0 : _a.includes(technologyId));
19989
+ }
19990
+ );
19991
+ }
19992
+ /**
19993
+ * Gets all products for a brand (from top-level collection)
19994
+ */
19995
+ async getByBrand(brandId) {
19996
+ const q = query37(
19997
+ this.getTopLevelProductsRef(),
19998
+ where37("brandId", "==", brandId),
19999
+ where37("isActive", "==", true),
20000
+ orderBy23("name")
20001
+ );
20002
+ const snapshot = await getDocs37(q);
20003
+ return snapshot.docs.map(
20004
+ (doc44) => ({
20005
+ id: doc44.id,
20006
+ ...doc44.data()
20007
+ })
20008
+ );
20009
+ }
19747
20010
  };
19748
20011
 
19749
20012
  // src/backoffice/services/constants.service.ts
19750
20013
  import {
19751
- arrayRemove as arrayRemove9,
19752
- arrayUnion as arrayUnion10,
20014
+ arrayRemove as arrayRemove10,
20015
+ arrayUnion as arrayUnion11,
19753
20016
  doc as doc43,
19754
20017
  getDoc as getDoc44,
19755
20018
  setDoc as setDoc30,
@@ -19817,7 +20080,7 @@ var ConstantsService = class extends BaseService {
19817
20080
  await setDoc30(this.treatmentBenefitsDocRef, { benefits: [newBenefit] });
19818
20081
  } else {
19819
20082
  await updateDoc40(this.treatmentBenefitsDocRef, {
19820
- benefits: arrayUnion10(newBenefit)
20083
+ benefits: arrayUnion11(newBenefit)
19821
20084
  });
19822
20085
  }
19823
20086
  return newBenefit;
@@ -19871,7 +20134,7 @@ var ConstantsService = class extends BaseService {
19871
20134
  return;
19872
20135
  }
19873
20136
  await updateDoc40(this.treatmentBenefitsDocRef, {
19874
- benefits: arrayRemove9(benefitToRemove)
20137
+ benefits: arrayRemove10(benefitToRemove)
19875
20138
  });
19876
20139
  }
19877
20140
  // =================================================================
@@ -19924,7 +20187,7 @@ var ConstantsService = class extends BaseService {
19924
20187
  });
19925
20188
  } else {
19926
20189
  await updateDoc40(this.contraindicationsDocRef, {
19927
- contraindications: arrayUnion10(newContraindication)
20190
+ contraindications: arrayUnion11(newContraindication)
19928
20191
  });
19929
20192
  }
19930
20193
  return newContraindication;
@@ -19980,7 +20243,7 @@ var ConstantsService = class extends BaseService {
19980
20243
  return;
19981
20244
  }
19982
20245
  await updateDoc40(this.contraindicationsDocRef, {
19983
- contraindications: arrayRemove9(toRemove)
20246
+ contraindications: arrayRemove10(toRemove)
19984
20247
  });
19985
20248
  }
19986
20249
  };
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.40",
4
+ "version": "1.12.41",
5
5
  "description": "Firebase authentication service with anonymous upgrade support",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -0,0 +1,116 @@
1
+ import * as admin from 'firebase-admin';
2
+ import { Product } from '../types/product.types';
3
+
4
+ /**
5
+ * Migration script to copy existing products from technology subcollections
6
+ * to the new top-level products collection
7
+ *
8
+ * Usage: Run this once to migrate existing data
9
+ */
10
+ export async function migrateProductsToTopLevel(db: admin.firestore.Firestore) {
11
+ console.log('🚀 Starting product migration...');
12
+
13
+ // Get all technologies
14
+ const technologiesSnapshot = await db.collection('technologies').get();
15
+
16
+ const productMap = new Map<string, {
17
+ product: any;
18
+ technologyIds: string[];
19
+ }>();
20
+
21
+ let totalProcessed = 0;
22
+
23
+ // Step 1: Collect all products from all technology subcollections
24
+ for (const techDoc of technologiesSnapshot.docs) {
25
+ const technologyId = techDoc.id;
26
+ console.log(`📦 Processing technology: ${technologyId}`);
27
+
28
+ const productsSnapshot = await db
29
+ .collection('technologies')
30
+ .doc(technologyId)
31
+ .collection('products')
32
+ .get();
33
+
34
+ for (const productDoc of productsSnapshot.docs) {
35
+ const productId = productDoc.id;
36
+ const productData = productDoc.data();
37
+
38
+ totalProcessed++;
39
+
40
+ // Deduplicate by name + brandId
41
+ const key = `${productData.name}_${productData.brandId}`;
42
+
43
+ if (productMap.has(key)) {
44
+ // Product already exists, just add this technology
45
+ productMap.get(key)!.technologyIds.push(technologyId);
46
+ } else {
47
+ // New product
48
+ productMap.set(key, {
49
+ product: productData,
50
+ technologyIds: [technologyId],
51
+ });
52
+ }
53
+ }
54
+ }
55
+
56
+ console.log(`✅ Found ${productMap.size} unique products from ${totalProcessed} total entries`);
57
+
58
+ // Step 2: Create products in top-level collection
59
+ const batch = db.batch();
60
+ let batchCount = 0;
61
+ const MAX_BATCH_SIZE = 500;
62
+ let createdCount = 0;
63
+
64
+ for (const [key, { product, technologyIds }] of productMap.entries()) {
65
+ const productRef = db.collection('products').doc();
66
+
67
+ const migratedProduct: any = {
68
+ ...product,
69
+ assignedTechnologyIds: technologyIds, // Track all assigned technologies
70
+ migratedAt: admin.firestore.FieldValue.serverTimestamp(),
71
+ migrationKey: key, // For debugging
72
+ };
73
+
74
+ batch.set(productRef, migratedProduct);
75
+ createdCount++;
76
+ batchCount++;
77
+
78
+ if (batchCount >= MAX_BATCH_SIZE) {
79
+ await batch.commit();
80
+ console.log(`💾 Committed batch of ${batchCount} products (${createdCount}/${productMap.size})`);
81
+ batchCount = 0;
82
+ }
83
+ }
84
+
85
+ if (batchCount > 0) {
86
+ await batch.commit();
87
+ console.log(`💾 Committed final batch of ${batchCount} products`);
88
+ }
89
+
90
+ console.log('✅ Migration complete!');
91
+ console.log(`📊 Summary:`);
92
+ console.log(` - Products processed: ${totalProcessed}`);
93
+ console.log(` - Unique products created: ${createdCount}`);
94
+ console.log(` - Average technologies per product: ${(createdCount > 0 ? totalProcessed / createdCount : 0).toFixed(2)}`);
95
+
96
+ return {
97
+ totalProcessed,
98
+ uniqueCreated: createdCount,
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Run migration (for testing)
104
+ */
105
+ if (require.main === module) {
106
+ (async () => {
107
+ try {
108
+ await migrateProductsToTopLevel(admin.firestore());
109
+ process.exit(0);
110
+ } catch (error) {
111
+ console.error('❌ Migration failed:', error);
112
+ process.exit(1);
113
+ }
114
+ })();
115
+ }
116
+