Skip to content
Open
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
33 changes: 33 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
*.pyc
__pycache__
*.pyo
*.pyd
.Python
env/
venv/
ENV/
.env
.venv
pip-log.txt
pip-delete-this-directory.txt
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.log
.git
.gitignore
.mypy_cache
.pytest_cache
.hypothesis
db.sqlite3
*.sqlite3
.DS_Store
.idea/
.vscode/
*.swp
*.swo
*~
29 changes: 29 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Django Configuration
SECRET_KEY=your-super-secret-key-change-this-in-production
DEBUG=True
ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0

# Database Configuration - PostgreSQL (for Docker)
DB_HOST=db
DB_NAME=library_db
DB_USER=library_user
DB_PASSWORD=library_password
DB_PORT=5432

# For local development with SQLite (set to True)
USE_SQLITE=False

# Stripe Configuration
STRIPE_SECRET_KEY=sk_test_your_stripe_secret_key_here
STRIPE_PUBLISHABLE_KEY=pk_test_your_stripe_publishable_key_here

# Telegram Bot Configuration
TELEGRAM_BOT_TOKEN=1234567890:ABCdefGHIjklMNOpqrsTUVwxyz
TELEGRAM_CHAT_ID=-1001234567890

# Redis Configuration (for Django-Q)
REDIS_HOST=redis
REDIS_PORT=6379

# Fine Multiplier (optional - default is 2)
# FINE_MULTIPLIER=2
29 changes: 27 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,27 @@
.idea
venv
# Python
venv/
__pycache__/
*.py[cod]
*$py.class
*.so
.Python

# Django
*.log
db.sqlite3
db.sqlite3-journal
/staticfiles/
/media/

# Environment variables
.env

# IDE
.idea/
.vscode/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db
30 changes: 30 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
FROM python:3.13-slim

# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# Set work directory
WORKDIR /app

# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*

# Install Python dependencies
COPY requirements.txt /app/
RUN pip install --upgrade pip && pip install -r requirements.txt

# Copy project
COPY . /app/

# Create directory for static files
RUN mkdir -p /app/staticfiles

# Collect static files
RUN python manage.py collectstatic --noinput || true

# Run migrations and start server
CMD ["sh", "-c", "python manage.py migrate && python manage.py runserver 0.0.0.0:8000"]
91 changes: 90 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,90 @@
# library-service
# Library Service API

A full-stack Django REST API for managing a library system: book inventory, borrowings, users, payments (Stripe), and Telegram notifications.

## 📋 Project Description

This project modernizes a traditional library by implementing an online management system for book borrowings. The system optimizes library administrators' work and makes the service more user-friendly.

**Problem solved:**
- Manual paper-based tracking of books, borrowings, and payments
- No real-time inventory management
- Cash-only payments
- No automated overdue notifications

**Solution:**
- Web-based REST API for all library operations
- Automated Stripe payment processing
- Real-time Telegram notifications
- JWT-based authentication
- Scheduled daily overdue checks

---

## ✨ Features

### Books Management
- ✅ Full CRUD operations (admin only)
- ✅ Public book listing and search
- ✅ Inventory tracking
- ✅ Cover type (HARD/SOFT) support

### User Management
- ✅ Custom user model with email authentication
- ✅ JWT token-based authentication
- ✅ User registration and profile management
- ✅ Admin/staff role permissions

### Borrowing System
- ✅ Create borrowings with automatic inventory updates
- ✅ Filter by user and active/returned status
- ✅ Return functionality with fine calculation
- ✅ Automatic payment creation on borrowing

### Payment Processing
- ✅ Stripe payment integration
- ✅ Automatic payment session creation
- ✅ Payment success/cancel webhooks
- ✅ Fine calculation for overdue returns
- ✅ Payment status tracking (PENDING/PAID)

### Notifications
- ✅ Telegram bot integration
- ✅ New borrowing notifications
- ✅ Daily overdue check notifications
- ✅ Successful payment notifications

### Background Tasks
- ✅ Django-Q integration for async tasks
- ✅ Scheduled daily overdue checks
- ✅ Redis-backed task queue

---

## 🏗️ Architecture

The system follows a microservices-inspired architecture with the following components:

- **Books Service**: Manage book catalog and inventory
- **Users Service**: Handle authentication and user profiles
- **Borrowings Service**: Manage borrowing operations
- **Payments Service**: Process payments via Stripe
- **Notifications Service**: Send Telegram notifications
- **Background Tasks**: Django-Q cluster for scheduled tasks

All services communicate via REST API endpoints documented in Swagger.

---

## 🚀 Quick Start

### Prerequisites

- Python 3.13+
- PostgreSQL 15+ (or SQLite for development)
- Redis 7+
- Docker & Docker Compose (optional)

### Local Development Setup

1. **Clone the repository**
Empty file added books/__init__.py
Empty file.
12 changes: 12 additions & 0 deletions books/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from django.contrib import admin

from books.models import Book


@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
"""Admin configuration for Book model."""

list_display = ["title", "author", "cover", "inventory", "daily_fee"]
list_filter = ["cover"]
search_fields = ["title", "author"]
6 changes: 6 additions & 0 deletions books/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class BooksConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "books"
41 changes: 41 additions & 0 deletions books/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Generated by Django 4.2.7 on 2025-11-05 11:25

from django.db import migrations, models


class Migration(migrations.Migration):
initial = True

dependencies = []

operations = [
migrations.CreateModel(
name="Book",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("title", models.CharField(max_length=255)),
("author", models.CharField(max_length=255)),
(
"cover",
models.CharField(
choices=[("HARD", "Hard cover"), ("SOFT", "Soft cover")],
default="SOFT",
max_length=4,
),
),
("inventory", models.PositiveIntegerField()),
("daily_fee", models.DecimalField(decimal_places=2, max_digits=6)),
],
options={
"ordering": ["title"],
},
),
]
Empty file added books/migrations/__init__.py
Empty file.
29 changes: 29 additions & 0 deletions books/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from django.db import models


class Book(models.Model):
"""Model representing a book in the library."""

COVER_HARD = "HARD"
COVER_SOFT = "SOFT"

COVER_CHOICES = [
(COVER_HARD, "Hard cover"),
(COVER_SOFT, "Soft cover"),
]

title = models.CharField(max_length=255)
author = models.CharField(max_length=255)
cover = models.CharField(
max_length=4,
choices=COVER_CHOICES,
default=COVER_SOFT
)
inventory = models.PositiveIntegerField()
daily_fee = models.DecimalField(max_digits=6, decimal_places=2)

class Meta:
ordering = ["title"]

def __str__(self):
return f"{self.title} by {self.author}"
12 changes: 12 additions & 0 deletions books/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from rest_framework import serializers

from books.models import Book


class BookSerializer(serializers.ModelSerializer):
"""Serializer for Book model."""

class Meta:
model = Book
fields = ["id", "title", "author", "cover", "inventory", "daily_fee"]
read_only_fields = ["id"]
63 changes: 63 additions & 0 deletions books/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient

from books.models import Book


class BookTests(TestCase):
"""Tests for Book model and API."""

def setUp(self):
self.client = APIClient()
self.user = get_user_model().objects.create_user(
email="user@example.com", password="testpass123"
)
self.admin = get_user_model().objects.create_superuser(
email="admin@example.com", password="adminpass123"
)
self.book_data = {
"title": "Test Book",
"author": "Test Author",
"cover": "SOFT",
"inventory": 10,
"daily_fee": "1.50",
}
self.book = Book.objects.create(**self.book_data)

def test_list_books_unauthenticated(self):
"""Test listing books without authentication."""
url = reverse("books:book-list")
response = self.client.get(url)

self.assertEqual(response.status_code, status.HTTP_200_OK)

def test_create_book_as_admin(self):
"""Test creating book as admin."""
self.client.force_authenticate(user=self.admin)
url = reverse("books:book-list")
response = self.client.post(url, self.book_data)

self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data["title"], self.book_data["title"])

def test_create_book_as_user_forbidden(self):
"""Test regular user cannot create books."""
self.client.force_authenticate(user=self.user)
url = reverse("books:book-list")
response = self.client.post(url, self.book_data)

self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

def test_update_book_as_admin(self):
"""Test updating book as admin."""
self.client.force_authenticate(user=self.admin)
url = reverse("books:book-detail", args=[self.book.id])
updated_data = {"title": "Updated Title"}
response = self.client.patch(url, updated_data)

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.book.refresh_from_db()
self.assertEqual(self.book.title, "Updated Title")
Loading