Repository: NHSDigital/connecting-party-manager
Pull request #550: [feature/PI-815] Recreate a product Team
Author: login: jameslinnell
Created: 2025-03-19T16:11:30Z | Merged: 2025-03-21T15:17:11Z



Diff:
diff --git a/src/api/deleteProductTeam/policies/dynamodb.json b/src/api/deleteProductTeam/policies/dynamodb.json
index b8bdf27b8..fecf4f6cc 100644
--- a/src/api/deleteProductTeam/policies/dynamodb.json
+++ b/src/api/deleteProductTeam/policies/dynamodb.json
@@ -1 +1,6 @@
-["dynamodb:Query", "dynamodb:UpdateItem"]
+[
+  "dynamodb:Query",
+  "dynamodb:UpdateItem",
+  "dynamodb:PutItem",
+  "dynamodb:DeleteItem"
+]
diff --git a/src/api/deleteProductTeam/tests/test_index.py b/src/api/deleteProductTeam/tests/test_index.py
index fb42103e1..79ebc156a 100644
--- a/src/api/deleteProductTeam/tests/test_index.py
+++ b/src/api/deleteProductTeam/tests/test_index.py
@@ -241,3 +241,102 @@ def test_index_existing_products(version):
     _response_assertions(
         result=result, expected=expected, check_body=True, check_content_length=True
     )
+
+
+@pytest.mark.parametrize(
+    "version",
+    [
+        "1",
+    ],
+)
+def test_index_create_multiple_times(version):
+    org = Root.create_ods_organisation(ods_code=CPM_PRODUCT_TEAM_NO_ID["ods_code"])
+    product_team = org.create_product_team(
+        name=CPM_PRODUCT_TEAM_NO_ID["name"], keys=CPM_PRODUCT_TEAM_NO_ID["keys"]
+    )
+
+    with mock_table_cpm(TABLE_NAME) as client, mock.patch.dict(
+        os.environ,
+        {
+            "DYNAMODB_TABLE": TABLE_NAME,
+            "AWS_DEFAULT_REGION": "eu-west-2",
+        },
+        clear=True,
+    ):
+        from api.deleteProductTeam.index import cache, handler
+
+        cache["DYNAMODB_CLIENT"] = client
+
+        product_team_repo = ProductTeamRepository(
+            table_name=TABLE_NAME, dynamodb_client=client
+        )
+        product_team_repo.write(entity=product_team)
+
+        result = handler(
+            event={
+                "headers": {"version": version},
+                "pathParameters": {"product_team_id": product_team.id},
+            }
+        )
+    expected_result = json.dumps(
+        {
+            "code": "RESOURCE_DELETED",
+            "message": f"{product_team.id} has been deleted.",
+        }
+    )
+
+    expected = {
+        "statusCode": 200,
+        "body": expected_result,
+        "headers": {
+            "Content-Length": str(len(expected_result)),
+            "Content-Type": "application/json",
+            "Version": version,
+        },
+    }
+    _response_assertions(
+        result=result, expected=expected, check_body=True, check_content_length=True
+    )
+
+    with mock_table_cpm(TABLE_NAME) as client, mock.patch.dict(
+        os.environ,
+        {
+            "DYNAMODB_TABLE": TABLE_NAME,
+            "AWS_DEFAULT_REGION": "eu-west-2",
+        },
+        clear=True,
+    ):
+        from api.deleteProductTeam.index import cache, handler
+
+        cache["DYNAMODB_CLIENT"] = client
+
+        product_team_repo = ProductTeamRepository(
+            table_name=TABLE_NAME, dynamodb_client=client
+        )
+        product_team_repo.write(entity=product_team)
+
+        result = handler(
+            event={
+                "headers": {"version": version},
+                "pathParameters": {"product_team_id": product_team.id},
+            }
+        )
+    expected_result = json.dumps(
+        {
+            "code": "RESOURCE_DELETED",
+            "message": f"{product_team.id} has been deleted.",
+        }
+    )
+
+    expected = {
+        "statusCode": 200,
+        "body": expected_result,
+        "headers": {
+            "Content-Length": str(len(expected_result)),
+            "Content-Type": "application/json",
+            "Version": version,
+        },
+    }
+    _response_assertions(
+        result=result, expected=expected, check_body=True, check_content_length=True
+    )
diff --git a/src/api/tests/feature_tests/features/deleteProductTeam.success.feature b/src/api/tests/feature_tests/features/deleteProductTeam.success.feature
index e82cddf61..f3b2cabf1 100644
--- a/src/api/tests/feature_tests/features/deleteProductTeam.success.feature
+++ b/src/api/tests/feature_tests/features/deleteProductTeam.success.feature
@@ -39,11 +39,19 @@ Feature: Delete Product Team - success scenarios
       | keys.0.key_value | 0a78ee8f-5bcf-4db1-9341-ef1d67248715 |
     And I note the response field "$.id" as "product_team_id"
     And I have already made a "DELETE" request with "default" headers to "ProductTeam/<product_team_id>"
-    When I make a "GET" request with "default" headers to "ProductTeam/<product_team_id>"
+    When I make a "GET" request with "default" headers to "ProductTeam/${ note(product_team_id) }"
     Then I receive a status code "404" with body
-      | path             | value                                                    |
-      | errors.0.code    | RESOURCE_NOT_FOUND                                       |
-      | errors.0.message | Could not find ProductTeam for key ('<product_team_id>') |
+      | path             | value                                                             |
+      | errors.0.code    | RESOURCE_NOT_FOUND                                                |
+      | errors.0.message | Could not find ProductTeam for key ('${ note(product_team_id) }') |
+    And the response headers contain:
+      | name         | value            |
+      | Content-Type | application/json |
+    When I make a "GET" request with "default" headers to "ProductTeam/0a78ee8f-5bcf-4db1-9341-ef1d67248715"
+    Then I receive a status code "404" with body
+      | path             | value                                                                       |
+      | errors.0.code    | RESOURCE_NOT_FOUND                                                          |
+      | errors.0.message | Could not find ProductTeam for key ('0a78ee8f-5bcf-4db1-9341-ef1d67248715') |
     And the response headers contain:
       | name         | value            |
       | Content-Type | application/json |
@@ -52,3 +60,71 @@ Feature: Delete Product Team - success scenarios
       | product_team_id                      |
       | ${ note(product_team_id) }           |
       | 0a78ee8f-5bcf-4db1-9341-ef1d67248715 |
+
+  Scenario: Successfully delete a CPM Product Team multiple times.
+    Given I have already made a "POST" request with "default" headers to "ProductTeam" with body:
+      | path             | value                                |
+      | name             | My Great Product Team                |
+      | ods_code         | F5H1R                                |
+      | keys.0.key_type  | product_team_id                      |
+      | keys.0.key_value | 0a78ee8f-5bcf-4db1-9341-ef1d67248715 |
+    And I note the response field "$.id" as "product_team_id"
+    And I have already made a "DELETE" request with "default" headers to "ProductTeam/${ note(product_team_id) }"
+    When I make a "GET" request with "default" headers to "ProductTeam/${ note(product_team_id) }"
+    Then I receive a status code "404" with body
+      | path             | value                                                             |
+      | errors.0.code    | RESOURCE_NOT_FOUND                                                |
+      | errors.0.message | Could not find ProductTeam for key ('${ note(product_team_id) }') |
+    And the response headers contain:
+      | name         | value            |
+      | Content-Type | application/json |
+    When I make a "GET" request with "default" headers to "ProductTeam/0a78ee8f-5bcf-4db1-9341-ef1d67248715"
+    Then I receive a status code "404" with body
+      | path             | value                                                                       |
+      | errors.0.code    | RESOURCE_NOT_FOUND                                                          |
+      | errors.0.message | Could not find ProductTeam for key ('0a78ee8f-5bcf-4db1-9341-ef1d67248715') |
+    And the response headers contain:
+      | name         | value            |
+      | Content-Type | application/json |
+    When I make a "POST" request with "default" headers to "ProductTeam" with body:
+      | path             | value                                |
+      | name             | My Great Product Team                |
+      | ods_code         | F5H1R                                |
+      | keys.0.key_type  | product_team_id                      |
+      | keys.0.key_value | 0a78ee8f-5bcf-4db1-9341-ef1d67248715 |
+    And I note the response field "$.id" as "product_team_id_2"
+    When I make a "GET" request with "default" headers to "ProductTeam/${ note(product_team_id_2) }"
+    Then I receive a status code "200"
+    And the response headers contain:
+      | name         | value            |
+      | Content-Type | application/json |
+    When I make a "GET" request with "default" headers to "ProductTeam/0a78ee8f-5bcf-4db1-9341-ef1d67248715"
+    Then I receive a status code "200"
+    And the response headers contain:
+      | name         | value            |
+      | Content-Type | application/json |
+    When I make a "DELETE" request with "default" headers to "ProductTeam/${ note(product_team_id_2) }"
+    Then I receive a status code "200" with body
+      | path    | value                                          |
+      | code    | RESOURCE_DELETED                               |
+      | message | ${ note(product_team_id_2) } has been deleted. |
+    And the response headers contain:
+      | name           | value            |
+      | Content-Type   | application/json |
+      | Content-Length | 97               |
+    When I make a "GET" request with "default" headers to "ProductTeam/${ note(product_team_id_2) }"
+    Then I receive a status code "404" with body
+      | path             | value                                                               |
+      | errors.0.code    | RESOURCE_NOT_FOUND                                                  |
+      | errors.0.message | Could not find ProductTeam for key ('${ note(product_team_id_2) }') |
+    And the response headers contain:
+      | name         | value            |
+      | Content-Type | application/json |
+    When I make a "GET" request with "default" headers to "ProductTeam/0a78ee8f-5bcf-4db1-9341-ef1d67248715"
+    Then I receive a status code "404" with body
+      | path             | value                                                                       |
+      | errors.0.code    | RESOURCE_NOT_FOUND                                                          |
+      | errors.0.message | Could not find ProductTeam for key ('0a78ee8f-5bcf-4db1-9341-ef1d67248715') |
+    And the response headers contain:
+      | name         | value            |
+      | Content-Type | application/json |
diff --git a/src/layers/domain/repository/cpm_repository/v1.py b/src/layers/domain/repository/cpm_repository/v1.py
index 7c5cf7e95..0fd2760c6 100644
--- a/src/layers/domain/repository/cpm_repository/v1.py
+++ b/src/layers/domain/repository/cpm_repository/v1.py
@@ -4,7 +4,6 @@
 
 from domain.core.aggregate_root import AggregateRoot
 from domain.core.enum import EntityType
-from domain.core.product_team_key.v1 import ProductTeamKeyType
 from domain.repository.errors import ItemNotFound
 from domain.repository.keys import KEY_SEPARATOR, TableKey
 from domain.repository.marshall import marshall, unmarshall
@@ -161,14 +160,6 @@ def update_indexes(self, pk: str, id: str, keys: list[str], data: dict):
             ]
         else:
             primary_keys = [marshall(pk=pk, sk=pk)]
-            for key in keys:
-                if key["key_type"] == ProductTeamKeyType.PRODUCT_TEAM_ID:
-                    primary_keys.append(
-                        marshall(
-                            pk=TableKey.PRODUCT_TEAM.key(key["key_value"]),
-                            sk=TableKey.PRODUCT_TEAM.key(key["key_value"]),
-                        )
-                    )
         return update_transactions(
             table_name=self.table_name, primary_keys=primary_keys, data=data
         )
diff --git a/src/layers/domain/repository/product_team_repository/v1.py b/src/layers/domain/repository/product_team_repository/v1.py
index fb3f60eb1..6ef6e9f7f 100644
--- a/src/layers/domain/repository/product_team_repository/v1.py
+++ b/src/layers/domain/repository/product_team_repository/v1.py
@@ -1,3 +1,5 @@
+from datetime import datetime
+
 from attr import asdict
 from domain.core.enum import EntityType
 from domain.core.product_team import (
@@ -50,9 +52,29 @@ def handle_ProductTeamCreatedEvent(self, event: ProductTeamCreatedEvent):
         return [create_root_transaction] + create_key_transactions
 
     def handle_ProductTeamDeletedEvent(self, event: ProductTeamDeletedEvent):
-        return self.update_indexes(
+        keys = {ProductTeamKey(**key) for key in event.keys}
+        product_team_key_delete_transactions = [
+            self.delete_index(id=key.key_value) for key in keys
+        ]
+        timestamp = int(datetime.now().timestamp() * 1000)
+        product_team_key_create_transactions = [
+            self.create_index(
+                id=f"{key.key_value}-{timestamp}",
+                parent_key_parts=(f"{key.key_value}-{timestamp}",),
+                data=asdict(event),
+                root=False,
+                row_type=EntityType.PRODUCT_TEAM_ALIAS,
+            )
+            for key in keys
+        ]
+        product_team_update = self.update_indexes(
             pk=TableKey.PRODUCT_TEAM.key(event.id),
             id=event.id,
             keys=event.keys,
             data=asdict(event),
         )
+        return (
+            product_team_update
+            + product_team_key_create_transactions
+            + product_team_key_delete_transactions
+        )


Reviews:
user: login: megan-bower4
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T09:52:56Z
user: login: megan-bower4
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T09:54:35Z
user: login: megan-bower4
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T09:59:04Z
user: login: megan-bower4
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T10:32:53Z

Review comments:
diff_hunk: @@ -39,11 +39,19 @@ Feature: Delete Product Team - success scenarios
       | keys.0.key_value | 0a78ee8f-5bcf-4db1-9341-ef1d67248715 |
     And I note the response field "$.id" as "product_team_id"
     And I have already made a "DELETE" request with "default" headers to "ProductTeam/<product_team_id>"
-    When I make a "GET" request with "default" headers to "ProductTeam/<product_team_id>"
+    When I make a "GET" request with "default" headers to "ProductTeam/${ note(product_team_id) }"
     Then I receive a status code "404" with body
-      | path             | value                                                    |
-      | errors.0.code    | RESOURCE_NOT_FOUND                                       |
-      | errors.0.message | Could not find ProductTeam for key ('<product_team_id>') |
+      | path             | value                                                             |
+      | errors.0.code    | RESOURCE_NOT_FOUND                                                |
+      | errors.0.message | Could not find ProductTeam for key ('${ note(product_team_id) }') |
+    And the response headers contain:
+      | name         | value            |
+      | Content-Type | application/json |
+    When I make a "GET" request with "default" headers to "ProductTeam/0a78ee8f-5bcf-4db1-9341-ef1d67248715"
path: src/api/tests/feature_tests/features/deleteProductTeam.success.feature
user: login: megan-bower4
body: You shouldn't need to specifically do this is the examples block includes product_team_id and the alias/key value already
created_at: 2025-03-20T09:52:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -52,3 +60,71 @@ Feature: Delete Product Team - success scenarios
       | product_team_id                      |
       | ${ note(product_team_id) }           |
       | 0a78ee8f-5bcf-4db1-9341-ef1d67248715 |
+
+  Scenario: Successfully delete a CPM Product Team multiple times.
+    Given I have already made a "POST" request with "default" headers to "ProductTeam" with body:
+      | path             | value                                |
+      | name             | My Great Product Team                |
+      | ods_code         | F5H1R                                |
+      | keys.0.key_type  | product_team_id                      |
+      | keys.0.key_value | 0a78ee8f-5bcf-4db1-9341-ef1d67248715 |
+    And I note the response field "$.id" as "product_team_id"
+    And I have already made a "DELETE" request with "default" headers to "ProductTeam/${ note(product_team_id) }"
+    When I make a "GET" request with "default" headers to "ProductTeam/${ note(product_team_id) }"
+    Then I receive a status code "404" with body
+      | path             | value                                                             |
+      | errors.0.code    | RESOURCE_NOT_FOUND                                                |
+      | errors.0.message | Could not find ProductTeam for key ('${ note(product_team_id) }') |
+    And the response headers contain:
+      | name         | value            |
+      | Content-Type | application/json |
+    When I make a "GET" request with "default" headers to "ProductTeam/0a78ee8f-5bcf-4db1-9341-ef1d67248715"
+    Then I receive a status code "404" with body
+      | path             | value                                                                       |
+      | errors.0.code    | RESOURCE_NOT_FOUND                                                          |
+      | errors.0.message | Could not find ProductTeam for key ('0a78ee8f-5bcf-4db1-9341-ef1d67248715') |
+    And the response headers contain:
+      | name         | value            |
+      | Content-Type | application/json |
+    When I make a "POST" request with "default" headers to "ProductTeam" with body:
+      | path             | value                                |
+      | name             | My Great Product Team                |
+      | ods_code         | F5H1R                                |
+      | keys.0.key_type  | product_team_id                      |
+      | keys.0.key_value | 0a78ee8f-5bcf-4db1-9341-ef1d67248715 |
+    And I note the response field "$.id" as "product_team_id_2"
+    When I make a "GET" request with "default" headers to "ProductTeam/${ note(product_team_id_2) }"
+    Then I receive a status code "200"
+    And the response headers contain:
+      | name         | value            |
+      | Content-Type | application/json |
+    When I make a "GET" request with "default" headers to "ProductTeam/0a78ee8f-5bcf-4db1-9341-ef1d67248715"
+    Then I receive a status code "200"
+    And the response headers contain:
+      | name         | value            |
+      | Content-Type | application/json |
+    When I make a "DELETE" request with "default" headers to "ProductTeam/${ note(product_team_id_2) }"
+    Then I receive a status code "200" with body
+      | path    | value                                          |
+      | code    | RESOURCE_DELETED                               |
+      | message | ${ note(product_team_id_2) } has been deleted. |
+    And the response headers contain:
+      | name           | value            |
+      | Content-Type   | application/json |
+      | Content-Length | 97               |
+    When I make a "GET" request with "default" headers to "ProductTeam/${ note(product_team_id_2) }"
+    Then I receive a status code "404" with body
+      | path             | value                                                               |
+      | errors.0.code    | RESOURCE_NOT_FOUND                                                  |
+      | errors.0.message | Could not find ProductTeam for key ('${ note(product_team_id_2) }') |
+    And the response headers contain:
+      | name         | value            |
+      | Content-Type | application/json |
+    When I make a "GET" request with "default" headers to "ProductTeam/0a78ee8f-5bcf-4db1-9341-ef1d67248715"
+    Then I receive a status code "404" with body
+      | path             | value                                                                       |
+      | errors.0.code    | RESOURCE_NOT_FOUND                                                          |
+      | errors.0.message | Could not find ProductTeam for key ('0a78ee8f-5bcf-4db1-9341-ef1d67248715') |
+    And the response headers contain:
+      | name         | value            |
+      | Content-Type | application/json |
path: src/api/tests/feature_tests/features/deleteProductTeam.success.feature
user: login: megan-bower4
body: If you make this a scenario outline with examples, you can interchangeably use the internal prod id and the external key id - will also test that you can delete it using the key as well then, otherwise will need a test for that too 
created_at: 2025-03-20T09:54:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -50,9 +52,29 @@ def handle_ProductTeamCreatedEvent(self, event: ProductTeamCreatedEvent):
         return [create_root_transaction] + create_key_transactions
 
     def handle_ProductTeamDeletedEvent(self, event: ProductTeamDeletedEvent):
-        return self.update_indexes(
+        keys = {ProductTeamKey(**key) for key in event.keys}
+        product_team_key_delete_transactions = [
+            self.delete_index(id=key.key_value) for key in keys
+        ]
+        timestamp = int(datetime.now().timestamp() * 1000)
+        product_team_key_create_transactions = [
+            self.create_index(
+                id=f"{key.key_value}-{timestamp}",
+                parent_key_parts=(f"{key.key_value}-{timestamp}",),
+                data=asdict(event),
+                root=False,
+                row_type=EntityType.PRODUCT_TEAM_ALIAS,
+            )
+            for key in keys
path: src/layers/domain/repository/product_team_repository/v1.py
user: login: megan-bower4
body: Just trying to picture this - so for every key that was on the root product team, a new row will be created with the same keys or with no keys now?
created_at: 2025-03-20T09:59:04Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-03-19T17:41:49Z
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=NHSDigital_connecting-party-manager&pullRequest=550) **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=NHSDigital_connecting-party-manager&pullRequest=550&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=NHSDigital_connecting-party-manager&pullRequest=550&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=NHSDigital_connecting-party-manager&pullRequest=550&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [100.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=NHSDigital_connecting-party-manager&pullRequest=550&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=NHSDigital_connecting-party-manager&pullRequest=550&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=NHSDigital_connecting-party-manager&pullRequest=550)


author_association: NONE