Repository: airqo-platform/AirQo-api
Pull request #4661: feat: bulk update for health tips & enforce title/category uniqueness
Author: login: Baalmart
Labels: name: ready for review
Created: 2025-04-09T09:01:15Z | Merged: 2025-04-09T10:27:59Z

## Description

This PR introduces a bulk update endpoint for health tips, enforces uniqueness on the title and AQI category combination, and enhances validation for data integrity.

## Changes Made

- [x] Added a bulk update endpoint for health tips, allowing modification of multiple tips based on AQI category.
- [x] Implemented a unique constraint on the title and aqi_category combination in the tipsSchema to prevent duplicate health tips.
- [x] Enhanced validation logic to prevent duplicate tips within the same bulk update request.
- [x] Updated the model's bulkModify method to handle multiple tips per AQI category and prevent duplicates.
- [x] Modified the validation schema to reflect the new data structure for bulk updates.
- [x] Ensured backward compatibility with existing health tips endpoints.
- [x] Updated the validation schema to ensure that in the provided request body, we do not have any 2 or more tips which share the same title and aqi_category.

## Testing

- [ ] Tested locally
- [ ] Tested against staging environment
- [ ] Relevant tests passed: [List test names]

## Affected Services

- [ ] Which services were modified:
  - [ ] Device Registry

## Endpoints Ready for Testing

- [x] New Endpoint: PUT /tips/bulk - Bulk updates health tips based on AQI category.
- [x] Updated Model: src/device-registry/models/HealthTips.js - Added bulkModify method and unique index.
- [x] Updated Utility: src/device-registry/utils/health-tips.util.js - Added bulkUpdate method.
- [x] Updated Controller: src/device-registry/controllers/health-tips.controller.js - Added bulkUpdate method.
- [x] Updated Route: src/device-registry/routes/v2/tips.routes.js - Added route for bulk update.
- [x] Updated Validation: src/device-registry/validators/tips.validators.js - Added validation for bulk update requests.

     
## API Documentation Updated?

- [ ] Yes, API documentation was updated
- [ ] No, API documentation does not need updating

## Additional Notes


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Introduced bulk update capability for health tips, allowing multiple records to be updated in a single operation.
  - Added a new API endpoint for handling these bulk updates with comprehensive request validation.
  - Enhanced data integrity by enforcing unique constraints on health tip titles in combination with their category.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/src/device-registry/controllers/health-tips.controller.js b/src/device-registry/controllers/health-tips.controller.js
index 5f301989281..b3a7ddbc3eb 100644
--- a/src/device-registry/controllers/health-tips.controller.js
+++ b/src/device-registry/controllers/health-tips.controller.js
@@ -97,6 +97,57 @@ const createHealthTips = {
       return;
     }
   },
+  bulkUpdate: async (req, res, next) => {
+    try {
+      const errors = extractErrorsFromRequest(req);
+      if (errors) {
+        next(
+          new HttpError("bad request errors", httpStatus.BAD_REQUEST, errors)
+        );
+        return;
+      }
+
+      const request = req;
+      const defaultTenant = constants.DEFAULT_TENANT || "airqo";
+      request.query.tenant = isEmpty(req.query.tenant)
+        ? defaultTenant
+        : req.query.tenant;
+
+      const result = await createHealthTipUtil.bulkUpdate(request, next);
+
+      if (isEmpty(result) || res.headersSent) {
+        return;
+      }
+
+      if (result.success === true) {
+        const status = result.status ? result.status : httpStatus.OK;
+        res.status(status).json({
+          success: true,
+          message: result.message,
+          updated_tips: result.data,
+        });
+      } else if (result.success === false) {
+        const status = result.status
+          ? result.status
+          : httpStatus.INTERNAL_SERVER_ERROR;
+        res.status(status).json({
+          success: false,
+          message: result.message,
+          errors: result.errors ? result.errors : { message: "" },
+        });
+      }
+    } catch (error) {
+      logger.error(`🐛🐛 Internal Server Error ${error.message}`);
+      next(
+        new HttpError(
+          "Internal Server Error",
+          httpStatus.INTERNAL_SERVER_ERROR,
+          { message: error.message }
+        )
+      );
+      return;
+    }
+  },
   create: async (req, res, next) => {
     try {
       const errors = extractErrorsFromRequest(req);
diff --git a/src/device-registry/models/HealthTips.js b/src/device-registry/models/HealthTips.js
index 6ca490eb48e..8a5efc74e66 100644
--- a/src/device-registry/models/HealthTips.js
+++ b/src/device-registry/models/HealthTips.js
@@ -44,6 +44,12 @@ const tipsSchema = new Schema(
   },
   {
     timestamps: true,
+    indexes: [
+      {
+        fields: { title: 1, aqi_category: 1 },
+        unique: true,
+      },
+    ],
   }
 );
 
@@ -52,7 +58,7 @@ tipsSchema.pre("save", function(next) {
 });
 
 tipsSchema.plugin(uniqueValidator, {
-  message: `{VALUE} already taken!`,
+  message: `A health tip with the title '{VALUE}' and this aqi_category already exists!`,
 });
 
 tipsSchema.methods = {
@@ -68,6 +74,66 @@ tipsSchema.methods = {
 };
 
 tipsSchema.statics = {
+  async bulkModify(updates, next) {
+    try {
+      logText("bulk updating tips....");
+      let updatedCount = 0;
+      const bulkOps = [];
+
+      for (const update of updates) {
+        const { aqi_category, tips } = update;
+        const { min, max } = aqi_category;
+
+        const filter = {
+          "aqi_category.min": { $lte: max },
+          "aqi_category.max": { $gte: min },
+        };
+
+        // Create update operations for each tip in the array
+        for (const tip of tips) {
+          bulkOps.push({
+            updateOne: {
+              filter,
+              update: { $set: tip }, // Update with the specific tip data
+            },
+          });
+        }
+      }
+
+      if (bulkOps.length > 0) {
+        const result = await this.bulkWrite(bulkOps);
+        updatedCount = result.modifiedCount;
+      }
+
+      return {
+        success: true,
+        message: `Successfully updated ${updatedCount} tips`,
+        data: { updatedCount },
+        status: httpStatus.OK,
+      };
+    } catch (error) {
+      logObject("the error", error);
+      logger.error(`🐛🐛 Internal Server Error -- ${error.message}`);
+      let response = {};
+      let message = "validation errors for some of the provided fields";
+      let status = httpStatus.CONFLICT;
+      if (!isEmpty(error.code) && error.code === 11000) {
+        Object.entries(error.keyPattern).forEach(([key, value]) => {
+          response[key] = "duplicate value";
+          response["message"] = "duplicate value";
+          return response;
+        });
+      } else if (!isEmpty(error.errors)) {
+        Object.entries(error.errors).forEach(([key, value]) => {
+          response[key] = value.message;
+          response["message"] = value.message;
+          return response;
+        });
+      }
+
+      next(new HttpError(message, status, response));
+    }
+  },
   async register(args, next) {
     try {
       logText("registering a new tip....");
diff --git a/src/device-registry/routes/v2/tips.routes.js b/src/device-registry/routes/v2/tips.routes.js
index ee3a35af5d9..cc8be3f92ab 100644
--- a/src/device-registry/routes/v2/tips.routes.js
+++ b/src/device-registry/routes/v2/tips.routes.js
@@ -14,6 +14,11 @@ router.get(
   healthTipController.list
 );
 router.post("/", tipsValidations.createTip, healthTipController.create);
+router.put(
+  "/bulk",
+  tipsValidations.bulkUpdateTips,
+  healthTipController.bulkUpdate
+);
 router.put("/", tipsValidations.updateTip, healthTipController.update);
 router.delete("/", tipsValidations.deleteTip, healthTipController.delete);
 
diff --git a/src/device-registry/utils/health-tips.util.js b/src/device-registry/utils/health-tips.util.js
index 3b5af4a0f63..c9195d17b6f 100644
--- a/src/device-registry/utils/health-tips.util.js
+++ b/src/device-registry/utils/health-tips.util.js
@@ -61,6 +61,28 @@ const createHealthTips = {
       );
     }
   },
+  bulkUpdate: async (request, next) => {
+    try {
+      const { query, body } = request;
+      const { tenant } = query;
+      const updates = body.updates;
+
+      const responseFromBulkModifyHealthTip = await HealthTipModel(
+        tenant
+      ).bulkModify(updates, next);
+
+      return responseFromBulkModifyHealthTip;
+    } catch (error) {
+      logger.error(`🐛🐛 Internal Server Error ${error.message}`);
+      next(
+        new HttpError(
+          "Internal Server Error",
+          httpStatus.INTERNAL_SERVER_ERROR,
+          { message: error.message }
+        )
+      );
+    }
+  },
   delete: async (request, next) => {
     try {
       const { query, body } = request;
diff --git a/src/device-registry/validators/tips.validators.js b/src/device-registry/validators/tips.validators.js
index a400dcb88bd..7cbb174628c 100644
--- a/src/device-registry/validators/tips.validators.js
+++ b/src/device-registry/validators/tips.validators.js
@@ -91,6 +91,129 @@ const commonValidations = {
 };
 
 const healthTipValidations = {
+  bulkUpdate: [
+    ...commonValidations.tenant,
+    body("updates")
+      .exists()
+      .withMessage("the updates array is missing in request")
+      .bail()
+      .isArray()
+      .withMessage("updates must be an array")
+      .bail()
+      .notEmpty()
+      .withMessage("updates array cannot be empty")
+      .custom((updates) => {
+        // Custom validation to check for duplicate title and aqi_category within the updates array
+        const seen = new Set();
+        for (const update of updates) {
+          const key = `${update.title}-${JSON.stringify(update.aqi_category)}`;
+          if (seen.has(key)) {
+            throw new Error(
+              `Duplicate title "${
+                update.title
+              }" and aqi_category "${JSON.stringify(
+                update.aqi_category
+              )}" found within the updates array`
+            );
+          }
+          seen.add(key);
+        }
+        return true;
+      }),
+    body("updates.*.aqi_category")
+      .exists()
+      .withMessage("aqi_category is required for each update")
+      .bail()
+      .isObject()
+      .withMessage("aqi_category must be an object")
+      .bail(),
+    body("updates.*.aqi_category.min")
+      .exists()
+      .withMessage("aqi_category.min is required")
+      .bail()
+      .isNumeric()
+      .withMessage("aqi_category.min must be a number"),
+    body("updates.*.aqi_category.max")
+      .exists()
+      .withMessage("aqi_category.max is required")
+      .bail()
+      .isNumeric()
+      .withMessage("aqi_category.max must be a number")
+      .custom((value, { req }) => {
+        if (
+          value <=
+          req.body.updates[
+            req.body.updates.indexOf(
+              req.body.updates.find(
+                (update) => update.aqi_category.max === value
+              )
+            )
+          ].aqi_category.min
+        ) {
+          throw new Error("max value must be greater than min value");
+        }
+        return true;
+      }),
+    body("updates.*.tips")
+      .exists()
+      .withMessage("tips array is required for each update")
+      .bail()
+      .isArray()
+      .withMessage("tips must be an array")
+      .bail()
+      .notEmpty()
+      .withMessage("tips array cannot be empty")
+      .custom((tips, { req, location, path }) => {
+        // Custom validation to check for duplicate title and aqi_category within the tips array
+        const seen = new Set();
+        for (const tip of tips) {
+          const updateIndex = req.body.updates.indexOf(
+            req.body.updates.find((update) => update.tips === tips)
+          );
+          const key = `${tip.title}-${JSON.stringify(
+            req.body.updates[updateIndex].aqi_category
+          )}`;
+          if (seen.has(key)) {
+            throw new Error(
+              `Duplicate title "${
+                tip.title
+              }" and aqi_category "${JSON.stringify(
+                req.body.updates[updateIndex].aqi_category
+              )}" found within the tips array`
+            );
+          }
+          seen.add(key);
+        }
+        return true;
+      }),
+    body("updates.*.tips.*.title")
+      .exists()
+      .withMessage("title is required for each tip")
+      .bail()
+      .notEmpty()
+      .withMessage("title cannot be empty")
+      .trim(),
+    body("updates.*.tips.*.tag_line")
+      .optional()
+      .notEmpty()
+      .withMessage("tag_line cannot be empty if provided")
+      .trim(),
+    body("updates.*.tips.*.description")
+      .exists()
+      .withMessage("description is required for each tip")
+      .bail()
+      .notEmpty()
+      .withMessage("description cannot be empty")
+      .trim(),
+    body("updates.*.tips.*.image")
+      .exists()
+      .withMessage("image is required for each tip")
+      .bail()
+      .notEmpty()
+      .withMessage("image cannot be empty")
+      .trim(),
+    handleValidationErrors,
+  ],
   create: [
     ...commonValidations.tenant,
     body("description")
@@ -221,6 +344,7 @@ const tipsValidations = {
   updateTip: healthTipValidations.update,
   deleteTip: healthTipValidations.delete,
   pagination: commonValidations.pagination,
+  bulkUpdateTips: healthTipValidations.bulkUpdate,
 };
 
 module.exports = tipsValidations;


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (6)</summary><blockquote>

<details>
<summary>src/device-registry/utils/health-tips.util.js (1)</summary><blockquote>

`64-85`: **Implementation looks good with a minor suggested improvement**

The implementation of the `bulkUpdate` function follows the established pattern in this file. It correctly extracts data from the request and calls the appropriate model method.


Consider adding a validation check before accessing `body.updates` to make the code more robust:

```diff
-      const updates = body.updates;
+      const updates = body.updates || [];
+      if (!updates || !updates.length) {
+        return { success: false, message: "No updates provided" };
+      }
```

</blockquote></details>
<details>
<summary>src/device-registry/validators/tips.validators.js (2)</summary><blockquote>

`123-156`: **Simplify the max value validation logic**

The current implementation for validating that max > min is overly complex and hard to read.


Simplify the validation logic:

```diff
     body("updates.*.aqi_category.max")
       .exists()
       .withMessage("aqi_category.max is required")
       .bail()
       .isNumeric()
       .withMessage("aqi_category.max must be a number")
       .custom((value, { req }) => {
-        if (
-          value <=
-          req.body.updates[
-            req.body.updates.indexOf(
-              req.body.updates.find(
-                (update) => update.aqi_category.max === value
-              )
-            )
-          ].aqi_category.min
-        ) {
+        const path = req.path;
+        const updateIndex = parseInt(path.match(/updates\[(\d+)\]/)[1], 10);
+        if (value <= req.body.updates[updateIndex].aqi_category.min) {
           throw new Error("max value must be greater than min value");
         }
         return true;
       }),
```

---

`157-188`: **Improve duplicate title detection within tips array**

The duplicate detection for tips has complex logic to find the update index, and it's creating a key based on properties that might not exist if earlier validation fails.


Simplify the implementation and make it more robust:

```diff
       .custom((tips, { req, location, path }) => {
         // Custom validation to check for duplicate title and aqi_category within the tips array
+        const updatePath = path.replace('.tips', '');
+        const updateIndex = parseInt(updatePath.match(/updates\[(\d+)\]/)[1], 10);
+        const update = req.body.updates[updateIndex];
+        
         const seen = new Set();
         for (const tip of tips) {
-          const updateIndex = req.body.updates.indexOf(
-            req.body.updates.find((update) => update.tips === tips)
-          );
-          const key = `${tip.title}-${JSON.stringify(
-            req.body.updates[updateIndex].aqi_category
-          )}`;
+          const key = `${tip.title}`;
           if (seen.has(key)) {
             throw new Error(
-              `Duplicate title "${
-                tip.title
-              }" and aqi_category "${JSON.stringify(
-                req.body.updates[updateIndex].aqi_category
-              )}" found within the tips array`
+              `Duplicate title "${tip.title}" found within the tips array for aqi_category ${JSON.stringify(update.aqi_category)}`
             );
           }
           seen.add(key);
         }
         return true;
       }),
```

</blockquote></details>
<details>
<summary>src/device-registry/models/HealthTips.js (3)</summary><blockquote>

`94-99`: **Consider more specific update operations.**

The current implementation uses `$set` for updates, which updates only the specified fields. This is flexible but might lead to incomplete tips if not all fields are specified in the update.


Consider validating that all required fields are present in each tip object before performing the update, or use a more targeted update approach that specifies exactly which fields to update.

---

`114-135`: **Extract common error handling logic.**

The error handling code in this method is similar to other methods in the file (see lines 159-180, 217-242, etc.). This duplication could be reduced by extracting the common logic into a utility function.


Consider creating a shared error handling utility function:

```javascript
// Extract to a utility function at the top of the file or in a separate utility file
function handleModelError(error, next) {
  logObject("the error", error);
  logger.error(`🐛🐛 Internal Server Error -- ${error.message}`);
  let response = {};
  let message = "validation errors for some of the provided fields";
  let status = httpStatus.CONFLICT;
  
  if (!isEmpty(error.code) && error.code === 11000) {
    Object.entries(error.keyPattern).forEach(([key, value]) => {
      response[key] = "duplicate value";
      response["message"] = "duplicate value";
      return response;
    });
  } else if (!isEmpty(error.errors)) {
    Object.entries(error.errors).forEach(([key, value]) => {
      response[key] = value.message;
      response["message"] = value.message;
      return response;
    });
  }

  next(new HttpError(message, status, response));
}

// Then in your method:
catch (error) {
  handleModelError(error, next);
}
```

---

`103-106`: **Consider handling the case when no tips are updated.**

The code correctly handles the case when there are no bulk operations to perform, but it doesn't differentiate between the case where operations were performed but no documents were updated.


Consider providing a more specific message when operations were performed but no documents were updated:

```diff
if (bulkOps.length > 0) {
  const result = await this.bulkWrite(bulkOps);
  updatedCount = result.modifiedCount;
+  if (updatedCount === 0) {
+    return {
+      success: true,
+      message: "No matching tips found to update",
+      data: { updatedCount },
+      status: httpStatus.OK,
+    };
+  }
}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 759f03fd00aa294c5dfe5d3eb1795f74a6727a6c and b45dc9ff9e16f418c7bc293fe11e39f40d8b388c.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `src/device-registry/controllers/health-tips.controller.js` (1 hunks)
* `src/device-registry/models/HealthTips.js` (3 hunks)
* `src/device-registry/routes/v2/tips.routes.js` (1 hunks)
* `src/device-registry/utils/health-tips.util.js` (1 hunks)
* `src/device-registry/validators/tips.validators.js` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (4)</summary>

<details>
<summary>src/device-registry/routes/v2/tips.routes.js (1)</summary><blockquote>

<details>
<summary>src/device-registry/validators/tips.validators.js (1)</summary>

* `tipsValidations` (341-348)

</details>

</blockquote></details>
<details>
<summary>src/device-registry/controllers/health-tips.controller.js (4)</summary><blockquote>

<details>
<summary>src/device-registry/validators/tips.validators.js (3)</summary>

* `httpStatus` (13-13)
* `constants` (8-8)
* `isEmpty` (11-11)

</details>
<details>
<summary>src/device-registry/utils/health-tips.util.js (8)</summary>

* `httpStatus` (1-1)
* `request` (19-19)
* `request` (66-66)
* `request` (88-88)
* `request` (115-115)
* `request` (144-144)
* `constants` (3-3)
* `isEmpty` (9-9)

</details>
<details>
<summary>src/device-registry/models/HealthTips.js (9)</summary>

* `httpStatus` (8-8)
* `defaultTenant` (337-337)
* `constants` (7-7)
* `isEmpty` (6-6)
* `status` (119-119)
* `status` (164-164)
* `status` (222-222)
* `status` (266-266)
* `status` (316-316)

</details>
<details>
<summary>src/device-registry/validators/activities.validators.js (1)</summary>

* `error` (567-569)

</details>

</blockquote></details>
<details>
<summary>src/device-registry/models/HealthTips.js (3)</summary><blockquote>

<details>
<summary>src/device-registry/utils/health-tips.util.js (5)</summary>

* `updates` (68-68)
* `update` (92-92)
* `update` (119-119)
* `httpStatus` (1-1)
* `isEmpty` (9-9)

</details>
<details>
<summary>src/device-registry/controllers/health-tips.controller.js (18)</summary>

* `result` (65-65)
* `result` (116-116)
* `result` (167-167)
* `result` (218-218)
* `result` (269-269)
* `httpStatus` (1-1)
* `status` (35-35)
* `status` (72-72)
* `status` (79-81)
* `status` (123-123)
* `status` (130-132)
* `status` (174-174)
* `status` (181-183)
* `status` (225-225)
* `status` (232-234)
* `status` (276-276)
* `status` (283-285)
* `isEmpty` (15-15)

</details>
<details>
<summary>src/device-registry/validators/tips.validators.js (2)</summary>

* `httpStatus` (13-13)
* `isEmpty` (11-11)

</details>

</blockquote></details>
<details>
<summary>src/device-registry/validators/tips.validators.js (2)</summary><blockquote>

<details>
<summary>src/device-registry/utils/health-tips.util.js (3)</summary>

* `updates` (68-68)
* `update` (92-92)
* `update` (119-119)

</details>
<details>
<summary>src/device-registry/models/HealthTips.js (1)</summary>

* `update` (84-84)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: build-push-deploy-device-registry
* GitHub Check: Analyze (python)
* GitHub Check: Analyze (javascript)

</details>

<details>
<summary>🔇 Additional comments (7)</summary><blockquote>

<details>
<summary>src/device-registry/routes/v2/tips.routes.js (1)</summary>

`17-21`: **Well-structured route implementation**

The new bulk update route is correctly integrated with the existing routes, following the established pattern of using validation middleware before the controller method.

</details>
<details>
<summary>src/device-registry/controllers/health-tips.controller.js (1)</summary>

`100-150`: **Controller method implementation is consistent and complete**

The `bulkUpdate` controller method follows the established pattern in this file, with proper error handling, tenant defaulting, and response formatting. The structure mirrors other methods like `list`, `create`, etc., which is excellent for consistency and maintainability.

</details>
<details>
<summary>src/device-registry/validators/tips.validators.js (2)</summary>

`189-216`: **Validation schema for tip properties looks good**

The validation rules for individual tip properties are well-structured and thorough, ensuring all required fields are present and properly formatted.

---

`347-347`: **Correct export of the bulkUpdateTips validation**

The export of the bulkUpdateTips validation is properly integrated with the existing exports.

</details>
<details>
<summary>src/device-registry/models/HealthTips.js (3)</summary>

`47-52`: **Good addition of a unique index for data integrity.**

This is a well-implemented addition that enforces uniqueness on the combination of `title` and `aqi_category`, preventing duplicate health tips as required in the PR objectives.

---

`61-61`: **Improved error message for better user experience.**

The updated error message now explicitly mentions both the title and AQI category, providing clearer feedback when a duplicate health tip is encountered.

---

`87-90`: **Clarify the logic for AQI category filtering.**

The current implementation filters tips where the AQI ranges overlap, not just for exact matches. While this might be intentional, it could lead to unexpected updates if a tip's range overlaps with multiple update ranges.


Please verify if this overlapping range filter is the intended behavior. If you're looking for exact matches instead, you might want to modify the filter criteria.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T09:04:31Z

Review comments:
diff_hunk: @@ -91,6 +91,129 @@ const commonValidations = {
 };
 
 const healthTipValidations = {
+  bulkUpdate: [
+    ...commonValidations.tenant,
+    body("updates")
+      .exists()
+      .withMessage("the updates array is missing in request")
+      .bail()
+      .isArray()
+      .withMessage("updates must be an array")
+      .bail()
+      .notEmpty()
+      .withMessage("updates array cannot be empty")
+      .custom((updates) => {
+        // Custom validation to check for duplicate title and aqi_category within the updates array
+        const seen = new Set();
+        for (const update of updates) {
+          const key = `${update.title}-${JSON.stringify(update.aqi_category)}`;
+          if (seen.has(key)) {
+            throw new Error(
+              `Duplicate title "${
+                update.title
+              }" and aqi_category "${JSON.stringify(
+                update.aqi_category
+              )}" found within the updates array`
+            );
+          }
+          seen.add(key);
+        }
+        return true;
+      }),
path: src/device-registry/validators/tips.validators.js
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix duplicate detection logic in updates array validation**

The custom validation for duplicate detection has an issue: it's using `update.title` in the key construction, but the schema doesn't validate `updates.*.title` elsewhere—only `updates.*.tips.*.title`. This suggests a misunderstanding of the intended data structure.


Modify the duplicate check to align with the actual data structure:

```diff
-        const seen = new Set();
-        for (const update of updates) {
-          const key = `${update.title}-${JSON.stringify(update.aqi_category)}`;
-          if (seen.has(key)) {
-            throw new Error(
-              `Duplicate title "${
-                update.title
-              }" and aqi_category "${JSON.stringify(
-                update.aqi_category
-              )}" found within the updates array`
-            );
-          }
-          seen.add(key);
-        }
+        // Check for duplicate aqi_category entries
+        const seen = new Set();
+        for (const update of updates) {
+          const categoryKey = JSON.stringify(update.aqi_category);
+          if (seen.has(categoryKey)) {
+            throw new Error(
+              `Duplicate aqi_category "${categoryKey}" found within the updates array`
+            );
+          }
+          seen.add(categoryKey);
+        }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  bulkUpdate: [
    ...commonValidations.tenant,
    body("updates")
      .exists()
      .withMessage("the updates array is missing in request")
      .bail()
      .isArray()
      .withMessage("updates must be an array")
      .bail()
      .notEmpty()
      .withMessage("updates array cannot be empty")
      .custom((updates) => {
        // Check for duplicate aqi_category entries
        const seen = new Set();
        for (const update of updates) {
          const categoryKey = JSON.stringify(update.aqi_category);
          if (seen.has(categoryKey)) {
            throw new Error(
              `Duplicate aqi_category "${categoryKey}" found within the updates array`
            );
          }
          seen.add(categoryKey);
        }
        return true;
      }),
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-09T09:04:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,6 +74,66 @@ tipsSchema.methods = {
 };
 
 tipsSchema.statics = {
+  async bulkModify(updates, next) {
+    try {
+      logText("bulk updating tips....");
+      let updatedCount = 0;
+      const bulkOps = [];
+
+      for (const update of updates) {
+        const { aqi_category, tips } = update;
+        const { min, max } = aqi_category;
+
+        const filter = {
+          "aqi_category.min": { $lte: max },
+          "aqi_category.max": { $gte: min },
+        };
+
+        // Create update operations for each tip in the array
+        for (const tip of tips) {
+          bulkOps.push({
+            updateOne: {
+              filter,
+              update: { $set: tip }, // Update with the specific tip data
+            },
+          });
+        }
+      }
+
+      if (bulkOps.length > 0) {
+        const result = await this.bulkWrite(bulkOps);
+        updatedCount = result.modifiedCount;
+      }
+
+      return {
+        success: true,
+        message: `Successfully updated ${updatedCount} tips`,
+        data: { updatedCount },
+        status: httpStatus.OK,
+      };
+    } catch (error) {
+      logObject("the error", error);
+      logger.error(`🐛🐛 Internal Server Error -- ${error.message}`);
+      let response = {};
+      let message = "validation errors for some of the provided fields";
+      let status = httpStatus.CONFLICT;
+      if (!isEmpty(error.code) && error.code === 11000) {
+        Object.entries(error.keyPattern).forEach(([key, value]) => {
+          response[key] = "duplicate value";
+          response["message"] = "duplicate value";
+          return response;
+        });
+      } else if (!isEmpty(error.errors)) {
+        Object.entries(error.errors).forEach(([key, value]) => {
+          response[key] = value.message;
+          response["message"] = value.message;
+          return response;
+        });
+      }
+
+      next(new HttpError(message, status, response));
+    }
+  },
path: src/device-registry/models/HealthTips.js
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Consider adding input validation to the bulkModify method.**

The implementation of the `bulkModify` method is well-structured with proper error handling and efficiently uses MongoDB's bulkWrite operation. However, there's no validation on the input data structure at the beginning of the method.


Consider adding validation to ensure `updates` is a non-null array and that each object has the expected properties before processing:

```diff
async bulkModify(updates, next) {
  try {
    logText("bulk updating tips....");
+   if (!updates || !Array.isArray(updates) || updates.length === 0) {
+     return {
+       success: false,
+       message: "Invalid or empty updates array",
+       status: httpStatus.BAD_REQUEST,
+     };
+   }
    let updatedCount = 0;
    const bulkOps = [];
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  async bulkModify(updates, next) {
    try {
      logText("bulk updating tips....");
      if (!updates || !Array.isArray(updates) || updates.length === 0) {
        return {
          success: false,
          message: "Invalid or empty updates array",
          status: httpStatus.BAD_REQUEST,
        };
      }
      let updatedCount = 0;
      const bulkOps = [];

      for (const update of updates) {
        const { aqi_category, tips } = update;
        const { min, max } = aqi_category;

        const filter = {
          "aqi_category.min": { $lte: max },
          "aqi_category.max": { $gte: min },
        };

        // Create update operations for each tip in the array
        for (const tip of tips) {
          bulkOps.push({
            updateOne: {
              filter,
              update: { $set: tip }, // Update with the specific tip data
            },
          });
        }
      }

      if (bulkOps.length > 0) {
        const result = await this.bulkWrite(bulkOps);
        updatedCount = result.modifiedCount;
      }

      return {
        success: true,
        message: `Successfully updated ${updatedCount} tips`,
        data: { updatedCount },
        status: httpStatus.OK,
      };
    } catch (error) {
      logObject("the error", error);
      logger.error(`🐛🐛 Internal Server Error -- ${error.message}`);
      let response = {};
      let message = "validation errors for some of the provided fields";
      let status = httpStatus.CONFLICT;
      if (!isEmpty(error.code) && error.code === 11000) {
        Object.entries(error.keyPattern).forEach(([key, value]) => {
          response[key] = "duplicate value";
          response["message"] = "duplicate value";
          return response;
        });
      } else if (!isEmpty(error.errors)) {
        Object.entries(error.errors).forEach(([key, value]) => {
          response[key] = value.message;
          response["message"] = value.message;
          return response;
        });
      }

      next(new HttpError(message, status, response));
    }
  },
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-09T09:04:31Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-09T09:01:21Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

<details>
<summary>📝 Walkthrough</summary>

## Walkthrough
This pull request implements bulk update functionality for health tips. A new method in the controller processes bulk update requests by extracting errors, applying tenant defaults, and invoking a utility function. The utility method in turn calls a new model method (`bulkModify`) that performs the updates, applying a unique index on specific fields. Additionally, a new validation schema is introduced to ensure correct data formatting for the bulk update endpoint, which is accessible via a new PUT route.

## Changes

| File(s) | Change Summary |
| ------- | -------------- |
| `src/.../health-tips.controller.js`, `src/.../tips.routes.js`, `src/.../health-tips.util.js` | Added bulk update functionality: a new controller method `bulkUpdate`, a corresponding PUT `/bulk` endpoint, and a utility function that invokes model updates. |
| `src/.../HealthTips.js` | Introduced a new static method `bulkModify` for bulk updating, added a unique index on `title` and `aqi_category`, and refined the error message for uniqueness validation. |
| `src/.../tips.validators.js` | Added new validation schema (`bulkUpdateTips`) for bulk update operations, ensuring proper structure and value constraints for incoming update data. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Client
    participant Router
    participant Validator
    participant Controller
    participant Util
    participant Model

    Client->>Router: PUT /bulk
    Router->>Validator: Validate request body
    Validator-->>Router: Validated data
    Router->>Controller: Call bulkUpdate
    Controller->>Util: Invoke bulkUpdate(request)
    Util->>Model: Execute bulkModify(updates)
    Model-->>Util: Return update results/error
    Util-->>Controller: Return bulk update response
    Controller-->>Client: Send success/error response
```

## Possibly related PRs
- **airqo-platform/AirQo-api#4047:** Implements a similar bulk update functionality for devices using a `bulkModify` approach.
- **airqo-platform/AirQo-api#4108:** Directly interacts with the `bulkModify` method through modifications in the bulk update process.
- **airqo-platform/AirQo-api#4384:** Introduces validation for bulk update operations, aligning with the new `bulkUpdateTips` validation.

## Suggested labels
`ready for review`

## Suggested reviewers
- Codebmk
- Mozart299
- 2phonebabykeem

## Poem
> In code we weave our daily art,  
> Health tips updated, a fresh new start.  
> Bulk flows merge with logic so fine,  
> Indexes and validations align.  
> Cheers to progress—our code does shine!

</details>

<!-- walkthrough_end -->

<!-- This is an auto-generated comment: resource warnings by coderabbit.ai -->

> [!WARNING]
> ## Review ran into problems
> 
> <details>
> <summary>🔥 Problems</summary>
> 
> Errors were encountered while retrieving linked issues.
> 
> <details>
> <summary>Errors (1)</summary>
> 
> * JIRA integration encountered authorization issues. Please disconnect and reconnect the integration in the CodeRabbit UI.
> 
> </details>
> 
> </details>

<!-- end of auto-generated comment: resource warnings by coderabbit.ai -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=airqo-platform/AirQo-api&utm_content=4661):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-04-09T09:02:19Z
body: ## [Codecov](https://app.codecov.io/gh/airqo-platform/AirQo-api/pull/4661?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airqo-platform) Report
Attention: Patch coverage is `1.26582%` with `78 lines` in your changes missing coverage. Please review.
> Project coverage is 11.40%. Comparing base [(`759f03f`)](https://app.codecov.io/gh/airqo-platform/AirQo-api/commit/759f03fd00aa294c5dfe5d3eb1795f74a6727a6c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airqo-platform) to head [(`b45dc9f`)](https://app.codecov.io/gh/airqo-platform/AirQo-api/commit/b45dc9ff9e16f418c7bc293fe11e39f40d8b388c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airqo-platform).

| [Files with missing lines](https://app.codecov.io/gh/airqo-platform/AirQo-api/pull/4661?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airqo-platform) | Patch % | Lines |
|---|---|---|
| [src/device-registry/models/HealthTips.js](https://app.codecov.io/gh/airqo-platform/AirQo-api/pull/4661?src=pr&el=tree&filepath=src%2Fdevice-registry%2Fmodels%2FHealthTips.js&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airqo-platform#diff-c3JjL2RldmljZS1yZWdpc3RyeS9tb2RlbHMvSGVhbHRoVGlwcy5qcw==) | 0.00% | [30 Missing :warning: ](https://app.codecov.io/gh/airqo-platform/AirQo-api/pull/4661?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airqo-platform) |
| [...ice-registry/controllers/health-tips.controller.js](https://app.codecov.io/gh/airqo-platform/AirQo-api/pull/4661?src=pr&el=tree&filepath=src%2Fdevice-registry%2Fcontrollers%2Fhealth-tips.controller.js&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airqo-platform#diff-c3JjL2RldmljZS1yZWdpc3RyeS9jb250cm9sbGVycy9oZWFsdGgtdGlwcy5jb250cm9sbGVyLmpz) | 0.00% | [20 Missing :warning: ](https://app.codecov.io/gh/airqo-platform/AirQo-api/pull/4661?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airqo-platform) |
| [src/device-registry/validators/tips.validators.js](https://app.codecov.io/gh/airqo-platform/AirQo-api/pull/4661?src=pr&el=tree&filepath=src%2Fdevice-registry%2Fvalidators%2Ftips.validators.js&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airqo-platform#diff-c3JjL2RldmljZS1yZWdpc3RyeS92YWxpZGF0b3JzL3RpcHMudmFsaWRhdG9ycy5qcw==) | 0.00% | [20 Missing :warning: ](https://app.codecov.io/gh/airqo-platform/AirQo-api/pull/4661?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airqo-platform) |
| [src/device-registry/utils/health-tips.util.js](https://app.codecov.io/gh/airqo-platform/AirQo-api/pull/4661?src=pr&el=tree&filepath=src%2Fdevice-registry%2Futils%2Fhealth-tips.util.js&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airqo-platform#diff-c3JjL2RldmljZS1yZWdpc3RyeS91dGlscy9oZWFsdGgtdGlwcy51dGlsLmpz) | 0.00% | [8 Missing :warning: ](https://app.codecov.io/gh/airqo-platform/AirQo-api/pull/4661?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airqo-platform) |

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://app.codecov.io/gh/airqo-platform/AirQo-api/pull/4661/graphs/tree.svg?width=650&height=150&src=pr&token=HHq3qS3cL6&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airqo-platform)](https://app.codecov.io/gh/airqo-platform/AirQo-api/pull/4661?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airqo-platform)

```diff
@@             Coverage Diff             @@
##           staging    #4661      +/-   ##
===========================================
- Coverage    11.44%   11.40%   -0.05%     
===========================================
  Files          155      155              
  Lines        18864    18943      +79     
  Branches       549      549              
===========================================
+ Hits          2159     2160       +1     
- Misses       16701    16779      +78     
  Partials         4        4              
```

| [Files with missing lines](https://app.codecov.io/gh/airqo-platform/AirQo-api/pull/4661?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airqo-platform) | Coverage Δ | |
|---|---|---|
| [src/device-registry/routes/v2/tips.routes.js](https://app.codecov.io/gh/airqo-platform/AirQo-api/pull/4661?src=pr&el=tree&filepath=src%2Fdevice-registry%2Froutes%2Fv2%2Ftips.routes.js&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airqo-platform#diff-c3JjL2RldmljZS1yZWdpc3RyeS9yb3V0ZXMvdjIvdGlwcy5yb3V0ZXMuanM=) | `100.00% <100.00%> (ø)` | |
| [src/device-registry/utils/health-tips.util.js](https://app.codecov.io/gh/airqo-platform/AirQo-api/pull/4661?src=pr&el=tree&filepath=src%2Fdevice-registry%2Futils%2Fhealth-tips.util.js&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airqo-platform#diff-c3JjL2RldmljZS1yZWdpc3RyeS91dGlscy9oZWFsdGgtdGlwcy51dGlsLmpz) | `15.78% <0.00%> (-1.86%)` | :arrow_down: |
| [...ice-registry/controllers/health-tips.controller.js](https://app.codecov.io/gh/airqo-platform/AirQo-api/pull/4661?src=pr&el=tree&filepath=src%2Fdevice-registry%2Fcontrollers%2Fhealth-tips.controller.js&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airqo-platform#diff-c3JjL2RldmljZS1yZWdpc3RyeS9jb250cm9sbGVycy9oZWFsdGgtdGlwcy5jb250cm9sbGVyLmpz) | `7.56% <0.00%> (-1.53%)` | :arrow_down: |
| [src/device-registry/validators/tips.validators.js](https://app.codecov.io/gh/airqo-platform/AirQo-api/pull/4661?src=pr&el=tree&filepath=src%2Fdevice-registry%2Fvalidators%2Ftips.validators.js&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airqo-platform#diff-c3JjL2RldmljZS1yZWdpc3RyeS92YWxpZGF0b3JzL3RpcHMudmFsaWRhdG9ycy5qcw==) | `21.05% <0.00%> (-11.38%)` | :arrow_down: |
| [src/device-registry/models/HealthTips.js](https://app.codecov.io/gh/airqo-platform/AirQo-api/pull/4661?src=pr&el=tree&filepath=src%2Fdevice-registry%2Fmodels%2FHealthTips.js&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=airqo-platform#diff-c3JjL2RldmljZS1yZWdpc3RyeS9tb2RlbHMvSGVhbHRoVGlwcy5qcw==) | `12.58% <0.00%> (-3.12%)` | :arrow_down: |

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-09T09:03:41Z
body: Device registry changes in this PR available for preview [here](https://health-tips-bulk-update-and-uniqueness-device-reg-w7kzhvlewq-ew.a.run.app)
author_association: CONTRIBUTOR