Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion library/tests/test_borrowing_api_auth_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
45 changes: 28 additions & 17 deletions library/tests/test_borrowing_api_create_return.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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()
Expand All @@ -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"]

Expand All @@ -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."
)
16 changes: 8 additions & 8 deletions library/tests/test_borrowing_api_filters_pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"})
Expand All @@ -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"})
Expand Down
Empty file removed notifications/admin.py
Empty file.
Empty file removed notifications/models.py
Empty file.
111 changes: 73 additions & 38 deletions notifications/tasks.py
Original file line number Diff line number Diff line change
@@ -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,
)
22 changes: 7 additions & 15 deletions notifications/tests/test_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 3 additions & 6 deletions notifications/tests/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Empty file removed notifications/views.py
Empty file.
18 changes: 7 additions & 11 deletions payment/tests/test_payment_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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

Expand All @@ -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)
Loading