From ed1b9403bdbba53ccaca7f65674ae31c07317984 Mon Sep 17 00:00:00 2001 From: Ali Asadi Date: Mon, 17 Aug 2026 14:33:24 +0330 Subject: [PATCH] feat: add optional details JSON field to conversations Frontend can send arbitrary JSON into `details` on conversation create; it's returned on all conversation responses (create, list, close, reopen), mirroring the existing `title` field. Co-Authored-By: Claude Sonnet 5 --- .../migrations/0006_conversation_details.py | 18 +++++++++++++ apps/chat/models/conversation.py | 1 + apps/chat/serializers/conversations.py | 3 +++ apps/chat/services/conversation.py | 8 ++++-- apps/chat/tests/test_conversations.py | 25 +++++++++++++++++++ apps/chat/views/conversations.py | 2 ++ 6 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 apps/chat/migrations/0006_conversation_details.py diff --git a/apps/chat/migrations/0006_conversation_details.py b/apps/chat/migrations/0006_conversation_details.py new file mode 100644 index 0000000..f3aa0ce --- /dev/null +++ b/apps/chat/migrations/0006_conversation_details.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.4 on 2026-08-17 11:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('chat', '0005_conversation_title'), + ] + + operations = [ + migrations.AddField( + model_name='conversation', + name='details', + field=models.JSONField(blank=True, null=True), + ), + ] diff --git a/apps/chat/models/conversation.py b/apps/chat/models/conversation.py index 6f6ec7d..100cfe8 100644 --- a/apps/chat/models/conversation.py +++ b/apps/chat/models/conversation.py @@ -28,6 +28,7 @@ class Conversation(models.Model): default=ConversationStatus.OPEN, ) title = models.CharField(max_length=255, null=True, blank=True) + details = models.JSONField(null=True, blank=True) # Set only while status is CLOSED; identifies who is allowed to reopen it. closed_by_uuid = models.UUIDField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) diff --git a/apps/chat/serializers/conversations.py b/apps/chat/serializers/conversations.py index 1dd54e1..657611f 100644 --- a/apps/chat/serializers/conversations.py +++ b/apps/chat/serializers/conversations.py @@ -44,6 +44,7 @@ class CreateConversationSerializer(serializers.Serializer): title = serializers.CharField( max_length=255, required=False, allow_null=True, allow_blank=True ) + details = serializers.JSONField(required=False, allow_null=True) class ConversationUserActionSerializer(serializers.Serializer): @@ -61,6 +62,7 @@ class ConversationSerializer(serializers.ModelSerializer): "type", "status", "title", + "details", "closed_by_uuid", "created_at", "participants", @@ -88,6 +90,7 @@ class ConversationListSerializer(serializers.Serializer): type = serializers.CharField() status = serializers.CharField() title = serializers.CharField(allow_null=True) + details = serializers.JSONField(allow_null=True) closed_by_uuid = serializers.UUIDField(allow_null=True) created_at = serializers.DateTimeField() participants = serializers.ListField(child=serializers.UUIDField()) diff --git a/apps/chat/services/conversation.py b/apps/chat/services/conversation.py index 6c142b2..8e6c0d5 100644 --- a/apps/chat/services/conversation.py +++ b/apps/chat/services/conversation.py @@ -22,7 +22,11 @@ class ConversationService: self._mm = mattermost_client or MattermostClient() def create( - self, user_1_uuid: UUID, user_2_uuid: UUID, title: str | None = None + self, + user_1_uuid: UUID, + user_2_uuid: UUID, + title: str | None = None, + details: dict | None = None, ) -> Conversation: if not self._account.validate_user(user_1_uuid): raise ValueError(f"User {user_1_uuid} is not valid") @@ -45,7 +49,7 @@ class ConversationService: with transaction.atomic(): conversation = Conversation.objects.create( - mattermost_channel_id=channel_id, title=title + mattermost_channel_id=channel_id, title=title, details=details ) ConversationParticipant.objects.create( conversation=conversation, user_uuid=user_1_uuid diff --git a/apps/chat/tests/test_conversations.py b/apps/chat/tests/test_conversations.py index 53dfdb1..9fd7d38 100644 --- a/apps/chat/tests/test_conversations.py +++ b/apps/chat/tests/test_conversations.py @@ -57,6 +57,31 @@ def test_create_conversation_without_title_defaults_to_none(): assert Conversation.objects.get(uuid=conv.uuid).title is None +@pytest.mark.django_db +def test_create_conversation_stores_details(): + user_1 = uuid.uuid4() + user_2 = uuid.uuid4() + + svc = ConversationService(mattermost_client=_make_mm_client()) + conv = svc.create(user_1, user_2, details={"source": "mobile_app", "order_id": 123}) + + assert Conversation.objects.get(uuid=conv.uuid).details == { + "source": "mobile_app", + "order_id": 123, + } + + +@pytest.mark.django_db +def test_create_conversation_without_details_defaults_to_none(): + user_1 = uuid.uuid4() + user_2 = uuid.uuid4() + + svc = ConversationService(mattermost_client=_make_mm_client()) + conv = svc.create(user_1, user_2) + + assert Conversation.objects.get(uuid=conv.uuid).details is None + + @pytest.mark.django_db def test_create_conversation_creates_mattermost_mapping(): user_1 = uuid.uuid4() diff --git a/apps/chat/views/conversations.py b/apps/chat/views/conversations.py index a4d8f51..9150f2d 100644 --- a/apps/chat/views/conversations.py +++ b/apps/chat/views/conversations.py @@ -35,6 +35,7 @@ class ConversationCreateView(APIView): serializer.validated_data["user_1_uuid"], serializer.validated_data["user_2_uuid"], title=serializer.validated_data.get("title"), + details=serializer.validated_data.get("details"), ) return Response( @@ -102,6 +103,7 @@ class UserConversationListView(APIView): "type": conv.type, "status": conv.status, "title": conv.title, + "details": conv.details, "closed_by_uuid": conv.closed_by_uuid, "created_at": conv.created_at, "participants": participants,