diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..2d99708 --- /dev/null +++ b/core/__init__.py @@ -0,0 +1,15 @@ +"""Core domain models for JIGAIDO.""" + +from core.models import ( + Bounty, + TrackedBounty, + RoomData, + TrackingData, +) + +__all__ = [ + "Bounty", + "TrackedBounty", + "RoomData", + "TrackingData", +] diff --git a/core/models.py b/core/models.py new file mode 100644 index 0000000..fe57645 --- /dev/null +++ b/core/models.py @@ -0,0 +1,61 @@ +"""Domain dataclasses for JIGAIDO bounty tracker.""" + +from dataclasses import dataclass + + +@dataclass +class Bounty: + """A bounty created by a user. + + The created_by_user_id field always refers to the user who created the bounty. + It does NOT indicate whether the bounty is a group or personal bounty. + """ + + id: int + text: str | None + link: str | None + due_date_ts: int | None + created_at: int + created_by_user_id: int + + +@dataclass +class TrackedBounty: + """A bounty that a user is tracking. + + Lightweight relation/pointer - the actual tracking context (including room) + lives in TrackingData, not here. + """ + + bounty_id: int + created_at: int + + +@dataclass +class RoomData: + """All data for a room (group or DM). + + The room_id can be negative for Telegram groups or positive for DMs. + The next_id field is used to generate unique bounty IDs within this room. + """ + + room_id: int + bounties: list[Bounty] + next_id: int + + +@dataclass +class TrackingData: + """User tracking state within a room (group or DM). + + TrackingData vs TrackedBounty: + - Use TrackingData to store ALL tracked bounties for a user in a specific room. + It contains the room_id, user_id, and a list of TrackedBounty entries. + - Use TrackedBounty to represent a single tracked bounty entry within that list. + + TrackingData is the container, TrackedBounty is the item. + """ + + room_id: int + user_id: int + tracked: list[TrackedBounty] diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..ca45860 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,143 @@ +"""Tests for core/models.py — domain dataclasses.""" + +import time + +import pytest + +from core.models import ( + Bounty, + TrackedBounty, + RoomData, + TrackingData, +) + + +class TestBounty: + def test_create_bounty(self): + b = Bounty( + id=1, + text="Fix the bug", + link="https://github.com/example/repo/issues/1", + due_date_ts=1735689600, + created_at=1735603200, + created_by_user_id=123, + ) + assert b.id == 1 + assert b.text == "Fix the bug" + assert b.link == "https://github.com/example/repo/issues/1" + assert b.due_date_ts == 1735689600 + assert b.created_at == 1735603200 + assert b.created_by_user_id == 123 + + def test_bounty_optional_fields_can_be_none(self): + b = Bounty( + id=1, + text=None, + link=None, + due_date_ts=None, + created_at=0, + created_by_user_id=123, + ) + assert b.text is None + assert b.link is None + assert b.due_date_ts is None + + def test_bounty_comparison_equal(self): + b1 = Bounty( + id=1, + text="a", + link=None, + due_date_ts=None, + created_at=0, + created_by_user_id=123, + ) + b2 = Bounty( + id=1, + text="a", + link=None, + due_date_ts=None, + created_at=0, + created_by_user_id=123, + ) + assert b1 == b2 + + def test_bounty_comparison_not_equal(self): + b1 = Bounty( + id=1, + text="a", + link=None, + due_date_ts=None, + created_at=0, + created_by_user_id=123, + ) + b2 = Bounty( + id=2, + text="b", + link=None, + due_date_ts=None, + created_at=0, + created_by_user_id=456, + ) + assert b1 != b2 + + +class TestTrackedBounty: + def test_create_tracked_bounty(self): + tb = TrackedBounty(bounty_id=5, created_at=1735600000) + assert tb.bounty_id == 5 + assert tb.created_at == 1735600000 + + def test_tracked_bounty_comparison(self): + tb1 = TrackedBounty(bounty_id=1, created_at=0) + tb2 = TrackedBounty(bounty_id=1, created_at=0) + assert tb1 == tb2 + + +class TestRoomData: + def test_create_group_room_data(self): + rd = RoomData( + room_id=-1001, + bounties=[], + next_id=1, + ) + assert rd.room_id == -1001 + assert rd.bounties == [] + assert rd.next_id == 1 + + def test_create_dm_room_data(self): + rd = RoomData( + room_id=123456, + bounties=[], + next_id=1, + ) + assert rd.room_id == 123456 + assert rd.bounties == [] + assert rd.next_id == 1 + + def test_room_data_with_bounties(self): + b = Bounty( + id=1, + text="Task", + link=None, + due_date_ts=None, + created_at=0, + created_by_user_id=123, + ) + rd = RoomData(room_id=-1001, bounties=[b], next_id=2) + assert len(rd.bounties) == 1 + assert rd.bounties[0].text == "Task" + assert rd.bounties[0].created_by_user_id == 123 + + +class TestTrackingData: + def test_create_tracking_data(self): + td = TrackingData(room_id=-1001, user_id=123456, tracked=[]) + assert td.room_id == -1001 + assert td.user_id == 123456 + assert td.tracked == [] + + def test_tracking_data_with_tracked(self): + tb = TrackedBounty(bounty_id=5, created_at=0) + td = TrackingData(room_id=-1001, user_id=123, tracked=[tb]) + assert len(td.tracked) == 1 + assert td.tracked[0].bounty_id == 5