Repository: Selco-Foundation/E4H_Digital_Platform
Pull request #652: added facility ingestion function
Author: login: Nickg-30
Created: 2025-05-12T06:34:33Z | Merged: 2025-05-12T07:12:17Z



Diff:
diff --git a/backend/municipal-services/ingestion-service/.env b/backend/municipal-services/ingestion-service/.env
index 3bfdfa204..b30b6c0d5 100644
--- a/backend/municipal-services/ingestion-service/.env
+++ b/backend/municipal-services/ingestion-service/.env
@@ -2,4 +2,5 @@ VENDOR_SERVICE_URL=http://localhost:8071
 MDMS_URL=http://localhost:8090
 BOUNDARY_SERVICE_URL=http://localhost:8091
 TIME_OUT=60
-PROJECT_SERVICE_URL=http://localhost:8092
\ No newline at end of file
+PROJECT_SERVICE_URL=http://localhost:8092
+FACILITY_SERVICE_URL=http://localhost:8081
\ No newline at end of file
diff --git a/backend/municipal-services/ingestion-service/app/api/endpoints/file_ingestion.py b/backend/municipal-services/ingestion-service/app/api/endpoints/file_ingestion.py
index bd0fc684c..cbc4e0194 100644
--- a/backend/municipal-services/ingestion-service/app/api/endpoints/file_ingestion.py
+++ b/backend/municipal-services/ingestion-service/app/api/endpoints/file_ingestion.py
@@ -12,7 +12,8 @@
 from app.processor.factory.boundary_data_processor_factory import BoundaryDataProcessorFactory
 from app.processor.factory.vendor_data_processor_factory import VendorDataProcessorFactory
 from app.utils.convertor import request_info_from_json, create_vendor_request, \
-    create_facility_with_supervisor_update_payload
+    create_facility_with_supervisor_update_payload, create_facility_payload
+from app.utils.facility_service_client import FacilityServiceClient
 from app.utils.organization_service_client import OrganizationServiceClient
 from app.utils.project_service_client import ProjectServiceClient
 
@@ -24,8 +25,8 @@
 load_dotenv()
 mdms_url = os.getenv("MDMS_URL")
 org_service_url = os.getenv("VENDOR_SERVICE_URL")
-project_service_url= os.getenv("PROJECT_SERVICE_URL")
-
+project_service_url = os.getenv("PROJECT_SERVICE_URL")
+facility_service_url = os.getenv("FACILITY_SERVICE_URL")
 
 @router.post('/vendors',
              summary='Upload and process vendor Excel file with multiple sheets',
@@ -164,6 +165,81 @@ async def upload_boundaries_excel_sheet(
         if input_temp_file and os.path.exists(input_temp_file.name):
             os.unlink(input_temp_file.name)
 
+@router.post('/facilities',
+             summary='Upload and process facility Excel file',
+             response_description='Returns processed Excel file with validations results')
+async def upload_facilities_excel_sheet(
+        facility_file: UploadFile = File(description="Excel file containing facility data"),
+        facility_sheet_name: str = Form(default="FacilityIngestionTemplate",
+                                        description="Name of the sheet containing facility data"),
+        request_info: str = Form(default="")
+):
+    input_temp_file = None
+    output_temp_file = None
+    request_info = request_info_from_json(request_info)
+    get_authorized_request_info(request_info)
+
+    try:
+        input_temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx")
+        content = await facility_file.read()
+        input_temp_file.write(content)
+        input_temp_file.close()
+        facility_file_path = input_temp_file.name
+
+        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+        output_filename = f"facility_ingestion_results_{timestamp}.xlsx"
+        output_temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx")
+        output_temp_file.close()
+        output_file_path = output_temp_file.name
+
+        with open(facility_file_path, 'rb') as src, open(output_file_path, 'wb') as dst:
+            dst.write(src.read())
+
+        df = pd.read_excel(facility_file_path, sheet_name=facility_sheet_name)
+
+        if 'status' not in df.columns:
+            df['status'] = ''
+        if 'error' not in df.columns:
+            df['error'] = ''
+
+        if facility_service_url and not df.empty:
+            facility_client = FacilityServiceClient(facility_service_url)
+            for index, row in df[df['status'] != 'success'].iterrows():
+                try:
+                    facility_data_payload = create_facility_payload(request_info, row)
+                    response = facility_client.create_facility(facility_data_payload)
+                    if response.status_code in (200, 201):
+                        df.at[index, 'status'] = 'success'
+                        df.at[index, 'error'] = ''
+                    elif response.status_code == 400:
+                        error_data = response.json()
+                        error_message = error_data.get('Errors', [{}])[0].get('message', 'Unknown error')
+                        df.at[index, 'status'] = 'failed'
+                        df.at[index, 'error'] = error_message
+                    else:
+                        df.at[index, 'status'] = 'failed'
+                        df.at[index, 'error'] = f'{response.status_code}: {response.text}'
+                except Exception as e:
+                    df.at[index, 'status'] = 'failed'
+                    df.at[index, 'error'] = f'Exception: {str(e)}'
+
+        writer = ExcelDataWriter(output_file_path, output_sheet=facility_sheet_name)
+        writer.write_data(df)
+
+        return FileResponse(
+            path=output_file_path,
+            filename=output_filename,
+            media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+        )
+    except Exception as e:
+        logger.error(f"Error processing facility data: {e}")
+        raise HTTPException(
+            status_code=500,
+            detail=f"Failed to process facility data: {str(e)}"
+        )
+    finally:
+        if input_temp_file and os.path.exists(input_temp_file.name):
+            os.unlink(input_temp_file.name)
 
 @router.post('/facilityWithSupervisors',
              summary='Upload and process facility with supervisors Excel file',
@@ -234,4 +310,4 @@ async def upload_boundaries_excel_sheet(
         )
     finally:
         if input_temp_file and os.path.exists(input_temp_file.name):
-            os.unlink(input_temp_file.name)
\ No newline at end of file
+            os.unlink(input_temp_file.name)
diff --git a/backend/municipal-services/ingestion-service/app/utils/convertor.py b/backend/municipal-services/ingestion-service/app/utils/convertor.py
index d70dd4d71..fa2151370 100644
--- a/backend/municipal-services/ingestion-service/app/utils/convertor.py
+++ b/backend/municipal-services/ingestion-service/app/utils/convertor.py
@@ -1,6 +1,7 @@
 import json
 from typing import Dict, Any, Optional, List
 
+import pandas as pd
 from pandas import Series
 from pydantic import ValidationError
 
@@ -211,3 +212,40 @@ def create_facility_with_supervisor_update_payload(request_info: RequestInfo, ro
             'email': row.get('Email Address (Mandatory)', '')
         }
     }
+
+def safe_get(row, key, default=None):
+    val = row.get(key, default)
+    return default if pd.isna(val) else val
+
+def create_facility_payload(request_info: RequestInfo, row: Series):
+    return {
+        'RequestInfo': request_info.model_dump(by_alias=True, exclude_none=True),
+        'facility': {
+            'tenant_id': 'in',
+            'facility_name': safe_get(row, 'Health Centre Name (Mandatory)'),
+            'facility_type': safe_get(row, 'Type of HC (Mandatory)'),
+            'facility_category': safe_get(row, 'Category', 'HEALTH'),
+            'facility_ownership': safe_get(row, 'Ownership', 'GOVERNMENT'),
+            'facility_region': safe_get(row, 'Region', 'RURAL'),
+            'isActive': True,
+            'address': {
+                'tenantId': 'in',
+                'latitude': safe_get(row, 'Latitude'),
+                'longitude': safe_get(row, 'Longitude'),
+                'addressLine1': safe_get(row, 'Address'),
+                'state': safe_get(row, 'State'),
+                'district': safe_get(row, 'District'),
+                'block': safe_get(row, 'Block')
+            },
+            'facility_details': {
+                'boundaryCode': safe_get(row, 'Boundary Code (Mandatory)'),
+                'vendorCode': safe_get(row, 'Vendor Code (Mandatory)'),
+                'solutionDesignType': safe_get(row, 'Solution Design Type (Mandatory)'),
+                'pocName': safe_get(row, 'HC PoC Name (Mandatory)'),
+                'pocDesignation': safe_get(row, 'HC PoC Designation'),
+                'pocContact': safe_get(row, 'HC PoC Contact number (Mandatory)'),
+                'hfrId': safe_get(row, 'HFR ID'),
+                'ninId': safe_get(row, 'NIN ID')
+            }
+        }
+    }
diff --git a/backend/municipal-services/ingestion-service/app/utils/facility_service_client.py b/backend/municipal-services/ingestion-service/app/utils/facility_service_client.py
new file mode 100644
index 000000000..abbadf4a0
--- /dev/null
+++ b/backend/municipal-services/ingestion-service/app/utils/facility_service_client.py
@@ -0,0 +1,31 @@
+from typing import Dict, Any
+
+import requests
+
+
+class FacilityServiceClient:
+    def __init__(self, facility_service_url: str):
+        self.facility_service_url = facility_service_url
+
+    def create_facility(self, facility_payload:Dict[str,Any]):
+        url = f"{self.facility_service_url}/facility-service/v2/facility/create"
+        headers = {
+            "Content-Type": "application/json"
+        }
+        payload = facility_payload
+        try:
+            response = requests.post(url, headers=headers, json=payload)
+            return response
+
+        except requests.exceptions.HTTPError as http_err:
+            print(f"HTTP error occurred: {http_err}")
+            raise http_err
+        except requests.exceptions.ConnectionError as conn_err:
+            print(f"Connection error occurred: {conn_err}")
+            raise conn_err
+        except requests.exceptions.Timeout as timeout_err:
+            print(f"Timeout error occurred: {timeout_err}")
+            raise timeout_err
+        except requests.exceptions.RequestException as req_err:
+            print(f"An error occurred: {req_err}")
+            raise req_err
\ No newline at end of file


Reviews:
user: login: vineet875713
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T06:35:18Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

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

<details>
<summary>backend/municipal-services/ingestion-service/app/utils/facility_service_client.py (3)</summary><blockquote>

`6-9`: **Class structure looks good but missing documentation.**

The `FacilityServiceClient` class is properly initialized with the facility service URL from the environment.


Consider adding a docstring to describe the purpose and usage of this class:

```diff
 class FacilityServiceClient:
+    """Client for interacting with the Facility Service API.
+    
+    This class provides methods to create and manage facility data through 
+    the external Facility Service.
+    """
     def __init__(self, facility_service_url: str):
         self.facility_service_url = facility_service_url
```

---

`10-19`: **Method implementation is correct but could use documentation and async support.**

The method correctly constructs the URL, sets headers, and sends the payload, but lacks documentation and could benefit from async implementation for better performance in a FastAPI application.

Consider adding a docstring and converting to an async method:

```diff
-    def create_facility(self, facility_payload:Dict[str,Any]):
+    async def create_facility(self, facility_payload: Dict[str, Any]):
+        """Create a new facility in the facility service.
+        
+        Args:
+            facility_payload: Dictionary containing the facility data
+            
+        Returns:
+            Response object from the facility service API
+            
+        Raises:
+            Various request exceptions if the API call fails
+        """
         url = f"{self.facility_service_url}/facility-service/v2/facility/create"
         headers = {
             "Content-Type": "application/json"
         }
         payload = facility_payload
         try:
-            response = requests.post(url, headers=headers, json=payload)
+            async with aiohttp.ClientSession() as session:
+                async with session.post(url, headers=headers, json=payload) as response:
+                    return response
             return response
```

Note: This would require adding `import aiohttp` at the top of the file and updating call sites to use `await`.

---

`20-31`: **Error handling could be improved with more context.**

The error handling catches and re-raises exceptions properly but only prints the error message without adding context about the operation that failed.

Enhance error handling to provide more context:

```diff
         try:
             response = requests.post(url, headers=headers, json=payload)
             return response

         except requests.exceptions.HTTPError as http_err:
-            print(f"HTTP error occurred: {http_err}")
-            raise http_err
+            print(f"HTTP error occurred while creating facility: {http_err}")
+            raise http_err from http_err
         except requests.exceptions.ConnectionError as conn_err:
-            print(f"Connection error occurred: {conn_err}")
-            raise conn_err
+            print(f"Connection error occurred while creating facility: {conn_err}")
+            raise conn_err from conn_err
         except requests.exceptions.Timeout as timeout_err:
-            print(f"Timeout error occurred: {timeout_err}")
-            raise timeout_err
+            print(f"Timeout error occurred while creating facility: {timeout_err}")
+            raise timeout_err from timeout_err
         except requests.exceptions.RequestException as req_err:
-            print(f"An error occurred: {req_err}")
-            raise req_err
+            print(f"Request error occurred while creating facility: {req_err}")
+            raise req_err from req_err
```

</blockquote></details>
<details>
<summary>backend/municipal-services/ingestion-service/app/utils/convertor.py (2)</summary><blockquote>

`216-218`: **Good utility function but missing documentation.**

The `safe_get` function is a useful helper that handles missing or NaN values, but it lacks documentation and type hints.

Add docstring and type hints:

```diff
-def safe_get(row, key, default=None):
+def safe_get(row: pd.Series, key: str, default=None) -> Any:
+    """Safely get a value from a pandas Series, handling NaN values.
+    
+    Args:
+        row: The pandas Series to extract value from
+        key: The key to look up in the Series
+        default: The default value to return if key is missing or value is NaN
+        
+    Returns:
+        The value from the Series if it exists and is not NaN, otherwise the default
+    """
     val = row.get(key, default)
     return default if pd.isna(val) else val
```

---

`220-251`: **Facility payload constructor well implemented but contains hardcoded values.**

The `create_facility_payload` function correctly builds the facility payload structure, using the `safe_get` utility to handle missing values. However, it contains hardcoded values that could be constants or configurable.

Consider extracting hardcoded values as constants:

```diff
+# Define constants for default values
+DEFAULT_TENANT_ID = 'in'
+DEFAULT_FACILITY_CATEGORY = 'HEALTH'
+DEFAULT_FACILITY_OWNERSHIP = 'GOVERNMENT'
+DEFAULT_FACILITY_REGION = 'RURAL'
+
 def create_facility_payload(request_info: RequestInfo, row: Series):
+    """Create a payload for the facility service create API.
+    
+    Args:
+        request_info: The RequestInfo object containing authentication details
+        row: A pandas Series row containing facility data from Excel
+        
+    Returns:
+        Dict containing the structured payload for facility creation
+    """
     return {
         'RequestInfo': request_info.model_dump(by_alias=True, exclude_none=True),
         'facility': {
-            'tenant_id': 'in',
+            'tenant_id': DEFAULT_TENANT_ID,
             'facility_name': safe_get(row, 'Health Centre Name (Mandatory)'),
             'facility_type': safe_get(row, 'Type of HC (Mandatory)'),
-            'facility_category': safe_get(row, 'Category', 'HEALTH'),
-            'facility_ownership': safe_get(row, 'Ownership', 'GOVERNMENT'),
-            'facility_region': safe_get(row, 'Region', 'RURAL'),
+            'facility_category': safe_get(row, 'Category', DEFAULT_FACILITY_CATEGORY),
+            'facility_ownership': safe_get(row, 'Ownership', DEFAULT_FACILITY_OWNERSHIP),
+            'facility_region': safe_get(row, 'Region', DEFAULT_FACILITY_REGION),
             'isActive': True,
             'address': {
-                'tenantId': 'in',
+                'tenantId': DEFAULT_TENANT_ID,
                 'latitude': safe_get(row, 'Latitude'),
                 'longitude': safe_get(row, 'Longitude'),
                 'addressLine1': safe_get(row, 'Address'),
                 'state': safe_get(row, 'State'),
                 'district': safe_get(row, 'District'),
                 'block': safe_get(row, 'Block')
             },
```

</blockquote></details>
<details>
<summary>backend/municipal-services/ingestion-service/app/api/endpoints/file_ingestion.py (4)</summary><blockquote>

`168-176`: **Endpoint declaration looks good but has a static analysis issue.**

The endpoint declaration is clear and correctly specifies the expected parameters, but there's a static analysis issue with using a function call in argument defaults.


Fix the static analysis issue B008 (function call in argument defaults):

```diff
 async def upload_facilities_excel_sheet(
-        facility_file: UploadFile = File(description="Excel file containing facility data"),
+        facility_file: UploadFile = File(..., description="Excel file containing facility data"),
         facility_sheet_name: str = Form(default="FacilityIngestionTemplate",
                                        description="Name of the sheet containing facility data"),
         request_info: str = Form(default="")
 ):
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

172-172: Do not perform function call `File` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

</details>

</details>

---

`182-197`: **File handling should use context managers.**

The file handling code works correctly but doesn't follow best practices for file operations. Static analysis tools flagged this issue (SIM115).

Use context managers for file operations:

```diff
     try:
-        input_temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx")
-        content = await facility_file.read()
-        input_temp_file.write(content)
-        input_temp_file.close()
-        facility_file_path = input_temp_file.name
+        with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as input_temp_file:
+            content = await facility_file.read()
+            input_temp_file.write(content)
+            facility_file_path = input_temp_file.name

         timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
         output_filename = f"facility_ingestion_results_{timestamp}.xlsx"
-        output_temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx")
-        output_temp_file.close()
-        output_file_path = output_temp_file.name
+        with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as output_temp_file:
+            output_file_path = output_temp_file.name
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

183-183: Use a context manager for opening files

(SIM115)

---

191-191: Use a context manager for opening files

(SIM115)

</details>

</details>

---

`234-239`: **Error handling should use 'raise from' syntax.**

The error handling code works but doesn't follow best practices for exception chaining. Static analysis flagged this issue (B904).

Use 'raise from' syntax for better exception chaining:

```diff
     except Exception as e:
         logger.error(f"Error processing facility data: {e}")
         raise HTTPException(
             status_code=500,
             detail=f"Failed to process facility data: {str(e)}"
-        )
+        ) from e
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

236-239: Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling

(B904)

</details>

</details>

---

`168-243`: **Consider factoring out common patterns into helper functions.**

There's significant duplication in the file handling and Excel processing logic across multiple endpoints. This could be refactored into helper functions.

Create helper functions for common operations:

```python
def prepare_excel_files(input_file: UploadFile, filename_prefix: str):
    """Create temporary input and output files for Excel processing."""
    with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as input_temp_file:
        content = input_file.read()
        input_temp_file.write(content)
        input_file_path = input_temp_file.name

    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    output_filename = f"{filename_prefix}_{timestamp}.xlsx"
    
    with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as output_temp_file:
        output_file_path = output_temp_file.name
        
    # Copy input to output
    with open(input_file_path, 'rb') as src, open(output_file_path, 'wb') as dst:
        dst.write(src.read())
        
    return input_file_path, output_file_path, output_filename

def prepare_response_file(output_file_path: str, output_filename: str):
    """Prepare a FileResponse object for the Excel file."""
    return FileResponse(
        path=output_file_path,
        filename=output_filename,
        media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
```

Then use these helpers in your endpoint:

```python
@router.post('/facilities', ...)
async def upload_facilities_excel_sheet(...):
    input_file_path = None
    
    try:
        input_file_path, output_file_path, output_filename = await prepare_excel_files(
            facility_file, "facility_ingestion_results")
            
        # Process the file...
        
        return prepare_response_file(output_file_path, output_filename)
    
    except Exception as e:
        # Handle errors...
        
    finally:
        if input_file_path and os.path.exists(input_file_path):
            os.unlink(input_file_path)
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

172-172: Do not perform function call `File` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

---

183-183: Use a context manager for opening files

(SIM115)

---

191-191: Use a context manager for opening files

(SIM115)

---

236-239: Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling

(B904)

</details>

</details>

</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 f1c375067623c9a37fb252ac511610d12cc0affd and 57c00445eaa0b00032666c5697ec5adce19775e4.

</details>

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

* `backend/municipal-services/ingestion-service/.env` (1 hunks)
* `backend/municipal-services/ingestion-service/app/api/endpoints/file_ingestion.py` (4 hunks)
* `backend/municipal-services/ingestion-service/app/utils/convertor.py` (2 hunks)
* `backend/municipal-services/ingestion-service/app/utils/facility_service_client.py` (1 hunks)

</details>

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

<details>
<summary>🪛 Ruff (0.8.2)</summary>

<details>
<summary>backend/municipal-services/ingestion-service/app/api/endpoints/file_ingestion.py</summary>

172-172: Do not perform function call `File` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

---

183-183: Use a context manager for opening files

(SIM115)

---

191-191: Use a context manager for opening files

(SIM115)

---

236-239: Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling

(B904)

</details>

</details>

</details>

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

<details>
<summary>backend/municipal-services/ingestion-service/.env (1)</summary>

`5-6`: **LGTM: Environment variables correctly added for facility service integration.**

The new environment variables `PROJECT_SERVICE_URL` and `FACILITY_SERVICE_URL` follow the existing naming convention and pattern. These local development URLs will need to be updated for production deployment.

</details>
<details>
<summary>backend/municipal-services/ingestion-service/app/api/endpoints/file_ingestion.py (3)</summary>

`15-16`: **Imports and environment variable loading look good.**

The new imports and environment variable loading for facility-related functionality follow the existing pattern in the file.



Also applies to: 28-29

---

`198-204`: **Excel file validation is implemented properly.**

The code correctly reads the Excel file and ensures the presence of 'status' and 'error' columns, which are essential for tracking processing results.

---

`205-225`: **Facility data processing logic looks good.**

The facility data processing loop correctly handles each row, creates the payload, sends it to the facility service, and updates the status based on the response. Error handling is comprehensive.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T06:37:32Z
user: login: Nickg-30
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T07:09:49Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T07:11:25Z

Review comments:
diff_hunk: @@ -164,6 +165,81 @@ async def upload_boundaries_excel_sheet(
         if input_temp_file and os.path.exists(input_temp_file.name):
             os.unlink(input_temp_file.name)
 
+@router.post('/facilities',
+             summary='Upload and process facility Excel file',
+             response_description='Returns processed Excel file with validations results')
+async def upload_facilities_excel_sheet(
+        facility_file: UploadFile = File(description="Excel file containing facility data"),
+        facility_sheet_name: str = Form(default="FacilityIngestionTemplate",
+                                        description="Name of the sheet containing facility data"),
+        request_info: str = Form(default="")
+):
+    input_temp_file = None
+    output_temp_file = None
+    request_info = request_info_from_json(request_info)
+    get_authorized_request_info(request_info)
+
+    try:
+        input_temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx")
+        content = await facility_file.read()
+        input_temp_file.write(content)
+        input_temp_file.close()
+        facility_file_path = input_temp_file.name
+
+        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+        output_filename = f"facility_ingestion_results_{timestamp}.xlsx"
+        output_temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx")
+        output_temp_file.close()
+        output_file_path = output_temp_file.name
+
+        with open(facility_file_path, 'rb') as src, open(output_file_path, 'wb') as dst:
+            dst.write(src.read())
+
+        df = pd.read_excel(facility_file_path, sheet_name=facility_sheet_name)
+
+        if 'status' not in df.columns:
+            df['status'] = ''
+        if 'error' not in df.columns:
+            df['error'] = ''
+
+        if facility_service_url and not df.empty:
+            facility_client = FacilityServiceClient(facility_service_url)
+            for index, row in df[df['status'] != 'success'].iterrows():
+                try:
+                    facility_data_payload = create_facility_payload(request_info, row)
+                    response = facility_client.create_facility(facility_data_payload)
+                    if response.status_code in (200, 201):
+                        df.at[index, 'status'] = 'success'
+                        df.at[index, 'error'] = ''
+                    elif response.status_code == 400:
+                        error_data = response.json()
+                        error_message = error_data.get('Errors', [{}])[0].get('message', 'Unknown error')
+                        df.at[index, 'status'] = 'failed'
+                        df.at[index, 'error'] = error_message
+                    else:
+                        df.at[index, 'status'] = 'failed'
+                        df.at[index, 'error'] = f'{response.status_code}: {response.text}'
+                except Exception as e:
+                    df.at[index, 'status'] = 'failed'
+                    df.at[index, 'error'] = f'Exception: {str(e)}'
+
+        writer = ExcelDataWriter(output_file_path, output_sheet=facility_sheet_name)
+        writer.write_data(df)
+
+        return FileResponse(
+            path=output_file_path,
+            filename=output_filename,
+            media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+        )
+    except Exception as e:
+        logger.error(f"Error processing facility data: {e}")
+        raise HTTPException(
+            status_code=500,
+            detail=f"Failed to process facility data: {str(e)}"
+        )
+    finally:
+        if input_temp_file and os.path.exists(input_temp_file.name):
+            os.unlink(input_temp_file.name)
 
path: backend/municipal-services/ingestion-service/app/api/endpoints/file_ingestion.py
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Consider adding validation for facility data and improving error messages.**

While the endpoint handles basic error cases, it doesn't validate the required columns in the Excel file before processing, which could lead to cryptic errors if the file format is incorrect.


Consider adding validation for the Excel structure before processing:

```diff
         df = pd.read_excel(facility_file_path, sheet_name=facility_sheet_name)
 
+        # Validate required columns
+        required_columns = [
+            'Health Centre Name (Mandatory)',
+            'Type of HC (Mandatory)',
+            'Boundary Code (Mandatory)',
+            'Vendor Code (Mandatory)',
+            'Solution Design Type (Mandatory)',
+            'HC PoC Name (Mandatory)',
+            'HC PoC Contact number (Mandatory)'
+        ]
+        missing_columns = [col for col in required_columns if col not in df.columns]
+        if missing_columns:
+            raise HTTPException(
+                status_code=400,
+                detail=f"Missing required columns in Excel file: {', '.join(missing_columns)}"
+            )
+
         if 'status' not in df.columns:
             df['status'] = ''
         if 'error' not in df.columns:
             df['error'] = ''
```

Let me verify if these required columns match what's in the code:

---

🏁 Script executed:

```shell
#!/bin/bash
# Find all references to mandatory fields in the create_facility_payload function
grep -A 30 "create_facility_payload" backend/municipal-services/ingestion-service/app/utils/convertor.py | grep "Mandatory"
```

Length of output: 657

---



</details>

**Add validation for required columns in the facility Excel file**  

To fail fast and provide clear feedback when the uploaded sheet is missing required fields, validate its structure immediately after loading it. The following columns are used by `create_facility_payload` and must be present:

- Health Centre Name (Mandatory)  
- Type of HC (Mandatory)  
- Boundary Code (Mandatory)  
- Vendor Code (Mandatory)  
- Solution Design Type (Mandatory)  
- HC PoC Name (Mandatory)  
- HC PoC Contact number (Mandatory)  

Suggested diff:

```diff
         df = pd.read_excel(facility_file_path, sheet_name=facility_sheet_name)
+
+        # Validate required columns
+        required_columns = [
+            'Health Centre Name (Mandatory)',
+            'Type of HC (Mandatory)',
+            'Boundary Code (Mandatory)',
+            'Vendor Code (Mandatory)',
+            'Solution Design Type (Mandatory)',
+            'HC PoC Name (Mandatory)',
+            'HC PoC Contact number (Mandatory)'
+        ]
+        missing_columns = [col for col in required_columns if col not in df.columns]
+        if missing_columns:
+            raise HTTPException(
+                status_code=400,
+                detail=f"Missing required columns in Excel file: {', '.join(missing_columns)}"
+            )
```

This ensures any missing fields are caught up front with a clear 400 response.  


<!-- 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
        df = pd.read_excel(facility_file_path, sheet_name=facility_sheet_name)

        # Validate required columns
        required_columns = [
            'Health Centre Name (Mandatory)',
            'Type of HC (Mandatory)',
            'Boundary Code (Mandatory)',
            'Vendor Code (Mandatory)',
            'Solution Design Type (Mandatory)',
            'HC PoC Name (Mandatory)',
            'HC PoC Contact number (Mandatory)'
        ]
        missing_columns = [col for col in required_columns if col not in df.columns]
        if missing_columns:
            raise HTTPException(
                status_code=400,
                detail=f"Missing required columns in Excel file: {', '.join(missing_columns)}"
            )

        if 'status' not in df.columns:
            df['status'] = ''
        if 'error' not in df.columns:
            df['error'] = ''
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Ruff (0.8.2)</summary>

172-172: Do not perform function call `File` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)

---

183-183: Use a context manager for opening files

(SIM115)

---

191-191: Use a context manager for opening files

(SIM115)

---

236-239: Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling

(B904)

</details>

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-12T06:37:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -164,6 +165,81 @@ async def upload_boundaries_excel_sheet(
         if input_temp_file and os.path.exists(input_temp_file.name):
             os.unlink(input_temp_file.name)
 
+@router.post('/facilities',
+             summary='Upload and process facility Excel file',
+             response_description='Returns processed Excel file with validations results')
+async def upload_facilities_excel_sheet(
+        facility_file: UploadFile = File(description="Excel file containing facility data"),
+        facility_sheet_name: str = Form(default="FacilityIngestionTemplate",
+                                        description="Name of the sheet containing facility data"),
+        request_info: str = Form(default="")
+):
+    input_temp_file = None
+    output_temp_file = None
+    request_info = request_info_from_json(request_info)
+    get_authorized_request_info(request_info)
+
+    try:
+        input_temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx")
+        content = await facility_file.read()
+        input_temp_file.write(content)
+        input_temp_file.close()
+        facility_file_path = input_temp_file.name
+
+        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+        output_filename = f"facility_ingestion_results_{timestamp}.xlsx"
+        output_temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx")
+        output_temp_file.close()
+        output_file_path = output_temp_file.name
+
+        with open(facility_file_path, 'rb') as src, open(output_file_path, 'wb') as dst:
+            dst.write(src.read())
+
+        df = pd.read_excel(facility_file_path, sheet_name=facility_sheet_name)
+
+        if 'status' not in df.columns:
+            df['status'] = ''
+        if 'error' not in df.columns:
+            df['error'] = ''
+
+        if facility_service_url and not df.empty:
+            facility_client = FacilityServiceClient(facility_service_url)
+            for index, row in df[df['status'] != 'success'].iterrows():
+                try:
+                    facility_data_payload = create_facility_payload(request_info, row)
+                    response = facility_client.create_facility(facility_data_payload)
+                    if response.status_code in (200, 201):
+                        df.at[index, 'status'] = 'success'
+                        df.at[index, 'error'] = ''
+                    elif response.status_code == 400:
+                        error_data = response.json()
+                        error_message = error_data.get('Errors', [{}])[0].get('message', 'Unknown error')
+                        df.at[index, 'status'] = 'failed'
+                        df.at[index, 'error'] = error_message
+                    else:
+                        df.at[index, 'status'] = 'failed'
+                        df.at[index, 'error'] = f'{response.status_code}: {response.text}'
+                except Exception as e:
+                    df.at[index, 'status'] = 'failed'
+                    df.at[index, 'error'] = f'Exception: {str(e)}'
+
+        writer = ExcelDataWriter(output_file_path, output_sheet=facility_sheet_name)
+        writer.write_data(df)
+
+        return FileResponse(
+            path=output_file_path,
+            filename=output_filename,
+            media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+        )
+    except Exception as e:
+        logger.error(f"Error processing facility data: {e}")
+        raise HTTPException(
+            status_code=500,
+            detail=f"Failed to process facility data: {str(e)}"
+        )
+    finally:
+        if input_temp_file and os.path.exists(input_temp_file.name):
+            os.unlink(input_temp_file.name)
 
path: backend/municipal-services/ingestion-service/app/api/endpoints/file_ingestion.py
user: login: Nickg-30
body: This is handled by backend api call
created_at: 2025-05-12T07:09:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -164,6 +165,81 @@ async def upload_boundaries_excel_sheet(
         if input_temp_file and os.path.exists(input_temp_file.name):
             os.unlink(input_temp_file.name)
 
+@router.post('/facilities',
+             summary='Upload and process facility Excel file',
+             response_description='Returns processed Excel file with validations results')
+async def upload_facilities_excel_sheet(
+        facility_file: UploadFile = File(description="Excel file containing facility data"),
+        facility_sheet_name: str = Form(default="FacilityIngestionTemplate",
+                                        description="Name of the sheet containing facility data"),
+        request_info: str = Form(default="")
+):
+    input_temp_file = None
+    output_temp_file = None
+    request_info = request_info_from_json(request_info)
+    get_authorized_request_info(request_info)
+
+    try:
+        input_temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx")
+        content = await facility_file.read()
+        input_temp_file.write(content)
+        input_temp_file.close()
+        facility_file_path = input_temp_file.name
+
+        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+        output_filename = f"facility_ingestion_results_{timestamp}.xlsx"
+        output_temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx")
+        output_temp_file.close()
+        output_file_path = output_temp_file.name
+
+        with open(facility_file_path, 'rb') as src, open(output_file_path, 'wb') as dst:
+            dst.write(src.read())
+
+        df = pd.read_excel(facility_file_path, sheet_name=facility_sheet_name)
+
+        if 'status' not in df.columns:
+            df['status'] = ''
+        if 'error' not in df.columns:
+            df['error'] = ''
+
+        if facility_service_url and not df.empty:
+            facility_client = FacilityServiceClient(facility_service_url)
+            for index, row in df[df['status'] != 'success'].iterrows():
+                try:
+                    facility_data_payload = create_facility_payload(request_info, row)
+                    response = facility_client.create_facility(facility_data_payload)
+                    if response.status_code in (200, 201):
+                        df.at[index, 'status'] = 'success'
+                        df.at[index, 'error'] = ''
+                    elif response.status_code == 400:
+                        error_data = response.json()
+                        error_message = error_data.get('Errors', [{}])[0].get('message', 'Unknown error')
+                        df.at[index, 'status'] = 'failed'
+                        df.at[index, 'error'] = error_message
+                    else:
+                        df.at[index, 'status'] = 'failed'
+                        df.at[index, 'error'] = f'{response.status_code}: {response.text}'
+                except Exception as e:
+                    df.at[index, 'status'] = 'failed'
+                    df.at[index, 'error'] = f'Exception: {str(e)}'
+
+        writer = ExcelDataWriter(output_file_path, output_sheet=facility_sheet_name)
+        writer.write_data(df)
+
+        return FileResponse(
+            path=output_file_path,
+            filename=output_filename,
+            media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+        )
+    except Exception as e:
+        logger.error(f"Error processing facility data: {e}")
+        raise HTTPException(
+            status_code=500,
+            detail=f"Failed to process facility data: {str(e)}"
+        )
+    finally:
+        if input_temp_file and os.path.exists(input_temp_file.name):
+            os.unlink(input_temp_file.name)
 
path: backend/municipal-services/ingestion-service/app/api/endpoints/file_ingestion.py
user: login: coderabbitai[bot]
body: @Nickg-30 I understand - your implementation is already handling validation through the backend API calls. The code correctly processes any validation errors returned by the API (status code 400) and updates the Excel file with appropriate error messages. This approach centralizes all validation rules in the backend service.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: Nickg-30
PR: Selco-Foundation/E4H_Digital_Platform#652
File: backend/municipal-services/ingestion-service/app/api/endpoints/file_ingestion.py:168-243
Timestamp: 2025-05-12T07:10:53.064Z
Learning: Validation for facility data in the ingestion service is handled by the backend API rather than preprocessing the Excel file in the frontend. The implementation sends data to the facility service and processes error responses to update the status and error columns in the output file.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@Nickg-30 I understand - your implementation is already handling validation through the backend API calls. The code correctly processes any validation errors returned by the API (status code 400) and updates the Excel file with appropriate error messages. This approach centralizes all validation rules in the backend service.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: Nickg-30
PR: Selco-Foundation/E4H_Digital_Platform#652
File: backend/municipal-services/ingestion-service/app/api/endpoints/file_ingestion.py:168-243
Timestamp: 2025-05-12T07:10:53.064Z
Learning: Validation for facility data in the ingestion service is handled by the backend API rather than preprocessing the Excel file in the frontend. The implementation sends data to the facility service and processes error responses to update the status and error columns in the output file.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-12T07:11:25Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-12T06:37:28Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

A new facility ingestion workflow was implemented, introducing a POST endpoint for uploading and processing facility Excel files. Supporting utility functions and a client for interacting with an external facility service were added. The environment configuration was updated to include the facility service URL, and new logic was provided for constructing and sending facility payloads.

## Changes

| File(s)                                                                                  | Change Summary                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
|------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| backend/municipal-services/ingestion-service/.env                                        | Added `FACILITY_SERVICE_URL` environment variable set to `http://localhost:8081`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| backend/municipal-services/ingestion-service/app/api/endpoints/file_ingestion.py         | Added a POST endpoint `/facilities` to handle facility Excel uploads, process each row, interact with the facility service, update statuses/errors, and return the processed file. Integrated environment variable loading and new imports for facility client and payload creation.                                                                                                                                            |
| backend/municipal-services/ingestion-service/app/utils/convertor.py                      | Added `safe_get` utility for robust Series value retrieval and `create_facility_payload` for constructing facility payloads from DataFrame rows and request info, handling defaults and missing values gracefully.                                                                                                                               |
| backend/municipal-services/ingestion-service/app/utils/facility_service_client.py        | Introduced `FacilityServiceClient` class with methods to initialize using a service URL and to send facility creation requests to the external service, including error handling for HTTP and connection issues.                                                                                                                                |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant API (file_ingestion.py)
    participant FacilityServiceClient
    participant Facility Service

    User->>API (file_ingestion.py): POST /facilities (Excel file, sheet name, request info)
    API (file_ingestion.py)->>API (file_ingestion.py): Parse Excel, iterate rows
    loop For each row not marked 'success'
        API (file_ingestion.py)->>convertor.py: create_facility_payload()
        API (file_ingestion.py)->>FacilityServiceClient: create_facility(payload)
        FacilityServiceClient->>Facility Service: POST /facility-service/v2/facility/create
        Facility Service-->>FacilityServiceClient: Response (success/error)
        FacilityServiceClient-->>API (file_ingestion.py): Response
        API (file_ingestion.py)->>API (file_ingestion.py): Update row status/error
    end
    API (file_ingestion.py)-->>User: Return processed Excel file
```

## Poem

> In the warren where data hops and flows,  
> A new endpoint blooms as the spring wind blows.  
> With Excel in paw, each facility hops through,  
> Payloads assembled, sent off anew.  
> The service replies with a carrot or stick—  
> Success or error, the DataFrame’s quick!  
> 🐇✨ Excel returns, all neat and slick.

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> - CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> 
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->
<!-- internal state start -->


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

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

<details open="true">
<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=Selco-Foundation/E4H_Digital_Platform&utm_content=652):

- 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.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

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 generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@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: sonarqubecloud[bot]
created_at: 2025-05-12T06:37:45Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=Selco-Foundation_e4h-incident-management&pullRequest=652) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=Selco-Foundation_e4h-incident-management&pullRequest=652&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=Selco-Foundation_e4h-incident-management&pullRequest=652&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=Selco-Foundation_e4h-incident-management&pullRequest=652&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=Selco-Foundation_e4h-incident-management&pullRequest=652&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=Selco-Foundation_e4h-incident-management&pullRequest=652&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=Selco-Foundation_e4h-incident-management&pullRequest=652)


author_association: NONE