@blackcode_sa/metaestetics-api 1.12.39 → 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,9 +2236,57 @@ var AppointmentService = class extends BaseService {
2236
2236
  * @returns The updated appointment
2237
2237
  */
2238
2238
  async updateAppointment(appointmentId, data) {
2239
+ var _a, _b, _c, _d;
2239
2240
  try {
2240
2241
  console.log(`[APPOINTMENT_SERVICE] Updating appointment with ID: ${appointmentId}`);
2242
+ if ((_a = data.metadata) == null ? void 0 : _a.zonePhotos) {
2243
+ const migratedZonePhotos = {};
2244
+ for (const [key, value] of Object.entries(data.metadata.zonePhotos)) {
2245
+ if (Array.isArray(value)) {
2246
+ migratedZonePhotos[key] = value;
2247
+ } else {
2248
+ console.log(`[APPOINTMENT_SERVICE] Auto-migrating ${key} from object to array format`);
2249
+ const oldData = value;
2250
+ migratedZonePhotos[key] = [
2251
+ {
2252
+ before: oldData.before || null,
2253
+ after: oldData.after || null,
2254
+ beforeNote: null,
2255
+ afterNote: null
2256
+ }
2257
+ ];
2258
+ }
2259
+ }
2260
+ data.metadata.zonePhotos = migratedZonePhotos;
2261
+ }
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
+ });
2274
+ if (validRecommendations.length !== data.metadata.recommendedProcedures.length) {
2275
+ console.log(
2276
+ `[APPOINTMENT_SERVICE] \u{1F9F9} Removing ${data.metadata.recommendedProcedures.length - validRecommendations.length} invalid recommended procedures with empty notes`
2277
+ );
2278
+ data.metadata.recommendedProcedures = validRecommendations;
2279
+ } else {
2280
+ console.log("[APPOINTMENT_SERVICE] \u2705 All recommendedProcedures are valid");
2281
+ }
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...");
2241
2288
  const validatedData = await updateAppointmentSchema.parseAsync(data);
2289
+ console.log("[APPOINTMENT_SERVICE] \u2705 Zod validation passed!");
2242
2290
  const updatedAppointment = await updateAppointmentUtil(this.db, appointmentId, validatedData);
2243
2291
  console.log(`[APPOINTMENT_SERVICE] Appointment ${appointmentId} updated successfully`);
2244
2292
  return updatedAppointment;
@@ -2876,7 +2924,25 @@ var AppointmentService = class extends BaseService {
2876
2924
  finalbilling: null,
2877
2925
  finalizationNotes: null
2878
2926
  };
2879
- const currentZonePhotos = currentMetadata.zonePhotos || {};
2927
+ let currentZonePhotos = {};
2928
+ if (currentMetadata.zonePhotos) {
2929
+ for (const [key, value] of Object.entries(currentMetadata.zonePhotos)) {
2930
+ if (Array.isArray(value)) {
2931
+ currentZonePhotos[key] = value;
2932
+ } else {
2933
+ console.log(`[APPOINTMENT_SERVICE] Auto-migrating ${key} from object to array format`);
2934
+ const oldData = value;
2935
+ currentZonePhotos[key] = [
2936
+ {
2937
+ before: oldData.before || null,
2938
+ after: oldData.after || null,
2939
+ beforeNote: null,
2940
+ afterNote: null
2941
+ }
2942
+ ];
2943
+ }
2944
+ }
2945
+ }
2880
2946
  if (!currentZonePhotos[zoneId]) {
2881
2947
  currentZonePhotos[zoneId] = [];
2882
2948
  }
@@ -3275,7 +3341,13 @@ var AppointmentService = class extends BaseService {
3275
3341
  console.log(
3276
3342
  `[APPOINTMENT_SERVICE] Adding recommended procedure ${procedureId} to appointment ${appointmentId}`
3277
3343
  );
3278
- return await addRecommendedProcedureUtil(this.db, appointmentId, procedureId, note, timeframe);
3344
+ return await addRecommendedProcedureUtil(
3345
+ this.db,
3346
+ appointmentId,
3347
+ procedureId,
3348
+ note,
3349
+ timeframe
3350
+ );
3279
3351
  } catch (error) {
3280
3352
  console.error(`[APPOINTMENT_SERVICE] Error adding recommended procedure:`, error);
3281
3353
  throw error;
@@ -3312,7 +3384,12 @@ var AppointmentService = class extends BaseService {
3312
3384
  console.log(
3313
3385
  `[APPOINTMENT_SERVICE] Updating recommended procedure at index ${recommendationIndex} in appointment ${appointmentId}`
3314
3386
  );
3315
- return await updateRecommendedProcedureUtil(this.db, appointmentId, recommendationIndex, updates);
3387
+ return await updateRecommendedProcedureUtil(
3388
+ this.db,
3389
+ appointmentId,
3390
+ recommendationIndex,
3391
+ updates
3392
+ );
3316
3393
  } catch (error) {
3317
3394
  console.error(`[APPOINTMENT_SERVICE] Error updating recommended procedure:`, error);
3318
3395
  throw error;
@@ -18882,8 +18959,14 @@ import {
18882
18959
  updateDoc as updateDoc38,
18883
18960
  where as where36,
18884
18961
  arrayUnion as arrayUnion9,
18885
- arrayRemove as arrayRemove8
18962
+ arrayRemove as arrayRemove8,
18963
+ writeBatch as writeBatch7
18886
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
18887
18970
  var DEFAULT_CERTIFICATION_REQUIREMENT = {
18888
18971
  minimumLevel: "aesthetician" /* AESTHETICIAN */,
18889
18972
  requiredSpecialties: []
@@ -19481,6 +19564,95 @@ var TechnologyService = class extends BaseService {
19481
19564
  })
19482
19565
  );
19483
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
+ }
19484
19656
  };
19485
19657
 
19486
19658
  // src/backoffice/services/product.service.ts
@@ -19497,16 +19669,20 @@ import {
19497
19669
  limit as limit21,
19498
19670
  orderBy as orderBy23,
19499
19671
  startAfter as startAfter19,
19500
- getCountFromServer as getCountFromServer7
19672
+ getCountFromServer as getCountFromServer7,
19673
+ arrayUnion as arrayUnion10,
19674
+ arrayRemove as arrayRemove9
19501
19675
  } from "firebase/firestore";
19502
-
19503
- // src/backoffice/types/product.types.ts
19504
- var PRODUCTS_COLLECTION = "products";
19505
-
19506
- // src/backoffice/services/product.service.ts
19507
19676
  var ProductService = class extends BaseService {
19508
19677
  /**
19509
- * 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)
19510
19686
  * @param technologyId - ID of the technology
19511
19687
  * @returns Firestore collection reference
19512
19688
  */
@@ -19522,6 +19698,7 @@ var ProductService = class extends BaseService {
19522
19698
  ...product,
19523
19699
  brandId,
19524
19700
  technologyId,
19701
+ // Required for old subcollection structure
19525
19702
  createdAt: now,
19526
19703
  updatedAt: now,
19527
19704
  isActive: true
@@ -19580,31 +19757,24 @@ var ProductService = class extends BaseService {
19580
19757
  return snapshot.data().count;
19581
19758
  }
19582
19759
  /**
19583
- * Gets counts of active products grouped by category, subcategory, and technology.
19584
- * 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.
19585
19763
  */
19586
19764
  async getProductCounts() {
19587
- const q = query37(collectionGroup3(this.db, PRODUCTS_COLLECTION), where37("isActive", "==", true));
19588
- const snapshot = await getDocs37(q);
19589
19765
  const counts = {
19590
19766
  byCategory: {},
19591
19767
  bySubcategory: {},
19592
19768
  byTechnology: {}
19593
19769
  };
19594
- if (snapshot.empty) {
19595
- return counts;
19596
- }
19770
+ const q = query37(this.getTopLevelProductsRef(), where37("isActive", "==", true));
19771
+ const snapshot = await getDocs37(q);
19597
19772
  snapshot.docs.forEach((doc44) => {
19598
19773
  const product = doc44.data();
19599
- const { categoryId, subcategoryId, technologyId } = product;
19600
- if (categoryId) {
19601
- counts.byCategory[categoryId] = (counts.byCategory[categoryId] || 0) + 1;
19602
- }
19603
- if (subcategoryId) {
19604
- counts.bySubcategory[subcategoryId] = (counts.bySubcategory[subcategoryId] || 0) + 1;
19605
- }
19606
- if (technologyId) {
19607
- 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
+ });
19608
19778
  }
19609
19779
  });
19610
19780
  return counts;
@@ -19683,12 +19853,166 @@ var ProductService = class extends BaseService {
19683
19853
  ...docSnap.data()
19684
19854
  };
19685
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
+ }
19686
20010
  };
19687
20011
 
19688
20012
  // src/backoffice/services/constants.service.ts
19689
20013
  import {
19690
- arrayRemove as arrayRemove9,
19691
- arrayUnion as arrayUnion10,
20014
+ arrayRemove as arrayRemove10,
20015
+ arrayUnion as arrayUnion11,
19692
20016
  doc as doc43,
19693
20017
  getDoc as getDoc44,
19694
20018
  setDoc as setDoc30,
@@ -19756,7 +20080,7 @@ var ConstantsService = class extends BaseService {
19756
20080
  await setDoc30(this.treatmentBenefitsDocRef, { benefits: [newBenefit] });
19757
20081
  } else {
19758
20082
  await updateDoc40(this.treatmentBenefitsDocRef, {
19759
- benefits: arrayUnion10(newBenefit)
20083
+ benefits: arrayUnion11(newBenefit)
19760
20084
  });
19761
20085
  }
19762
20086
  return newBenefit;
@@ -19810,7 +20134,7 @@ var ConstantsService = class extends BaseService {
19810
20134
  return;
19811
20135
  }
19812
20136
  await updateDoc40(this.treatmentBenefitsDocRef, {
19813
- benefits: arrayRemove9(benefitToRemove)
20137
+ benefits: arrayRemove10(benefitToRemove)
19814
20138
  });
19815
20139
  }
19816
20140
  // =================================================================
@@ -19863,7 +20187,7 @@ var ConstantsService = class extends BaseService {
19863
20187
  });
19864
20188
  } else {
19865
20189
  await updateDoc40(this.contraindicationsDocRef, {
19866
- contraindications: arrayUnion10(newContraindication)
20190
+ contraindications: arrayUnion11(newContraindication)
19867
20191
  });
19868
20192
  }
19869
20193
  return newContraindication;
@@ -19919,7 +20243,7 @@ var ConstantsService = class extends BaseService {
19919
20243
  return;
19920
20244
  }
19921
20245
  await updateDoc40(this.contraindicationsDocRef, {
19922
- contraindications: arrayRemove9(toRemove)
20246
+ contraindications: arrayRemove10(toRemove)
19923
20247
  });
19924
20248
  }
19925
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.39",
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
+