feat(core): implement domain dataclasses for issue #5 #19

Merged
shoko merged 4 commits from feat/issue-5-core-models into main 2026-04-03 00:37:30 +02:00
3 changed files with 219 additions and 0 deletions

15
core/__init__.py Normal file
View File

@@ -0,0 +1,15 @@
"""Core domain models for JIGAIDO."""
from core.models import (
Bounty,
TrackedBounty,
RoomData,
TrackingData,
)
__all__ = [
"Bounty",
"TrackedBounty",
"RoomData",
"TrackingData",
]

61
core/models.py Normal file
View File

@@ -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
han marked this conversation as resolved
Review

can we simplify the Bounty by not having both GroupBounty and PersonalBounty? can we utilize Bounty by adding optional created_by_user_id so in this case we can use Bounty for Group and Personal Bounty. what do you think?

can we simplify the Bounty by not having both GroupBounty and PersonalBounty? can we utilize Bounty by adding optional created_by_user_id so in this case we can use Bounty for Group and Personal Bounty. what do you think?
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.
"""
han marked this conversation as resolved
Review

since room_id can be negative in RoomData, I don't think we need is_group anymore. if its negative, its group, that should be clear enough

since room_id can be negative in RoomData, I don't think we need is_group anymore. if its negative, its group, that should be clear enough
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]

143
tests/test_models.py Normal file
View File

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