diff --git a/library/tests/test_borrowing_api_auth_permissions.py b/library/tests/test_borrowing_api_auth_permissions.py index 19787bb..9c6ccfd 100644 --- a/library/tests/test_borrowing_api_auth_permissions.py +++ b/library/tests/test_borrowing_api_auth_permissions.py @@ -19,7 +19,11 @@ def borrowing_detail_url(borrowing_id: int) -> str: def get_results(data): - return data["results"] if isinstance(data, dict) and "results" in data else data # noqa + return ( + data["results"] + if isinstance(data, dict) and "results" in data + else data + ) # noqa class BorrowingAuthPermissionsTests(TestCase): diff --git a/library/tests/test_borrowing_api_create_return.py b/library/tests/test_borrowing_api_create_return.py index f620379..d85e864 100644 --- a/library/tests/test_borrowing_api_create_return.py +++ b/library/tests/test_borrowing_api_create_return.py @@ -46,7 +46,8 @@ def _payload(self, book_id: int): return { "book": book_id, "expected_return_date": ( - timezone.localdate() + timedelta(days=7)).isoformat(), + timezone.localdate() + timedelta(days=7) + ).isoformat(), } def _mock_stripe_session(self, mock_create_session): @@ -63,19 +64,23 @@ def test_borrowing_list_requires_auth(self): self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED) def test_create_borrowing_requires_auth(self): - res = self.client.post(BORROWING_LIST_CREATE, - self._payload(self.book_b.id)) + res = self.client.post( + BORROWING_LIST_CREATE, self._payload(self.book_b.id) + ) self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED) # ---- Create borrowing ---- @patch("library.serializers.create_payment_session") - def test_create_borrowing_decreases_inventory_and_attaches_user(self, mock_create_session): # noqa + def test_create_borrowing_decreases_inventory_and_attaches_user( + self, mock_create_session + ): # noqa self._mock_stripe_session(mock_create_session) self.client.force_authenticate(user=self.user) - res = self.client.post(BORROWING_LIST_CREATE, - self._payload(self.book_b.id)) + res = self.client.post( + BORROWING_LIST_CREATE, self._payload(self.book_b.id) + ) self.assertEqual(res.status_code, status.HTTP_201_CREATED) self.assertIn("id", res.data) @@ -90,16 +95,18 @@ def test_create_borrowing_decreases_inventory_and_attaches_user(self, mock_creat mock_create_session.assert_called() @patch("library.serializers.create_payment_session") - def test_create_borrowing_out_of_stock_returns_400(self, - mock_create_session): + def test_create_borrowing_out_of_stock_returns_400( + self, mock_create_session + ): self._mock_stripe_session(mock_create_session) self.book_b.inventory = 0 self.book_b.save(update_fields=["inventory"]) self.client.force_authenticate(user=self.user) - res = self.client.post(BORROWING_LIST_CREATE, - self._payload(self.book_b.id)) + res = self.client.post( + BORROWING_LIST_CREATE, self._payload(self.book_b.id) + ) self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) self.assertIn("book", res.data) @@ -110,13 +117,15 @@ def test_create_borrowing_out_of_stock_returns_400(self, @patch("library.serializers.create_payment_session") def test_return_borrowing_sets_actual_return_date_and_increases_inventory( - self, mock_create_session): + self, mock_create_session + ): self._mock_stripe_session(mock_create_session) self.client.force_authenticate(user=self.user) - res_create = self.client.post(BORROWING_LIST_CREATE, - self._payload(self.book_a.id)) + res_create = self.client.post( + BORROWING_LIST_CREATE, self._payload(self.book_a.id) + ) self.assertEqual(res_create.status_code, status.HTTP_201_CREATED) self.book_a.refresh_from_db() @@ -139,8 +148,9 @@ def test_return_borrowing_twice_returns_400(self, mock_create_session): self.client.force_authenticate(user=self.user) - res_create = self.client.post(BORROWING_LIST_CREATE, - self._payload(self.book_a.id)) + res_create = self.client.post( + BORROWING_LIST_CREATE, self._payload(self.book_a.id) + ) self.assertEqual(res_create.status_code, status.HTTP_201_CREATED) borrowing_id = res_create.data["id"] @@ -149,5 +159,6 @@ def test_return_borrowing_twice_returns_400(self, mock_create_session): res2 = self.client.post(borrowing_return_url(borrowing_id)) self.assertEqual(res2.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(res2.data["detail"], - "This borrowing is already returned.") + self.assertEqual( + res2.data["detail"], "This borrowing is already returned." + ) diff --git a/library/tests/test_borrowing_api_filters_pagination.py b/library/tests/test_borrowing_api_filters_pagination.py index 945521f..b88ccf0 100644 --- a/library/tests/test_borrowing_api_filters_pagination.py +++ b/library/tests/test_borrowing_api_filters_pagination.py @@ -81,10 +81,10 @@ def test_list_is_paginated(self): # ---- Filters: is_active ---- def test_filter_is_active_true(self): - active = self._create_borrowing(self.user, self.book_a, - returned=False) - returned = self._create_borrowing(self.user, self.book_a, - returned=True) + active = self._create_borrowing(self.user, self.book_a, returned=False) + returned = self._create_borrowing( + self.user, self.book_a, returned=True + ) self.client.force_authenticate(user=self.user) res = self.client.get(BORROWING_LIST, {"is_active": "true"}) @@ -95,10 +95,10 @@ def test_filter_is_active_true(self): self.assertNotIn(returned.id, ids) def test_filter_is_active_false(self): - active = self._create_borrowing(self.user, self.book_a, - returned=False) - returned = self._create_borrowing(self.user, self.book_a, - returned=True) + active = self._create_borrowing(self.user, self.book_a, returned=False) + returned = self._create_borrowing( + self.user, self.book_a, returned=True + ) self.client.force_authenticate(user=self.user) res = self.client.get(BORROWING_LIST, {"is_active": "false"}) diff --git a/notifications/admin.py b/notifications/admin.py deleted file mode 100644 index e69de29..0000000 diff --git a/notifications/models.py b/notifications/models.py deleted file mode 100644 index e69de29..0000000 diff --git a/notifications/tasks.py b/notifications/tasks.py index f63d190..7c1fa95 100644 --- a/notifications/tasks.py +++ b/notifications/tasks.py @@ -1,67 +1,102 @@ +import logging + from celery import shared_task from django.utils.timezone import now +from library.models import Borrowing from payment.models import Payment from .services.telegram_client import send_telegram_message -from library.models import Borrowing + +logger = logging.getLogger(__name__) @shared_task def notify_new_borrowing(borrowing_id): - borrowing = Borrowing.objects.get(id=borrowing_id) - message = ( - f"πŸ“š New Borrowing Created!\n" - f"πŸ‘€ User: {borrowing.user.email}\n" - f"πŸ“– Book: {borrowing.book.title}\n" - f"πŸ“… Borrow Date: {borrowing.borrow_date}\n" - f"πŸ“… Expected Return: {borrowing.expected_return_date}" - ) - send_telegram_message(message) + try: + borrowing = Borrowing.objects.select_related("user", "book").get( + id=borrowing_id + ) + + message = ( + f"πŸ“š New Borrowing Created!\n" + f"πŸ‘€ User: {borrowing.user.email}\n" + f"πŸ“– Book: {borrowing.book.title}\n" + f"πŸ“… Borrow Date: {borrowing.borrow_date}\n" + f"πŸ“… Expected Return: {borrowing.expected_return_date}" + ) + + send_telegram_message(message) + + except Borrowing.DoesNotExist: + logger.warning( + "Borrowing with id=%s does not exist. Skipping notification.", + borrowing_id, + ) + + except Exception as exc: + logger.exception( + "Failed to send telegram notification for borrowing id=%s: %s", + borrowing_id, + exc, + ) @shared_task def notify_overdue_borrowings(): today = now().date() - overdue_borrowings = Borrowing.objects.filter( - expected_return_date__lte=today, actual_return_date__isnull=True - ).select_related("user", "book") + try: + overdue_borrowings = Borrowing.objects.filter( + expected_return_date__lte=today, actual_return_date__isnull=True + ).select_related("user", "book") - messages = [ - ( - "πŸ“š Borrowing overdue!\n" - f"πŸ‘€ User: {borrowing.user.email}\n" - f"πŸ“– Book: {borrowing.book.title}\n" - f"πŸ“… Expected return date: {borrowing.expected_return_date}" - ) - for borrowing in overdue_borrowings - ] + if not overdue_borrowings.exists(): + send_telegram_message("βœ… No borrowings overdue today!") + return - if not messages: - send_telegram_message("βœ… No borrowings overdue today!") - return + for borrowing in overdue_borrowings: + message = ( + "πŸ“š Borrowing overdue!\n" + f"πŸ‘€ User: {borrowing.user.email}\n" + f"πŸ“– Book: {borrowing.book.title}\n" + f"πŸ“… Expected return date: {borrowing.expected_return_date}" + ) + send_telegram_message(message) - for msg in messages: - send_telegram_message(msg) + except Exception as exc: + logger.exception("Failed to process overdue borrowings: %s", exc) @shared_task def notify_payment_success(payment_id): - payment = Payment.objects.get(id=payment_id) - actual_money = payment.money / 100 - message = ( - f"πŸ’³ Payment Successful!\n" - f"πŸ‘€ User: {payment.borrowing.user.email}\n" - f"πŸ“– Book: {payment.borrowing.book.title}\n" - f"πŸ’° Amount: ${actual_money}" - ) - if payment.type == "FN": + try: + payment = Payment.objects.select_related( + "borrowing__user", "borrowing__book" + ).get(id=payment_id) + + actual_money = payment.money / 100 + + message_prefix = "🚩Fine payment🚩\n" if payment.type == "FN" else "" + message = ( - f"🚩Fine payment🚩\n" + f"{message_prefix}" f"πŸ’³ Payment Successful!\n" f"πŸ‘€ User: {payment.borrowing.user.email}\n" f"πŸ“– Book: {payment.borrowing.book.title}\n" f"πŸ’° Amount: ${actual_money}" ) - send_telegram_message(message) + send_telegram_message(message) + + except Payment.DoesNotExist: + logger.warning( + "Payment with id=%s does not exist. Skipping notification.", + payment_id, + ) + + except Exception as exc: + logger.exception( + "Failed to send payment notification for payment id=%s: %s", + payment_id, + exc, + ) diff --git a/notifications/tests/test_notifications.py b/notifications/tests/test_notifications.py index 4895b1d..c16cb4c 100644 --- a/notifications/tests/test_notifications.py +++ b/notifications/tests/test_notifications.py @@ -11,30 +11,22 @@ class BorrowingNotificationAPITests(TestCase): def test_notify_task_called_on_borrowing_create(self): user = get_user_model().objects.create_user( - email="test@example.com", - password="password123" + email="test@example.com", password="password123" ) book = Book.objects.create( - title="Test Book", - author="Author", - inventory=5, - daily_fee=10 + title="Test Book", author="Author", inventory=5, daily_fee=10 ) client = APIClient() client.force_authenticate(user=user) - payload = { - "book": book.id, - "expected_return_date": "2030-01-20" - } + payload = {"book": book.id, "expected_return_date": "2030-01-20"} - with patch("notifications.tasks.notify_new_borrowing.delay") as mocked_task: # noqa - response = client.post( - reverse("library:borrowing-list"), - payload - ) + with patch( + "notifications.tasks.notify_new_borrowing.delay" + ) as mocked_task: + response = client.post(reverse("library:borrowing-list"), payload) self.assertEqual(response.status_code, 201) self.assertEqual(Borrowing.objects.count(), 1) diff --git a/notifications/tests/test_tasks.py b/notifications/tests/test_tasks.py index 17d0293..e16f913 100644 --- a/notifications/tests/test_tasks.py +++ b/notifications/tests/test_tasks.py @@ -10,21 +10,18 @@ class NotifyNewBorrowingTests(TestCase): def test_notify_new_borrowing_calls_telegram_helper(self): user = get_user_model().objects.create_user( - email="user@test.com", - password="password" + email="user@test.com", password="password" ) book = Book.objects.create( title="Clean Code", author="Robert Martin", inventory=3, - daily_fee=1 + daily_fee=1, ) borrowing = Borrowing.objects.create( - user=user, - book=book, - expected_return_date="2030-01-20" + user=user, book=book, expected_return_date="2030-01-20" ) with patch("notifications.tasks.send_telegram_message") as mocked_send: diff --git a/notifications/views.py b/notifications/views.py deleted file mode 100644 index e69de29..0000000 diff --git a/payment/tests/test_payment_api.py b/payment/tests/test_payment_api.py index e02b083..57d17d1 100644 --- a/payment/tests/test_payment_api.py +++ b/payment/tests/test_payment_api.py @@ -38,10 +38,7 @@ def setUp(self): ) self.book = Book.objects.create( - title="Test Book", - author="Author", - inventory=10, - daily_fee=5 + title="Test Book", author="Author", inventory=10, daily_fee=5 ) borrow_date = date.today() @@ -107,10 +104,7 @@ def setUp(self): ) self.book = Book.objects.create( - title="Test Book", - author="Author", - inventory=10, - daily_fee=5 + title="Test Book", author="Author", inventory=10, daily_fee=5 ) self.days = 6 @@ -129,19 +123,21 @@ def setUp(self): book=self.book, borrow_date=date.today(), expected_return_date=date.today() + timedelta(days=5), - actual_return_date=date.today() + timedelta(days=10) + actual_return_date=date.today() + timedelta(days=10), ) def test_correct_total_price(self): session = create_payment_session(self.borrowing) price = session.total_price / 100 - calculate_total = (self.days * self.book.daily_fee) + calculate_total = self.days * self.book.daily_fee self.assertEqual(price, calculate_total) def test_correct_fee_price(self): session = create_payment_session(self.exp_borrowing) price = session.total_price / 100 days = ( - self.exp_borrowing.actual_return_date - self.exp_borrowing.expected_return_date).days # noqa + self.exp_borrowing.actual_return_date + - self.exp_borrowing.expected_return_date + ).days # noqa calculate_total = days * self.book.daily_fee self.assertEqual(price, calculate_total) diff --git a/pyproject.toml b/pyproject.toml index 68f7d83..ed39cb3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,5 @@ exclude = ''' migrations | venv | .venv - | tests )/ ''' diff --git a/users/tests/test_models.py b/users/tests/test_models.py index 028eb2d..e8923a9 100644 --- a/users/tests/test_models.py +++ b/users/tests/test_models.py @@ -8,8 +8,7 @@ def test_create_user_with_email_successful(self): email = "test@example.com" password = "password123" user = get_user_model().objects.create_user( - email=email, - password=password + email=email, password=password ) self.assertEqual(user.email, email) diff --git a/users/tests/test_serializers.py b/users/tests/test_serializers.py index 70fc158..426ca2c 100644 --- a/users/tests/test_serializers.py +++ b/users/tests/test_serializers.py @@ -5,7 +5,7 @@ class SerializerTests(TestCase): def test_user_serializer(self): - """Test user serializer validation """ + """Test user serializer validation""" payload = { "email": "test@example.com", "password": "VeryLongPassword", @@ -15,11 +15,11 @@ def test_user_serializer(self): self.assertTrue(serializer.is_valid()) def test_user_serializer_validation(self): - """Test user serializer validation """ + """Test user serializer validation""" payload = { "email": "test@example.com", "password": "short", - "first_name": "John" + "first_name": "John", } serializer = UserSerializer(data=payload) diff --git a/users/tests/test_views.py b/users/tests/test_views.py index 5f18e2e..1a3a47e 100644 --- a/users/tests/test_views.py +++ b/users/tests/test_views.py @@ -8,8 +8,7 @@ class ViewTests(APITestCase): def setUp(self): self.url = reverse("users:manage") self.user = get_user_model().objects.create_user( - "test@ex.com", - "pass12345" + "test@ex.com", "pass12345" ) def test_retrieve_user_unauthorized(self):