Merge pull request 'feat(core): implement domain dataclasses for issue #5' (#19) from feat/issue-5-core-models into main

This commit was merged in pull request #19.
This commit is contained in:
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
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]

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