Models & Storage layer for category feature (Issue #85):
- Add Category dataclass to core/models.py
- id (slug): lowercase alphabetic only
- name: display name
- created_at: Unix timestamp
- deleted_at: soft delete timestamp (None if active)
- Add category_ids field to Bounty dataclass
- list[str] for multiple categories per bounty
- Default empty list for backward compatibility
- Add categories field to RoomData dataclass
- list[Category] for room-level categories
- Default empty list
- Update JsonFileRoomStorage to serialize/deserialize:
- Category fields (id, name, created_at, deleted_at)
- Bounty.category_ids
- RoomData.categories
Backward compatible: existing data without categories works fine.
- Replace admin_user_ids (list[int]) with admin_usernames (list[str])
- Update all service methods to use username for permission checks
- Add delete button to bot responses for message cleanup
- Update tests to match new implementation
Note: Breaking change - existing data files need fresh start
When loading room data with admin_user_ids=null in JSON,
the code now properly initializes admin_user_ids to []
instead of incorrectly creating a new RoomData.
Also removed debug logging added during troubleshooting.
- Add /recover command for listing and recovering soft-deleted bounties
- /recover - list recoverable bounties (admin only)
- /recover <id> [<id>...] - recover specific bounties (admin only)
- Fix /admin list to show @username instead of admin_id
- Add recover_bounty and recover_bounties methods to BountyService
- Add get_deleted_bounty method to BountyService
- Clean up duplicate cmd_admin functions
- Add /recover to bot command menu
- Fixes#49 and #50
Add filtering methods to JsonFileRoomStorage for Phase 2 soft delete support:
- list_bounties(room_id): returns only non-deleted bounties for normal queries
- list_all_bounties(room_id, include_deleted=True): returns all bounties for /recover
Update RoomStorage protocol to include the new methods.
Update mock classes in tests to pass isinstance checks.
Fixes#42
Issue #41: Model updates for Phase 2 features
Bounty model:
- Add deleted_at: int | None - timestamp when deleted (soft-delete)
- Add created_by_username: str | None - username for display purposes
RoomData model:
- Add timezone: str | None - room's timezone (e.g., "Asia/Jakarta")
- Add admin_user_ids: list[int] - list of admin user IDs
Storage adapter updated to handle new fields in load/save operations.
Tests added for new fields.
- Add BountyService for room bounty operations (group and personal)
- Add TrackingService for tracking bounty operations
- Uses RoomStorage and TrackingStorage ports
- PermissionError raised when non-creator edits/deletes
- ValueError raised when bounty not found in tracking
Tests with SimpleRoomStorage and SimpleTrackingStorage (without ensure_*)
show that add_bounty() and track_bounty() work fine without explicit
ensure methods - they create rooms/tracking internally.
This simplifies the Protocol to only essential methods.
- Removed PersonalStorage (redundant - RoomStorage handles both via room_id)
- Added ensure_room() and ensure_tracking() methods for explicit creation
- Added @runtime_checkable to Protocols for isinstance checks
- Added tests/test_ports.py with 11 unit tests for storage protocols
- Bounty.created_by_user_id is now non-optional (always required)
- Removed is_group from RoomData (negative room_id is self-documenting)
- Added room_id to TrackedBounty to track which room bounty was tracked from
- Added clarifying docstrings explaining TrackingData vs TrackedBounty
- Updated tests to match new model structure
- Remove GroupBounty/PersonalBounty subclasses, use Bounty with optional created_by_user_id
- Combine UserData/GroupData into RoomData with room_id and is_group fields
- Add group_id field to TrackingData (supports negative Telegram group IDs)
- Add test_bounty_comparison_not_equal for verifying different bounties are not equal
- Update core/__init__.py exports