From 8069ed646501b5768a2cc36145ff582c5d86be50 Mon Sep 17 00:00:00 2001 From: shokollm <270575765+shokollm@users.noreply.github.com> Date: Sat, 4 Apr 2026 06:39:11 +0000 Subject: [PATCH] feat: add multi-ID delete support with per-ID results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add delete_bounties method to BountyService that returns individual results per bounty ID (deleted, not_found, permission_denied) - Update cmd_delete to accept multiple IDs and show per-ID messages - Add tests for delete_bounties Example output: /delete 1 2 3 ✅ Bounty #1 deleted. ✅ Bounty #2 deleted. ⛔ Bounty #3 not found. Fixes #47 --- apps/telegram-bot/commands.py | 34 +++++++++++++------------ core/services.py | 25 +++++++++++++++++++ tests/test_services.py | 47 +++++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 16 deletions(-) diff --git a/apps/telegram-bot/commands.py b/apps/telegram-bot/commands.py index b0ae05e..b26f59a 100644 --- a/apps/telegram-bot/commands.py +++ b/apps/telegram-bot/commands.py @@ -210,32 +210,34 @@ cmd_edit = cmd_update async def cmd_delete(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: args = extract_args(update.message.text) if not args: - await update.message.reply_text("Usage: /delete ") + await update.message.reply_text("Usage: /delete [bounty_id ...]") return try: - bounty_id = int(args[0]) + bounty_ids = [int(arg) for arg in args] except ValueError: - await update.message.reply_text("Invalid bounty ID.") + await update.message.reply_text("Invalid bounty ID(s).") return user_id = get_user_id(update) room_id = get_room_id(update) - try: - success = BOUNTY_SERVICE.delete_bounty( - room_id=room_id, - bounty_id=bounty_id, - user_id=user_id, - ) - except PermissionError as e: - await update.message.reply_text(f"⛔ {e}") - return + results = BOUNTY_SERVICE.delete_bounties( + room_id=room_id, + bounty_ids=bounty_ids, + user_id=user_id, + ) - if success: - await update.message.reply_text(f"✅ Bounty #{bounty_id} deleted.") - else: - await update.message.reply_text("Bounty not found.") + lines = [] + for bounty_id, result in results.items(): + if result == "deleted": + lines.append(f"✅ Bounty #{bounty_id} deleted.") + elif result == "not_found": + lines.append(f"⛔ Bounty #{bounty_id} not found.") + elif result == "permission_denied": + lines.append(f"⛔ Bounty #{bounty_id} - only admins can delete.") + + await update.message.reply_text("\n".join(lines)) async def cmd_track(update: Update, ctx: ContextTypes.DEFAULT_TYPE) -> None: diff --git a/core/services.py b/core/services.py index 2f727b4..3ced3e5 100644 --- a/core/services.py +++ b/core/services.py @@ -210,6 +210,31 @@ class BountyService: self._storage.update_bounty(room_id, bounty) return True + def delete_bounties( + self, room_id: int, bounty_ids: list[int], user_id: int + ) -> dict[int, str]: + """Soft delete multiple bounties. Only admins can delete. + + Returns a dict mapping bounty_id to result: + - "deleted": Successfully soft-deleted + - "not_found": Bounty does not exist + - "permission_denied": User is not admin + """ + results = {} + for bounty_id in bounty_ids: + bounty = self._storage.get_bounty(room_id, bounty_id) + if not bounty: + results[bounty_id] = "not_found" + continue + if not self.is_admin(room_id, user_id): + results[bounty_id] = "permission_denied" + continue + + bounty.deleted_at = int(time.time()) + self._storage.update_bounty(room_id, bounty) + results[bounty_id] = "deleted" + return results + class TrackingService: """Service for tracking bounty operations.""" diff --git a/tests/test_services.py b/tests/test_services.py index 8a92134..bd7c74b 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -210,6 +210,53 @@ class TestBountyService: result = self.service.delete_bounty(-1001, 999, self.admin_user_id) assert result is False + def test_delete_bounties_multiple_success(self): + """Test delete_bounties soft deletes multiple bounties.""" + b1 = self.service.add_bounty( + room_id=-1001, user_id=self.admin_user_id, text="First" + ) + b2 = self.service.add_bounty( + room_id=-1001, user_id=self.admin_user_id, text="Second" + ) + b3 = self.service.add_bounty( + room_id=-1001, user_id=self.admin_user_id, text="Third" + ) + results = self.service.delete_bounties( + -1001, [b1.id, b2.id, b3.id], self.admin_user_id + ) + assert results == {b1.id: "deleted", b2.id: "deleted", b3.id: "deleted"} + assert self.service.get_bounty(-1001, b1.id) is None + assert self.service.get_bounty(-1001, b2.id) is None + assert self.service.get_bounty(-1001, b3.id) is None + + def test_delete_bounties_mixed_results(self): + """Test delete_bounties returns individual results per ID.""" + b1 = self.service.add_bounty( + room_id=-1001, user_id=self.admin_user_id, text="Exists" + ) + results = self.service.delete_bounties( + -1001, [b1.id, 999, 888], self.admin_user_id + ) + assert results == {b1.id: "deleted", 999: "not_found", 888: "not_found"} + + def test_delete_bounties_permission_denied(self): + """Test delete_bounties returns permission_denied for non-admin.""" + b1 = self.service.add_bounty( + room_id=-1001, user_id=self.admin_user_id, text="First" + ) + b2 = self.service.add_bounty( + room_id=-1001, user_id=self.admin_user_id, text="Second" + ) + results = self.service.delete_bounties( + -1001, + [b1.id, b2.id], + 999, # non-admin user + ) + assert results == {b1.id: "permission_denied", b2.id: "permission_denied"} + # Bounties should not be deleted + assert self.service.get_bounty(-1001, b1.id) is not None + assert self.service.get_bounty(-1001, b2.id) is not None + class TestTrackingService: """Unit tests for TrackingService."""