Repository: ethyca/fides
Pull request #6022: LJ-392 Add comments and attachments to manual access webhooks
Author: login: JadeCara
Created: 2025-04-09T15:10:51Z | Merged: 2025-04-09T17:16:59Z

Closes [LJ-392](https://ethyca.atlassian.net/browse/LJ-392?atlOrigin=eyJpIjoiMzU4ZDVkMTU2M2VjNDIwZDg1ODhiOGFmYWQ5YWIxZjEiLCJwIjoiaiJ9)

### Description Of Changes

Added attachments and comments relationships to Manual Webhooks as well as adding a new "types" field to manual webhooks. This will allow the user to indicate they want to require some kind of attachment to the manual step. This will satisfy the following requirement:

> - When configuring DSRs, I can add one or more “Manual steps” to my DSR process that can be configured to support file uploads and comments instead of the current fields.

Additionally added att

### Code Changes

* Added comment and attachment relationships to manual webhooks with tests
* Added attachment relationship to comments with tests
* Added "types" to fields in manual webhooks to allow users to indicate there they expect an attachment rather than just a string with tests. 
* Added backwards compatibility tests.

### Steps to Confirm

1. This will be fidesplus functionality. There should be no changes to fides functionality. Tests should cover all changes. 

### Pre-Merge Checklist

* [ ] Issue requirements met
* [ ] All CI pipelines succeeded
* [ ] `CHANGELOG.md` updated
  * [ ] Add a https://github.com/ethyca/fides/labels/db-migration label to the entry if your change includes a DB migration
  * [ ] Add a https://github.com/ethyca/fides/labels/high-risk label to the entry if your change includes a high-risk change (i.e. potential for performance impact or unexpected regression) that should be flagged
* Followup issues:
  * [ ] Followup issues created (include link)
  * [ ] No followup issues
* Database migrations:
  * [ ] Ensure that your downrev is up to date with the latest revision on `main`
  * [ ] Ensure that your `downgrade()` migration is correct and works
    * [ ] If a downgrade migration is not possible for this change, please call this out in the PR description!
  * [ ] No migrations
* Documentation:
  * [ ] Documentation complete, [PR opened in fidesdocs](https://github.com/ethyca/fidesdocs/pulls)
  * [ ] Documentation [issue created in fidesdocs](https://github.com/ethyca/fidesdocs/issues/new/choose)
  * [ ] If there are any new client scopes created as part of the pull request, remember to update public-facing documentation that references our scope registry
  * [ ] No documentation updates required


[LJ-392]: https://ethyca.atlassian.net/browse/LJ-392?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ


Diff:
diff --git a/src/fides/api/models/comment.py b/src/fides/api/models/comment.py
index 2f48aefe749..d79742b7ab9 100644
--- a/src/fides/api/models/comment.py
+++ b/src/fides/api/models/comment.py
@@ -95,6 +95,17 @@ class Comment(Base):
         primaryjoin=lambda: Comment.id == orm.foreign(CommentReference.comment_id),
     )
 
+    attachments = relationship(
+        "Attachment",
+        secondary="attachment_reference",
+        primaryjoin="and_(Comment.id == AttachmentReference.reference_id, "
+        "AttachmentReference.reference_type == 'comment')",
+        secondaryjoin="Attachment.id == AttachmentReference.attachment_id",
+        order_by="Attachment.created_at",
+        viewonly=True,
+        uselist=True,
+    )
+
     def delete(self, db: Session) -> None:
         """Delete the comment and all associated references."""
         # Delete the comment
diff --git a/src/fides/api/models/manual_webhook.py b/src/fides/api/models/manual_webhook.py
index 9f17e026678..441b3773849 100644
--- a/src/fides/api/models/manual_webhook.py
+++ b/src/fides/api/models/manual_webhook.py
@@ -1,5 +1,6 @@
-from typing import Any, Dict, List, Optional
+from typing import Any, Optional
 
+from loguru import logger
 from pydantic import ConfigDict, create_model
 from sqlalchemy import Column, ForeignKey, String, or_, text
 from sqlalchemy.dialects.postgresql import JSONB
@@ -7,6 +8,8 @@
 from sqlalchemy.orm import Session, relationship
 
 from fides.api.db.base_class import Base
+from fides.api.models.attachment import Attachment, AttachmentReference
+from fides.api.models.comment import Comment, CommentReference
 from fides.api.models.connectionconfig import ConnectionConfig
 from fides.api.schemas.base_class import FidesSchema
 from fides.api.schemas.policy import ActionType
@@ -30,21 +33,49 @@ class AccessManualWebhook(Base):
         ConnectionConfig, back_populates="access_manual_webhook", uselist=False
     )
 
+    attachments = relationship(
+        "Attachment",
+        secondary="attachment_reference",
+        primaryjoin="and_(AccessManualWebhook.id == AttachmentReference.reference_id, "
+        "AttachmentReference.reference_type == 'manual_step')",
+        secondaryjoin="Attachment.id == AttachmentReference.attachment_id",
+        order_by="Attachment.created_at",
+        viewonly=True,
+        uselist=True,
+    )
+
+    comments = relationship(
+        "Comment",
+        secondary="comment_reference",
+        primaryjoin="and_(AccessManualWebhook.id == CommentReference.reference_id, "
+        "CommentReference.reference_type == 'manual_step')",
+        secondaryjoin="Comment.id == CommentReference.comment_id",
+        order_by="Comment.created_at",
+        viewonly=True,
+        uselist=True,
+    )
+
     fields = Column(MutableList.as_mutable(JSONB), nullable=False)
 
-    def access_field_definitions(self) -> Dict[str, Any]:
+    def access_field_definitions(self) -> dict[str, Any]:
         """Shared access field definitions for manual webhook schemas"""
-        return {
-            field["dsr_package_label"]: (Optional[str], None)
-            for field in self.fields or []
-        }
-
-    def erasure_field_definitions(self) -> Dict[str, Any]:
+        field_definitions = {}
+        for field in self.fields or []:
+            # Include all fields for access, regardless of type
+            if "dsr_package_label" in field:
+                field_definitions[field["dsr_package_label"]] = (Optional[str], None)
+        return field_definitions
+
+    def erasure_field_definitions(self) -> dict[str, Any]:
         """Shared erasure field definitions for manual webhook schemas"""
-        return {
-            field["dsr_package_label"]: (Optional[bool], None)
-            for field in self.fields or []
-        }
+        field_definitions = {}
+        for field in self.fields or []:
+            # Use types if present, otherwise default to ["string"]
+            field_types = field.get("types", ["string"])
+            # Only include string fields for erasure
+            if "dsr_package_label" in field and "string" in field_types:
+                field_definitions[field["dsr_package_label"]] = (Optional[bool], None)
+        return field_definitions
 
     @property
     def fields_schema(self) -> FidesSchema:
@@ -90,7 +121,7 @@ def erasure_fields_non_strict_schema(self) -> FidesSchema:
         )
 
     @property
-    def empty_fields_dict(self) -> Dict[str, None]:
+    def empty_fields_dict(self) -> dict[str, None]:
         """Return a dictionary that maps defined dsr_package_labels to None
 
         Returned as a default if no data has been uploaded for a privacy request.
@@ -103,7 +134,7 @@ def empty_fields_dict(self) -> Dict[str, None]:
     @classmethod
     def get_enabled(
         cls, db: Session, action_type: Optional[ActionType] = None
-    ) -> List["AccessManualWebhook"]:
+    ) -> list["AccessManualWebhook"]:
         """Get all enabled access manual webhooks with fields"""
 
         query = db.query(cls).filter(
@@ -123,3 +154,47 @@ def get_enabled(
             )
 
         return query.all()
+
+    def get_comment_by_id(self, db: Session, comment_id: str) -> Optional[Comment]:
+        """Get the comment associated with the manual webhook"""
+        comment = (
+            db.query(Comment)
+            .join(CommentReference, Comment.id == CommentReference.comment_id)
+            .filter(
+                CommentReference.reference_id == self.id,
+                Comment.id == comment_id,
+            )
+            .first()
+        )
+        if not comment:
+            logger.info(
+                f"Comment with id {comment_id} not found on manual webhook {self.id}"
+            )
+        return comment
+
+    def get_attachment_by_id(
+        self, db: Session, attachment_id: str
+    ) -> Optional[Attachment]:
+        """Get the attachment associated with the manual webhook"""
+        attachment = (
+            db.query(Attachment)
+            .join(
+                AttachmentReference, Attachment.id == AttachmentReference.attachment_id
+            )
+            .filter(
+                AttachmentReference.reference_id == self.id,
+                Attachment.id == attachment_id,
+            )
+            .first()
+        )
+        if not attachment:
+            logger.info(
+                f"Attachment with id {attachment_id} not found on manual webhook {self.id}"
+            )
+        return attachment
+
+    def delete_attachment_by_id(self, db: Session, attachment_id: str) -> None:
+        """Delete the attachment associated with the manual webhook"""
+        attachment = self.get_attachment_by_id(db, attachment_id)
+        if attachment:
+            attachment.delete(db)
diff --git a/src/fides/api/schemas/manual_webhook_schemas.py b/src/fides/api/schemas/manual_webhook_schemas.py
index f2dc8842a03..4d3cf8030fc 100644
--- a/src/fides/api/schemas/manual_webhook_schemas.py
+++ b/src/fides/api/schemas/manual_webhook_schemas.py
@@ -1,4 +1,4 @@
-from typing import TYPE_CHECKING, Annotated, List, Optional, Set
+from typing import TYPE_CHECKING, Annotated, Literal, Optional, Set
 
 from fideslang.validation import FidesKey
 from pydantic import ConfigDict, Field, StringConstraints, field_validator
@@ -20,8 +20,9 @@ class ManualWebhookField(FidesSchema):
     pii_field: Annotated[
         str, StringConstraints(min_length=1, max_length=200, strip_whitespace=True)
     ]
+    types: list[Literal["string", "file"]] = ["string"]
     dsr_package_label: Optional[DSRLabelFieldType] = None
-    data_categories: Optional[List[FidesKey]] = None
+    data_categories: Optional[list[FidesKey]] = None
 
     @field_validator("dsr_package_label")
     @classmethod
@@ -38,9 +39,9 @@ def convert_empty_string_dsr_package_label(
 
 
 if TYPE_CHECKING:
-    ManualWebhookFieldsList = List[ManualWebhookField]
+    ManualWebhookFieldsList = list[ManualWebhookField]
 else:
-    ManualWebhookFieldsList = Annotated[List[ManualWebhookField], Field(min_length=1)]
+    ManualWebhookFieldsList = Annotated[list[ManualWebhookField], Field(min_length=1)]
 
 
 class AccessManualWebhooks(FidesSchema):
@@ -52,8 +53,8 @@ class AccessManualWebhooks(FidesSchema):
     @field_validator("fields")
     @classmethod
     def check_for_duplicates(
-        cls, value: List[ManualWebhookField]
-    ) -> List[ManualWebhookField]:
+        cls, value: list[ManualWebhookField]
+    ) -> list[ManualWebhookField]:
         """
         Verify that pii_fields and dsr_package_labels are unique.
 
@@ -82,8 +83,8 @@ def check_for_duplicates(
     @field_validator("fields")
     @classmethod
     def fields_must_exist(
-        cls, value: List[ManualWebhookField]
-    ) -> List[ManualWebhookField]:
+        cls, value: list[ManualWebhookField]
+    ) -> list[ManualWebhookField]:
         """
         Verify that pii_fields and dsr_package_labels are unique.
 
diff --git a/tests/ctl/models/test_comment.py b/tests/ctl/models/test_comment.py
index 887659974f4..7c86bf04588 100644
--- a/tests/ctl/models/test_comment.py
+++ b/tests/ctl/models/test_comment.py
@@ -363,3 +363,71 @@ def mock_get_s3_client(auth_method, storage_secrets):
 
     # Refresh the session to ensure clean state
     db.expire_all()
+
+
+def test_comment_attachment_relationship(
+    s3_client, db, comment, attachment, monkeypatch
+):
+    """Test the relationship between a comment and its attachments."""
+
+    def mock_get_s3_client(auth_method, storage_secrets):
+        return s3_client
+
+    monkeypatch.setattr("fides.api.tasks.storage.get_s3_client", mock_get_s3_client)
+
+    # Create attachment reference
+    attachment_ref = AttachmentReference.create(
+        db,
+        data={
+            "attachment_id": attachment.id,
+            "reference_id": comment.id,
+            "reference_type": AttachmentReferenceType.comment,
+        },
+    )
+    db.refresh(comment)
+
+    # Test the relationship
+    assert len(comment.attachments) == 1
+    assert comment.attachments[0] == attachment
+    assert comment.attachments[0].file_name == attachment.file_name
+
+    # Clean up
+    attachment_ref.delete(db)
+    attachment.delete(db)
+
+
+def test_comment_multiple_attachments_relationship(
+    s3_client, db, comment, multiple_attachments, monkeypatch
+):
+    """Test the relationship between a comment and multiple attachments."""
+
+    def mock_get_s3_client(auth_method, storage_secrets):
+        return s3_client
+
+    monkeypatch.setattr("fides.api.tasks.storage.get_s3_client", mock_get_s3_client)
+
+    # Create references for all attachments
+    refs = []
+    for attachment in multiple_attachments:
+        ref = AttachmentReference.create(
+            db,
+            data={
+                "attachment_id": attachment.id,
+                "reference_id": comment.id,
+                "reference_type": AttachmentReferenceType.comment,
+            },
+        )
+        refs.append(ref)
+
+    db.refresh(comment)
+
+    # Test the relationships
+    assert len(comment.attachments) == len(multiple_attachments)
+    for attachment in multiple_attachments:
+        assert attachment in comment.attachments
+
+    # Clean up
+    for ref in refs:
+        ref.delete(db)
+    for attachment in multiple_attachments:
+        attachment.delete(db)
diff --git a/tests/fixtures/manual_webhook_fixtures.py b/tests/fixtures/manual_webhook_fixtures.py
index 85f8a6b3ca0..65023ae5700 100644
--- a/tests/fixtures/manual_webhook_fixtures.py
+++ b/tests/fixtures/manual_webhook_fixtures.py
@@ -40,11 +40,13 @@ def access_manual_webhook(db, integration_manual_webhook_config) -> ConnectionCo
                     "pii_field": "email",
                     "dsr_package_label": "email",
                     "data_categories": ["user.contact.email"],
+                    "types": ["string"],
                 },
                 {
                     "pii_field": "Last Name",
                     "dsr_package_label": "last_name",
                     "data_categories": ["user.name"],
+                    "types": ["string"],
                 },
             ],
         },
diff --git a/tests/ops/api/v1/endpoints/test_manual_webhooks.py b/tests/ops/api/v1/endpoints/test_manual_webhooks.py
index 5e027ba2b82..7360f803379 100644
--- a/tests/ops/api/v1/endpoints/test_manual_webhooks.py
+++ b/tests/ops/api/v1/endpoints/test_manual_webhooks.py
@@ -1,3 +1,5 @@
+from typing import Optional
+
 import pytest
 from sqlalchemy.orm import Session
 from starlette.testclient import TestClient
@@ -80,18 +82,23 @@ def test_get_manual_webhook(
 
         resp = response.json()
 
-        assert resp["fields"] == [
+        expected_fields = [
             {
                 "pii_field": "email",
                 "dsr_package_label": "email",
                 "data_categories": ["user.contact.email"],
+                "types": ["string"],
             },
             {
                 "pii_field": "Last Name",
                 "dsr_package_label": "last_name",
                 "data_categories": ["user.name"],
+                "types": ["string"],
             },
         ]
+
+        assert resp["fields"] == expected_fields
+
         connection_config_details = resp["connection_config"]
         assert connection_config_details["key"] == integration_manual_webhook_config.key
         assert connection_config_details["connection_type"] == "manual_webhook"
@@ -100,6 +107,40 @@ def test_get_manual_webhook(
         assert connection_config_details["updated_at"] is not None
         assert connection_config_details["secrets"] is None
 
+    def test_get_manual_webhook_with_multiple_types(
+        self,
+        api_client: TestClient,
+        db,
+        url,
+        generate_auth_header,
+        integration_manual_webhook_config,
+    ):
+        """Test retrieving a manual webhook with multiple types per field"""
+        manual_webhook = AccessManualWebhook.create(
+            db=db,
+            data={
+                "connection_config_id": integration_manual_webhook_config.id,
+                "fields": [
+                    {
+                        "pii_field": "medical_records",
+                        "dsr_package_label": "medical_records",
+                        "data_categories": ["user.medical"],
+                        "types": ["string", "file"],
+                    }
+                ],
+            },
+        )
+
+        auth_header = generate_auth_header([WEBHOOK_READ])
+        response = api_client.get(url, headers=auth_header)
+
+        assert response.status_code == 200
+        resp = response.json()
+
+        assert resp["fields"][0]["types"] == ["string", "file"]
+
+        manual_webhook.delete(db)
+
 
 class TestPostAccessManualWebhook:
     @pytest.fixture(scope="function")
@@ -116,16 +157,19 @@ def payload(self):
                     "pii_field": "First name",
                     "dsr_package_label": None,
                     "data_categories": ["user.name"],
+                    "types": ["string"],
                 },
                 {
                     "pii_field": "Last name",
                     "dsr_package_label": "last_name",
                     "data_categories": ["user.name"],
+                    "types": ["string"],
                 },
                 {
                     "pii_field": "Order number",
                     "dsr_package_label": "order_number",
                     "data_categories": None,
+                    "types": ["string"],
                 },
             ]
         }
@@ -248,11 +292,13 @@ def test_post_access_manual_webhook_dsr_package_labels_empty_string(
                 "pii_field": "first_name",
                 "dsr_package_label": "First Name",
                 "data_categories": ["user.name"],
+                "types": ["string"],
             },
             {
                 "pii_field": "last_name",
                 "dsr_package_label": "last_name",
                 "data_categories": ["user.name"],
+                "types": ["string"],
             },
         ]
 
@@ -265,11 +311,13 @@ def test_post_access_manual_webhook_dsr_package_labels_spaces(
                     "pii_field": "first_name",
                     "dsr_package_label": "First Name",
                     "data_categories": ["user.name"],
+                    "types": ["string"],
                 },
                 {
                     "pii_field": "last_name",
                     "dsr_package_label": "  ",
                     "data_categories": ["user.name"],
+                    "types": ["string"],
                 },
             ]
         }
@@ -281,11 +329,13 @@ def test_post_access_manual_webhook_dsr_package_labels_spaces(
                 "pii_field": "first_name",
                 "dsr_package_label": "First Name",
                 "data_categories": ["user.name"],
+                "types": ["string"],
             },
             {
                 "pii_field": "last_name",
                 "dsr_package_label": "last_name",
                 "data_categories": ["user.name"],
+                "types": ["string"],
             },
         ]
 
@@ -334,16 +384,19 @@ def test_post_manual_webhook(
                 "pii_field": "First name",
                 "dsr_package_label": "first_name",
                 "data_categories": ["user.name"],
+                "types": ["string"],
             },
             {
                 "pii_field": "Last name",
                 "dsr_package_label": "last_name",
                 "data_categories": ["user.name"],
+                "types": ["string"],
             },
             {
                 "pii_field": "Order number",
                 "dsr_package_label": "order_number",
                 "data_categories": None,
+                "types": ["string"],
             },
         ]
         connection_config_details = resp["connection_config"]
@@ -357,6 +410,63 @@ def test_post_manual_webhook(
         manual_webhook = AccessManualWebhook.get(db=db, object_id=resp["id"])
         manual_webhook.delete(db)
 
+    def test_post_manual_webhook_with_multiple_types(
+        self,
+        db: Session,
+        api_client: TestClient,
+        url,
+        generate_auth_header,
+        integration_manual_webhook_config,
+    ):
+        """Test creating a manual webhook with multiple types per field"""
+        auth_header = generate_auth_header([WEBHOOK_CREATE_OR_UPDATE])
+        payload = {
+            "fields": [
+                {
+                    "pii_field": "medical_records",
+                    "dsr_package_label": "medical_records",
+                    "data_categories": ["user.medical"],
+                    "types": ["string", "file"],
+                },
+                {
+                    "pii_field": "patient_notes",
+                    "dsr_package_label": "patient_notes",
+                    "data_categories": ["user.medical.notes"],
+                    "types": ["string"],
+                },
+            ]
+        }
+        response = api_client.post(url, headers=auth_header, json=payload)
+        assert response.status_code == 201
+        resp = response.json()
+
+        assert resp["fields"][0]["types"] == ["string", "file"]
+        assert resp["fields"][1]["types"] == ["string"]
+
+    def test_post_manual_webhook_invalid_types(
+        self,
+        db: Session,
+        api_client: TestClient,
+        url,
+        generate_auth_header,
+    ):
+        """Test validation of field types"""
+        auth_header = generate_auth_header([WEBHOOK_CREATE_OR_UPDATE])
+        payload = {
+            "fields": [
+                {
+                    "pii_field": "test_field",
+                    "dsr_package_label": "test_field",
+                    "types": ["invalid_type"],
+                }
+            ]
+        }
+        response = api_client.post(url, headers=auth_header, json=payload)
+        assert response.status_code == 422
+        assert (
+            "Input should be 'string' or 'file'" in response.json()["detail"][0]["msg"]
+        )
+
 
 class TestPatchAccessManualWebhook:
     @pytest.fixture(scope="function")
@@ -387,6 +497,7 @@ def test_patch_manual_webhook_does_not_exist(
                     "pii_field": "New Field",
                     "dsr_package_label": None,
                     "data_categories": None,
+                    "types": ["string"],
                 },
             ]
         }
@@ -418,6 +529,7 @@ def test_patch_manual_webhook(
                         "user.contact.address.city",
                         "user.contact.address.state",
                     ],
+                    "types": ["string", "file"],
                 },
             ]
         }
@@ -436,6 +548,7 @@ def test_patch_manual_webhook(
                     "user.contact.address.city",
                     "user.contact.address.state",
                 ],
+                "types": ["string", "file"],
             },
         ]
         connection_config_details = resp["connection_config"]
@@ -446,6 +559,62 @@ def test_patch_manual_webhook(
         assert connection_config_details["updated_at"] is not None
         assert connection_config_details["secrets"] is None
 
+    def test_patch_manual_webhook_with_multiple_types(
+        self,
+        api_client: TestClient,
+        db,
+        url,
+        generate_auth_header,
+        access_manual_webhook,
+        integration_manual_webhook_config,
+    ):
+        """Test updating a manual webhook with multiple types per field"""
+        auth_header = generate_auth_header([WEBHOOK_CREATE_OR_UPDATE])
+        payload = {
+            "fields": [
+                {
+                    "pii_field": "medical_records",
+                    "dsr_package_label": "medical_records",
+                    "data_categories": ["user.medical"],
+                    "types": ["string", "file"],
+                }
+            ]
+        }
+
+        response = api_client.patch(url, headers=auth_header, json=payload)
+        assert response.status_code == 200
+
+        resp = response.json()
+        assert resp["fields"][0]["types"] == ["string", "file"]
+
+    def test_patch_manual_webhook_backward_compatibility(
+        self,
+        api_client: TestClient,
+        db,
+        url,
+        generate_auth_header,
+        access_manual_webhook,
+        integration_manual_webhook_config,
+    ):
+        """Test that old-style fields without types still work"""
+        auth_header = generate_auth_header([WEBHOOK_CREATE_OR_UPDATE])
+        payload = {
+            "fields": [
+                {
+                    "pii_field": "email",
+                    "dsr_package_label": "email",
+                    "data_categories": ["user.contact.email"],
+                    "types": ["string"],
+                }
+            ]
+        }
+
+        response = api_client.patch(url, headers=auth_header, json=payload)
+        assert response.status_code == 200
+
+        resp = response.json()
+        assert resp["fields"][0]["types"] == ["string"]  # Default type
+
 
 class TestDeleteAccessManualWebhook:
     @pytest.fixture(scope="function")
@@ -554,13 +723,16 @@ def test_get_manual_webhooks(
                 "pii_field": "email",
                 "dsr_package_label": "email",
                 "data_categories": ["user.contact.email"],
+                "types": ["string"],
             },
             {
                 "pii_field": "Last Name",
                 "dsr_package_label": "last_name",
                 "data_categories": ["user.name"],
+                "types": ["string"],
             },
         ]
+
         connection_config_details = resp["connection_config"]
         assert connection_config_details["key"] == integration_manual_webhook_config.key
         assert connection_config_details["connection_type"] == "manual_webhook"
@@ -636,3 +808,274 @@ def test_connection_test_manual_webhook(
         response = api_client.get(url, headers=auth_header)
         assert 200 == response.status_code
         assert response.json()["test_status"] == "succeeded"
+
+    def test_manual_webhook_backwards_compatibility(
+        self,
+        db: Session,
+        api_client: TestClient,
+        url,
+        generate_auth_header,
+        integration_manual_webhook_config,
+    ):
+        """Test that old-style fields without types still work"""
+        # Create a webhook with old-style fields (no types)
+        manual_webhook = AccessManualWebhook.create(
+            db=db,
+            data={
+                "connection_config_id": integration_manual_webhook_config.id,
+                "fields": [
+                    {
+                        "pii_field": "email",
+                        "dsr_package_label": "email",
+                        "data_categories": ["user.contact.email"],
+                    }
+                ],
+            },
+        )
+
+        auth_header = generate_auth_header([CONNECTION_READ])
+        response = api_client.get(url, headers=auth_header)
+
+        assert response.status_code == 200
+        resp = response.json()
+
+        assert resp["test_status"] == "succeeded"
+
+        # Clean up
+        manual_webhook.delete(db)
+
+
+class TestManualWebhookBackwardsCompatibility:
+    def test_field_definitions_backwards_compatibility(
+        self,
+        db: Session,
+        integration_manual_webhook_config,
+    ):
+        """Test that field definitions work with both old and new style fields"""
+        # Create a webhook with old-style fields (no types)
+        manual_webhook = AccessManualWebhook.create(
+            db=db,
+            data={
+                "connection_config_id": integration_manual_webhook_config.id,
+                "fields": [
+                    {
+                        "pii_field": "email",
+                        "dsr_package_label": "email",
+                        "data_categories": ["user.contact.email"],
+                    },
+                    {
+                        "pii_field": "document",
+                        "dsr_package_label": "document",
+                        "data_categories": ["user.documents"],
+                        "types": ["file"],  # New style field
+                    },
+                ],
+            },
+        )
+
+        try:
+            # Test access field definitions
+            access_fields = manual_webhook.access_field_definitions()
+            assert "email" in access_fields
+            assert "document" in access_fields
+            assert access_fields["email"] == (Optional[str], None)
+            assert access_fields["document"] == (Optional[str], None)
+
+            # Test erasure field definitions
+            erasure_fields = manual_webhook.erasure_field_definitions()
+            assert (
+                "email" in erasure_fields
+            )  # Old field defaults to string, so included
+            assert "document" not in erasure_fields  # File type field not included
+            assert erasure_fields["email"] == (Optional[bool], None)
+
+            # Test that the schema generation works
+            schema = manual_webhook.fields_schema
+            assert "email" in schema.schema()["properties"]
+            assert "document" in schema.schema()["properties"]
+
+            erasure_schema = manual_webhook.erasure_fields_schema
+            assert "email" in erasure_schema.schema()["properties"]
+            assert "document" not in erasure_schema.schema()["properties"]
+
+        finally:
+            manual_webhook.delete(db)
+
+    def test_mixed_field_types_compatibility(
+        self,
+        db: Session,
+        integration_manual_webhook_config,
+    ):
+        """Test that fields can handle mixed types in both old and new formats"""
+        manual_webhook = AccessManualWebhook.create(
+            db=db,
+            data={
+                "connection_config_id": integration_manual_webhook_config.id,
+                "fields": [
+                    {
+                        "pii_field": "name",
+                        "dsr_package_label": "name",
+                        "data_categories": ["user.name"],
+                    },
+                    {
+                        "pii_field": "medical_records",
+                        "dsr_package_label": "medical_records",
+                        "data_categories": ["user.medical"],
+                        "types": ["string", "file"],
+                    },
+                ],
+            },
+        )
+
+        try:
+            # Test that old style field defaults to string
+            access_fields = manual_webhook.access_field_definitions()
+            assert "name" in access_fields
+            assert "medical_records" in access_fields
+
+            # Test that both fields appear in erasure fields since they both support string
+            erasure_fields = manual_webhook.erasure_field_definitions()
+            assert "name" in erasure_fields
+            assert "medical_records" in erasure_fields
+
+            # Verify schema generation works for mixed fields
+            schema = manual_webhook.fields_schema
+            properties = schema.schema()["properties"]
+            assert "name" in properties
+            assert "medical_records" in properties
+
+        finally:
+            manual_webhook.delete(db)
+
+    def test_schema_validation_backwards_compatibility(
+        self,
+        db: Session,
+        integration_manual_webhook_config,
+    ):
+        """Test that schema validation works correctly with mixed old and new style fields"""
+        manual_webhook = AccessManualWebhook.create(
+            db=db,
+            data={
+                "connection_config_id": integration_manual_webhook_config.id,
+                "fields": [
+                    {
+                        "pii_field": "email",
+                        "dsr_package_label": "email",
+                        "data_categories": ["user.contact.email"],
+                    },  # old style
+                    {
+                        "pii_field": "document",
+                        "dsr_package_label": "document",
+                        "data_categories": ["user.documents"],
+                        "types": ["file"],
+                    },  # new style
+                ],
+            },
+        )
+
+        try:
+            # Test validation with old style field
+            schema = manual_webhook.fields_schema
+            data = {"email": "test@example.com"}
+            validated = schema.model_validate(data)
+            assert validated.email == "test@example.com"
+
+            # Test validation with new style field
+            data = {"document": "file_content"}
+            validated = schema.model_validate(data)
+            assert validated.document == "file_content"
+
+            # Test validation with both fields
+            data = {"email": "test@example.com", "document": "file_content"}
+            validated = schema.model_validate(data)
+            assert validated.email == "test@example.com"
+            assert validated.document == "file_content"
+
+        finally:
+            manual_webhook.delete(db)
+
+    def test_non_strict_schema_backwards_compatibility(
+        self,
+        db: Session,
+        integration_manual_webhook_config,
+    ):
+        """Test that non-strict schemas handle both old and new style fields"""
+        manual_webhook = AccessManualWebhook.create(
+            db=db,
+            data={
+                "connection_config_id": integration_manual_webhook_config.id,
+                "fields": [
+                    {
+                        "pii_field": "email",
+                        "dsr_package_label": "email",
+                    },  # minimal old style
+                    {
+                        "pii_field": "document",
+                        "dsr_package_label": "document",
+                        "types": ["file"],
+                    },  # minimal new style
+                ],
+            },
+        )
+
+        try:
+            # Test non-strict schema with extra fields
+            schema = manual_webhook.fields_non_strict_schema
+            data = {
+                "email": "test@example.com",
+                "document": "file_content",
+                "extra_field": "should be ignored",
+            }
+            validated = schema.model_validate(data)
+            assert validated.email == "test@example.com"
+            assert validated.document == "file_content"
+            assert not hasattr(validated, "extra_field")
+
+            # Test non-strict erasure schema
+            schema = manual_webhook.erasure_fields_non_strict_schema
+            data = {
+                "email": True,  # Only email should be included since it defaults to string
+                "document": True,  # Should be ignored since it's a file type
+                "extra_field": True,
+            }
+            validated = schema.model_validate(data)
+            assert validated.email is True
+            assert not hasattr(validated, "document")
+            assert not hasattr(validated, "extra_field")
+
+        finally:
+            manual_webhook.delete(db)
+
+    def test_empty_fields_dict_backwards_compatibility(
+        self,
+        db: Session,
+        integration_manual_webhook_config,
+    ):
+        """Test that empty_fields_dict works correctly with both old and new style fields"""
+        manual_webhook = AccessManualWebhook.create(
+            db=db,
+            data={
+                "connection_config_id": integration_manual_webhook_config.id,
+                "fields": [
+                    {
+                        "pii_field": "email",
+                        "dsr_package_label": "email",
+                    },  # old style
+                    {
+                        "pii_field": "document",
+                        "dsr_package_label": "document",
+                        "types": ["file"],
+                    },  # new style
+                ],
+            },
+        )
+
+        try:
+            empty_dict = manual_webhook.empty_fields_dict
+            assert "email" in empty_dict
+            assert empty_dict["email"] is None
+            assert "document" in empty_dict
+            assert empty_dict["document"] is None
+
+        finally:
+            manual_webhook.delete(db)
diff --git a/tests/ops/models/test_manual_webhook.py b/tests/ops/models/test_manual_webhook.py
index 6b2f94b306a..039b5c495cb 100644
--- a/tests/ops/models/test_manual_webhook.py
+++ b/tests/ops/models/test_manual_webhook.py
@@ -1,5 +1,7 @@
 import pytest
 
+from fides.api.models.attachment import Attachment, AttachmentReference
+from fides.api.models.comment import Comment, CommentReference
 from fides.api.models.manual_webhook import AccessManualWebhook
 from fides.api.schemas.policy import ActionType
 
@@ -69,3 +71,116 @@ def test_get_enabled_webhooks_connection_config_fields_is_empty(
         access_manual_webhook.fields = []
         access_manual_webhook.save(db)
         assert AccessManualWebhook.get_enabled(db) == []
+
+    def test_get_comment_by_id(self, db, access_manual_webhook, comment):
+        """Test retrieving a comment associated with a manual webhook."""
+        # Create a comment reference
+        comment_ref = CommentReference(
+            comment_id=comment.id,
+            reference_id=access_manual_webhook.id,
+            reference_type="manual_step",
+        )
+        db.add(comment_ref)
+        db.commit()
+
+        # Test getting the comment
+        retrieved_comment = access_manual_webhook.get_comment_by_id(db, comment.id)
+        assert retrieved_comment == comment
+
+        # Test getting non-existent comment
+        non_existent = access_manual_webhook.get_comment_by_id(db, "fake_id")
+        assert non_existent is None
+
+    def test_get_attachment_by_id(self, db, access_manual_webhook, attachment):
+        """Test retrieving an attachment associated with a manual webhook."""
+        # Create an attachment reference
+        attachment_ref = AttachmentReference(
+            attachment_id=attachment.id,
+            reference_id=access_manual_webhook.id,
+            reference_type="manual_step",
+        )
+        db.add(attachment_ref)
+        db.commit()
+
+        # Test getting the attachment
+        retrieved_attachment = access_manual_webhook.get_attachment_by_id(
+            db, attachment.id
+        )
+        assert retrieved_attachment == attachment
+
+        # Test getting non-existent attachment
+        non_existent = access_manual_webhook.get_attachment_by_id(db, "fake_id")
+        assert non_existent is None
+
+    def test_delete_attachment_by_id(self, db, access_manual_webhook, attachment):
+        """Test deleting an attachment associated with a manual webhook."""
+        # Create an attachment reference
+        attachment_ref = AttachmentReference(
+            attachment_id=attachment.id,
+            reference_id=access_manual_webhook.id,
+            reference_type="manual_step",
+        )
+        db.add(attachment_ref)
+        db.commit()
+
+        # Delete the attachment
+        access_manual_webhook.delete_attachment_by_id(db, attachment.id)
+
+        # Verify attachment is deleted
+        assert access_manual_webhook.get_attachment_by_id(db, attachment.id) is None
+
+    def test_relationships(self, db, access_manual_webhook, comment, attachment):
+        """Test the relationship properties of manual webhook."""
+        # Create references
+        comment_ref = CommentReference(
+            comment_id=comment.id,
+            reference_id=access_manual_webhook.id,
+            reference_type="manual_step",
+        )
+        attachment_ref = AttachmentReference(
+            attachment_id=attachment.id,
+            reference_id=access_manual_webhook.id,
+            reference_type="manual_step",
+        )
+        db.add_all([comment_ref, attachment_ref])
+        db.commit()
+
+        # Test relationship properties
+        assert comment in access_manual_webhook.comments
+        assert attachment in access_manual_webhook.attachments
+
+        # Test ordering by created_at
+        assert access_manual_webhook.comments == sorted(
+            access_manual_webhook.comments, key=lambda x: x.created_at
+        )
+        assert access_manual_webhook.attachments == sorted(
+            access_manual_webhook.attachments, key=lambda x: x.created_at
+        )
+
+    def test_manual_webhook_with_multiple_types(
+        self, integration_manual_webhook_config, db
+    ):
+        """Test creating a manual webhook with multiple types per field"""
+        manual_webhook = AccessManualWebhook.create(
+            db=db,
+            data={
+                "connection_config_id": integration_manual_webhook_config.id,
+                "fields": [
+                    {
+                        "pii_field": "medical_records",
+                        "dsr_package_label": "medical_records",
+                        "data_categories": ["user.medical"],
+                        "types": ["string", "file"],
+                    },
+                    {
+                        "pii_field": "patient_notes",
+                        "dsr_package_label": "patient_notes",
+                        "data_categories": ["user.medical.notes"],
+                        "types": ["string"],
+                    },
+                ],
+            },
+        )
+        assert len(manual_webhook.fields) == 2
+        assert manual_webhook.fields[0]["types"] == ["string", "file"]
+        assert manual_webhook.fields[1]["types"] == ["string"]


Reviews:
user: login: JadeCara
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T15:16:48Z
user: login: JadeCara
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T15:17:27Z
user: login: JadeCara
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T15:17:48Z
user: login: JadeCara
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T15:18:19Z
user: login: JadeCara
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T15:19:04Z
user: login: JadeCara
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T15:20:28Z
user: login: JadeCara
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T15:56:03Z
user: login: JadeCara
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T16:00:50Z
user: login: JadeCara
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T16:01:52Z
user: login: erosselli
body: LGTM! Left a non-blocking question
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T16:21:37Z
user: login: JadeCara
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T16:31:26Z

Review comments:
diff_hunk: @@ -49,68 +48,57 @@ class AccessManualWebhooks(FidesSchema):
     fields: ManualWebhookFieldsList
     model_config = ConfigDict(from_attributes=True)
 
-    @field_validator("fields")
-    @classmethod
-    def check_for_duplicates(
-        cls, value: List[ManualWebhookField]
-    ) -> List[ManualWebhookField]:
+    @model_validator(mode="after")
+    def check_for_duplicates(self) -> "AccessManualWebhooks":
         """
         Verify that pii_fields and dsr_package_labels are unique.
-
         Set the dsr_package_label to a snake_cased lower case version of pii field if it doesn't exist.
         """
-        unique_pii_fields: Set[str] = {field.pii_field for field in value}
-        if len(value) != len(unique_pii_fields):
+        unique_pii_fields: Set[str] = {field.pii_field for field in self.fields}
+        if len(self.fields) != len(unique_pii_fields):
             raise ValueError("pii_fields must be unique")
 
-        for field in value:
+        for field in self.fields:
             if not field.dsr_package_label:
                 field.dsr_package_label = DSRLabelFieldType(
                     to_snake_case(field.pii_field)
                 )
 
-        unique_dsr_package_labels: Set[Optional[str]] = {
-            field.dsr_package_label for field in value
+        unique_dsr_package_labels: Set[str] = {
+            field.dsr_package_label
+            for field in self.fields
+            if field.dsr_package_label is not None
         }
-        if len(value) != len(unique_dsr_package_labels):
-            # Postponing dsr_package_label uniqueness check in case we get overlaps
-            # above when we fallback to converting pii_fields to dsr_package_labels
+        if len(self.fields) != len(unique_dsr_package_labels):
path: src/fides/api/schemas/manual_webhook_schemas.py
user: login: JadeCara
body: ```suggestion
        if len(value) != len(unique_dsr_package_labels):
            # Postponing dsr_package_label uniqueness check in case we get overlaps
            # above when we fallback to converting pii_fields to dsr_package_labels
```
created_at: 2025-04-09T15:16:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -49,68 +48,57 @@ class AccessManualWebhooks(FidesSchema):
     fields: ManualWebhookFieldsList
     model_config = ConfigDict(from_attributes=True)
 
-    @field_validator("fields")
-    @classmethod
-    def check_for_duplicates(
-        cls, value: List[ManualWebhookField]
-    ) -> List[ManualWebhookField]:
+    @model_validator(mode="after")
+    def check_for_duplicates(self) -> "AccessManualWebhooks":
         """
         Verify that pii_fields and dsr_package_labels are unique.
-
         Set the dsr_package_label to a snake_cased lower case version of pii field if it doesn't exist.
         """
-        unique_pii_fields: Set[str] = {field.pii_field for field in value}
-        if len(value) != len(unique_pii_fields):
+        unique_pii_fields: Set[str] = {field.pii_field for field in self.fields}
+        if len(self.fields) != len(unique_pii_fields):
             raise ValueError("pii_fields must be unique")
 
-        for field in value:
+        for field in self.fields:
             if not field.dsr_package_label:
                 field.dsr_package_label = DSRLabelFieldType(
                     to_snake_case(field.pii_field)
                 )
 
-        unique_dsr_package_labels: Set[Optional[str]] = {
-            field.dsr_package_label for field in value
+        unique_dsr_package_labels: Set[str] = {
+            field.dsr_package_label
+            for field in self.fields
+            if field.dsr_package_label is not None
path: src/fides/api/schemas/manual_webhook_schemas.py
user: login: JadeCara
body: ```suggestion
        unique_dsr_package_labels: Set[Optional[str]] = {
            field.dsr_package_label for field in value
```
created_at: 2025-04-09T15:17:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -49,68 +48,57 @@ class AccessManualWebhooks(FidesSchema):
     fields: ManualWebhookFieldsList
     model_config = ConfigDict(from_attributes=True)
 
-    @field_validator("fields")
-    @classmethod
-    def check_for_duplicates(
-        cls, value: List[ManualWebhookField]
-    ) -> List[ManualWebhookField]:
+    @model_validator(mode="after")
+    def check_for_duplicates(self) -> "AccessManualWebhooks":
         """
         Verify that pii_fields and dsr_package_labels are unique.
-
         Set the dsr_package_label to a snake_cased lower case version of pii field if it doesn't exist.
         """
-        unique_pii_fields: Set[str] = {field.pii_field for field in value}
-        if len(value) != len(unique_pii_fields):
+        unique_pii_fields: Set[str] = {field.pii_field for field in self.fields}
+        if len(self.fields) != len(unique_pii_fields):
             raise ValueError("pii_fields must be unique")
 
-        for field in value:
+        for field in self.fields:
path: src/fides/api/schemas/manual_webhook_schemas.py
user: login: JadeCara
body: ```suggestion
        for field in value:
```
created_at: 2025-04-09T15:17:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -49,68 +48,57 @@ class AccessManualWebhooks(FidesSchema):
     fields: ManualWebhookFieldsList
     model_config = ConfigDict(from_attributes=True)
 
-    @field_validator("fields")
-    @classmethod
-    def check_for_duplicates(
-        cls, value: List[ManualWebhookField]
-    ) -> List[ManualWebhookField]:
+    @model_validator(mode="after")
+    def check_for_duplicates(self) -> "AccessManualWebhooks":
         """
         Verify that pii_fields and dsr_package_labels are unique.
-
         Set the dsr_package_label to a snake_cased lower case version of pii field if it doesn't exist.
         """
-        unique_pii_fields: Set[str] = {field.pii_field for field in value}
-        if len(value) != len(unique_pii_fields):
+        unique_pii_fields: Set[str] = {field.pii_field for field in self.fields}
+        if len(self.fields) != len(unique_pii_fields):
path: src/fides/api/schemas/manual_webhook_schemas.py
user: login: JadeCara
body: ```suggestion
        unique_pii_fields: Set[str] = {field.pii_field for field in value}
        if len(value) != len(unique_pii_fields):
```
created_at: 2025-04-09T15:18:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -49,68 +48,57 @@ class AccessManualWebhooks(FidesSchema):
     fields: ManualWebhookFieldsList
     model_config = ConfigDict(from_attributes=True)
 
-    @field_validator("fields")
-    @classmethod
-    def check_for_duplicates(
-        cls, value: List[ManualWebhookField]
-    ) -> List[ManualWebhookField]:
+    @model_validator(mode="after")
+    def check_for_duplicates(self) -> "AccessManualWebhooks":
path: src/fides/api/schemas/manual_webhook_schemas.py
user: login: JadeCara
body: ```suggestion
    @field_validator("fields")
    @classmethod
    def check_for_duplicates(
        cls, value: List[ManualWebhookField]
    ) -> List[ManualWebhookField]:
```
created_at: 2025-04-09T15:19:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -49,68 +48,57 @@ class AccessManualWebhooks(FidesSchema):
     fields: ManualWebhookFieldsList
     model_config = ConfigDict(from_attributes=True)
 
-    @field_validator("fields")
-    @classmethod
-    def check_for_duplicates(
-        cls, value: List[ManualWebhookField]
-    ) -> List[ManualWebhookField]:
+    @model_validator(mode="after")
+    def check_for_duplicates(self) -> "AccessManualWebhooks":
         """
         Verify that pii_fields and dsr_package_labels are unique.
-
         Set the dsr_package_label to a snake_cased lower case version of pii field if it doesn't exist.
         """
-        unique_pii_fields: Set[str] = {field.pii_field for field in value}
-        if len(value) != len(unique_pii_fields):
+        unique_pii_fields: Set[str] = {field.pii_field for field in self.fields}
+        if len(self.fields) != len(unique_pii_fields):
             raise ValueError("pii_fields must be unique")
 
-        for field in value:
+        for field in self.fields:
             if not field.dsr_package_label:
                 field.dsr_package_label = DSRLabelFieldType(
                     to_snake_case(field.pii_field)
                 )
 
-        unique_dsr_package_labels: Set[Optional[str]] = {
-            field.dsr_package_label for field in value
+        unique_dsr_package_labels: Set[str] = {
+            field.dsr_package_label
+            for field in self.fields
+            if field.dsr_package_label is not None
         }
-        if len(value) != len(unique_dsr_package_labels):
-            # Postponing dsr_package_label uniqueness check in case we get overlaps
-            # above when we fallback to converting pii_fields to dsr_package_labels
+        if len(self.fields) != len(unique_dsr_package_labels):
             raise ValueError("dsr_package_labels must be unique")
 
-        return value
-
-    @field_validator("fields")
-    @classmethod
-    def fields_must_exist(
-        cls, value: List[ManualWebhookField]
-    ) -> List[ManualWebhookField]:
-        """
-        Verify that pii_fields and dsr_package_labels are unique.
-
-        Set the dsr_package_label to a snake_cased lower case version of pii field if it doesn't exist.
-        """
-        unique_pii_fields: Set[str] = {field.pii_field for field in value}
-        if len(value) != len(unique_pii_fields):
-            raise ValueError("pii_fields must be unique")
-
-        for field in value:
-            if not field.dsr_package_label:
-                field.dsr_package_label = DSRLabelFieldType(
-                    to_snake_case(field.pii_field)
-                )
-
-        unique_dsr_package_labels: Set[Optional[str]] = {
-            field.dsr_package_label for field in value
+        return self
+
+    def access_field_definitions(self) -> Dict[str, Any]:
+        """Shared access field definitions for manual webhook schemas"""
+        field_definitions = {}
+        for field in self.fields or []:
+            if field.dsr_package_label:
+                if "file" in field.types:
+                    field_definitions[field.dsr_package_label] = (Optional[str], None)
+                else:
+                    field_definitions[field.dsr_package_label] = (Optional[str], None)
+        return field_definitions
+
+    def erasure_field_definitions(self) -> Dict[str, Any]:
+        """Shared erasure field definitions for manual webhook schemas.
+        Only string fields can be used for erasure confirmation."""
+        return {
+            field.dsr_package_label: (Optional[bool], None)
+            for field in self.fields or []
+            if field.dsr_package_label
+            and "string" in field.types  # only include string fields
path: src/fides/api/schemas/manual_webhook_schemas.py
user: login: JadeCara
body: ```suggestion
        return value

    @field_validator("fields")
    @classmethod
    def fields_must_exist(
        cls, value: List[ManualWebhookField]
    ) -> List[ManualWebhookField]:
        """
        Verify that pii_fields and dsr_package_labels are unique.
        Set the dsr_package_label to a snake_cased lower case version of pii field if it doesn't exist.
        """
        unique_pii_fields: Set[str] = {field.pii_field for field in value}
        if len(value) != len(unique_pii_fields):
            raise ValueError("pii_fields must be unique")

        for field in value:
            if not field.dsr_package_label:
                field.dsr_package_label = DSRLabelFieldType(
                    to_snake_case(field.pii_field)
                )

        unique_dsr_package_labels: Set[Optional[str]] = {
            field.dsr_package_label for field in value
```
created_at: 2025-04-09T15:20:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,21 +33,49 @@ class AccessManualWebhook(Base):
         ConnectionConfig, back_populates="access_manual_webhook", uselist=False
     )
 
+    attachments = relationship(
+        "Attachment",
+        secondary="attachment_reference",
+        primaryjoin="and_(AccessManualWebhook.id == AttachmentReference.reference_id, "
+        "AttachmentReference.reference_type == 'manual_step')",
+        secondaryjoin="Attachment.id == AttachmentReference.attachment_id",
+        order_by="Attachment.created_at",
+        viewonly=True,
+        uselist=True,
+    )
+
+    comments = relationship(
+        "Comment",
+        secondary="comment_reference",
+        primaryjoin="and_(AccessManualWebhook.id == CommentReference.reference_id, "
+        "CommentReference.reference_type == 'manual_step')",
+        secondaryjoin="Comment.id == CommentReference.comment_id",
+        order_by="Comment.created_at",
+        viewonly=True,
+        uselist=True,
+    )
+
     fields = Column(MutableList.as_mutable(JSONB), nullable=False)
 
-    def access_field_definitions(self) -> Dict[str, Any]:
+    def access_field_definitions(self) -> dict[str, Any]:
         """Shared access field definitions for manual webhook schemas"""
-        return {
-            field["dsr_package_label"]: (Optional[str], None)
-            for field in self.fields or []
-        }
-
-    def erasure_field_definitions(self) -> Dict[str, Any]:
+        field_definitions = {}
+        for field in self.fields or []:
+            # Include all fields for access, regardless of type
+            if "dsr_package_label" in field:
+                field_definitions[field["dsr_package_label"]] = (Optional[str], None)
+        return field_definitions
+
+    def erasure_field_definitions(self) -> dict[str, Any]:
         """Shared erasure field definitions for manual webhook schemas"""
-        return {
-            field["dsr_package_label"]: (Optional[bool], None)
-            for field in self.fields or []
-        }
+        field_definitions = {}
+        for field in self.fields or []:
+            # Use types if present, otherwise default to ["string"]
+            field_types = field.get("types", ["string"])
+            # Only include string fields for erasure
+            if "dsr_package_label" in field and "string" in field_types:
+                field_definitions[field["dsr_package_label"]] = (Optional[bool], None)
+        return field_definitions
path: src/fides/api/models/manual_webhook.py
user: login: JadeCara
body: Updated these to verify backwards compatibility. 
created_at: 2025-04-09T15:56:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -90,7 +121,7 @@ def erasure_fields_non_strict_schema(self) -> FidesSchema:
         )
 
     @property
-    def empty_fields_dict(self) -> Dict[str, None]:
+    def empty_fields_dict(self) -> dict[str, None]:
path: src/fides/api/models/manual_webhook.py
user: login: JadeCara
body: Updated `List` and `Dict` to `list` and `dict` since they updated this in 3.9 and later versions [PEP-585](https://peps.python.org/pep-0585/) and it cleans up our imports a little.
created_at: 2025-04-09T16:00:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,8 +20,9 @@ class ManualWebhookField(FidesSchema):
     pii_field: Annotated[
         str, StringConstraints(min_length=1, max_length=200, strip_whitespace=True)
     ]
+    types: list[Literal["string", "file"]] = ["string"]
path: src/fides/api/schemas/manual_webhook_schemas.py
user: login: JadeCara
body: This is the real change here - the rest are mostly [PEP-585](https://peps.python.org/pep-0585/) updates
created_at: 2025-04-09T16:01:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,21 +33,49 @@ class AccessManualWebhook(Base):
         ConnectionConfig, back_populates="access_manual_webhook", uselist=False
     )
 
+    attachments = relationship(
+        "Attachment",
+        secondary="attachment_reference",
+        primaryjoin="and_(AccessManualWebhook.id == AttachmentReference.reference_id, "
+        "AttachmentReference.reference_type == 'manual_step')",
+        secondaryjoin="Attachment.id == AttachmentReference.attachment_id",
+        order_by="Attachment.created_at",
+        viewonly=True,
+        uselist=True,
+    )
+
+    comments = relationship(
+        "Comment",
+        secondary="comment_reference",
+        primaryjoin="and_(AccessManualWebhook.id == CommentReference.reference_id, "
+        "CommentReference.reference_type == 'manual_step')",
+        secondaryjoin="Comment.id == CommentReference.comment_id",
+        order_by="Comment.created_at",
+        viewonly=True,
+        uselist=True,
+    )
+
     fields = Column(MutableList.as_mutable(JSONB), nullable=False)
 
-    def access_field_definitions(self) -> Dict[str, Any]:
+    def access_field_definitions(self) -> dict[str, Any]:
         """Shared access field definitions for manual webhook schemas"""
-        return {
-            field["dsr_package_label"]: (Optional[str], None)
-            for field in self.fields or []
-        }
-
-    def erasure_field_definitions(self) -> Dict[str, Any]:
+        field_definitions = {}
+        for field in self.fields or []:
+            # Include all fields for access, regardless of type
+            if "dsr_package_label" in field:
+                field_definitions[field["dsr_package_label"]] = (Optional[str], None)
+        return field_definitions
+
+    def erasure_field_definitions(self) -> dict[str, Any]:
         """Shared erasure field definitions for manual webhook schemas"""
-        return {
-            field["dsr_package_label"]: (Optional[bool], None)
-            for field in self.fields or []
-        }
+        field_definitions = {}
+        for field in self.fields or []:
+            # Use types if present, otherwise default to ["string"]
+            field_types = field.get("types", ["string"])
+            # Only include string fields for erasure
+            if "dsr_package_label" in field and "string" in field_types:
+                field_definitions[field["dsr_package_label"]] = (Optional[bool], None)
path: src/fides/api/models/manual_webhook.py
user: login: erosselli
body: I don't think I follow here - what is the `field_types` used for? just to check if `"string"` is in it? 
created_at: 2025-04-09T16:19:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,21 +33,49 @@ class AccessManualWebhook(Base):
         ConnectionConfig, back_populates="access_manual_webhook", uselist=False
     )
 
+    attachments = relationship(
+        "Attachment",
+        secondary="attachment_reference",
+        primaryjoin="and_(AccessManualWebhook.id == AttachmentReference.reference_id, "
+        "AttachmentReference.reference_type == 'manual_step')",
+        secondaryjoin="Attachment.id == AttachmentReference.attachment_id",
+        order_by="Attachment.created_at",
+        viewonly=True,
+        uselist=True,
+    )
+
+    comments = relationship(
+        "Comment",
+        secondary="comment_reference",
+        primaryjoin="and_(AccessManualWebhook.id == CommentReference.reference_id, "
+        "CommentReference.reference_type == 'manual_step')",
+        secondaryjoin="Comment.id == CommentReference.comment_id",
+        order_by="Comment.created_at",
+        viewonly=True,
+        uselist=True,
+    )
+
     fields = Column(MutableList.as_mutable(JSONB), nullable=False)
 
-    def access_field_definitions(self) -> Dict[str, Any]:
+    def access_field_definitions(self) -> dict[str, Any]:
         """Shared access field definitions for manual webhook schemas"""
-        return {
-            field["dsr_package_label"]: (Optional[str], None)
-            for field in self.fields or []
-        }
-
-    def erasure_field_definitions(self) -> Dict[str, Any]:
+        field_definitions = {}
+        for field in self.fields or []:
+            # Include all fields for access, regardless of type
+            if "dsr_package_label" in field:
+                field_definitions[field["dsr_package_label"]] = (Optional[str], None)
+        return field_definitions
+
+    def erasure_field_definitions(self) -> dict[str, Any]:
         """Shared erasure field definitions for manual webhook schemas"""
-        return {
-            field["dsr_package_label"]: (Optional[bool], None)
-            for field in self.fields or []
-        }
+        field_definitions = {}
+        for field in self.fields or []:
+            # Use types if present, otherwise default to ["string"]
+            field_types = field.get("types", ["string"])
+            # Only include string fields for erasure
+            if "dsr_package_label" in field and "string" in field_types:
+                field_definitions[field["dsr_package_label"]] = (Optional[bool], None)
path: src/fides/api/models/manual_webhook.py
user: login: JadeCara
body: This is primarily an Erasure vs Access thing:
For access requests, all fields can be returned (strings, files, etc.)
For erasure requests, you can only "erase" string fields - you can't meaningfully "erase" a file (attachment) in the same way. We use the same base for both but only string fields get erased through this mechanism while attachments need to be deleted through their own mechanism.

They also have different kinds of responses. The erasure field type is Optional[bool] because the response indicates whether the field should be erased (`True`) or not (`False`). This is different from access requests where you return the actual data (`Optional[str]`)

This allows explicit backwards compatibility. The .get("types", ["string"]) default ensures old-style fields (without types) are treated as string fields. This maintains compatibility with existing webhooks in the database.

created_at: 2025-04-09T16:31:26Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-04-09T15:10:59Z
body: [vc]: #IGGxDw3tkC/FgT64UcJIbpO2Gg4Ra0buN0T+6FzeFq8=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJmaWRlcy1wbHVzLW5pZ2h0bHkiLCJyb290RGlyZWN0b3J5IjoiY2xpZW50cy9hZG1pbi11aSIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJmaWRlcy1wbHVzLW5pZ2h0bHktZ2l0LWxqLTM5Mi1iZS11cGRhdGUtYWNjZXNzLW1hLWQ0Yzc3OC1ldGh5Y2EudmVyY2VsLmFwcCJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vZXRoeWNhL2ZpZGVzLXBsdXMtbmlnaHRseS81MzdOQlc2ZU5uQmJHalhFaDRWTU02ZGFSbjNWIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwicHJldmlld1VybCI6ImZpZGVzLXBsdXMtbmlnaHRseS1naXQtbGotMzkyLWJlLXVwZGF0ZS1hY2Nlc3MtbWEtZDRjNzc4LWV0aHljYS52ZXJjZWwuYXBwIn0seyJuYW1lIjoiZmlkZXMtcHJpdmFjeS1jZW50ZXIiLCJyb290RGlyZWN0b3J5IjoiY2xpZW50cy9wcml2YWN5LWNlbnRlciIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJmaWRlcy1wcml2YWN5LWNlbnRlci1naXQtbGotMzkyLWJlLXVwZGF0ZS1hY2Nlc3MtNjBmODY4LWV0aHljYS52ZXJjZWwuYXBwIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9ldGh5Y2EvZmlkZXMtcHJpdmFjeS1jZW50ZXIvMktZVEVhS0JzR0tMWk1GdGhZVmtNeGZuMVZQdiIsInByZXZpZXdVcmwiOiJmaWRlcy1wcml2YWN5LWNlbnRlci1naXQtbGotMzkyLWJlLXVwZGF0ZS1hY2Nlc3MtNjBmODY4LWV0aHljYS52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIn1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **fides-plus-nightly** | ✅ Ready ([Inspect](https://vercel.com/ethyca/fides-plus-nightly/537NBW6eNnBbGjXEh4VMM6daRn3V)) | [Visit Preview](https://fides-plus-nightly-git-lj-392-be-update-access-ma-d4c778-ethyca.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/fides-plus-nightly-git-lj-392-be-update-access-ma-d4c778-ethyca.vercel.app?via=pr-comment-feedback-link) | Apr 9, 2025 5:14pm |
| **fides-privacy-center** | ✅ Ready ([Inspect](https://vercel.com/ethyca/fides-privacy-center/2KYTEaKBsGKLZMFthYVkMxfn1VPv)) | [Visit Preview](https://fides-privacy-center-git-lj-392-be-update-access-60f868-ethyca.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/fides-privacy-center-git-lj-392-be-update-access-60f868-ethyca.vercel.app?via=pr-comment-feedback-link) | Apr 9, 2025 5:14pm |



author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-04-09T16:25:23Z
body: ## [Codecov](https://app.codecov.io/gh/ethyca/fides/pull/6022?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethyca) Report
Attention: Patch coverage is `92.50000%` with `3 lines` in your changes missing coverage. Please review.
> Project coverage is 86.86%. Comparing base [(`5daf864`)](https://app.codecov.io/gh/ethyca/fides/commit/5daf864b87f643a8cc971108a7a64ba2bb0a85c1?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethyca) to head [(`f730911`)](https://app.codecov.io/gh/ethyca/fides/commit/f730911fa03edd8493838bf84de0d8db518c5e2a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethyca).
> Report is 1 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/ethyca/fides/pull/6022?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethyca) | Patch % | Lines |
|---|---|---|
| [src/fides/api/models/manual\_webhook.py](https://app.codecov.io/gh/ethyca/fides/pull/6022?src=pr&el=tree&filepath=src%2Ffides%2Fapi%2Fmodels%2Fmanual_webhook.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethyca#diff-c3JjL2ZpZGVzL2FwaS9tb2RlbHMvbWFudWFsX3dlYmhvb2sucHk=) | 94.11% | [0 Missing and 2 partials :warning: ](https://app.codecov.io/gh/ethyca/fides/pull/6022?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethyca) |
| [src/fides/api/schemas/manual\_webhook\_schemas.py](https://app.codecov.io/gh/ethyca/fides/pull/6022?src=pr&el=tree&filepath=src%2Ffides%2Fapi%2Fschemas%2Fmanual_webhook_schemas.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethyca#diff-c3JjL2ZpZGVzL2FwaS9zY2hlbWFzL21hbnVhbF93ZWJob29rX3NjaGVtYXMucHk=) | 80.00% | [1 Missing :warning: ](https://app.codecov.io/gh/ethyca/fides/pull/6022?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethyca) |

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


```diff
@@           Coverage Diff           @@
##             main    #6022   +/-   ##
=======================================
  Coverage   86.86%   86.86%           
=======================================
  Files         418      418           
  Lines       25885    25915   +30     
  Branches     2819     2826    +7     
=======================================
+ Hits        22484    22512   +28     
  Misses       2785     2785           
- Partials      616      618    +2     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/ethyca/fides/pull/6022?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethyca).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ethyca).

<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: cypress[bot]
created_at: 2025-04-09T17:33:54Z
body: 
<h3>
  <a href="https://cloud.cypress.io/projects/bauzeh?utm_source=github&utm_medium=PASSED&utm_campaign=project">fides</a>&nbsp;
  <img src="https://assets.cypress.io/github-pr-comment-icons/chevron.svg" alt="" height="14" width="14" />&nbsp;
  <a href="https://cloud.cypress.io/projects/bauzeh/runs/12795/overview?utm_source=github&utm_medium=PASSED&utm_campaign=run%20number">Run #12795</a>
</h3>

<details>
  <summary>
    <strong>Run Properties:</strong>&nbsp;
    <code><img src="https://assets.cypress.io/github-pr-comment-icons/passed.svg" alt="status check passed" height="10" width="10" />&nbsp;Passed&nbsp;<a href="https://cloud.cypress.io/projects/bauzeh/runs/12795/overview?utm_source=github&utm_medium=PASSED&utm_campaign=run%20number">#12795</a></code> &nbsp;&bull;&nbsp;
    <code><img src="https://assets.cypress.io/github-pr-comment-icons/git-commit.svg" alt="git commit" width="10" height="10" /> <a href="https://github.com/ethyca/fides/commit/4e319374ffda5a6e52622833eb716868c39fbddc">4e319374ff</a>: LJ-392 Add comments and attachments to manual access webhooks (#6022)</code>
  </summary><br />

  <table width="744">
    <tbody>
      <tr />
      <tr height="48">
        <td width="224">
          <strong>Project</strong>
        </td>
        <td width="520">
          <code><a href="https://cloud.cypress.io/projects/bauzeh?utm_source=github&utm_medium=PASSED&utm_campaign=project">fides</a></code>
        </td>
      </tr>
      <tr />
      <tr height="48">
        <td width="224">
          <strong>Branch Review</strong>
        </th>
        <td>
          <code><a href="https://cloud.cypress.io/projects/bauzeh/branches/main/review?utm_source=github&utm_medium=PASSED&utm_campaign=review%20changes&changeRequest=6022">main</a></code>
        </td>
      </tr>
      <tr />
      <tr height="48">
        <td><strong>Run status</strong></td>
        <td>
          <code><img src="https://assets.cypress.io/github-pr-comment-icons/passed.svg" alt="status check passed" height="10" width="10" />&nbsp;Passed&nbsp;<a href="https://cloud.cypress.io/projects/bauzeh/runs/12795/overview?utm_source=github&utm_medium=PASSED&utm_campaign=run%20number">#12795</a></code>
        </td>
      </tr>
      <tr />
      <tr height="48">
        <td><strong>Run duration</strong></td>
        <td><code>00m 54s</code></td>
      </tr>
      <tr />
      <tr height="48">
        <td><strong>Commit</strong></td>
        <td>
          <code><img src="https://assets.cypress.io/github-pr-comment-icons/git-commit.svg" alt="git commit" width="10" height="10" /> <a href="https://github.com/ethyca/fides/commit/4e319374ffda5a6e52622833eb716868c39fbddc">4e319374ff</a>: LJ-392 Add comments and attachments to manual access webhooks (#6022)</code>
        </td>
      </tr>
      <tr />
      <tr height="48">
        <td><strong>Committer</strong></td>
        <td><code>JadeWibbels</code></td>
      </tr>
    </tbody>
    <tfoot>
      <tr />
      <tr height="48">
        <td colspan="2">
          <a href="https://cloud.cypress.io/projects/bauzeh/runs/12795/properties?utm_source=github&utm_medium=PASSED&utm_campaign=properties">View all properties for this run ↗︎</a>
        </td>
      </tr>
    </tfoot>
  </table>
  <hr />
</details>

<table width="744">
  <thead>
    <tr />
    <tr height="48">
      <th colspan="2" align="left" width="744">Test results</th>
    </tr>
  </thead>
  <tbody>
    <tr height="48">
      <td width="224">
        <img src="https://assets.cypress.io/github-pr-comment-icons/failed.svg" alt="Tests that failed" width="10" height="10" />&nbsp;
        <a href="https://cloud.cypress.io/projects/bauzeh/runs/12795/overview?reviewViewBy=FAILED&utm_source=github&utm_medium=PASSED&utm_campaign=failed%20tests">
          <strong>Failures</strong>
        </a>
      </td>
      <td>
        <code><strong>0</strong></code>
      </td>
    </tr>
    <tr />
    <tr height="48">
      <td width="224">
        <img src="https://assets.cypress.io/github-pr-comment-icons/flaky-v3.svg" alt="Tests that were flaky" width="10" height="10" />&nbsp;
        <a href="https://cloud.cypress.io/projects/bauzeh/runs/12795/overview?reviewViewBy=FLAKY&utm_source=github&utm_medium=PASSED&utm_campaign=flaky%20tests">
          <strong>Flaky</strong>
        </a>
      </td>
      <td>
        <code><strong>0</strong></code>
      </td>
    </tr>
    <tr />
    <tr height="48">
      <td width="224">
        <img src="https://assets.cypress.io/github-pr-comment-icons/pending.svg" alt="Tests that did not run due to a developer annotating a test with .skip" width="10" height="10" />&nbsp;
        <a href="https://cloud.cypress.io/projects/bauzeh/runs/12795/test-results?utm_source=github&utm_medium=PASSED&utm_campaign=pending%20tests&statuses=%5B%7B%22value%22%3A%22PENDING%22%2C%22label%22%3A%22PENDING%22%7D%5D">
          <strong>Pending</strong>
        </a>
      </td>
      <td>
        <code><strong>0</strong></code>
      </td>
    </tr>
    <tr />
    <tr height="48">
      <td width="224">
        <img src="https://assets.cypress.io/github-pr-comment-icons/skipped-v3.svg" alt="Tests that did not run due to a failure in a mocha hook" width="10" height="10" />&nbsp;
        <a href="https://cloud.cypress.io/projects/bauzeh/runs/12795/test-results?utm_source=github&utm_medium=PASSED&utm_campaign=skipped%20tests&statuses=%5B%7B%22value%22%3A%22SKIPPED%22%2C%22label%22%3A%22SKIPPED%22%7D%5D">
          <strong>Skipped</strong>
        </a>
      </td>
      <td>
        <code><strong>0</strong></code>
      </td>
    </tr>
    <tr />
    <tr height="48">
      <td width="224">
        <img src="https://assets.cypress.io/github-pr-comment-icons/passed.svg" alt="Tests that passed" width="10" height="10" />&nbsp;
        <a href="https://cloud.cypress.io/projects/bauzeh/runs/12795/overview?utm_source=github&utm_medium=PASSED&utm_campaign=passed%20tests">
          <strong>Passing</strong>
        </a>
      </td>
      <td>
        <code><strong>5</strong></code>
      </td>
    </tr><tfoot>
    <tr height="48"><td colspan="2"><a href="https://cloud.cypress.io/projects/bauzeh/branches/main/review?utm_source=github&utm_medium=PASSED&utm_campaign=review%20changes&changeRequest=6022">View all changes introduced in this branch ↗︎</a></td></tr></tfoot>
  </tbody>
</table>






author_association: NONE