Compare commits

...

2 commits

Author SHA1 Message Date
9f8b723f64 Merge pull request 'feat: add optional details JSON field to conversations' (#12) from feature/conversation-details into master
Reviewed-on: #12
2026-08-17 07:20:21 -04:00
ed1b9403bd 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 <noreply@anthropic.com>
2026-08-17 14:33:24 +03:30
6 changed files with 55 additions and 2 deletions

View file

@ -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),
),
]

View file

@ -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)

View file

@ -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())

View file

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

View file

@ -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()

View file

@ -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,